From c2bb06a592b056365bc5a703683898b841aaef94 Mon Sep 17 00:00:00 2001 From: Harald Leithner Date: Wed, 27 Apr 2022 18:32:52 +0200 Subject: [PATCH 01/21] Replace FinderHelperRoute import with namespaced version --- administrator/components/com_finder/src/Indexer/Query.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/administrator/components/com_finder/src/Indexer/Query.php b/administrator/components/com_finder/src/Indexer/Query.php index 50e97d2a4df04..e482bbc55f2a4 100644 --- a/administrator/components/com_finder/src/Indexer/Query.php +++ b/administrator/components/com_finder/src/Indexer/Query.php @@ -17,6 +17,7 @@ use Joomla\CMS\Language\Text; use Joomla\CMS\Uri\Uri; use Joomla\Component\Finder\Administrator\Helper\LanguageHelper; +use Joomla\Component\Finder\Site\Helper\RouteHelper; use Joomla\Database\DatabaseAwareTrait; use Joomla\Database\DatabaseInterface; use Joomla\Database\ParameterType; @@ -24,8 +25,6 @@ use Joomla\String\StringHelper; use Joomla\Utilities\ArrayHelper; -\JLoader::register('FinderHelperRoute', JPATH_SITE . '/components/com_finder/helpers/route.php'); - /** * Query class for the Finder indexer package. * @@ -385,7 +384,7 @@ public function toUri($base = '') 'q' => $uri->getVar('q'), ); - $item = \FinderHelperRoute::getItemid($query); + $item = RouteHelper::getItemid($query); // Add the menu item id if present. if ($item !== null) From cc5e55e982cd553028b742428491e64fd3f088d8 Mon Sep 17 00:00:00 2001 From: Harald Leithner Date: Wed, 27 Apr 2022 18:43:42 +0200 Subject: [PATCH 02/21] CS on switch statements --- administrator/components/com_finder/src/Indexer/Query.php | 4 ---- administrator/components/com_users/src/Model/UsersModel.php | 2 ++ libraries/src/Form/FormField.php | 2 ++ plugins/authentication/ldap/ldap.php | 4 ---- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/administrator/components/com_finder/src/Indexer/Query.php b/administrator/components/com_finder/src/Indexer/Query.php index e482bbc55f2a4..262901fb2df8b 100644 --- a/administrator/components/com_finder/src/Indexer/Query.php +++ b/administrator/components/com_finder/src/Indexer/Query.php @@ -844,7 +844,6 @@ protected function processString($input, $lang, $mode) // Handle a before and after date filters. case 'before': case 'after': - { // Get the time offset. $offset = Factory::getApplication()->get('offset'); @@ -869,11 +868,9 @@ protected function processString($input, $lang, $mode) } break; - } // Handle a taxonomy branch filter. default: - { // Try to find the node id. $return = Taxonomy::getNodeByTitle($modifier, $value); @@ -895,7 +892,6 @@ protected function processString($input, $lang, $mode) } break; - } } // Clean up the input string again. diff --git a/administrator/components/com_users/src/Model/UsersModel.php b/administrator/components/com_users/src/Model/UsersModel.php index 7ec7fd620a985..67e169d15d85c 100644 --- a/administrator/components/com_users/src/Model/UsersModel.php +++ b/administrator/components/com_users/src/Model/UsersModel.php @@ -611,6 +611,8 @@ private function buildDateRange($range) case 'post_year': $dNow = false; + // no break + case 'past_year': $dStart->modify('-1 year'); break; diff --git a/libraries/src/Form/FormField.php b/libraries/src/Form/FormField.php index 7252803deb7f0..2f985e369ca8d 100644 --- a/libraries/src/Form/FormField.php +++ b/libraries/src/Form/FormField.php @@ -515,6 +515,7 @@ public function __set($name, $value) case 'class': // Removes spaces from left & right and extra spaces from middle $value = preg_replace('/\s+/', ' ', trim((string) $value)); + // no break case 'description': case 'hint': @@ -551,6 +552,7 @@ public function __set($name, $value) // Allow for field classes to force the multiple values option. $value = (string) $value; $value = $value === '' && isset($this->forceMultiple) ? (string) $this->forceMultiple : $value; + // no break case 'required': case 'disabled': diff --git a/plugins/authentication/ldap/ldap.php b/plugins/authentication/ldap/ldap.php index 95a30e2cf7a63..de01d4281e0c9 100644 --- a/plugins/authentication/ldap/ldap.php +++ b/plugins/authentication/ldap/ldap.php @@ -78,7 +78,6 @@ public function onUserAuthenticate($credentials, $options, &$response) switch ($auth_method) { case 'search': - { try { $dn = str_replace('[username]', $this->params->get('username', ''), $this->params->get('users_dn', '')); @@ -135,10 +134,8 @@ public function onUserAuthenticate($credentials, $options, &$response) } break; - } case 'bind': - { // We just accept the result here try { @@ -172,7 +169,6 @@ public function onUserAuthenticate($credentials, $options, &$response) } break; - } default: // Unsupported configuration From a9b35cce0da1297d6f27c4f959d229a3a1b15145 Mon Sep 17 00:00:00 2001 From: Harald Leithner Date: Wed, 27 Apr 2022 19:09:15 +0200 Subject: [PATCH 03/21] Remove unnecessary require not used since 2010 --- libraries/src/Form/Field/MenuField.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/libraries/src/Form/Field/MenuField.php b/libraries/src/Form/Field/MenuField.php index a61e55dbaabb5..cd9c551d1a687 100644 --- a/libraries/src/Form/Field/MenuField.php +++ b/libraries/src/Form/Field/MenuField.php @@ -14,9 +14,6 @@ use Joomla\CMS\Language\Text; use Joomla\Database\ParameterType; -// Import the com_menus helper. -require_once realpath(JPATH_ADMINISTRATOR . '/components/com_menus/helpers/menus.php'); - /** * Supports an HTML select list of menus * From 3636b98b23d5f37b1397e92e075c6acd55f51bc3 Mon Sep 17 00:00:00 2001 From: Harald Leithner Date: Wed, 27 Apr 2022 19:12:58 +0200 Subject: [PATCH 04/21] Remove unnecessary require not used anymore --- libraries/src/Form/Field/MenuitemField.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/libraries/src/Form/Field/MenuitemField.php b/libraries/src/Form/Field/MenuitemField.php index f644fc1b439c4..a58891826f1cf 100644 --- a/libraries/src/Form/Field/MenuitemField.php +++ b/libraries/src/Form/Field/MenuitemField.php @@ -13,9 +13,6 @@ use Joomla\CMS\HTML\HTMLHelper; use Joomla\Component\Menus\Administrator\Helper\MenusHelper; -// Import the com_menus helper. -require_once realpath(JPATH_ADMINISTRATOR . '/components/com_menus/helpers/menus.php'); - /** * Supports an HTML grouped select list of menu item grouped by menu * From a12b247437ca2c3a3ebd0c1be834d5fe02bcd6f3 Mon Sep 17 00:00:00 2001 From: Harald Leithner Date: Wed, 27 Apr 2022 20:50:22 +0200 Subject: [PATCH 05/21] Deprecate constant JCOMPAT_UNICODE_PROPERTIES Prepare to remove JCOMPAT_UNICODE_PROPERTIES constants from global namespace and replace it with a static variable within the test function. This is a preparation for PSR12 transformation to satisfy PSR1.Files.SideEffects.FoundWithSymbols rule. Beside that it doesn't make sense to declare a global constant which is only used in the local class. --- libraries/src/Form/FormRule.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/libraries/src/Form/FormRule.php b/libraries/src/Form/FormRule.php index c6a9af1d7f290..36302dd44c655 100644 --- a/libraries/src/Form/FormRule.php +++ b/libraries/src/Form/FormRule.php @@ -4,6 +4,9 @@ * * @copyright (C) 2017 Open Source Matters, Inc. * @license GNU General Public License version 2 or later; see LICENSE.txt + * + * Remove phpcs exception with deprecated constant JCOMPAT_UNICODE_PROPERTIES + * @phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols */ namespace Joomla\CMS\Form; @@ -20,6 +23,8 @@ * * @var boolean * @since 1.6 + * + * @deprecated 4.2 Will be removed without replacement in Joomla 5.0 (Also remove phpcs exception) */ \define('JCOMPAT_UNICODE_PROPERTIES', (bool) @preg_match('/\pL/u', 'a')); } @@ -71,8 +76,16 @@ public function test(\SimpleXMLElement $element, $value, $group = null, Registry throw new \UnexpectedValueException(sprintf('%s has invalid regex.', \get_class($this))); } + // Detect if we have full UTF-8 and unicode PCRE support. + static $unicodePropertiesSupport = null; + + if ($unicodePropertiesSupport === null) + { + $unicodePropertiesSupport = (bool) @\preg_match('/\pL/u', 'a'); + } + // Add unicode property support if available. - if (JCOMPAT_UNICODE_PROPERTIES) + if ($unicodePropertiesSupport) { $this->modifiers = (strpos($this->modifiers, 'u') !== false) ? $this->modifiers : $this->modifiers . 'u'; } From 8eb20eb644482039718a39b22c9b4d61e6bbb2e1 Mon Sep 17 00:00:00 2001 From: Harald Leithner Date: Wed, 27 Apr 2022 21:03:48 +0200 Subject: [PATCH 06/21] Add phpcs exception for BufferStreamHandler The handler auto register should have been removed in Joomla! 4.0. --- libraries/src/Utility/BufferStreamHandler.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libraries/src/Utility/BufferStreamHandler.php b/libraries/src/Utility/BufferStreamHandler.php index 0d7d04d1822f7..3382c0a1a7d0c 100644 --- a/libraries/src/Utility/BufferStreamHandler.php +++ b/libraries/src/Utility/BufferStreamHandler.php @@ -4,13 +4,19 @@ * * @copyright (C) 2007 Open Source Matters, Inc. * @license GNU General Public License version 2 or later; see LICENSE.txt + * + * Remove phpcs exception with deprecated autoloading BufferStreamHandler::stream_register(); + * @phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols */ namespace Joomla\CMS\Utility; \defined('JPATH_PLATFORM') or die; -// Workaround for B/C. Will be removed with 4.0 +/** + * @deprecated Workaround for B/C. Will be removed with 5.0 (removal missed in 4.0, also remove phpcs exception). + * If BufferStreamHandler is needed directly call BufferStreamHandler::stream_register(); + */ BufferStreamHandler::stream_register(); /** From 49a3a61cfc27495f09a6b35ae5f39bb67a907576 Mon Sep 17 00:00:00 2001 From: Harald Leithner Date: Wed, 27 Apr 2022 21:44:05 +0200 Subject: [PATCH 07/21] Fix phpcs:disable --- libraries/src/Form/FormRule.php | 2 +- libraries/src/Utility/BufferStreamHandler.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/src/Form/FormRule.php b/libraries/src/Form/FormRule.php index 36302dd44c655..20c3d06a80429 100644 --- a/libraries/src/Form/FormRule.php +++ b/libraries/src/Form/FormRule.php @@ -6,7 +6,7 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt * * Remove phpcs exception with deprecated constant JCOMPAT_UNICODE_PROPERTIES - * @phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols + * @phpcs:disable PSR1.Files.SideEffects */ namespace Joomla\CMS\Form; diff --git a/libraries/src/Utility/BufferStreamHandler.php b/libraries/src/Utility/BufferStreamHandler.php index 3382c0a1a7d0c..415b3d84fb3bc 100644 --- a/libraries/src/Utility/BufferStreamHandler.php +++ b/libraries/src/Utility/BufferStreamHandler.php @@ -6,7 +6,7 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt * * Remove phpcs exception with deprecated autoloading BufferStreamHandler::stream_register(); - * @phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols + * @phpcs:disable PSR1.Files.SideEffects */ namespace Joomla\CMS\Utility; From 67b554118b666774b2779e28931256d30ab95c37 Mon Sep 17 00:00:00 2001 From: Harald Leithner Date: Wed, 27 Apr 2022 22:09:32 +0200 Subject: [PATCH 08/21] Add no break to switch in com_config controller --- .../com_config/src/Controller/ComponentController.php | 3 +++ administrator/components/com_users/src/Model/UsersModel.php | 3 ++- libraries/src/Form/FormField.php | 6 ++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/administrator/components/com_config/src/Controller/ComponentController.php b/administrator/components/com_config/src/Controller/ComponentController.php index a7718e366b545..247aecede26fb 100644 --- a/administrator/components/com_config/src/Controller/ComponentController.php +++ b/administrator/components/com_config/src/Controller/ComponentController.php @@ -161,6 +161,9 @@ public function save($key = null, $urlVar = null) case 'save': $this->setMessage(Text::_('COM_CONFIG_SAVE_SUCCESS'), 'message'); + + // No break + default: $redirect = 'index.php?option=' . $option; diff --git a/administrator/components/com_users/src/Model/UsersModel.php b/administrator/components/com_users/src/Model/UsersModel.php index 67e169d15d85c..2cd6c20c56b6e 100644 --- a/administrator/components/com_users/src/Model/UsersModel.php +++ b/administrator/components/com_users/src/Model/UsersModel.php @@ -611,7 +611,8 @@ private function buildDateRange($range) case 'post_year': $dNow = false; - // no break + + // No break case 'past_year': $dStart->modify('-1 year'); diff --git a/libraries/src/Form/FormField.php b/libraries/src/Form/FormField.php index 2f985e369ca8d..460119d07e69c 100644 --- a/libraries/src/Form/FormField.php +++ b/libraries/src/Form/FormField.php @@ -515,7 +515,8 @@ public function __set($name, $value) case 'class': // Removes spaces from left & right and extra spaces from middle $value = preg_replace('/\s+/', ' ', trim((string) $value)); - // no break + + // No break case 'description': case 'hint': @@ -552,7 +553,8 @@ public function __set($name, $value) // Allow for field classes to force the multiple values option. $value = (string) $value; $value = $value === '' && isset($this->forceMultiple) ? (string) $this->forceMultiple : $value; - // no break + + // No break case 'required': case 'disabled': From 37435f2ff3b444e69b9b1e8319abb69a442c557d Mon Sep 17 00:00:00 2001 From: Harald Leithner Date: Tue, 17 May 2022 20:38:24 +0200 Subject: [PATCH 09/21] PSR-12 Preparation --- layouts/joomla/content/blog_style_default_item_title.php | 5 ++--- libraries/src/Form/FormRule.php | 4 ++-- libraries/src/MVC/Model/BaseDatabaseModel.php | 3 --- libraries/src/Utility/BufferStreamHandler.php | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/layouts/joomla/content/blog_style_default_item_title.php b/layouts/joomla/content/blog_style_default_item_title.php index b2a5ec482e854..24a594496a065 100644 --- a/layouts/joomla/content/blog_style_default_item_title.php +++ b/layouts/joomla/content/blog_style_default_item_title.php @@ -19,15 +19,14 @@ $canEdit = $displayData->params->get('access-edit'); $currentDate = Factory::getDate()->format('Y-m-d H:i:s'); +$link = RouteHelper::getArticleRoute($displayData->slug, $displayData->catid, $displayData->language); ?> state == 0 || $params->get('show_title') || ($params->get('show_author') && !empty($displayData->author ))) : ?> @@ -30,6 +30,7 @@ export default { name: 'MediaBrowserItemVideo', // eslint-disable-next-line vue/require-prop-types props: ['item', 'focused'], + emits: ['toggle-settings'], data() { return { showActions: false, @@ -44,6 +45,9 @@ export default { openPreview() { this.$refs.container.openPreview(); }, + toggleSettings(bool) { + this.$emit('toggle-settings', bool); + }, }, }; From 026dc0fc009c5e701d5395b7fc54a329c2adbef0 Mon Sep 17 00:00:00 2001 From: George Wilson Date: Mon, 27 Jun 2022 16:51:17 +0100 Subject: [PATCH 13/21] Fix multiselect when not all rows have checkboxes (#38146) --- .../media_source/system/js/multiselect.es6.js | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/build/media_source/system/js/multiselect.es6.js b/build/media_source/system/js/multiselect.es6.js index 5ac631e6db782..58003123513f2 100644 --- a/build/media_source/system/js/multiselect.es6.js +++ b/build/media_source/system/js/multiselect.es6.js @@ -66,12 +66,14 @@ return; } - const currentRowNum = this.rows.indexOf(target.closest('tr')); - const currentCheckBox = this.checkallToggle ? currentRowNum + 1 : currentRowNum; - let isChecked = this.boxes[currentCheckBox].checked; + const closestRow = target.closest('tr'); + const currentRowNum = this.rows.indexOf(closestRow); + const currentCheckBox = closestRow.querySelector('td input[type=checkbox]'); - if (currentCheckBox >= 0) { - if (!(target.id === this.boxes[currentCheckBox].id)) { + if (currentCheckBox) { + let isChecked = currentCheckBox.checked; + + if (!(target.id === currentCheckBox.id)) { // We will prevent selecting text to prevent artifacts if (shiftKey) { document.body.style['-webkit-user-select'] = 'none'; @@ -80,12 +82,12 @@ document.body.style['user-select'] = 'none'; } - this.boxes[currentCheckBox].checked = !this.boxes[currentCheckBox].checked; - isChecked = this.boxes[currentCheckBox].checked; - Joomla.isChecked(this.boxes[currentCheckBox].checked, this.tableEl.id); + currentCheckBox.checked = !currentCheckBox.checked; + isChecked = currentCheckBox.checked; + Joomla.isChecked(isChecked, this.tableEl.id); } - this.changeBg(this.rows[currentCheckBox - 1], isChecked); + this.changeBg(this.rows[currentRowNum], isChecked); // Restore normality if (shiftKey) { From 0eeae2d35693699b34125dd85e4a2273455a6ed5 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Mon, 27 Jun 2022 19:08:07 +0300 Subject: [PATCH 14/21] in updates XML always resulted in an error (#38121) * in updates XML always resulted in an error * Use uppercase for array key and language string key Co-authored-by: Richard Fath --- administrator/language/en-GB/lib_joomla.ini | 8 ++++++++ libraries/src/Updater/Adapter/ExtensionAdapter.php | 11 +++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/administrator/language/en-GB/lib_joomla.ini b/administrator/language/en-GB/lib_joomla.ini index 178145886bce9..e424927663bd4 100644 --- a/administrator/language/en-GB/lib_joomla.ini +++ b/administrator/language/en-GB/lib_joomla.ini @@ -758,3 +758,11 @@ JLIB_SIZE_PB="PiB" JLIB_SIZE_EB="EiB" JLIB_SIZE_ZB="ZiB" JLIB_SIZE_YB="YiB" + +; Database server technology types in human readable terms. Used in the Updater package. +JLIB_DB_SERVER_TYPE_MARIADB="MariaDB" +JLIB_DB_SERVER_TYPE_MSSQL="Microsoft SQL Server" +JLIB_DB_SERVER_TYPE_MYSQL="MySQL" +JLIB_DB_SERVER_TYPE_ORACLE="Oracle" +JLIB_DB_SERVER_TYPE_POSTGRESQL="PostgreSQL" +JLIB_DB_SERVER_TYPE_SQLITE="SQLite" diff --git a/libraries/src/Updater/Adapter/ExtensionAdapter.php b/libraries/src/Updater/Adapter/ExtensionAdapter.php index 59740ab424bc3..f84302bdafff1 100644 --- a/libraries/src/Updater/Adapter/ExtensionAdapter.php +++ b/libraries/src/Updater/Adapter/ExtensionAdapter.php @@ -155,10 +155,13 @@ protected function _endElement($parser, $name) } } + // $supportedDbs has uppercase keys because they are XML attribute names + $dbTypeUcase = strtoupper($dbType); + // Do we have an entry for the database? - if (\array_key_exists($dbType, $supportedDbs)) + if (\array_key_exists($dbTypeUcase, $supportedDbs)) { - $minimumVersion = $supportedDbs[$dbType]; + $minimumVersion = $supportedDbs[$dbTypeUcase]; $dbMatch = version_compare($dbVersion, $minimumVersion, '>='); if (!$dbMatch) @@ -168,7 +171,7 @@ protected function _endElement($parser, $name) 'JLIB_INSTALLER_AVAILABLE_UPDATE_DB_MINIMUM', $this->currentUpdate->name, $this->currentUpdate->version, - Text::_($db->name), + Text::_('JLIB_DB_SERVER_TYPE_' . $dbTypeUcase), $dbVersion, $minimumVersion ); @@ -183,7 +186,7 @@ protected function _endElement($parser, $name) 'JLIB_INSTALLER_AVAILABLE_UPDATE_DB_TYPE', $this->currentUpdate->name, $this->currentUpdate->version, - Text::_($db->name) + Text::_('JLIB_DB_SERVER_TYPE_' . $dbTypeUcase) ); Factory::getApplication()->enqueueMessage($dbMsg, 'warning'); From 8b2cf5a00a450af2de6205149cecd14675df7816 Mon Sep 17 00:00:00 2001 From: George Wilson Date: Mon, 27 Jun 2022 17:10:23 +0100 Subject: [PATCH 15/21] Media Manager Folder Selectory a11y (#38126) * Remove the item vue and merge it into the tree vue for ease * Add accessible keyboard support for the media manager disk view --- .../scripts/components/tree/drive.vue | 26 ++++- .../scripts/components/tree/item.vue | 96 --------------- .../scripts/components/tree/tree.vue | 110 +++++++++++++++++- .../resources/scripts/mediamanager.es6.js | 2 - 4 files changed, 124 insertions(+), 110 deletions(-) delete mode 100644 administrator/components/com_media/resources/scripts/components/tree/item.vue diff --git a/administrator/components/com_media/resources/scripts/components/tree/drive.vue b/administrator/components/com_media/resources/scripts/components/tree/drive.vue index fcd0aa88ee175..96a30b01e1331 100644 --- a/administrator/components/com_media/resources/scripts/components/tree/drive.vue +++ b/administrator/components/com_media/resources/scripts/components/tree/drive.vue @@ -10,18 +10,26 @@ >
  • - + {{ drive.displayName }}
  • @@ -50,6 +58,12 @@ export default { onDriveClick() { this.navigateTo(this.drive.root); }, + moveFocusToChildElement(nextRoot) { + this.$refs[nextRoot].setFocusToFirstChild(); + }, + restoreFocus() { + this.$refs['drive-root'].focus(); + }, }, }; diff --git a/administrator/components/com_media/resources/scripts/components/tree/item.vue b/administrator/components/com_media/resources/scripts/components/tree/item.vue deleted file mode 100644 index 0794d69384788..0000000000000 --- a/administrator/components/com_media/resources/scripts/components/tree/item.vue +++ /dev/null @@ -1,96 +0,0 @@ - - - diff --git a/administrator/components/com_media/resources/scripts/components/tree/tree.vue b/administrator/components/com_media/resources/scripts/components/tree/tree.vue index 21740eb62edf5..5f3555700f1af 100644 --- a/administrator/components/com_media/resources/scripts/components/tree/tree.vue +++ b/administrator/components/com_media/resources/scripts/components/tree/tree.vue @@ -3,20 +3,52 @@ class="media-tree" role="group" > - + class="media-tree-item" + :class="{active: isActive(item)}" + role="none" + > + + + {{ item.name }} + + + + + diff --git a/administrator/components/com_media/resources/scripts/mediamanager.es6.js b/administrator/components/com_media/resources/scripts/mediamanager.es6.js index 52b763d2ec326..5eac760e70196 100644 --- a/administrator/components/com_media/resources/scripts/mediamanager.es6.js +++ b/administrator/components/com_media/resources/scripts/mediamanager.es6.js @@ -4,7 +4,6 @@ import App from './components/app.vue'; import Disk from './components/tree/disk.vue'; import Drive from './components/tree/drive.vue'; import Tree from './components/tree/tree.vue'; -import TreeItem from './components/tree/item.vue'; import Toolbar from './components/toolbar/toolbar.vue'; import Breadcrumb from './components/breadcrumb/breadcrumb.vue'; import Browser from './components/browser/browser.vue'; @@ -38,7 +37,6 @@ app.use(translate); app.component('MediaDrive', Drive); app.component('MediaDisk', Disk); app.component('MediaTree', Tree); -app.component('MediaTreeItem', TreeItem); app.component('MediaToolbar', Toolbar); app.component('MediaBreadcrumb', Breadcrumb); app.component('MediaBrowser', Browser); From 170f91a637b61011f4d95f859b5f0c24b7c608a3 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Mon, 27 Jun 2022 20:16:07 +0300 Subject: [PATCH 16/21] Refactored WebAuthn with Windows Hello support (#37910) * Refactored WebAuthn plugin * Fix the WebAuthn management page which was broken in https://github.com/joomla/joomla-cms/pull/37464 * Fix wrong `@since` doc tag * Fix docblock typo * Fix docblock typo * Fix docblock typo * Fix docblock typo * Fix docblock typo * Fix broken management interface * Make unnecessarily static method back into non-static * Replace static helper with injected object * Come on, commit the ENTIRE file! * Use the user factory * Fix error when going through the user factory * Fix: cannot add WebAuthn authenticator right back after deleting it * Remove useless switch branch * Remove useless exception * Display make and model of the authenticator, if possible * Add missing JWT signature algorithms * Fix copyright date * Fix for PHP 8 using FIDO keys and Android phones * Reactivate the tooltips after adding an authenticator * Option to disable the attestation support * The Windows Hello icon was invisible on white background * Attempt to fix Appveyor not having Sodium in the Windows build * Work around third party library bug... * Create events in a forwards-compatible manner * Concrete events * Fix event woes * Update plugins/system/webauthn/webauthn.xml Co-authored-by: Brian Teeman * Update administrator/language/en-GB/plg_system_webauthn.ini Co-authored-by: Brian Teeman * Improve the layout for editing an authenticator It now follows the Bootstrap 5 form aesthetic. Moreover, there are gaps between the text input and the Save and Cancel buttons. * Confirm deletion of authenticators * Make the bots happy again * Code polishing * Marking classes final * Use setApplication / getApplication in the plugin class * Remove unused `$db` from the plugin class * Blind fix Currently #38060 has broken everything it seems? * Bring application injection in sync with core * Remove whitespace * Add use statement * Fix wrong event creation in AjaxHandlerLogin * License change Co-authored-by: Richard Fath Co-authored-by: Brian Teeman Co-authored-by: Roland Dalmulder Co-authored-by: Allon Moritz Co-authored-by: Harald Leithner Co-authored-by: George Wilson --- .appveyor.yml | 2 +- .../language/en-GB/plg_system_webauthn.ini | 8 +- .../plg_system_webauthn/images/fido.png | Bin 0 -> 3501 bytes .../plg_system_webauthn/js/login.es6.js | 22 +- .../plg_system_webauthn/js/management.es6.js | 133 +++- composer.json | 4 +- composer.lock | 687 +++++++++++++++- layouts/plugins/system/webauthn/manage.php | 73 +- libraries/src/Event/CoreEventAware.php | 15 + .../src/Event/Plugin/System/Webauthn/Ajax.php | 20 + .../Plugin/System/Webauthn/AjaxChallenge.php | 45 ++ .../Plugin/System/Webauthn/AjaxCreate.php | 25 + .../Plugin/System/Webauthn/AjaxDelete.php | 25 + .../Plugin/System/Webauthn/AjaxInitCreate.php | 46 ++ .../Plugin/System/Webauthn/AjaxLogin.php | 21 + .../Plugin/System/Webauthn/AjaxSaveLabel.php | 25 + plugins/system/webauthn/services/provider.php | 89 +++ .../system/webauthn/src/Authentication.php | 570 ++++++++++++++ .../webauthn/src/CredentialRepository.php | 269 ++++++- .../src/Exception/AjaxNonCmsAppException.php | 24 - .../webauthn/src/Extension/Webauthn.php | 189 +++++ .../webauthn/src/Field/WebauthnField.php | 22 +- .../src/Helper/CredentialsCreation.php | 358 --------- plugins/system/webauthn/src/Helper/Joomla.php | 744 ------------------ .../AndroidKeyAttestationStatementSupport.php | 270 +++++++ .../FidoU2FAttestationStatementSupport.php | 230 ++++++ plugins/system/webauthn/src/Hotfix/Server.php | 452 +++++++++++ .../webauthn/src/MetadataRepository.php | 246 ++++++ .../PluginTraits/AdditionalLoginButtons.php | 167 ++-- .../webauthn/src/PluginTraits/AjaxHandler.php | 151 ++-- .../src/PluginTraits/AjaxHandlerChallenge.php | 99 +-- .../src/PluginTraits/AjaxHandlerCreate.php | 48 +- .../src/PluginTraits/AjaxHandlerDelete.php | 46 +- .../PluginTraits/AjaxHandlerInitCreate.php | 62 ++ .../src/PluginTraits/AjaxHandlerLogin.php | 366 +++++---- .../src/PluginTraits/AjaxHandlerSaveLabel.php | 46 +- .../src/PluginTraits/EventReturnAware.php | 45 ++ .../src/PluginTraits/UserDeletion.php | 33 +- .../src/PluginTraits/UserProfileFields.php | 108 ++- plugins/system/webauthn/webauthn.php | 78 -- plugins/system/webauthn/webauthn.xml | 21 +- 41 files changed, 4090 insertions(+), 1794 deletions(-) create mode 100644 build/media_source/plg_system_webauthn/images/fido.png create mode 100644 libraries/src/Event/Plugin/System/Webauthn/Ajax.php create mode 100644 libraries/src/Event/Plugin/System/Webauthn/AjaxChallenge.php create mode 100644 libraries/src/Event/Plugin/System/Webauthn/AjaxCreate.php create mode 100644 libraries/src/Event/Plugin/System/Webauthn/AjaxDelete.php create mode 100644 libraries/src/Event/Plugin/System/Webauthn/AjaxInitCreate.php create mode 100644 libraries/src/Event/Plugin/System/Webauthn/AjaxLogin.php create mode 100644 libraries/src/Event/Plugin/System/Webauthn/AjaxSaveLabel.php create mode 100644 plugins/system/webauthn/services/provider.php create mode 100644 plugins/system/webauthn/src/Authentication.php delete mode 100644 plugins/system/webauthn/src/Exception/AjaxNonCmsAppException.php create mode 100644 plugins/system/webauthn/src/Extension/Webauthn.php delete mode 100644 plugins/system/webauthn/src/Helper/CredentialsCreation.php delete mode 100644 plugins/system/webauthn/src/Helper/Joomla.php create mode 100644 plugins/system/webauthn/src/Hotfix/AndroidKeyAttestationStatementSupport.php create mode 100644 plugins/system/webauthn/src/Hotfix/FidoU2FAttestationStatementSupport.php create mode 100644 plugins/system/webauthn/src/Hotfix/Server.php create mode 100644 plugins/system/webauthn/src/MetadataRepository.php create mode 100644 plugins/system/webauthn/src/PluginTraits/AjaxHandlerInitCreate.php create mode 100644 plugins/system/webauthn/src/PluginTraits/EventReturnAware.php delete mode 100644 plugins/system/webauthn/webauthn.php diff --git a/.appveyor.yml b/.appveyor.yml index 40911f2de72a9..a0f9e6b32ef4e 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -44,7 +44,7 @@ install: - choco install composer - cd C:\projects\joomla-cms - refreshenv - - composer install --no-progress --profile + - composer install --no-progress --profile --ignore-platform-req=ext-sodium before_test: # Database setup for MySQL via PowerShell tools - > diff --git a/administrator/language/en-GB/plg_system_webauthn.ini b/administrator/language/en-GB/plg_system_webauthn.ini index af0b5a61f1595..1f1f7dcf8fba5 100644 --- a/administrator/language/en-GB/plg_system_webauthn.ini +++ b/administrator/language/en-GB/plg_system_webauthn.ini @@ -18,17 +18,21 @@ PLG_SYSTEM_WEBAUTHN_ERR_CREDENTIAL_ID_ALREADY_IN_USE="Cannot save credentials. T PLG_SYSTEM_WEBAUTHN_ERR_EMPTY_USERNAME="You need to enter your username (but NOT your password) before selecting the Web Authentication login button." PLG_SYSTEM_WEBAUTHN_ERR_INVALID_USERNAME="The specified username does not correspond to a user account that has enabled passwordless login on this site." PLG_SYSTEM_WEBAUTHN_ERR_LABEL_NOT_SAVED="Could not save the new label" +PLG_SYSTEM_WEBAUTHN_ERR_NOT_DELETED="Could not remove the authenticator" PLG_SYSTEM_WEBAUTHN_ERR_NO_BROWSER_SUPPORT="Sorry, your browser does not support the W3C Web Authentication standard for passwordless logins or your site is not being served over HTTPS with a valid certificate, signed by a Certificate Authority your browser trusts. You will need to log into this site using your username and password." PLG_SYSTEM_WEBAUTHN_ERR_NO_STORED_CREDENTIAL="Cannot find the stored credentials for your login authenticator." -PLG_SYSTEM_WEBAUTHN_ERR_NOT_DELETED="Could not remove the authenticator" PLG_SYSTEM_WEBAUTHN_ERR_USER_REMOVED="The user for this authenticator seems to no longer exist on this site." +PLG_SYSTEM_WEBAUTHN_ERR_XHR_INITCREATE="Cannot get the authenticator registration information from your site." +PLG_SYSTEM_WEBAUTHN_FIELD_ATTESTATION_SUPPORT_DESC="Only allow authenticators with verifiable cryptographic signatures to be used for WebAuthn logins. Strongly recommended for high security environments. Requires your site to be able to access https://mds.fidoalliance.org/ directly, a writeable cache directory, the system temporary directory being writeable by PHP, and the OpenSSL extension. May prevent some cheaper, non-certified authenticators from working at all. Disabling it also prevents Joomla from identifying the make and model of the authenticator you are using (no icon will be displayed next to the Authenticator Name).
    Pro tip: If you are behind a firewall you can place the data downloaded from the FIDO Alliance into the file administrator/cache/fido.jwt for this feature to work properly." +PLG_SYSTEM_WEBAUTHN_FIELD_ATTESTATION_SUPPORT_LABEL="Attestation Support" PLG_SYSTEM_WEBAUTHN_FIELD_DESC="Lets you manage passwordless login methods using the W3C Web Authentication standard. You need a supported browser and authenticator (eg Google Chrome or Firefox with a FIDO2 certified security key).

    MacOS/iOS/watchOS: Touch/Face ID.
    Windows: Hello (Fingerprint / Facial Recognition / PIN).
    Android: Biometric screen lock.

    You can find more details in the WebAuthn Passwordless Login documentation." PLG_SYSTEM_WEBAUTHN_FIELD_LABEL="W3C Web Authentication (WebAuthn) Login" PLG_SYSTEM_WEBAUTHN_FIELD_N_AUTHENTICATORS_REGISTERED="%d WebAuthn authenticators already set up: %s" PLG_SYSTEM_WEBAUTHN_FIELD_N_AUTHENTICATORS_REGISTERED_0="No WebAuthn authenticator has been set up yet" PLG_SYSTEM_WEBAUTHN_FIELD_N_AUTHENTICATORS_REGISTERED_1="One WebAuthn authenticator already set up: %2$s" PLG_SYSTEM_WEBAUTHN_HEADER="W3C Web Authentication (WebAuthn) Login" -PLG_SYSTEM_WEBAUTHN_LBL_DEFAULT_AUTHENTICATOR_LABEL="Authenticator added on %s" +PLG_SYSTEM_WEBAUTHN_LBL_DEFAULT_AUTHENTICATOR="Generic Authenticator" +PLG_SYSTEM_WEBAUTHN_LBL_DEFAULT_AUTHENTICATOR_LABEL="%s added on %s" PLG_SYSTEM_WEBAUTHN_LOGIN_DESC="Login without a password using the W3C Web Authentication (WebAuthn) standard in compatible browsers. You need to have already set up WebAuthn authentication in your user profile." PLG_SYSTEM_WEBAUTHN_LOGIN_LABEL="Web Authentication" PLG_SYSTEM_WEBAUTHN_MANAGE_BTN_ADD_LABEL="Add New Authenticator" diff --git a/build/media_source/plg_system_webauthn/images/fido.png b/build/media_source/plg_system_webauthn/images/fido.png new file mode 100644 index 0000000000000000000000000000000000000000..444bab5f4c1174f53d2e3ec48815d0760a776ac6 GIT binary patch literal 3501 zcmV;e4N~%nP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0008mNklI4`5_y=y&st$>egm_LSAt+ywAye0=|oON&e9x8@(AkdW^5pu4kkXz;lzDcR6# zI4q}=l8*HC_h)8gFexdkE8iG~0dT*)!)l!{{P zfzR&)MeXf@HeUlE?f3}*4R`CiySe~KKmn+$sU?I|>8=ByW$~%16s4RN1J;QNLP+5S zEr3(%lL^v80Q&U$=vNdB2HV@(D2jT}(ZTb4C=}v2?%9*40Q!0jEXxAO$~?{a{T#>T zoXG;Pw757uH4Q*_^%^0>>2%5y65=S4-ENO;yE6xIadEUlK?qSKCQ2j{u~;mV$>eex zfK)2|GXEKXPFE?5kC(}08jS{k&*z((oCHu_Q4T<*QWa_ocoLqUn~OdNoZkgspFcQ`f#Fs)Dka5`PTe1|}a4hRN=ktm4gc?C^xdAEu$mxw{> z`wZzB>2A0CrO^l=5D1iADZ6#&_Pv&tw6wH^g|8pxKINay%gN1+{!~aLl1Pj=;c(dD z*ib5!!^6X3imGpH936R4b)yOZySA2^dd$$H-`?K7R8rj3+ { + const handleLoginChallenge = (publicKey) => { const arrayToBase64String = (a) => btoa(String.fromCharCode(...a)); const base64url2base64 = (input) => { @@ -172,7 +170,8 @@ window.Joomla = window.Joomla || {}; }; // Send the response to your server - window.location = `${callbackUrl}&option=com_ajax&group=system&plugin=webauthn&` + const paths = Joomla.getOptions('system.paths'); + window.location = `${paths ? `${paths.base}/index.php` : window.location.pathname}?${Joomla.getOptions('csrf.token')}=1&option=com_ajax&group=system&plugin=webauthn&` + `format=raw&akaction=login&encoding=redirect&data=${ btoa(JSON.stringify(publicKeyCredential))}`; }) @@ -187,13 +186,11 @@ window.Joomla = window.Joomla || {}; * for the user. * * @param {string} formId The login form's or login module's HTML ID - * @param {string} callbackUrl The URL we will use to post back to the server. Must include - * the anti-CSRF token. * * @returns {boolean} Always FALSE to prevent BUTTON elements from reloading the page. */ // eslint-disable-next-line no-unused-vars - Joomla.plgSystemWebauthnLogin = (formId, callbackUrl) => { + Joomla.plgSystemWebauthnLogin = (formId) => { // Get the username const elFormContainer = document.getElementById(formId); const elUsername = lookForField(elFormContainer, 'input[name=username]'); @@ -226,9 +223,14 @@ window.Joomla = window.Joomla || {}; username, returnUrl, }; + postBackData[Joomla.getOptions('csrf.token')] = 1; + + const paths = Joomla.getOptions('system.paths'); Joomla.request({ - url: callbackUrl, + url: `${paths ? `${paths.base}/index.php` : window.location.pathname}?${Joomla.getOptions( + 'csrf.token', + )}=1`, method: 'POST', data: interpolateParameters(postBackData), onSuccess(rawResponse) { @@ -243,7 +245,7 @@ window.Joomla = window.Joomla || {}; */ } - handleLoginChallenge(jsonData, callbackUrl); + handleLoginChallenge(jsonData); }, onError: (xhr) => { handleLoginError(`${xhr.status} ${xhr.statusText}`); @@ -258,7 +260,7 @@ window.Joomla = window.Joomla || {}; if (loginButtons.length) { loginButtons.forEach((button) => { button.addEventListener('click', ({ currentTarget }) => { - Joomla.plgSystemWebauthnLogin(currentTarget.getAttribute('data-webauthn-form'), currentTarget.getAttribute('data-webauthn-url')); + Joomla.plgSystemWebauthnLogin(currentTarget.getAttribute('data-webauthn-form')); }); }); } diff --git a/build/media_source/plg_system_webauthn/js/management.es6.js b/build/media_source/plg_system_webauthn/js/management.es6.js index 58cdbb4d12873..d4abb618bcdea 100644 --- a/build/media_source/plg_system_webauthn/js/management.es6.js +++ b/build/media_source/plg_system_webauthn/js/management.es6.js @@ -62,13 +62,9 @@ window.Joomla = window.Joomla || {}; * Posts the credentials to the URL defined in post_url using AJAX. * That URL must re-render the management interface. * These contents will replace the element identified by the interface_selector CSS selector. - * - * @param {String} storeID CSS ID for the element storing the configuration in its - * data properties - * @param {String} interfaceSelector CSS selector for the GUI container */ // eslint-disable-next-line no-unused-vars - Joomla.plgSystemWebauthnCreateCredentials = (storeID, interfaceSelector) => { + Joomla.plgSystemWebauthnInitCreateCredentials = () => { // Make sure the browser supports Webauthn if (!('credentials' in navigator)) { Joomla.renderMessages({ error: [Joomla.Text._('PLG_SYSTEM_WEBAUTHN_ERR_NO_BROWSER_SUPPORT')] }); @@ -76,15 +72,42 @@ window.Joomla = window.Joomla || {}; return; } - // Extract the configuration from the store - const elStore = document.getElementById(storeID); + // Get the public key creation options through AJAX. + const paths = Joomla.getOptions('system.paths'); + const postURL = `${paths ? `${paths.base}/index.php` : window.location.pathname}`; - if (!elStore) { - return; - } + const postBackData = { + option: 'com_ajax', + group: 'system', + plugin: 'webauthn', + format: 'json', + akaction: 'initcreate', + encoding: 'json', + }; + postBackData[Joomla.getOptions('csrf.token')] = 1; + + Joomla.request({ + url: postURL, + method: 'POST', + data: interpolateParameters(postBackData), + onSuccess(response) { + try { + const publicKey = JSON.parse(response); + + Joomla.plgSystemWebauthnCreateCredentials(publicKey); + } catch (exception) { + handleCreationError(Joomla.Text._('PLG_SYSTEM_WEBAUTHN_ERR_XHR_INITCREATE')); + } + }, + onError: (xhr) => { + handleCreationError(`${xhr.status} ${xhr.statusText}`); + }, + }); + }; - const publicKey = JSON.parse(atob(elStore.dataset.public_key)); - const postURL = atob(elStore.dataset.postback_url); + Joomla.plgSystemWebauthnCreateCredentials = (publicKey) => { + const paths = Joomla.getOptions('system.paths'); + const postURL = `${paths ? `${paths.base}/index.php` : window.location.pathname}`; const arrayToBase64String = (a) => btoa(String.fromCharCode(...a)); @@ -137,13 +160,14 @@ window.Joomla = window.Joomla || {}; encoding: 'raw', data: btoa(JSON.stringify(publicKeyCredential)), }; + postBackData[Joomla.getOptions('csrf.token')] = 1; Joomla.request({ url: postURL, method: 'POST', data: interpolateParameters(postBackData), onSuccess(responseHTML) { - const elements = document.querySelectorAll(interfaceSelector); + const elements = document.querySelectorAll('#plg_system_webauthn-management-interface'); if (!elements) { return; @@ -154,6 +178,7 @@ window.Joomla = window.Joomla || {}; elContainer.outerHTML = responseHTML; Joomla.plgSystemWebauthnInitialize(); + Joomla.plgSystemWebauthnReactivateTooltips(); }, onError: (xhr) => { handleCreationError(`${xhr.status} ${xhr.statusText}`); @@ -175,15 +200,9 @@ window.Joomla = window.Joomla || {}; * properties */ // eslint-disable-next-line no-unused-vars - Joomla.plgSystemWebauthnEditLabel = (that, storeID) => { - // Extract the configuration from the store - const elStore = document.getElementById(storeID); - - if (!elStore) { - return false; - } - - const postURL = atob(elStore.dataset.postback_url); + Joomla.plgSystemWebauthnEditLabel = (that) => { + const paths = Joomla.getOptions('system.paths'); + const postURL = `${paths ? `${paths.base}/index.php` : window.location.pathname}`; // Find the UI elements const elTR = that.parentElement.parentElement; @@ -198,10 +217,14 @@ window.Joomla = window.Joomla || {}; // Show the editor const oldLabel = elLabelTD.innerText; + const elContainer = document.createElement('div'); + elContainer.className = 'webauthnManagementEditorRow d-flex gap-2'; + const elInput = document.createElement('input'); elInput.type = 'text'; elInput.name = 'label'; elInput.defaultValue = oldLabel; + elInput.className = 'form-control'; const elSave = document.createElement('button'); elSave.className = 'btn btn-success btn-sm'; @@ -220,6 +243,7 @@ window.Joomla = window.Joomla || {}; credential_id: credentialId, new_label: elNewLabel, }; + postBackData[Joomla.getOptions('csrf.token')] = 1; Joomla.request({ url: postURL, @@ -268,9 +292,10 @@ window.Joomla = window.Joomla || {}; }, false); elLabelTD.innerHTML = ''; - elLabelTD.appendChild(elInput); - elLabelTD.appendChild(elSave); - elLabelTD.appendChild(elCancel); + elContainer.appendChild(elInput); + elContainer.appendChild(elSave); + elContainer.appendChild(elCancel); + elLabelTD.appendChild(elContainer); elEdit.disabled = true; elDelete.disabled = true; @@ -281,19 +306,15 @@ window.Joomla = window.Joomla || {}; * Delete button * * @param {Element} that The button being clicked - * @param {String} storeID CSS ID for the element storing the configuration in its data - * properties */ // eslint-disable-next-line no-unused-vars - Joomla.plgSystemWebauthnDelete = (that, storeID) => { - // Extract the configuration from the store - const elStore = document.getElementById(storeID); - - if (!elStore) { + Joomla.plgSystemWebauthnDelete = (that) => { + if (!window.confirm(Joomla.Text._('JGLOBAL_CONFIRM_DELETE'))) { return false; } - const postURL = atob(elStore.dataset.postback_url); + const paths = Joomla.getOptions('system.paths'); + const postURL = `${paths ? `${paths.base}/index.php` : window.location.pathname}`; // Find the UI elements const elTR = that.parentElement.parentElement; @@ -317,6 +338,7 @@ window.Joomla = window.Joomla || {}; akaction: 'delete', credential_id: credentialId, }; + postBackData[Joomla.getOptions('csrf.token')] = 1; Joomla.request({ url: postURL, @@ -354,6 +376,45 @@ window.Joomla = window.Joomla || {}; return false; }; + Joomla.plgSystemWebauthnReactivateTooltips = () => { + const tooltips = Joomla.getOptions('bootstrap.tooltip'); + if (typeof tooltips === 'object' && tooltips !== null) { + Object.keys(tooltips).forEach((tooltip) => { + const opt = tooltips[tooltip]; + const options = { + animation: opt.animation ? opt.animation : true, + container: opt.container ? opt.container : false, + delay: opt.delay ? opt.delay : 0, + html: opt.html ? opt.html : false, + selector: opt.selector ? opt.selector : false, + trigger: opt.trigger ? opt.trigger : 'hover focus', + fallbackPlacement: opt.fallbackPlacement ? opt.fallbackPlacement : null, + boundary: opt.boundary ? opt.boundary : 'clippingParents', + title: opt.title ? opt.title : '', + customClass: opt.customClass ? opt.customClass : '', + sanitize: opt.sanitize ? opt.sanitize : true, + sanitizeFn: opt.sanitizeFn ? opt.sanitizeFn : null, + popperConfig: opt.popperConfig ? opt.popperConfig : null, + }; + + if (opt.placement) { + options.placement = opt.placement; + } + if (opt.template) { + options.template = opt.template; + } + if (opt.allowList) { + options.allowList = opt.allowList; + } + + const elements = Array.from(document.querySelectorAll(tooltip)); + if (elements.length) { + elements.map((el) => new window.bootstrap.Tooltip(el, options)); + } + }); + } + }; + /** * Add New Authenticator button click handler * @@ -364,7 +425,7 @@ window.Joomla = window.Joomla || {}; Joomla.plgSystemWebauthnAddOnClick = (event) => { event.preventDefault(); - Joomla.plgSystemWebauthnCreateCredentials(event.currentTarget.getAttribute('data-random-id'), '#plg_system_webauthn-management-interface'); + Joomla.plgSystemWebauthnInitCreateCredentials(); return false; }; @@ -379,7 +440,7 @@ window.Joomla = window.Joomla || {}; Joomla.plgSystemWebauthnEditOnClick = (event) => { event.preventDefault(); - Joomla.plgSystemWebauthnEditLabel(event.currentTarget, event.currentTarget.getAttribute('data-random-id')); + Joomla.plgSystemWebauthnEditLabel(event.currentTarget); return false; }; @@ -394,7 +455,7 @@ window.Joomla = window.Joomla || {}; Joomla.plgSystemWebauthnDeleteOnClick = (event) => { event.preventDefault(); - Joomla.plgSystemWebauthnDelete(event.currentTarget, event.currentTarget.getAttribute('data-random-id')); + Joomla.plgSystemWebauthnDelete(event.currentTarget); return false; }; diff --git a/composer.json b/composer.json index b36efc65d95b3..15a080c3503bd 100644 --- a/composer.json +++ b/composer.json @@ -94,7 +94,9 @@ "web-auth/webauthn-lib": "2.1.*", "composer/ca-bundle": "^1.2", "dragonmantank/cron-expression": "^3.1", - "enshrined/svg-sanitize": "^0.15.4" + "enshrined/svg-sanitize": "^0.15.4", + "lcobucci/jwt": "^3.4.6", + "web-token/signature-pack": "^2.2.11" }, "require-dev": { "phpunit/phpunit": "^8.5", diff --git a/composer.lock b/composer.lock index 71d70731a7558..73645bad93777 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e02971ec0a050805d6d4f8e175c3876e", + "content-hash": "13763190f851172e079da1acab56a59b", "packages": [ { "name": "algo26-matthias/idna-convert", @@ -2423,6 +2423,83 @@ ], "time": "2020-09-14T14:23:00+00:00" }, + { + "name": "lcobucci/jwt", + "version": "3.4.6", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "3ef8657a78278dfeae7707d51747251db4176240" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/3ef8657a78278dfeae7707d51747251db4176240", + "reference": "3ef8657a78278dfeae7707d51747251db4176240", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-openssl": "*", + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "mikey179/vfsstream": "~1.5", + "phpmd/phpmd": "~2.2", + "phpunit/php-invoker": "~1.1", + "phpunit/phpunit": "^5.7 || ^7.3", + "squizlabs/php_codesniffer": "~2.3" + }, + "suggest": { + "lcobucci/clock": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "files": [ + "compat/class-aliases.php", + "compat/json-exception-polyfill.php", + "compat/lcobucci-clock-polyfill.php" + ], + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Otávio Cobucci Oblonczyk", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "support": { + "issues": "https://github.com/lcobucci/jwt/issues", + "source": "https://github.com/lcobucci/jwt/tree/3.4.6" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2021-09-28T19:18:28+00:00" + }, { "name": "maximebf/debugbar", "version": "dev-master", @@ -5315,6 +5392,614 @@ }, "time": "2019-09-09T12:04:09+00:00" }, + { + "name": "web-token/jwt-core", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-core.git", + "reference": "53beb6f6c1eec4fa93c1c3e5d9e5701e71fa1678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-core/zipball/53beb6f6c1eec4fa93c1c3e5d9e5701e71fa1678", + "reference": "53beb6f6c1eec4fa93c1c3e5d9e5701e71fa1678", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.17|^0.9", + "ext-json": "*", + "ext-mbstring": "*", + "fgrosse/phpasn1": "^2.0", + "php": ">=7.2", + "spomky-labs/base64url": "^1.0|^2.0" + }, + "conflict": { + "spomky-labs/jose": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Core\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "Core component of the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-core/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2021-03-17T14:55:52+00:00" + }, + { + "name": "web-token/jwt-signature", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature.git", + "reference": "015b59aaf3b6e8fb9f5bd1338845b7464c7d8103" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature/zipball/015b59aaf3b6e8fb9f5bd1338845b7464c7d8103", + "reference": "015b59aaf3b6e8fb9f5bd1338845b7464c7d8103", + "shasum": "" + }, + "require": { + "web-token/jwt-core": "^2.1" + }, + "suggest": { + "web-token/jwt-signature-algorithm-ecdsa": "ECDSA Based Signature Algorithms", + "web-token/jwt-signature-algorithm-eddsa": "EdDSA Based Signature Algorithms", + "web-token/jwt-signature-algorithm-experimental": "Experimental Signature Algorithms", + "web-token/jwt-signature-algorithm-hmac": "HMAC Based Signature Algorithms", + "web-token/jwt-signature-algorithm-none": "None Signature Algorithm", + "web-token/jwt-signature-algorithm-rsa": "RSA Based Signature Algorithms" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Signature\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-signature/contributors" + } + ], + "description": "Signature component of the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2021-03-01T19:55:28+00:00" + }, + { + "name": "web-token/jwt-signature-algorithm-ecdsa", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature-algorithm-ecdsa.git", + "reference": "44cbbb4374c51f1cf48b82ae761efbf24e1a8591" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature-algorithm-ecdsa/zipball/44cbbb4374c51f1cf48b82ae761efbf24e1a8591", + "reference": "44cbbb4374c51f1cf48b82ae761efbf24e1a8591", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "web-token/jwt-signature": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Signature\\Algorithm\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "ECDSA Based Signature Algorithms the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature-algorithm-ecdsa/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2021-01-21T19:18:03+00:00" + }, + { + "name": "web-token/jwt-signature-algorithm-eddsa", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature-algorithm-eddsa.git", + "reference": "b805ecca593c56e60e0463bd2cacc9b1341910f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature-algorithm-eddsa/zipball/b805ecca593c56e60e0463bd2cacc9b1341910f6", + "reference": "b805ecca593c56e60e0463bd2cacc9b1341910f6", + "shasum": "" + }, + "require": { + "ext-sodium": "*", + "web-token/jwt-signature": "^2.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Signature\\Algorithm\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "EdDSA Signature Algorithm the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature-algorithm-eddsa/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2021-01-21T19:18:03+00:00" + }, + { + "name": "web-token/jwt-signature-algorithm-experimental", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature-algorithm-experimental.git", + "reference": "b84ea38f9361d68806f100f091db17c1cde6f96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature-algorithm-experimental/zipball/b84ea38f9361d68806f100f091db17c1cde6f96c", + "reference": "b84ea38f9361d68806f100f091db17c1cde6f96c", + "shasum": "" + }, + "require": { + "web-token/jwt-signature-algorithm-hmac": "^2.1", + "web-token/jwt-signature-algorithm-rsa": "^2.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Signature\\Algorithm\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "Experimental Signature Algorithms the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature-algorithm-experimental/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2021-01-21T19:18:03+00:00" + }, + { + "name": "web-token/jwt-signature-algorithm-hmac", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature-algorithm-hmac.git", + "reference": "d208b1c50b408fa711bfeedeed9fb5d9be1d3080" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature-algorithm-hmac/zipball/d208b1c50b408fa711bfeedeed9fb5d9be1d3080", + "reference": "d208b1c50b408fa711bfeedeed9fb5d9be1d3080", + "shasum": "" + }, + "require": { + "web-token/jwt-signature": "^2.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Signature\\Algorithm\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "HMAC Based Signature Algorithms the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature-algorithm-hmac/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2021-01-21T19:18:03+00:00" + }, + { + "name": "web-token/jwt-signature-algorithm-none", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature-algorithm-none.git", + "reference": "c78319392e12e30678eb17d78f16031b5b768388" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature-algorithm-none/zipball/c78319392e12e30678eb17d78f16031b5b768388", + "reference": "c78319392e12e30678eb17d78f16031b5b768388", + "shasum": "" + }, + "require": { + "web-token/jwt-signature": "^2.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Signature\\Algorithm\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "None Signature Algorithm the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature-algorithm-none/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2021-01-21T19:18:03+00:00" + }, + { + "name": "web-token/jwt-signature-algorithm-rsa", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature-algorithm-rsa.git", + "reference": "513ad90eb5ef1886ff176727a769bda4618141b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature-algorithm-rsa/zipball/513ad90eb5ef1886ff176727a769bda4618141b0", + "reference": "513ad90eb5ef1886ff176727a769bda4618141b0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.17|^0.9", + "ext-openssl": "*", + "web-token/jwt-signature": "^2.1" + }, + "suggest": { + "ext-bcmath": "GMP or BCMath is highly recommended to improve the library performance", + "ext-gmp": "GMP or BCMath is highly recommended to improve the library performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Signature\\Algorithm\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "RSA Based Signature Algorithms the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature-algorithm-rsa/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2021-01-21T19:18:03+00:00" + }, + { + "name": "web-token/signature-pack", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/signature-pack.git", + "reference": "13fd2709a95a8a6a0943e33a537af8088760c6c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/signature-pack/zipball/13fd2709a95a8a6a0943e33a537af8088760c6c0", + "reference": "13fd2709a95a8a6a0943e33a537af8088760c6c0", + "shasum": "" + }, + "require": { + "web-token/jwt-signature-algorithm-ecdsa": "^2.0", + "web-token/jwt-signature-algorithm-eddsa": "^2.0", + "web-token/jwt-signature-algorithm-experimental": "^2.0", + "web-token/jwt-signature-algorithm-hmac": "^2.0", + "web-token/jwt-signature-algorithm-none": "^2.0", + "web-token/jwt-signature-algorithm-rsa": "^2.0" + }, + "type": "symfony-pack", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A pack with all signature algorithms for the web-token/jwt-signature package", + "support": { + "source": "https://github.com/web-token/signature-pack/tree/v2.2.1" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2019-06-22T12:27:33+00:00" + }, { "name": "webmozart/assert", "version": "1.10.0", diff --git a/layouts/plugins/system/webauthn/manage.php b/layouts/plugins/system/webauthn/manage.php index 99c3180e6de93..49fc727ac5d46 100644 --- a/layouts/plugins/system/webauthn/manage.php +++ b/layouts/plugins/system/webauthn/manage.php @@ -10,18 +10,15 @@ defined('_JEXEC') or die; use Joomla\CMS\Factory; +use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\FileLayout; -use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\User; -use Joomla\CMS\User\UserHelper; -use Joomla\Plugin\System\Webauthn\Helper\CredentialsCreation; -use Joomla\Plugin\System\Webauthn\Helper\Joomla; +use Webauthn\PublicKeyCredentialSource; /** * Passwordless Login management interface * - * * Generic data * * @var FileLayout $this The Joomla layout renderer @@ -29,10 +26,12 @@ * * Layout specific data * - * @var User $user The Joomla user whose passwordless login we are managing - * @var bool $allow_add Are we allowed to add passwordless login methods - * @var array $credentials The already stored credentials for the user - * @var string $error Any error messages + * @var User $user The Joomla user whose passwordless login we are managing + * @var bool $allow_add Are we allowed to add passwordless login methods + * @var array $credentials The already stored credentials for the user + * @var string $error Any error messages + * @var array $knownAuthenticators Known authenticator metadata + * @var boolean $attestationSupport Is authenticator attestation supported in the plugin? */ // Extract the data. Do not remove until the unset() line. @@ -50,46 +49,36 @@ } $defaultDisplayData = [ - 'user' => $loggedInUser, - 'allow_add' => false, - 'credentials' => [], - 'error' => '', + 'user' => $loggedInUser, + 'allow_add' => false, + 'credentials' => [], + 'error' => '', + 'knownAuthenticators' => [], + 'attestationSupport' => true, ]; extract(array_merge($defaultDisplayData, $displayData)); if ($displayData['allow_add'] === false) { $error = Text::_('PLG_SYSTEM_WEBAUTHN_CANNOT_ADD_FOR_A_USER'); + //phpcs:ignore $allow_add = false; } // Ensure the GMP or BCmath extension is loaded in PHP - as this is required by third party library +//phpcs:ignore if ($allow_add && function_exists('gmp_intval') === false && function_exists('bccomp') === false) { $error = Text::_('PLG_SYSTEM_WEBAUTHN_REQUIRES_GMP'); + //phpcs:ignore $allow_add = false; } -/** - * Why not push these configuration variables directly to JavaScript? - * - * We need to reload them every time we return from an attempt to authorize an authenticator. Whenever that - * happens we push raw HTML to the page. However, any SCRIPT tags in that HTML do not get parsed, i.e. they - * do not replace existing values. This causes any retries to fail. By using a data storage object we circumvent - * that problem. - */ -$randomId = 'plg_system_webauthn_' . UserHelper::genRandomPassword(32); -// phpcs:ignore -$publicKey = $allow_add ? base64_encode(CredentialsCreation::createPublicKey($user)) : '{}'; -$postbackURL = base64_encode(rtrim(Uri::base(), '/') . '/index.php?' . Joomla::getToken() . '=1'); +Text::script('JGLOBAL_CONFIRM_DELETE'); +HTMLHelper::_('bootstrap.tooltip', '.plg_system_webauth-has-tooltip'); ?>
    - -
    @@ -103,7 +92,9 @@ - + colspan="2" scope="col"> + + @@ -111,13 +102,26 @@ + getAaguid() : ''; + $authMetadata = $knownAuthenticators[$aaguid->toString()] ?? $knownAuthenticators['']; + ?> + + <?php echo $authMetadata->description ?> + + - - @@ -141,8 +145,7 @@ diff --git a/libraries/src/Event/CoreEventAware.php b/libraries/src/Event/CoreEventAware.php index ff8b76dc2d702..7657d4684b674 100644 --- a/libraries/src/Event/CoreEventAware.php +++ b/libraries/src/Event/CoreEventAware.php @@ -9,6 +9,13 @@ namespace Joomla\CMS\Event; use Joomla\CMS\Event\Model\BeforeBatchEvent; +use Joomla\CMS\Event\Plugin\System\Webauthn\Ajax as PlgSystemWebauthnAjax; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxChallenge as PlgSystemWebauthnAjaxChallenge; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxCreate as PlgSystemWebauthnAjaxCreate; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxDelete as PlgSystemWebauthnAjaxDelete; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxInitCreate as PlgSystemWebauthnAjaxInitCreate; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxLogin as PlgSystemWebauthnAjaxLogin; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxSaveLabel as PlgSystemWebauthnAjaxSaveLabel; use Joomla\CMS\Event\QuickIcon\GetIconEvent; use Joomla\CMS\Event\Table\AfterBindEvent; use Joomla\CMS\Event\Table\AfterCheckinEvent; @@ -97,6 +104,14 @@ trait CoreEventAware 'onWorkflowFunctionalityUsed' => WorkflowFunctionalityUsedEvent::class, 'onWorkflowAfterTransition' => WorkflowTransitionEvent::class, 'onWorkflowBeforeTransition' => WorkflowTransitionEvent::class, + // Plugin: System, WebAuthn + 'onAjaxWebauthn' => PlgSystemWebauthnAjax::class, + 'onAjaxWebauthnChallenge' => PlgSystemWebauthnAjaxChallenge::class, + 'onAjaxWebauthnCreate' => PlgSystemWebauthnAjaxCreate::class, + 'onAjaxWebauthnDelete' => PlgSystemWebauthnAjaxDelete::class, + 'onAjaxWebauthnInitcreate' => PlgSystemWebauthnAjaxInitCreate::class, + 'onAjaxWebauthnLogin' => PlgSystemWebauthnAjaxLogin::class, + 'onAjaxWebauthnSavelabel' => PlgSystemWebauthnAjaxSaveLabel::class, ]; /** diff --git a/libraries/src/Event/Plugin/System/Webauthn/Ajax.php b/libraries/src/Event/Plugin/System/Webauthn/Ajax.php new file mode 100644 index 0000000000000..c3b46fd813f2a --- /dev/null +++ b/libraries/src/Event/Plugin/System/Webauthn/Ajax.php @@ -0,0 +1,20 @@ + + * @license General Public License version 2 or later; see LICENSE + */ + +namespace Joomla\CMS\Event\Plugin\System\Webauthn; + +use Joomla\CMS\Event\AbstractImmutableEvent; + +/** + * Concrete event class for the onAjaxWebauthn event + * + * @since __DEPLOY_VERSION__ + */ +class Ajax extends AbstractImmutableEvent +{ +} diff --git a/libraries/src/Event/Plugin/System/Webauthn/AjaxChallenge.php b/libraries/src/Event/Plugin/System/Webauthn/AjaxChallenge.php new file mode 100644 index 0000000000000..b2b657c6f59ef --- /dev/null +++ b/libraries/src/Event/Plugin/System/Webauthn/AjaxChallenge.php @@ -0,0 +1,45 @@ + + * @license General Public License version 2 or later; see LICENSE + */ + +namespace Joomla\CMS\Event\Plugin\System\Webauthn; + +use InvalidArgumentException; +use Joomla\CMS\Event\AbstractImmutableEvent; +use Joomla\CMS\Event\Result\ResultAware; +use Joomla\CMS\Event\Result\ResultAwareInterface; + +/** + * Concrete event class for the onAjaxWebauthnChallenge event + * + * @since __DEPLOY_VERSION__ + */ +class AjaxChallenge extends AbstractImmutableEvent implements ResultAwareInterface +{ + use ResultAware; + + /** + * Make sure the result is valid JSON or boolean false + * + * @param mixed $data The data to check + * + * @return void + * @since __DEPLOY_VERSION__ + */ + public function typeCheckResult($data): void + { + if ($data === false) + { + return; + } + + if (!is_string($data) || @json_decode($data) === null) + { + throw new InvalidArgumentException(sprintf('Event %s only accepts JSON results.', $this->getName())); + } + } +} diff --git a/libraries/src/Event/Plugin/System/Webauthn/AjaxCreate.php b/libraries/src/Event/Plugin/System/Webauthn/AjaxCreate.php new file mode 100644 index 0000000000000..6a7f3bc6aac4f --- /dev/null +++ b/libraries/src/Event/Plugin/System/Webauthn/AjaxCreate.php @@ -0,0 +1,25 @@ + + * @license General Public License version 2 or later; see LICENSE + */ + +namespace Joomla\CMS\Event\Plugin\System\Webauthn; + +use Joomla\CMS\Event\AbstractImmutableEvent; +use Joomla\CMS\Event\Result\ResultAware; +use Joomla\CMS\Event\Result\ResultAwareInterface; +use Joomla\CMS\Event\Result\ResultTypeStringAware; + +/** + * Concrete event class for the onAjaxWebauthnCreate event + * + * @since __DEPLOY_VERSION__ + */ +class AjaxCreate extends AbstractImmutableEvent implements ResultAwareInterface +{ + use ResultAware; + use ResultTypeStringAware; +} diff --git a/libraries/src/Event/Plugin/System/Webauthn/AjaxDelete.php b/libraries/src/Event/Plugin/System/Webauthn/AjaxDelete.php new file mode 100644 index 0000000000000..a86c2bab0609a --- /dev/null +++ b/libraries/src/Event/Plugin/System/Webauthn/AjaxDelete.php @@ -0,0 +1,25 @@ + + * @license General Public License version 2 or later; see LICENSE + */ + +namespace Joomla\CMS\Event\Plugin\System\Webauthn; + +use Joomla\CMS\Event\AbstractImmutableEvent; +use Joomla\CMS\Event\Result\ResultAware; +use Joomla\CMS\Event\Result\ResultAwareInterface; +use Joomla\CMS\Event\Result\ResultTypeBooleanAware; + +/** + * Concrete event class for the onAjaxWebauthnDelete event + * + * @since __DEPLOY_VERSION__ + */ +class AjaxDelete extends AbstractImmutableEvent implements ResultAwareInterface +{ + use ResultAware; + use ResultTypeBooleanAware; +} diff --git a/libraries/src/Event/Plugin/System/Webauthn/AjaxInitCreate.php b/libraries/src/Event/Plugin/System/Webauthn/AjaxInitCreate.php new file mode 100644 index 0000000000000..5dec092fbb193 --- /dev/null +++ b/libraries/src/Event/Plugin/System/Webauthn/AjaxInitCreate.php @@ -0,0 +1,46 @@ + + * @license General Public License version 2 or later; see LICENSE + */ + +namespace Joomla\CMS\Event\Plugin\System\Webauthn; + +use Joomla\CMS\Event\AbstractImmutableEvent; +use Joomla\CMS\Event\Result\ResultAware; +use Joomla\CMS\Event\Result\ResultAwareInterface; +use Joomla\CMS\Event\Result\ResultTypeObjectAware; +use Webauthn\PublicKeyCredentialCreationOptions; + +/** + * Concrete event class for the onAjaxWebauthnInitcreate event + * + * @since __DEPLOY_VERSION__ + */ +class AjaxInitCreate extends AbstractImmutableEvent implements ResultAwareInterface +{ + use ResultAware; + use ResultTypeObjectAware; + + /** + * Constructor + * + * @param string $name Event name + * @param array $arguments Event arguments + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(string $name, array $arguments = []) + { + parent::__construct($name, $arguments); + + $this->resultAcceptableClasses = [ + \stdClass::class, + PublicKeyCredentialCreationOptions::class + ]; + } + + +} diff --git a/libraries/src/Event/Plugin/System/Webauthn/AjaxLogin.php b/libraries/src/Event/Plugin/System/Webauthn/AjaxLogin.php new file mode 100644 index 0000000000000..5e84806472a19 --- /dev/null +++ b/libraries/src/Event/Plugin/System/Webauthn/AjaxLogin.php @@ -0,0 +1,21 @@ + + * @license General Public License version 2 or later; see LICENSE + */ + +namespace Joomla\CMS\Event\Plugin\System\Webauthn; + +use Joomla\CMS\Event\AbstractImmutableEvent; + +/** + * Concrete event class for the onAjaxWebauthnLogin event + * + * @since __DEPLOY_VERSION__ + */ +class AjaxLogin extends AbstractImmutableEvent +{ + +} diff --git a/libraries/src/Event/Plugin/System/Webauthn/AjaxSaveLabel.php b/libraries/src/Event/Plugin/System/Webauthn/AjaxSaveLabel.php new file mode 100644 index 0000000000000..377225f0b5294 --- /dev/null +++ b/libraries/src/Event/Plugin/System/Webauthn/AjaxSaveLabel.php @@ -0,0 +1,25 @@ + + * @license General Public License version 2 or later; see LICENSE + */ + +namespace Joomla\CMS\Event\Plugin\System\Webauthn; + +use Joomla\CMS\Event\AbstractImmutableEvent; +use Joomla\CMS\Event\Result\ResultAware; +use Joomla\CMS\Event\Result\ResultAwareInterface; +use Joomla\CMS\Event\Result\ResultTypeBooleanAware; + +/** + * Concrete event class for the onAjaxWebauthnSavelabel event + * + * @since __DEPLOY_VERSION__ + */ +class AjaxSaveLabel extends AbstractImmutableEvent implements ResultAwareInterface +{ + use ResultAware; + use ResultTypeBooleanAware; +} diff --git a/plugins/system/webauthn/services/provider.php b/plugins/system/webauthn/services/provider.php new file mode 100644 index 0000000000000..bb3e639d9c248 --- /dev/null +++ b/plugins/system/webauthn/services/provider.php @@ -0,0 +1,89 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') || die; + +use Joomla\Application\ApplicationInterface; +use Joomla\Application\SessionAwareWebApplicationInterface; +use Joomla\CMS\Application\CMSApplicationInterface; +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Joomla\Plugin\System\Webauthn\Authentication; +use Joomla\Plugin\System\Webauthn\CredentialRepository; +use Joomla\Plugin\System\Webauthn\Extension\Webauthn; +use Joomla\Plugin\System\Webauthn\MetadataRepository; +use Webauthn\MetadataService\MetadataStatementRepository; +use Webauthn\PublicKeyCredentialSourceRepository; + +return new class implements ServiceProviderInterface { + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function register(Container $container) + { + $container->set( + PluginInterface::class, + function (Container $container) { + $config = (array) PluginHelper::getPlugin('system', 'webauthn'); + $subject = $container->get(DispatcherInterface::class); + + $app = Factory::getApplication(); + $session = $container->has('session') ? $container->get('session') : $this->getSession($app); + + $db = $container->get('DatabaseDriver'); + $credentialsRepository = $container->has(PublicKeyCredentialSourceRepository::class) + ? $container->get(PublicKeyCredentialSourceRepository::class) + : new CredentialRepository($db); + + $metadataRepository = null; + $params = new Joomla\Registry\Registry($config['params'] ?? '{}'); + + if ($params->get('attestationSupport', 1) == 1) + { + $metadataRepository = $container->has(MetadataStatementRepository::class) + ? $container->get(MetadataStatementRepository::class) + : new MetadataRepository; + } + + $authenticationHelper = $container->has(Authentication::class) + ? $container->get(Authentication::class) + : new Authentication($app, $session, $credentialsRepository, $metadataRepository); + + $plugin = new Webauthn($subject, $config, $authenticationHelper); + $plugin->setApplication($app); + + return $plugin; + } + ); + } + + /** + * Get the current application session object + * + * @param ApplicationInterface $app The application we are running in + * + * @return \Joomla\Session\SessionInterface|null + * + * @since __DEPLOY_VERSION__ + */ + private function getSession(ApplicationInterface $app) + { + return $app instanceof SessionAwareWebApplicationInterface ? $app->getSession() : null; + } +}; diff --git a/plugins/system/webauthn/src/Authentication.php b/plugins/system/webauthn/src/Authentication.php new file mode 100644 index 0000000000000..eed39e452f2ad --- /dev/null +++ b/plugins/system/webauthn/src/Authentication.php @@ -0,0 +1,570 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Plugin\System\Webauthn; + +// Protect from unauthorized access +\defined('_JEXEC') or die(); + +use Exception; +use Joomla\Application\ApplicationInterface; +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Factory; +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Log\Log; +use Joomla\CMS\Uri\Uri; +use Joomla\CMS\User\User; +use Joomla\Plugin\System\Webauthn\Hotfix\Server; +use Joomla\Session\SessionInterface; +use Laminas\Diactoros\ServerRequestFactory; +use RuntimeException; +use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs; +use Webauthn\AuthenticatorSelectionCriteria; +use Webauthn\MetadataService\MetadataStatementRepository; +use Webauthn\PublicKeyCredentialCreationOptions; +use Webauthn\PublicKeyCredentialDescriptor; +use Webauthn\PublicKeyCredentialRequestOptions; +use Webauthn\PublicKeyCredentialRpEntity; +use Webauthn\PublicKeyCredentialSource; +use Webauthn\PublicKeyCredentialSourceRepository; +use Webauthn\PublicKeyCredentialUserEntity; + +/** + * Helper class to aid in credentials creation (link an authenticator to a user account) + * + * @since __DEPLOY_VERSION__ + * @internal + */ +final class Authentication +{ + /** + * The credentials repository + * + * @var CredentialRepository + * @since __DEPLOY_VERSION__ + */ + private $credentialsRepository; + + /** + * The application we are running in. + * + * @var CMSApplication + * @since __DEPLOY_VERSION__ + */ + private $app; + + /** + * The application session + * + * @var SessionInterface + * @since __DEPLOY_VERSION__ + */ + private $session; + + /** + * A simple metadata statement repository + * + * @var MetadataStatementRepository + * @since __DEPLOY_VERSION__ + */ + private $metadataRepository; + + /** + * Should I permit attestation support if a Metadata Statement Repository object is present and + * non-empty? + * + * @var boolean + * @since __DEPLOY_VERSION__ + */ + private $attestationSupport = true; + + /** + * Public constructor. + * + * @param ApplicationInterface|null $app The app we are running in + * @param SessionInterface|null $session The app session object + * @param PublicKeyCredentialSourceRepository|null $credRepo Credentials repo + * @param MetadataStatementRepository|null $mdsRepo Authenticator metadata repo + * + * @since __DEPLOY_VERSION__ + */ + public function __construct( + ApplicationInterface $app = null, + SessionInterface $session = null, + PublicKeyCredentialSourceRepository $credRepo = null, + ?MetadataStatementRepository $mdsRepo = null + ) + { + $this->app = $app; + $this->session = $session; + $this->credentialsRepository = $credRepo; + $this->metadataRepository = $mdsRepo; + } + + /** + * Get the known FIDO authenticators and their metadata + * + * @return object[] + * @since __DEPLOY_VERSION__ + */ + public function getKnownAuthenticators(): array + { + $return = (!empty($this->metadataRepository) && method_exists($this->metadataRepository, 'getKnownAuthenticators')) + ? $this->metadataRepository->getKnownAuthenticators() + : []; + + // Add a generic authenticator entry + $image = HTMLHelper::_('image', 'plg_system_webauthn/fido.png', '', '', true, true); + $image = $image ? JPATH_ROOT . substr($image, \strlen(Uri::root(true))) : (JPATH_BASE . '/media/plg_system_webauthn/images/fido.png'); + $image = file_exists($image) ? file_get_contents($image) : ''; + + $return[''] = (object) [ + 'description' => Text::_('PLG_SYSTEM_WEBAUTHN_LBL_DEFAULT_AUTHENTICATOR'), + 'icon' => 'data:image/png;base64,' . base64_encode($image) + ]; + + return $return; + } + + /** + * Returns the Public Key credential source repository object + * + * @return PublicKeyCredentialSourceRepository|null + * + * @since __DEPLOY_VERSION__ + */ + public function getCredentialsRepository(): ?PublicKeyCredentialSourceRepository + { + return $this->credentialsRepository; + } + + /** + * Returns the authenticator metadata repository object + * + * @return MetadataStatementRepository|null + * + * @since __DEPLOY_VERSION__ + */ + public function getMetadataRepository(): ?MetadataStatementRepository + { + return $this->metadataRepository; + } + + /** + * Generate the public key creation options. + * + * This is used for the first step of attestation (key registration). + * + * The PK creation options and the user ID are stored in the session. + * + * @param User $user The Joomla user to create the public key for + * + * @return PublicKeyCredentialCreationOptions + * + * @throws Exception + * @since __DEPLOY_VERSION__ + */ + public function getPubKeyCreationOptions(User $user): PublicKeyCredentialCreationOptions + { + /** + * We will only ask for attestation information if our MDS is guaranteed not empty. + * + * We check that by trying to load a known good AAGUID (Yubico Security Key NFC). If it's + * missing, we have failed to load the MDS data e.g. we could not contact the server, it + * was taking too long, the cache is unwritable etc. In this case asking for attestation + * conveyance would cause the attestation to fail (since we cannot verify its signature). + * Therefore we have to ask for no attestation to be conveyed. The downside is that in this + * case we do not have any information about the make and model of the authenticator. So be + * it! After all, that's a convenience feature for us. + */ + $attestationMode = $this->hasAttestationSupport() + ? PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT + : PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE; + + $publicKeyCredentialCreationOptions = $this->getWebauthnServer()->generatePublicKeyCredentialCreationOptions( + $this->getUserEntity($user), + $attestationMode, + $this->getPubKeyDescriptorsForUser($user), + new AuthenticatorSelectionCriteria( + AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE, + false, + AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED + ), + new AuthenticationExtensionsClientInputs + ); + + // Save data in the session + $this->session->set('plg_system_webauthn.publicKeyCredentialCreationOptions', base64_encode(serialize($publicKeyCredentialCreationOptions))); + $this->session->set('plg_system_webauthn.registration_user_id', $user->id); + + return $publicKeyCredentialCreationOptions; + } + + /** + * Get the public key request options. + * + * This is used in the first step of the assertion (login) flow. + * + * @param User $user The Joomla user to get the PK request options for + * + * @return PublicKeyCredentialRequestOptions + * + * @throws Exception + * @since __DEPLOY_VERSION__ + */ + public function getPubkeyRequestOptions(User $user): ?PublicKeyCredentialRequestOptions + { + Log::add('Creating PK request options', Log::DEBUG, 'webauthn.system'); + $publicKeyCredentialRequestOptions = $this->getWebauthnServer()->generatePublicKeyCredentialRequestOptions( + PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, + $this->getPubKeyDescriptorsForUser($user) + ); + + // Save in session. This is used during the verification stage to prevent replay attacks. + $this->session->set('plg_system_webauthn.publicKeyCredentialRequestOptions', base64_encode(serialize($publicKeyCredentialRequestOptions))); + + return $publicKeyCredentialRequestOptions; + } + + /** + * Validate the authenticator assertion. + * + * This is used in the second step of the assertion (login) flow. The server verifies that the + * assertion generated by the authenticator has not been tampered with. + * + * @param string $data The data + * @param User $user The user we are trying to log in + * + * @return PublicKeyCredentialSource + * + * @throws Exception + * @since __DEPLOY_VERSION__ + */ + public function validateAssertionResponse(string $data, User $user): PublicKeyCredentialSource + { + // Make sure the public key credential request options in the session are valid + $encodedPkOptions = $this->session->get('plg_system_webauthn.publicKeyCredentialRequestOptions', null); + $serializedOptions = base64_decode($encodedPkOptions); + $publicKeyCredentialRequestOptions = unserialize($serializedOptions); + + if (!is_object($publicKeyCredentialRequestOptions) + || empty($publicKeyCredentialRequestOptions) + || !($publicKeyCredentialRequestOptions instanceof PublicKeyCredentialRequestOptions)) + { + Log::add('Cannot retrieve valid plg_system_webauthn.publicKeyCredentialRequestOptions from the session', Log::NOTICE, 'webauthn.system'); + throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); + } + + $data = base64_decode($data); + + if (empty($data)) + { + Log::add('No or invalid assertion data received from the browser', Log::NOTICE, 'webauthn.system'); + + throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); + } + + return $this->getWebauthnServer()->loadAndCheckAssertionResponse( + $data, + $this->getPKCredentialRequestOptions(), + $this->getUserEntity($user), + ServerRequestFactory::fromGlobals() + ); + } + + /** + * Validate the authenticator attestation. + * + * This is used for the second step of attestation (key registration), when the user has + * interacted with the authenticator and we need to validate the legitimacy of its response. + * + * An exception will be returned on error. Also, under very rare conditions, you may receive + * NULL instead of a PublicKeyCredentialSource object which means that something was off in the + * returned data from the browser. + * + * @param string $data The data + * + * @return PublicKeyCredentialSource|null + * + * @throws Exception + * @since __DEPLOY_VERSION__ + */ + public function validateAttestationResponse(string $data): PublicKeyCredentialSource + { + // Retrieve the PublicKeyCredentialCreationOptions object created earlier and perform sanity checks + $encodedOptions = $this->session->get('plg_system_webauthn.publicKeyCredentialCreationOptions', null); + + if (empty($encodedOptions)) + { + Log::add('Cannot retrieve plg_system_webauthn.publicKeyCredentialCreationOptions from the session', Log::NOTICE, 'webauthn.system'); + + throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_NO_PK')); + } + + /** @var PublicKeyCredentialCreationOptions|null $publicKeyCredentialCreationOptions */ + try + { + $publicKeyCredentialCreationOptions = unserialize(base64_decode($encodedOptions)); + } + catch (Exception $e) + { + Log::add('The plg_system_webauthn.publicKeyCredentialCreationOptions in the session is invalid', Log::NOTICE, 'webauthn.system'); + $publicKeyCredentialCreationOptions = null; + } + + if (!is_object($publicKeyCredentialCreationOptions) || !($publicKeyCredentialCreationOptions instanceof PublicKeyCredentialCreationOptions)) + { + throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_NO_PK')); + } + + // Retrieve the stored user ID and make sure it's the same one in the request. + $storedUserId = $this->session->get('plg_system_webauthn.registration_user_id', 0); + $myUser = $this->app->getIdentity() ?? new User; + $myUserId = $myUser->id; + + if (($myUser->guest) || ($myUserId != $storedUserId)) + { + $message = sprintf('Invalid user! We asked the authenticator to attest user ID %d, the current user ID is %d', $storedUserId, $myUserId); + Log::add($message, Log::NOTICE, 'webauthn.system'); + + throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_USER')); + } + + // We init the PSR-7 request object using Diactoros + return $this->getWebauthnServer()->loadAndCheckAttestationResponse( + base64_decode($data), + $publicKeyCredentialCreationOptions, + ServerRequestFactory::fromGlobals() + ); + } + + /** + * Get the authentiactor attestation support. + * + * @return boolean + * @since __DEPLOY_VERSION__ + */ + public function hasAttestationSupport(): bool + { + return $this->attestationSupport + && ($this->metadataRepository instanceof MetadataStatementRepository) + && $this->metadataRepository->findOneByAAGUID('6d44ba9b-f6ec-2e49-b930-0c8fe920cb73'); + } + + /** + * Change the authenticator attestation support. + * + * @param bool $attestationSupport The desired setting + * + * @return void + * @since __DEPLOY_VERSION__ + */ + public function setAttestationSupport(bool $attestationSupport): void + { + $this->attestationSupport = $attestationSupport; + } + + /** + * Try to find the site's favicon in the site's root, images, media, templates or current + * template directory. + * + * @return string|null + * + * @since __DEPLOY_VERSION__ + */ + private function getSiteIcon(): ?string + { + $filenames = [ + 'apple-touch-icon.png', + 'apple_touch_icon.png', + 'favicon.ico', + 'favicon.png', + 'favicon.gif', + 'favicon.bmp', + 'favicon.jpg', + 'favicon.svg', + ]; + + try + { + $paths = [ + '/', + '/images/', + '/media/', + '/templates/', + '/templates/' . $this->app->getTemplate(), + ]; + } + catch (Exception $e) + { + return null; + } + + foreach ($paths as $path) + { + foreach ($filenames as $filename) + { + $relFile = $path . $filename; + $filePath = JPATH_BASE . $relFile; + + if (is_file($filePath)) + { + break 2; + } + + $relFile = null; + } + } + + if (!isset($relFile) || \is_null($relFile)) + { + return null; + } + + return rtrim(Uri::base(), '/') . '/' . ltrim($relFile, '/'); + } + + /** + * Returns a User Entity object given a Joomla user + * + * @param User $user The Joomla user to get the user entity for + * + * @return PublicKeyCredentialUserEntity + * + * @since __DEPLOY_VERSION__ + */ + private function getUserEntity(User $user): PublicKeyCredentialUserEntity + { + $repository = $this->credentialsRepository; + + return new PublicKeyCredentialUserEntity( + $user->username, + $repository->getHandleFromUserId($user->id), + $user->name, + $this->getAvatar($user, 64) + ); + } + + /** + * Get the user's avatar (through Gravatar) + * + * @param User $user The Joomla user object + * @param int $size The dimensions of the image to fetch (default: 64 pixels) + * + * @return string The URL to the user's avatar + * + * @since __DEPLOY_VERSION__ + */ + private function getAvatar(User $user, int $size = 64) + { + $scheme = Uri::getInstance()->getScheme(); + $subdomain = ($scheme == 'https') ? 'secure' : 'www'; + + return sprintf('%s://%s.gravatar.com/avatar/%s.jpg?s=%u&d=mm', $scheme, $subdomain, md5($user->email), $size); + } + + /** + * Returns an array of the PK credential descriptors (registered authenticators) for the given + * user. + * + * @param User $user The Joomla user to get the PK descriptors for + * + * @return PublicKeyCredentialDescriptor[] + * + * @since __DEPLOY_VERSION__ + */ + private function getPubKeyDescriptorsForUser(User $user): array + { + $userEntity = $this->getUserEntity($user); + $repository = $this->credentialsRepository; + $descriptors = []; + $records = $repository->findAllForUserEntity($userEntity); + + foreach ($records as $record) + { + $descriptors[] = $record->getPublicKeyCredentialDescriptor(); + } + + return $descriptors; + } + + /** + * Retrieve the public key credential request options saved in the session. + * + * If they do not exist or are corrupt it is a hacking attempt and we politely tell the + * attacker to go away. + * + * @return PublicKeyCredentialRequestOptions + * + * @throws Exception + * @since __DEPLOY_VERSION__ + */ + private function getPKCredentialRequestOptions(): PublicKeyCredentialRequestOptions + { + $encodedOptions = $this->session->get('plg_system_webauthn.publicKeyCredentialRequestOptions', null); + + if (empty($encodedOptions)) + { + Log::add('Cannot retrieve plg_system_webauthn.publicKeyCredentialRequestOptions from the session', Log::NOTICE, 'webauthn.system'); + + throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); + } + + try + { + $publicKeyCredentialRequestOptions = unserialize(base64_decode($encodedOptions)); + } + catch (Exception $e) + { + Log::add('Invalid plg_system_webauthn.publicKeyCredentialRequestOptions in the session', Log::NOTICE, 'webauthn.system'); + + throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); + } + + if (!is_object($publicKeyCredentialRequestOptions) || !($publicKeyCredentialRequestOptions instanceof PublicKeyCredentialRequestOptions)) + { + throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); + } + + return $publicKeyCredentialRequestOptions; + } + + /** + * Get the WebAuthn library's Server object which facilitates WebAuthn operations + * + * @return Server + * @throws Exception + * @since __DEPLOY_VERSION__ + */ + private function getWebauthnServer(): \Webauthn\Server + { + $siteName = $this->app->get('sitename'); + + // Credentials repository + $repository = $this->credentialsRepository; + + // Relaying Party -- Our site + $rpEntity = new PublicKeyCredentialRpEntity( + $siteName, + Uri::getInstance()->toString(['host']), + $this->getSiteIcon() + ); + + $server = new Server($rpEntity, $repository, $this->metadataRepository); + + // Ed25519 is only available with libsodium + if (!function_exists('sodium_crypto_sign_seed_keypair')) + { + $server->setSelectedAlgorithms(['RS256', 'RS512', 'PS256', 'PS512', 'ES256', 'ES512']); + } + + return $server; + } +} diff --git a/plugins/system/webauthn/src/CredentialRepository.php b/plugins/system/webauthn/src/CredentialRepository.php index 87394ca3d30d2..e74d8f33ec75c 100644 --- a/plugins/system/webauthn/src/CredentialRepository.php +++ b/plugins/system/webauthn/src/CredentialRepository.php @@ -14,11 +14,16 @@ use Exception; use InvalidArgumentException; +use Joomla\CMS\Date\Date; use Joomla\CMS\Encrypt\Aes; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; +use Joomla\CMS\User\UserFactoryInterface; +use Joomla\Database\DatabaseAwareInterface; +use Joomla\Database\DatabaseAwareTrait; use Joomla\Database\DatabaseDriver; -use Joomla\Plugin\System\Webauthn\Helper\Joomla; +use Joomla\Database\DatabaseInterface; +use Joomla\Plugin\System\Webauthn\Extension\Webauthn; use Joomla\Registry\Registry; use JsonException; use RuntimeException; @@ -32,8 +37,22 @@ * * @since 4.0.0 */ -class CredentialRepository implements PublicKeyCredentialSourceRepository +final class CredentialRepository implements PublicKeyCredentialSourceRepository, DatabaseAwareInterface { + use DatabaseAwareTrait; + + /** + * Public constructor. + * + * @param DatabaseInterface|null $db The database driver object to use for persistence. + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(DatabaseInterface $db = null) + { + $this->setDatabase($db); + } + /** * Returns a PublicKeyCredentialSource object given the public key credential ID * @@ -46,7 +65,7 @@ class CredentialRepository implements PublicKeyCredentialSourceRepository public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource { /** @var DatabaseDriver $db */ - $db = Factory::getContainer()->get('DatabaseDriver'); + $db = $this->getDatabase(); $credentialId = base64_encode($publicKeyCredentialId); $query = $db->getQuery(true) ->select($db->qn('credential')) @@ -86,7 +105,7 @@ public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKey public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array { /** @var DatabaseDriver $db */ - $db = Factory::getContainer()->get('DatabaseDriver'); + $db = $this->getDatabase(); $userHandle = $publicKeyCredentialUserEntity->getId(); $query = $db->getQuery(true) ->select('*') @@ -123,12 +142,12 @@ public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCre } catch (JsonException $e) { - return; + return null; } if (empty($data)) { - return; + return null; } try @@ -137,7 +156,7 @@ public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCre } catch (InvalidArgumentException $e) { - return; + return null; } }; @@ -177,18 +196,27 @@ public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCre public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void { // Default values for saving a new credential source - $credentialId = base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId()); - $user = Factory::getApplication()->getIdentity(); - $o = (object) [ + /** @var Webauthn $plugin */ + $plugin = Factory::getApplication()->bootPlugin('webauthn', 'system'); + $knownAuthenticators = $plugin->getAuthenticationHelper()->getKnownAuthenticators(); + $aaguid = (string) ($publicKeyCredentialSource->getAaguid() ?? ''); + $defaultName = ($knownAuthenticators[$aaguid] ?? $knownAuthenticators[''])->description; + $credentialId = base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId()); + $user = Factory::getApplication()->getIdentity(); + $o = (object) [ 'id' => $credentialId, 'user_id' => $this->getHandleFromUserId($user->id), - 'label' => Text::sprintf('PLG_SYSTEM_WEBAUTHN_LBL_DEFAULT_AUTHENTICATOR_LABEL', Joomla::formatDate('now')), + 'label' => Text::sprintf( + 'PLG_SYSTEM_WEBAUTHN_LBL_DEFAULT_AUTHENTICATOR_LABEL', + $defaultName, + $this->formatDate('now') + ), 'credential' => json_encode($publicKeyCredentialSource), ]; - $update = false; + $update = false; /** @var DatabaseDriver $db */ - $db = Factory::getContainer()->get('DatabaseDriver'); + $db = $this->getDatabase(); // Try to find an existing record try @@ -259,7 +287,7 @@ public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredent public function getAll(int $userId): array { /** @var DatabaseDriver $db */ - $db = Factory::getContainer()->get('DatabaseDriver'); + $db = $this->getDatabase(); $userHandle = $this->getHandleFromUserId($userId); $query = $db->getQuery(true) ->select('*') @@ -281,7 +309,50 @@ public function getAll(int $userId): array return []; } - return $results; + /** + * Decodes the credentials on each record. + * + * @param array $record The record to convert + * + * @return array + * @since __DEPLOY_VERSION__ + */ + $recordsMapperClosure = function ($record) + { + try + { + $json = $this->decryptCredential($record['credential']); + $data = json_decode($json, true); + } + catch (JsonException $e) + { + $record['credential'] = null; + + return $record; + } + + if (empty($data)) + { + $record['credential'] = null; + + return $record; + } + + try + { + $record['credential'] = PublicKeyCredentialSource::createFromArray($data); + + return $record; + } + catch (InvalidArgumentException $e) + { + $record['credential'] = null; + + return $record; + } + }; + + return array_map($recordsMapperClosure, $results); } /** @@ -296,7 +367,7 @@ public function getAll(int $userId): array public function has(string $credentialId): bool { /** @var DatabaseDriver $db */ - $db = Factory::getContainer()->get('DatabaseDriver'); + $db = $this->getDatabase(); $credentialId = base64_encode($credentialId); $query = $db->getQuery(true) ->select('COUNT(*)') @@ -329,7 +400,7 @@ public function has(string $credentialId): bool public function setLabel(string $credentialId, string $label): void { /** @var DatabaseDriver $db */ - $db = Factory::getContainer()->get('DatabaseDriver'); + $db = $this->getDatabase(); $credentialId = base64_encode($credentialId); $o = (object) [ 'id' => $credentialId, @@ -356,7 +427,7 @@ public function remove(string $credentialId): void } /** @var DatabaseDriver $db */ - $db = Factory::getContainer()->get('DatabaseDriver'); + $db = $this->getDatabase(); $credentialId = base64_encode($credentialId); $query = $db->getQuery(true) ->delete($db->qn('#__webauthn_credentials')) @@ -410,6 +481,105 @@ public function getHandleFromUserId(int $id): string return hash_hmac('sha256', $data, $key, false); } + /** + * Get the user ID from the user handle + * + * This is a VERY inefficient method. Since the user handle is an HMAC-SHA-256 of the user ID we can't just go + * directly from a handle back to an ID. We have to iterate all user IDs, calculate their handles and compare them + * to the given handle. + * + * To prevent a lengthy infinite loop in case of an invalid user handle we don't iterate the entire 2+ billion valid + * 32-bit integer range. We load the user IDs of active users (not blocked, not pending activation) and iterate + * through them. + * + * To avoid memory outage on large sites with thousands of active user records we load up to 10000 users at a time. + * Each block of 10,000 user IDs takes about 60-80 msec to iterate. On a site with 200,000 active users this method + * will take less than 1.5 seconds. This is slow but not impractical, even on crowded shared hosts with a quarter of + * the performance of my test subject (a mid-range, shared hosting server). + * + * @param string|null $userHandle The user handle which will be converted to a user ID. + * + * @return integer|null + * @since __DEPLOY_VERSION__ + */ + public function getUserIdFromHandle(?string $userHandle): ?int + { + if (empty($userHandle)) + { + return null; + } + + /** @var DatabaseDriver $db */ + $db = $this->getDatabase(); + + // Check that the userHandle does exist in the database + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->qn('#__webauthn_credentials')) + ->where($db->qn('user_id') . ' = ' . $db->q($userHandle)); + + try + { + $numRecords = $db->setQuery($query)->loadResult(); + } + catch (Exception $e) + { + return null; + } + + if (is_null($numRecords) || ($numRecords < 1)) + { + return null; + } + + // Prepare the query + $query = $db->getQuery(true) + ->select([$db->qn('id')]) + ->from($db->qn('#__users')) + ->where($db->qn('block') . ' = 0') + ->where( + '(' . + $db->qn('activation') . ' IS NULL OR ' . + $db->qn('activation') . ' = 0 OR ' . + $db->qn('activation') . ' = ' . $db->q('') . + ')' + ); + + $key = $this->getEncryptionKey(); + $start = 0; + $limit = 10000; + + while (true) + { + try + { + $ids = $db->setQuery($query, $start, $limit)->loadColumn(); + } + catch (Exception $e) + { + return null; + } + + if (empty($ids)) + { + return null; + } + + foreach ($ids as $userId) + { + $data = sprintf('%010u', $userId); + $thisHandle = hash_hmac('sha256', $data, $key, false); + + if ($thisHandle == $userHandle) + { + return $userId; + } + } + + $start += $limit; + } + } + /** * Encrypt the credential source before saving it to the database * @@ -485,4 +655,67 @@ private function getEncryptionKey(): string return $secret; } + + /** + * Format a date for display. + * + * The $tzAware parameter defines whether the formatted date will be timezone-aware. If set to false the formatted + * date will be rendered in the UTC timezone. If set to true the code will automatically try to use the logged in + * user's timezone or, if none is set, the site's default timezone (Server Timezone). If set to a positive integer + * the same thing will happen but for the specified user ID instead of the currently logged in user. + * + * @param string|\DateTime $date The date to format + * @param string|null $format The format string, default is Joomla's DATE_FORMAT_LC6 (usually "Y-m-d + * H:i:s") + * @param bool $tzAware Should the format be timezone aware? See notes above. + * + * @return string + * @since __DEPLOY_VERSION__ + */ + private function formatDate($date, ?string $format = null, bool $tzAware = true): string + { + $utcTimeZone = new \DateTimeZone('UTC'); + $jDate = new Date($date, $utcTimeZone); + + // Which timezone should I use? + $tz = null; + + if ($tzAware !== false) + { + $userId = is_bool($tzAware) ? null : (int) $tzAware; + + try + { + $tzDefault = Factory::getApplication()->get('offset'); + } + catch (\Exception $e) + { + $tzDefault = 'GMT'; + } + + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId ?? 0); + $tz = $user->getParam('timezone', $tzDefault); + } + + if (!empty($tz)) + { + try + { + $userTimeZone = new \DateTimeZone($tz); + + $jDate->setTimezone($userTimeZone); + } + catch (\Exception $e) + { + // Nothing. Fall back to UTC. + } + } + + if (empty($format)) + { + $format = Text::_('DATE_FORMAT_LC6'); + } + + return $jDate->format($format, true); + } } diff --git a/plugins/system/webauthn/src/Exception/AjaxNonCmsAppException.php b/plugins/system/webauthn/src/Exception/AjaxNonCmsAppException.php deleted file mode 100644 index fcf3e3f98f615..0000000000000 --- a/plugins/system/webauthn/src/Exception/AjaxNonCmsAppException.php +++ /dev/null @@ -1,24 +0,0 @@ - - * @license GNU General Public License version 2 or later; see LICENSE.txt - */ - -namespace Joomla\Plugin\System\Webauthn\Exception; - -// Protect from unauthorized access -\defined('_JEXEC') or die(); - -use RuntimeException; - -/** - * Exception indicating that the Joomla application object is not a CMSApplication subclass. - * - * @since 4.0.0 - */ -class AjaxNonCmsAppException extends RuntimeException -{ -} diff --git a/plugins/system/webauthn/src/Extension/Webauthn.php b/plugins/system/webauthn/src/Extension/Webauthn.php new file mode 100644 index 0000000000000..787b41d3699bd --- /dev/null +++ b/plugins/system/webauthn/src/Extension/Webauthn.php @@ -0,0 +1,189 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Plugin\System\Webauthn\Extension; + +// Protect from unauthorized access +defined('_JEXEC') or die(); + +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Application\CMSApplicationInterface; +use Joomla\CMS\Event\CoreEventAware; +use Joomla\CMS\Factory; +use Joomla\CMS\Log\Log; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Database\DatabaseAwareInterface; +use Joomla\Database\DatabaseAwareTrait; +use Joomla\Database\DatabaseDriver; +use Joomla\Event\DispatcherInterface; +use Joomla\Event\SubscriberInterface; +use Joomla\Plugin\System\Webauthn\Authentication; +use Joomla\Plugin\System\Webauthn\PluginTraits\AdditionalLoginButtons; +use Joomla\Plugin\System\Webauthn\PluginTraits\AjaxHandler; +use Joomla\Plugin\System\Webauthn\PluginTraits\AjaxHandlerChallenge; +use Joomla\Plugin\System\Webauthn\PluginTraits\AjaxHandlerCreate; +use Joomla\Plugin\System\Webauthn\PluginTraits\AjaxHandlerDelete; +use Joomla\Plugin\System\Webauthn\PluginTraits\AjaxHandlerInitCreate; +use Joomla\Plugin\System\Webauthn\PluginTraits\AjaxHandlerLogin; +use Joomla\Plugin\System\Webauthn\PluginTraits\AjaxHandlerSaveLabel; +use Joomla\Plugin\System\Webauthn\PluginTraits\EventReturnAware; +use Joomla\Plugin\System\Webauthn\PluginTraits\UserDeletion; +use Joomla\Plugin\System\Webauthn\PluginTraits\UserProfileFields; + +/** + * WebAuthn Passwordless Login plugin + * + * The plugin features are broken down into Traits for the sole purpose of making an otherwise + * supermassive class somewhat manageable. You can find the Traits inside the Webauthn/PluginTraits + * folder. + * + * @since 4.0.0 + */ +final class Webauthn extends CMSPlugin implements SubscriberInterface +{ + use CoreEventAware; + + /** + * Autoload the language files + * + * @var boolean + * @since __DEPLOY_VERSION__ + */ + protected $autoloadLanguage = true; + + /** + * Should I try to detect and register legacy event listeners? + * + * @var boolean + * @since __DEPLOY_VERSION__ + * + * @deprecated + */ + protected $allowLegacyListeners = false; + + /** + * The WebAuthn authentication helper object + * + * @var Authentication + * @since __DEPLOY_VERSION__ + */ + protected $authenticationHelper; + + // AJAX request handlers + use AjaxHandler; + use AjaxHandlerInitCreate; + use AjaxHandlerCreate; + use AjaxHandlerSaveLabel; + use AjaxHandlerDelete; + use AjaxHandlerChallenge; + use AjaxHandlerLogin; + + // Custom user profile fields + use UserProfileFields; + + // Handle user profile deletion + use UserDeletion; + + // Add WebAuthn buttons + use AdditionalLoginButtons; + + // Utility methods for setting the events' return values + use EventReturnAware; + + /** + * Constructor. Loads the language files as well. + * + * @param DispatcherInterface $subject The object to observe + * @param array $config An optional associative array of configuration + * settings. Recognized key values include 'name', + * 'group', 'params', 'language (this list is not meant + * to be comprehensive). + * @param Authentication|null $authHelper The WebAuthn helper object + * + * @since 4.0.0 + */ + public function __construct(&$subject, array $config = [], Authentication $authHelper = null) + { + parent::__construct($subject, $config); + + /** + * Note: Do NOT try to load the language in the constructor. This is called before Joomla initializes the + * application language. Therefore the temporary Joomla language object and all loaded strings in it will be + * destroyed on application initialization. As a result we need to call loadLanguage() in each method + * individually, even though all methods make use of language strings. + */ + + // Register a debug log file writer + $logLevels = Log::ERROR | Log::CRITICAL | Log::ALERT | Log::EMERGENCY; + + if (\defined('JDEBUG') && JDEBUG) + { + $logLevels = Log::ALL; + } + + Log::addLogger([ + 'text_file' => "webauthn_system.php", + 'text_entry_format' => '{DATETIME} {PRIORITY} {CLIENTIP} {MESSAGE}', + ], $logLevels, ["webauthn.system"] + ); + + $this->authenticationHelper = $authHelper ?? (new Authentication); + $this->authenticationHelper->setAttestationSupport($this->params->get('attestationSupport', 1) == 1); + } + + /** + * Returns the Authentication helper object + * + * @return Authentication + * + * @since __DEPLOY_VERSION__ + */ + public function getAuthenticationHelper(): Authentication + { + return $this->authenticationHelper; + } + + /** + * Returns an array of events this subscriber will listen to. + * + * @return array + * + * @since __DEPLOY_VERSION__ + */ + public static function getSubscribedEvents(): array + { + try + { + $app = Factory::getApplication(); + } + catch (\Exception $e) + { + return []; + } + + if (!$app->isClient('site') && !$app->isClient('administrator')) + { + return []; + } + + return [ + 'onAjaxWebauthn' => 'onAjaxWebauthn', + 'onAjaxWebauthnChallenge' => 'onAjaxWebauthnChallenge', + 'onAjaxWebauthnCreate' => 'onAjaxWebauthnCreate', + 'onAjaxWebauthnDelete' => 'onAjaxWebauthnDelete', + 'onAjaxWebauthnInitcreate' => 'onAjaxWebauthnInitcreate', + 'onAjaxWebauthnLogin' => 'onAjaxWebauthnLogin', + 'onAjaxWebauthnSavelabel' => 'onAjaxWebauthnSavelabel', + 'onUserAfterDelete' => 'onUserAfterDelete', + 'onUserLoginButtons' => 'onUserLoginButtons', + 'onContentPrepareForm' => 'onContentPrepareForm', + 'onContentPrepareData' => 'onContentPrepareData', + ]; + } +} diff --git a/plugins/system/webauthn/src/Field/WebauthnField.php b/plugins/system/webauthn/src/Field/WebauthnField.php index 973c576c5c01a..c4dbc1158413d 100644 --- a/plugins/system/webauthn/src/Field/WebauthnField.php +++ b/plugins/system/webauthn/src/Field/WebauthnField.php @@ -16,9 +16,9 @@ use Joomla\CMS\Factory; use Joomla\CMS\Form\FormField; use Joomla\CMS\Language\Text; +use Joomla\CMS\Layout\FileLayout; use Joomla\CMS\User\UserFactoryInterface; -use Joomla\Plugin\System\Webauthn\CredentialRepository; -use Joomla\Plugin\System\Webauthn\Helper\Joomla; +use Joomla\Plugin\System\Webauthn\Extension\Webauthn; /** * Custom Joomla Form Field to display the WebAuthn interface @@ -58,17 +58,25 @@ public function getInput() Text::script('PLG_SYSTEM_WEBAUTHN_MANAGE_BTN_CANCEL_LABEL', true); Text::script('PLG_SYSTEM_WEBAUTHN_MSG_SAVED_LABEL', true); Text::script('PLG_SYSTEM_WEBAUTHN_ERR_LABEL_NOT_SAVED', true); + Text::script('PLG_SYSTEM_WEBAUTHN_ERR_XHR_INITCREATE', true); $app = Factory::getApplication(); - $credentialRepository = new CredentialRepository; + /** @var Webauthn $plugin */ + $plugin = $app->bootPlugin('webauthn', 'system'); $app->getDocument()->getWebAssetManager() ->registerAndUseScript('plg_system_webauthn.management', 'plg_system_webauthn/management.js', [], ['defer' => true], ['core']); - return Joomla::renderLayout('plugins.system.webauthn.manage', [ - 'user' => Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId), - 'allow_add' => $userId == $app->getIdentity()->id, - 'credentials' => $credentialRepository->getAll($userId), + $layoutFile = new FileLayout('plugins.system.webauthn.manage'); + + return $layoutFile->render([ + 'user' => Factory::getContainer() + ->get(UserFactoryInterface::class) + ->loadUserById($userId), + 'allow_add' => $userId == $app->getIdentity()->id, + 'credentials' => $plugin->getAuthenticationHelper()->getCredentialsRepository()->getAll($userId), + 'knownAuthenticators' => $plugin->getAuthenticationHelper()->getKnownAuthenticators(), + 'attestationSupport' => $plugin->getAuthenticationHelper()->hasAttestationSupport(), ] ); } diff --git a/plugins/system/webauthn/src/Helper/CredentialsCreation.php b/plugins/system/webauthn/src/Helper/CredentialsCreation.php deleted file mode 100644 index ed8dc34f24afa..0000000000000 --- a/plugins/system/webauthn/src/Helper/CredentialsCreation.php +++ /dev/null @@ -1,358 +0,0 @@ - - * @license GNU General Public License version 2 or later; see LICENSE.txt - */ - -namespace Joomla\Plugin\System\Webauthn\Helper; - -// Protect from unauthorized access -\defined('_JEXEC') or die(); - -use CBOR\Decoder; -use CBOR\OtherObject\OtherObjectManager; -use CBOR\Tag\TagObjectManager; -use Cose\Algorithm\Manager; -use Cose\Algorithm\Signature\ECDSA; -use Cose\Algorithm\Signature\EdDSA; -use Cose\Algorithm\Signature\RSA; -use Cose\Algorithms; -use Exception; -use Joomla\CMS\Application\CMSApplication; -use Joomla\CMS\Crypt\Crypt; -use Joomla\CMS\Factory; -use Joomla\CMS\Language\Text; -use Joomla\CMS\Uri\Uri; -use Joomla\CMS\User\User; -use Joomla\CMS\User\UserFactoryInterface; -use Joomla\Plugin\System\Webauthn\CredentialRepository; -use Laminas\Diactoros\ServerRequestFactory; -use RuntimeException; -use Webauthn\AttestationStatement\AndroidKeyAttestationStatementSupport; -use Webauthn\AttestationStatement\AttestationObjectLoader; -use Webauthn\AttestationStatement\AttestationStatementSupportManager; -use Webauthn\AttestationStatement\FidoU2FAttestationStatementSupport; -use Webauthn\AttestationStatement\NoneAttestationStatementSupport; -use Webauthn\AttestationStatement\PackedAttestationStatementSupport; -use Webauthn\AttestationStatement\TPMAttestationStatementSupport; -use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs; -use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler; -use Webauthn\AuthenticatorAttestationResponse; -use Webauthn\AuthenticatorAttestationResponseValidator; -use Webauthn\AuthenticatorSelectionCriteria; -use Webauthn\PublicKeyCredentialCreationOptions; -use Webauthn\PublicKeyCredentialDescriptor; -use Webauthn\PublicKeyCredentialLoader; -use Webauthn\PublicKeyCredentialParameters; -use Webauthn\PublicKeyCredentialRpEntity; -use Webauthn\PublicKeyCredentialSource; -use Webauthn\PublicKeyCredentialUserEntity; -use Webauthn\TokenBinding\TokenBindingNotSupportedHandler; - -/** - * Helper class to aid in credentials creation (link an authenticator to a user account) - * - * @since 4.0.0 - */ -abstract class CredentialsCreation -{ - /** - * Create a public key for credentials creation. The result is a JSON string which can be used in Javascript code - * with navigator.credentials.create(). - * - * @param User $user The Joomla user to create the public key for - * - * @return string - * - * @since 4.0.0 - */ - public static function createPublicKey(User $user): string - { - /** @var CMSApplication $app */ - try - { - $app = Factory::getApplication(); - $siteName = $app->getConfig()->get('sitename', 'Joomla! Site'); - } - catch (Exception $e) - { - $siteName = 'Joomla! Site'; - } - - // Credentials repository - $repository = new CredentialRepository; - - // Relaying Party -- Our site - $rpEntity = new PublicKeyCredentialRpEntity( - $siteName, - Uri::getInstance()->toString(['host']), - self::getSiteIcon() - ); - - // User Entity - $userEntity = new PublicKeyCredentialUserEntity( - $user->username, - $repository->getHandleFromUserId($user->id), - $user->name - ); - - // Challenge - try - { - $challenge = random_bytes(32); - } - catch (Exception $e) - { - $challenge = Crypt::genRandomBytes(32); - } - - // Public Key Credential Parameters - $publicKeyCredentialParametersList = [ - new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_ES256), - ]; - - // Timeout: 60 seconds (given in milliseconds) - $timeout = 60000; - - // Devices to exclude (already set up authenticators) - $excludedPublicKeyDescriptors = []; - $records = $repository->findAllForUserEntity($userEntity); - - /** @var PublicKeyCredentialSource $record */ - foreach ($records as $record) - { - $excludedPublicKeyDescriptors[] = new PublicKeyCredentialDescriptor($record->getType(), $record->getCredentialPublicKey()); - } - - // Authenticator Selection Criteria (we used default values) - $authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria; - - // Extensions (not yet supported by the library) - $extensions = new AuthenticationExtensionsClientInputs; - - // Attestation preference - $attestationPreference = PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE; - - // Public key credential creation options - $publicKeyCredentialCreationOptions = new PublicKeyCredentialCreationOptions( - $rpEntity, - $userEntity, - $challenge, - $publicKeyCredentialParametersList, - $timeout, - $excludedPublicKeyDescriptors, - $authenticatorSelectionCriteria, - $attestationPreference, - $extensions - ); - - // Save data in the session - Joomla::setSessionVar('publicKeyCredentialCreationOptions', - base64_encode(serialize($publicKeyCredentialCreationOptions)), - 'plg_system_webauthn' - ); - Joomla::setSessionVar('registration_user_id', $user->id, 'plg_system_webauthn'); - - return json_encode($publicKeyCredentialCreationOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); - } - - /** - * Validate the authentication data returned by the device and return the public key credential source on success. - * - * An exception will be returned on error. Also, under very rare conditions, you may receive NULL instead of - * a PublicKeyCredentialSource object which means that something was off in the returned data from the browser. - * - * @param string $data The JSON-encoded data returned by the browser during the authentication flow - * - * @return PublicKeyCredentialSource|null - * - * @since 4.0.0 - */ - public static function validateAuthenticationData(string $data): ?PublicKeyCredentialSource - { - // Retrieve the PublicKeyCredentialCreationOptions object created earlier and perform sanity checks - $encodedOptions = Joomla::getSessionVar('publicKeyCredentialCreationOptions', null, 'plg_system_webauthn'); - - if (empty($encodedOptions)) - { - throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_NO_PK')); - } - - try - { - $publicKeyCredentialCreationOptions = unserialize(base64_decode($encodedOptions)); - } - catch (Exception $e) - { - $publicKeyCredentialCreationOptions = null; - } - - if (!\is_object($publicKeyCredentialCreationOptions) || !($publicKeyCredentialCreationOptions instanceof PublicKeyCredentialCreationOptions)) - { - throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_NO_PK')); - } - - // Retrieve the stored user ID and make sure it's the same one in the request. - $storedUserId = Joomla::getSessionVar('registration_user_id', 0, 'plg_system_webauthn'); - - try - { - $myUser = Factory::getApplication()->getIdentity(); - } - catch (Exception $e) - { - $dummyUserId = 0; - $myUser = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($dummyUserId); - } - - $myUserId = $myUser->id; - - if (($myUser->guest) || ($myUserId != $storedUserId)) - { - throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_USER')); - } - - // Cose Algorithm Manager - $coseAlgorithmManager = new Manager; - $coseAlgorithmManager->add(new ECDSA\ES256); - $coseAlgorithmManager->add(new ECDSA\ES512); - $coseAlgorithmManager->add(new EdDSA\EdDSA); - $coseAlgorithmManager->add(new RSA\RS1); - $coseAlgorithmManager->add(new RSA\RS256); - $coseAlgorithmManager->add(new RSA\RS512); - - // Create a CBOR Decoder object - $otherObjectManager = new OtherObjectManager; - $tagObjectManager = new TagObjectManager; - $decoder = new Decoder($tagObjectManager, $otherObjectManager); - - // The token binding handler - $tokenBindingHandler = new TokenBindingNotSupportedHandler; - - // Attestation Statement Support Manager - $attestationStatementSupportManager = new AttestationStatementSupportManager; - $attestationStatementSupportManager->add(new NoneAttestationStatementSupport); - $attestationStatementSupportManager->add(new FidoU2FAttestationStatementSupport($decoder)); - - /** - $attestationStatementSupportManager->add( - new AndroidSafetyNetAttestationStatementSupport(HttpFactory::getHttp(), - 'GOOGLE_SAFETYNET_API_KEY', - new RequestFactory - ) - ); - */ - $attestationStatementSupportManager->add(new AndroidKeyAttestationStatementSupport($decoder)); - $attestationStatementSupportManager->add(new TPMAttestationStatementSupport); - $attestationStatementSupportManager->add(new PackedAttestationStatementSupport($decoder, $coseAlgorithmManager)); - - // Attestation Object Loader - $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager, $decoder); - - // Public Key Credential Loader - $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader, $decoder); - - // Credential Repository - $credentialRepository = new CredentialRepository; - - // Extension output checker handler - $extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler; - - // Authenticator Attestation Response Validator - $authenticatorAttestationResponseValidator = new AuthenticatorAttestationResponseValidator( - $attestationStatementSupportManager, - $credentialRepository, - $tokenBindingHandler, - $extensionOutputCheckerHandler - ); - - // Any Throwable from this point will bubble up to the GUI - - // We init the PSR-7 request object using Diactoros - $request = ServerRequestFactory::fromGlobals(); - - // Load the data - $publicKeyCredential = $publicKeyCredentialLoader->load(base64_decode($data)); - $response = $publicKeyCredential->getResponse(); - - // Check if the response is an Authenticator Attestation Response - if (!$response instanceof AuthenticatorAttestationResponse) - { - throw new RuntimeException('Not an authenticator attestation response'); - } - - // Check the response against the request - $authenticatorAttestationResponseValidator->check($response, $publicKeyCredentialCreationOptions, $request); - - /** - * Everything is OK here. You can get the Public Key Credential Source. This object should be persisted using - * the Public Key Credential Source repository. - */ - return PublicKeyCredentialSource::createFromPublicKeyCredential( - $publicKeyCredential, - $publicKeyCredentialCreationOptions->getUser()->getId() - ); - } - - /** - * Try to find the site's favicon in the site's root, images, media, templates or current template directory. - * - * @return string|null - * - * @since 4.0.0 - */ - protected static function getSiteIcon(): ?string - { - $filenames = [ - 'apple-touch-icon.png', - 'apple_touch_icon.png', - 'favicon.ico', - 'favicon.png', - 'favicon.gif', - 'favicon.bmp', - 'favicon.jpg', - 'favicon.svg', - ]; - - try - { - $paths = [ - '/', - '/images/', - '/media/', - '/templates/', - '/templates/' . Factory::getApplication()->getTemplate(), - ]; - } - catch (Exception $e) - { - return null; - } - - foreach ($paths as $path) - { - foreach ($filenames as $filename) - { - $relFile = $path . $filename; - $filePath = JPATH_BASE . $relFile; - - if (is_file($filePath)) - { - break 2; - } - - $relFile = null; - } - } - - if (!isset($relFile) || \is_null($relFile)) - { - return null; - } - - return rtrim(Uri::base(), '/') . '/' . ltrim($relFile, '/'); - } -} diff --git a/plugins/system/webauthn/src/Helper/Joomla.php b/plugins/system/webauthn/src/Helper/Joomla.php deleted file mode 100644 index 4d6deae9bf44d..0000000000000 --- a/plugins/system/webauthn/src/Helper/Joomla.php +++ /dev/null @@ -1,744 +0,0 @@ - - * @license GNU General Public License version 2 or later; see LICENSE.txt - */ - -namespace Joomla\Plugin\System\Webauthn\Helper; - -// Protect from unauthorized access -\defined('_JEXEC') or die(); - -use DateTime; -use DateTimeZone; -use Exception; -use JLoader; -use Joomla\Application\AbstractApplication; -use Joomla\CMS\Application\CliApplication; -use Joomla\CMS\Application\CMSApplication; -use Joomla\CMS\Application\ConsoleApplication; -use Joomla\CMS\Authentication\Authentication; -use Joomla\CMS\Authentication\AuthenticationResponse; -use Joomla\CMS\Date\Date; -use Joomla\CMS\Factory; -use Joomla\CMS\Language\Text; -use Joomla\CMS\Layout\FileLayout; -use Joomla\CMS\Log\Log; -use Joomla\CMS\Plugin\PluginHelper; -use Joomla\CMS\User\User; -use Joomla\CMS\User\UserFactoryInterface; -use Joomla\CMS\User\UserHelper; -use Joomla\Registry\Registry; -use RuntimeException; - -/** - * A helper class for abstracting core features in Joomla! 3.4 and later, including 4.x - * - * @since 4.0.0 - */ -abstract class Joomla -{ - /** - * A fake session storage for CLI apps. Since CLI applications cannot have a session we are - * using a Registry object we manage internally. - * - * @var Registry - * @since 4.0.0 - */ - protected static $fakeSession = null; - - /** - * Are we inside the administrator application - * - * @var boolean - * @since 4.0.0 - */ - protected static $isAdmin = null; - - /** - * Are we inside a CLI application - * - * @var boolean - * @since 4.0.0 - */ - protected static $isCli = null; - - /** - * Which plugins have already registered a text file logger. Prevents double registration of a - * log file. - * - * @var array - * @since 4.0.0 - */ - protected static $registeredLoggers = []; - - /** - * The current Joomla Document type - * - * @var string|null - * @since 4.0.0 - */ - protected static $joomlaDocumentType = null; - - /** - * Is the current user allowed to edit the social login configuration of $user? To do so I must - * either be editing my own account OR I have to be a Super User. - * - * @param User $user The user you want to know if we're allowed to edit - * - * @return boolean - * - * @since 4.0.0 - */ - public static function canEditUser(User $user = null): bool - { - // I can edit myself - if (empty($user)) - { - return true; - } - - // Guests can't have social logins associated - if ($user->guest) - { - return false; - } - - // Get the currently logged in used - try - { - $myUser = Factory::getApplication()->getIdentity(); - } - catch (Exception $e) - { - // Cannot get the application; no user, therefore no edit privileges. - return false; - } - - // Same user? I can edit myself - if ($myUser->id == $user->id) - { - return true; - } - - // To edit a different user I must be a Super User myself. If I'm not, I can't edit another user! - if (!$myUser->authorise('core.admin')) - { - return false; - } - - // I am a Super User editing another user. That's allowed. - return true; - } - - /** - * Helper method to render a JLayout. - * - * @param string $layoutFile Dot separated path to the layout file, relative to base path - * (plugins/system/webauthn/layout) - * @param object $displayData Object which properties are used inside the layout file to - * build displayed output - * @param string $includePath Additional path holding layout files - * @param mixed $options Optional custom options to load. Registry or array format. - * Set 'debug'=>true to output debug information. - * - * @return string - * - * @since 4.0.0 - */ - public static function renderLayout(string $layoutFile, $displayData = null, - string $includePath = '', array $options = [] - ): string - { - $basePath = JPATH_SITE . '/plugins/system/webauthn/layout'; - $layout = new FileLayout($layoutFile, $basePath, $options); - - if (!empty($includePath)) - { - $layout->addIncludePath($includePath); - } - - return $layout->render($displayData); - } - - /** - * Unset a variable from the user session - * - * This method cannot be replaced with a call to Factory::getSession->set(). This method takes - * into account running under CLI, using a fake session storage. In the end of the day this - * plugin doesn't work under CLI but being able to fake session storage under CLI means that we - * don't have to add gnarly if-blocks everywhere in the code to make sure it doesn't break CLI - * either! - * - * @param string $name The name of the variable to unset - * @param string $namespace (optional) The variable's namespace e.g. the component name. - * Default: 'default' - * - * @return void - * - * @since 4.0.0 - */ - public static function unsetSessionVar(string $name, string $namespace = 'default'): void - { - self::setSessionVar($name, null, $namespace); - } - - /** - * Set a variable in the user session. - * - * This method cannot be replaced with a call to Factory::getSession->set(). This method takes - * into account running under CLI, using a fake session storage. In the end of the day this - * plugin doesn't work under CLI but being able to fake session storage under CLI means that we - * don't have to add gnarly if-blocks everywhere in the code to make sure it doesn't break CLI - * either! - * - * @param string $name The name of the variable to set - * @param string $value (optional) The value to set it to, default is null - * @param string $namespace (optional) The variable's namespace e.g. the component name. - * Default: 'default' - * - * @return void - * - * @since 4.0.0 - */ - public static function setSessionVar(string $name, ?string $value = null, - string $namespace = 'default' - ): void - { - $qualifiedKey = "$namespace.$name"; - - if (self::isCli()) - { - self::getFakeSession()->set($qualifiedKey, $value); - - return; - } - - try - { - Factory::getApplication()->getSession()->set($qualifiedKey, $value); - } - catch (Exception $e) - { - return; - } - } - - /** - * Are we inside a CLI application - * - * @param CMSApplication $app The current CMS application which tells us if we are inside - * an admin page - * - * @return boolean - * - * @since 4.0.0 - */ - public static function isCli(CMSApplication $app = null): bool - { - if (\is_null(self::$isCli)) - { - if (\is_null($app)) - { - try - { - $app = Factory::getApplication(); - } - catch (Exception $e) - { - $app = null; - } - } - - if (\is_null($app)) - { - self::$isCli = true; - } - - if (\is_object($app)) - { - self::$isCli = $app instanceof Exception; - - if (class_exists('Joomla\\CMS\\Application\\CliApplication')) - { - self::$isCli = self::$isCli || $app instanceof CliApplication || $app instanceof ConsoleApplication; - } - } - } - - return self::$isCli; - } - - /** - * Get a fake session registry for CLI applications - * - * @return Registry - * - * @since 4.0.0 - */ - protected static function getFakeSession(): Registry - { - if (!\is_object(self::$fakeSession)) - { - self::$fakeSession = new Registry; - } - - return self::$fakeSession; - } - - /** - * Return the session token. This method goes through our session abstraction to prevent a - * fatal exception if it's accidentally called under CLI. - * - * @return mixed - * - * @since 4.0.0 - */ - public static function getToken(): string - { - // For CLI apps we implement our own fake token system - if (self::isCli()) - { - $token = self::getSessionVar('session.token'); - - // Create a token - if (\is_null($token)) - { - $token = UserHelper::genRandomPassword(32); - - self::setSessionVar('session.token', $token); - } - - return (string) $token; - } - - // Web application, go through the regular Joomla! API. - try - { - return Factory::getApplication()->getSession()->getToken(); - } - catch (Exception $e) - { - return ''; - } - } - - /** - * Get a variable from the user session - * - * This method cannot be replaced with a call to Factory::getSession->get(). This method takes - * into account running under CLI, using a fake session storage. In the end of the day this - * plugin doesn't work under CLI but being able to fake session storage under CLI means that we - * don't have to add gnarly if-blocks everywhere in the code to make sure it doesn't break CLI - * either! - * - * @param string $name The name of the variable to set - * @param string $default (optional) The default value to return if the variable does not - * exit, default: null - * @param string $namespace (optional) The variable's namespace e.g. the component name. - * Default: 'default' - * - * @return mixed - * - * @since 4.0.0 - */ - public static function getSessionVar(string $name, ?string $default = null, - string $namespace = 'default' - ) - { - $qualifiedKey = "$namespace.$name"; - - if (self::isCli()) - { - return self::getFakeSession()->get("$namespace.$name", $default); - } - - try - { - return Factory::getApplication()->getSession()->get($qualifiedKey, $default); - } - catch (Exception $e) - { - return $default; - } - } - - /** - * Register a debug log file writer for a Social Login plugin. - * - * @param string $plugin The Social Login plugin for which to register a debug log file - * writer - * - * @return void - * - * @since 4.0.0 - */ - public static function addLogger(string $plugin): void - { - // Make sure this logger is not already registered - if (\in_array($plugin, self::$registeredLoggers)) - { - return; - } - - self::$registeredLoggers[] = $plugin; - - // We only log errors unless Site Debug is enabled - $logLevels = Log::ERROR | Log::CRITICAL | Log::ALERT | Log::EMERGENCY; - - if (\defined('JDEBUG') && JDEBUG) - { - $logLevels = Log::ALL; - } - - // Add a formatted text logger - Log::addLogger([ - 'text_file' => "webauthn_{$plugin}.php", - 'text_entry_format' => '{DATETIME} {PRIORITY} {CLIENTIP} {MESSAGE}', - ], $logLevels, [ - "webauthn.{$plugin}", - ] - ); - } - - /** - * Logs in a user to the site, bypassing the authentication plugins. - * - * @param int $userId The user ID to log in - * @param AbstractApplication $app The application we are running in. Skip to - * auto-detect (recommended). - * - * @return void - * - * @throws Exception - * - * @since 4.0.0 - */ - public static function loginUser(int $userId, AbstractApplication $app = null): void - { - // Trick the class auto-loader into loading the necessary classes - class_exists('Joomla\\CMS\\Authentication\\Authentication', true); - - // Fake a successful login message - if (!\is_object($app)) - { - $app = Factory::getApplication(); - } - - $isAdmin = $app->isClient('administrator'); - /** @var User $user */ - $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - - // Does the user account have a pending activation? - if (!empty($user->activation)) - { - throw new RuntimeException(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - // Is the user account blocked? - if ($user->block) - { - throw new RuntimeException(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $statusSuccess = Authentication::STATUS_SUCCESS; - - $response = self::getAuthenticationResponseObject(); - $response->status = $statusSuccess; - $response->username = $user->username; - $response->fullname = $user->name; - // phpcs:ignore - $response->error_message = ''; - $response->language = $user->getParam('language'); - $response->type = 'Passwordless'; - - if ($isAdmin) - { - $response->language = $user->getParam('admin_language'); - } - - /** - * Set up the login options. - * - * The 'remember' element forces the use of the Remember Me feature when logging in with Webauthn, as the - * users would expect. - * - * The 'action' element is actually required by plg_user_joomla. It is the core ACL action the logged in user - * must be allowed for the login to succeed. Please note that front-end and back-end logins use a different - * action. This allows us to provide the social login button on both front- and back-end and be sure that if a - * used with no backend access tries to use it to log in Joomla! will just slap him with an error message about - * insufficient privileges - the same thing that'd happen if you tried to use your front-end only username and - * password in a back-end login form. - */ - $options = [ - 'remember' => true, - 'action' => 'core.login.site', - ]; - - if (self::isAdminPage()) - { - $options['action'] = 'core.login.admin'; - } - - // Run the user plugins. They CAN block login by returning boolean false and setting $response->error_message. - PluginHelper::importPlugin('user'); - - /** @var CMSApplication $app */ - $results = $app->triggerEvent('onUserLogin', [(array) $response, $options]); - - // If there is no boolean FALSE result from any plugin the login is successful. - if (\in_array(false, $results, true) == false) - { - // Set the user in the session, letting Joomla! know that we are logged in. - $app->getSession()->set('user', $user); - - // Trigger the onUserAfterLogin event - $options['user'] = $user; - $options['responseType'] = $response->type; - - // The user is successfully logged in. Run the after login events - $app->triggerEvent('onUserAfterLogin', [$options]); - - return; - } - - // If we are here the plugins marked a login failure. Trigger the onUserLoginFailure Event. - $app->triggerEvent('onUserLoginFailure', [(array) $response]); - - // Log the failure - // phpcs:ignore - Log::add($response->error_message, Log::WARNING, 'jerror'); - - // Throw an exception to let the caller know that the login failed - // phpcs:ignore - throw new RuntimeException($response->error_message); - } - - /** - * Returns a (blank) Joomla! authentication response - * - * @return AuthenticationResponse - * - * @since 4.0.0 - */ - public static function getAuthenticationResponseObject(): AuthenticationResponse - { - // Force the class auto-loader to load the JAuthentication class - JLoader::import('joomla.user.authentication'); - class_exists('Joomla\\CMS\\Authentication\\Authentication', true); - - return new AuthenticationResponse; - } - - /** - * Are we inside an administrator page? - * - * @param CMSApplication $app The current CMS application which tells us if we are inside - * an admin page - * - * @return boolean - * - * @throws Exception - * - * @since 4.0.0 - */ - public static function isAdminPage(CMSApplication $app = null): bool - { - if (\is_null(self::$isAdmin)) - { - if (\is_null($app)) - { - $app = Factory::getApplication(); - } - - self::$isAdmin = $app->isClient('administrator'); - } - - return self::$isAdmin; - } - - /** - * Have Joomla! process a login failure - * - * @param AuthenticationResponse $response The Joomla! auth response object - * @param AbstractApplication $app The application we are running in. Skip to - * auto-detect (recommended). - * @param string $logContext Logging context (plugin name). Default: - * system. - * - * @return boolean - * - * @throws Exception - * - * @since 4.0.0 - */ - public static function processLoginFailure(AuthenticationResponse $response, - AbstractApplication $app = null, - string $logContext = 'system' - ) - { - // Import the user plugin group. - PluginHelper::importPlugin('user'); - - if (!\is_object($app)) - { - $app = Factory::getApplication(); - } - - // Trigger onUserLoginFailure Event. - self::log($logContext, "Calling onUserLoginFailure plugin event"); - /** @var CMSApplication $app */ - $app->triggerEvent('onUserLoginFailure', [(array) $response]); - - // If status is success, any error will have been raised by the user plugin - $expectedStatus = Authentication::STATUS_SUCCESS; - - if ($response->status !== $expectedStatus) - { - self::log($logContext, "The login failure has been logged in Joomla's error log"); - - // Everything logged in the 'jerror' category ends up being enqueued in the application message queue. - // phpcs:ignore - Log::add($response->error_message, Log::WARNING, 'jerror'); - } - else - { - $message = "The login failure was caused by a third party user plugin but it did not " . - "return any further information. Good luck figuring this one out..."; - self::log($logContext, $message, Log::WARNING); - } - - return false; - } - - /** - * Writes a log message to the debug log - * - * @param string $plugin The Social Login plugin which generated this log message - * @param string $message The message to write to the log - * @param int $priority Log message priority, default is Log::DEBUG - * - * @return void - * - * @since 4.0.0 - */ - public static function log(string $plugin, string $message, $priority = Log::DEBUG): void - { - Log::add($message, $priority, 'webauthn.' . $plugin); - } - - /** - * Format a date for display. - * - * The $tzAware parameter defines whether the formatted date will be timezone-aware. If set to - * false the formatted date will be rendered in the UTC timezone. If set to true the code will - * automatically try to use the logged in user's timezone or, if none is set, the site's - * default timezone (Server Timezone). If set to a positive integer the same thing will happen - * but for the specified user ID instead of the currently logged in user. - * - * @param string|DateTime $date The date to format - * @param string $format The format string, default is Joomla's DATE_FORMAT_LC6 - * (usually "Y-m-d H:i:s") - * @param bool|int $tzAware Should the format be timezone aware? See notes above. - * - * @return string - * - * @since 4.0.0 - */ - public static function formatDate($date, ?string $format = null, bool $tzAware = true): string - { - $utcTimeZone = new DateTimeZone('UTC'); - $jDate = new Date($date, $utcTimeZone); - - // Which timezone should I use? - $tz = null; - - if ($tzAware !== false) - { - $userId = \is_bool($tzAware) ? null : (int) $tzAware; - - try - { - /** @var CMSApplication $app */ - $app = Factory::getApplication(); - $tzDefault = $app->get('offset'); - } - catch (Exception $e) - { - $tzDefault = 'GMT'; - } - - /** @var User $user */ - if (empty($userId)) - { - $user = $app->getIdentity(); - } - else - { - $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - } - - $tz = $user->getParam('timezone', $tzDefault); - } - - if (!empty($tz)) - { - try - { - $userTimeZone = new DateTimeZone($tz); - - $jDate->setTimezone($userTimeZone); - } - catch (Exception $e) - { - // Nothing. Fall back to UTC. - } - } - - if (empty($format)) - { - $format = Text::_('DATE_FORMAT_LC6'); - } - - return $jDate->format($format, true); - } - - /** - * Returns the current Joomla document type. - * - * The error catching is necessary because the application document object or even the - * application object itself may have not yet been initialized. For example, a system plugin - * running inside a custom application object which does not create a document object or which - * does not go through Joomla's Factory to create the application object. In practice these are - * CLI and custom web applications used for maintenance and third party service callbacks. They - * end up loading the system plugins but either don't go through Factory or at least don't - * create a document object. - * - * @return string - * - * @since 4.0.0 - */ - public static function getDocumentType(): string - { - if (\is_null(self::$joomlaDocumentType)) - { - try - { - /** @var CMSApplication $app */ - $app = Factory::getApplication(); - $document = $app->getDocument(); - } - catch (Exception $e) - { - $document = null; - } - - self::$joomlaDocumentType = (\is_null($document)) ? 'error' : $document->getType(); - } - - return self::$joomlaDocumentType; - } -} diff --git a/plugins/system/webauthn/src/Hotfix/AndroidKeyAttestationStatementSupport.php b/plugins/system/webauthn/src/Hotfix/AndroidKeyAttestationStatementSupport.php new file mode 100644 index 0000000000000..c280ddfd5fd5d --- /dev/null +++ b/plugins/system/webauthn/src/Hotfix/AndroidKeyAttestationStatementSupport.php @@ -0,0 +1,270 @@ + + * @license MIT; see libraries/vendor/web-auth/webauthn-lib/LICENSE + */ + +namespace Joomla\Plugin\System\Webauthn\Hotfix; + +// Protect from unauthorized access +defined('_JEXEC') or die(); + +use Assert\Assertion; +use CBOR\Decoder; +use CBOR\OtherObject\OtherObjectManager; +use CBOR\Tag\TagObjectManager; +use Cose\Algorithms; +use Cose\Key\Ec2Key; +use Cose\Key\Key; +use Cose\Key\RsaKey; +use FG\ASN1\ASNObject; +use FG\ASN1\ExplicitlyTaggedObject; +use FG\ASN1\Universal\OctetString; +use FG\ASN1\Universal\Sequence; +use Webauthn\AttestationStatement\AttestationStatement; +use Webauthn\AttestationStatement\AttestationStatementSupport; +use Webauthn\AuthenticatorData; +use Webauthn\CertificateToolbox; +use Webauthn\MetadataService\MetadataStatementRepository; +use Webauthn\StringStream; +use Webauthn\TrustPath\CertificateTrustPath; + +/** + * We had to fork the key attestation support object from the WebAuthn server package to address an + * issue with PHP 8. + * + * We are currently using an older version of the WebAuthn library (2.x) which was written before + * PHP 8 was developed. We cannot upgrade the WebAuthn library to a newer major version because of + * Joomla's Semantic Versioning promise. + * + * The AndroidKeyAttestationStatementSupport class forces an assertion on the result of the + * openssl_pkey_get_public() function, assuming it will return a resource. However, starting with + * PHP 8.0 this function returns an OpenSSLAsymmetricKey object and the assertion fails. As a + * result, you cannot use Android or FIDO U2F keys with WebAuthn. + * + * The assertion check is in a private method, therefore we have to fork both attestation support + * class to change the assertion. The assertion takes place through a third party library we cannot + * (and should not!) modify. + * + * @since __DEPLOY_VERSION__ + * + * @deprecated 5.0 We will upgrade the WebAuthn library to version 3 or later and this will go away. + */ +final class AndroidKeyAttestationStatementSupport implements AttestationStatementSupport +{ + /** + * @var Decoder + * @since __DEPLOY_VERSION__ + */ + private $decoder; + + /** + * @var MetadataStatementRepository|null + * @since __DEPLOY_VERSION__ + */ + private $metadataStatementRepository; + + /** + * @param Decoder|null $decoder Obvious + * @param MetadataStatementRepository|null $metadataStatementRepository Obvious + * + * @since __DEPLOY_VERSION__ + */ + public function __construct( + ?Decoder $decoder = null, + ?MetadataStatementRepository $metadataStatementRepository = null + ) + { + if ($decoder !== null) + { + @trigger_error('The argument "$decoder" is deprecated since 2.1 and will be removed in v3.0. Set null instead', E_USER_DEPRECATED); + } + + if ($metadataStatementRepository === null) + { + @trigger_error( + 'Setting "null" for argument "$metadataStatementRepository" is deprecated since 2.1 and will be mandatory in v3.0.', + E_USER_DEPRECATED + ); + } + + $this->decoder = $decoder ?? new Decoder(new TagObjectManager, new OtherObjectManager); + $this->metadataStatementRepository = $metadataStatementRepository; + } + + /** + * @return string + * @since __DEPLOY_VERSION__ + */ + public function name(): string + { + return 'android-key'; + } + + /** + * @param array $attestation Obvious + * + * @return AttestationStatement + * @throws \Assert\AssertionFailedException + * @since __DEPLOY_VERSION__ + */ + public function load(array $attestation): AttestationStatement + { + Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object'); + + foreach (['sig', 'x5c', 'alg'] as $key) + { + Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key)); + } + + $certificates = $attestation['attStmt']['x5c']; + + Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.'); + Assertion::greaterThan(\count($certificates), 0, 'The attestation statement value "x5c" must be a list with at least one certificate.'); + Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.'); + + $certificates = CertificateToolbox::convertAllDERToPEM($certificates); + + return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates)); + } + + /** + * @param string $clientDataJSONHash Obvious + * @param AttestationStatement $attestationStatement Obvious + * @param AuthenticatorData $authenticatorData Obvious + * + * @return boolean + * @throws \Assert\AssertionFailedException + * @since __DEPLOY_VERSION__ + */ + public function isValid( + string $clientDataJSONHash, + AttestationStatement $attestationStatement, + AuthenticatorData $authenticatorData + ): bool + { + $trustPath = $attestationStatement->getTrustPath(); + Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path'); + + $certificates = $trustPath->getCertificates(); + + if ($this->metadataStatementRepository !== null) + { + $certificates = CertificateToolbox::checkAttestationMedata( + $attestationStatement, + $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(), + $certificates, + $this->metadataStatementRepository + ); + } + + // Decode leaf attestation certificate + $leaf = $certificates[0]; + $this->checkCertificateAndGetPublicKey($leaf, $clientDataJSONHash, $authenticatorData); + + $signedData = $authenticatorData->getAuthData() . $clientDataJSONHash; + $alg = $attestationStatement->get('alg'); + + return openssl_verify($signedData, $attestationStatement->get('sig'), $leaf, Algorithms::getOpensslAlgorithmFor((int) $alg)) === 1; + } + + /** + * @param string $certificate Obvious + * @param string $clientDataHash Obvious + * @param AuthenticatorData $authenticatorData Obvious + * + * @return void + * @throws \Assert\AssertionFailedException + * @throws \FG\ASN1\Exception\ParserException + * @since __DEPLOY_VERSION__ + */ + private function checkCertificateAndGetPublicKey( + string $certificate, + string $clientDataHash, + AuthenticatorData $authenticatorData + ): void + { + $resource = openssl_pkey_get_public($certificate); + + if (version_compare(PHP_VERSION, '8.0', 'lt')) + { + Assertion::isResource($resource, 'Unable to read the certificate'); + } + else + { + /** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */ + Assertion::isInstanceOf($resource, \OpenSSLAsymmetricKey::class, 'Unable to read the certificate'); + } + + $details = openssl_pkey_get_details($resource); + Assertion::isArray($details, 'Unable to read the certificate'); + + // Check that authData publicKey matches the public key in the attestation certificate + $attestedCredentialData = $authenticatorData->getAttestedCredentialData(); + Assertion::notNull($attestedCredentialData, 'No attested credential data found'); + $publicKeyData = $attestedCredentialData->getCredentialPublicKey(); + Assertion::notNull($publicKeyData, 'No attested public key found'); + $publicDataStream = new StringStream($publicKeyData); + $coseKey = $this->decoder->decode($publicDataStream)->getNormalizedData(false); + Assertion::true($publicDataStream->isEOF(), 'Invalid public key data. Presence of extra bytes.'); + $publicDataStream->close(); + $publicKey = Key::createFromData($coseKey); + + Assertion::true(($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey), 'Unsupported key type'); + Assertion::eq($publicKey->asPEM(), $details['key'], 'Invalid key'); + + $certDetails = openssl_x509_parse($certificate); + + // Find Android KeyStore Extension with OID “1.3.6.1.4.1.11129.2.1.17” in certificate extensions + Assertion::keyExists($certDetails, 'extensions', 'The certificate has no extension'); + Assertion::isArray($certDetails['extensions'], 'The certificate has no extension'); + Assertion::keyExists( + $certDetails['extensions'], + '1.3.6.1.4.1.11129.2.1.17', + 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is missing' + ); + $extension = $certDetails['extensions']['1.3.6.1.4.1.11129.2.1.17']; + $extensionAsAsn1 = ASNObject::fromBinary($extension); + Assertion::isInstanceOf($extensionAsAsn1, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); + $objects = $extensionAsAsn1->getChildren(); + + // Check that attestationChallenge is set to the clientDataHash. + Assertion::keyExists($objects, 4, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); + Assertion::isInstanceOf($objects[4], OctetString::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); + Assertion::eq($clientDataHash, hex2bin(($objects[4])->getContent()), 'The client data hash is not valid'); + + // Check that both teeEnforced and softwareEnforced structures don’t contain allApplications(600) tag. + Assertion::keyExists($objects, 6, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); + $softwareEnforcedFlags = $objects[6]; + Assertion::isInstanceOf($softwareEnforcedFlags, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); + $this->checkAbsenceOfAllApplicationsTag($softwareEnforcedFlags); + + Assertion::keyExists($objects, 7, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); + $teeEnforcedFlags = $objects[6]; + Assertion::isInstanceOf($teeEnforcedFlags, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); + $this->checkAbsenceOfAllApplicationsTag($teeEnforcedFlags); + } + + /** + * @param Sequence $sequence Obvious + * + * @return void + * @throws \Assert\AssertionFailedException + * @since __DEPLOY_VERSION__ + */ + private function checkAbsenceOfAllApplicationsTag(Sequence $sequence): void + { + foreach ($sequence->getChildren() as $tag) + { + Assertion::isInstanceOf($tag, ExplicitlyTaggedObject::class, 'Invalid tag'); + + /** + * @var ExplicitlyTaggedObject $tag It is silly that I have to do that for PHPCS to be happy. + */ + Assertion::notEq(600, (int) $tag->getTag(), 'Forbidden tag 600 found'); + } + } +} diff --git a/plugins/system/webauthn/src/Hotfix/FidoU2FAttestationStatementSupport.php b/plugins/system/webauthn/src/Hotfix/FidoU2FAttestationStatementSupport.php new file mode 100644 index 0000000000000..6ad177b47406e --- /dev/null +++ b/plugins/system/webauthn/src/Hotfix/FidoU2FAttestationStatementSupport.php @@ -0,0 +1,230 @@ + + * @license MIT; see libraries/vendor/web-auth/webauthn-lib/LICENSE + */ + +namespace Joomla\Plugin\System\Webauthn\Hotfix; + +// Protect from unauthorized access +defined('_JEXEC') or die(); + +use Assert\Assertion; +use CBOR\Decoder; +use CBOR\MapObject; +use CBOR\OtherObject\OtherObjectManager; +use CBOR\Tag\TagObjectManager; +use Cose\Key\Ec2Key; +use Webauthn\AttestationStatement\AttestationStatement; +use Webauthn\AttestationStatement\AttestationStatementSupport; +use Webauthn\AuthenticatorData; +use Webauthn\CertificateToolbox; +use Webauthn\MetadataService\MetadataStatementRepository; +use Webauthn\StringStream; +use Webauthn\TrustPath\CertificateTrustPath; + +/** + * We had to fork the key attestation support object from the WebAuthn server package to address an + * issue with PHP 8. + * + * We are currently using an older version of the WebAuthn library (2.x) which was written before + * PHP 8 was developed. We cannot upgrade the WebAuthn library to a newer major version because of + * Joomla's Semantic Versioning promise. + * + * The FidoU2FAttestationStatementSupport class forces an assertion on the result of the + * openssl_pkey_get_public() function, assuming it will return a resource. However, starting with + * PHP 8.0 this function returns an OpenSSLAsymmetricKey object and the assertion fails. As a + * result, you cannot use Android or FIDO U2F keys with WebAuthn. + * + * The assertion check is in a private method, therefore we have to fork both attestation support + * class to change the assertion. The assertion takes place through a third party library we cannot + * (and should not!) modify. + * + * @since __DEPLOY_VERSION__ + * + * @deprecated 5.0 We will upgrade the WebAuthn library to version 3 or later and this will go away. + */ +final class FidoU2FAttestationStatementSupport implements AttestationStatementSupport +{ + /** + * @var Decoder + * @since __DEPLOY_VERSION__ + */ + private $decoder; + + /** + * @var MetadataStatementRepository|null + * @since __DEPLOY_VERSION__ + */ + private $metadataStatementRepository; + + /** + * @param Decoder|null $decoder Obvious + * @param MetadataStatementRepository|null $metadataStatementRepository Obvious + * + * @since __DEPLOY_VERSION__ + */ + public function __construct( + ?Decoder $decoder = null, + ?MetadataStatementRepository $metadataStatementRepository = null + ) + { + if ($decoder !== null) + { + @trigger_error('The argument "$decoder" is deprecated since 2.1 and will be removed in v3.0. Set null instead', E_USER_DEPRECATED); + } + + if ($metadataStatementRepository === null) + { + @trigger_error( + 'Setting "null" for argument "$metadataStatementRepository" is deprecated since 2.1 and will be mandatory in v3.0.', + E_USER_DEPRECATED + ); + } + + $this->decoder = $decoder ?? new Decoder(new TagObjectManager, new OtherObjectManager); + $this->metadataStatementRepository = $metadataStatementRepository; + } + + /** + * @return string + * @since __DEPLOY_VERSION__ + */ + public function name(): string + { + return 'fido-u2f'; + } + + /** + * @param array $attestation Obvious + * + * @return AttestationStatement + * @throws \Assert\AssertionFailedException + * + * @since __DEPLOY_VERSION__ + */ + public function load(array $attestation): AttestationStatement + { + Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object'); + + foreach (['sig', 'x5c'] as $key) + { + Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key)); + } + + $certificates = $attestation['attStmt']['x5c']; + Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with one certificate.'); + Assertion::count($certificates, 1, 'The attestation statement value "x5c" must be a list with one certificate.'); + Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with one certificate.'); + + reset($certificates); + $certificates = CertificateToolbox::convertAllDERToPEM($certificates); + $this->checkCertificate($certificates[0]); + + return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates)); + } + + /** + * @param string $clientDataJSONHash Obvious + * @param AttestationStatement $attestationStatement Obvious + * @param AuthenticatorData $authenticatorData Obvious + * + * @return boolean + * @throws \Assert\AssertionFailedException + * @since __DEPLOY_VERSION__ + */ + public function isValid( + string $clientDataJSONHash, + AttestationStatement $attestationStatement, + AuthenticatorData $authenticatorData + ): bool + { + Assertion::eq( + $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(), + '00000000-0000-0000-0000-000000000000', + 'Invalid AAGUID for fido-u2f attestation statement. Shall be "00000000-0000-0000-0000-000000000000"' + ); + + if ($this->metadataStatementRepository !== null) + { + CertificateToolbox::checkAttestationMedata( + $attestationStatement, + $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(), + [], + $this->metadataStatementRepository + ); + } + + $trustPath = $attestationStatement->getTrustPath(); + Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path'); + $dataToVerify = "\0"; + $dataToVerify .= $authenticatorData->getRpIdHash(); + $dataToVerify .= $clientDataJSONHash; + $dataToVerify .= $authenticatorData->getAttestedCredentialData()->getCredentialId(); + $dataToVerify .= $this->extractPublicKey($authenticatorData->getAttestedCredentialData()->getCredentialPublicKey()); + + return openssl_verify($dataToVerify, $attestationStatement->get('sig'), $trustPath->getCertificates()[0], OPENSSL_ALGO_SHA256) === 1; + } + + /** + * @param string|null $publicKey Obvious + * + * @return string + * @throws \Assert\AssertionFailedException + * @since __DEPLOY_VERSION__ + */ + private function extractPublicKey(?string $publicKey): string + { + Assertion::notNull($publicKey, 'The attested credential data does not contain a valid public key.'); + + $publicKeyStream = new StringStream($publicKey); + $coseKey = $this->decoder->decode($publicKeyStream); + Assertion::true($publicKeyStream->isEOF(), 'Invalid public key. Presence of extra bytes.'); + $publicKeyStream->close(); + Assertion::isInstanceOf($coseKey, MapObject::class, 'The attested credential data does not contain a valid public key.'); + + $coseKey = $coseKey->getNormalizedData(); + $ec2Key = new Ec2Key($coseKey + [Ec2Key::TYPE => 2, Ec2Key::DATA_CURVE => Ec2Key::CURVE_P256]); + + return "\x04" . $ec2Key->x() . $ec2Key->y(); + } + + /** + * @param string $publicKey Obvious + * + * @return void + * @throws \Assert\AssertionFailedException + * @since __DEPLOY_VERSION__ + */ + private function checkCertificate(string $publicKey): void + { + try + { + $resource = openssl_pkey_get_public($publicKey); + + if (version_compare(PHP_VERSION, '8.0', 'lt')) + { + Assertion::isResource($resource, 'Unable to read the certificate'); + } + else + { + /** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */ + Assertion::isInstanceOf($resource, \OpenSSLAsymmetricKey::class, 'Unable to read the certificate'); + } + } + catch (\Throwable $throwable) + { + throw new \InvalidArgumentException('Invalid certificate or certificate chain', 0, $throwable); + } + + $details = openssl_pkey_get_details($resource); + Assertion::keyExists($details, 'ec', 'Invalid certificate or certificate chain'); + Assertion::keyExists($details['ec'], 'curve_name', 'Invalid certificate or certificate chain'); + Assertion::eq($details['ec']['curve_name'], 'prime256v1', 'Invalid certificate or certificate chain'); + Assertion::keyExists($details['ec'], 'curve_oid', 'Invalid certificate or certificate chain'); + Assertion::eq($details['ec']['curve_oid'], '1.2.840.10045.3.1.7', 'Invalid certificate or certificate chain'); + } +} diff --git a/plugins/system/webauthn/src/Hotfix/Server.php b/plugins/system/webauthn/src/Hotfix/Server.php new file mode 100644 index 0000000000000..f44820b29d34b --- /dev/null +++ b/plugins/system/webauthn/src/Hotfix/Server.php @@ -0,0 +1,452 @@ + + * @license MIT; see libraries/vendor/web-auth/webauthn-lib/LICENSE + */ + +namespace Joomla\Plugin\System\Webauthn\Hotfix; + +// Protect from unauthorized access +defined('_JEXEC') or die(); + +use Assert\Assertion; +use Cose\Algorithm\Algorithm; +use Cose\Algorithm\ManagerFactory; +use Cose\Algorithm\Signature\ECDSA; +use Cose\Algorithm\Signature\EdDSA; +use Cose\Algorithm\Signature\RSA; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\ServerRequestInterface; +use Webauthn\AttestationStatement\AndroidSafetyNetAttestationStatementSupport; +use Webauthn\AttestationStatement\AttestationObjectLoader; +use Webauthn\AttestationStatement\AttestationStatementSupportManager; +use Webauthn\AttestationStatement\NoneAttestationStatementSupport; +use Webauthn\AttestationStatement\PackedAttestationStatementSupport; +use Webauthn\AttestationStatement\TPMAttestationStatementSupport; +use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs; +use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler; +use Webauthn\AuthenticatorAssertionResponse; +use Webauthn\AuthenticatorAssertionResponseValidator; +use Webauthn\AuthenticatorAttestationResponse; +use Webauthn\AuthenticatorAttestationResponseValidator; +use Webauthn\AuthenticatorSelectionCriteria; +use Webauthn\MetadataService\MetadataStatementRepository; +use Webauthn\PublicKeyCredentialCreationOptions; +use Webauthn\PublicKeyCredentialDescriptor; +use Webauthn\PublicKeyCredentialLoader; +use Webauthn\PublicKeyCredentialParameters; +use Webauthn\PublicKeyCredentialRequestOptions; +use Webauthn\PublicKeyCredentialRpEntity; +use Webauthn\PublicKeyCredentialSource; +use Webauthn\PublicKeyCredentialSourceRepository; +use Webauthn\PublicKeyCredentialUserEntity; +use Webauthn\TokenBinding\TokenBindingNotSupportedHandler; + +/** + * Customised WebAuthn server object. + * + * We had to fork the server object from the WebAuthn server package to address an issue with PHP 8. + * + * We are currently using an older version of the WebAuthn library (2.x) which was written before + * PHP 8 was developed. We cannot upgrade the WebAuthn library to a newer major version because of + * Joomla's Semantic Versioning promise. + * + * The FidoU2FAttestationStatementSupport and AndroidKeyAttestationStatementSupport classes force + * an assertion on the result of the openssl_pkey_get_public() function, assuming it will return a + * resource. However, starting with PHP 8.0 this function returns an OpenSSLAsymmetricKey object + * and the assertion fails. As a result, you cannot use Android or FIDO U2F keys with WebAuthn. + * + * The assertion check is in a private method, therefore we have to fork both attestation support + * classes to change the assertion. The assertion takes place through a third party library we + * cannot (and should not!) modify. + * + * The assertions objects, however, are injected to the attestation support manager in a private + * method of the Server object. Because literally everything in this class is private we have no + * option than to fork the entire class to apply our two forked attestation support classes. + * + * This is marked as deprecated because we'll be able to upgrade the WebAuthn library on Joomla 5. + * + * @since __DEPLOY_VERSION__ + * + * @deprecated 5.0 We will upgrade the WebAuthn library to version 3 or later and this will go away. + */ +final class Server extends \Webauthn\Server +{ + /** + * @var integer + * @since __DEPLOY_VERSION__ + */ + public $timeout = 60000; + + /** + * @var integer + * @since __DEPLOY_VERSION__ + */ + public $challengeSize = 32; + + /** + * @var PublicKeyCredentialRpEntity + * @since __DEPLOY_VERSION__ + */ + private $rpEntity; + + /** + * @var ManagerFactory + * @since __DEPLOY_VERSION__ + */ + private $coseAlgorithmManagerFactory; + + /** + * @var PublicKeyCredentialSourceRepository + * @since __DEPLOY_VERSION__ + */ + private $publicKeyCredentialSourceRepository; + + /** + * @var TokenBindingNotSupportedHandler + * @since __DEPLOY_VERSION__ + */ + private $tokenBindingHandler; + + /** + * @var ExtensionOutputCheckerHandler + * @since __DEPLOY_VERSION__ + */ + private $extensionOutputCheckerHandler; + + /** + * @var string[] + * @since __DEPLOY_VERSION__ + */ + private $selectedAlgorithms; + + /** + * @var MetadataStatementRepository|null + * @since __DEPLOY_VERSION__ + */ + private $metadataStatementRepository; + + /** + * @var ClientInterface + * @since __DEPLOY_VERSION__ + */ + private $httpClient; + + /** + * @var string + * @since __DEPLOY_VERSION__ + */ + private $googleApiKey; + + /** + * @var RequestFactoryInterface + * @since __DEPLOY_VERSION__ + */ + private $requestFactory; + + /** + * Overridden constructor. + * + * @param PublicKeyCredentialRpEntity $relayingParty Obvious + * @param PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository Obvious + * @param MetadataStatementRepository|null $metadataStatementRepository Obvious + * + * @since __DEPLOY_VERSION__ + */ + public function __construct( + PublicKeyCredentialRpEntity $relayingParty, + PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, + ?MetadataStatementRepository $metadataStatementRepository + ) + { + $this->rpEntity = $relayingParty; + + $this->coseAlgorithmManagerFactory = new ManagerFactory; + $this->coseAlgorithmManagerFactory->add('RS1', new RSA\RS1); + $this->coseAlgorithmManagerFactory->add('RS256', new RSA\RS256); + $this->coseAlgorithmManagerFactory->add('RS384', new RSA\RS384); + $this->coseAlgorithmManagerFactory->add('RS512', new RSA\RS512); + $this->coseAlgorithmManagerFactory->add('PS256', new RSA\PS256); + $this->coseAlgorithmManagerFactory->add('PS384', new RSA\PS384); + $this->coseAlgorithmManagerFactory->add('PS512', new RSA\PS512); + $this->coseAlgorithmManagerFactory->add('ES256', new ECDSA\ES256); + $this->coseAlgorithmManagerFactory->add('ES256K', new ECDSA\ES256K); + $this->coseAlgorithmManagerFactory->add('ES384', new ECDSA\ES384); + $this->coseAlgorithmManagerFactory->add('ES512', new ECDSA\ES512); + $this->coseAlgorithmManagerFactory->add('Ed25519', new EdDSA\Ed25519); + + $this->selectedAlgorithms = ['RS256', 'RS512', 'PS256', 'PS512', 'ES256', 'ES512', 'Ed25519']; + $this->publicKeyCredentialSourceRepository = $publicKeyCredentialSourceRepository; + $this->tokenBindingHandler = new TokenBindingNotSupportedHandler; + $this->extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler; + $this->metadataStatementRepository = $metadataStatementRepository; + } + + /** + * @param string[] $selectedAlgorithms Obvious + * + * @return void + * @since __DEPLOY_VERSION__ + */ + public function setSelectedAlgorithms(array $selectedAlgorithms): void + { + $this->selectedAlgorithms = $selectedAlgorithms; + } + + /** + * @param TokenBindingNotSupportedHandler $tokenBindingHandler Obvious + * + * @return void + * @since __DEPLOY_VERSION__ + */ + public function setTokenBindingHandler(TokenBindingNotSupportedHandler $tokenBindingHandler): void + { + $this->tokenBindingHandler = $tokenBindingHandler; + } + + /** + * @param string $alias Obvious + * @param Algorithm $algorithm Obvious + * + * @return void + * @since __DEPLOY_VERSION__ + */ + public function addAlgorithm(string $alias, Algorithm $algorithm): void + { + $this->coseAlgorithmManagerFactory->add($alias, $algorithm); + $this->selectedAlgorithms[] = $alias; + $this->selectedAlgorithms = array_unique($this->selectedAlgorithms); + } + + /** + * @param ExtensionOutputCheckerHandler $extensionOutputCheckerHandler Obvious + * + * @return void + * @since __DEPLOY_VERSION__ + */ + public function setExtensionOutputCheckerHandler(ExtensionOutputCheckerHandler $extensionOutputCheckerHandler): void + { + $this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler; + } + + /** + * @param string|null $userVerification Obvious + * @param PublicKeyCredentialDescriptor[] $allowedPublicKeyDescriptors Obvious + * @param AuthenticationExtensionsClientInputs|null $extensions Obvious + * + * @return PublicKeyCredentialRequestOptions + * @throws \Exception + * @since __DEPLOY_VERSION__ + */ + public function generatePublicKeyCredentialRequestOptions( + ?string $userVerification = PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, + array $allowedPublicKeyDescriptors = [], + ?AuthenticationExtensionsClientInputs $extensions = null + ): PublicKeyCredentialRequestOptions + { + return new PublicKeyCredentialRequestOptions( + random_bytes($this->challengeSize), + $this->timeout, + $this->rpEntity->getId(), + $allowedPublicKeyDescriptors, + $userVerification, + $extensions ?? new AuthenticationExtensionsClientInputs + ); + } + + /** + * @param PublicKeyCredentialUserEntity $userEntity Obvious + * @param string|null $attestationMode Obvious + * @param PublicKeyCredentialDescriptor[] $excludedPublicKeyDescriptors Obvious + * @param AuthenticatorSelectionCriteria|null $criteria Obvious + * @param AuthenticationExtensionsClientInputs|null $extensions Obvious + * + * @return PublicKeyCredentialCreationOptions + * @throws \Exception + * @since __DEPLOY_VERSION__ + */ + public function generatePublicKeyCredentialCreationOptions( + PublicKeyCredentialUserEntity $userEntity, + ?string $attestationMode = PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE, + array $excludedPublicKeyDescriptors = [], + ?AuthenticatorSelectionCriteria $criteria = null, + ?AuthenticationExtensionsClientInputs $extensions = null + ): PublicKeyCredentialCreationOptions + { + $coseAlgorithmManager = $this->coseAlgorithmManagerFactory->create($this->selectedAlgorithms); + $publicKeyCredentialParametersList = []; + + foreach ($coseAlgorithmManager->all() as $algorithm) + { + $publicKeyCredentialParametersList[] = new PublicKeyCredentialParameters( + PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, + $algorithm::identifier() + ); + } + + $criteria = $criteria ?? new AuthenticatorSelectionCriteria; + $extensions = $extensions ?? new AuthenticationExtensionsClientInputs; + $challenge = random_bytes($this->challengeSize); + + return new PublicKeyCredentialCreationOptions( + $this->rpEntity, + $userEntity, + $challenge, + $publicKeyCredentialParametersList, + $this->timeout, + $excludedPublicKeyDescriptors, + $criteria, + $attestationMode, + $extensions + ); + } + + /** + * @param string $data Obvious + * @param PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions Obvious + * @param ServerRequestInterface $serverRequest Obvious + * + * @return PublicKeyCredentialSource + * @throws \Assert\AssertionFailedException + * @since __DEPLOY_VERSION__ + */ + public function loadAndCheckAttestationResponse( + string $data, + PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, + ServerRequestInterface $serverRequest + ): PublicKeyCredentialSource + { + $attestationStatementSupportManager = $this->getAttestationStatementSupportManager(); + $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager); + $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader); + + $publicKeyCredential = $publicKeyCredentialLoader->load($data); + $authenticatorResponse = $publicKeyCredential->getResponse(); + Assertion::isInstanceOf($authenticatorResponse, AuthenticatorAttestationResponse::class, 'Not an authenticator attestation response'); + + $authenticatorAttestationResponseValidator = new AuthenticatorAttestationResponseValidator( + $attestationStatementSupportManager, + $this->publicKeyCredentialSourceRepository, + $this->tokenBindingHandler, + $this->extensionOutputCheckerHandler + ); + + return $authenticatorAttestationResponseValidator->check($authenticatorResponse, $publicKeyCredentialCreationOptions, $serverRequest); + } + + /** + * @param string $data Obvious + * @param PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions Obvious + * @param PublicKeyCredentialUserEntity|null $userEntity Obvious + * @param ServerRequestInterface $serverRequest Obvious + * + * @return PublicKeyCredentialSource + * @throws \Assert\AssertionFailedException + * @since __DEPLOY_VERSION__ + */ + public function loadAndCheckAssertionResponse( + string $data, + PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, + ?PublicKeyCredentialUserEntity $userEntity, + ServerRequestInterface $serverRequest + ): PublicKeyCredentialSource + { + $attestationStatementSupportManager = $this->getAttestationStatementSupportManager(); + $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager); + $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader); + + $publicKeyCredential = $publicKeyCredentialLoader->load($data); + $authenticatorResponse = $publicKeyCredential->getResponse(); + Assertion::isInstanceOf($authenticatorResponse, AuthenticatorAssertionResponse::class, 'Not an authenticator assertion response'); + + $authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator( + $this->publicKeyCredentialSourceRepository, + null, + $this->tokenBindingHandler, + $this->extensionOutputCheckerHandler, + $this->coseAlgorithmManagerFactory->create($this->selectedAlgorithms) + ); + + return $authenticatorAssertionResponseValidator->check( + $publicKeyCredential->getRawId(), + $authenticatorResponse, + $publicKeyCredentialRequestOptions, + $serverRequest, + null !== $userEntity ? $userEntity->getId() : null + ); + } + + /** + * @param ClientInterface $client Obvious + * @param string $apiKey Obvious + * @param RequestFactoryInterface $requestFactory Obvious + * + * @return void + * @since __DEPLOY_VERSION__ + */ + public function enforceAndroidSafetyNetVerification( + ClientInterface $client, + string $apiKey, + RequestFactoryInterface $requestFactory + ): void + { + $this->httpClient = $client; + $this->googleApiKey = $apiKey; + $this->requestFactory = $requestFactory; + } + + /** + * @return AttestationStatementSupportManager + * @since __DEPLOY_VERSION__ + */ + private function getAttestationStatementSupportManager(): AttestationStatementSupportManager + { + $attestationStatementSupportManager = new AttestationStatementSupportManager; + $attestationStatementSupportManager->add(new NoneAttestationStatementSupport); + + if ($this->metadataStatementRepository !== null) + { + $coseAlgorithmManager = $this->coseAlgorithmManagerFactory->create($this->selectedAlgorithms); + $attestationStatementSupportManager->add(new FidoU2FAttestationStatementSupport(null, $this->metadataStatementRepository)); + + /** + * Work around a third party library (web-token/jwt-signature-algorithm-eddsa) bug. + * + * On PHP 8 libsodium is compiled into PHP, it is not an extension. However, the third party library does + * not check if the libsodium function are available; it checks if the "sodium" extension is loaded. This of + * course causes an immediate failure with a Runtime exception EVEN IF the attested data isn't attested by + * Android Safety Net. Therefore we have to not even load the AndroidSafetyNetAttestationStatementSupport + * class in this case... + */ + if (function_exists('sodium_crypto_sign_seed_keypair') && function_exists('extension_loaded') && extension_loaded('sodium')) + { + $attestationStatementSupportManager->add( + new AndroidSafetyNetAttestationStatementSupport( + $this->httpClient, + $this->googleApiKey, + $this->requestFactory, + 2000, + 60000, + $this->metadataStatementRepository + ) + ); + } + + $attestationStatementSupportManager->add(new AndroidKeyAttestationStatementSupport(null, $this->metadataStatementRepository)); + $attestationStatementSupportManager->add(new TPMAttestationStatementSupport($this->metadataStatementRepository)); + $attestationStatementSupportManager->add( + new PackedAttestationStatementSupport( + null, + $coseAlgorithmManager, + $this->metadataStatementRepository + ) + ); + } + + return $attestationStatementSupportManager; + } +} diff --git a/plugins/system/webauthn/src/MetadataRepository.php b/plugins/system/webauthn/src/MetadataRepository.php new file mode 100644 index 0000000000000..65e5ae190726e --- /dev/null +++ b/plugins/system/webauthn/src/MetadataRepository.php @@ -0,0 +1,246 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Plugin\System\Webauthn; + +// Protect from unauthorized access +defined('_JEXEC') or die(); + +use Exception; +use Joomla\CMS\Date\Date; +use Joomla\CMS\Http\HttpFactory; +use Lcobucci\JWT\Configuration; +use Lcobucci\JWT\Token\Plain; +use Webauthn\MetadataService\MetadataStatement; +use Webauthn\MetadataService\MetadataStatementRepository; +use function defined; + +/** + * Authenticator metadata repository. + * + * This repository contains the metadata of all FIDO authenticators as published by the FIDO + * Alliance in their MDS version 3.0. + * + * @see https://fidoalliance.org/metadata/ + * @since __DEPLOY_VERSION__ + */ +final class MetadataRepository implements MetadataStatementRepository +{ + /** + * Cache of authenticator metadata statements + * + * @var MetadataStatement[] + * @since __DEPLOY_VERSION__ + */ + private $mdsCache = []; + + /** + * Map of AAGUID to $mdsCache index + * + * @var array + * @since __DEPLOY_VERSION__ + */ + private $mdsMap = []; + + /** + * Public constructor. + * + * @since __DEPLOY_VERSION__ + */ + public function __construct() + { + $this->load(); + } + + /** + * Find an authenticator metadata statement given an AAGUID + * + * @param string $aaguid The AAGUID to find + * + * @return MetadataStatement|null The metadata statement; null if the AAGUID is unknown + * @since __DEPLOY_VERSION__ + */ + public function findOneByAAGUID(string $aaguid): ?MetadataStatement + { + $idx = $this->mdsMap[$aaguid] ?? null; + + return $idx ? $this->mdsCache[$idx] : null; + } + + /** + * Get basic information of the known FIDO authenticators by AAGUID + * + * @return object[] + * @since __DEPLOY_VERSION__ + */ + public function getKnownAuthenticators(): array + { + $mapKeys = function (MetadataStatement $meta) + { + return $meta->getAaguid(); + }; + $mapvalues = function (MetadataStatement $meta) + { + return $meta->getAaguid() ? (object) [ + 'description' => $meta->getDescription(), + 'icon' => $meta->getIcon(), + ] : null; + }; + $keys = array_map($mapKeys, $this->mdsCache); + $values = array_map($mapvalues, $this->mdsCache); + $return = array_combine($keys, $values) ?: []; + + $filter = function ($x) + { + return !empty($x); + }; + + return array_filter($return, $filter); + } + + /** + * Load the authenticator metadata cache + * + * @param bool $force Force reload from the web service + * + * @return void + * @since __DEPLOY_VERSION__ + */ + private function load(bool $force = false): void + { + $this->mdsCache = []; + $this->mdsMap = []; + $jwtFilename = JPATH_CACHE . '/fido.jwt'; + + // If the file exists and it's over one month old do retry loading it. + if (file_exists($jwtFilename) && filemtime($jwtFilename) < (time() - 2592000)) + { + $force = true; + } + + /** + * Try to load the MDS source from the FIDO Alliance and cache it. + * + * We use a short timeout limit to avoid delaying the page load for way too long. If we fail + * to download the file in a reasonable amount of time we write an empty string in the + * file which causes this method to not proceed any further. + */ + if (!file_exists($jwtFilename) || $force) + { + // Only try to download anything if we can actually cache it! + if ((file_exists($jwtFilename) && is_writable($jwtFilename)) || (!file_exists($jwtFilename) && is_writable(JPATH_CACHE))) + { + $http = HttpFactory::getHttp(); + $response = $http->get('https://mds.fidoalliance.org/', [], 5); + $content = ($response->code < 200 || $response->code > 299) ? '' : $response->body; + } + + /** + * If we could not download anything BUT a non-empty file already exists we must NOT + * overwrite it. + * + * This allows, for example, the site owner to manually place the FIDO MDS cache file + * in administrator/cache/fido.jwt. This would be useful for high security sites which + * require attestation BUT are behind a firewall (or disconnected from the Internet), + * therefore cannot download the MDS cache! + */ + if (!empty($content) || !file_exists($jwtFilename) || filesize($jwtFilename) <= 1024) + { + file_put_contents($jwtFilename, $content); + } + } + + $rawJwt = file_get_contents($jwtFilename); + + if (!is_string($rawJwt) || strlen($rawJwt) < 1024) + { + return; + } + + try + { + $jwtConfig = Configuration::forUnsecuredSigner(); + $token = $jwtConfig->parser()->parse($rawJwt); + } + catch (Exception $e) + { + return; + } + + if (!($token instanceof Plain)) + { + return; + } + + unset($rawJwt); + + // Do I need to forcibly update the cache? The JWT has the nextUpdate claim to tell us when to do that. + try + { + $nextUpdate = new Date($token->claims()->get('nextUpdate', '2020-01-01')); + + if (!$force && !$nextUpdate->diff(new Date)->invert) + { + $this->load(true); + + return; + } + } + catch (Exception $e) + { + // OK, don't worry if don't know when the next update is. + } + + $entriesMapper = function (object $entry) + { + try + { + $array = json_decode(json_encode($entry->metadataStatement), true); + + /** + * This prevents an error when we're asking for attestation on authenticators which + * don't allow it. We are really not interested in the attestation per se, but + * requiring an attestation is the only way we can get the AAGUID of the + * authenticator. + */ + if (isset($array['attestationTypes'])) + { + unset($array['attestationTypes']); + } + + return MetadataStatement::createFromArray($array); + } + catch (Exception $e) + { + return null; + } + }; + $entries = array_map($entriesMapper, $token->claims()->get('entries', [])); + + unset($token); + + $entriesFilter = function ($x) + { + return !empty($x); + }; + $this->mdsCache = array_filter($entries, $entriesFilter); + + foreach ($this->mdsCache as $idx => $meta) + { + $aaguid = $meta->getAaguid(); + + if (empty($aaguid)) + { + continue; + } + + $this->mdsMap[$aaguid] = $idx; + } + } +} diff --git a/plugins/system/webauthn/src/PluginTraits/AdditionalLoginButtons.php b/plugins/system/webauthn/src/PluginTraits/AdditionalLoginButtons.php index 42efc2565fd21..f3775a555ab76 100644 --- a/plugins/system/webauthn/src/PluginTraits/AdditionalLoginButtons.php +++ b/plugins/system/webauthn/src/PluginTraits/AdditionalLoginButtons.php @@ -1,10 +1,10 @@ - * @license GNU General Public License version 2 or later; see LICENSE.txt + * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\System\Webauthn\PluginTraits; @@ -13,13 +13,14 @@ \defined('_JEXEC') or die(); use Exception; -use Joomla\CMS\Factory; +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Document\HtmlDocument; use Joomla\CMS\Helper\AuthenticationHelper; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\UserHelper; -use Joomla\Plugin\System\Webauthn\Helper\Joomla; +use Joomla\Event\Event; /** * Inserts Webauthn buttons into login modules @@ -29,7 +30,8 @@ trait AdditionalLoginButtons { /** - * Do I need to I inject buttons? Automatically detected (i.e. disabled if I'm already logged in). + * Do I need to inject buttons? Automatically detected (i.e. disabled if I'm already logged + * in). * * @var boolean|null * @since 4.0.0 @@ -44,6 +46,57 @@ trait AdditionalLoginButtons */ private $injectedCSSandJS = false; + /** + * Creates additional login buttons + * + * @param Event $event The event we are handling + * + * @return void + * + * @see AuthenticationHelper::getLoginButtons() + * + * @since 4.0.0 + */ + public function onUserLoginButtons(Event $event): void + { + /** @var string $form The HTML ID of the form we are enclosed in */ + [$form] = $event->getArguments(); + + // If we determined we should not inject a button return early + if (!$this->mustDisplayButton()) + { + return; + } + + // Load necessary CSS and Javascript files + $this->addLoginCSSAndJavascript(); + + // Unique ID for this button (allows display of multiple modules on the page) + $randomId = 'plg_system_webauthn-' . + UserHelper::genRandomPassword(12) . '-' . UserHelper::genRandomPassword(8); + + // Get local path to image + $image = HTMLHelper::_('image', 'plg_system_webauthn/webauthn.svg', '', '', true, true); + + // If you can't find the image then skip it + $image = $image ? JPATH_ROOT . substr($image, \strlen(Uri::root(true))) : ''; + + // Extract image if it exists + $image = file_exists($image) ? file_get_contents($image) : ''; + + $this->returnFromEvent($event, [ + [ + 'label' => 'PLG_SYSTEM_WEBAUTHN_LOGIN_LABEL', + 'tooltip' => 'PLG_SYSTEM_WEBAUTHN_LOGIN_DESC', + 'id' => $randomId, + 'data-webauthn-form' => $form, + 'svg' => $image, + 'class' => 'plg_system_webauthn_login_button', + ], + ] + ); + } + /** * Should I allow this plugin to add a WebAuthn login button? * @@ -53,6 +106,24 @@ trait AdditionalLoginButtons */ private function mustDisplayButton(): bool { + // We must have a valid application + if (!($this->getApplication() instanceof CMSApplication)) + { + return false; + } + + // This plugin only applies to the frontend and administrator applications + if (!$this->getApplication()->isClient('site') && !$this->getApplication()->isClient('administrator')) + { + return false; + } + + // We must have a valid user + if (empty($this->getApplication()->getIdentity())) + { + return false; + } + if (\is_null($this->allowButtonDisplay)) { $this->allowButtonDisplay = false; @@ -60,35 +131,24 @@ private function mustDisplayButton(): bool /** * Do not add a WebAuthn login button if we are already logged in */ - try - { - if (!Factory::getApplication()->getIdentity()->guest) - { - return false; - } - } - catch (Exception $e) + if (!$this->getApplication()->getIdentity()->guest) { return false; } /** - * Don't try to show a button if we can't figure out if this is a front- or backend page (it's probably a - * CLI or custom application). + * Only display a button on HTML output */ try { - Joomla::isAdminPage(); + $document = $this->getApplication()->getDocument(); } catch (Exception $e) { - return false; + $document = null; } - /** - * Only display a button on HTML output - */ - if (Joomla::getDocumentType() != 'html') + if (!($document instanceof HtmlDocument)) { return false; } @@ -109,65 +169,6 @@ private function mustDisplayButton(): bool return $this->allowButtonDisplay; } - /** - * Creates additional login buttons - * - * @param string $form The HTML ID of the form we are enclosed in - * - * @return array - * - * @throws Exception - * - * @see AuthenticationHelper::getLoginButtons() - * - * @since 4.0.0 - */ - public function onUserLoginButtons(string $form): array - { - // If we determined we should not inject a button return early - if (!$this->mustDisplayButton()) - { - return []; - } - - // Load the language files - $this->loadLanguage(); - - // Load necessary CSS and Javascript files - $this->addLoginCSSAndJavascript(); - - // Return URL - $uri = new Uri(Uri::base() . 'index.php'); - $uri->setVar(Joomla::getToken(), '1'); - - // Unique ID for this button (allows display of multiple modules on the page) - $randomId = 'plg_system_webauthn-' . UserHelper::genRandomPassword(12) . '-' . UserHelper::genRandomPassword(8); - - // Set up the JavaScript callback - $url = $uri->toString(); - - // Get local path to image - $image = HTMLHelper::_('image', 'plg_system_webauthn/webauthn.svg', '', '', true, true); - - // If you can't find the image then skip it - $image = $image ? JPATH_ROOT . substr($image, \strlen(Uri::root(true))) : ''; - - // Extract image if it exists - $image = file_exists($image) ? file_get_contents($image) : ''; - - return [ - [ - 'label' => 'PLG_SYSTEM_WEBAUTHN_LOGIN_LABEL', - 'tooltip' => 'PLG_SYSTEM_WEBAUTHN_LOGIN_DESC', - 'id' => $randomId, - 'data-webauthn-form' => $form, - 'data-webauthn-url' => $url, - 'svg' => $image, - 'class' => 'plg_system_webauthn_login_button', - ], - ]; - } - /** * Injects the WebAuthn CSS and Javascript for frontend logins, but only once per page load. * @@ -186,7 +187,7 @@ private function addLoginCSSAndJavascript(): void $this->injectedCSSandJS = true; /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + $wa = $this->getApplication()->getDocument()->getWebAssetManager(); if (!$wa->assetExists('style', 'plg_system_webauthn.button')) { @@ -207,7 +208,7 @@ private function addLoginCSSAndJavascript(): void Text::script('PLG_SYSTEM_WEBAUTHN_ERR_INVALID_USERNAME'); // Store the current URL as the default return URL after login (or failure) - Joomla::setSessionVar('returnUrl', Uri::current(), 'plg_system_webauthn'); + $this->getApplication()->getSession()->set('plg_system_webauthn.returnUrl', Uri::current()); } } diff --git a/plugins/system/webauthn/src/PluginTraits/AjaxHandler.php b/plugins/system/webauthn/src/PluginTraits/AjaxHandler.php index 5b81697b24aee..2a171f95e26c6 100644 --- a/plugins/system/webauthn/src/PluginTraits/AjaxHandler.php +++ b/plugins/system/webauthn/src/PluginTraits/AjaxHandler.php @@ -1,10 +1,10 @@ - * @license GNU General Public License version 2 or later; see LICENSE.txt + * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\System\Webauthn\PluginTraits; @@ -14,17 +14,26 @@ use Exception; use Joomla\CMS\Application\CMSApplication; -use Joomla\CMS\Factory; +use Joomla\CMS\Event\AbstractEvent; +use Joomla\CMS\Event\GenericEvent; +use Joomla\CMS\Event\Plugin\System\Webauthn\Ajax; +use Joomla\CMS\Event\Plugin\System\Webauthn\Ajax as PlgSystemWebauthnAjax; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxChallenge as PlgSystemWebauthnAjaxChallenge; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxCreate as PlgSystemWebauthnAjaxCreate; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxDelete as PlgSystemWebauthnAjaxDelete; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxInitCreate as PlgSystemWebauthnAjaxInitCreate; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxLogin as PlgSystemWebauthnAjaxLogin; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxSaveLabel as PlgSystemWebauthnAjaxSaveLabel; +use Joomla\CMS\Event\Result\ResultAwareInterface; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Uri\Uri; -use Joomla\Plugin\System\Webauthn\Exception\AjaxNonCmsAppException; -use Joomla\Plugin\System\Webauthn\Helper\Joomla; +use Joomla\Event\Event; use RuntimeException; /** - * Allows the plugin to handle AJAX requests in the backend of the site, where com_ajax is not available when we are not - * logged in. + * Allows the plugin to handle AJAX requests in the backend of the site, where com_ajax is not + * available when we are not logged in. * * @since 4.0.0 */ @@ -33,41 +42,39 @@ trait AjaxHandler /** * Processes the callbacks from the passwordless login views. * - * Note: this method is called from Joomla's com_ajax or, in the case of backend logins, through the special - * onAfterInitialize handler we have created to work around com_ajax usage limitations in the backend. + * Note: this method is called from Joomla's com_ajax or, in the case of backend logins, + * through the special onAfterInitialize handler we have created to work around com_ajax usage + * limitations in the backend. + * + * @param Event $event The event we are handling * * @return void * * @throws Exception - * * @since 4.0.0 */ - public function onAjaxWebauthn(): void + public function onAjaxWebauthn(Ajax $event): void { - // Load the language files - $this->loadLanguage(); - - /** @var CMSApplication $app */ - $app = Factory::getApplication(); - $input = $app->input; + $input = $this->getApplication()->input; // Get the return URL from the session - $returnURL = Joomla::getSessionVar('returnUrl', Uri::base(), 'plg_system_webauthn'); - $result = null; + $returnURL = $this->getApplication()->getSession()->get('plg_system_webauthn.returnUrl', Uri::base()); + $result = null; try { - Joomla::log('system', "Received AJAX callback."); + Log::add("Received AJAX callback.", Log::DEBUG, 'webauthn.system'); - if (!($app instanceof CMSApplication)) + if (!($this->getApplication() instanceof CMSApplication)) { - throw new AjaxNonCmsAppException; + Log::add("This is not a CMS application", Log::NOTICE, 'webauthn.system'); + + return; } $akaction = $input->getCmd('akaction'); - $token = Joomla::getToken(); - if ($input->getInt($token, 0) != 1) + if (!$this->getApplication()->checkToken('request')) { throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR')); } @@ -79,32 +86,62 @@ public function onAjaxWebauthn(): void } // Call the plugin event onAjaxWebauthnSomething where Something is the akaction param. - $eventName = 'onAjaxWebauthn' . ucfirst($akaction); + /** @var AbstractEvent|ResultAwareInterface $triggerEvent */ + $eventName = 'onAjaxWebauthn' . ucfirst($akaction); - $results = $app->triggerEvent($eventName, []); - - foreach ($results as $r) + switch ($eventName) { - if (\is_null($r)) - { - continue; - } + case 'onAjaxWebauthn': + $eventClass = PlgSystemWebauthnAjax::class; + break; - $result = $r; + case 'onAjaxWebauthnChallenge': + $eventClass = PlgSystemWebauthnAjaxChallenge::class; + break; + + case 'onAjaxWebauthnCreate': + $eventClass = PlgSystemWebauthnAjaxCreate::class; + break; + + case 'onAjaxWebauthnDelete': + $eventClass = PlgSystemWebauthnAjaxDelete::class; + break; + + case 'onAjaxWebauthnInitcreate': + $eventClass = PlgSystemWebauthnAjaxInitCreate::class; + break; - break; + case 'onAjaxWebauthnLogin': + $eventClass = PlgSystemWebauthnAjaxLogin::class; + break; + + case 'onAjaxWebauthnSavelabel': + $eventClass = PlgSystemWebauthnAjaxSaveLabel::class; + break; + + default: + $eventClass = GenericEvent::class; + break; } - } - catch (AjaxNonCmsAppException $e) - { - Joomla::log('system', "This is not a CMS application", Log::NOTICE); + + $triggerEvent = new $eventClass($eventName, []); + $result = $this->getApplication()->getDispatcher()->dispatch($eventName, $triggerEvent); + $results = ($result instanceof ResultAwareInterface) ? ($result['result'] ?? []) : []; + $result = array_reduce( + $results, + function ($carry, $result) + { + return $carry ?? $result; + }, + null + ); } catch (Exception $e) { - Joomla::log('system', "Callback failure, redirecting to $returnURL."); - Joomla::setSessionVar('returnUrl', null, 'plg_system_webauthn'); - $app->enqueueMessage($e->getMessage(), 'error'); - $app->redirect($returnURL); + Log::add("Callback failure, redirecting to $returnURL.", Log::DEBUG, 'webauthn.system'); + $this->getApplication()->getSession()->set('plg_system_webauthn.returnUrl', null); + $this->getApplication()->enqueueMessage($e->getMessage(), 'error'); + $this->getApplication()->redirect($returnURL); return; } @@ -113,14 +150,8 @@ public function onAjaxWebauthn(): void { switch ($input->getCmd('encoding', 'json')) { - case 'jsonhash': - Joomla::log('system', "Callback complete, returning JSON inside ### markers."); - echo '###' . json_encode($result) . '###'; - - break; - case 'raw': - Joomla::log('system', "Callback complete, returning raw response."); + Log::add("Callback complete, returning raw response.", Log::DEBUG, 'webauthn.system'); echo $result; break; @@ -131,35 +162,35 @@ public function onAjaxWebauthn(): void if (isset($result['message'])) { $type = $result['type'] ?? 'info'; - $app->enqueueMessage($result['message'], $type); + $this->getApplication()->enqueueMessage($result['message'], $type); $modifiers = " and setting a system message of type $type"; } if (isset($result['url'])) { - Joomla::log('system', "Callback complete, performing redirection to {$result['url']}{$modifiers}."); - $app->redirect($result['url']); + Log::add("Callback complete, performing redirection to {$result['url']}{$modifiers}.", Log::DEBUG, 'webauthn.system'); + $this->getApplication()->redirect($result['url']); } - Joomla::log('system', "Callback complete, performing redirection to {$result}{$modifiers}."); - $app->redirect($result); + Log::add("Callback complete, performing redirection to {$result}{$modifiers}.", Log::DEBUG, 'webauthn.system'); + $this->getApplication()->redirect($result); return; default: - Joomla::log('system', "Callback complete, returning JSON."); + Log::add("Callback complete, returning JSON.", Log::DEBUG, 'webauthn.system'); echo json_encode($result); break; } - $app->close(200); + $this->getApplication()->close(200); } - Joomla::log('system', "Null response from AJAX callback, redirecting to $returnURL"); - Joomla::setSessionVar('returnUrl', null, 'plg_system_webauthn'); + Log::add("Null response from AJAX callback, redirecting to $returnURL", Log::DEBUG, 'webauthn.system'); + $this->getApplication()->getSession()->set('plg_system_webauthn.returnUrl', null); - $app->redirect($returnURL); + $this->getApplication()->redirect($returnURL); } } diff --git a/plugins/system/webauthn/src/PluginTraits/AjaxHandlerChallenge.php b/plugins/system/webauthn/src/PluginTraits/AjaxHandlerChallenge.php index 7123ee46a0d61..47182532a4471 100644 --- a/plugins/system/webauthn/src/PluginTraits/AjaxHandlerChallenge.php +++ b/plugins/system/webauthn/src/PluginTraits/AjaxHandlerChallenge.php @@ -13,17 +13,13 @@ \defined('_JEXEC') or die(); use Exception; -use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxChallenge; use Joomla\CMS\Factory; use Joomla\CMS\Uri\Uri; +use Joomla\CMS\User\User; +use Joomla\CMS\User\UserFactoryInterface; use Joomla\CMS\User\UserHelper; -use Joomla\Plugin\System\Webauthn\CredentialRepository; -use Joomla\Plugin\System\Webauthn\Helper\Joomla; -use Throwable; -use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs; -use Webauthn\PublicKeyCredentialRequestOptions; -use Webauthn\PublicKeyCredentialSource; -use Webauthn\PublicKeyCredentialUserEntity; +use Joomla\Event\Event; /** * Ajax handler for akaction=challenge @@ -39,27 +35,23 @@ trait AjaxHandlerChallenge * Returns the public key set for the user and a unique challenge in a Public Key Credential Request encoded as * JSON. * - * @return string A JSON-encoded object or JSON-encoded false if the username is invalid or no credentials stored + * @param AjaxChallenge $event The event we are handling * - * @throws Exception + * @return void * + * @throws Exception * @since 4.0.0 */ - public function onAjaxWebauthnChallenge() + public function onAjaxWebauthnChallenge(AjaxChallenge $event): void { - // Load the language files - $this->loadLanguage(); - // Initialize objects - /** @var CMSApplication $app */ - $app = Factory::getApplication(); - $input = $app->input; - $repository = new CredentialRepository; + $session = $this->getApplication()->getSession(); + $input = $this->getApplication()->input; // Retrieve data from the request $username = $input->getUsername('username', ''); $returnUrl = base64_encode( - Joomla::getSessionVar('returnUrl', Uri::current(), 'plg_system_webauthn') + $session->get('plg_system_webauthn.returnUrl', Uri::current()) ); $returnUrl = $input->getBase64('returnUrl', $returnUrl); $returnUrl = base64_decode($returnUrl); @@ -71,12 +63,14 @@ public function onAjaxWebauthnChallenge() $returnUrl = Uri::base(); } - Joomla::setSessionVar('returnUrl', $returnUrl, 'plg_system_webauthn'); + $session->set('plg_system_webauthn.returnUrl', $returnUrl); // Do I have a username? if (empty($username)) { - return json_encode(false); + $event->addResult(false); + + return; } // Is the username valid? @@ -91,73 +85,32 @@ public function onAjaxWebauthnChallenge() if ($userId <= 0) { - return json_encode(false); + $event->addResult(false); + + return; } - // Load the saved credentials into an array of PublicKeyCredentialDescriptor objects try { - $userEntity = new PublicKeyCredentialUserEntity( - '', $repository->getHandleFromUserId($userId), '' - ); - $credentials = $repository->findAllForUserEntity($userEntity); + $myUser = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); } catch (Exception $e) { - return json_encode(false); + $myUser = new User; } - // No stored credentials? - if (empty($credentials)) + if ($myUser->id != $userId || $myUser->guest) { - return json_encode(false); - } + $event->addResult(false); - $registeredPublicKeyCredentialDescriptors = []; - - /** @var PublicKeyCredentialSource $record */ - foreach ($credentials as $record) - { - try - { - $registeredPublicKeyCredentialDescriptors[] = $record->getPublicKeyCredentialDescriptor(); - } - catch (Throwable $e) - { - continue; - } + return; } - // Extensions - $extensions = new AuthenticationExtensionsClientInputs; - - // Public Key Credential Request Options - $publicKeyCredentialRequestOptions = new PublicKeyCredentialRequestOptions( - random_bytes(32), - 60000, - Uri::getInstance()->toString(['host']), - $registeredPublicKeyCredentialDescriptors, - PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, - $extensions - ); + $publicKeyCredentialRequestOptions = $this->authenticationHelper->getPubkeyRequestOptions($myUser); - // Save in session. This is used during the verification stage to prevent replay attacks. - Joomla::setSessionVar( - 'publicKeyCredentialRequestOptions', - base64_encode(serialize($publicKeyCredentialRequestOptions)), - 'plg_system_webauthn' - ); - Joomla::setSessionVar( - 'userHandle', - $repository->getHandleFromUserId($userId), - 'plg_system_webauthn' - ); - Joomla::setSessionVar('userId', $userId, 'plg_system_webauthn'); + $session->set('plg_system_webauthn.userId', $userId); // Return the JSON encoded data to the caller - return json_encode( - $publicKeyCredentialRequestOptions, - JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE - ); + $event->addResult(json_encode($publicKeyCredentialRequestOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); } } diff --git a/plugins/system/webauthn/src/PluginTraits/AjaxHandlerCreate.php b/plugins/system/webauthn/src/PluginTraits/AjaxHandlerCreate.php index 06934e2561ba0..990639bc8702d 100644 --- a/plugins/system/webauthn/src/PluginTraits/AjaxHandlerCreate.php +++ b/plugins/system/webauthn/src/PluginTraits/AjaxHandlerCreate.php @@ -13,13 +13,12 @@ \defined('_JEXEC') or die(); use Exception; -use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxCreate; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; +use Joomla\CMS\Layout\FileLayout; use Joomla\CMS\User\UserFactoryInterface; -use Joomla\Plugin\System\Webauthn\CredentialRepository; -use Joomla\Plugin\System\Webauthn\Helper\CredentialsCreation; -use Joomla\Plugin\System\Webauthn\Helper\Joomla; +use Joomla\Event\Event; use RuntimeException; use Webauthn\PublicKeyCredentialSource; @@ -35,17 +34,15 @@ trait AjaxHandlerCreate /** * Handle the callback to add a new WebAuthn authenticator * - * @return string + * @param AjaxCreate $event The event we are handling * - * @throws Exception + * @return void * + * @throws Exception * @since 4.0.0 */ - public function onAjaxWebauthnCreate(): string + public function onAjaxWebauthnCreate(AjaxCreate $event): void { - // Load the language files - $this->loadLanguage(); - /** * Fundamental sanity check: this callback is only allowed after a Public Key has been created server-side and * the user it was created for matches the current user. @@ -55,7 +52,8 @@ public function onAjaxWebauthnCreate(): string * someone else's Webauthn configuration thus mitigating a major privacy and security risk. So, please, DO NOT * remove this sanity check! */ - $storedUserId = Joomla::getSessionVar('registration_user_id', 0, 'plg_system_webauthn'); + $session = $this->getApplication()->getSession(); + $storedUserId = $session->get('plg_system_webauthn.registration_user_id', 0); $thatUser = empty($storedUserId) ? Factory::getApplication()->getIdentity() : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($storedUserId); @@ -64,27 +62,25 @@ public function onAjaxWebauthnCreate(): string if ($thatUser->guest || ($thatUser->id != $myUser->id)) { // Unset the session variables used for registering authenticators (security precaution). - Joomla::unsetSessionVar('registration_user_id', 'plg_system_webauthn'); - Joomla::unsetSessionVar('publicKeyCredentialCreationOptions', 'plg_system_webauthn'); + $session->set('plg_system_webauthn.registration_user_id', null); + $session->set('plg_system_webauthn.publicKeyCredentialCreationOptions', null); // Politely tell the presumed hacker trying to abuse this callback to go away. throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_USER')); } // Get the credentials repository object. It's outside the try-catch because I also need it to display the GUI. - $credentialRepository = new CredentialRepository; + $credentialRepository = $this->authenticationHelper->getCredentialsRepository(); // Try to validate the browser data. If there's an error I won't save anything and pass the message to the GUI. try { - /** @var CMSApplication $app */ - $app = Factory::getApplication(); - $input = $app->input; + $input = $this->getApplication()->input; // Retrieve the data sent by the device $data = $input->get('data', '', 'raw'); - $publicKeyCredentialSource = CredentialsCreation::validateAuthenticationData($data); + $publicKeyCredentialSource = $this->authenticationHelper->validateAttestationResponse($data); if (!\is_object($publicKeyCredentialSource) || !($publicKeyCredentialSource instanceof PublicKeyCredentialSource)) { @@ -100,14 +96,16 @@ public function onAjaxWebauthnCreate(): string } // Unset the session variables used for registering authenticators (security precaution). - Joomla::unsetSessionVar('registration_user_id', 'plg_system_webauthn'); - Joomla::unsetSessionVar('publicKeyCredentialCreationOptions', 'plg_system_webauthn'); + $session->set('plg_system_webauthn.registration_user_id', null); + $session->set('plg_system_webauthn.publicKeyCredentialCreationOptions', null); // Render the GUI and return it $layoutParameters = [ - 'user' => $thatUser, - 'allow_add' => $thatUser->id == $myUser->id, - 'credentials' => $credentialRepository->getAll($thatUser->id), + 'user' => $thatUser, + 'allow_add' => $thatUser->id == $myUser->id, + 'credentials' => $credentialRepository->getAll($thatUser->id), + 'knownAuthenticators' => $this->authenticationHelper->getKnownAuthenticators(), + 'attestationSupport' => $this->authenticationHelper->hasAttestationSupport(), ]; if (isset($error) && !empty($error)) @@ -115,6 +113,8 @@ public function onAjaxWebauthnCreate(): string $layoutParameters['error'] = $error; } - return Joomla::renderLayout('plugins.system.webauthn.manage', $layoutParameters); + $layout = new FileLayout('plugins.system.webauthn.manage', JPATH_SITE . '/plugins/system/webauthn/layout'); + + $event->addResult($layout->render($layoutParameters)); } } diff --git a/plugins/system/webauthn/src/PluginTraits/AjaxHandlerDelete.php b/plugins/system/webauthn/src/PluginTraits/AjaxHandlerDelete.php index 6bf7264be4d9f..0246c8a14e831 100644 --- a/plugins/system/webauthn/src/PluginTraits/AjaxHandlerDelete.php +++ b/plugins/system/webauthn/src/PluginTraits/AjaxHandlerDelete.php @@ -13,9 +13,9 @@ \defined('_JEXEC') or die(); use Exception; -use Joomla\CMS\Application\CMSApplication; -use Joomla\CMS\Factory; -use Joomla\Plugin\System\Webauthn\CredentialRepository; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxDelete; +use Joomla\CMS\User\User; +use Joomla\Event\Event; /** * Ajax handler for akaction=savelabel @@ -29,21 +29,16 @@ trait AjaxHandlerDelete /** * Handle the callback to remove an authenticator * - * @return boolean - * @throws Exception + * @param AjaxDelete $event The event we are handling * + * @return void * @since 4.0.0 */ - public function onAjaxWebauthnDelete(): bool + public function onAjaxWebauthnDelete(AjaxDelete $event): void { - // Load the language files - $this->loadLanguage(); - // Initialize objects - /** @var CMSApplication $app */ - $app = Factory::getApplication(); - $input = $app->input; - $repository = new CredentialRepository; + $input = $this->getApplication()->input; + $repository = $this->authenticationHelper->getCredentialsRepository(); // Retrieve data from the request $credentialId = $input->getBase64('credential_id', ''); @@ -51,30 +46,39 @@ public function onAjaxWebauthnDelete(): bool // Is this a valid credential? if (empty($credentialId)) { - return false; + $event->addResult(false); + + return; } $credentialId = base64_decode($credentialId); if (empty($credentialId) || !$repository->has($credentialId)) { - return false; + $event->addResult(false); + + return; } // Make sure I am editing my own key try { + $user = $this->getApplication()->getIdentity() ?? new User; $credentialHandle = $repository->getUserHandleFor($credentialId); - $myHandle = $repository->getHandleFromUserId($app->getIdentity()->id); + $myHandle = $repository->getHandleFromUserId($user->id); } catch (Exception $e) { - return false; + $event->addResult(false); + + return; } if ($credentialHandle !== $myHandle) { - return false; + $event->addResult(false); + + return; } // Delete the record @@ -84,9 +88,11 @@ public function onAjaxWebauthnDelete(): bool } catch (Exception $e) { - return false; + $event->addResult(false); + + return; } - return true; + $event->addResult(true); } } diff --git a/plugins/system/webauthn/src/PluginTraits/AjaxHandlerInitCreate.php b/plugins/system/webauthn/src/PluginTraits/AjaxHandlerInitCreate.php new file mode 100644 index 0000000000000..9b27f5f7e947c --- /dev/null +++ b/plugins/system/webauthn/src/PluginTraits/AjaxHandlerInitCreate.php @@ -0,0 +1,62 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Plugin\System\Webauthn\PluginTraits; + +// Protect from unauthorized access +\defined('_JEXEC') or die(); + +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxInitCreate; +use Joomla\CMS\Factory; +use Joomla\CMS\User\User; + +/** + * Ajax handler for akaction=initcreate + * + * Returns the Public Key Creation Options to start the attestation ceremony on the browser. + * + * @since __DEPLOY_VERSION__ + */ +trait AjaxHandlerInitCreate +{ + /** + * Returns the Public Key Creation Options to start the attestation ceremony on the browser. + * + * @param AjaxInitCreate $event The event we are handling + * + * @return void + * @throws \Exception + * @since __DEPLOY_VERSION__ + */ + public function onAjaxWebauthnInitcreate(AjaxInitCreate $event): void + { + // Make sure I have a valid user + $user = Factory::getApplication()->getIdentity(); + + if (!($user instanceof User) || $user->guest) + { + $event->addResult(new \stdClass); + + return; + } + + // I need the server to have either GMP or BCComp support to attest new authenticators + if (function_exists('gmp_intval') === false && function_exists('bccomp') === false) + { + $event->addResult(new \stdClass); + + return; + } + + $session = $this->getApplication()->getSession(); + $session->set('plg_system_webauthn.registration_user_id', $user->id); + + $event->addResult($this->authenticationHelper->getPubKeyCreationOptions($user)); + } +} diff --git a/plugins/system/webauthn/src/PluginTraits/AjaxHandlerLogin.php b/plugins/system/webauthn/src/PluginTraits/AjaxHandlerLogin.php index ec6093af09c85..3afc29fc75cdb 100644 --- a/plugins/system/webauthn/src/PluginTraits/AjaxHandlerLogin.php +++ b/plugins/system/webauthn/src/PluginTraits/AjaxHandlerLogin.php @@ -12,39 +12,19 @@ // Protect from unauthorized access \defined('_JEXEC') or die(); -use CBOR\Decoder; -use CBOR\OtherObject\OtherObjectManager; -use CBOR\Tag\TagObjectManager; -use Cose\Algorithm\Manager; -use Cose\Algorithm\Signature\ECDSA; -use Cose\Algorithm\Signature\EdDSA; -use Cose\Algorithm\Signature\RSA; use Exception; -use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Authentication\Authentication; +use Joomla\CMS\Authentication\AuthenticationResponse; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxLogin; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; +use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Uri\Uri; +use Joomla\CMS\User\User; use Joomla\CMS\User\UserFactoryInterface; -use Joomla\Plugin\System\Webauthn\CredentialRepository; -use Joomla\Plugin\System\Webauthn\Helper\Joomla; -use Laminas\Diactoros\ServerRequestFactory; use RuntimeException; use Throwable; -use Webauthn\AttestationStatement\AndroidKeyAttestationStatementSupport; -use Webauthn\AttestationStatement\AttestationObjectLoader; -use Webauthn\AttestationStatement\AttestationStatementSupportManager; -use Webauthn\AttestationStatement\FidoU2FAttestationStatementSupport; -use Webauthn\AttestationStatement\NoneAttestationStatementSupport; -use Webauthn\AttestationStatement\PackedAttestationStatementSupport; -use Webauthn\AttestationStatement\TPMAttestationStatementSupport; -use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler; -use Webauthn\AuthenticatorAssertionResponse; -use Webauthn\AuthenticatorAssertionResponseValidator; -use Webauthn\PublicKeyCredentialLoader; -use Webauthn\PublicKeyCredentialRequestOptions; -use Webauthn\TokenBinding\TokenBindingNotSupportedHandler; /** * Ajax handler for akaction=login @@ -59,56 +39,90 @@ trait AjaxHandlerLogin * Returns the public key set for the user and a unique challenge in a Public Key Credential Request encoded as * JSON. * + * @param AjaxLogin $event The event we are handling + * * @return void * - * @throws Exception * @since 4.0.0 */ - public function onAjaxWebauthnLogin(): void + public function onAjaxWebauthnLogin(AjaxLogin $event): void { - // Load the language files - $this->loadLanguage(); - - $returnUrl = Joomla::getSessionVar('returnUrl', Uri::base(), 'plg_system_webauthn'); - $userId = Joomla::getSessionVar('userId', 0, 'plg_system_webauthn'); + $session = $this->getApplication()->getSession(); + $returnUrl = $session->get('plg_system_webauthn.returnUrl', Uri::base()); + $userId = $session->get('plg_system_webauthn.userId', 0); try { - // Sanity check + $credentialRepository = $this->authenticationHelper->getCredentialsRepository(); + + // No user ID: no username was provided and the resident credential refers to an unknown user handle. DIE! if (empty($userId)) { + Log::add('Cannot determine the user ID', Log::NOTICE, 'webauthn.system'); + throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); } - // Make sure the user exists + // Do I have a valid user? $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); if ($user->id != $userId) { + $message = sprintf('User #%d does not exist', $userId); + Log::add($message, Log::NOTICE, 'webauthn.system'); + throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); } - // Validate the authenticator response - $this->validateResponse(); + // Validate the authenticator response and get the user handle + $userHandle = $this->getUserHandleFromResponse($user); + + if (is_null($userHandle)) + { + Log::add('Cannot retrieve the user handle from the request; the browser did not assert our request.', Log::NOTICE, 'webauthn.system'); + + throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); + } + + // Does the user handle match the user ID? This should never trigger by definition of the login check. + $validUserHandle = $credentialRepository->getHandleFromUserId($userId); + + if ($userHandle != $validUserHandle) + { + $message = sprintf('Invalid user handle; expected %s, got %s', $validUserHandle, $userHandle); + Log::add($message, Log::NOTICE, 'webauthn.system'); + + throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); + } + + // Make sure the user exists + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + + if ($user->id != $userId) + { + $message = sprintf('Invalid user ID; expected %d, got %d', $userId, $user->id); + Log::add($message, Log::NOTICE, 'webauthn.system'); + + throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); + } // Login the user - Joomla::log('system', "Logging in the user", Log::INFO); - Joomla::loginUser((int) $userId); + Log::add("Logging in the user", Log::INFO, 'webauthn.system'); + $this->loginUser((int) $userId); } catch (Throwable $e) { - Joomla::setSessionVar('publicKeyCredentialRequestOptions', null, 'plg_system_webauthn'); - Joomla::setSessionVar('userHandle', null, 'plg_system_webauthn'); + $session->set('plg_system_webauthn.publicKeyCredentialRequestOptions', null); - $response = Joomla::getAuthenticationResponseObject(); + $response = $this->getAuthenticationResponseObject(); $response->status = Authentication::STATUS_UNKNOWN; // phpcs:ignore $response->error_message = $e->getMessage(); - Joomla::log('system', sprintf("Received login failure. Message: %s", $e->getMessage()), Log::ERROR); + Log::add(sprintf("Received login failure. Message: %s", $e->getMessage()), Log::ERROR, 'webauthn.system'); // This also enqueues the login failure message for display after redirection. Look for JLog in that method. - Joomla::processLoginFailure($response, null, 'system'); + $this->processLoginFailure($response, null, 'system'); } finally { @@ -118,153 +132,199 @@ public function onAjaxWebauthnLogin(): void */ // Remove temporary information for security reasons - Joomla::setSessionVar('publicKeyCredentialRequestOptions', null, 'plg_system_webauthn'); - Joomla::setSessionVar('userHandle', null, 'plg_system_webauthn'); - Joomla::setSessionVar('returnUrl', null, 'plg_system_webauthn'); - Joomla::setSessionVar('userId', null, 'plg_system_webauthn'); + $session->set('plg_system_webauthn.publicKeyCredentialRequestOptions', null); + $session->set('plg_system_webauthn.returnUrl', null); + $session->set('plg_system_webauthn.userId', null); // Redirect back to the page we were before. - Factory::getApplication()->redirect($returnUrl); + $this->getApplication()->redirect($returnUrl); } } /** - * Validate the authenticator response sent to us by the browser. + * Logs in a user to the site, bypassing the authentication plugins. * - * @return void + * @param int $userId The user ID to log in * + * @return void * @throws Exception - * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ - private function validateResponse(): void + private function loginUser(int $userId): void { - // Initialize objects - /** @var CMSApplication $app */ - $app = Factory::getApplication(); - $input = $app->input; - $credentialRepository = new CredentialRepository; + // Trick the class auto-loader into loading the necessary classes + class_exists('Joomla\\CMS\\Authentication\\Authentication', true); - // Retrieve data from the request and session - $data = $input->getBase64('data', ''); - $data = base64_decode($data); + // Fake a successful login message + $isAdmin = $this->getApplication()->isClient('administrator'); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - if (empty($data)) + // Does the user account have a pending activation? + if (!empty($user->activation)) { - throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); + throw new RuntimeException(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); } - $publicKeyCredentialRequestOptions = $this->getPKCredentialRequestOptions(); - - // Cose Algorithm Manager - $coseAlgorithmManager = new Manager; - $coseAlgorithmManager->add(new ECDSA\ES256); - $coseAlgorithmManager->add(new ECDSA\ES512); - $coseAlgorithmManager->add(new EdDSA\EdDSA); - $coseAlgorithmManager->add(new RSA\RS1); - $coseAlgorithmManager->add(new RSA\RS256); - $coseAlgorithmManager->add(new RSA\RS512); - - // Create a CBOR Decoder object - $otherObjectManager = new OtherObjectManager; - $tagObjectManager = new TagObjectManager; - $decoder = new Decoder($tagObjectManager, $otherObjectManager); - - // Attestation Statement Support Manager - $attestationStatementSupportManager = new AttestationStatementSupportManager; - $attestationStatementSupportManager->add(new NoneAttestationStatementSupport); - $attestationStatementSupportManager->add(new FidoU2FAttestationStatementSupport($decoder)); - - /* - $attestationStatementSupportManager->add( - new AndroidSafetyNetAttestationStatementSupport( - HttpFactory::getHttp(), 'GOOGLE_SAFETYNET_API_KEY', new RequestFactory - ) - ); - */ - $attestationStatementSupportManager->add(new AndroidKeyAttestationStatementSupport($decoder)); - $attestationStatementSupportManager->add(new TPMAttestationStatementSupport); - $attestationStatementSupportManager->add(new PackedAttestationStatementSupport($decoder, $coseAlgorithmManager)); - - // Attestation Object Loader - $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager, $decoder); - - // Public Key Credential Loader - $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader, $decoder); - - // The token binding handler - $tokenBindingHandler = new TokenBindingNotSupportedHandler; - - // Extension Output Checker Handler - $extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler; - - // Authenticator Assertion Response Validator - $authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator( - $credentialRepository, - $decoder, - $tokenBindingHandler, - $extensionOutputCheckerHandler, - $coseAlgorithmManager - ); + // Is the user account blocked? + if ($user->block) + { + throw new RuntimeException(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } - // We init the Symfony Request object - $request = ServerRequestFactory::fromGlobals(); + $statusSuccess = Authentication::STATUS_SUCCESS; - // Load the data - $publicKeyCredential = $publicKeyCredentialLoader->load($data); - $response = $publicKeyCredential->getResponse(); + $response = $this->getAuthenticationResponseObject(); + $response->status = $statusSuccess; + $response->username = $user->username; + $response->fullname = $user->name; + // phpcs:ignore + $response->error_message = ''; + $response->language = $user->getParam('language'); + $response->type = 'Passwordless'; - // Check if the response is an Authenticator Assertion Response - if (!$response instanceof AuthenticatorAssertionResponse) + if ($isAdmin) { - throw new RuntimeException('Not an authenticator assertion response'); + $response->language = $user->getParam('admin_language'); } - // Check the response against the attestation request - $userHandle = Joomla::getSessionVar('userHandle', null, 'plg_system_webauthn'); - /** @var AuthenticatorAssertionResponse $authenticatorAssertionResponse */ - $authenticatorAssertionResponse = $publicKeyCredential->getResponse(); - $authenticatorAssertionResponseValidator->check( - $publicKeyCredential->getRawId(), - $authenticatorAssertionResponse, - $publicKeyCredentialRequestOptions, - $request, - $userHandle - ); + /** + * Set up the login options. + * + * The 'remember' element forces the use of the Remember Me feature when logging in with Webauthn, as the + * users would expect. + * + * The 'action' element is actually required by plg_user_joomla. It is the core ACL action the logged in user + * must be allowed for the login to succeed. Please note that front-end and back-end logins use a different + * action. This allows us to provide the WebAuthn button on both front- and back-end and be sure that if a + * used with no backend access tries to use it to log in Joomla! will just slap him with an error message about + * insufficient privileges - the same thing that'd happen if you tried to use your front-end only username and + * password in a back-end login form. + */ + $options = [ + 'remember' => true, + 'action' => 'core.login.site', + ]; + + if ($isAdmin) + { + $options['action'] = 'core.login.admin'; + } + + // Run the user plugins. They CAN block login by returning boolean false and setting $response->error_message. + PluginHelper::importPlugin('user'); + $eventClassName = self::getEventClassByEventName('onUserLogin'); + $event = new $eventClassName('onUserLogin', [(array) $response, $options]); + $result = $this->getApplication()->getDispatcher()->dispatch($event->getName(), $event); + $results = !isset($result['result']) || \is_null($result['result']) ? [] : $result['result']; + + // If there is no boolean FALSE result from any plugin the login is successful. + if (in_array(false, $results, true) === false) + { + // Set the user in the session, letting Joomla! know that we are logged in. + $this->getApplication()->getSession()->set('user', $user); + + // Trigger the onUserAfterLogin event + $options['user'] = $user; + $options['responseType'] = $response->type; + + // The user is successfully logged in. Run the after login events + $eventClassName = self::getEventClassByEventName('onUserAfterLogin'); + $event = new $eventClassName('onUserAfterLogin', [$options]); + $this->getApplication()->getDispatcher()->dispatch($event->getName(), $event); + + return; + } + + // If we are here the plugins marked a login failure. Trigger the onUserLoginFailure Event. + $eventClassName = self::getEventClassByEventName('onUserLoginFailure'); + $event = new $eventClassName('onUserLoginFailure', [(array) $response]); + $this->getApplication()->getDispatcher()->dispatch($event->getName(), $event); + + // Log the failure + // phpcs:ignore + Log::add($response->error_message, Log::WARNING, 'jerror'); + + // Throw an exception to let the caller know that the login failed + // phpcs:ignore + throw new RuntimeException($response->error_message); } /** - * Retrieve the public key credential request options saved in the session. If they do not exist or are corrupt it - * is a hacking attempt and we politely tell the hacker to go away. + * Returns a (blank) Joomla! authentication response * - * @return PublicKeyCredentialRequestOptions + * @return AuthenticationResponse * - * @since 4.0.0 + * @since __DEPLOY_VERSION__ */ - private function getPKCredentialRequestOptions(): PublicKeyCredentialRequestOptions + private function getAuthenticationResponseObject(): AuthenticationResponse { - $encodedOptions = Joomla::getSessionVar('publicKeyCredentialRequestOptions', null, 'plg_system_webauthn'); + // Force the class auto-loader to load the JAuthentication class + class_exists('Joomla\\CMS\\Authentication\\Authentication', true); - if (empty($encodedOptions)) - { - throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); - } + return new AuthenticationResponse; + } - try + /** + * Have Joomla! process a login failure + * + * @param AuthenticationResponse $response The Joomla! auth response object + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + private function processLoginFailure(AuthenticationResponse $response): bool + { + // Import the user plugin group. + PluginHelper::importPlugin('user'); + + // Trigger onUserLoginFailure Event. + Log::add('Calling onUserLoginFailure plugin event', Log::INFO, 'plg_system_webauthn'); + + $eventClassName = self::getEventClassByEventName('onUserLoginFailure'); + $event = new $eventClassName('onUserLoginFailure', [(array) $response]); + $this->getApplication()->getDispatcher()->dispatch($event->getName(), $event); + + // If status is success, any error will have been raised by the user plugin + $expectedStatus = Authentication::STATUS_SUCCESS; + + if ($response->status !== $expectedStatus) { - $publicKeyCredentialCreationOptions = unserialize(base64_decode($encodedOptions)); + Log::add('The login failure has been logged in Joomla\'s error log', Log::INFO, 'webauthn.system'); + + // Everything logged in the 'jerror' category ends up being enqueued in the application message queue. + // phpcs:ignore + Log::add($response->error_message, Log::WARNING, 'jerror'); } - catch (Exception $e) + else { - throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); + $message = 'A login failure was caused by a third party user plugin but it did not return any' . + 'further information.'; + Log::add($message, Log::WARNING, 'webauthn.system'); } - if (!\is_object($publicKeyCredentialCreationOptions) - || !($publicKeyCredentialCreationOptions instanceof PublicKeyCredentialRequestOptions)) - { - throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); - } + return false; + } + + /** + * Validate the authenticator response sent to us by the browser. + * + * @param User $user The user we are trying to log in. + * + * @return string|null The user handle or null + * + * @throws Exception + * @since __DEPLOY_VERSION__ + */ + private function getUserHandleFromResponse(User $user): ?string + { + // Retrieve data from the request and session + $pubKeyCredentialSource = $this->authenticationHelper->validateAssertionResponse( + $this->getApplication()->input->getBase64('data', ''), + $user + ); - return $publicKeyCredentialCreationOptions; + return $pubKeyCredentialSource ? $pubKeyCredentialSource->getUserHandle() : null; } + } diff --git a/plugins/system/webauthn/src/PluginTraits/AjaxHandlerSaveLabel.php b/plugins/system/webauthn/src/PluginTraits/AjaxHandlerSaveLabel.php index b2fae9ac359f4..49b152945fdcb 100644 --- a/plugins/system/webauthn/src/PluginTraits/AjaxHandlerSaveLabel.php +++ b/plugins/system/webauthn/src/PluginTraits/AjaxHandlerSaveLabel.php @@ -13,9 +13,8 @@ \defined('_JEXEC') or die(); use Exception; -use Joomla\CMS\Application\CMSApplication; -use Joomla\CMS\Factory; -use Joomla\Plugin\System\Webauthn\CredentialRepository; +use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxSaveLabel; +use Joomla\CMS\User\User; /** * Ajax handler for akaction=savelabel @@ -29,19 +28,17 @@ trait AjaxHandlerSaveLabel /** * Handle the callback to rename an authenticator * - * @return boolean + * @param AjaxSaveLabel $event The event we are handling * - * @throws Exception + * @return void * * @since 4.0.0 */ - public function onAjaxWebauthnSavelabel(): bool + public function onAjaxWebauthnSavelabel(AjaxSaveLabel $event): void { // Initialize objects - /** @var CMSApplication $app */ - $app = Factory::getApplication(); - $input = $app->input; - $repository = new CredentialRepository; + $input = $this->getApplication()->input; + $repository = $this->authenticationHelper->getCredentialsRepository(); // Retrieve data from the request $credentialId = $input->getBase64('credential_id', ''); @@ -50,36 +47,47 @@ public function onAjaxWebauthnSavelabel(): bool // Is this a valid credential? if (empty($credentialId)) { - return false; + $event->addResult(false); + + return; } $credentialId = base64_decode($credentialId); if (empty($credentialId) || !$repository->has($credentialId)) { - return false; + $event->addResult(false); + + return; } // Make sure I am editing my own key try { $credentialHandle = $repository->getUserHandleFor($credentialId); - $myHandle = $repository->getHandleFromUserId($app->getIdentity()->id); + $user = $this->getApplication()->getIdentity() ?? new User; + $myHandle = $repository->getHandleFromUserId($user->id); } catch (Exception $e) { - return false; + $event->addResult(false); + + return; } if ($credentialHandle !== $myHandle) { - return false; + $event->addResult(false); + + return; } // Make sure the new label is not empty if (empty($newLabel)) { - return false; + $event->addResult(false); + + return; } // Save the new label @@ -89,9 +97,11 @@ public function onAjaxWebauthnSavelabel(): bool } catch (Exception $e) { - return false; + $event->addResult(false); + + return; } - return true; + $event->addResult(true); } } diff --git a/plugins/system/webauthn/src/PluginTraits/EventReturnAware.php b/plugins/system/webauthn/src/PluginTraits/EventReturnAware.php new file mode 100644 index 0000000000000..7327f698e21ac --- /dev/null +++ b/plugins/system/webauthn/src/PluginTraits/EventReturnAware.php @@ -0,0 +1,45 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Plugin\System\Webauthn\PluginTraits; + +defined('_JEXEC') or die(); + +use Joomla\Event\Event; + +/** + * Utility trait to facilitate returning data from event handlers. + * + * @since __DEPLOY_VERSION__ + */ +trait EventReturnAware +{ + /** + * Adds a result value to an event + * + * @param Event $event The event we were processing + * @param mixed $value The value to append to the event's results + * + * @return void + * @since __DEPLOY_VERSION__ + */ + private function returnFromEvent(Event $event, $value = null): void + { + $result = $event->getArgument('result') ?: []; + + if (!is_array($result)) + { + $result = [$result]; + } + + $result[] = $value; + + $event->setArgument('result', $result); + } +} diff --git a/plugins/system/webauthn/src/PluginTraits/UserDeletion.php b/plugins/system/webauthn/src/PluginTraits/UserDeletion.php index 24708deafa3c8..ae36c7dd38388 100644 --- a/plugins/system/webauthn/src/PluginTraits/UserDeletion.php +++ b/plugins/system/webauthn/src/PluginTraits/UserDeletion.php @@ -14,8 +14,9 @@ use Exception; use Joomla\CMS\Factory; +use Joomla\CMS\Log\Log; use Joomla\Database\DatabaseDriver; -use Joomla\Plugin\System\Webauthn\Helper\Joomla; +use Joomla\Event\Event; use Joomla\Utilities\ArrayHelper; /** @@ -30,28 +31,31 @@ trait UserDeletion * * This method is called after user data is deleted from the database. * - * @param array $user Holds the user data - * @param bool $success True if user was successfully stored in the database - * @param string $msg Message + * @param Event $event The event we are handling * * @return void * - * @throws Exception - * * @since 4.0.0 */ - public function onUserAfterDelete(array $user, bool $success, ?string $msg): void + public function onUserAfterDelete(Event $event): void { + /** + * @var array $user Holds the user data + * @var bool $success True if user was successfully stored in the database + * @var string|null $msg Message + */ + [$user, $success, $msg] = $event->getArguments(); + if (!$success) { - return; + $this->returnFromEvent($event, true); } $userId = ArrayHelper::getValue($user, 'id', 0, 'int'); if ($userId) { - Joomla::log('system', "Removing WebAuthn Passwordless Login information for deleted user #{$userId}"); + Log::add("Removing WebAuthn Passwordless Login information for deleted user #{$userId}", Log::DEBUG, 'webauthn.system'); /** @var DatabaseDriver $db */ $db = Factory::getContainer()->get('DatabaseDriver'); @@ -61,7 +65,16 @@ public function onUserAfterDelete(array $user, bool $success, ?string $msg): voi ->where($db->qn('user_id') . ' = :userId') ->bind(':userId', $userId); - $db->setQuery($query)->execute(); + try + { + $db->setQuery($query)->execute(); + } + catch (Exception $e) + { + // Don't worry if this fails + } + + $this->returnFromEvent($event, true); } } } diff --git a/plugins/system/webauthn/src/PluginTraits/UserProfileFields.php b/plugins/system/webauthn/src/PluginTraits/UserProfileFields.php index 3b7d192751db3..72a4fbe3235e0 100644 --- a/plugins/system/webauthn/src/PluginTraits/UserProfileFields.php +++ b/plugins/system/webauthn/src/PluginTraits/UserProfileFields.php @@ -1,10 +1,10 @@ - * @license GNU General Public License version 2 or later; see LICENSE.txt + * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\System\Webauthn\PluginTraits; @@ -17,11 +17,12 @@ use Joomla\CMS\Form\Form; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; +use Joomla\CMS\Log\Log; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\User; use Joomla\CMS\User\UserFactoryInterface; -use Joomla\Plugin\System\Webauthn\CredentialRepository; -use Joomla\Plugin\System\Webauthn\Helper\Joomla; +use Joomla\Event\Event; +use Joomla\Plugin\System\Webauthn\Extension\Webauthn; use Joomla\Registry\Registry; /** @@ -59,6 +60,7 @@ trait UserProfileFields * stored value. We only use it as a proxy to render a sub-form. * * @return string + * @since 4.0.0 */ public static function renderWebauthnProfileField($value): string { @@ -67,10 +69,13 @@ public static function renderWebauthnProfileField($value): string return ''; } - $credentialRepository = new CredentialRepository; + /** @var Webauthn $plugin */ + $plugin = Factory::getApplication()->bootPlugin('webauthn', 'system'); + $credentialRepository = $plugin->getAuthenticationHelper()->getCredentialsRepository(); $credentials = $credentialRepository->getAll(self::$userFromFormData->id); $authenticators = array_map( - function (array $credential) { + function (array $credential) + { return $credential['label']; }, $credentials @@ -82,32 +87,36 @@ function (array $credential) { /** * Adds additional fields to the user editing form * - * @param Form $form The form to be altered. - * @param mixed $data The associated data for the form. + * @param Event $event The event we are handling * - * @return boolean + * @return void * * @throws Exception - * * @since 4.0.0 */ - public function onContentPrepareForm(Form $form, $data) + public function onContentPrepareForm(Event $event) { + /** + * @var Form $form The form to be altered. + * @var mixed $data The associated data for the form. + */ + [$form, $data] = $event->getArguments(); + // This feature only applies to HTTPS sites. if (!Uri::getInstance()->isSsl()) { - return true; + return; } $name = $form->getName(); $allowedForms = [ - 'com_users.user', 'com_users.profile', 'com_users.registration', + 'com_admin.profile', 'com_users.user', 'com_users.profile', 'com_users.registration', ]; if (!\in_array($name, $allowedForms)) { - return true; + return; } // Get the user object @@ -116,25 +125,49 @@ public function onContentPrepareForm(Form $form, $data) // Make sure the loaded user is the correct one if (\is_null($user)) { - return true; + return; } // Make sure I am either editing myself OR I am a Super User - if (!Joomla::canEditUser($user)) + if (!$this->canEditUser($user)) { - return true; + return; } // Add the fields to the form. - Joomla::log( - 'system', - 'Injecting WebAuthn Passwordless Login fields in user profile edit page' - ); + Log::add('Injecting WebAuthn Passwordless Login fields in user profile edit page', Log::DEBUG, 'webauthn.system'); + Form::addFormPath(JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/forms'); - $this->loadLanguage(); $form->loadFile('webauthn', false); + } + + /** + * @param Event $event The event we are handling + * + * @return void + * + * @throws Exception + * @since 4.0.0 + */ + public function onContentPrepareData(Event $event): void + { + /** + * @var string|null $context The context for the data + * @var array|object|null $data An object or array containing the data for the form. + */ + [$context, $data] = $event->getArguments(); + + if (!\in_array($context, ['com_users.profile', 'com_users.user'])) + { + return; + } + + self::$userFromFormData = $this->getUserFromData($data); - return true; + if (!HTMLHelper::isRegistered('users.webauthnWebauthn')) + { + HTMLHelper::register('users.webauthn', [__CLASS__, 'renderWebauthnProfileField']); + } } /** @@ -178,27 +211,28 @@ private function getUserFromData($data): ?User } /** - * @param string|null $context The context for the data - * @param array|object|null $data An object or array containing the data for the form. + * Is the current user allowed to edit the WebAuthn configuration of $user? * - * @return bool + * To do so I must either be editing my own account OR I have to be a Super User. * - * @since 4.0.0 + * @param ?User $user The user you want to know if we're allowed to edit + * + * @return boolean + * + * @since __DEPLOY_VERSION__ */ - public function onContentPrepareData(?string $context, $data): bool + private function canEditUser(?User $user = null): bool { - if (!\in_array($context, ['com_users.profile', 'com_users.user'])) + // I can edit myself, but Guests can't have passwordless logins associated + if (empty($user) || $user->guest) { return true; } - self::$userFromFormData = $this->getUserFromData($data); - - if (!HTMLHelper::isRegistered('users.webauthnWebauthn')) - { - HTMLHelper::register('users.webauthn', [__CLASS__, 'renderWebauthnProfileField']); - } + // Get the currently logged in used + $myUser = $this->getApplication()->getIdentity() ?? new User; - return true; + // I can edit myself. If I'm a Super user I can edit other users too. + return ($myUser->id == $user->id) || $myUser->authorise('core.admin'); } } diff --git a/plugins/system/webauthn/webauthn.php b/plugins/system/webauthn/webauthn.php deleted file mode 100644 index 758c923ab5924..0000000000000 --- a/plugins/system/webauthn/webauthn.php +++ /dev/null @@ -1,78 +0,0 @@ - - * @license GNU General Public License version 2 or later; see LICENSE.txt - */ - -// Protect from unauthorized access -defined('_JEXEC') or die(); - -use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\System\Webauthn\Helper\Joomla; -use Joomla\Plugin\System\Webauthn\PluginTraits\AdditionalLoginButtons; -use Joomla\Plugin\System\Webauthn\PluginTraits\AjaxHandler; -use Joomla\Plugin\System\Webauthn\PluginTraits\AjaxHandlerChallenge; -use Joomla\Plugin\System\Webauthn\PluginTraits\AjaxHandlerCreate; -use Joomla\Plugin\System\Webauthn\PluginTraits\AjaxHandlerDelete; -use Joomla\Plugin\System\Webauthn\PluginTraits\AjaxHandlerLogin; -use Joomla\Plugin\System\Webauthn\PluginTraits\AjaxHandlerSaveLabel; -use Joomla\Plugin\System\Webauthn\PluginTraits\UserDeletion; -use Joomla\Plugin\System\Webauthn\PluginTraits\UserProfileFields; - -/** - * WebAuthn Passwordless Login plugin - * - * The plugin features are broken down into Traits for the sole purpose of making an otherwise supermassive class - * somewhat manageable. You can find the Traits inside the Webauthn/PluginTraits folder. - * - * @since 4.0.0 - */ -class PlgSystemWebauthn extends CMSPlugin -{ - // AJAX request handlers - use AjaxHandler; - use AjaxHandlerCreate; - use AjaxHandlerSaveLabel; - use AjaxHandlerDelete; - use AjaxHandlerChallenge; - use AjaxHandlerLogin; - - // Custom user profile fields - use UserProfileFields; - - // Handle user profile deletion - use UserDeletion; - - // Add WebAuthn buttons - use AdditionalLoginButtons; - - /** - * Constructor. Loads the language files as well. - * - * @param DispatcherInterface $subject The object to observe - * @param array $config An optional associative array of configuration - * settings. Recognized key values include 'name', - * 'group', 'params', 'language (this list is not meant - * to be comprehensive). - * - * @since 4.0.0 - */ - public function __construct(&$subject, array $config = []) - { - parent::__construct($subject, $config); - - /** - * Note: Do NOT try to load the language in the constructor. This is called before Joomla initializes the - * application language. Therefore the temporary Joomla language object and all loaded strings in it will be - * destroyed on application initialization. As a result we need to call loadLanguage() in each method - * individually, even though all methods make use of language strings. - */ - - // Register a debug log file writer - Joomla::addLogger('system'); - } -} diff --git a/plugins/system/webauthn/webauthn.xml b/plugins/system/webauthn/webauthn.xml index 36aac9222ffdc..d36d8b89e7581 100644 --- a/plugins/system/webauthn/webauthn.xml +++ b/plugins/system/webauthn/webauthn.xml @@ -11,12 +11,31 @@ PLG_SYSTEM_WEBAUTHN_DESCRIPTION Joomla\Plugin\System\Webauthn - webauthn.php forms + services src language/en-GB/plg_system_webauthn.ini language/en-GB/plg_system_webauthn.sys.ini + + +
    + + + + + +
    +
    +
    From 7475255109c88e3568d01db6f18e3d9cba996dec Mon Sep 17 00:00:00 2001 From: Harald Leithner Date: Tue, 17 May 2022 22:38:57 +0200 Subject: [PATCH 17/21] Add psr12 converter Thanks to @brianteeman --- build/psr12/.editorconfig | 16 ++ build/psr12/clean_errors.php | 174 ++++++++++++++ build/psr12/convert_pull_requests.php | 275 ++++++++++++++++++++++ build/psr12/phpcs.joomla.report.php | 316 ++++++++++++++++++++++++++ build/psr12/psr12_converter.php | 292 ++++++++++++++++++++++++ build/psr12/ruleset.xml | 287 +++++++++++++++++++++++ 6 files changed, 1360 insertions(+) create mode 100644 build/psr12/.editorconfig create mode 100644 build/psr12/clean_errors.php create mode 100644 build/psr12/convert_pull_requests.php create mode 100644 build/psr12/phpcs.joomla.report.php create mode 100644 build/psr12/psr12_converter.php create mode 100644 build/psr12/ruleset.xml diff --git a/build/psr12/.editorconfig b/build/psr12/.editorconfig new file mode 100644 index 0000000000000..34010d1b7e1dc --- /dev/null +++ b/build/psr12/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style end of lines and a blank line at the end of the file +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{js,json,scss,css,vue}] +indent_size = 2 diff --git a/build/psr12/clean_errors.php b/build/psr12/clean_errors.php new file mode 100644 index 0000000000000..257cd183d49f8 --- /dev/null +++ b/build/psr12/clean_errors.php @@ -0,0 +1,174 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +$tmpDir = dirname(__DIR__) . '/tmp/psr12'; + +$cleaned = []; + +$json = file_get_contents($tmpDir . '/cleanup.json'); + +$data = json_decode($json, JSON_OBJECT_AS_ARRAY); + +// Fixing the later issues in a file first should allow us to preserve the line value per error +$data = array_reverse($data); + +foreach ($data as $error) { + $file = file_get_contents($error['file']); + switch ($error['cleanup']) { + // Remove defined JEXEC statement, PSR-12 doesn't allow functional and symbol code in the same file + case 'definedJEXEC': + $file = str_replace([ + /** + * I know this looks silly but makes it more clear what's happening + * We remove the different types of execution check from files which + * only defines symbols (like classes). + * + * The order is important. + */ + "\defined('_JEXEC') || die();", + "defined('_JEXEC') || die();", + "\defined('_JEXEC') || die;", + "defined('_JEXEC') || die;", + "\defined('_JEXEC') or die();", + "defined('_JEXEC') or die();", + "\defined('_JEXEC') or die;", + "defined('_JEXEC') or die;", + "\defined('JPATH_PLATFORM') or die();", + "defined('JPATH_PLATFORM') or die();", + "\defined('JPATH_PLATFORM') or die;", + "defined('JPATH_PLATFORM') or die;", + "\defined('JPATH_BASE') or die();", + "defined('JPATH_BASE') or die();", + "\defined('JPATH_BASE') or die;", + "defined('JPATH_BASE') or die;", + /** + * We have variants of comments in front of the 'defined die' statement + * which we would like to remove too. + * + * The order is important. + */ + "// No direct access.", + "// No direct access", + "// no direct access", + "// Restrict direct access", + "// Protect from unauthorized access", + ], '', $file); + break; + + // Not all files need a namespace + case 'MissingNamespace': + // We search for the end of the first doc block and add the exception for this file + $pos = strpos($file, ' */'); + $file = substr_replace( + $file, + "\n * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace\n", + $pos, + 0 + ); + + break; + // Not all classes have to be camelcase + case 'ValidClassNameNotCamelCaps': + // We search for the end of the first doc block and add the exception for this file + $pos = strpos($file, ' */'); + $file = substr_replace( + $file, + "\n * @phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps\n", + $pos, + 0 + ); + + break; + + case 'ConstantVisibility': + // add public to const declaration if defined in a class + $fileContent = file($error['file']); + $fileContent[$error['line'] - 1] = substr_replace($fileContent[$error['line'] - 1], 'public ', $error['column'] - 1, 0); + $file = implode('', $fileContent); + + break; + + case 'SpaceAfterCloseBrace': + // We only move single comments (starting with //) to the next line + + $fileContent = file($error['file']); + + $lineNo = $error['line']; + + // We skip blank lines + do { + $nextLine = ltrim($fileContent[$lineNo]); + if (empty($nextLine)) { + $lineNo = $lineNo + 1; + $nextLine = ltrim($fileContent[$lineNo]); + } + } while (empty($nextLine)); + + $sourceLineStartNo = $lineNo; + $sourceLineEndNo = $lineNo; + $found = false; + + while (substr(ltrim($fileContent[$sourceLineEndNo]), 0, 2) === '//') { + $sourceLineEndNo++; + $found = true; + } + + if ($sourceLineStartNo === $sourceLineEndNo) { + if (substr(ltrim($fileContent[$sourceLineStartNo]), 0, 2) === '/*') { + while (substr(ltrim($fileContent[$sourceLineEndNo]), 0, 2) !== '*/') { + $sourceLineEndNo++; + } + $sourceLineEndNo++; + $found = true; + } + } + + if (!$found) { + echo "Unrecoverable error while running SpaceAfterCloseBrace cleanup"; + var_dump($error['file'], $sourceLineStartNo, $sourceLineEndNo); + die(1); + } + $targetLineNo = $sourceLineEndNo + 1; + + // Adjust the indentation to match the next line of code + for ($indent = 0; $indent <= strlen($fileContent[$targetLineNo]); $indent++) { + if ($fileContent[$targetLineNo][$indent] !== ' ') { + break; + } + } + + $replace = []; + for ($i = $sourceLineStartNo; $i < $sourceLineEndNo; $i++) { + $newLine = ltrim($fileContent[$i]); + // Fix codeblocks not starting with /** + if (substr($newLine, 0, 2) === '/*') { + $newLine = "/**\n"; + } + + $localIndent = $indent; + if ($newLine[0] === '*') { + $localIndent++; + } + $replace[] = str_repeat(' ', $localIndent) . $newLine; + } + array_unshift($replace, $fileContent[$sourceLineEndNo]); + + array_splice($fileContent, $sourceLineStartNo, count($replace), $replace); + + $file = implode('', $fileContent); + + break; + } + + file_put_contents($error['file'], $file); + $cleaned[] = $error['file'] . ' ' . $error['cleanup']; +} + +file_put_contents($tmpDir . '/cleaned.log', implode("\n", $cleaned)); diff --git a/build/psr12/convert_pull_requests.php b/build/psr12/convert_pull_requests.php new file mode 100644 index 0000000000000..415ff07886450 --- /dev/null +++ b/build/psr12/convert_pull_requests.php @@ -0,0 +1,275 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Set defaults +$scriptRoot = __DIR__; +$repoRoot = false; +$prNumber = false; +$createPullRequest = true; +$php = 'php'; +$git = 'git'; +$gh = 'gh'; +$checkPath = false; +$ghRepo = 'joomla/joomla-cms'; +$baseBranches = '4.2-dev'; // '4.1-dev,4.2-dev,4.3-dev'; We only check for 4.2-dev + +$script = array_shift($argv); + +if (empty($argv)) { + echo <<] --repo= + + Description: + The converter converts all open pull requests on github + to the PSR12 standard and pushes the changes back to github. + + Flow: + * Load all pull requests from Github for the repository + $ghRepo + * Checkout each open pull request with the base branch matching + $baseBranches + * Merge into the checked out branch up to the psr12anchor tag + which includes the conversion script. + * Run the psr12_converter.php with the task "BRANCH" + * Merge up to the psr12final tag with strategy "OURS" + * Push the changes back to the PR or create a new PR if we + don't have commit rights to the repository + + --pr: + Only convert the given github pull request id. + + --repo: + The path to the repository root. + + TEXT; + die(1); +} + +foreach ($argv as $arg) { + if (substr($arg, 0, 2) === '--') { + $argi = explode('=', $arg, 2); + switch ($argi[0]) { + case '--pr': + $prNumber = $argi[1]; + break; + case '--repo': + $repoRoot = $argi[1]; + break; + } + } else { + $checkPath = $arg; + break; + } +} + +if (!$repoRoot) { + die('You have to set the repository root! (--repo)'); +} + +$cmd = $git . ' -C "' . $scriptRoot . '" rev-parse --show-toplevel'; +$output = []; +$scriptInRepo = false; +$repoScript = ''; +exec($cmd, $output, $result); +if ($result === 0) { + $scriptInRepo = true; + $repoScript = $output[0]; +} + +$cmd = $git . ' -C "' . $repoRoot . '" rev-parse --show-toplevel'; +$output = []; +exec($cmd, $output, $result); +if ($result !== 0) { + die($repoRoot . ' is not a git repository.'); +} + +$repoRoot = $output[0]; + +if ($scriptInRepo && $repoRoot === $repoScript) { + die($script . ' must be located outside of the git repository'); +} + +echo "Changing to working directory: " . $repoRoot . "\n"; +chdir($repoRoot); + +echo "Validate gh client...\n"; +$cmd = $gh; +$output = []; +exec($cmd, $output, $result); +if ($result !== 0) { + die('Github cli client not found. Please install the client first (https://cli.github.com)'); +} + +echo "Validate gh authentication...\n"; +$cmd = $gh . ' auth status'; +passthru($cmd, $result); +if ($result !== 0) { + die('Please login with the github cli client first. (gh auth login)'); +} + +$fieldList = [ + "number", + "author", + "baseRefName", + "headRefName", + "headRepository", + "headRepositoryOwner", + "isCrossRepository", + "maintainerCanModify", + "mergeStateStatus", + "mergeable", + "state", + "title", + "url", + "labels", +]; + +$branches = 'base:' . implode(' base:', explode(',', $baseBranches)); + + +if (!empty($prNumber)) { + echo "Retrieving Pull Request " . $prNumber . "...\n"; + $cmd = $gh . ' pr view ' . $prNumber . ' --json ' . implode(',', $fieldList); +} else { + echo "Retrieving Pull Request list...\n"; + $cmd = $gh . ' pr list --limit 1000 --json ' . implode(',', $fieldList) . ' --search "is:pr is:open -label:psr12 ' . $branches . '"'; +} + +$output = []; +exec($cmd, $output, $result); +if ($result !== 0) { + var_dump([$cmd, $output, $result]); + die('Unable to retrieve PR list.'); +} + +$json = $output[0]; + +if (!empty($prNumber)) { + $json = '[' . $json . ']'; +} + +$list = json_decode($json, true); + +echo "\nFound " . count($list) . " pull request(s).\n"; + +foreach ($list as $pr) { + echo "Checkout #" . $pr['number'] . "\n"; + + $cmd = $gh . ' pr checkout ' . $pr['url'] . ' --force -b psr12/merge/' . $pr['number']; + $output = []; + exec($cmd, $output, $result); + if ($result !== 0) { + var_dump([$cmd, $output, $result]); + die('Unable to checkout pr #' . $pr['number']); + } + + echo "Upmerge to psr12anchor\n"; + + $cmd = $git . ' merge psr12anchor'; + $output = []; + exec($cmd, $output, $result); + if ($result !== 0) { + var_dump([$cmd, $output, $result]); + echo 'Unable to upmerge to psr12anchor pr #' . $pr['number'] . "\n"; + echo "Abort merge...\n"; + $cmd = $git . ' merge --abort'; + $output = []; + exec($cmd, $output, $result); + continue; + } + + echo "Run PSR-12 converter script\n"; + + $cmd = $php . ' ' . $scriptRoot . '/psr12_converter.php --task=branch --repo="' . $repoRoot . '"'; + + passthru($cmd, $result); + if ($result !== 0) { + var_dump([$cmd, $result]); + die('Unable to convert to psr-12 pr #' . $pr['number']); + } + + echo "Upmerge to psr12final\n"; + + $cmd = $git . ' merge --strategy=ort --strategy-option=ours psr12final'; + $output = []; + exec($cmd, $output, $result); + if ($result !== 0) { + var_dump([$cmd, $result]); + die('Unable to upmerge to psr-12 pr #' . $pr['number']); + } + + if (!$createPullRequest && $pr['maintainerCanModify'] === true) { + echo "Push directly to PR branch\n"; + + $cmd = $git . ' push git@github.com:' . $pr['headRepositoryOwner']['login'] . '/' . $pr['headRepository']['name'] . '.git ' + . 'psr12/merge/' . $pr['number'] . ':' . $pr['headRefName']; + $output = []; + exec($cmd, $output, $result); + if ($result !== 0) { + var_dump([$cmd, $output, $result]); + die('Unable to directly push for pr #' . $pr['number']); + } + + $cmd = $gh . ' pr comment ' . $pr['url'] . ' --body "This pull requests has been automatically converted to the PSR-12 coding standard."'; + $output = []; + exec($cmd, $output, $result); + if ($result !== 0) { + var_dump([$cmd, $output, $result]); + die('Unable to create a comment for pr #' . $pr['number']); + } + } else { + echo "Create pull request\n"; + + $cmd = $git . ' push --force -u github HEAD'; + $output = []; + exec($cmd, $output, $result); + if ($result !== 0) { + var_dump([$cmd, $output, $result]); + die('Unable to push to github for pr #' . $pr['number']); + } + + $cmd = $gh . ' pr create --title "PSR-12 conversion" --body "This pull requests converts the branch to the PSR-12 coding standard." ' + . '-R ' . $pr['headRepositoryOwner']['login'] . '/' . $pr['headRepository']['name'] . ' -B ' . $pr['headRefName']; + $output = []; + exec($cmd, $output, $result); + if ($result !== 0) { + var_dump([$cmd, $output, $result]); + die('Unable to create pull request for pr #' . $pr['number']); + } + + $cmd = $gh . ' pr comment ' . $pr['url'] + . ' --body "A new pull request has been created automatically to convert this PR to the PSR-12 coding standard.' + . ' The pr can be found at ' . $output[0] . '"'; + $output = []; + exec($cmd, $output, $result); + if ($result !== 0) { + var_dump([$cmd, $output, $result]); + die('Unable to create a comment for pr #' . $pr['number']); + } + } + + // Set label + echo "Set psr12 label\n"; + + $cmd = $gh . ' pr edit ' . $pr['url'] . ' --add-label psr12'; + $output = []; + exec($cmd, $output, $result); + if ($result !== 0) { + var_dump([$cmd, $output, $result]); + die('Unable to set psr12 label for pr #' . $pr['number']); + } +} diff --git a/build/psr12/phpcs.joomla.report.php b/build/psr12/phpcs.joomla.report.php new file mode 100644 index 0000000000000..c058aec96beb9 --- /dev/null +++ b/build/psr12/phpcs.joomla.report.php @@ -0,0 +1,316 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Reports; + +use PHP_CodeSniffer\Config; +use PHP_CodeSniffer\Files\File; + +use function array_keys; +use function array_merge; +use function array_values; +use function file_exists; +use function file_get_contents; +use function file_put_contents; +use function json_encode; +use function str_replace; + +use const JSON_OBJECT_AS_ARRAY; +use const JSON_PRETTY_PRINT; + + +class Joomla implements \PHP_CodeSniffer\Reports\Report +{ + private $tmpDir = __DIR__ . '/../tmp/psr12'; + + private $html = ''; + + private $preProcessing = []; + + /** + * Generate a partial report for a single processed file. + * + * Function should return TRUE if it printed or stored data about the file + * and FALSE if it ignored the file. Returning TRUE indicates that the file and + * its data should be counted in the grand totals. + * + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. + * + * @return bool + */ + public function generateFileReport($report, File $phpcsFile, $showSources = false, $width = 80) + { + if ($report['errors'] === 0 && $report['warnings'] === 0) { + return false; + } + + $template = [ + 'headline' => $report['filename'], + 'text' => 'Errors: ' . $report['errors'] . ' Warnings: ' . $report['warnings'] . ' Fixable: ' . $report['fixable'], + ]; + + $this->html = << +

    {$template['headline']}

    +

    {$template['text']}

    + HTML; + + foreach ($report['messages'] as $line => $lineErrors) { + foreach ($lineErrors as $column => $colErrors) { + foreach ($colErrors as $error) { + $error['type'] = strtolower($error['type']); + if ($phpcsFile->config->encoding !== 'utf-8') { + $error['message'] = iconv($phpcsFile->config->encoding, 'utf-8', $error['message']); + } + + $error['fixable'] = $error['fixable'] === true ? 'Yes' : 'No'; + + $this->html .= << + Line: $line + Column: $column + Fixable: {$error['fixable']} + Severity: {$error['severity']} + Rule: {$error['source']} +
    {$error['message']}
    +
    + HTML; + $this->prepareProcessing($report['filename'],$phpcsFile, $line, $column, $error); + } + } + } + + $this->html .= << + HTML; + + $this->writeFile(); + + return true; + } + + private function prepareProcessing($file, $phpcsFile, $line, $column, $error) { + + switch ($error['source']) { + case 'PSR1.Files.SideEffects.FoundWithSymbols': + $fileContent = file_get_contents($file); + + if ( + strpos($fileContent, "defined('_JEXEC')") !== false + || strpos($fileContent, "defined('JPATH_PLATFORM')") !== false + || strpos($fileContent, "defined('JPATH_BASE')") !== false + ) { + $this->preProcessing[] = [ + 'file' => $file, + 'line' => $line, + 'column' => $column, + 'cleanup' => 'definedJEXEC' + ]; + } else { + $targetFile = $this->tmpDir . '/' . $error['source'] . '.txt'; + $fileContent = ''; + if (file_exists($targetFile)) { + $fileContent = file_get_contents($targetFile); + } + + static $replace = null; + + if ($replace === null) { + $replace = [ + "\\" => '/', + dirname(dirname(__DIR__)) . '/' => '', + '.' => '\.', + ]; + } + + $fileContent .= " " . str_replace(array_keys($replace), $replace, $file) . "\n"; + file_put_contents($targetFile, $fileContent); + } + break; + + case 'PSR1.Classes.ClassDeclaration.MissingNamespace': + $this->preProcessing[] = [ + 'file' => $file, + 'line' => $line, + 'column' => $column, + 'cleanup' => 'MissingNamespace' + ]; + break; + + case 'Squiz.Classes.ValidClassName.NotCamelCaps': + if ( + strpos($file, 'localise') !== false + || strpos($file, 'recaptcha_invisible') !== false + ) { + $this->preProcessing[] = [ + 'file' => $file, + 'line' => $line, + 'column' => $column, + 'cleanup' => 'ValidClassNameNotCamelCaps' + ]; + } + break; + + case 'Squiz.ControlStructures.ControlSignature.SpaceAfterCloseBrace': + $this->preProcessing[] = [ + 'file' => $file, + 'line' => $line, + 'column' => $column, + 'cleanup' => 'SpaceAfterCloseBrace' + ]; + break; + + case 'PSR12.Properties.ConstantVisibility.NotFound': + $this->preProcessing[] = [ + 'file' => $file, + 'line' => $line, + 'column' => $column, + 'cleanup' => 'ConstantVisibility' + ]; + break; + + case 'PSR2.Classes.PropertyDeclaration.Underscore': + case 'PSR2.Methods.MethodDeclaration.Underscore': + case 'PSR1.Classes.ClassDeclaration.MultipleClasses': + case 'PSR1.Methods.CamelCapsMethodName.NotCamelCaps': + + $targetFile = $this->tmpDir . '/' . $error['source'] . '.txt'; + $fileContent = ''; + if (file_exists($targetFile)) { + $fileContent = file_get_contents($targetFile); + } + + static $replace = null; + + if ($replace === null) { + $replace = [ + "\\" => '/', + dirname(dirname(__DIR__)) . '/' => '', + '.' => '\.', + ]; + } + + $fileContent .= " " . str_replace(array_keys($replace), $replace, $file) . "\n"; + file_put_contents($targetFile, $fileContent); + break; + } + } + + /** + * Prints all violations for processed files, in a proprietary XML format. + * + * @param string $cachedData Any partial report data that was returned from + * generateFileReport during the run. + * @param int $totalFiles Total number of files processed during the run. + * @param int $totalErrors Total number of errors found during the run. + * @param int $totalWarnings Total number of warnings found during the run. + * @param int $totalFixable Total number of problems that can be fixed. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. + * @param bool $interactive Are we running in interactive mode? + * @param bool $toScreen Is the report being printed to screen? + * + * @return void + */ + public function generate( + $cachedData, + $totalFiles, + $totalErrors, + $totalWarnings, + $totalFixable, + $showSources = false, + $width = 80, + $interactive = false, + $toScreen = true + ) { + + $preprocessing = []; + if (file_exists($this->tmpDir .'/cleanup.json')) { + $preprocessing = json_decode(file_get_contents($this->tmpDir .'/cleanup.json'), JSON_OBJECT_AS_ARRAY); + } + + $preprocessing = array_merge($this->preProcessing, $preprocessing); + file_put_contents($this->tmpDir .'/cleanup.json', json_encode($preprocessing, JSON_PRETTY_PRINT)); + } + + private function getTemplate($section) + { + $sections = [ + 'header' => << + + Report + + + +
    +
    + + +
    +
    Check
    + HTML, + 'footer' => << +
    +
    + + + HTML, + 'line' => << +

    %HEADLINE%

    +

    %TEXT%

    +
    +
    %ERROR%
    +
    +
    + HTML + ]; + + return $sections[$section]; + } + + private function htmlAddBlock($headline, $text, $error) + { + $line = $this->getTemplate('line'); + + $replace = [ + '%HEADLINE%' => $headline, + '%TEXT%' => $text, + '%ERROR%' => $error, + ]; + + $this->html .= str_replace(array_keys($replace), $replace, $line); + } + + private function writeFile() + { + $file = $this->tmpDir . '/result.html'; + + if (file_exists($file)) { + $html = file_get_contents($file); + } else { + $html = $this->getTemplate('header'); + $html .= ''; + $html .= $this->getTemplate('footer'); + } + + $html = str_replace('', $this->html . '', $html); + + file_put_contents($this->tmpDir . '/result.html', $html); + } +} diff --git a/build/psr12/psr12_converter.php b/build/psr12/psr12_converter.php new file mode 100644 index 0000000000000..79a644081ff8e --- /dev/null +++ b/build/psr12/psr12_converter.php @@ -0,0 +1,292 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Set defaults +$root = dirname(dirname(__DIR__)); +$php = 'php'; +$git = 'git'; +$checkPath = false; +$tasks = [ + 'CBF' => false, + 'CS' => false, + 'CLEAN' => false, + 'CMS' => false, + 'BRANCH' => false, +]; + +$script = array_shift($argv); + +if (empty($argv)) { + echo << [path] + + Description: + The converter has several tasks which can be run separately. + You can combine them seperated by a comma (,). + + --tasks: + * CBF + This task executes the PHP Code Beautifier and Fixer. It + does the heavy lifting making the code PSR-12 compatible. + (beware this tasks modifies many files) + * CS + This task executes the PHP Code Sniffer. It collects all + issues and generates a HTML report in build/tmp/psr12. + Also it generates a collection of issues which can't be + fixed by CBF. This information is saved as json in + build/tmp/psr12/cleanup.json. If this option is activated + the tmp directory is cleaned before running. + * CLEAN + This tasks loads the cleanup.json generated by the CS task + and changes the cms specific files. After completing this + task it re-runs the CBF and CS task. + * CMS + This tasks activates all other tasks and automatically + commits the changes after both CBF runs. Usually only + needed for the first cms conversion. + * BRANCH + This tasks updates all files changed by the current + branch compared to the psr12anchor tag. This allows + to update a create pull request. + + --repo: + The path to the repository root. + + Path: + Providing a path will only check the directories or files + specified. It's possible to add multiple files and folder + seperated by a comma (,). + + + TEXT; + die(1); +} + +foreach ($argv as $arg) { + if (substr($arg, 0, 2) === '--') { + $argi = explode('=', $arg, 2); + switch ($argi[0]) { + case '--task': + foreach ($tasks as $task => $value) { + if (stripos($argi[1], $task) !== false) { + $tasks[$task] = true; + } + } + break; + case '--repo': + $root = $argi[1]; + break; + } + } else { + $checkPath = $arg; + break; + } +} + +$tmpDir = $root . '/build/tmp/psr12'; + +if ($tasks['CMS']) { + $tasks['CBF'] = true; + $tasks['CS'] = true; + $tasks['CLEAN'] = true; +} + +if ($tasks['BRANCH']) { + $tasks['CMS'] = true; + $tasks['CBF'] = true; + $tasks['CS'] = true; + $tasks['CLEAN'] = true; + + $cmd = $git . ' --no-pager diff --name-only psr12anchor..HEAD'; + exec($cmd, $output, $result); + if ($result !== 0) { + die('Unable to find changes for this branch'); + } + + foreach($output as $k => $line) { + if (substr($line, -4) !== '.php') { + unset($output[$k]); + } + } + + $checkPath = implode(',', $output); + if (empty($checkPath)) { + die(0); + } +} + +$items = []; +if ($checkPath) { + $items = explode(',', $checkPath); +} else { + $items[] = 'index.php'; + $items[] = 'administrator/index.php'; + + $baseFolders = [ + 'administrator/components', + 'administrator/includes', + 'administrator/language', + 'administrator/modules', + 'administrator/templates', + 'api', + 'cli', + 'components', + 'includes', + 'installation', + 'language', + 'layouts', + 'libraries', + 'modules', + 'plugins', + 'templates', + ]; + + foreach ($baseFolders as $folder) { + $dir = dir($root . '/' . $folder); + while (false !== ($entry = $dir->read())) { + if (($entry === ".") || ($entry === "..")) { + continue; + } + if (!is_dir($dir->path . '/' . $entry)) { + if (substr($entry, -4) !== '.php') { + continue; + } + } + if ( + $folder === 'libraries' + && ( + $entry === 'php-encryption' + || $entry === 'phpass' + || $entry === 'vendor' + ) + ) { + continue; + } + $items[] = str_replace($root . '/', '', $dir->path) . '/' . $entry; + } + $dir->close(); + } +} +$executedTasks = implode( + ',', + array_keys( + array_filter($tasks, function ($task) { + return $task; + }) + ) +); +$executedPaths = implode("\n", $items); + +echo << + + The Joomla CMS PSR-12 exceptions. + + + build/* + cache/* + docs/* + logs/* + media/* + node_modules/* + tmp/* + tests/* + + + libraries/php-encryption/* + libraries/phpass/* + libraries/vendor/* + + stubs.php + configuration.php + + + + + + + + + + + + + + administrator/components/com_banners/src/Controller/BannersController\.php + + + + administrator/components/com_banners/src/Model/DownloadModel\.php + administrator/components/com_banners/src/Table/BannerTable\.php + administrator/components/com_banners/src/Table/ClientTable\.php + administrator/components/com_cache/src/Model/CacheModel\.php + administrator/components/com_contact/src/Table/ContactTable\.php + administrator/components/com_fields/src/Table/FieldTable\.php + administrator/components/com_fields/src/Table/GroupTable\.php + administrator/components/com_finder/src/Table/FilterTable\.php + administrator/components/com_finder/src/Table/LinkTable\.php + administrator/components/com_installer/src/Model/DatabaseModel\.php + administrator/components/com_installer/src/Model/InstallModel\.php + administrator/components/com_installer/src/Table/UpdatesiteTable\.php + administrator/components/com_mails/src/Table/TemplateTable\.php + administrator/components/com_menus/src/Helper/MenusHelper\.php + administrator/components/com_menus/src/Model/MenuModel\.php + administrator/components/com_newsfeeds/src/Table/NewsfeedTable\.php + administrator/components/com_plugins/src/Model/PluginModel\.php + administrator/components/com_privacy/src/Table/RequestTable\.php + administrator/components/com_scheduler/src/Table/TaskTable\.php + administrator/components/com_tags/src/Table/TagTable\.php + administrator/components/com_templates/src/Model/StyleModel\.php + administrator/components/com_users/src/Model/UserModel\.php + administrator/components/com_users/src/Table/NoteTable\.php + administrator/components/com_workflow/src/Table/StageTable\.php + administrator/components/com_workflow/src/Table/TransitionTable\.php + administrator/components/com_workflow/src/Table/WorkflowTable\.php + components/com_banners/src/Model/BannerModel\.php + components/com_contact/src/Model/CategoriesModel\.php + components/com_contact/src/Model/CategoryModel\.php + components/com_contact/src/Model/ContactModel\.php + components/com_content/src/Model/ArchiveModel\.php + components/com_content/src/Model/ArticleModel\.php + components/com_content/src/Model/CategoriesModel\.php + components/com_content/src/Model/CategoryModel\.php + components/com_content/src/Model/FeaturedModel\.php + components/com_newsfeeds/src/Model/CategoriesModel\.php + components/com_newsfeeds/src/Model/CategoryModel\.php + components/com_newsfeeds/src/Model/NewsfeedModel\.php + components/com_tags/src/Model/TagsModel\.php + installation/src/View/Preinstall/HtmlView\.php + libraries/src/Adapter/Adapter\.php + libraries/src/Application/ApplicationHelper\.php + libraries/src/Cache/Cache\.php + libraries/src/Cache/CacheStorage\.php + libraries/src/Cache/Controller/OutputController\.php + libraries/src/Cache/Controller/PageController\.php + libraries/src/Cache/Storage/FileStorage\.php + libraries/src/Cache/Storage/MemcachedStorage\.php + libraries/src/Cache/Storage/RedisStorage\.php + libraries/src/Categories/Categories\.php + libraries/src/Categories/CategoryNode\.php + libraries/src/Client/FtpClient\.php + libraries/src/Document/Document\.php + libraries/src/Document/DocumentRenderer\.php + libraries/src/Document/ErrorDocument\.php + libraries/src/Document/HtmlDocument\.php + libraries/src/Document/JsonDocument\.php + libraries/src/Document/OpensearchDocument\.php + libraries/src/Document/Renderer/Feed/AtomRenderer\.php + libraries/src/Document/Renderer/Feed/RssRenderer\.php + libraries/src/Editor/Editor\.php + libraries/src/Encrypt/Totp\.php + libraries/src/Form/Field/CaptchaField\.php + libraries/src/Input/Json\.php + libraries/src/MVC/Model/DatabaseAwareTrait\.php + libraries/src/MVC/Model/FormBehaviorTrait\.php + libraries/src/MVC/Model/ItemModel\.php + libraries/src/MVC/Model/StateBehaviorTrait\.php + libraries/src/MVC/View/AbstractView\.php + libraries/src/MVC/View/HtmlView\.php + libraries/src/MVC/View/JsonView\.php + libraries/src/Object/CMSObject\.php + libraries/src/Plugin/CMSPlugin\.php + libraries/src/Router/Route\.php + libraries/src/Table/Category\.php + libraries/src/Table/Content\.php + libraries/src/Table/CoreContent\.php + libraries/src/Table/Extension\.php + libraries/src/Table/Menu\.php + libraries/src/Table/Module\.php + libraries/src/Table/Nested\.php + libraries/src/Table/Table\.php + libraries/src/Table/Update\.php + libraries/src/Table/User\.php + libraries/src/Toolbar/Toolbar\.php + libraries/src/Tree/ImmutableNodeTrait\.php + libraries/src/Updater/UpdateAdapter\.php + libraries/src/User/User\.php + plugins/editors/tinymce/tinymce\.php + plugins/system/cache/cache\.php + + + + administrator/components/com_content/src/Service/HTML/Icon\.php + administrator/components/com_installer/src/Controller/InstallController\.php + administrator/components/com_installer/src/Model/DiscoverModel\.php + administrator/components/com_installer/src/Model/WarningsModel\.php + administrator/components/com_users/src/Service/HTML/Users\.php + components/com_content/helpers/icon\.php + components/com_content/helpers/icon\.php + libraries/src/Filesystem/Stream\.php + libraries/src/Filesystem/Streams/StreamString\.php + libraries/src/Installer/Adapter/ComponentAdapter\.php + libraries/src/Installer/Adapter/LanguageAdapter\.php + libraries/src/Installer/Adapter/ModuleAdapter\.php + libraries/src/Installer/Installer\.php + libraries/src/Installer/InstallerAdapter\.php + libraries/src/Language/Transliterate\.php + libraries/src/Mail/Mail\.php + libraries/src/Pagination/Pagination\.php + libraries/src/Toolbar/ToolbarHelper\.php + libraries/src/Utility/BufferStreamHandler\.php + + + + + libraries/cms\.php + libraries/loader\.php + + + administrator/components/com_fields/src/Model/FieldsModel\.php + administrator/components/com_fields/src/Model/GroupsModel\.php + administrator/components/com_fields/src/Table/FieldTable\.php + administrator/components/com_fields/src/Table/GroupTable\.php + administrator/components/com_finder/src/Model/MapsModel\.php + administrator/components/com_installer/src/Model/InstallerModel\.php + administrator/components/com_installer/src/Model/InstallModel\.php + administrator/components/com_installer/src/Model/LanguagesModel\.php + administrator/components/com_installer/src/Model/UpdateModel\.php + administrator/components/com_login/src/Model/LoginModel\.php + administrator/components/com_modules/src/Model/ModulesModel\.php + administrator/components/com_plugins/src/Model/PluginsModel\.php + administrator/components/com_scheduler/src/Model/TasksModel\.php + administrator/components/com_scheduler/src/Table/TaskTable\.php + administrator/components/com_users/src/Model/UsersModel\.php + administrator/components/com_workflow/src/Table/StageTable\.php + administrator/components/com_workflow/src/Table/TransitionTable\.php + administrator/components/com_workflow/src/Table/WorkflowTable\.php + api/components/com_contact/src/Controller/ContactController\.php + components/com_config/src/View/Config/HtmlView\.php + components/com_config/src/View/Modules/HtmlView\.php + components/com_config/src/View/Templates/HtmlView\.php + components/com_contact/src/Controller/ContactController\.php + components/com_contact/src/View/Contact/HtmlView\.php + components/com_contact/src/View/Featured/HtmlView\.php + components/com_contact/src/View/Form/HtmlView\.php + components/com_content/src/Model/CategoryModel\.php + components/com_content/src/View/Archive/HtmlView\.php + components/com_content/src/View/Article/HtmlView\.php + components/com_content/src/View/Featured/HtmlView\.php + components/com_content/src/View/Form/HtmlView\.php + components/com_newsfeeds/src/View/Newsfeed/HtmlView\.php + components/com_tags/src/Helper/RouteHelper\.php + components/com_tags/src/View/Tag/HtmlView\.php + components/com_tags/src/View/Tags/HtmlView\.php + installation/src/Form/Field/Installation/LanguageField\.php + libraries/src/Cache/Cache\.php + libraries/src/Cache/CacheStorage\.php + libraries/src/Cache/Controller/CallbackController\.php + libraries/src/Cache/Controller/PageController\.php + libraries/src/Cache/Controller/ViewController\.php + libraries/src/Cache/Storage/FileStorage\.php + libraries/src/Cache/Storage/MemcachedStorage\.php + libraries/src/Captcha/Captcha\.php + libraries/src/Categories/Categories\.php + libraries/src/Client/FtpClient\.php + libraries/src/Document/Document\.php + libraries/src/Document/DocumentRenderer\.php + libraries/src/Document/HtmlDocument\.php + libraries/src/Editor/Editor\.php + libraries/src/Encrypt/Base32\.php + libraries/src/Environment/Browser\.php + libraries/src/Feed/FeedFactory\.php + libraries/src/Filesystem/Folder\.php + libraries/src/Filesystem/Stream\.php + libraries/src/Filesystem/Support/StringController\.php + libraries/src/HTML/Helpers/Grid\.php + libraries/src/Installer/Adapter/ComponentAdapter\.php + libraries/src/Installer/Adapter/LanguageAdapter\.php + libraries/src/Installer/Adapter/ModuleAdapter\.php + libraries/src/Installer/Adapter/PackageAdapter\.php + libraries/src/MVC/Model/BaseDatabaseModel\.php + libraries/src/MVC/Model/LegacyModelLoaderTrait\.php + libraries/src/MVC/Model/ListModel\.php + libraries/src/MVC/View/HtmlView\.php + libraries/src/Pagination/Pagination\.php + libraries/src/Table/Category\.php + libraries/src/Table/Category\.php + libraries/src/Table/Content\.php + libraries/src/Table/Language\.php + libraries/src/Table/MenuType\.php + libraries/src/Table/Module\.php + libraries/src/Table/Nested\.php + libraries/src/Table/Table\.php + libraries/src/Toolbar/Button/ConfirmButton\.php + libraries/src/Toolbar/Button/HelpButton\.php + libraries/src/Toolbar/Button/PopupButton\.php + libraries/src/Toolbar/Button/StandardButton\.php + libraries/src/Updater/Adapter/CollectionAdapter\.php + libraries/src/Updater/Adapter/ExtensionAdapter\.php + libraries/src/Updater/Update\.php + libraries/src/Updater/UpdateAdapter\.php + modules/mod_articles_category/src/Helper/ArticlesCategoryHelper\.php + plugins/content/emailcloak/emailcloak\.php + plugins/content/joomla/joomla\.php + plugins/content/loadmodule/loadmodule\.php + plugins/content/pagebreak/pagebreak\.php + plugins/editors/none/none\.php + plugins/user/joomla/joomla\.php + + + + + administrator/components/com_joomlaupdate/finalisation\.php + + + + + administrator/components/com_installer/src/Model/DatabaseModel\.php + libraries/src/Client/FtpClient\.php + libraries/src/Filesystem/Path\.php + libraries/src/Filesystem/Streams/StreamString\.php + + + index\.php + administrator/index\.php + administrator/components/com_joomlaupdate/extract\.php + administrator/components/com_joomlaupdate/restore_finalisation\.php + administrator/includes/app\.php + administrator/includes/defines\.php + administrator/includes/framework\.php + api/includes/app\.php + api/includes/defines\.php + api/includes/framework\.php + api/index\.php + cli/joomla\.php + includes/app\.php + includes/defines\.php + includes/framework\.php + installation/includes/app\.php + installation/includes/defines\.php + installation/index\.php + libraries/cms\.php + libraries/bootstrap\.php + libraries/import\.php + libraries/import\.legacy\.php + libraries/loader\.php + + From e7f5cc182bb1cd39c7f085d529888ac4fa6fc77e Mon Sep 17 00:00:00 2001 From: Harald Leithner Date: Mon, 27 Jun 2022 20:18:44 +0200 Subject: [PATCH 18/21] Phase 1 convert CMS to PSR-12 --- .../com_actionlogs/services/provider.php | 46 +- .../src/Controller/ActionlogsController.php | 266 +- .../src/Controller/DisplayController.php | 15 +- .../src/Field/ExtensionField.php | 76 +- .../src/Field/LogcreatorField.php | 95 +- .../src/Field/LogsdaterangeField.php | 77 +- .../com_actionlogs/src/Field/LogtypeField.php | 62 +- .../src/Field/PlugininfoField.php | 75 +- .../src/Helper/ActionlogsHelper.php | 659 +- .../src/Model/ActionlogConfigModel.php | 41 +- .../src/Model/ActionlogModel.php | 285 +- .../src/Model/ActionlogsModel.php | 749 +- .../src/Plugin/ActionLogPlugin.php | 151 +- .../com_admin/postinstall/addnosniff.php | 3 +- .../com_admin/postinstall/behindproxy.php | 69 +- .../com_admin/postinstall/htaccesssvg.php | 3 +- .../postinstall/languageaccess340.php | 30 +- .../com_admin/postinstall/statscollection.php | 3 +- .../com_admin/postinstall/textfilter3919.php | 3 +- administrator/components/com_admin/script.php | 17438 ++++++++-------- .../com_admin/services/provider.php | 48 +- .../src/Controller/DisplayController.php | 44 +- .../com_admin/src/Dispatcher/Dispatcher.php | 17 +- .../src/Extension/AdminComponent.php | 43 +- .../com_admin/src/Model/HelpModel.php | 330 +- .../com_admin/src/Model/SysinfoModel.php | 1413 +- .../com_admin/src/Service/HTML/Directory.php | 73 +- .../com_admin/src/Service/HTML/PhpSetting.php | 67 +- .../com_admin/src/Service/HTML/System.php | 23 +- .../com_admin/src/View/Help/HtmlView.php | 123 +- .../com_admin/src/View/Sysinfo/HtmlView.php | 170 +- .../com_admin/src/View/Sysinfo/JsonView.php | 90 +- .../com_admin/src/View/Sysinfo/TextView.php | 308 +- .../com_admin/tmpl/help/default.php | 55 +- .../com_admin/tmpl/help/langforum.php | 6 +- .../com_admin/tmpl/sysinfo/default.php | 35 +- .../com_admin/tmpl/sysinfo/default_config.php | 55 +- .../tmpl/sysinfo/default_directory.php | 55 +- .../tmpl/sysinfo/default_phpinfo.php | 3 +- .../tmpl/sysinfo/default_phpsettings.php | 355 +- .../com_admin/tmpl/sysinfo/default_system.php | 227 +- administrator/components/com_ajax/ajax.php | 1 + .../layouts/joomla/searchtools/default.php | 158 +- .../com_associations/services/provider.php | 46 +- .../src/Controller/AssociationController.php | 127 +- .../src/Controller/AssociationsController.php | 226 +- .../src/Controller/DisplayController.php | 17 +- .../src/Dispatcher/Dispatcher.php | 66 +- .../src/Field/ItemlanguageField.php | 154 +- .../src/Field/ItemtypeField.php | 72 +- .../src/Field/Modal/AssociationField.php | 161 +- .../src/Helper/AssociationsHelper.php | 1316 +- .../src/Model/AssociationModel.php | 33 +- .../src/Model/AssociationsModel.php | 1123 +- .../src/View/Association/HtmlView.php | 700 +- .../src/View/Associations/HtmlView.php | 420 +- .../tmpl/association/edit.php | 115 +- .../tmpl/associations/default.php | 269 +- .../tmpl/associations/modal.php | 254 +- .../com_banners/helpers/banners.php | 2 +- .../com_banners/services/provider.php | 56 +- .../src/Controller/BannerController.php | 168 +- .../src/Controller/BannersController.php | 158 +- .../src/Controller/ClientController.php | 17 +- .../src/Controller/ClientsController.php | 45 +- .../src/Controller/DisplayController.php | 88 +- .../src/Controller/TracksController.php | 289 +- .../src/Extension/BannersComponent.php | 84 +- .../src/Field/BannerclientField.php | 37 +- .../com_banners/src/Field/ClicksField.php | 45 +- .../com_banners/src/Field/ImpmadeField.php | 45 +- .../com_banners/src/Field/ImptotalField.php | 57 +- .../com_banners/src/Helper/BannersHelper.php | 300 +- .../com_banners/src/Model/BannerModel.php | 857 +- .../com_banners/src/Model/BannersModel.php | 482 +- .../com_banners/src/Model/ClientModel.php | 224 +- .../com_banners/src/Model/ClientsModel.php | 562 +- .../com_banners/src/Model/DownloadModel.php | 114 +- .../com_banners/src/Model/TracksModel.php | 982 +- .../com_banners/src/Service/Html/Banner.php | 184 +- .../com_banners/src/Table/BannerTable.php | 690 +- .../com_banners/src/Table/ClientTable.php | 146 +- .../com_banners/src/View/Banner/HtmlView.php | 229 +- .../com_banners/src/View/Banners/HtmlView.php | 366 +- .../com_banners/src/View/Client/HtmlView.php | 249 +- .../com_banners/src/View/Clients/HtmlView.php | 287 +- .../src/View/Download/HtmlView.php | 66 +- .../com_banners/src/View/Tracks/HtmlView.php | 249 +- .../com_banners/src/View/Tracks/RawView.php | 68 +- .../com_banners/tmpl/banner/edit.php | 115 +- .../com_banners/tmpl/banners/default.php | 327 +- .../tmpl/banners/default_batch_body.php | 47 +- .../tmpl/banners/default_batch_footer.php | 5 +- .../com_banners/tmpl/banners/emptystate.php | 14 +- .../com_banners/tmpl/client/edit.php | 75 +- .../com_banners/tmpl/clients/default.php | 307 +- .../com_banners/tmpl/clients/emptystate.php | 14 +- .../com_banners/tmpl/download/default.php | 35 +- .../com_banners/tmpl/tracks/default.php | 139 +- .../com_banners/tmpl/tracks/emptystate.php | 7 +- .../com_cache/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 272 +- .../com_cache/src/Model/CacheModel.php | 515 +- .../com_categories/helpers/categories.php | 1 + .../joomla/form/field/categoryedit.php | 117 +- .../com_categories/services/provider.php | 48 +- .../src/Controller/AjaxController.php | 123 +- .../src/Controller/CategoriesController.php | 168 +- .../src/Controller/CategoryController.php | 440 +- .../src/Controller/DisplayController.php | 175 +- .../src/Dispatcher/Dispatcher.php | 30 +- .../src/Extension/CategoriesComponent.php | 37 +- .../src/Field/CategoryeditField.php | 720 +- .../src/Field/ComponentsCategoryField.php | 129 +- .../src/Field/Modal/CategoryField.php | 565 +- .../src/Helper/CategoriesHelper.php | 160 +- .../src/Helper/CategoryAssociationHelper.php | 88 +- .../src/Model/CategoriesModel.php | 879 +- .../src/Model/CategoryModel.php | 2602 ++- .../src/Service/HTML/AdministratorService.php | 146 +- .../src/Table/CategoryTable.php | 29 +- .../src/View/Categories/HtmlView.php | 589 +- .../src/View/Category/HtmlView.php | 530 +- .../tmpl/categories/default.php | 525 +- .../tmpl/categories/default_batch_body.php | 86 +- .../tmpl/categories/default_batch_footer.php | 6 +- .../tmpl/categories/emptystate.php | 31 +- .../com_categories/tmpl/categories/modal.php | 206 +- .../com_categories/tmpl/category/edit.php | 162 +- .../com_categories/tmpl/category/modal.php | 5 +- .../com_checkin/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 192 +- .../com_checkin/src/Model/CheckinModel.php | 450 +- .../com_checkin/src/View/Checkin/HtmlView.php | 209 +- .../com_checkin/tmpl/checkin/default.php | 103 +- .../com_checkin/tmpl/checkin/emptystate.php | 9 +- .../com_config/services/provider.php | 50 +- .../src/Controller/ApplicationController.php | 556 +- .../src/Controller/ComponentController.php | 376 +- .../src/Controller/DisplayController.php | 74 +- .../src/Controller/RequestController.php | 151 +- .../src/Extension/ConfigComponent.php | 3 +- .../src/Field/ConfigComponentsField.php | 96 +- .../com_config/src/Field/FiltersField.php | 295 +- .../com_config/src/Helper/ConfigHelper.php | 213 +- .../com_config/src/Model/ApplicationModel.php | 2563 ++- .../com_config/src/Model/ComponentModel.php | 425 +- .../src/View/Application/HtmlView.php | 187 +- .../src/View/Component/HtmlView.php | 233 +- .../com_config/tmpl/application/default.php | 95 +- .../tmpl/application/default_cache.php | 1 + .../tmpl/application/default_cookie.php | 1 + .../tmpl/application/default_database.php | 1 + .../tmpl/application/default_debug.php | 1 + .../tmpl/application/default_filters.php | 1 + .../tmpl/application/default_locale.php | 1 + .../tmpl/application/default_logging.php | 1 + .../application/default_logging_custom.php | 1 + .../tmpl/application/default_mail.php | 11 +- .../tmpl/application/default_metadata.php | 1 + .../tmpl/application/default_navigation.php | 27 +- .../tmpl/application/default_permissions.php | 1 + .../tmpl/application/default_proxy.php | 1 + .../tmpl/application/default_seo.php | 1 + .../tmpl/application/default_server.php | 1 + .../tmpl/application/default_session.php | 1 + .../tmpl/application/default_site.php | 1 + .../tmpl/application/default_webservices.php | 1 + .../com_config/tmpl/component/default.php | 215 +- .../tmpl/component/default_navigation.php | 36 +- .../com_contact/helpers/contact.php | 1 + .../com_contact/services/provider.php | 60 +- .../src/Controller/AjaxController.php | 97 +- .../src/Controller/ContactController.php | 249 +- .../src/Controller/ContactsController.php | 197 +- .../src/Controller/DisplayController.php | 69 +- .../src/Extension/ContactComponent.php | 202 +- .../src/Field/Modal/ContactField.php | 558 +- .../src/Helper/AssociationsHelper.php | 367 +- .../com_contact/src/Helper/ContactHelper.php | 1 + .../com_contact/src/Model/ContactModel.php | 996 +- .../com_contact/src/Model/ContactsModel.php | 683 +- .../src/Service/HTML/AdministratorService.php | 234 +- .../com_contact/src/Service/HTML/Icon.php | 284 +- .../com_contact/src/Table/ContactTable.php | 479 +- .../com_contact/src/View/Contact/HtmlView.php | 321 +- .../src/View/Contacts/HtmlView.php | 380 +- .../com_contact/tmpl/contact/edit.php | 186 +- .../com_contact/tmpl/contact/modal.php | 5 +- .../com_contact/tmpl/contacts/default.php | 341 +- .../tmpl/contacts/default_batch_body.php | 68 +- .../tmpl/contacts/default_batch_footer.php | 6 +- .../com_contact/tmpl/contacts/emptystate.php | 14 +- .../com_contact/tmpl/contacts/modal.php | 226 +- .../com_content/helpers/content.php | 1 + .../com_content/services/provider.php | 60 +- .../src/Controller/AjaxController.php | 97 +- .../src/Controller/ArticleController.php | 303 +- .../src/Controller/ArticlesController.php | 264 +- .../src/Controller/DisplayController.php | 69 +- .../src/Controller/FeaturedController.php | 138 +- .../src/Event/Model/FeatureEvent.php | 112 +- .../src/Extension/ContentComponent.php | 612 +- .../com_content/src/Field/AssocField.php | 58 +- .../src/Field/Modal/ArticleField.php | 550 +- .../com_content/src/Field/VotelistField.php | 58 +- .../com_content/src/Field/VoteradioField.php | 58 +- .../src/Helper/AssociationsHelper.php | 362 +- .../com_content/src/Helper/ContentHelper.php | 313 +- .../com_content/src/Model/ArticleModel.php | 2207 +- .../com_content/src/Model/ArticlesModel.php | 1313 +- .../com_content/src/Model/FeatureModel.php | 57 +- .../com_content/src/Model/FeaturedModel.php | 148 +- .../src/Service/HTML/AdministratorService.php | 142 +- .../com_content/src/Service/HTML/Icon.php | 251 +- .../com_content/src/Table/ArticleTable.php | 1 + .../com_content/src/Table/FeaturedTable.php | 23 +- .../com_content/src/View/Article/HtmlView.php | 409 +- .../src/View/Articles/HtmlView.php | 440 +- .../src/View/Featured/HtmlView.php | 369 +- .../com_content/tmpl/article/edit.php | 267 +- .../com_content/tmpl/article/modal.php | 5 +- .../com_content/tmpl/article/pagebreak.php | 47 +- .../com_content/tmpl/articles/default.php | 683 +- .../tmpl/articles/default_batch_body.php | 72 +- .../tmpl/articles/default_batch_footer.php | 6 +- .../com_content/tmpl/articles/emptystate.php | 14 +- .../com_content/tmpl/articles/modal.php | 254 +- .../com_content/tmpl/featured/default.php | 675 +- .../tmpl/featured/default_stage_body.php | 16 +- .../tmpl/featured/default_stage_footer.php | 6 +- .../com_content/tmpl/featured/emptystate.php | 12 +- .../helpers/contenthistory.php | 1 + .../com_contenthistory/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 2 +- .../src/Controller/HistoryController.php | 126 +- .../src/Controller/PreviewController.php | 31 +- .../src/Dispatcher/Dispatcher.php | 34 +- .../src/Helper/ContenthistoryHelper.php | 692 +- .../src/Model/CompareModel.php | 310 +- .../src/Model/HistoryModel.php | 756 +- .../src/Model/PreviewModel.php | 237 +- .../src/View/Compare/HtmlView.php | 72 +- .../src/View/History/HtmlView.php | 220 +- .../src/View/Preview/HtmlView.php | 73 +- .../tmpl/compare/compare.php | 101 +- .../com_contenthistory/tmpl/history/modal.php | 159 +- .../tmpl/preview/preview.php | 87 +- .../com_cpanel/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 105 +- .../com_cpanel/src/Dispatcher/Dispatcher.php | 25 +- .../com_cpanel/src/View/Cpanel/HtmlView.php | 282 +- .../com_cpanel/tmpl/cpanel/default.php | 81 +- .../components/com_fields/helpers/fields.php | 2 + .../com_fields/services/provider.php | 50 +- .../src/Controller/DisplayController.php | 77 +- .../src/Controller/FieldController.php | 334 +- .../src/Controller/FieldsController.php | 47 +- .../src/Controller/GroupController.php | 347 +- .../src/Controller/GroupsController.php | 47 +- .../com_fields/src/Dispatcher/Dispatcher.php | 42 +- .../src/Extension/FieldsComponent.php | 29 +- .../src/Field/ComponentsFieldgroupField.php | 182 +- .../src/Field/ComponentsFieldsField.php | 182 +- .../com_fields/src/Field/FieldLayoutField.php | 279 +- .../src/Field/FieldcontextsField.php | 70 +- .../com_fields/src/Field/FieldgroupsField.php | 94 +- .../com_fields/src/Field/SectionField.php | 81 +- .../com_fields/src/Field/SubfieldsField.php | 201 +- .../com_fields/src/Field/TypeField.php | 103 +- .../com_fields/src/Helper/FieldsHelper.php | 1363 +- .../com_fields/src/Model/FieldModel.php | 2379 +-- .../com_fields/src/Model/FieldsModel.php | 902 +- .../com_fields/src/Model/GroupModel.php | 707 +- .../com_fields/src/Model/GroupsModel.php | 438 +- .../src/Plugin/FieldsListPlugin.php | 98 +- .../com_fields/src/Plugin/FieldsPlugin.php | 582 +- .../com_fields/src/Table/FieldTable.php | 596 +- .../com_fields/src/Table/GroupTable.php | 379 +- .../com_fields/src/View/Field/HtmlView.php | 277 +- .../com_fields/src/View/Fields/HtmlView.php | 340 +- .../com_fields/src/View/Group/HtmlView.php | 327 +- .../com_fields/src/View/Groups/HtmlView.php | 344 +- .../components/com_fields/tmpl/field/edit.php | 137 +- .../com_fields/tmpl/fields/default.php | 339 +- .../tmpl/fields/default_batch_body.php | 80 +- .../tmpl/fields/default_batch_footer.php | 6 +- .../com_fields/tmpl/fields/modal.php | 175 +- .../components/com_fields/tmpl/group/edit.php | 109 +- .../com_fields/tmpl/groups/default.php | 279 +- .../tmpl/groups/default_batch_body.php | 30 +- .../tmpl/groups/default_batch_footer.php | 6 +- .../com_finder/helpers/indexer/adapter.php | 1 + .../com_finder/helpers/indexer/helper.php | 1 + .../com_finder/helpers/indexer/parser.php | 1 + .../com_finder/helpers/indexer/query.php | 1 + .../com_finder/helpers/indexer/result.php | 1 + .../com_finder/helpers/indexer/taxonomy.php | 1 + .../com_finder/helpers/indexer/token.php | 1 + .../com_finder/helpers/language.php | 1 + .../com_finder/services/provider.php | 52 +- .../src/Controller/DisplayController.php | 97 +- .../src/Controller/FilterController.php | 418 +- .../src/Controller/FiltersController.php | 45 +- .../src/Controller/IndexController.php | 150 +- .../src/Controller/IndexerController.php | 539 +- .../src/Controller/MapsController.php | 45 +- .../src/Controller/SearchesController.php | 38 +- .../src/Extension/FinderComponent.php | 61 +- .../com_finder/src/Field/BranchesField.php | 39 +- .../com_finder/src/Field/ContentmapField.php | 207 +- .../src/Field/ContenttypesField.php | 96 +- .../src/Field/SearchfilterField.php | 63 +- .../com_finder/src/Helper/FinderHelper.php | 39 +- .../com_finder/src/Helper/LanguageHelper.php | 254 +- .../com_finder/src/Indexer/Adapter.php | 1796 +- .../com_finder/src/Indexer/Helper.php | 926 +- .../com_finder/src/Indexer/Indexer.php | 2002 +- .../com_finder/src/Indexer/Language.php | 306 +- .../com_finder/src/Indexer/Language/El.php | 1901 +- .../com_finder/src/Indexer/Language/Zh.php | 110 +- .../com_finder/src/Indexer/Parser.php | 202 +- .../com_finder/src/Indexer/Parser/Html.php | 269 +- .../com_finder/src/Indexer/Parser/Rtf.php | 39 +- .../com_finder/src/Indexer/Parser/Txt.php | 27 +- .../com_finder/src/Indexer/Query.php | 2729 ++- .../com_finder/src/Indexer/Result.php | 1095 +- .../com_finder/src/Indexer/Taxonomy.php | 976 +- .../com_finder/src/Indexer/Token.php | 309 +- .../com_finder/src/Model/FilterModel.php | 255 +- .../com_finder/src/Model/FiltersModel.php | 230 +- .../com_finder/src/Model/IndexModel.php | 897 +- .../com_finder/src/Model/IndexerModel.php | 1 + .../com_finder/src/Model/MapsModel.php | 764 +- .../com_finder/src/Model/SearchesModel.php | 300 +- .../com_finder/src/Model/StatisticsModel.php | 102 +- .../com_finder/src/Response/Response.php | 113 +- .../com_finder/src/Service/HTML/Filter.php | 929 +- .../com_finder/src/Service/HTML/Finder.php | 210 +- .../com_finder/src/Service/HTML/Query.php | 275 +- .../com_finder/src/Table/FilterTable.php | 290 +- .../com_finder/src/Table/LinkTable.php | 66 +- .../com_finder/src/Table/MapTable.php | 86 +- .../com_finder/src/View/Filter/HtmlView.php | 295 +- .../com_finder/src/View/Filters/HtmlView.php | 300 +- .../com_finder/src/View/Index/HtmlView.php | 408 +- .../com_finder/src/View/Indexer/HtmlView.php | 2 +- .../com_finder/src/View/Maps/HtmlView.php | 297 +- .../com_finder/src/View/Searches/HtmlView.php | 267 +- .../src/View/Statistics/HtmlView.php | 62 +- .../com_finder/tmpl/filter/edit.php | 117 +- .../com_finder/tmpl/filters/default.php | 197 +- .../com_finder/tmpl/filters/emptystate.php | 20 +- .../com_finder/tmpl/index/default.php | 265 +- .../com_finder/tmpl/index/emptystate.php | 63 +- .../com_finder/tmpl/indexer/default.php | 25 +- .../com_finder/tmpl/maps/default.php | 278 +- .../com_finder/tmpl/maps/emptystate.php | 11 +- .../com_finder/tmpl/searches/default.php | 109 +- .../com_finder/tmpl/searches/emptystate.php | 13 +- .../com_finder/tmpl/statistics/default.php | 75 +- .../com_installer/helpers/installer.php | 2 +- .../com_installer/services/provider.php | 48 +- .../src/Controller/DatabaseController.php | 147 +- .../src/Controller/DiscoverController.php | 129 +- .../src/Controller/DisplayController.php | 143 +- .../src/Controller/InstallController.php | 174 +- .../src/Controller/ManageController.php | 301 +- .../src/Controller/UpdateController.php | 359 +- .../src/Controller/UpdatesiteController.php | 1 + .../src/Controller/UpdatesitesController.php | 268 +- .../src/Extension/InstallerComponent.php | 41 +- .../src/Field/ExtensionstatusField.php | 39 +- .../com_installer/src/Field/FolderField.php | 39 +- .../com_installer/src/Field/LocationField.php | 39 +- .../com_installer/src/Field/PackageField.php | 23 +- .../com_installer/src/Field/TypeField.php | 39 +- .../src/Helper/InstallerHelper.php | 937 +- .../com_installer/src/Model/DatabaseModel.php | 1315 +- .../com_installer/src/Model/DiscoverModel.php | 582 +- .../com_installer/src/Model/InstallModel.php | 774 +- .../src/Model/InstallerModel.php | 401 +- .../src/Model/LanguagesModel.php | 494 +- .../com_installer/src/Model/ManageModel.php | 881 +- .../com_installer/src/Model/UpdateModel.php | 1192 +- .../src/Model/UpdatesiteModel.php | 287 +- .../src/Model/UpdatesitesModel.php | 1285 +- .../com_installer/src/Model/WarningsModel.php | 328 +- .../com_installer/src/Service/HTML/Manage.php | 95 +- .../src/Service/HTML/Updatesites.php | 75 +- .../src/Table/UpdatesiteTable.php | 41 +- .../src/View/Database/HtmlView.php | 199 +- .../src/View/Discover/HtmlView.php | 117 +- .../src/View/Install/HtmlView.php | 75 +- .../src/View/Installer/HtmlView.php | 143 +- .../src/View/Languages/HtmlView.php | 96 +- .../src/View/Manage/HtmlView.php | 187 +- .../src/View/Update/HtmlView.php | 210 +- .../src/View/Updatesite/HtmlView.php | 186 +- .../src/View/Updatesites/HtmlView.php | 211 +- .../src/View/Warnings/HtmlView.php | 68 +- .../com_installer/tmpl/database/default.php | 209 +- .../com_installer/tmpl/discover/default.php | 201 +- .../tmpl/discover/emptystate.php | 17 +- .../com_installer/tmpl/install/default.php | 71 +- .../tmpl/installer/default_message.php | 13 +- .../com_installer/tmpl/languages/default.php | 195 +- .../com_installer/tmpl/manage/default.php | 307 +- .../com_installer/tmpl/update/default.php | 267 +- .../com_installer/tmpl/update/emptystate.php | 14 +- .../com_installer/tmpl/updatesite/edit.php | 7 +- .../tmpl/updatesites/default.php | 266 +- .../com_installer/tmpl/warnings/default.php | 77 +- .../tmpl/warnings/emptystate.php | 9 +- .../components/com_joomlaupdate/extract.php | 3786 ++-- .../com_joomlaupdate/finalisation.php | 449 +- .../com_joomlaupdate/restore_finalisation.php | 1 + .../com_joomlaupdate/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 163 +- .../src/Controller/UpdateController.php | 1365 +- .../src/Dispatcher/Dispatcher.php | 26 +- .../src/Model/UpdateModel.php | 3448 ++- .../src/View/Joomlaupdate/HtmlView.php | 585 +- .../src/View/Update/HtmlView.php | 31 +- .../src/View/Upload/HtmlView.php | 170 +- .../tmpl/joomlaupdate/complete.php | 19 +- .../tmpl/joomlaupdate/noupdate.php | 37 +- .../tmpl/joomlaupdate/preupdatecheck.php | 599 +- .../tmpl/joomlaupdate/reinstall.php | 38 +- .../tmpl/joomlaupdate/selfupdate.php | 12 +- .../tmpl/joomlaupdate/update.php | 40 +- .../com_joomlaupdate/tmpl/update/default.php | 159 +- .../tmpl/update/finaliseconfirm.php | 101 +- .../com_joomlaupdate/tmpl/upload/captive.php | 101 +- .../com_joomlaupdate/tmpl/upload/default.php | 117 +- .../com_languages/services/provider.php | 48 +- .../src/Controller/DisplayController.php | 75 +- .../src/Controller/InstalledController.php | 170 +- .../src/Controller/LanguageController.php | 29 +- .../src/Controller/LanguagesController.php | 31 +- .../src/Controller/OverrideController.php | 349 +- .../src/Controller/OverridesController.php | 111 +- .../src/Controller/StringsController.php | 45 +- .../src/Extension/LanguagesComponent.php | 37 +- .../src/Field/LanguageclientField.php | 92 +- .../src/Helper/LanguagesHelper.php | 67 +- .../src/Helper/MultilangstatusHelper.php | 653 +- .../src/Model/InstalledModel.php | 735 +- .../com_languages/src/Model/LanguageModel.php | 505 +- .../src/Model/LanguagesModel.php | 417 +- .../com_languages/src/Model/OverrideModel.php | 391 +- .../src/Model/OverridesModel.php | 473 +- .../com_languages/src/Model/StringsModel.php | 323 +- .../src/Service/HTML/Languages.php | 126 +- .../src/View/Installed/HtmlView.php | 238 +- .../src/View/Language/HtmlView.php | 202 +- .../src/View/Languages/HtmlView.php | 251 +- .../src/View/Multilangstatus/HtmlView.php | 43 +- .../src/View/Override/HtmlView.php | 237 +- .../src/View/Overrides/HtmlView.php | 190 +- .../com_languages/tmpl/installed/default.php | 223 +- .../com_languages/tmpl/language/edit.php | 109 +- .../com_languages/tmpl/languages/default.php | 274 +- .../tmpl/multilangstatus/default.php | 621 +- .../com_languages/tmpl/override/edit.php | 125 +- .../com_languages/tmpl/overrides/default.php | 166 +- .../com_login/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 210 +- .../com_login/src/Dispatcher/Dispatcher.php | 62 +- .../com_login/src/Model/LoginModel.php | 355 +- .../com_login/src/View/Login/HtmlView.php | 2 +- .../com_login/tmpl/login/default.php | 8 +- .../com_mails/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 72 +- .../src/Controller/TemplateController.php | 538 +- .../com_mails/src/Helper/MailsHelper.php | 173 +- .../com_mails/src/Model/TemplateModel.php | 773 +- .../com_mails/src/Model/TemplatesModel.php | 391 +- .../com_mails/src/Table/TemplateTable.php | 37 +- .../com_mails/src/View/Template/HtmlView.php | 254 +- .../com_mails/src/View/Templates/HtmlView.php | 232 +- .../com_mails/tmpl/template/edit.php | 165 +- .../com_mails/tmpl/templates/default.php | 181 +- .../components/com_media/helpers/media.php | 51 +- .../layouts/toolbar/create-folder.php | 11 +- .../com_media/layouts/toolbar/delete.php | 11 +- .../com_media/layouts/toolbar/upload.php | 11 +- .../com_media/services/provider.php | 46 +- .../src/Adapter/AdapterInterface.php | 361 +- .../src/Controller/ApiController.php | 748 +- .../src/Controller/DisplayController.php | 83 +- .../src/Controller/PluginController.php | 258 +- .../com_media/src/Dispatcher/Dispatcher.php | 46 +- .../AbstractMediaItemValidationEvent.php | 198 +- .../src/Event/FetchMediaItemEvent.php | 114 +- .../src/Event/FetchMediaItemUrlEvent.php | 142 +- .../src/Event/FetchMediaItemsEvent.php | 112 +- .../src/Event/MediaProviderEvent.php | 63 +- .../src/Event/OAuthCallbackEvent.php | 129 +- .../src/Exception/FileExistsException.php | 1 + .../src/Exception/FileNotFoundException.php | 1 + .../src/Exception/InvalidPathException.php | 1 + .../com_media/src/Model/ApiModel.php | 1039 +- .../com_media/src/Model/FileModel.php | 80 +- .../com_media/src/Model/MediaModel.php | 59 +- .../src/Plugin/MediaActionPlugin.php | 133 +- .../src/Provider/ProviderInterface.php | 49 +- .../src/Provider/ProviderManager.php | 218 +- .../Provider/ProviderManagerHelperTrait.php | 275 +- .../com_media/src/View/File/HtmlView.php | 81 +- .../com_media/tmpl/file/default.php | 47 +- .../components/com_menus/helpers/menus.php | 1 + .../layouts/joomla/menu/edit_modules.php | 61 +- .../layouts/joomla/searchtools/default.php | 156 +- .../com_menus/services/provider.php | 60 +- .../src/Controller/AjaxController.php | 99 +- .../src/Controller/DisplayController.php | 81 +- .../src/Controller/ItemController.php | 1144 +- .../src/Controller/ItemsController.php | 460 +- .../src/Controller/MenuController.php | 403 +- .../src/Controller/MenusController.php | 367 +- .../src/Extension/MenusComponent.php | 42 +- .../src/Field/MenuItemByTypeField.php | 495 +- .../com_menus/src/Field/MenuOrderingField.php | 201 +- .../com_menus/src/Field/MenuParentField.php | 187 +- .../com_menus/src/Field/MenuPresetField.php | 56 +- .../com_menus/src/Field/MenutypeField.php | 183 +- .../com_menus/src/Field/Modal/MenuField.php | 826 +- .../src/Helper/AssociationsHelper.php | 314 +- .../com_menus/src/Helper/MenusHelper.php | 1831 +- .../com_menus/src/Model/ItemModel.php | 3647 ++-- .../com_menus/src/Model/ItemsModel.php | 1178 +- .../com_menus/src/Model/MenuModel.php | 787 +- .../com_menus/src/Model/MenusModel.php | 593 +- .../com_menus/src/Model/MenutypesModel.php | 1174 +- .../com_menus/src/Service/HTML/Menus.php | 207 +- .../com_menus/src/Table/MenuTable.php | 123 +- .../com_menus/src/Table/MenuTypeTable.php | 2 +- .../com_menus/src/View/Item/HtmlView.php | 346 +- .../com_menus/src/View/Items/HtmlView.php | 735 +- .../com_menus/src/View/Menu/HtmlView.php | 223 +- .../com_menus/src/View/Menu/XmlView.php | 263 +- .../com_menus/src/View/Menus/HtmlView.php | 227 +- .../com_menus/src/View/Menutypes/HtmlView.php | 273 +- .../components/com_menus/tmpl/item/edit.php | 313 +- .../com_menus/tmpl/item/edit_container.php | 150 +- .../com_menus/tmpl/item/edit_modules.php | 226 +- .../components/com_menus/tmpl/item/modal.php | 5 +- .../com_menus/tmpl/items/default.php | 477 +- .../tmpl/items/default_batch_body.php | 119 +- .../tmpl/items/default_batch_footer.php | 12 +- .../components/com_menus/tmpl/items/modal.php | 299 +- .../components/com_menus/tmpl/menu/edit.php | 69 +- .../com_menus/tmpl/menus/default.php | 464 +- .../com_menus/tmpl/menutypes/default.php | 41 +- .../com_messages/services/provider.php | 48 +- .../src/Controller/ConfigController.php | 150 +- .../src/Controller/DisplayController.php | 69 +- .../src/Controller/MessageController.php | 72 +- .../src/Controller/MessagesController.php | 31 +- .../src/Extension/MessagesComponent.php | 37 +- .../src/Field/MessageStatesField.php | 39 +- .../src/Field/UserMessagesField.php | 114 +- .../src/Helper/MessagesHelper.php | 33 +- .../com_messages/src/Model/ConfigModel.php | 333 +- .../com_messages/src/Model/MessageModel.php | 977 +- .../com_messages/src/Model/MessagesModel.php | 395 +- .../src/Service/HTML/Messages.php | 66 +- .../com_messages/src/Table/MessageTable.php | 132 +- .../com_messages/src/View/Config/HtmlView.php | 134 +- .../src/View/Message/HtmlView.php | 149 +- .../src/View/Messages/HtmlView.php | 286 +- .../com_messages/tmpl/config/default.php | 31 +- .../com_messages/tmpl/message/default.php | 83 +- .../com_messages/tmpl/message/edit.php | 43 +- .../com_messages/tmpl/messages/default.php | 135 +- .../com_messages/tmpl/messages/emptystate.php | 18 +- .../com_modules/helpers/modules.php | 2 +- .../joomla/form/field/modulespositionedit.php | 41 +- .../layouts/toolbar/cancelselect.php | 7 +- .../com_modules/services/provider.php | 48 +- .../src/Controller/DisplayController.php | 120 +- .../src/Controller/ModuleController.php | 668 +- .../src/Controller/ModulesController.php | 179 +- .../src/Extension/ModulesComponent.php | 37 +- .../src/Field/ModulesModuleField.php | 176 +- .../src/Field/ModulesPositionField.php | 176 +- .../src/Field/ModulesPositioneditField.php | 230 +- .../com_modules/src/Helper/ModulesHelper.php | 582 +- .../com_modules/src/Model/ModuleModel.php | 2225 +- .../com_modules/src/Model/ModulesModel.php | 861 +- .../com_modules/src/Model/PositionsModel.php | 426 +- .../com_modules/src/Model/SelectModel.php | 268 +- .../com_modules/src/Service/HTML/Modules.php | 464 +- .../com_modules/src/View/Module/HtmlView.php | 274 +- .../com_modules/src/View/Modules/HtmlView.php | 448 +- .../com_modules/src/View/Select/HtmlView.php | 143 +- .../com_modules/tmpl/module/edit.php | 297 +- .../tmpl/module/edit_assignment.php | 251 +- .../com_modules/tmpl/module/modal.php | 5 +- .../com_modules/tmpl/modules/default.php | 363 +- .../tmpl/modules/default_batch_body.php | 89 +- .../tmpl/modules/default_batch_footer.php | 6 +- .../com_modules/tmpl/modules/emptystate.php | 18 +- .../com_modules/tmpl/modules/modal.php | 195 +- .../com_modules/tmpl/select/default.php | 91 +- .../com_modules/tmpl/select/modal.php | 5 +- .../com_newsfeeds/helpers/newsfeeds.php | 1 + .../com_newsfeeds/services/provider.php | 60 +- .../src/Controller/AjaxController.php | 97 +- .../src/Controller/DisplayController.php | 69 +- .../src/Controller/NewsfeedController.php | 179 +- .../src/Controller/NewsfeedsController.php | 31 +- .../src/Extension/NewsfeedsComponent.php | 108 +- .../src/Field/Modal/NewsfeedField.php | 560 +- .../src/Field/NewsfeedsField.php | 80 +- .../src/Helper/AssociationsHelper.php | 365 +- .../src/Helper/NewsfeedsHelper.php | 299 +- .../com_newsfeeds/src/Model/NewsfeedModel.php | 820 +- .../src/Model/NewsfeedsModel.php | 602 +- .../src/Service/HTML/AdministratorService.php | 153 +- .../com_newsfeeds/src/Table/NewsfeedTable.php | 347 +- .../src/View/Newsfeed/HtmlView.php | 261 +- .../src/View/Newsfeeds/HtmlView.php | 333 +- .../com_newsfeeds/tmpl/newsfeed/edit.php | 159 +- .../tmpl/newsfeed/edit_display.php | 9 +- .../com_newsfeeds/tmpl/newsfeed/modal.php | 5 +- .../com_newsfeeds/tmpl/newsfeeds/default.php | 329 +- .../tmpl/newsfeeds/default_batch_body.php | 58 +- .../tmpl/newsfeeds/default_batch_footer.php | 5 +- .../tmpl/newsfeeds/emptystate.php | 14 +- .../com_newsfeeds/tmpl/newsfeeds/modal.php | 196 +- .../com_plugins/helpers/plugins.php | 2 +- .../com_plugins/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 69 +- .../src/Controller/PluginController.php | 2 + .../src/Controller/PluginsController.php | 67 +- .../src/Field/PluginElementField.php | 39 +- .../com_plugins/src/Field/PluginTypeField.php | 39 +- .../src/Field/PluginorderingField.php | 87 +- .../com_plugins/src/Helper/PluginsHelper.php | 200 +- .../com_plugins/src/Model/PluginModel.php | 697 +- .../com_plugins/src/Model/PluginsModel.php | 538 +- .../com_plugins/src/View/Plugin/HtmlView.php | 174 +- .../com_plugins/src/View/Plugins/HtmlView.php | 184 +- .../com_plugins/tmpl/plugin/edit.php | 211 +- .../com_plugins/tmpl/plugin/modal.php | 5 +- .../com_plugins/tmpl/plugins/default.php | 237 +- .../com_postinstall/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 50 +- .../src/Controller/MessageController.php | 326 +- .../src/Helper/PostinstallHelper.php | 40 +- .../src/Model/MessagesModel.php | 1453 +- .../src/View/Messages/HtmlView.php | 121 +- .../com_postinstall/tmpl/messages/default.php | 95 +- .../tmpl/messages/emptystate.php | 19 +- .../com_privacy/services/provider.php | 52 +- .../src/Controller/ConsentsController.php | 143 +- .../src/Controller/DisplayController.php | 190 +- .../src/Controller/RequestController.php | 758 +- .../src/Controller/RequestsController.php | 31 +- .../com_privacy/src/Dispatcher/Dispatcher.php | 26 +- .../com_privacy/src/Export/Domain.php | 91 +- .../com_privacy/src/Export/Field.php | 29 +- .../com_privacy/src/Export/Item.php | 77 +- .../src/Extension/PrivacyComponent.php | 39 +- .../src/Field/RequeststatusField.php | 39 +- .../src/Field/RequesttypeField.php | 35 +- .../com_privacy/src/Helper/PrivacyHelper.php | 103 +- .../src/Model/CapabilitiesModel.php | 144 +- .../com_privacy/src/Model/ConsentsModel.php | 426 +- .../com_privacy/src/Model/ExportModel.php | 585 +- .../com_privacy/src/Model/RemoveModel.php | 368 +- .../com_privacy/src/Model/RequestModel.php | 822 +- .../com_privacy/src/Model/RequestsModel.php | 343 +- .../com_privacy/src/Plugin/PrivacyPlugin.php | 279 +- .../com_privacy/src/Removal/Status.php | 29 +- .../com_privacy/src/Service/HTML/Privacy.php | 54 +- .../com_privacy/src/Table/ConsentTable.php | 72 +- .../com_privacy/src/Table/RequestTable.php | 93 +- .../src/View/Capabilities/HtmlView.php | 102 +- .../src/View/Consents/HtmlView.php | 257 +- .../com_privacy/src/View/Export/XmlView.php | 62 +- .../com_privacy/src/View/Request/HtmlView.php | 307 +- .../src/View/Requests/HtmlView.php | 232 +- .../com_privacy/tmpl/capabilities/default.php | 57 +- .../com_privacy/tmpl/consents/default.php | 185 +- .../com_privacy/tmpl/consents/emptystate.php | 9 +- .../com_privacy/tmpl/request/default.php | 129 +- .../com_privacy/tmpl/request/edit.php | 31 +- .../com_privacy/tmpl/requests/default.php | 181 +- .../com_privacy/tmpl/requests/emptystate.php | 14 +- .../com_redirect/helpers/redirect.php | 1 + .../com_redirect/layouts/toolbar/batch.php | 5 +- .../com_redirect/services/provider.php | 48 +- .../src/Controller/DisplayController.php | 120 +- .../src/Controller/LinkController.php | 4 +- .../src/Controller/LinksController.php | 339 +- .../src/Extension/RedirectComponent.php | 37 +- .../com_redirect/src/Field/RedirectField.php | 194 +- .../src/Helper/RedirectHelper.php | 131 +- .../com_redirect/src/Model/LinkModel.php | 411 +- .../com_redirect/src/Model/LinksModel.php | 479 +- .../src/Service/HTML/Redirect.php | 73 +- .../com_redirect/src/Table/LinkTable.php | 241 +- .../com_redirect/src/View/Link/HtmlView.php | 189 +- .../com_redirect/src/View/Links/HtmlView.php | 392 +- .../com_redirect/tmpl/link/edit.php | 45 +- .../com_redirect/tmpl/links/default.php | 279 +- .../tmpl/links/default_addform.php | 75 +- .../tmpl/links/default_batch_body.php | 18 +- .../tmpl/links/default_batch_footer.php | 6 +- .../com_redirect/tmpl/links/emptystate.php | 87 +- .../layouts/form/field/webcron_link.php | 29 +- .../com_scheduler/services/provider.php | 57 +- .../src/Controller/DisplayController.php | 140 +- .../src/Controller/TaskController.php | 177 +- .../src/Controller/TasksController.php | 139 +- .../src/Event/ExecuteTaskEvent.php | 128 +- .../src/Extension/SchedulerComponent.php | 37 +- .../com_scheduler/src/Field/CronField.php | 334 +- .../src/Field/ExecutionRuleField.php | 43 +- .../com_scheduler/src/Field/IntervalField.php | 130 +- .../src/Field/TaskStateField.php | 39 +- .../com_scheduler/src/Field/TaskTypeField.php | 74 +- .../src/Field/WebcronLinkField.php | 47 +- .../src/Helper/ExecRuleHelper.php | 190 +- .../src/Helper/SchedulerHelper.php | 70 +- .../com_scheduler/src/Model/SelectModel.php | 69 +- .../com_scheduler/src/Model/TaskModel.php | 1565 +- .../com_scheduler/src/Model/TasksModel.php | 840 +- .../src/Rule/ExecutionRulesRule.php | 111 +- .../com_scheduler/src/Scheduler/Scheduler.php | 607 +- .../com_scheduler/src/Table/TaskTable.php | 541 +- .../com_scheduler/src/Task/Status.php | 161 +- .../com_scheduler/src/Task/Task.php | 1061 +- .../com_scheduler/src/Task/TaskOption.php | 154 +- .../com_scheduler/src/Task/TaskOptions.php | 91 +- .../src/Traits/TaskPluginTrait.php | 617 +- .../src/View/Select/HtmlView.php | 206 +- .../com_scheduler/src/View/Task/HtmlView.php | 236 +- .../com_scheduler/src/View/Tasks/HtmlView.php | 323 +- .../com_scheduler/tmpl/select/default.php | 105 +- .../com_scheduler/tmpl/select/modal.php | 20 +- .../com_scheduler/tmpl/task/edit.php | 310 +- .../com_scheduler/tmpl/tasks/default.php | 472 +- .../com_scheduler/tmpl/tasks/empty_state.php | 14 +- .../components/com_tags/services/provider.php | 50 +- .../src/Controller/DisplayController.php | 71 +- .../com_tags/src/Controller/TagController.php | 96 +- .../src/Controller/TagsController.php | 140 +- .../com_tags/src/Extension/TagsComponent.php | 3 +- .../com_tags/src/Model/TagModel.php | 788 +- .../com_tags/src/Model/TagsModel.php | 615 +- .../com_tags/src/Table/TagTable.php | 435 +- .../com_tags/src/View/Tag/HtmlView.php | 267 +- .../com_tags/src/View/Tags/HtmlView.php | 330 +- .../components/com_tags/tmpl/tag/edit.php | 85 +- .../components/com_tags/tmpl/tags/default.php | 454 +- .../com_tags/tmpl/tags/default_batch_body.php | 30 +- .../tmpl/tags/default_batch_footer.php | 6 +- .../com_tags/tmpl/tags/emptystate.php | 14 +- .../com_templates/helpers/template.php | 1 + .../com_templates/helpers/templates.php | 1 + .../com_templates/services/provider.php | 48 +- .../src/Controller/DisplayController.php | 74 +- .../src/Controller/StyleController.php | 249 +- .../src/Controller/StylesController.php | 247 +- .../src/Controller/TemplateController.php | 1997 +- .../src/Extension/TemplatesComponent.php | 37 +- .../src/Field/TemplatelocationField.php | 39 +- .../src/Field/TemplatenameField.php | 53 +- .../src/Helper/TemplateHelper.php | 299 +- .../src/Helper/TemplatesHelper.php | 253 +- .../com_templates/src/Model/StyleModel.php | 1543 +- .../com_templates/src/Model/StylesModel.php | 424 +- .../com_templates/src/Model/TemplateModel.php | 4300 ++-- .../src/Model/TemplatesModel.php | 391 +- .../src/Service/HTML/Templates.php | 302 +- .../com_templates/src/Table/StyleTable.php | 264 +- .../com_templates/src/View/Style/HtmlView.php | 241 +- .../com_templates/src/View/Style/JsonView.php | 86 +- .../src/View/Styles/HtmlView.php | 256 +- .../src/View/Template/HtmlView.php | 746 +- .../src/View/Templates/HtmlView.php | 254 +- .../com_templates/tmpl/style/edit.php | 175 +- .../tmpl/style/edit_assignment.php | 57 +- .../com_templates/tmpl/styles/default.php | 243 +- .../com_templates/tmpl/template/default.php | 832 +- .../tmpl/template/default_description.php | 17 +- .../tmpl/template/default_folders.php | 25 +- .../tmpl/template/default_media_folders.php | 30 +- .../template/default_modal_child_body.php | 123 +- .../template/default_modal_child_footer.php | 1 + .../tmpl/template/default_modal_copy_body.php | 35 +- .../template/default_modal_copy_footer.php | 1 + .../template/default_modal_delete_body.php | 11 +- .../template/default_modal_delete_footer.php | 15 +- .../tmpl/template/default_modal_file_body.php | 166 +- .../template/default_modal_file_footer.php | 1 + .../template/default_modal_folder_body.php | 80 +- .../template/default_modal_folder_footer.php | 15 +- .../template/default_modal_rename_body.php | 35 +- .../template/default_modal_rename_footer.php | 1 + .../template/default_modal_resize_body.php | 49 +- .../template/default_modal_resize_footer.php | 1 + .../tmpl/template/default_tree.php | 92 +- .../tmpl/template/default_tree_media.php | 89 +- .../tmpl/template/default_updated_files.php | 135 +- .../com_templates/tmpl/template/readonly.php | 23 +- .../com_templates/tmpl/templates/default.php | 223 +- .../components/com_users/helpers/debug.php | 1 + .../components/com_users/helpers/users.php | 1 + .../com_users/postinstall/multifactorauth.php | 29 +- .../com_users/services/provider.php | 52 +- .../src/Controller/CallbackController.php | 86 +- .../src/Controller/CaptiveController.php | 378 +- .../src/Controller/DisplayController.php | 220 +- .../src/Controller/GroupController.php | 91 +- .../src/Controller/GroupsController.php | 229 +- .../src/Controller/LevelController.php | 193 +- .../src/Controller/LevelsController.php | 42 +- .../src/Controller/MailController.php | 83 +- .../src/Controller/MethodController.php | 918 +- .../src/Controller/MethodsController.php | 356 +- .../src/Controller/NoteController.php | 66 +- .../src/Controller/NotesController.php | 45 +- .../src/Controller/UserController.php | 265 +- .../src/Controller/UsersController.php | 296 +- .../src/DataShape/CaptiveRenderOptions.php | 276 +- .../src/DataShape/MethodDescriptor.php | 155 +- .../src/DataShape/SetupRenderOptions.php | 308 +- .../com_users/src/Dispatcher/Dispatcher.php | 89 +- .../src/Extension/UsersComponent.php | 122 +- .../com_users/src/Field/GroupparentField.php | 133 +- .../com_users/src/Field/LevelsField.php | 39 +- .../src/Field/ModulesPositionField.php | 1 + .../com_users/src/Helper/DebugHelper.php | 281 +- .../components/com_users/src/Helper/Mfa.php | 660 +- .../com_users/src/Helper/UsersHelper.php | 315 +- .../com_users/src/Model/BackupcodesModel.php | 532 +- .../com_users/src/Model/CaptiveModel.php | 789 +- .../com_users/src/Model/DebuggroupModel.php | 489 +- .../com_users/src/Model/DebuguserModel.php | 456 +- .../com_users/src/Model/GroupModel.php | 602 +- .../com_users/src/Model/GroupsModel.php | 450 +- .../com_users/src/Model/LevelModel.php | 548 +- .../com_users/src/Model/LevelsModel.php | 417 +- .../com_users/src/Model/MailModel.php | 440 +- .../com_users/src/Model/MethodModel.php | 469 +- .../com_users/src/Model/MethodsModel.php | 398 +- .../com_users/src/Model/NoteModel.php | 227 +- .../com_users/src/Model/NotesModel.php | 415 +- .../com_users/src/Model/UserModel.php | 2068 +- .../com_users/src/Model/UsersModel.php | 1184 +- .../com_users/src/Service/Encrypt.php | 219 +- .../com_users/src/Service/HTML/Users.php | 814 +- .../com_users/src/Table/MfaTable.php | 744 +- .../com_users/src/Table/NoteTable.php | 202 +- .../com_users/src/View/Captive/HtmlView.php | 376 +- .../src/View/Debuggroup/HtmlView.php | 216 +- .../com_users/src/View/Debuguser/HtmlView.php | 214 +- .../com_users/src/View/Group/HtmlView.php | 192 +- .../com_users/src/View/Groups/HtmlView.php | 193 +- .../com_users/src/View/Level/HtmlView.php | 192 +- .../com_users/src/View/Levels/HtmlView.php | 193 +- .../com_users/src/View/Mail/HtmlView.php | 92 +- .../com_users/src/View/Method/HtmlView.php | 383 +- .../com_users/src/View/Methods/HtmlView.php | 327 +- .../com_users/src/View/Note/HtmlView.php | 225 +- .../com_users/src/View/Notes/HtmlView.php | 308 +- .../com_users/src/View/SiteTemplateTrait.php | 69 +- .../com_users/src/View/User/HtmlView.php | 315 +- .../com_users/src/View/Users/HtmlView.php | 303 +- .../com_users/tmpl/debuggroup/default.php | 179 +- .../com_users/tmpl/debuguser/default.php | 245 +- .../components/com_users/tmpl/group/edit.php | 27 +- .../com_users/tmpl/groups/default.php | 220 +- .../components/com_users/tmpl/level/edit.php | 61 +- .../com_users/tmpl/levels/default.php | 214 +- .../com_users/tmpl/mail/default.php | 101 +- .../components/com_users/tmpl/note/edit.php | 45 +- .../com_users/tmpl/notes/default.php | 193 +- .../com_users/tmpl/notes/emptystate.php | 14 +- .../components/com_users/tmpl/notes/modal.php | 73 +- .../components/com_users/tmpl/user/edit.php | 95 +- .../com_users/tmpl/user/edit_groups.php | 3 +- .../tmpl/users/default_batch_body.php | 72 +- .../tmpl/users/default_batch_footer.php | 6 +- .../components/com_users/tmpl/users/modal.php | 169 +- .../com_workflow/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 184 +- .../src/Controller/StageController.php | 304 +- .../src/Controller/StagesController.php | 346 +- .../src/Controller/TransitionController.php | 306 +- .../src/Controller/TransitionsController.php | 219 +- .../src/Controller/WorkflowController.php | 425 +- .../src/Controller/WorkflowsController.php | 307 +- .../src/Dispatcher/Dispatcher.php | 30 +- .../src/Field/ComponentsWorkflowField.php | 182 +- .../src/Field/WorkflowcontextsField.php | 77 +- .../com_workflow/src/Helper/StageHelper.php | 1 + .../src/Helper/WorkflowHelper.php | 2 +- .../com_workflow/src/Model/StageModel.php | 708 +- .../com_workflow/src/Model/StagesModel.php | 353 +- .../src/Model/TransitionModel.php | 625 +- .../src/Model/TransitionsModel.php | 451 +- .../com_workflow/src/Model/WorkflowModel.php | 752 +- .../com_workflow/src/Model/WorkflowsModel.php | 502 +- .../com_workflow/src/Table/StageTable.php | 508 +- .../src/Table/TransitionTable.php | 234 +- .../com_workflow/src/Table/WorkflowTable.php | 602 +- .../com_workflow/src/View/Stage/HtmlView.php | 296 +- .../com_workflow/src/View/Stages/HtmlView.php | 382 +- .../src/View/Transition/HtmlView.php | 371 +- .../src/View/Transitions/HtmlView.php | 369 +- .../src/View/Workflow/HtmlView.php | 302 +- .../src/View/Workflows/HtmlView.php | 328 +- .../com_workflow/tmpl/stage/edit.php | 91 +- .../com_workflow/tmpl/stages/default.php | 240 +- .../com_workflow/tmpl/transition/edit.php | 62 +- .../com_workflow/tmpl/transitions/default.php | 262 +- .../com_workflow/tmpl/workflow/edit.php | 96 +- .../com_workflow/tmpl/workflows/default.php | 295 +- .../com_wrapper/services/provider.php | 50 +- .../src/Extension/WrapperComponent.php | 3 +- administrator/includes/app.php | 30 +- administrator/includes/defines.php | 21 +- administrator/includes/framework.php | 124 +- administrator/index.php | 18 +- administrator/language/en-GB/localise.php | 132 +- .../modules/mod_custom/mod_custom.php | 8 +- .../modules/mod_custom/tmpl/default.php | 3 +- administrator/modules/mod_feed/mod_feed.php | 1 + .../mod_feed/src/Helper/FeedHelper.php | 61 +- .../modules/mod_feed/tmpl/default.php | 194 +- .../modules/mod_frontend/mod_frontend.php | 1 + .../modules/mod_frontend/tmpl/default.php | 17 +- .../modules/mod_latest/mod_latest.php | 33 +- .../mod_latest/src/Helper/LatestHelper.php | 218 +- .../modules/mod_latest/tmpl/default.php | 97 +- .../mod_latestactions/mod_latestactions.php | 11 +- .../src/Helper/LatestActionsHelper.php | 86 +- .../mod_latestactions/tmpl/default.php | 55 +- .../modules/mod_logged/mod_logged.php | 19 +- .../mod_logged/src/Helper/LoggedHelper.php | 109 +- .../modules/mod_logged/tmpl/default.php | 95 +- .../modules/mod_logged/tmpl/disabled.php | 9 +- administrator/modules/mod_login/mod_login.php | 1 + .../mod_login/src/Helper/LoginHelper.php | 90 +- .../modules/mod_login/tmpl/default.php | 193 +- .../mod_loginsupport/mod_loginsupport.php | 6 +- .../modules/mod_loginsupport/tmpl/default.php | 73 +- administrator/modules/mod_menu/mod_menu.php | 1 + .../modules/mod_menu/src/Menu/CssMenu.php | 1046 +- .../modules/mod_menu/tmpl/default.php | 18 +- .../modules/mod_menu/tmpl/default_submenu.php | 224 +- .../modules/mod_messages/mod_messages.php | 29 +- .../modules/mod_messages/tmpl/default.php | 24 +- .../mod_multilangstatus.php | 1 + .../mod_multilangstatus/tmpl/default.php | 42 +- .../modules/mod_popular/mod_popular.php | 36 +- .../mod_popular/src/Helper/PopularHelper.php | 199 +- .../modules/mod_popular/tmpl/default.php | 87 +- .../mod_post_installation_messages.php | 22 +- .../tmpl/default.php | 32 +- .../mod_privacy_dashboard.php | 26 +- .../src/Helper/PrivacyDashboardHelper.php | 60 +- .../mod_privacy_dashboard/tmpl/default.php | 83 +- .../mod_privacy_status/mod_privacy_status.php | 8 +- .../src/Helper/PrivacyStatusHelper.php | 304 +- .../mod_privacy_status/tmpl/default.php | 261 +- .../mod_quickicon/services/provider.php | 31 +- .../src/Dispatcher/Dispatcher.php | 29 +- .../src/Event/QuickIconsEvent.php | 71 +- .../src/Helper/QuickIconHelper.php | 551 +- .../modules/mod_quickicon/tmpl/default.php | 11 +- .../modules/mod_sampledata/mod_sampledata.php | 1 + .../src/Helper/SampledataHelper.php | 47 +- .../modules/mod_sampledata/tmpl/default.php | 63 +- .../mod_stats_admin/mod_stats_admin.php | 1 + .../src/Helper/StatsAdminHelper.php | 247 +- .../modules/mod_stats_admin/tmpl/default.php | 21 +- .../modules/mod_submenu/mod_submenu.php | 35 +- .../modules/mod_submenu/src/Menu/Menu.php | 420 +- .../modules/mod_submenu/tmpl/default.php | 175 +- administrator/modules/mod_title/mod_title.php | 6 +- .../modules/mod_title/tmpl/default.php | 7 +- .../modules/mod_toolbar/mod_toolbar.php | 1 + .../modules/mod_toolbar/tmpl/default.php | 1 + administrator/modules/mod_user/mod_user.php | 1 + .../modules/mod_user/tmpl/default.php | 70 +- .../modules/mod_version/mod_version.php | 1 + .../mod_version/src/Helper/VersionHelper.php | 21 +- .../modules/mod_version/tmpl/default.php | 11 +- administrator/templates/atum/component.php | 17 +- administrator/templates/atum/cpanel.php | 1 + administrator/templates/atum/error.php | 12 +- administrator/templates/atum/error_full.php | 241 +- administrator/templates/atum/error_login.php | 181 +- .../atum/html/layouts/chromes/body.php | 60 +- .../atum/html/layouts/chromes/header-item.php | 8 +- .../atum/html/layouts/chromes/title.php | 8 +- .../atum/html/layouts/chromes/well.php | 74 +- .../templates/atum/html/layouts/status.php | 70 +- administrator/templates/atum/index.php | 181 +- administrator/templates/atum/login.php | 137 +- administrator/templates/system/component.php | 7 +- administrator/templates/system/error.php | 91 +- administrator/templates/system/index.php | 1 + .../src/Controller/BannersController.php | 29 +- .../src/Controller/ClientsController.php | 29 +- .../src/View/Banners/JsonapiView.php | 149 +- .../src/View/Clients/JsonapiView.php | 93 +- .../src/Controller/CategoriesController.php | 238 +- .../src/View/Categories/JsonapiView.php | 255 +- .../src/Controller/ApplicationController.php | 241 +- .../src/Controller/ComponentController.php | 259 +- .../src/View/Application/JsonapiView.php | 188 +- .../src/View/Component/JsonapiView.php | 214 +- .../src/Controller/ContactController.php | 449 +- .../src/Serializer/ContactSerializer.php | 208 +- .../src/View/Contacts/JsonapiView.php | 380 +- .../src/Controller/ArticlesController.php | 176 +- .../com_content/src/Helper/ContentHelper.php | 32 +- .../src/Serializer/ContentSerializer.php | 166 +- .../src/View/Articles/JsonapiView.php | 428 +- .../src/Controller/HistoryController.php | 210 +- .../src/View/History/JsonapiView.php | 77 +- .../src/Controller/FieldsController.php | 105 +- .../src/Controller/GroupsController.php | 105 +- .../src/View/Fields/JsonapiView.php | 196 +- .../src/View/Groups/JsonapiView.php | 182 +- .../src/Controller/ManageController.php | 75 +- .../src/View/Manage/JsonapiView.php | 61 +- .../src/Controller/LanguagesController.php | 29 +- .../src/Controller/OverridesController.php | 322 +- .../src/Controller/StringsController.php | 205 +- .../src/View/Languages/JsonapiView.php | 127 +- .../src/View/Overrides/JsonapiView.php | 144 +- .../src/View/Strings/JsonapiView.php | 147 +- .../src/Controller/ItemsController.php | 309 +- .../src/Controller/MenusController.php | 105 +- .../com_menus/src/View/Items/JsonapiView.php | 362 +- .../com_menus/src/View/Menus/JsonapiView.php | 61 +- .../src/Controller/MessagesController.php | 29 +- .../src/View/Messages/JsonapiView.php | 99 +- .../src/Controller/ModulesController.php | 207 +- .../src/View/Modules/JsonapiView.php | 231 +- .../src/Controller/FeedsController.php | 29 +- .../src/Serializer/NewsfeedSerializer.php | 170 +- .../src/View/Feeds/JsonapiView.php | 316 +- .../src/Controller/PluginsController.php | 192 +- .../src/View/Plugins/JsonapiView.php | 113 +- .../src/Controller/ConsentsController.php | 72 +- .../src/Controller/RequestsController.php | 116 +- .../src/View/Consents/JsonapiView.php | 183 +- .../src/View/Requests/JsonapiView.php | 72 +- .../src/Controller/RedirectController.php | 29 +- .../src/View/Redirect/JsonapiView.php | 73 +- .../src/Controller/TagsController.php | 29 +- .../com_tags/src/View/Tags/JsonapiView.php | 137 +- .../src/Controller/StylesController.php | 151 +- .../src/View/Styles/JsonapiView.php | 100 +- .../src/Controller/GroupsController.php | 29 +- .../src/Controller/LevelsController.php | 29 +- .../src/Controller/UsersController.php | 296 +- .../com_users/src/View/Groups/JsonapiView.php | 53 +- .../com_users/src/View/Levels/JsonapiView.php | 45 +- .../com_users/src/View/Users/JsonapiView.php | 185 +- api/includes/app.php | 21 +- api/includes/defines.php | 21 +- api/includes/framework.php | 135 +- api/index.php | 14 +- cli/joomla.php | 58 +- components/com_ajax/ajax.php | 377 +- .../src/Controller/DisplayController.php | 38 +- .../com_banners/src/Helper/BannerHelper.php | 36 +- .../com_banners/src/Model/BannerModel.php | 407 +- .../com_banners/src/Model/BannersModel.php | 744 +- .../com_banners/src/Service/Category.php | 27 +- components/com_banners/src/Service/Router.php | 159 +- .../src/Controller/ConfigController.php | 222 +- .../src/Controller/DisplayController.php | 25 +- .../src/Controller/ModulesController.php | 274 +- .../src/Controller/TemplatesController.php | 181 +- .../com_config/src/Dispatcher/Dispatcher.php | 51 +- .../com_config/src/Model/ConfigModel.php | 40 +- components/com_config/src/Model/FormModel.php | 494 +- .../com_config/src/Model/ModulesModel.php | 462 +- .../com_config/src/Model/TemplatesModel.php | 194 +- components/com_config/src/Service/Router.php | 31 +- .../com_config/src/View/Config/HtmlView.php | 218 +- .../com_config/src/View/Modules/HtmlView.php | 144 +- .../src/View/Templates/HtmlView.php | 254 +- components/com_config/tmpl/config/default.php | 73 +- .../tmpl/config/default_metadata.php | 25 +- .../com_config/tmpl/config/default_seo.php | 25 +- .../com_config/tmpl/config/default_site.php | 25 +- .../com_config/tmpl/modules/default.php | 306 +- .../tmpl/modules/default_options.php | 39 +- .../com_config/tmpl/templates/default.php | 63 +- .../tmpl/templates/default_options.php | 33 +- components/com_contact/helpers/route.php | 1 + .../com_contact/layouts/field/render.php | 31 +- .../com_contact/layouts/fields/render.php | 66 +- .../src/Controller/ContactController.php | 827 +- .../src/Controller/DisplayController.php | 98 +- .../com_contact/src/Dispatcher/Dispatcher.php | 45 +- .../src/Helper/AssociationHelper.php | 78 +- .../com_contact/src/Helper/RouteHelper.php | 112 +- .../com_contact/src/Model/CategoriesModel.php | 261 +- .../com_contact/src/Model/CategoryModel.php | 932 +- .../com_contact/src/Model/ContactModel.php | 844 +- .../com_contact/src/Model/FeaturedModel.php | 365 +- .../com_contact/src/Model/FormModel.php | 415 +- .../src/Rule/ContactEmailMessageRule.php | 56 +- .../com_contact/src/Rule/ContactEmailRule.php | 69 +- .../src/Rule/ContactEmailSubjectRule.php | 56 +- .../com_contact/src/Service/Category.php | 29 +- components/com_contact/src/Service/Router.php | 516 +- .../src/View/Categories/HtmlView.php | 25 +- .../src/View/Category/FeedView.php | 41 +- .../src/View/Category/HtmlView.php | 218 +- .../com_contact/src/View/Contact/HtmlView.php | 910 +- .../com_contact/src/View/Contact/VcfView.php | 169 +- .../src/View/Featured/HtmlView.php | 296 +- .../com_contact/src/View/Form/HtmlView.php | 308 +- .../com_contact/tmpl/categories/default.php | 9 +- .../tmpl/categories/default_items.php | 98 +- .../com_contact/tmpl/category/default.php | 9 +- .../tmpl/category/default_children.php | 61 +- .../tmpl/category/default_items.php | 365 +- .../com_contact/tmpl/contact/default.php | 314 +- .../tmpl/contact/default_address.php | 239 +- .../tmpl/contact/default_articles.php | 15 +- .../com_contact/tmpl/contact/default_form.php | 57 +- .../tmpl/contact/default_links.php | 49 +- .../tmpl/contact/default_profile.php | 63 +- .../contact/default_user_custom_fields.php | 45 +- .../com_contact/tmpl/featured/default.php | 25 +- .../tmpl/featured/default_items.php | 345 +- components/com_contact/tmpl/form/edit.php | 97 +- components/com_content/helpers/icon.php | 157 +- .../src/Controller/ArticleController.php | 833 +- .../src/Controller/DisplayController.php | 198 +- .../com_content/src/Dispatcher/Dispatcher.php | 65 +- .../src/Helper/AssociationHelper.php | 253 +- .../com_content/src/Helper/QueryHelper.php | 412 +- .../com_content/src/Helper/RouteHelper.php | 172 +- .../com_content/src/Model/ArchiveModel.php | 393 +- .../com_content/src/Model/ArticleModel.php | 845 +- .../com_content/src/Model/ArticlesModel.php | 1669 +- .../com_content/src/Model/CategoriesModel.php | 259 +- .../com_content/src/Model/CategoryModel.php | 941 +- .../com_content/src/Model/FeaturedModel.php | 289 +- .../com_content/src/Model/FormModel.php | 579 +- .../com_content/src/Service/Category.php | 27 +- components/com_content/src/Service/Router.php | 518 +- .../com_content/src/View/Archive/HtmlView.php | 459 +- .../com_content/src/View/Article/HtmlView.php | 644 +- .../src/View/Categories/HtmlView.php | 25 +- .../src/View/Category/FeedView.php | 89 +- .../src/View/Category/HtmlView.php | 423 +- .../src/View/Featured/FeedView.php | 173 +- .../src/View/Featured/HtmlView.php | 444 +- .../com_content/src/View/Form/HtmlView.php | 419 +- .../com_content/tmpl/archive/default.php | 57 +- .../tmpl/archive/default_items.php | 417 +- .../com_content/tmpl/article/default.php | 194 +- .../tmpl/article/default_links.php | 130 +- .../com_content/tmpl/categories/default.php | 9 +- .../tmpl/categories/default_items.php | 111 +- components/com_content/tmpl/category/blog.php | 221 +- .../tmpl/category/blog_children.php | 112 +- .../com_content/tmpl/category/blog_item.php | 125 +- .../com_content/tmpl/category/blog_links.php | 13 +- .../com_content/tmpl/category/default.php | 1 + .../tmpl/category/default_articles.php | 593 +- .../tmpl/category/default_children.php | 109 +- .../com_content/tmpl/featured/default.php | 111 +- .../tmpl/featured/default_item.php | 153 +- .../tmpl/featured/default_links.php | 13 +- components/com_content/tmpl/form/edit.php | 296 +- .../src/Controller/DisplayController.php | 27 +- .../src/Dispatcher/Dispatcher.php | 94 +- .../com_fields/layouts/field/render.php | 18 +- .../com_fields/layouts/fields/render.php | 75 +- .../src/Controller/DisplayController.php | 43 +- .../com_fields/src/Dispatcher/Dispatcher.php | 51 +- components/com_finder/helpers/route.php | 1 + .../src/Controller/DisplayController.php | 72 +- .../src/Controller/SuggestionsController.php | 151 +- .../com_finder/src/Helper/FinderHelper.php | 138 +- .../com_finder/src/Helper/RouteHelper.php | 267 +- .../com_finder/src/Model/SearchModel.php | 902 +- .../com_finder/src/Model/SuggestionsModel.php | 302 +- components/com_finder/src/Service/Router.php | 31 +- .../com_finder/src/View/Search/FeedView.php | 100 +- .../com_finder/src/View/Search/HtmlView.php | 601 +- .../src/View/Search/OpensearchView.php | 104 +- components/com_finder/tmpl/search/default.php | 41 +- .../com_finder/tmpl/search/default_form.php | 106 +- .../com_finder/tmpl/search/default_result.php | 164 +- .../tmpl/search/default_results.php | 113 +- .../com_media/src/Dispatcher/Dispatcher.php | 98 +- .../layouts/joomla/searchtools/default.php | 156 +- .../com_menus/src/Dispatcher/Dispatcher.php | 92 +- .../src/Controller/DisplayController.php | 40 +- .../com_modules/src/Dispatcher/Dispatcher.php | 97 +- components/com_newsfeeds/helpers/route.php | 1 + .../src/Controller/DisplayController.php | 56 +- .../src/Helper/AssociationHelper.php | 77 +- .../com_newsfeeds/src/Helper/RouteHelper.php | 104 +- .../src/Model/CategoriesModel.php | 261 +- .../com_newsfeeds/src/Model/CategoryModel.php | 786 +- .../com_newsfeeds/src/Model/NewsfeedModel.php | 404 +- .../com_newsfeeds/src/Service/Category.php | 25 +- .../com_newsfeeds/src/Service/Router.php | 478 +- .../src/View/Categories/HtmlView.php | 25 +- .../src/View/Category/HtmlView.php | 157 +- .../src/View/Newsfeed/HtmlView.php | 575 +- .../com_newsfeeds/tmpl/categories/default.php | 5 +- .../tmpl/categories/default_items.php | 93 +- .../com_newsfeeds/tmpl/category/default.php | 81 +- .../tmpl/category/default_children.php | 69 +- .../tmpl/category/default_items.php | 145 +- .../com_newsfeeds/tmpl/newsfeed/default.php | 253 +- .../src/Controller/DisplayController.php | 63 +- .../src/Controller/RequestController.php | 299 +- .../com_privacy/src/Model/ConfirmModel.php | 411 +- .../com_privacy/src/Model/RemindModel.php | 325 +- .../com_privacy/src/Model/RequestModel.php | 458 +- components/com_privacy/src/Service/Router.php | 37 +- .../com_privacy/src/View/Confirm/HtmlView.php | 191 +- .../com_privacy/src/View/Remind/HtmlView.php | 191 +- .../com_privacy/src/View/Request/HtmlView.php | 209 +- .../com_privacy/tmpl/confirm/default.php | 51 +- .../com_privacy/tmpl/remind/default.php | 51 +- .../com_privacy/tmpl/request/default.php | 65 +- components/com_tags/helpers/route.php | 1 + .../src/Controller/DisplayController.php | 72 +- .../src/Controller/TagsController.php | 69 +- .../com_tags/src/Helper/RouteHelper.php | 476 +- components/com_tags/src/Model/TagModel.php | 656 +- components/com_tags/src/Model/TagsModel.php | 285 +- components/com_tags/src/Service/Router.php | 375 +- components/com_tags/src/View/Tag/FeedView.php | 157 +- components/com_tags/src/View/Tag/HtmlView.php | 703 +- .../com_tags/src/View/Tags/FeedView.php | 99 +- .../com_tags/src/View/Tags/HtmlView.php | 299 +- components/com_tags/tmpl/tag/default.php | 85 +- .../com_tags/tmpl/tag/default_items.php | 153 +- components/com_tags/tmpl/tag/list.php | 71 +- components/com_tags/tmpl/tag/list_items.php | 210 +- components/com_tags/tmpl/tags/default.php | 33 +- .../com_tags/tmpl/tags/default_items.php | 223 +- .../src/Controller/CallbackController.php | 2 +- .../src/Controller/CaptiveController.php | 56 +- .../src/Controller/DisplayController.php | 235 +- .../src/Controller/MethodController.php | 56 +- .../src/Controller/MethodsController.php | 56 +- .../src/Controller/ProfileController.php | 410 +- .../src/Controller/RegistrationController.php | 417 +- .../src/Controller/RemindController.php | 72 +- .../src/Controller/ResetController.php | 326 +- .../src/Controller/UserController.php | 457 +- .../com_users/src/Model/BackupcodesModel.php | 2 +- .../com_users/src/Model/CaptiveModel.php | 2 +- components/com_users/src/Model/LoginModel.php | 256 +- .../com_users/src/Model/MethodModel.php | 2 +- .../com_users/src/Model/MethodsModel.php | 2 +- .../com_users/src/Model/ProfileModel.php | 599 +- .../com_users/src/Model/RegistrationModel.php | 1293 +- .../com_users/src/Model/RemindModel.php | 366 +- components/com_users/src/Model/ResetModel.php | 1001 +- .../src/Rule/LoginUniqueFieldRule.php | 72 +- .../src/Rule/LogoutUniqueFieldRule.php | 72 +- components/com_users/src/Service/Router.php | 105 +- .../com_users/src/View/Captive/HtmlView.php | 2 +- .../com_users/src/View/Login/HtmlView.php | 258 +- .../com_users/src/View/Method/HtmlView.php | 2 +- .../com_users/src/View/Methods/HtmlView.php | 2 +- .../com_users/src/View/Profile/HtmlView.php | 312 +- .../src/View/Registration/HtmlView.php | 230 +- .../com_users/src/View/Remind/HtmlView.php | 200 +- .../com_users/src/View/Reset/HtmlView.php | 215 +- components/com_users/tmpl/login/default.php | 16 +- .../com_users/tmpl/login/default_login.php | 199 +- .../com_users/tmpl/login/default_logout.php | 81 +- components/com_users/tmpl/profile/default.php | 39 +- .../com_users/tmpl/profile/default_core.php | 71 +- .../com_users/tmpl/profile/default_custom.php | 82 +- .../com_users/tmpl/profile/default_params.php | 47 +- components/com_users/tmpl/profile/edit.php | 107 +- .../com_users/tmpl/registration/complete.php | 11 +- .../com_users/tmpl/registration/default.php | 61 +- components/com_users/tmpl/remind/default.php | 51 +- components/com_users/tmpl/reset/complete.php | 51 +- components/com_users/tmpl/reset/confirm.php | 51 +- components/com_users/tmpl/reset/default.php | 51 +- .../src/Controller/DisplayController.php | 37 +- components/com_wrapper/src/Service/Router.php | 60 +- .../com_wrapper/src/View/Wrapper/HtmlView.php | 190 +- .../com_wrapper/tmpl/wrapper/default.php | 55 +- includes/app.php | 30 +- includes/defines.php | 21 +- includes/framework.php | 129 +- index.php | 18 +- installation/includes/app.php | 39 +- installation/includes/defines.php | 21 +- installation/includes/framework.php | 29 +- installation/index.php | 18 +- .../Application/InstallationApplication.php | 1076 +- .../src/Controller/DisplayController.php | 168 +- .../src/Controller/InstallationController.php | 503 +- .../src/Controller/JSONController.php | 94 +- .../src/Controller/LanguageController.php | 221 +- .../src/Error/Renderer/JsonRenderer.php | 41 +- .../Form/Field/Installation/LanguageField.php | 247 +- .../Form/Field/Installation/PrefixField.php | 112 +- installation/src/Form/Rule/PrefixRule.php | 29 +- installation/src/Form/Rule/UsernameRule.php | 50 +- installation/src/Helper/DatabaseHelper.php | 997 +- .../src/Model/BaseInstallationModel.php | 31 +- installation/src/Model/ChecksModel.php | 510 +- installation/src/Model/CleanupModel.php | 42 +- installation/src/Model/ConfigurationModel.php | 1227 +- installation/src/Model/DatabaseModel.php | 1043 +- installation/src/Model/LanguagesModel.php | 1038 +- installation/src/Model/SetupModel.php | 606 +- installation/src/Response/JsonResponse.php | 86 +- .../src/Router/InstallationRouter.php | 59 +- .../src/Service/Provider/Application.php | 72 +- installation/src/View/DefaultView.php | 43 +- installation/src/View/Error/HtmlView.php | 1 + installation/src/View/Preinstall/HtmlView.php | 63 +- installation/src/View/Remove/HtmlView.php | 111 +- installation/src/View/Setup/HtmlView.php | 1 + installation/template/body.php | 1 + installation/template/error.php | 161 +- installation/template/index.php | 145 +- installation/tmpl/error/default.php | 1 + installation/tmpl/preinstall/default.php | 49 +- installation/tmpl/remove/default.php | 541 +- installation/tmpl/setup/default.php | 209 +- language/en-GB/localise.php | 44 +- layouts/chromes/html5.php | 34 +- layouts/chromes/none.php | 1 + layouts/chromes/outline.php | 27 +- layouts/chromes/table.php | 28 +- layouts/joomla/button/action-button.php | 15 +- layouts/joomla/button/iconclass.php | 1 + layouts/joomla/button/transition-button.php | 59 +- layouts/joomla/content/associations.php | 27 +- .../content/blog_style_default_item_title.php | 53 +- layouts/joomla/content/categories_default.php | 29 +- .../content/categories_default_items.php | 6 +- layouts/joomla/content/category_default.php | 94 +- layouts/joomla/content/emptystate.php | 58 +- layouts/joomla/content/emptystate_module.php | 14 +- layouts/joomla/content/full_image.php | 20 +- layouts/joomla/content/icons.php | 15 +- layouts/joomla/content/icons/create.php | 7 +- layouts/joomla/content/icons/edit.php | 12 +- layouts/joomla/content/icons/edit_lock.php | 26 +- layouts/joomla/content/info_block.php | 78 +- .../content/info_block/associations.php | 29 +- layouts/joomla/content/info_block/author.php | 17 +- .../joomla/content/info_block/category.php | 23 +- .../joomla/content/info_block/create_date.php | 9 +- layouts/joomla/content/info_block/hits.php | 7 +- .../joomla/content/info_block/modify_date.php | 9 +- .../content/info_block/parent_category.php | 23 +- .../content/info_block/publish_date.php | 9 +- layouts/joomla/content/intro_image.php | 30 +- layouts/joomla/content/language.php | 24 +- layouts/joomla/content/options_default.php | 49 +- layouts/joomla/content/readmore.php | 49 +- layouts/joomla/content/tags.php | 27 +- layouts/joomla/content/text_filters.php | 47 +- layouts/joomla/edit/admin_modules.php | 54 +- layouts/joomla/edit/associations.php | 5 +- layouts/joomla/edit/fieldset.php | 71 +- layouts/joomla/edit/frontediting_modules.php | 57 +- layouts/joomla/edit/global.php | 68 +- layouts/joomla/edit/metadata.php | 42 +- layouts/joomla/edit/params.php | 307 +- layouts/joomla/edit/publishingdata.php | 49 +- layouts/joomla/edit/title_alias.php | 13 +- layouts/joomla/editors/buttons.php | 13 +- layouts/joomla/editors/buttons/button.php | 23 +- layouts/joomla/editors/buttons/modal.php | 48 +- layouts/joomla/error/backtrace.php | 88 +- layouts/joomla/form/field/calendar.php | 137 +- layouts/joomla/form/field/checkbox.php | 17 +- layouts/joomla/form/field/checkboxes.php | 53 +- layouts/joomla/form/field/color/advanced.php | 46 +- layouts/joomla/form/field/color/simple.php | 19 +- layouts/joomla/form/field/color/slider.php | 148 +- layouts/joomla/form/field/combo.php | 20 +- layouts/joomla/form/field/contenthistory.php | 39 +- layouts/joomla/form/field/email.php | 39 +- layouts/joomla/form/field/file.php | 25 +- .../form/field/groupedlist-fancy-select.php | 86 +- layouts/joomla/form/field/groupedlist.php | 71 +- layouts/joomla/form/field/hidden.php | 11 +- .../joomla/form/field/list-fancy-select.php | 59 +- layouts/joomla/form/field/list.php | 60 +- layouts/joomla/form/field/media.php | 189 +- layouts/joomla/form/field/meter.php | 17 +- layouts/joomla/form/field/moduleorder.php | 28 +- layouts/joomla/form/field/number.php | 52 +- layouts/joomla/form/field/password.php | 154 +- layouts/joomla/form/field/radio/buttons.php | 111 +- layouts/joomla/form/field/radio/switcher.php | 49 +- layouts/joomla/form/field/radiobasic.php | 59 +- layouts/joomla/form/field/range.php | 31 +- layouts/joomla/form/field/rules.php | 325 +- layouts/joomla/form/field/subform/default.php | 3 +- .../form/field/subform/repeatable-table.php | 143 +- .../repeatable-table/section-byfieldsets.php | 59 +- .../subform/repeatable-table/section.php | 53 +- .../joomla/form/field/subform/repeatable.php | 60 +- .../repeatable/section-byfieldsets.php | 51 +- .../form/field/subform/repeatable/section.php | 27 +- layouts/joomla/form/field/tag.php | 77 +- layouts/joomla/form/field/tel.php | 39 +- layouts/joomla/form/field/text.php | 86 +- layouts/joomla/form/field/textarea.php | 52 +- layouts/joomla/form/field/time.php | 35 +- layouts/joomla/form/field/url.php | 44 +- layouts/joomla/form/field/user.php | 124 +- layouts/joomla/form/renderfield.php | 45 +- layouts/joomla/form/renderlabel.php | 14 +- layouts/joomla/html/batch/access.php | 22 +- layouts/joomla/html/batch/adminlanguage.php | 7 +- layouts/joomla/html/batch/item.php | 35 +- layouts/joomla/html/batch/language.php | 7 +- layouts/joomla/html/batch/tag.php | 7 +- layouts/joomla/html/batch/user.php | 14 +- layouts/joomla/html/batch/workflowstage.php | 15 +- layouts/joomla/html/image.php | 31 +- layouts/joomla/html/treeprefix.php | 6 +- layouts/joomla/icon/iconclass.php | 52 +- layouts/joomla/installer/changelog.php | 93 +- layouts/joomla/links/groupclose.php | 1 + layouts/joomla/links/groupopen.php | 1 + layouts/joomla/links/groupsclose.php | 1 + layouts/joomla/links/groupseparator.php | 1 + layouts/joomla/links/groupsopen.php | 1 + layouts/joomla/links/link.php | 9 +- layouts/joomla/pagination/link.php | 120 +- layouts/joomla/pagination/links.php | 101 +- layouts/joomla/pagination/list.php | 19 +- layouts/joomla/quickicons/icon.php | 95 +- layouts/joomla/searchtools/default.php | 118 +- layouts/joomla/searchtools/default/bar.php | 57 +- .../joomla/searchtools/default/filters.php | 27 +- layouts/joomla/searchtools/default/list.php | 17 +- .../joomla/searchtools/default/noitems.php | 5 +- .../joomla/searchtools/default/selector.php | 9 +- layouts/joomla/searchtools/grid/sort.php | 47 +- layouts/joomla/sidebars/submenu.php | 97 +- layouts/joomla/system/message.php | 62 +- layouts/joomla/tinymce/textarea.php | 28 +- layouts/joomla/tinymce/togglebutton.php | 13 +- layouts/joomla/toolbar/base.php | 1 + layouts/joomla/toolbar/basic.php | 41 +- layouts/joomla/toolbar/batch.php | 5 +- layouts/joomla/toolbar/containerclose.php | 1 + layouts/joomla/toolbar/containeropen.php | 1 + layouts/joomla/toolbar/dropdown.php | 49 +- layouts/joomla/toolbar/iconclass.php | 1 + layouts/joomla/toolbar/inlinehelp.php | 3 +- layouts/joomla/toolbar/link.php | 19 +- layouts/joomla/toolbar/popup.php | 17 +- layouts/joomla/toolbar/separator.php | 17 +- layouts/joomla/toolbar/standard.php | 30 +- layouts/joomla/toolbar/title.php | 5 +- layouts/joomla/toolbar/versions.php | 63 +- .../libraries/html/bootstrap/modal/body.php | 8 +- .../libraries/html/bootstrap/modal/footer.php | 3 +- .../libraries/html/bootstrap/modal/header.php | 15 +- .../libraries/html/bootstrap/modal/iframe.php | 22 +- .../libraries/html/bootstrap/modal/main.php | 68 +- .../libraries/html/bootstrap/tab/addtab.php | 9 +- .../libraries/html/bootstrap/tab/endtab.php | 1 + .../html/bootstrap/tab/endtabset.php | 1 + .../html/bootstrap/tab/starttabset.php | 1 + .../editors/tinymce/field/tinymcebuilder.php | 226 +- .../field/tinymcebuilder/setaccess.php | 3 +- .../field/tinymcebuilder/setoptions.php | 3 +- .../plugins/system/privacyconsent/label.php | 50 +- .../plugins/system/privacyconsent/message.php | 2 +- layouts/plugins/system/webauthn/manage.php | 163 +- layouts/plugins/user/terms/label.php | 50 +- layouts/plugins/user/terms/message.php | 1 + layouts/plugins/user/token/token.php | 27 +- libraries/bootstrap.php | 40 +- libraries/classmap.php | 1023 +- libraries/cms.php | 49 +- libraries/extensions.classmap.php | 21 +- libraries/import.legacy.php | 38 +- libraries/import.php | 38 +- libraries/loader.php | 1440 +- libraries/namespacemap.php | 561 +- libraries/src/Access/Access.php | 2199 +- .../Access/Exception/AuthenticationFailed.php | 1 + libraries/src/Access/Exception/NotAllowed.php | 1 + libraries/src/Access/Rule.php | 296 +- libraries/src/Access/Rules.php | 378 +- libraries/src/Adapter/Adapter.php | 394 +- libraries/src/Adapter/AdapterInstance.php | 87 +- .../Application/AdministratorApplication.php | 937 +- libraries/src/Application/ApiApplication.php | 771 +- .../src/Application/ApplicationHelper.php | 374 +- libraries/src/Application/BaseApplication.php | 41 +- libraries/src/Application/CLI/CliInput.php | 25 +- libraries/src/Application/CLI/CliOutput.php | 120 +- libraries/src/Application/CLI/ColorStyle.php | 487 +- .../CLI/Output/Processor/ColorProcessor.php | 340 +- .../Output/Processor/ProcessorInterface.php | 21 +- .../src/Application/CLI/Output/Stdout.php | 33 +- libraries/src/Application/CLI/Output/Xml.php | 35 +- libraries/src/Application/CMSApplication.php | 2553 ++- .../Application/CMSApplicationInterface.php | 309 +- .../CMSWebApplicationInterface.php | 135 +- libraries/src/Application/CliApplication.php | 744 +- .../src/Application/ConsoleApplication.php | 851 +- .../src/Application/DaemonApplication.php | 1694 +- libraries/src/Application/EventAware.php | 162 +- .../src/Application/EventAwareInterface.php | 57 +- .../Application/Exception/NotAcceptable.php | 1 + .../Application/ExtensionNamespaceMapper.php | 27 +- libraries/src/Application/IdentityAware.php | 105 +- .../MultiFactorAuthenticationHandler.php | 945 +- libraries/src/Application/SiteApplication.php | 1750 +- libraries/src/Application/WebApplication.php | 794 +- .../AssociationExtensionHelper.php | 540 +- .../AssociationExtensionInterface.php | 39 +- .../AssociationServiceInterface.php | 17 +- .../Association/AssociationServiceTrait.php | 65 +- .../src/Authentication/Authentication.php | 393 +- .../Authentication/AuthenticationResponse.php | 219 +- .../Password/Argon2iHandler.php | 27 +- .../Password/Argon2idHandler.php | 27 +- .../Authentication/Password/BCryptHandler.php | 27 +- .../Password/ChainedHandler.php | 171 +- .../CheckIfRehashNeededHandlerInterface.php | 21 +- .../Authentication/Password/MD5Handler.php | 121 +- .../Authentication/Password/PHPassHandler.php | 127 +- ...iderAwareAuthenticationPluginInterface.php | 33 +- libraries/src/Autoload/ClassLoader.php | 76 +- libraries/src/Button/ActionButton.php | 591 +- libraries/src/Button/FeaturedButton.php | 197 +- libraries/src/Button/PublishedButton.php | 208 +- libraries/src/Button/TransitionButton.php | 79 +- libraries/src/Captcha/Captcha.php | 481 +- .../Google/HttpBridgePostRequestMethod.php | 90 +- libraries/src/Categories/Categories.php | 828 +- libraries/src/Categories/CategoryFactory.php | 91 +- .../Categories/CategoryFactoryInterface.php | 27 +- .../src/Categories/CategoryInterface.php | 23 +- libraries/src/Categories/CategoryNode.php | 962 +- .../Categories/CategoryServiceInterface.php | 67 +- .../src/Categories/CategoryServiceTrait.php | 189 +- .../Categories/SectionNotFoundException.php | 1 + libraries/src/Changelog/Changelog.php | 707 +- libraries/src/Client/ClientHelper.php | 405 +- libraries/src/Client/FtpClient.php | 3649 ++-- libraries/src/Component/ComponentHelper.php | 881 +- libraries/src/Component/ComponentRecord.php | 255 +- .../Exception/MissingComponentException.php | 27 +- libraries/src/Component/Router/RouterBase.php | 103 +- .../src/Component/Router/RouterFactory.php | 116 +- .../Router/RouterFactoryInterface.php | 23 +- .../src/Component/Router/RouterInterface.php | 77 +- .../src/Component/Router/RouterLegacy.php | 177 +- .../Router/RouterServiceInterface.php | 23 +- .../Component/Router/RouterServiceTrait.php | 71 +- libraries/src/Component/Router/RouterView.php | 525 +- .../src/Component/Router/Rules/MenuRules.php | 529 +- .../Component/Router/Rules/NomenuRules.php | 315 +- .../Component/Router/Rules/RulesInterface.php | 75 +- .../Component/Router/Rules/StandardRules.php | 551 +- libraries/src/Console/AddUserCommand.php | 570 +- .../src/Console/AddUserToGroupCommand.php | 549 +- .../src/Console/ChangeUserPasswordCommand.php | 281 +- .../src/Console/CheckJoomlaUpdatesCommand.php | 233 +- libraries/src/Console/CheckUpdatesCommand.php | 98 +- libraries/src/Console/CleanCacheCommand.php | 134 +- libraries/src/Console/DeleteUserCommand.php | 359 +- .../src/Console/ExtensionDiscoverCommand.php | 242 +- .../ExtensionDiscoverInstallCommand.php | 421 +- .../Console/ExtensionDiscoverListCommand.php | 160 +- .../src/Console/ExtensionInstallCommand.php | 367 +- .../src/Console/ExtensionRemoveCommand.php | 364 +- .../src/Console/ExtensionsListCommand.php | 438 +- libraries/src/Console/FinderIndexCommand.php | 942 +- .../src/Console/GetConfigurationCommand.php | 675 +- libraries/src/Console/ListUserCommand.php | 220 +- .../Loader/WritableContainerLoader.php | 166 +- .../Loader/WritableLoaderInterface.php | 23 +- .../src/Console/RemoveOldFilesCommand.php | 223 +- .../Console/RemoveUserFromGroupCommand.php | 556 +- libraries/src/Console/SessionGcCommand.php | 165 +- .../src/Console/SessionMetadataGcCommand.php | 157 +- .../src/Console/SetConfigurationCommand.php | 865 +- libraries/src/Console/SiteDownCommand.php | 169 +- libraries/src/Console/SiteUpCommand.php | 169 +- libraries/src/Console/TasksListCommand.php | 219 +- libraries/src/Console/TasksRunCommand.php | 242 +- libraries/src/Console/TasksStateCommand.php | 321 +- libraries/src/Console/UpdateCoreCommand.php | 700 +- libraries/src/Crypt/Cipher/CryptoCipher.php | 211 +- libraries/src/Crypt/Cipher/SodiumCipher.php | 236 +- libraries/src/Crypt/Crypt.php | 264 +- libraries/src/Date/Date.php | 868 +- .../Dispatcher/AbstractModuleDispatcher.php | 225 +- libraries/src/Dispatcher/ApiDispatcher.php | 80 +- .../src/Dispatcher/ComponentDispatcher.php | 298 +- .../Dispatcher/ComponentDispatcherFactory.php | 109 +- .../ComponentDispatcherFactoryInterface.php | 23 +- libraries/src/Dispatcher/Dispatcher.php | 77 +- .../src/Dispatcher/DispatcherInterface.php | 17 +- .../Dispatcher/LegacyComponentDispatcher.php | 84 +- libraries/src/Dispatcher/ModuleDispatcher.php | 70 +- .../Dispatcher/ModuleDispatcherFactory.php | 87 +- .../ModuleDispatcherFactoryInterface.php | 25 +- libraries/src/Document/Document.php | 2429 ++- libraries/src/Document/DocumentRenderer.php | 103 +- libraries/src/Document/ErrorDocument.php | 282 +- libraries/src/Document/Factory.php | 184 +- libraries/src/Document/FactoryInterface.php | 47 +- libraries/src/Document/Feed/FeedEnclosure.php | 55 +- libraries/src/Document/Feed/FeedImage.php | 109 +- libraries/src/Document/Feed/FeedItem.php | 271 +- libraries/src/Document/FeedDocument.php | 449 +- libraries/src/Document/HtmlDocument.php | 1694 +- libraries/src/Document/ImageDocument.php | 94 +- libraries/src/Document/JsonDocument.php | 181 +- libraries/src/Document/JsonapiDocument.php | 349 +- .../Document/Opensearch/OpensearchImage.php | 73 +- .../src/Document/Opensearch/OpensearchUrl.php | 55 +- libraries/src/Document/OpensearchDocument.php | 403 +- libraries/src/Document/PreloadManager.php | 290 +- .../src/Document/PreloadManagerInterface.php | 147 +- libraries/src/Document/RawDocument.php | 69 +- .../Document/Renderer/Feed/AtomRenderer.php | 338 +- .../Document/Renderer/Feed/RssRenderer.php | 460 +- .../Renderer/Html/ComponentRenderer.php | 31 +- .../Document/Renderer/Html/HeadRenderer.php | 39 +- .../Renderer/Html/MessageRenderer.php | 120 +- .../Document/Renderer/Html/MetasRenderer.php | 313 +- .../Document/Renderer/Html/ModuleRenderer.php | 153 +- .../Renderer/Html/ModulesRenderer.php | 75 +- .../Renderer/Html/ScriptsRenderer.php | 618 +- .../Document/Renderer/Html/StylesRenderer.php | 621 +- libraries/src/Document/RendererInterface.php | 25 +- libraries/src/Document/XmlDocument.php | 221 +- libraries/src/Editor/Editor.php | 574 +- libraries/src/Encrypt/AES/AbstractAES.php | 135 +- libraries/src/Encrypt/AES/AesInterface.php | 121 +- libraries/src/Encrypt/AES/Mcrypt.php | 351 +- libraries/src/Encrypt/AES/OpenSSL.php | 406 +- libraries/src/Encrypt/Aes.php | 527 +- libraries/src/Encrypt/Base32.php | 402 +- libraries/src/Encrypt/RandValInterface.php | 17 +- libraries/src/Encrypt/Randval.php | 29 +- libraries/src/Encrypt/Totp.php | 367 +- libraries/src/Environment/Browser.php | 1887 +- libraries/src/Error/AbstractRenderer.php | 159 +- .../AuthenticationFailedExceptionHandler.php | 76 +- .../CheckinCheckoutExceptionHandler.php | 74 +- .../InvalidParameterExceptionHandler.php | 46 +- .../JsonApi/InvalidRouteExceptionHandler.php | 76 +- .../JsonApi/NotAcceptableExceptionHandler.php | 76 +- .../JsonApi/NotAllowedExceptionHandler.php | 76 +- .../ResourceNotFoundExceptionHandler.php | 76 +- .../Error/JsonApi/SaveExceptionHandler.php | 80 +- .../JsonApi/SendEmailExceptionHandler.php | 74 +- libraries/src/Error/Renderer/CliRenderer.php | 85 +- libraries/src/Error/Renderer/FeedRenderer.php | 1 + libraries/src/Error/Renderer/HtmlRenderer.php | 93 +- libraries/src/Error/Renderer/JsonRenderer.php | 91 +- .../src/Error/Renderer/JsonapiRenderer.php | 105 +- libraries/src/Error/Renderer/XmlRenderer.php | 83 +- libraries/src/Error/RendererInterface.php | 37 +- libraries/src/Event/AbstractEvent.php | 287 +- .../src/Event/AbstractImmutableEvent.php | 136 +- .../src/Event/AfterExtensionBootEvent.php | 69 +- .../src/Event/BeforeExtensionBootEvent.php | 69 +- libraries/src/Event/CoreEventAware.php | 150 +- libraries/src/Event/ErrorEvent.php | 71 +- libraries/src/Event/GenericEvent.php | 1 + .../src/Event/Model/BeforeBatchEvent.php | 43 +- .../MultiFactor/BeforeDisplayMethods.php | 62 +- libraries/src/Event/MultiFactor/Callback.php | 56 +- libraries/src/Event/MultiFactor/Captive.php | 66 +- libraries/src/Event/MultiFactor/GetMethod.php | 31 +- libraries/src/Event/MultiFactor/GetSetup.php | 66 +- .../src/Event/MultiFactor/NotifyActionLog.php | 64 +- libraries/src/Event/MultiFactor/SaveSetup.php | 105 +- libraries/src/Event/MultiFactor/Validate.php | 133 +- .../src/Event/Plugin/System/Webauthn/Ajax.php | 1 + .../Plugin/System/Webauthn/AjaxChallenge.php | 39 +- .../Plugin/System/Webauthn/AjaxCreate.php | 5 +- .../Plugin/System/Webauthn/AjaxDelete.php | 5 +- .../Plugin/System/Webauthn/AjaxInitCreate.php | 39 +- .../Plugin/System/Webauthn/AjaxLogin.php | 2 +- .../Plugin/System/Webauthn/AjaxSaveLabel.php | 5 +- .../src/Event/QuickIcon/GetIconEvent.php | 66 +- libraries/src/Event/ReshapeArgumentsAware.php | 70 +- libraries/src/Event/Result/ResultAware.php | 134 +- .../src/Event/Result/ResultAwareInterface.php | 43 +- .../src/Event/Result/ResultTypeArrayAware.php | 82 +- .../Event/Result/ResultTypeBooleanAware.php | 57 +- .../src/Event/Result/ResultTypeFloatAware.php | 82 +- .../Event/Result/ResultTypeIntegerAware.php | 82 +- .../src/Event/Result/ResultTypeMixedAware.php | 31 +- .../Event/Result/ResultTypeNumericAware.php | 82 +- .../Event/Result/ResultTypeObjectAware.php | 121 +- .../Event/Result/ResultTypeStringAware.php | 82 +- libraries/src/Event/Table/AbstractEvent.php | 71 +- libraries/src/Event/Table/AfterBindEvent.php | 1 + .../src/Event/Table/AfterCheckinEvent.php | 1 + .../src/Event/Table/AfterCheckoutEvent.php | 1 + .../src/Event/Table/AfterDeleteEvent.php | 40 +- libraries/src/Event/Table/AfterHitEvent.php | 1 + libraries/src/Event/Table/AfterLoadEvent.php | 109 +- libraries/src/Event/Table/AfterMoveEvent.php | 157 +- .../src/Event/Table/AfterPublishEvent.php | 1 + .../src/Event/Table/AfterReorderEvent.php | 81 +- libraries/src/Event/Table/AfterResetEvent.php | 1 + libraries/src/Event/Table/AfterStoreEvent.php | 67 +- libraries/src/Event/Table/BeforeBindEvent.php | 115 +- .../src/Event/Table/BeforeCheckinEvent.php | 40 +- .../src/Event/Table/BeforeCheckoutEvent.php | 82 +- .../src/Event/Table/BeforeDeleteEvent.php | 40 +- libraries/src/Event/Table/BeforeHitEvent.php | 1 + libraries/src/Event/Table/BeforeLoadEvent.php | 77 +- libraries/src/Event/Table/BeforeMoveEvent.php | 157 +- .../src/Event/Table/BeforePublishEvent.php | 158 +- .../src/Event/Table/BeforeReorderEvent.php | 115 +- .../src/Event/Table/BeforeResetEvent.php | 1 + .../src/Event/Table/BeforeStoreEvent.php | 77 +- libraries/src/Event/Table/CheckEvent.php | 2 +- .../src/Event/Table/ObjectCreateEvent.php | 1 + libraries/src/Event/Table/SetNewTagsEvent.php | 77 +- libraries/src/Event/View/DisplayEvent.php | 75 +- .../src/Event/WebAsset/AbstractEvent.php | 36 +- .../WebAsset/WebAssetRegistryAssetChanged.php | 155 +- .../src/Event/Workflow/AbstractEvent.php | 71 +- .../WorkflowFunctionalityUsedEvent.php | 70 +- .../Workflow/WorkflowTransitionEvent.php | 72 +- libraries/src/Exception/ExceptionHandler.php | 411 +- .../Extension/BootableExtensionInterface.php | 29 +- libraries/src/Extension/Component.php | 65 +- .../src/Extension/ComponentInterface.php | 21 +- libraries/src/Extension/DummyPlugin.php | 1 + libraries/src/Extension/ExtensionHelper.php | 920 +- .../Extension/ExtensionManagerInterface.php | 65 +- .../src/Extension/ExtensionManagerTrait.php | 438 +- libraries/src/Extension/LegacyComponent.php | 489 +- libraries/src/Extension/MVCComponent.php | 3 +- libraries/src/Extension/Module.php | 126 +- libraries/src/Extension/ModuleInterface.php | 25 +- libraries/src/Extension/PluginInterface.php | 17 +- .../Service/Provider/CategoryFactory.php | 80 +- .../Provider/ComponentDispatcherFactory.php | 76 +- .../Service/Provider/HelperFactory.php | 80 +- .../Extension/Service/Provider/MVCFactory.php | 99 +- .../src/Extension/Service/Provider/Module.php | 44 +- .../Provider/ModuleDispatcherFactory.php | 76 +- .../Service/Provider/RouterFactory.php | 93 +- libraries/src/Factory.php | 1573 +- libraries/src/Feed/Feed.php | 690 +- libraries/src/Feed/FeedEntry.php | 431 +- libraries/src/Feed/FeedFactory.php | 270 +- libraries/src/Feed/FeedLink.php | 138 +- libraries/src/Feed/FeedParser.php | 529 +- libraries/src/Feed/FeedPerson.php | 91 +- libraries/src/Feed/Parser/AtomParser.php | 430 +- .../Feed/Parser/NamespaceParserInterface.php | 45 +- .../src/Feed/Parser/Rss/ItunesRssParser.php | 53 +- .../src/Feed/Parser/Rss/MediaRssParser.php | 53 +- libraries/src/Feed/Parser/RssParser.php | 836 +- .../src/Fields/FieldsServiceInterface.php | 41 +- libraries/src/Filesystem/File.php | 1211 +- libraries/src/Filesystem/FilesystemHelper.php | 679 +- libraries/src/Filesystem/Folder.php | 1324 +- libraries/src/Filesystem/Patcher.php | 1002 +- libraries/src/Filesystem/Path.php | 738 +- libraries/src/Filesystem/Stream.php | 2664 ++- .../src/Filesystem/Streams/StreamString.php | 474 +- .../Filesystem/Support/StringController.php | 94 +- libraries/src/Filter/InputFilter.php | 967 +- libraries/src/Filter/OutputFilter.php | 187 +- .../src/Form/Field/AccessiblemediaField.php | 339 +- libraries/src/Form/Field/AccesslevelField.php | 37 +- libraries/src/Form/Field/AliastagField.php | 97 +- libraries/src/Form/Field/AuthorField.php | 111 +- .../src/Form/Field/CachehandlerField.php | 58 +- libraries/src/Form/Field/CalendarField.php | 721 +- libraries/src/Form/Field/CaptchaField.php | 307 +- libraries/src/Form/Field/CategoryField.php | 141 +- libraries/src/Form/Field/CheckboxField.php | 237 +- libraries/src/Form/Field/CheckboxesField.php | 285 +- libraries/src/Form/Field/ChromestyleField.php | 447 +- libraries/src/Form/Field/ColorField.php | 705 +- libraries/src/Form/Field/ComboField.php | 92 +- .../src/Form/Field/ComponentlayoutField.php | 470 +- libraries/src/Form/Field/ComponentsField.php | 101 +- .../src/Form/Field/ContenthistoryField.php | 96 +- .../src/Form/Field/ContentlanguageField.php | 37 +- libraries/src/Form/Field/ContenttypeField.php | 175 +- .../Form/Field/DatabaseconnectionField.php | 104 +- libraries/src/Form/Field/EditorField.php | 615 +- libraries/src/Form/Field/EmailField.php | 85 +- libraries/src/Form/Field/FileField.php | 250 +- libraries/src/Form/Field/FilelistField.php | 426 +- libraries/src/Form/Field/FolderlistField.php | 426 +- .../src/Form/Field/FrontendlanguageField.php | 89 +- libraries/src/Form/Field/GroupedlistField.php | 274 +- libraries/src/Form/Field/HeadertagField.php | 62 +- libraries/src/Form/Field/HiddenField.php | 75 +- libraries/src/Form/Field/ImagelistField.php | 45 +- libraries/src/Form/Field/IntegerField.php | 104 +- libraries/src/Form/Field/LanguageField.php | 128 +- .../Form/Field/LastvisitdaterangeField.php | 61 +- libraries/src/Form/Field/LimitboxField.php | 168 +- libraries/src/Form/Field/ListField.php | 419 +- libraries/src/Form/Field/MediaField.php | 804 +- libraries/src/Form/Field/MenuField.php | 212 +- libraries/src/Form/Field/MenuitemField.php | 464 +- libraries/src/Form/Field/MeterField.php | 346 +- .../src/Form/Field/ModulelayoutField.php | 370 +- libraries/src/Form/Field/ModuleorderField.php | 234 +- .../src/Form/Field/ModulepositionField.php | 299 +- libraries/src/Form/Field/ModuletagField.php | 62 +- libraries/src/Form/Field/NoteField.php | 130 +- libraries/src/Form/Field/NumberField.php | 379 +- libraries/src/Form/Field/OrderingField.php | 332 +- libraries/src/Form/Field/PasswordField.php | 407 +- libraries/src/Form/Field/PluginsField.php | 307 +- .../src/Form/Field/PluginstatusField.php | 35 +- .../src/Form/Field/PredefinedlistField.php | 209 +- libraries/src/Form/Field/RadioField.php | 69 +- libraries/src/Form/Field/RadiobasicField.php | 83 +- libraries/src/Form/Field/RangeField.php | 87 +- .../src/Form/Field/RedirectStatusField.php | 41 +- .../Form/Field/RegistrationdaterangeField.php | 79 +- libraries/src/Form/Field/RulesField.php | 524 +- .../src/Form/Field/SessionhandlerField.php | 60 +- libraries/src/Form/Field/SpacerField.php | 222 +- libraries/src/Form/Field/SqlField.php | 572 +- libraries/src/Form/Field/StatusField.php | 41 +- libraries/src/Form/Field/SubformField.php | 888 +- libraries/src/Form/Field/TagField.php | 653 +- libraries/src/Form/Field/TelephoneField.php | 87 +- .../src/Form/Field/TemplatestyleField.php | 350 +- libraries/src/Form/Field/TextField.php | 551 +- libraries/src/Form/Field/TextareaField.php | 334 +- libraries/src/Form/Field/TimeField.php | 304 +- libraries/src/Form/Field/TimezoneField.php | 288 +- libraries/src/Form/Field/TransitionField.php | 306 +- libraries/src/Form/Field/UrlField.php | 95 +- libraries/src/Form/Field/UserField.php | 301 +- libraries/src/Form/Field/UseractiveField.php | 69 +- .../src/Form/Field/UsergrouplistField.php | 133 +- libraries/src/Form/Field/UserstateField.php | 35 +- .../Field/WorkflowComponentSectionsField.php | 73 +- .../src/Form/Field/WorkflowconditionField.php | 219 +- .../src/Form/Field/WorkflowstageField.php | 265 +- libraries/src/Form/Filter/IntarrayFilter.php | 65 +- libraries/src/Form/Filter/RawFilter.php | 39 +- libraries/src/Form/Filter/RulesFilter.php | 68 +- libraries/src/Form/Filter/SafehtmlFilter.php | 49 +- libraries/src/Form/Filter/TelFilter.php | 180 +- libraries/src/Form/Filter/UnsetFilter.php | 39 +- libraries/src/Form/Filter/UrlFilter.php | 146 +- libraries/src/Form/Form.php | 3871 ++-- libraries/src/Form/FormFactory.php | 41 +- .../src/Form/FormFactoryAwareInterface.php | 21 +- libraries/src/Form/FormFactoryAwareTrait.php | 82 +- libraries/src/Form/FormFactoryInterface.php | 23 +- libraries/src/Form/FormField.php | 2728 ++- libraries/src/Form/FormFilterInterface.php | 33 +- libraries/src/Form/FormHelper.php | 1072 +- libraries/src/Form/FormRule.php | 128 +- libraries/src/Form/Rule/BooleanRule.php | 29 +- libraries/src/Form/Rule/CalendarRule.php | 68 +- libraries/src/Form/Rule/CaptchaRule.php | 76 +- libraries/src/Form/Rule/ColorRule.php | 72 +- libraries/src/Form/Rule/CssIdentifierRule.php | 111 +- .../Form/Rule/CssIdentifierSubstringRule.php | 98 +- libraries/src/Form/Rule/EmailRule.php | 343 +- libraries/src/Form/Rule/EqualsRule.php | 83 +- libraries/src/Form/Rule/ExistsRule.php | 81 +- libraries/src/Form/Rule/FilePathRule.php | 96 +- .../src/Form/Rule/FolderPathExistsRule.php | 77 +- libraries/src/Form/Rule/ModuleLayoutRule.php | 31 +- libraries/src/Form/Rule/NotequalsRule.php | 74 +- libraries/src/Form/Rule/NumberRule.php | 82 +- libraries/src/Form/Rule/OptionsRule.php | 115 +- libraries/src/Form/Rule/PasswordRule.php | 335 +- libraries/src/Form/Rule/RulesRule.php | 190 +- libraries/src/Form/Rule/SubformRule.php | 114 +- libraries/src/Form/Rule/TelRule.php | 146 +- libraries/src/Form/Rule/TimeRule.php | 303 +- libraries/src/Form/Rule/UrlRule.php | 201 +- libraries/src/Form/Rule/UserIdRule.php | 72 +- libraries/src/Form/Rule/UsernameRule.php | 86 +- libraries/src/HTML/HTMLHelper.php | 2518 ++- libraries/src/HTML/HTMLRegistryAwareTrait.php | 72 +- libraries/src/HTML/Helpers/Access.php | 565 +- .../src/HTML/Helpers/ActionsDropdown.php | 432 +- libraries/src/HTML/Helpers/AdminLanguage.php | 76 +- libraries/src/HTML/Helpers/Behavior.php | 495 +- libraries/src/HTML/Helpers/Bootstrap.php | 1646 +- libraries/src/HTML/Helpers/Category.php | 379 +- libraries/src/HTML/Helpers/Content.php | 110 +- .../src/HTML/Helpers/ContentLanguage.php | 97 +- libraries/src/HTML/Helpers/Date.php | 133 +- libraries/src/HTML/Helpers/Debug.php | 87 +- libraries/src/HTML/Helpers/DraggableList.php | 105 +- libraries/src/HTML/Helpers/Dropdown.php | 592 +- libraries/src/HTML/Helpers/Email.php | 88 +- libraries/src/HTML/Helpers/Form.php | 104 +- libraries/src/HTML/Helpers/FormBehavior.php | 271 +- libraries/src/HTML/Helpers/Grid.php | 507 +- libraries/src/HTML/Helpers/Icons.php | 112 +- libraries/src/HTML/Helpers/JGrid.php | 822 +- libraries/src/HTML/Helpers/Jquery.php | 124 +- libraries/src/HTML/Helpers/Links.php | 209 +- libraries/src/HTML/Helpers/ListHelper.php | 507 +- libraries/src/HTML/Helpers/Menu.php | 849 +- libraries/src/HTML/Helpers/Number.php | 163 +- libraries/src/HTML/Helpers/SearchTools.php | 220 +- libraries/src/HTML/Helpers/Select.php | 1382 +- libraries/src/HTML/Helpers/Sidebar.php | 261 +- libraries/src/HTML/Helpers/SortableList.php | 39 +- libraries/src/HTML/Helpers/StringHelper.php | 534 +- libraries/src/HTML/Helpers/Tag.php | 383 +- libraries/src/HTML/Helpers/Telephone.php | 102 +- libraries/src/HTML/Helpers/UiTab.php | 176 +- libraries/src/HTML/Helpers/User.php | 109 +- libraries/src/HTML/Helpers/WorkflowStage.php | 92 +- libraries/src/HTML/Registry.php | 211 +- libraries/src/Help/Help.php | 345 +- libraries/src/Helper/AuthenticationHelper.php | 262 +- libraries/src/Helper/CMSHelper.php | 219 +- libraries/src/Helper/ContentHelper.php | 483 +- libraries/src/Helper/HelperFactory.php | 87 +- .../Helper/HelperFactoryAwareInterface.php | 21 +- .../src/Helper/HelperFactoryAwareTrait.php | 76 +- .../src/Helper/HelperFactoryInterface.php | 23 +- libraries/src/Helper/LibraryHelper.php | 310 +- libraries/src/Helper/MediaHelper.php | 974 +- libraries/src/Helper/ModuleHelper.php | 1410 +- libraries/src/Helper/RouteHelper.php | 532 +- libraries/src/Helper/TagsHelper.php | 2185 +- libraries/src/Helper/UserGroupsHelper.php | 646 +- libraries/src/Http/Http.php | 66 +- libraries/src/Http/HttpFactory.php | 224 +- libraries/src/Http/Response.php | 1 + .../src/Http/Transport/CurlTransport.php | 559 +- .../src/Http/Transport/SocketTransport.php | 528 +- .../src/Http/Transport/StreamTransport.php | 417 +- libraries/src/Http/TransportInterface.php | 1 + .../Exception/UnparsableImageException.php | 1 + libraries/src/Image/Filter/Backgroundfill.php | 218 +- libraries/src/Image/Filter/Brightness.php | 40 +- libraries/src/Image/Filter/Contrast.php | 40 +- libraries/src/Image/Filter/Edgedetect.php | 29 +- libraries/src/Image/Filter/Emboss.php | 30 +- libraries/src/Image/Filter/Grayscale.php | 29 +- libraries/src/Image/Filter/Negate.php | 29 +- libraries/src/Image/Filter/Sketchy.php | 29 +- libraries/src/Image/Filter/Smooth.php | 40 +- libraries/src/Image/Image.php | 2244 +- libraries/src/Image/ImageFilter.php | 85 +- libraries/src/Input/Cli.php | 353 +- libraries/src/Input/Cookie.php | 150 +- libraries/src/Input/Files.php | 237 +- libraries/src/Input/Input.php | 362 +- libraries/src/Input/Json.php | 98 +- .../Installer/Adapter/ComponentAdapter.php | 2976 ++- .../src/Installer/Adapter/FileAdapter.php | 1124 +- .../src/Installer/Adapter/LanguageAdapter.php | 1790 +- .../src/Installer/Adapter/LibraryAdapter.php | 965 +- .../src/Installer/Adapter/ModuleAdapter.php | 1416 +- .../src/Installer/Adapter/PackageAdapter.php | 1407 +- .../src/Installer/Adapter/PluginAdapter.php | 1210 +- .../src/Installer/Adapter/TemplateAdapter.php | 1334 +- libraries/src/Installer/Installer.php | 5049 +++-- libraries/src/Installer/InstallerAdapter.php | 2625 ++- .../src/Installer/InstallerExtension.php | 258 +- libraries/src/Installer/InstallerHelper.php | 685 +- libraries/src/Installer/InstallerScript.php | 766 +- .../Installer/InstallerScriptInterface.php | 105 +- .../src/Installer/LegacyInstallerScript.php | 317 +- libraries/src/Installer/Manifest.php | 233 +- .../Installer/Manifest/LibraryManifest.php | 157 +- .../Installer/Manifest/PackageManifest.php | 151 +- libraries/src/Language/Associations.php | 346 +- .../src/Language/CachingLanguageFactory.php | 50 +- libraries/src/Language/Language.php | 2351 +-- libraries/src/Language/LanguageFactory.php | 29 +- .../src/Language/LanguageFactoryInterface.php | 23 +- libraries/src/Language/LanguageHelper.php | 1276 +- libraries/src/Language/Multilanguage.php | 207 +- libraries/src/Language/Text.php | 738 +- libraries/src/Language/Transliterate.php | 481 +- libraries/src/Layout/BaseLayout.php | 555 +- libraries/src/Layout/FileLayout.php | 1207 +- libraries/src/Layout/LayoutHelper.php | 97 +- libraries/src/Layout/LayoutInterface.php | 41 +- libraries/src/Log/DelegatingPsrLogger.php | 156 +- libraries/src/Log/Log.php | 755 +- libraries/src/Log/LogEntry.php | 205 +- libraries/src/Log/Logger.php | 93 +- libraries/src/Log/Logger/CallbackLogger.php | 82 +- libraries/src/Log/Logger/DatabaseLogger.php | 277 +- libraries/src/Log/Logger/EchoLogger.php | 80 +- .../src/Log/Logger/FormattedtextLogger.php | 547 +- libraries/src/Log/Logger/InMemoryLogger.php | 113 +- .../src/Log/Logger/MessagequeueLogger.php | 66 +- libraries/src/Log/Logger/SyslogLogger.php | 207 +- libraries/src/Log/Logger/W3cLogger.php | 52 +- libraries/src/Log/LoggerRegistry.php | 142 +- .../src/MVC/Controller/AdminController.php | 914 +- .../src/MVC/Controller/ApiController.php | 1028 +- .../src/MVC/Controller/BaseController.php | 2156 +- .../MVC/Controller/ControllerInterface.php | 25 +- .../Controller/Exception/CheckinCheckout.php | 1 + .../Controller/Exception/ResourceNotFound.php | 1 + .../src/MVC/Controller/Exception/Save.php | 1 + .../MVC/Controller/Exception/SendEmail.php | 1 + .../src/MVC/Controller/FormController.php | 1737 +- libraries/src/MVC/Factory/ApiMVCFactory.php | 83 +- libraries/src/MVC/Factory/LegacyFactory.php | 204 +- libraries/src/MVC/Factory/MVCFactory.php | 673 +- .../src/MVC/Factory/MVCFactoryAwareTrait.php | 72 +- .../src/MVC/Factory/MVCFactoryInterface.php | 111 +- .../Factory/MVCFactoryServiceInterface.php | 19 +- .../MVC/Factory/MVCFactoryServiceTrait.php | 70 +- libraries/src/MVC/Model/AdminModel.php | 3423 ++- libraries/src/MVC/Model/BaseDatabaseModel.php | 745 +- libraries/src/MVC/Model/BaseModel.php | 246 +- .../src/MVC/Model/DatabaseAwareTrait.php | 84 +- .../src/MVC/Model/DatabaseModelInterface.php | 17 +- libraries/src/MVC/Model/FormBehaviorTrait.php | 316 +- libraries/src/MVC/Model/FormModel.php | 415 +- .../src/MVC/Model/FormModelInterface.php | 27 +- libraries/src/MVC/Model/ItemModel.php | 65 +- .../src/MVC/Model/ItemModelInterface.php | 23 +- .../src/MVC/Model/LegacyModelLoaderTrait.php | 297 +- libraries/src/MVC/Model/ListModel.php | 1379 +- .../src/MVC/Model/ListModelInterface.php | 21 +- libraries/src/MVC/Model/ModelInterface.php | 19 +- .../src/MVC/Model/StateBehaviorTrait.php | 142 +- .../src/MVC/Model/StatefulModelInterface.php | 45 +- .../src/MVC/Model/WorkflowBehaviorTrait.php | 786 +- .../src/MVC/Model/WorkflowModelInterface.php | 212 +- libraries/src/MVC/View/AbstractView.php | 461 +- libraries/src/MVC/View/CategoriesView.php | 221 +- libraries/src/MVC/View/CategoryFeedView.php | 249 +- libraries/src/MVC/View/CategoryView.php | 584 +- .../src/MVC/View/Event/OnGetApiFields.php | 331 +- libraries/src/MVC/View/FormView.php | 433 +- .../src/MVC/View/GenericDataException.php | 1 + libraries/src/MVC/View/HtmlView.php | 1173 +- libraries/src/MVC/View/JsonApiView.php | 509 +- libraries/src/MVC/View/JsonView.php | 143 +- libraries/src/MVC/View/ListView.php | 488 +- libraries/src/MVC/View/ViewInterface.php | 41 +- .../Mail/Exception/MailDisabledException.php | 95 +- libraries/src/Mail/Mail.php | 1338 +- libraries/src/Mail/MailHelper.php | 493 +- libraries/src/Mail/MailTemplate.php | 938 +- .../Mail/language/phpmailer.lang-en_gb.php | 1 + libraries/src/Menu/AbstractMenu.php | 794 +- libraries/src/Menu/AdministratorMenu.php | 23 +- libraries/src/Menu/AdministratorMenuItem.php | 43 +- libraries/src/Menu/MenuFactory.php | 74 +- libraries/src/Menu/MenuFactoryInterface.php | 23 +- libraries/src/Menu/MenuItem.php | 452 +- libraries/src/Menu/SiteMenu.php | 518 +- libraries/src/Microdata/Microdata.php | 1696 +- libraries/src/Object/CMSObject.php | 420 +- libraries/src/Pagination/Pagination.php | 1503 +- libraries/src/Pagination/PaginationObject.php | 89 +- libraries/src/Pathway/Pathway.php | 336 +- libraries/src/Pathway/SitePathway.php | 111 +- libraries/src/Plugin/CMSPlugin.php | 768 +- libraries/src/Plugin/PluginHelper.php | 558 +- libraries/src/Profiler/Profiler.php | 330 +- libraries/src/Response/JsonResponse.php | 177 +- libraries/src/Router/AdministratorRouter.php | 77 +- libraries/src/Router/ApiRouter.php | 372 +- .../Exception/RouteNotFoundException.php | 34 +- libraries/src/Router/Route.php | 337 +- libraries/src/Router/Router.php | 878 +- libraries/src/Router/SiteRouter.php | 1189 +- .../src/Router/SiteRouterAwareInterface.php | 21 +- libraries/src/Router/SiteRouterAwareTrait.php | 70 +- libraries/src/Schema/ChangeItem.php | 437 +- .../src/Schema/ChangeItem/MysqlChangeItem.php | 759 +- .../ChangeItem/PostgresqlChangeItem.php | 631 +- .../Schema/ChangeItem/SqlsrvChangeItem.php | 261 +- libraries/src/Schema/ChangeSet.php | 588 +- .../Serializer/Events/OnGetApiAttributes.php | 139 +- .../Serializer/Events/OnGetApiRelation.php | 118 +- libraries/src/Serializer/JoomlaSerializer.php | 176 +- .../src/Service/Provider/Application.php | 291 +- .../src/Service/Provider/Authentication.php | 217 +- .../src/Service/Provider/CacheController.php | 44 +- libraries/src/Service/Provider/Config.php | 64 +- libraries/src/Service/Provider/Console.php | 373 +- libraries/src/Service/Provider/Database.php | 234 +- libraries/src/Service/Provider/Dispatcher.php | 44 +- libraries/src/Service/Provider/Document.php | 48 +- libraries/src/Service/Provider/Form.php | 48 +- .../src/Service/Provider/HTMLRegistry.php | 40 +- libraries/src/Service/Provider/Language.php | 44 +- libraries/src/Service/Provider/Logger.php | 42 +- libraries/src/Service/Provider/Menu.php | 50 +- libraries/src/Service/Provider/Pathway.php | 67 +- libraries/src/Service/Provider/Router.php | 80 +- libraries/src/Service/Provider/Session.php | 609 +- libraries/src/Service/Provider/Toolbar.php | 48 +- libraries/src/Service/Provider/User.php | 44 +- .../src/Service/Provider/WebAssetRegistry.php | 54 +- .../EventListener/MetadataManagerListener.php | 101 +- .../Exception/UnsupportedStorageException.php | 1 + libraries/src/Session/MetadataManager.php | 597 +- libraries/src/Session/Session.php | 635 +- libraries/src/Session/SessionFactory.php | 278 +- libraries/src/Session/SessionManager.php | 103 +- .../src/Session/Storage/JoomlaStorage.php | 563 +- libraries/src/String/PunycodeHelper.php | 463 +- libraries/src/Table/Asset.php | 385 +- libraries/src/Table/Category.php | 515 +- libraries/src/Table/Content.php | 709 +- libraries/src/Table/ContentHistory.php | 407 +- libraries/src/Table/ContentType.php | 274 +- libraries/src/Table/CoreContent.php | 622 +- libraries/src/Table/Extension.php | 188 +- libraries/src/Table/Language.php | 261 +- libraries/src/Table/Menu.php | 577 +- libraries/src/Table/MenuType.php | 589 +- libraries/src/Table/Module.php | 385 +- libraries/src/Table/Nested.php | 3464 ++- libraries/src/Table/Table.php | 3814 ++-- libraries/src/Table/TableInterface.php | 215 +- libraries/src/Table/Ucm.php | 23 +- libraries/src/Table/Update.php | 172 +- libraries/src/Table/UpdateSite.php | 73 +- libraries/src/Table/User.php | 1039 +- libraries/src/Table/Usergroup.php | 618 +- libraries/src/Table/ViewLevel.php | 166 +- libraries/src/Tag/TagApiSerializerTrait.php | 50 +- libraries/src/Tag/TagServiceInterface.php | 25 +- libraries/src/Tag/TagServiceTrait.php | 99 +- libraries/src/Tag/TaggableTableInterface.php | 81 +- libraries/src/Tag/TaggableTableTrait.php | 89 +- .../Toolbar/Button/AbstractGroupButton.php | 98 +- libraries/src/Toolbar/Button/BasicButton.php | 49 +- .../src/Toolbar/Button/ConfirmButton.php | 154 +- libraries/src/Toolbar/Button/CustomButton.php | 93 +- .../src/Toolbar/Button/DropdownButton.php | 185 +- libraries/src/Toolbar/Button/HelpButton.php | 153 +- .../src/Toolbar/Button/InlinehelpButton.php | 150 +- libraries/src/Toolbar/Button/LinkButton.php | 121 +- libraries/src/Toolbar/Button/PopupButton.php | 354 +- .../src/Toolbar/Button/SeparatorButton.php | 41 +- .../src/Toolbar/Button/StandardButton.php | 241 +- .../Toolbar/ContainerAwareToolbarFactory.php | 185 +- libraries/src/Toolbar/CoreButtonsTrait.php | 1030 +- libraries/src/Toolbar/Toolbar.php | 893 +- libraries/src/Toolbar/ToolbarButton.php | 964 +- .../src/Toolbar/ToolbarFactoryInterface.php | 45 +- libraries/src/Toolbar/ToolbarHelper.php | 1465 +- libraries/src/Tree/ImmutableNodeInterface.php | 105 +- libraries/src/Tree/ImmutableNodeTrait.php | 272 +- libraries/src/Tree/NodeInterface.php | 91 +- libraries/src/Tree/NodeTrait.php | 152 +- libraries/src/UCM/UCM.php | 1 + libraries/src/UCM/UCMBase.php | 232 +- libraries/src/UCM/UCMContent.php | 422 +- libraries/src/UCM/UCMType.php | 386 +- .../src/Updater/Adapter/CollectionAdapter.php | 444 +- .../src/Updater/Adapter/ExtensionAdapter.php | 648 +- libraries/src/Updater/DownloadSource.php | 127 +- libraries/src/Updater/Update.php | 1024 +- libraries/src/Updater/UpdateAdapter.php | 526 +- libraries/src/Updater/Updater.php | 828 +- libraries/src/Uri/Uri.php | 549 +- libraries/src/User/CurrentUserInterface.php | 21 +- libraries/src/User/CurrentUserTrait.php | 80 +- libraries/src/User/User.php | 1747 +- libraries/src/User/UserFactory.php | 99 +- libraries/src/User/UserFactoryInterface.php | 41 +- libraries/src/User/UserHelper.php | 1247 +- libraries/src/Utility/BufferStreamHandler.php | 450 +- libraries/src/Utility/Utility.php | 103 +- libraries/src/Version.php | 597 +- .../Versioning/VersionableControllerTrait.php | 160 +- .../src/Versioning/VersionableModelTrait.php | 120 +- .../Versioning/VersionableTableInterface.php | 23 +- libraries/src/Versioning/Versioning.php | 260 +- .../src/WebAsset/AssetItem/CoreAssetItem.php | 49 +- .../AssetItem/FormValidateAssetItem.php | 37 +- .../WebAsset/AssetItem/KeepaliveAssetItem.php | 71 +- .../AssetItem/LangActiveAssetItem.php | 63 +- .../AssetItem/TableColumnsAssetItem.php | 31 +- .../Exception/InvalidActionException.php | 2 +- .../Exception/UnknownAssetException.php | 2 +- .../UnsatisfiedDependencyException.php | 2 +- .../Exception/WebAssetExceptionInterface.php | 1 + .../WebAssetAttachBehaviorInterface.php | 23 +- libraries/src/WebAsset/WebAssetItem.php | 643 +- .../src/WebAsset/WebAssetItemInterface.php | 190 +- libraries/src/WebAsset/WebAssetManager.php | 1988 +- .../src/WebAsset/WebAssetManagerInterface.php | 113 +- libraries/src/WebAsset/WebAssetRegistry.php | 849 +- .../WebAsset/WebAssetRegistryInterface.php | 94 +- libraries/src/Workflow/Workflow.php | 1040 +- .../src/Workflow/WorkflowPluginTrait.php | 253 +- .../src/Workflow/WorkflowServiceInterface.php | 153 +- .../src/Workflow/WorkflowServiceTrait.php | 306 +- .../mod_articles_archive.php | 1 + .../src/Helper/ArticlesArchiveHelper.php | 136 +- modules/mod_articles_archive/tmpl/default.php | 20 +- .../mod_articles_categories.php | 4 +- .../src/Helper/ArticlesCategoriesHelper.php | 61 +- .../mod_articles_categories/tmpl/default.php | 6 +- .../tmpl/default_items.php | 47 +- .../mod_articles_category.php | 91 +- .../src/Helper/ArticlesCategoryHelper.php | 984 +- .../mod_articles_category/tmpl/default.php | 32 +- .../tmpl/default_items.php | 113 +- .../mod_articles_latest/services/provider.php | 31 +- .../src/Dispatcher/Dispatcher.php | 35 +- .../src/Helper/ArticlesLatestHelper.php | 268 +- modules/mod_articles_latest/tmpl/default.php | 20 +- .../mod_articles_news/services/provider.php | 31 +- .../src/Dispatcher/Dispatcher.php | 35 +- .../src/Helper/ArticlesNewsHelper.php | 354 +- modules/mod_articles_news/tmpl/_item.php | 56 +- modules/mod_articles_news/tmpl/default.php | 16 +- modules/mod_articles_news/tmpl/horizontal.php | 16 +- modules/mod_articles_news/tmpl/vertical.php | 24 +- .../mod_articles_popular.php | 8 +- .../src/Helper/ArticlesPopularHelper.php | 146 +- modules/mod_articles_popular/tmpl/default.php | 20 +- modules/mod_banners/mod_banners.php | 1 + .../mod_banners/src/Helper/BannersHelper.php | 69 +- modules/mod_banners/tmpl/default.php | 167 +- modules/mod_breadcrumbs/mod_breadcrumbs.php | 1 + .../src/Helper/BreadcrumbsHelper.php | 136 +- modules/mod_breadcrumbs/tmpl/default.php | 160 +- modules/mod_custom/mod_custom.php | 8 +- modules/mod_custom/tmpl/default.php | 12 +- modules/mod_feed/mod_feed.php | 1 + modules/mod_feed/src/Helper/FeedHelper.php | 62 +- modules/mod_feed/tmpl/default.php | 207 +- modules/mod_finder/mod_finder.php | 27 +- .../mod_finder/src/Helper/FinderHelper.php | 94 +- modules/mod_finder/tmpl/default.php | 53 +- modules/mod_footer/mod_footer.php | 23 +- modules/mod_footer/tmpl/default.php | 5 +- modules/mod_languages/mod_languages.php | 1 + .../src/Helper/LanguagesHelper.php | 244 +- modules/mod_languages/tmpl/default.php | 175 +- modules/mod_login/mod_login.php | 6 +- modules/mod_login/src/Helper/LoginHelper.php | 140 +- modules/mod_login/tmpl/default.php | 215 +- modules/mod_login/tmpl/default_logout.php | 41 +- modules/mod_menu/mod_menu.php | 6 +- modules/mod_menu/src/Helper/MenuHelper.php | 445 +- modules/mod_menu/tmpl/collapse-default.php | 13 +- modules/mod_menu/tmpl/default.php | 149 +- modules/mod_menu/tmpl/default_component.php | 93 +- modules/mod_menu/tmpl/default_heading.php | 53 +- modules/mod_menu/tmpl/default_separator.php | 53 +- modules/mod_menu/tmpl/default_url.php | 90 +- modules/mod_random_image/mod_random_image.php | 1 + .../src/Helper/RandomImageHelper.php | 249 +- modules/mod_random_image/tmpl/default.php | 10 +- .../mod_related_items/mod_related_items.php | 8 +- .../src/Helper/RelatedItemsHelper.php | 294 +- modules/mod_related_items/tmpl/default.php | 9 +- modules/mod_stats/mod_stats.php | 1 + modules/mod_stats/src/Helper/StatsHelper.php | 281 +- modules/mod_stats/tmpl/default.php | 9 +- modules/mod_syndicate/mod_syndicate.php | 6 +- .../src/Helper/SyndicateHelper.php | 43 +- modules/mod_syndicate/tmpl/default.php | 3 +- modules/mod_tags_popular/mod_tags_popular.php | 8 +- .../src/Helper/TagsPopularHelper.php | 339 +- modules/mod_tags_popular/tmpl/cloud.php | 66 +- modules/mod_tags_popular/tmpl/default.php | 31 +- modules/mod_tags_similar/mod_tags_similar.php | 3 +- .../src/Helper/TagsSimilarHelper.php | 391 +- modules/mod_tags_similar/tmpl/default.php | 36 +- modules/mod_users_latest/mod_users_latest.php | 1 + .../src/Helper/UsersLatestHelper.php | 78 +- modules/mod_users_latest/tmpl/default.php | 15 +- modules/mod_whosonline/mod_whosonline.php | 28 +- .../src/Helper/WhosonlineHelper.php | 201 +- modules/mod_whosonline/tmpl/default.php | 35 +- modules/mod_whosonline/tmpl/disabled.php | 3 +- modules/mod_wrapper/mod_wrapper.php | 1 + .../mod_wrapper/src/Helper/WrapperHelper.php | 80 +- modules/mod_wrapper/tmpl/default.php | 19 +- .../actionlog/joomla/services/provider.php | 50 +- .../actionlog/joomla/src/Extension/Joomla.php | 2262 +- .../basic/services/provider.php | 52 +- .../basic/src/Extension/Basic.php | 200 +- .../token/services/provider.php | 54 +- .../token/src/Extension/Token.php | 732 +- plugins/authentication/cookie/cookie.php | 794 +- plugins/authentication/joomla/joomla.php | 151 +- plugins/authentication/ldap/ldap.php | 370 +- .../behaviour/taggable/services/provider.php | 48 +- .../taggable/src/Extension/Taggable.php | 637 +- .../versionable/services/provider.php | 54 +- .../versionable/src/Extension/Versionable.php | 247 +- plugins/captcha/recaptcha/recaptcha.php | 351 +- .../recaptcha_invisible.php | 353 +- .../content/confirmconsent/confirmconsent.php | 101 +- .../src/Field/ConsentBoxField.php | 626 +- plugins/content/contact/contact.php | 253 +- plugins/content/emailcloak/emailcloak.php | 812 +- plugins/content/fields/fields.php | 274 +- plugins/content/finder/finder.php | 187 +- plugins/content/joomla/joomla.php | 1209 +- plugins/content/loadmodule/loadmodule.php | 443 +- plugins/content/pagebreak/pagebreak.php | 688 +- plugins/content/pagebreak/tmpl/navigation.php | 45 +- plugins/content/pagebreak/tmpl/toc.php | 31 +- .../content/pagenavigation/pagenavigation.php | 452 +- .../content/pagenavigation/tmpl/default.php | 49 +- plugins/content/vote/tmpl/rating.php | 56 +- plugins/content/vote/tmpl/vote.php | 24 +- plugins/content/vote/vote.php | 242 +- plugins/editors-xtd/article/article.php | 98 +- plugins/editors-xtd/contact/contact.php | 96 +- plugins/editors-xtd/fields/fields.php | 107 +- plugins/editors-xtd/image/image.php | 300 +- plugins/editors-xtd/menu/menu.php | 98 +- plugins/editors-xtd/module/module.php | 98 +- plugins/editors-xtd/pagebreak/pagebreak.php | 100 +- plugins/editors-xtd/readmore/readmore.php | 91 +- plugins/editors/codemirror/codemirror.php | 594 +- .../layouts/editors/codemirror/element.php | 35 +- .../layouts/editors/codemirror/styles.php | 1 + .../codemirror/src/Field/FontsField.php | 56 +- plugins/editors/none/none.php | 153 +- .../tinymce/src/Field/TemplateslistField.php | 132 +- .../tinymce/src/Field/TinymcebuilderField.php | 304 +- .../tinymce/src/Field/UploaddirsField.php | 112 +- .../src/PluginTraits/ActiveSiteTemplate.php | 65 +- .../tinymce/src/PluginTraits/DisplayTrait.php | 1023 +- .../src/PluginTraits/GlobalFilters.php | 342 +- .../tinymce/src/PluginTraits/KnownButtons.php | 166 +- .../tinymce/src/PluginTraits/ResolveFiles.php | 184 +- .../src/PluginTraits/ToolbarPresets.php | 130 +- .../tinymce/src/PluginTraits/XTDButtons.php | 97 +- plugins/editors/tinymce/tinymce.php | 75 +- plugins/extension/finder/finder.php | 387 +- plugins/extension/joomla/joomla.php | 555 +- .../extension/namespacemap/namespacemap.php | 156 +- plugins/fields/calendar/calendar.php | 58 +- plugins/fields/calendar/tmpl/calendar.php | 11 +- plugins/fields/checkboxes/checkboxes.php | 82 +- plugins/fields/checkboxes/tmpl/checkboxes.php | 17 +- plugins/fields/color/color.php | 42 +- plugins/fields/color/tmpl/color.php | 11 +- plugins/fields/editor/editor.php | 44 +- plugins/fields/editor/tmpl/editor.php | 7 +- plugins/fields/imagelist/imagelist.php | 44 +- plugins/fields/imagelist/tmpl/imagelist.php | 75 +- plugins/fields/integer/integer.php | 1 + plugins/fields/integer/tmpl/integer.php | 17 +- plugins/fields/list/list.php | 95 +- plugins/fields/list/tmpl/list.php | 17 +- plugins/fields/radio/radio.php | 47 +- plugins/fields/radio/tmpl/radio.php | 17 +- plugins/fields/sql/sql.php | 96 +- plugins/fields/sql/tmpl/sql.php | 34 +- plugins/fields/subform/subform.php | 787 +- plugins/fields/subform/tmpl/subform.php | 78 +- plugins/fields/text/text.php | 1 + plugins/fields/text/tmpl/text.php | 11 +- plugins/fields/textarea/textarea.php | 1 + plugins/fields/textarea/tmpl/textarea.php | 7 +- plugins/fields/url/tmpl/url.php | 34 +- plugins/fields/url/url.php | 57 +- plugins/fields/user/tmpl/user.php | 34 +- plugins/fields/user/user.php | 40 +- .../usergrouplist/tmpl/usergrouplist.php | 17 +- .../fields/usergrouplist/usergrouplist.php | 1 + plugins/filesystem/local/local.php | 145 +- .../local/src/Adapter/LocalAdapter.php | 1613 +- plugins/finder/categories/categories.php | 884 +- plugins/finder/contacts/contacts.php | 827 +- plugins/finder/content/content.php | 722 +- plugins/finder/newsfeeds/newsfeeds.php | 677 +- plugins/finder/tags/tags.php | 653 +- .../folderinstaller/folderinstaller.php | 64 +- .../folderinstaller/tmpl/default.php | 39 +- plugins/installer/override/override.php | 772 +- .../packageinstaller/packageinstaller.php | 63 +- .../packageinstaller/tmpl/default.php | 139 +- .../installer/urlinstaller/tmpl/default.php | 25 +- .../installer/urlinstaller/urlinstaller.php | 64 +- .../installer/webinstaller/tmpl/default.php | 45 +- .../installer/webinstaller/webinstaller.php | 290 +- plugins/media-action/crop/crop.php | 49 +- plugins/media-action/resize/resize.php | 102 +- plugins/media-action/rotate/rotate.php | 1 + .../email/services/provider.php | 45 +- .../email/src/Extension/Email.php | 1081 +- .../fixed/services/provider.php | 41 +- .../fixed/src/Extension/Fixed.php | 519 +- .../totp/services/provider.php | 45 +- .../totp/src/Extension/Totp.php | 691 +- .../webauthn/services/provider.php | 45 +- .../webauthn/src/CredentialRepository.php | 444 +- .../webauthn/src/Extension/Webauthn.php | 812 +- .../webauthn/src/Helper/Credentials.php | 597 +- .../AndroidKeyAttestationStatementSupport.php | 414 +- .../FidoU2FAttestationStatementSupport.php | 345 +- .../webauthn/src/Hotfix/Server.php | 738 +- .../yubikey/services/provider.php | 45 +- .../yubikey/src/Extension/Yubikey.php | 1179 +- plugins/privacy/consents/consents.php | 75 +- plugins/privacy/contact/contact.php | 86 +- plugins/privacy/content/content.php | 71 +- plugins/privacy/message/message.php | 77 +- plugins/privacy/user/user.php | 428 +- plugins/quickicon/downloadkey/downloadkey.php | 216 +- .../extensionupdate/extensionupdate.php | 128 +- .../joomlaupdate/services/provider.php | 54 +- .../src/Extension/Joomlaupdate.php | 216 +- .../quickicon/overridecheck/overridecheck.php | 175 +- .../phpversioncheck/phpversioncheck.php | 436 +- .../quickicon/privacycheck/privacycheck.php | 118 +- plugins/sampledata/blog/blog.php | 3777 ++-- plugins/sampledata/multilang/multilang.php | 2660 ++- plugins/sampledata/testing/testing.php | 9352 ++++----- .../system/accessibility/accessibility.php | 174 +- plugins/system/debug/debug.php | 1354 +- .../debug/src/AbstractDataCollector.php | 157 +- .../debug/src/DataCollector/InfoCollector.php | 368 +- .../DataCollector/LanguageErrorsCollector.php | 234 +- .../DataCollector/LanguageFilesCollector.php | 188 +- .../LanguageStringsCollector.php | 324 +- .../src/DataCollector/ProfileCollector.php | 588 +- .../src/DataCollector/QueryCollector.php | 465 +- .../src/DataCollector/SessionCollector.php | 108 +- plugins/system/debug/src/DataFormatter.php | 113 +- .../system/debug/src/JavascriptRenderer.php | 224 +- plugins/system/debug/src/JoomlaHttpDriver.php | 247 +- .../system/debug/src/Storage/FileStorage.php | 239 +- plugins/system/fields/fields.php | 963 +- plugins/system/highlight/highlight.php | 221 +- plugins/system/httpheaders/httpheaders.php | 900 +- .../httpheaders/postinstall/introduction.php | 49 +- plugins/system/jooa11y/jooa11y.php | 465 +- plugins/system/languagecode/languagecode.php | 235 +- .../system/languagefilter/languagefilter.php | 1708 +- plugins/system/log/log.php | 84 +- plugins/system/logout/logout.php | 134 +- plugins/system/logrotation/logrotation.php | 466 +- .../system/privacyconsent/privacyconsent.php | 1392 +- .../privacyconsent/src/Field/PrivacyField.php | 210 +- plugins/system/redirect/redirect.php | 523 +- plugins/system/remember/remember.php | 235 +- .../system/schedulerunner/schedulerunner.php | 677 +- plugins/system/sef/sef.php | 419 +- plugins/system/sessiongc/sessiongc.php | 89 +- plugins/system/shortcut/services/provider.php | 50 +- .../shortcut/src/Extension/Shortcut.php | 218 +- plugins/system/skipto/skipto.php | 172 +- plugins/system/stats/layouts/field/data.php | 7 +- .../system/stats/layouts/field/uniqueid.php | 3 +- plugins/system/stats/layouts/message.php | 39 +- plugins/system/stats/layouts/stats.php | 43 +- .../stats/src/Field/AbstractStatsField.php | 33 +- plugins/system/stats/src/Field/DataField.php | 71 +- .../system/stats/src/Field/UniqueidField.php | 29 +- plugins/system/stats/stats.php | 1206 +- .../tasknotification/tasknotification.php | 573 +- .../postinstall/updatecachetime.php | 45 +- .../updatenotification/updatenotification.php | 717 +- plugins/system/webauthn/services/provider.php | 104 +- .../system/webauthn/src/Authentication.php | 1026 +- .../webauthn/src/CredentialRepository.php | 1283 +- .../webauthn/src/Extension/Webauthn.php | 273 +- .../webauthn/src/Field/WebauthnField.php | 91 +- .../AndroidKeyAttestationStatementSupport.php | 414 +- .../FidoU2FAttestationStatementSupport.php | 345 +- plugins/system/webauthn/src/Hotfix/Server.php | 738 +- .../webauthn/src/MetadataRepository.php | 399 +- .../PluginTraits/AdditionalLoginButtons.php | 349 +- .../webauthn/src/PluginTraits/AjaxHandler.php | 297 +- .../src/PluginTraits/AjaxHandlerChallenge.php | 155 +- .../src/PluginTraits/AjaxHandlerCreate.php | 167 +- .../src/PluginTraits/AjaxHandlerDelete.php | 130 +- .../PluginTraits/AjaxHandlerInitCreate.php | 67 +- .../src/PluginTraits/AjaxHandlerLogin.php | 542 +- .../src/PluginTraits/AjaxHandlerSaveLabel.php | 149 +- .../src/PluginTraits/EventReturnAware.php | 44 +- .../src/PluginTraits/UserDeletion.php | 84 +- .../src/PluginTraits/UserProfileFields.php | 382 +- plugins/task/checkfiles/checkfiles.php | 222 +- plugins/task/demotasks/services/provider.php | 50 +- .../demotasks/src/Extension/DemoTasks.php | 396 +- plugins/task/requests/services/provider.php | 52 +- .../task/requests/src/Extension/Requests.php | 261 +- plugins/task/sitestatus/services/provider.php | 52 +- .../sitestatus/src/Extension/SiteStatus.php | 325 +- .../user/contactcreator/contactcreator.php | 310 +- plugins/user/joomla/joomla.php | 984 +- plugins/user/profile/profile.php | 901 +- plugins/user/profile/src/Field/TosField.php | 233 +- plugins/user/terms/src/Field/TermsField.php | 195 +- plugins/user/terms/terms.php | 268 +- .../user/token/src/Field/JoomlatokenField.php | 273 +- plugins/user/token/token.php | 1241 +- plugins/webservices/banners/banners.php | 119 +- plugins/webservices/config/config.php | 57 +- plugins/webservices/contact/contact.php | 241 +- plugins/webservices/content/content.php | 175 +- plugins/webservices/installer/installer.php | 49 +- plugins/webservices/languages/languages.php | 214 +- plugins/webservices/menus/menus.php | 107 +- plugins/webservices/messages/messages.php | 49 +- plugins/webservices/modules/modules.php | 87 +- plugins/webservices/newsfeeds/newsfeeds.php | 59 +- plugins/webservices/plugins/plugins.php | 55 +- plugins/webservices/privacy/privacy.php | 79 +- plugins/webservices/redirect/redirect.php | 49 +- plugins/webservices/tags/tags.php | 49 +- plugins/webservices/templates/templates.php | 59 +- plugins/webservices/users/users.php | 115 +- plugins/workflow/featuring/featuring.php | 989 +- .../workflow/notification/notification.php | 624 +- plugins/workflow/publishing/publishing.php | 1013 +- templates/cassiopeia/component.php | 52 +- templates/cassiopeia/error.php | 241 +- .../cassiopeia/html/layouts/chromes/card.php | 41 +- .../html/layouts/chromes/noCard.php | 34 +- .../cassiopeia/html/mod_custom/banner.php | 16 +- .../html/mod_menu/collapse-metismenu.php | 13 +- .../html/mod_menu/dropdown-metismenu.php | 170 +- .../mod_menu/dropdown-metismenu_component.php | 98 +- .../mod_menu/dropdown-metismenu_heading.php | 75 +- .../mod_menu/dropdown-metismenu_separator.php | 75 +- .../html/mod_menu/dropdown-metismenu_url.php | 95 +- templates/cassiopeia/index.php | 311 +- templates/cassiopeia/offline.php | 225 +- templates/system/component.php | 7 +- templates/system/error.php | 129 +- templates/system/fatal.php | 19 +- templates/system/index.php | 1 + templates/system/offline.php | 84 +- 2572 files changed, 334991 insertions(+), 355437 deletions(-) diff --git a/administrator/components/com_actionlogs/services/provider.php b/administrator/components/com_actionlogs/services/provider.php index 0cad8c5826d5d..3d81708bd9fe0 100644 --- a/administrator/components/com_actionlogs/services/provider.php +++ b/administrator/components/com_actionlogs/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Actionlogs')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Actionlogs')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Actionlogs')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Actionlogs')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_actionlogs/src/Controller/ActionlogsController.php b/administrator/components/com_actionlogs/src/Controller/ActionlogsController.php index 6b429f54278e3..129f244ff7326 100644 --- a/administrator/components/com_actionlogs/src/Controller/ActionlogsController.php +++ b/administrator/components/com_actionlogs/src/Controller/ActionlogsController.php @@ -1,4 +1,5 @@ registerTask('exportSelectedLogs', 'exportLogs'); - } - - /** - * Method to export logs - * - * @return void - * - * @since 3.9.0 - * - * @throws Exception - */ - public function exportLogs() - { - // Check for request forgeries. - $this->checkToken(); - - $task = $this->getTask(); - - $pks = array(); - - if ($task == 'exportSelectedLogs') - { - // Get selected logs - $pks = ArrayHelper::toInteger(explode(',', $this->input->post->getString('cids'))); - } - - /** @var ActionlogsModel $model */ - $model = $this->getModel(); - - // Get the logs data - $data = $model->getLogDataAsIterator($pks); - - if (\count($data)) - { - try - { - $rows = ActionlogsHelper::getCsvData($data); - } - catch (InvalidArgumentException $exception) - { - $this->setMessage(Text::_('COM_ACTIONLOGS_ERROR_COULD_NOT_EXPORT_DATA'), 'error'); - $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false)); - - return; - } - - // Destroy the iterator now - unset($data); - - $date = new Date('now', new DateTimeZone('UTC')); - $filename = 'logs_' . $date->format('Y-m-d_His_T'); - - $csvDelimiter = ComponentHelper::getComponent('com_actionlogs')->getParams()->get('csv_delimiter', ','); - - $this->app->setHeader('Content-Type', 'application/csv', true) - ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '.csv"', true) - ->setHeader('Cache-Control', 'must-revalidate', true) - ->sendHeaders(); - - $output = fopen("php://output", "w"); - - foreach ($rows as $row) - { - fputcsv($output, $row, $csvDelimiter); - } - - fclose($output); - $this->app->triggerEvent('onAfterLogExport', array()); - $this->app->close(); - } - else - { - $this->setMessage(Text::_('COM_ACTIONLOGS_NO_LOGS_TO_EXPORT')); - $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false)); - } - } - - /** - * Method to get a model object, loading it if required. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return object The model. - * - * @since 3.9.0 - */ - public function getModel($name = 'Actionlogs', $prefix = 'Administrator', $config = ['ignore_request' => true]) - { - // Return the model - return parent::getModel($name, $prefix, $config); - } - - /** - * Clean out the logs - * - * @return void - * - * @since 3.9.0 - */ - public function purge() - { - // Check for request forgeries. - $this->checkToken(); - - $model = $this->getModel(); - - if ($model->purge()) - { - $message = Text::_('COM_ACTIONLOGS_PURGE_SUCCESS'); - } - else - { - $message = Text::_('COM_ACTIONLOGS_PURGE_FAIL'); - } - - $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false), $message); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 3.9.0 + * + * @throws Exception + */ + public function __construct($config = [], MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('exportSelectedLogs', 'exportLogs'); + } + + /** + * Method to export logs + * + * @return void + * + * @since 3.9.0 + * + * @throws Exception + */ + public function exportLogs() + { + // Check for request forgeries. + $this->checkToken(); + + $task = $this->getTask(); + + $pks = array(); + + if ($task == 'exportSelectedLogs') { + // Get selected logs + $pks = ArrayHelper::toInteger(explode(',', $this->input->post->getString('cids'))); + } + + /** @var ActionlogsModel $model */ + $model = $this->getModel(); + + // Get the logs data + $data = $model->getLogDataAsIterator($pks); + + if (\count($data)) { + try { + $rows = ActionlogsHelper::getCsvData($data); + } catch (InvalidArgumentException $exception) { + $this->setMessage(Text::_('COM_ACTIONLOGS_ERROR_COULD_NOT_EXPORT_DATA'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false)); + + return; + } + + // Destroy the iterator now + unset($data); + + $date = new Date('now', new DateTimeZone('UTC')); + $filename = 'logs_' . $date->format('Y-m-d_His_T'); + + $csvDelimiter = ComponentHelper::getComponent('com_actionlogs')->getParams()->get('csv_delimiter', ','); + + $this->app->setHeader('Content-Type', 'application/csv', true) + ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '.csv"', true) + ->setHeader('Cache-Control', 'must-revalidate', true) + ->sendHeaders(); + + $output = fopen("php://output", "w"); + + foreach ($rows as $row) { + fputcsv($output, $row, $csvDelimiter); + } + + fclose($output); + $this->app->triggerEvent('onAfterLogExport', array()); + $this->app->close(); + } else { + $this->setMessage(Text::_('COM_ACTIONLOGS_NO_LOGS_TO_EXPORT')); + $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false)); + } + } + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 3.9.0 + */ + public function getModel($name = 'Actionlogs', $prefix = 'Administrator', $config = ['ignore_request' => true]) + { + // Return the model + return parent::getModel($name, $prefix, $config); + } + + /** + * Clean out the logs + * + * @return void + * + * @since 3.9.0 + */ + public function purge() + { + // Check for request forgeries. + $this->checkToken(); + + $model = $this->getModel(); + + if ($model->purge()) { + $message = Text::_('COM_ACTIONLOGS_PURGE_SUCCESS'); + } else { + $message = Text::_('COM_ACTIONLOGS_PURGE_FAIL'); + } + + $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false), $message); + } } diff --git a/administrator/components/com_actionlogs/src/Controller/DisplayController.php b/administrator/components/com_actionlogs/src/Controller/DisplayController.php index 4c51bfe7ddbe5..9dbe28cfe7ec4 100644 --- a/administrator/components/com_actionlogs/src/Controller/DisplayController.php +++ b/administrator/components/com_actionlogs/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('extension')) - ->from($db->quoteName('#__action_logs')) - ->order($db->quoteName('extension')); + /** + * Method to get the options to populate list + * + * @return array The field option objects. + * + * @since 3.9.0 + */ + public function getOptions() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('extension')) + ->from($db->quoteName('#__action_logs')) + ->order($db->quoteName('extension')); - $db->setQuery($query); - $context = $db->loadColumn(); + $db->setQuery($query); + $context = $db->loadColumn(); - $options = array(); + $options = array(); - if (\count($context) > 0) - { - foreach ($context as $item) - { - $extensions[] = strtok($item, '.'); - } + if (\count($context) > 0) { + foreach ($context as $item) { + $extensions[] = strtok($item, '.'); + } - $extensions = array_unique($extensions); + $extensions = array_unique($extensions); - foreach ($extensions as $extension) - { - ActionlogsHelper::loadTranslationFiles($extension); - $options[] = HTMLHelper::_('select.option', $extension, Text::_($extension)); - } - } + foreach ($extensions as $extension) { + ActionlogsHelper::loadTranslationFiles($extension); + $options[] = HTMLHelper::_('select.option', $extension, Text::_($extension)); + } + } - return array_merge(parent::getOptions(), $options); - } + return array_merge(parent::getOptions(), $options); + } } diff --git a/administrator/components/com_actionlogs/src/Field/LogcreatorField.php b/administrator/components/com_actionlogs/src/Field/LogcreatorField.php index 0db00673751ca..856066be857de 100644 --- a/administrator/components/com_actionlogs/src/Field/LogcreatorField.php +++ b/administrator/components/com_actionlogs/src/Field/LogcreatorField.php @@ -1,4 +1,5 @@ element); + /** + * Method to get the options to populate list + * + * @return array The field option objects. + * + * @since 3.9.0 + */ + protected function getOptions() + { + // Accepted modifiers + $hash = md5($this->element); - if (!isset(static::$options[$hash])) - { - static::$options[$hash] = parent::getOptions(); + if (!isset(static::$options[$hash])) { + static::$options[$hash] = parent::getOptions(); - $db = $this->getDatabase(); + $db = $this->getDatabase(); - // Construct the query - $query = $db->getQuery(true) - ->select($db->quoteName('u.id', 'value')) - ->select($db->quoteName('u.username', 'text')) - ->from($db->quoteName('#__users', 'u')) - ->join('INNER', $db->quoteName('#__action_logs', 'c') . ' ON ' . $db->quoteName('c.user_id') . ' = ' . $db->quoteName('u.id')) - ->group($db->quoteName('u.id')) - ->group($db->quoteName('u.username')) - ->order($db->quoteName('u.username')); + // Construct the query + $query = $db->getQuery(true) + ->select($db->quoteName('u.id', 'value')) + ->select($db->quoteName('u.username', 'text')) + ->from($db->quoteName('#__users', 'u')) + ->join('INNER', $db->quoteName('#__action_logs', 'c') . ' ON ' . $db->quoteName('c.user_id') . ' = ' . $db->quoteName('u.id')) + ->group($db->quoteName('u.id')) + ->group($db->quoteName('u.username')) + ->order($db->quoteName('u.username')); - // Setup the query - $db->setQuery($query); + // Setup the query + $db->setQuery($query); - // Return the result - if ($options = $db->loadObjectList()) - { - static::$options[$hash] = array_merge(static::$options[$hash], $options); - } - } + // Return the result + if ($options = $db->loadObjectList()) { + static::$options[$hash] = array_merge(static::$options[$hash], $options); + } + } - return static::$options[$hash]; - } + return static::$options[$hash]; + } } diff --git a/administrator/components/com_actionlogs/src/Field/LogsdaterangeField.php b/administrator/components/com_actionlogs/src/Field/LogsdaterangeField.php index 89f94ac6f2d61..ccd29b534d340 100644 --- a/administrator/components/com_actionlogs/src/Field/LogsdaterangeField.php +++ b/administrator/components/com_actionlogs/src/Field/LogsdaterangeField.php @@ -1,4 +1,5 @@ 'COM_ACTIONLOGS_OPTION_RANGE_TODAY', - 'past_week' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_WEEK', - 'past_1month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_1MONTH', - 'past_3month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_3MONTH', - 'past_6month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_6MONTH', - 'past_year' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_YEAR', - ); - - /** - * Method to instantiate the form field object. - * - * @param Form $form The form to attach to the form field object. - * - * @since 3.9.0 - */ - public function __construct($form = null) - { - parent::__construct($form); - - // Load the required language - $lang = Factory::getLanguage(); - $lang->load('com_actionlogs', JPATH_ADMINISTRATOR); - } + /** + * The form field type. + * + * @var string + * @since 3.9.0 + */ + protected $type = 'logsdaterange'; + + /** + * Available options + * + * @var array + * @since 3.9.0 + */ + protected $predefinedOptions = array( + 'today' => 'COM_ACTIONLOGS_OPTION_RANGE_TODAY', + 'past_week' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_WEEK', + 'past_1month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_1MONTH', + 'past_3month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_3MONTH', + 'past_6month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_6MONTH', + 'past_year' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_YEAR', + ); + + /** + * Method to instantiate the form field object. + * + * @param Form $form The form to attach to the form field object. + * + * @since 3.9.0 + */ + public function __construct($form = null) + { + parent::__construct($form); + + // Load the required language + $lang = Factory::getLanguage(); + $lang->load('com_actionlogs', JPATH_ADMINISTRATOR); + } } diff --git a/administrator/components/com_actionlogs/src/Field/LogtypeField.php b/administrator/components/com_actionlogs/src/Field/LogtypeField.php index 6bffb3de8267b..bde6381e1fe5d 100644 --- a/administrator/components/com_actionlogs/src/Field/LogtypeField.php +++ b/administrator/components/com_actionlogs/src/Field/LogtypeField.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('extension')) - ->from($db->quoteName('#__action_logs_extensions')); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.9.0 + */ + public function getOptions() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension')) + ->from($db->quoteName('#__action_logs_extensions')); - $extensions = $db->setQuery($query)->loadColumn(); + $extensions = $db->setQuery($query)->loadColumn(); - $options = []; + $options = []; - foreach ($extensions as $extension) - { - ActionlogsHelper::loadTranslationFiles($extension); - $extensionName = Text::_($extension); - $options[ApplicationHelper::stringURLSafe($extensionName) . '_' . $extension] = HTMLHelper::_('select.option', $extension, $extensionName); - } + foreach ($extensions as $extension) { + ActionlogsHelper::loadTranslationFiles($extension); + $extensionName = Text::_($extension); + $options[ApplicationHelper::stringURLSafe($extensionName) . '_' . $extension] = HTMLHelper::_('select.option', $extension, $extensionName); + } - ksort($options); + ksort($options); - return array_merge(parent::getOptions(), array_values($options)); - } + return array_merge(parent::getOptions(), array_values($options)); + } } diff --git a/administrator/components/com_actionlogs/src/Field/PlugininfoField.php b/administrator/components/com_actionlogs/src/Field/PlugininfoField.php index 6b225cee279e7..78494cf8852af 100644 --- a/administrator/components/com_actionlogs/src/Field/PlugininfoField.php +++ b/administrator/components/com_actionlogs/src/Field/PlugininfoField.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('actionlog')) - ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')); - $db->setQuery($query); + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 3.9.2 + */ + protected function getInput() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('actionlog')) + ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')); + $db->setQuery($query); - $result = (int) $db->loadResult(); + $result = (int) $db->loadResult(); - $link = HTMLHelper::_( - 'link', - Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $result), - Text::_('PLG_SYSTEM_ACTIONLOGS_JOOMLA_ACTIONLOG_DISABLED'), - array('class' => 'alert-link') - ); + $link = HTMLHelper::_( + 'link', + Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $result), + Text::_('PLG_SYSTEM_ACTIONLOGS_JOOMLA_ACTIONLOG_DISABLED'), + array('class' => 'alert-link') + ); - return '
    ' - . '' - . Text::_('INFO') - . '' - . Text::sprintf('PLG_SYSTEM_ACTIONLOGS_JOOMLA_ACTIONLOG_DISABLED_REDIRECT', $link) - . '
    '; - } + return '
    ' + . '' + . Text::_('INFO') + . '' + . Text::sprintf('PLG_SYSTEM_ACTIONLOGS_JOOMLA_ACTIONLOG_DISABLED_REDIRECT', $link) + . '
    '; + } } diff --git a/administrator/components/com_actionlogs/src/Helper/ActionlogsHelper.php b/administrator/components/com_actionlogs/src/Helper/ActionlogsHelper.php index 309ed9d5dcc11..eb311ae6de4fa 100644 --- a/administrator/components/com_actionlogs/src/Helper/ActionlogsHelper.php +++ b/administrator/components/com_actionlogs/src/Helper/ActionlogsHelper.php @@ -1,4 +1,5 @@ extension, '.'); - - static::loadTranslationFiles($extension); - - yield array( - 'id' => $log->id, - 'message' => self::escapeCsvFormula(strip_tags(static::getHumanReadableLogMessage($log, false))), - 'extension' => self::escapeCsvFormula(Text::_($extension)), - 'date' => (new Date($log->log_date, new \DateTimeZone('UTC')))->format('Y-m-d H:i:s T'), - 'name' => self::escapeCsvFormula($log->name), - 'ip_address' => self::escapeCsvFormula($log->ip_address === 'COM_ACTIONLOGS_DISABLED' ? $disabledText : $log->ip_address) - ); - } - } - - /** - * Load the translation files for an extension - * - * @param string $extension Extension name - * - * @return void - * - * @since 3.9.0 - */ - public static function loadTranslationFiles($extension) - { - static $cache = array(); - $extension = strtolower($extension); - - if (isset($cache[$extension])) - { - return; - } - - $lang = Factory::getLanguage(); - $source = ''; - - switch (substr($extension, 0, 3)) - { - case 'com': - default: - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - break; - - case 'lib': - $source = JPATH_LIBRARIES . '/' . substr($extension, 4); - break; - - case 'mod': - $source = JPATH_SITE . '/modules/' . $extension; - break; - - case 'plg': - $parts = explode('_', $extension, 3); - - if (\count($parts) > 2) - { - $source = JPATH_PLUGINS . '/' . $parts[1] . '/' . $parts[2]; - } - break; - - case 'pkg': - $source = JPATH_SITE; - break; - - case 'tpl': - $source = JPATH_BASE . '/templates/' . substr($extension, 4); - break; - - } - - $lang->load($extension, JPATH_ADMINISTRATOR) - || $lang->load($extension, $source); - - if (!$lang->hasKey(strtoupper($extension))) - { - $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($extension . '.sys', $source); - } - - $cache[$extension] = true; - } - - /** - * Get parameters to be - * - * @param string $context The context of the content - * - * @return mixed An object contains content type parameters, or null if not found - * - * @since 3.9.0 - * - * @deprecated 5.0 Use the action log config model instead - */ - public static function getLogContentTypeParams($context) - { - return Factory::getApplication()->bootComponent('actionlogs')->getMVCFactory() - ->createModel('ActionlogConfig', 'Administrator')->getLogContentTypeParams($context); - } - - /** - * Get human readable log message for a User Action Log - * - * @param \stdClass $log A User Action log message record - * @param boolean $generateLinks Flag to disable link generation when creating a message - * - * @return string - * - * @since 3.9.0 - */ - public static function getHumanReadableLogMessage($log, $generateLinks = true) - { - static $links = array(); - - $message = Text::_($log->message_language_key); - $messageData = json_decode($log->message, true); - - // Special handling for translation extension name - if (isset($messageData['extension_name'])) - { - static::loadTranslationFiles($messageData['extension_name']); - $messageData['extension_name'] = Text::_($messageData['extension_name']); - } - - // Translating application - if (isset($messageData['app'])) - { - $messageData['app'] = Text::_($messageData['app']); - } - - // Translating type - if (isset($messageData['type'])) - { - $messageData['type'] = Text::_($messageData['type']); - } - - $linkMode = Factory::getApplication()->get('force_ssl', 0) >= 1 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - foreach ($messageData as $key => $value) - { - // Escape any markup in the values to prevent XSS attacks - $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); - - // Convert relative url to absolute url so that it is clickable in action logs notification email - if ($generateLinks && StringHelper::strpos($value, 'index.php?') === 0) - { - if (!isset($links[$value])) - { - $links[$value] = Route::link('administrator', $value, false, $linkMode, true); - } - - $value = $links[$value]; - } - - $message = str_replace('{' . $key . '}', $value, $message); - } - - return $message; - } - - /** - * Get link to an item of given content type - * - * @param string $component - * @param string $contentType - * @param integer $id - * @param string $urlVar - * @param CMSObject $object - * - * @return string Link to the content item - * - * @since 3.9.0 - */ - public static function getContentTypeLink($component, $contentType, $id, $urlVar = 'id', $object = null) - { - // Try to find the component helper. - $eName = str_replace('com_', '', $component); - $file = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/helpers/' . $eName . '.php'); - - if (file_exists($file)) - { - $prefix = ucfirst(str_replace('com_', '', $component)); - $cName = $prefix . 'Helper'; - - \JLoader::register($cName, $file); - - if (class_exists($cName) && \is_callable(array($cName, 'getContentTypeLink'))) - { - return $cName::getContentTypeLink($contentType, $id, $object); - } - } - - if (empty($urlVar)) - { - $urlVar = 'id'; - } - - // Return default link to avoid having to implement getContentTypeLink in most of our components - return 'index.php?option=' . $component . '&task=' . $contentType . '.edit&' . $urlVar . '=' . $id; - } - - /** - * Load both enabled and disabled actionlog plugins language file. - * - * It is used to make sure actions log is displayed properly instead of only language items displayed when a plugin is disabled. - * - * @return void - * - * @since 3.9.0 - */ - public static function loadActionLogPluginsLanguage() - { - $lang = Factory::getLanguage(); - $db = Factory::getDbo(); - - // Get all (both enabled and disabled) actionlog plugins - $query = $db->getQuery(true) - ->select( - $db->quoteName( - array( - 'folder', - 'element', - 'params', - 'extension_id' - ), - array( - 'type', - 'name', - 'params', - 'id' - ) - ) - ) - ->from('#__extensions') - ->where('type = ' . $db->quote('plugin')) - ->where('folder = ' . $db->quote('actionlog')) - ->where('state IN (0,1)') - ->order('ordering'); - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $rows = array(); - } - - if (empty($rows)) - { - return; - } - - foreach ($rows as $row) - { - $name = $row->name; - $type = $row->type; - $extension = 'Plg_' . $type . '_' . $name; - $extension = strtolower($extension); - - // If language already loaded, don't load it again. - if ($lang->getPaths($extension)) - { - continue; - } - - $lang->load($extension, JPATH_ADMINISTRATOR) - || $lang->load($extension, JPATH_PLUGINS . '/' . $type . '/' . $name); - } - - // Load plg_system_actionlogs too - $lang->load('plg_system_actionlogs', JPATH_ADMINISTRATOR); - - // Load com_privacy too. - $lang->load('com_privacy', JPATH_ADMINISTRATOR); - } - - /** - * Escapes potential characters that start a formula in a CSV value to prevent injection attacks - * - * @param mixed $value csv field value - * - * @return mixed - * - * @since 3.9.7 - */ - protected static function escapeCsvFormula($value) - { - if ($value == '') - { - return $value; - } - - if (\in_array($value[0], self::$characters, true)) - { - $value = ' ' . $value; - } - - return $value; - } + /** + * Array of characters starting a formula + * + * @var array + * + * @since 3.9.7 + */ + private static $characters = array('=', '+', '-', '@'); + + /** + * Method to convert logs objects array to an iterable type for use with a CSV export + * + * @param array|\Traversable $data The logs data objects to be exported + * + * @return Generator + * + * @since 3.9.0 + * + * @throws \InvalidArgumentException + */ + public static function getCsvData($data): Generator + { + if (!is_iterable($data)) { + throw new \InvalidArgumentException( + sprintf( + '%s() requires an array or object implementing the Traversable interface, a %s was given.', + __METHOD__, + \gettype($data) === 'object' ? \get_class($data) : \gettype($data) + ) + ); + } + + $disabledText = Text::_('COM_ACTIONLOGS_DISABLED'); + + // Header row + yield ['Id', 'Action', 'Extension', 'Date', 'Name', 'IP Address']; + + foreach ($data as $log) { + $extension = strtok($log->extension, '.'); + + static::loadTranslationFiles($extension); + + yield array( + 'id' => $log->id, + 'message' => self::escapeCsvFormula(strip_tags(static::getHumanReadableLogMessage($log, false))), + 'extension' => self::escapeCsvFormula(Text::_($extension)), + 'date' => (new Date($log->log_date, new \DateTimeZone('UTC')))->format('Y-m-d H:i:s T'), + 'name' => self::escapeCsvFormula($log->name), + 'ip_address' => self::escapeCsvFormula($log->ip_address === 'COM_ACTIONLOGS_DISABLED' ? $disabledText : $log->ip_address) + ); + } + } + + /** + * Load the translation files for an extension + * + * @param string $extension Extension name + * + * @return void + * + * @since 3.9.0 + */ + public static function loadTranslationFiles($extension) + { + static $cache = array(); + $extension = strtolower($extension); + + if (isset($cache[$extension])) { + return; + } + + $lang = Factory::getLanguage(); + $source = ''; + + switch (substr($extension, 0, 3)) { + case 'com': + default: + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + break; + + case 'lib': + $source = JPATH_LIBRARIES . '/' . substr($extension, 4); + break; + + case 'mod': + $source = JPATH_SITE . '/modules/' . $extension; + break; + + case 'plg': + $parts = explode('_', $extension, 3); + + if (\count($parts) > 2) { + $source = JPATH_PLUGINS . '/' . $parts[1] . '/' . $parts[2]; + } + break; + + case 'pkg': + $source = JPATH_SITE; + break; + + case 'tpl': + $source = JPATH_BASE . '/templates/' . substr($extension, 4); + break; + } + + $lang->load($extension, JPATH_ADMINISTRATOR) + || $lang->load($extension, $source); + + if (!$lang->hasKey(strtoupper($extension))) { + $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($extension . '.sys', $source); + } + + $cache[$extension] = true; + } + + /** + * Get parameters to be + * + * @param string $context The context of the content + * + * @return mixed An object contains content type parameters, or null if not found + * + * @since 3.9.0 + * + * @deprecated 5.0 Use the action log config model instead + */ + public static function getLogContentTypeParams($context) + { + return Factory::getApplication()->bootComponent('actionlogs')->getMVCFactory() + ->createModel('ActionlogConfig', 'Administrator')->getLogContentTypeParams($context); + } + + /** + * Get human readable log message for a User Action Log + * + * @param \stdClass $log A User Action log message record + * @param boolean $generateLinks Flag to disable link generation when creating a message + * + * @return string + * + * @since 3.9.0 + */ + public static function getHumanReadableLogMessage($log, $generateLinks = true) + { + static $links = array(); + + $message = Text::_($log->message_language_key); + $messageData = json_decode($log->message, true); + + // Special handling for translation extension name + if (isset($messageData['extension_name'])) { + static::loadTranslationFiles($messageData['extension_name']); + $messageData['extension_name'] = Text::_($messageData['extension_name']); + } + + // Translating application + if (isset($messageData['app'])) { + $messageData['app'] = Text::_($messageData['app']); + } + + // Translating type + if (isset($messageData['type'])) { + $messageData['type'] = Text::_($messageData['type']); + } + + $linkMode = Factory::getApplication()->get('force_ssl', 0) >= 1 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + foreach ($messageData as $key => $value) { + // Escape any markup in the values to prevent XSS attacks + $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + + // Convert relative url to absolute url so that it is clickable in action logs notification email + if ($generateLinks && StringHelper::strpos($value, 'index.php?') === 0) { + if (!isset($links[$value])) { + $links[$value] = Route::link('administrator', $value, false, $linkMode, true); + } + + $value = $links[$value]; + } + + $message = str_replace('{' . $key . '}', $value, $message); + } + + return $message; + } + + /** + * Get link to an item of given content type + * + * @param string $component + * @param string $contentType + * @param integer $id + * @param string $urlVar + * @param CMSObject $object + * + * @return string Link to the content item + * + * @since 3.9.0 + */ + public static function getContentTypeLink($component, $contentType, $id, $urlVar = 'id', $object = null) + { + // Try to find the component helper. + $eName = str_replace('com_', '', $component); + $file = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/helpers/' . $eName . '.php'); + + if (file_exists($file)) { + $prefix = ucfirst(str_replace('com_', '', $component)); + $cName = $prefix . 'Helper'; + + \JLoader::register($cName, $file); + + if (class_exists($cName) && \is_callable(array($cName, 'getContentTypeLink'))) { + return $cName::getContentTypeLink($contentType, $id, $object); + } + } + + if (empty($urlVar)) { + $urlVar = 'id'; + } + + // Return default link to avoid having to implement getContentTypeLink in most of our components + return 'index.php?option=' . $component . '&task=' . $contentType . '.edit&' . $urlVar . '=' . $id; + } + + /** + * Load both enabled and disabled actionlog plugins language file. + * + * It is used to make sure actions log is displayed properly instead of only language items displayed when a plugin is disabled. + * + * @return void + * + * @since 3.9.0 + */ + public static function loadActionLogPluginsLanguage() + { + $lang = Factory::getLanguage(); + $db = Factory::getDbo(); + + // Get all (both enabled and disabled) actionlog plugins + $query = $db->getQuery(true) + ->select( + $db->quoteName( + array( + 'folder', + 'element', + 'params', + 'extension_id' + ), + array( + 'type', + 'name', + 'params', + 'id' + ) + ) + ) + ->from('#__extensions') + ->where('type = ' . $db->quote('plugin')) + ->where('folder = ' . $db->quote('actionlog')) + ->where('state IN (0,1)') + ->order('ordering'); + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $rows = array(); + } + + if (empty($rows)) { + return; + } + + foreach ($rows as $row) { + $name = $row->name; + $type = $row->type; + $extension = 'Plg_' . $type . '_' . $name; + $extension = strtolower($extension); + + // If language already loaded, don't load it again. + if ($lang->getPaths($extension)) { + continue; + } + + $lang->load($extension, JPATH_ADMINISTRATOR) + || $lang->load($extension, JPATH_PLUGINS . '/' . $type . '/' . $name); + } + + // Load plg_system_actionlogs too + $lang->load('plg_system_actionlogs', JPATH_ADMINISTRATOR); + + // Load com_privacy too. + $lang->load('com_privacy', JPATH_ADMINISTRATOR); + } + + /** + * Escapes potential characters that start a formula in a CSV value to prevent injection attacks + * + * @param mixed $value csv field value + * + * @return mixed + * + * @since 3.9.7 + */ + protected static function escapeCsvFormula($value) + { + if ($value == '') { + return $value; + } + + if (\in_array($value[0], self::$characters, true)) { + $value = ' ' . $value; + } + + return $value; + } } diff --git a/administrator/components/com_actionlogs/src/Model/ActionlogConfigModel.php b/administrator/components/com_actionlogs/src/Model/ActionlogConfigModel.php index ec6149c81d666..ac1107a788620 100644 --- a/administrator/components/com_actionlogs/src/Model/ActionlogConfigModel.php +++ b/administrator/components/com_actionlogs/src/Model/ActionlogConfigModel.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select('a.*') - ->from($db->quoteName('#__action_log_config', 'a')) - ->where($db->quoteName('a.type_alias') . ' = :context') - ->bind(':context', $context); + /** + * Returns the action logs config for the given context. + * + * @param string $context The context of the content + * + * @return stdClass|null An object contains content type parameters, or null if not found + * + * @since 4.2.0 + */ + public function getLogContentTypeParams(string $context): ?stdClass + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('a.*') + ->from($db->quoteName('#__action_log_config', 'a')) + ->where($db->quoteName('a.type_alias') . ' = :context') + ->bind(':context', $context); - $db->setQuery($query); + $db->setQuery($query); - return $db->loadObject(); - } + return $db->loadObject(); + } } diff --git a/administrator/components/com_actionlogs/src/Model/ActionlogModel.php b/administrator/components/com_actionlogs/src/Model/ActionlogModel.php index 97fc399b36ca8..b87213c104550 100644 --- a/administrator/components/com_actionlogs/src/Model/ActionlogModel.php +++ b/administrator/components/com_actionlogs/src/Model/ActionlogModel.php @@ -1,4 +1,5 @@ getDatabase(); - $date = Factory::getDate(); - $params = ComponentHelper::getComponent('com_actionlogs')->getParams(); - - if ($params->get('ip_logging', 0)) - { - $ip = IpHelper::getIp(); - - if (!filter_var($ip, FILTER_VALIDATE_IP)) - { - $ip = 'COM_ACTIONLOGS_IP_INVALID'; - } - } - else - { - $ip = 'COM_ACTIONLOGS_DISABLED'; - } - - $loggedMessages = array(); - - foreach ($messages as $message) - { - $logMessage = new \stdClass; - $logMessage->message_language_key = $messageLanguageKey; - $logMessage->message = json_encode($message); - $logMessage->log_date = (string) $date; - $logMessage->extension = $context; - $logMessage->user_id = $user->id; - $logMessage->ip_address = $ip; - $logMessage->item_id = isset($message['id']) ? (int) $message['id'] : 0; - - try - { - $db->insertObject('#__action_logs', $logMessage); - $loggedMessages[] = $logMessage; - } - catch (\RuntimeException $e) - { - // Ignore it - } - } - - try - { - // Send notification email to users who choose to be notified about the action logs - $this->sendNotificationEmails($loggedMessages, $user->name, $context); - } - catch (MailDisabledException | phpMailerException $e) - { - // Ignore it - } - } - - /** - * Send notification emails about the action log - * - * @param array $messages The logged messages - * @param string $username The username - * @param string $context The Context - * - * @return void - * - * @since 3.9.0 - * - * @throws MailDisabledException if mail is disabled - * @throws phpmailerException if sending mail failed - */ - protected function sendNotificationEmails($messages, $username, $context) - { - $app = Factory::getApplication(); - $lang = $app->getLanguage(); - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query - ->select($db->quoteName(array('u.email', 'l.extensions'))) - ->from($db->quoteName('#__users', 'u')) - ->where($db->quoteName('u.block') . ' = 0') - ->join( - 'INNER', - $db->quoteName('#__action_logs_users', 'l') . ' ON ( ' . $db->quoteName('l.notify') . ' = 1 AND ' - . $db->quoteName('l.user_id') . ' = ' . $db->quoteName('u.id') . ')' - ); - - $db->setQuery($query); - - $users = $db->loadObjectList(); - - $recipients = array(); - - foreach ($users as $user) - { - $extensions = json_decode($user->extensions, true); - - if ($extensions && \in_array(strtok($context, '.'), $extensions)) - { - $recipients[] = $user->email; - } - } - - if (empty($recipients)) - { - return; - } - - $extension = strtok($context, '.'); - $lang->load('com_actionlogs', JPATH_ADMINISTRATOR); - ActionlogsHelper::loadTranslationFiles($extension); - $temp = []; - - foreach ($messages as $message) - { - $m = []; - $m['extension'] = Text::_($extension); - $m['message'] = ActionlogsHelper::getHumanReadableLogMessage($message); - $m['date'] = HTMLHelper::_('date', $message->log_date, 'Y-m-d H:i:s T', 'UTC'); - $m['username'] = $username; - $temp[] = $m; - } - - $templateData = [ - 'messages' => $temp - ]; - - $mailer = new MailTemplate('com_actionlogs.notification', $app->getLanguage()->getTag()); - $mailer->addTemplateData($templateData); - - foreach ($recipients as $recipient) - { - $mailer->addRecipient($recipient); - } - - $mailer->send(); - } + /** + * Function to add logs to the database + * This method adds a record to #__action_logs contains (message_language_key, message, date, context, user) + * + * @param array $messages The contents of the messages to be logged + * @param string $messageLanguageKey The language key of the message + * @param string $context The context of the content passed to the plugin + * @param integer $userId ID of user perform the action, usually ID of current logged in user + * + * @return void + * + * @since 3.9.0 + */ + public function addLog($messages, $messageLanguageKey, $context, $userId = null) + { + $user = Factory::getUser($userId); + $db = $this->getDatabase(); + $date = Factory::getDate(); + $params = ComponentHelper::getComponent('com_actionlogs')->getParams(); + + if ($params->get('ip_logging', 0)) { + $ip = IpHelper::getIp(); + + if (!filter_var($ip, FILTER_VALIDATE_IP)) { + $ip = 'COM_ACTIONLOGS_IP_INVALID'; + } + } else { + $ip = 'COM_ACTIONLOGS_DISABLED'; + } + + $loggedMessages = array(); + + foreach ($messages as $message) { + $logMessage = new \stdClass(); + $logMessage->message_language_key = $messageLanguageKey; + $logMessage->message = json_encode($message); + $logMessage->log_date = (string) $date; + $logMessage->extension = $context; + $logMessage->user_id = $user->id; + $logMessage->ip_address = $ip; + $logMessage->item_id = isset($message['id']) ? (int) $message['id'] : 0; + + try { + $db->insertObject('#__action_logs', $logMessage); + $loggedMessages[] = $logMessage; + } catch (\RuntimeException $e) { + // Ignore it + } + } + + try { + // Send notification email to users who choose to be notified about the action logs + $this->sendNotificationEmails($loggedMessages, $user->name, $context); + } catch (MailDisabledException | phpMailerException $e) { + // Ignore it + } + } + + /** + * Send notification emails about the action log + * + * @param array $messages The logged messages + * @param string $username The username + * @param string $context The Context + * + * @return void + * + * @since 3.9.0 + * + * @throws MailDisabledException if mail is disabled + * @throws phpmailerException if sending mail failed + */ + protected function sendNotificationEmails($messages, $username, $context) + { + $app = Factory::getApplication(); + $lang = $app->getLanguage(); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query + ->select($db->quoteName(array('u.email', 'l.extensions'))) + ->from($db->quoteName('#__users', 'u')) + ->where($db->quoteName('u.block') . ' = 0') + ->join( + 'INNER', + $db->quoteName('#__action_logs_users', 'l') . ' ON ( ' . $db->quoteName('l.notify') . ' = 1 AND ' + . $db->quoteName('l.user_id') . ' = ' . $db->quoteName('u.id') . ')' + ); + + $db->setQuery($query); + + $users = $db->loadObjectList(); + + $recipients = array(); + + foreach ($users as $user) { + $extensions = json_decode($user->extensions, true); + + if ($extensions && \in_array(strtok($context, '.'), $extensions)) { + $recipients[] = $user->email; + } + } + + if (empty($recipients)) { + return; + } + + $extension = strtok($context, '.'); + $lang->load('com_actionlogs', JPATH_ADMINISTRATOR); + ActionlogsHelper::loadTranslationFiles($extension); + $temp = []; + + foreach ($messages as $message) { + $m = []; + $m['extension'] = Text::_($extension); + $m['message'] = ActionlogsHelper::getHumanReadableLogMessage($message); + $m['date'] = HTMLHelper::_('date', $message->log_date, 'Y-m-d H:i:s T', 'UTC'); + $m['username'] = $username; + $temp[] = $m; + } + + $templateData = [ + 'messages' => $temp + ]; + + $mailer = new MailTemplate('com_actionlogs.notification', $app->getLanguage()->getTag()); + $mailer->addTemplateData($templateData); + + foreach ($recipients as $recipient) { + $mailer->addRecipient($recipient); + } + + $mailer->send(); + } } diff --git a/administrator/components/com_actionlogs/src/Model/ActionlogsModel.php b/administrator/components/com_actionlogs/src/Model/ActionlogsModel.php index aff8e7da27922..f8200a62ffb52 100644 --- a/administrator/components/com_actionlogs/src/Model/ActionlogsModel.php +++ b/administrator/components/com_actionlogs/src/Model/ActionlogsModel.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select('a.*') - ->select($db->quoteName('u.name')) - ->from($db->quoteName('#__action_logs', 'a')) - ->join('LEFT', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')); - - // Get ordering - $fullorderCol = $this->state->get('list.fullordering', 'a.id DESC'); - - // Apply ordering - if (!empty($fullorderCol)) - { - $query->order($db->escape($fullorderCol)); - } - - // Get filter by user - $user = $this->getState('filter.user'); - - // Apply filter by user - if (!empty($user)) - { - $user = (int) $user; - $query->where($db->quoteName('a.user_id') . ' = :userid') - ->bind(':userid', $user, ParameterType::INTEGER); - } - - // Get filter by extension - $extension = $this->getState('filter.extension'); - - // Apply filter by extension - if (!empty($extension)) - { - $extension = $extension . '%'; - $query->where($db->quoteName('a.extension') . ' LIKE :extension') - ->bind(':extension', $extension); - } - - // Get filter by date range - $dateRange = $this->getState('filter.dateRange'); - - // Apply filter by date range - if (!empty($dateRange)) - { - $date = $this->buildDateRange($dateRange); - - // If the chosen range is not more than a year ago - if ($date['dNow'] !== false && $date['dStart'] !== false) - { - $dStart = $date['dStart']->format('Y-m-d H:i:s'); - $dNow = $date['dNow']->format('Y-m-d H:i:s'); - $query->where( - $db->quoteName('a.log_date') . ' BETWEEN :dstart AND :dnow' - ); - $query->bind(':dstart', $dStart); - $query->bind(':dnow', $dNow); - } - } - - // Filter the items over the search string if set. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $ids, ParameterType::INTEGER); - } - elseif (stripos($search, 'item_id:') === 0) - { - $ids = (int) substr($search, 8); - $query->where($db->quoteName('a.item_id') . ' = :itemid') - ->bind(':itemid', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . $search . '%'; - $query->where($db->quoteName('a.message') . ' LIKE :message') - ->bind(':message', $search); - } - } - - return $query; - } - - /** - * Construct the date range to filter on. - * - * @param string $range The textual range to construct the filter for. - * - * @return array The date range to filter on. - * - * @since 3.9.0 - * - * @throws Exception - */ - private function buildDateRange($range) - { - // Get UTC for now. - $dNow = new Date; - $dStart = clone $dNow; - - switch ($range) - { - case 'past_week': - $dStart->modify('-7 day'); - break; - - case 'past_1month': - $dStart->modify('-1 month'); - break; - - case 'past_3month': - $dStart->modify('-3 month'); - break; - - case 'past_6month': - $dStart->modify('-6 month'); - break; - - case 'past_year': - $dStart->modify('-1 year'); - break; - - case 'today': - // Ranges that need to align with local 'days' need special treatment. - $offset = Factory::getApplication()->get('offset'); - - // Reset the start time to be the beginning of today, local time. - $dStart = new Date('now', $offset); - $dStart->setTime(0, 0, 0); - - // Now change the timezone back to UTC. - $tz = new DateTimeZone('GMT'); - $dStart->setTimezone($tz); - break; - } - - return array('dNow' => $dNow, 'dStart' => $dStart); - } - - /** - * Get all log entries for an item - * - * @param string $extension The extension the item belongs to - * @param integer $itemId The item ID - * - * @return array - * - * @since 3.9.0 - */ - public function getLogsForItem($extension, $itemId) - { - $itemId = (int) $itemId; - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('a.*') - ->select($db->quoteName('u.name')) - ->from($db->quoteName('#__action_logs', 'a')) - ->join('INNER', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')) - ->where($db->quoteName('a.extension') . ' = :extension') - ->where($db->quoteName('a.item_id') . ' = :itemid') - ->bind(':extension', $extension) - ->bind(':itemid', $itemId, ParameterType::INTEGER); - - // Get ordering - $fullorderCol = $this->getState('list.fullordering', 'a.id DESC'); - - // Apply ordering - if (!empty($fullorderCol)) - { - $query->order($db->escape($fullorderCol)); - } - - $db->setQuery($query); - - return $db->loadObjectList(); - } - - /** - * Get logs data into Table object - * - * @param integer[]|null $pks An optional array of log record IDs to load - * - * @return array All logs in the table - * - * @since 3.9.0 - */ - public function getLogsData($pks = null) - { - $db = $this->getDatabase(); - $query = $this->getLogDataQuery($pks); - - $db->setQuery($query); - - return $db->loadObjectList(); - } - - /** - * Get logs data as a database iterator - * - * @param integer[]|null $pks An optional array of log record IDs to load - * - * @return DatabaseIterator - * - * @since 3.9.0 - */ - public function getLogDataAsIterator($pks = null) - { - $db = $this->getDatabase(); - $query = $this->getLogDataQuery($pks); - - $db->setQuery($query); - - return $db->getIterator(); - } - - /** - * Get the query for loading logs data - * - * @param integer[]|null $pks An optional array of log record IDs to load - * - * @return DatabaseQuery - * - * @since 3.9.0 - */ - private function getLogDataQuery($pks = null) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('a.*') - ->select($db->quoteName('u.name')) - ->from($db->quoteName('#__action_logs', 'a')) - ->join('INNER', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')); - - if (\is_array($pks) && \count($pks) > 0) - { - $pks = ArrayHelper::toInteger($pks); - $query->whereIn($db->quoteName('a.id'), $pks); - } - - return $query; - } - - /** - * Delete logs - * - * @param array $pks Primary keys of logs - * - * @return boolean - * - * @since 3.9.0 - */ - public function delete(&$pks) - { - $keys = ArrayHelper::toInteger($pks); - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__action_logs')) - ->whereIn($db->quoteName('id'), $keys); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - Factory::getApplication()->triggerEvent('onAfterLogPurge', array()); - - return true; - } - - /** - * Removes all of logs from the table. - * - * @return boolean result of operation - * - * @since 3.9.0 - */ - public function purge() - { - try - { - $this->getDatabase()->truncateTable('#__action_logs'); - } - catch (Exception $e) - { - return false; - } - - Factory::getApplication()->triggerEvent('onAfterLogPurge', array()); - - return true; - } - - /** - * Get the filter form - * - * @param array $data data - * @param boolean $loadData load current data - * - * @return Form|boolean The Form object or false on error - * - * @since 3.9.0 - */ - public function getFilterForm($data = array(), $loadData = true) - { - $form = parent::getFilterForm($data, $loadData); - $params = ComponentHelper::getParams('com_actionlogs'); - $ipLogging = (bool) $params->get('ip_logging', 0); - - // Add ip sort options to sort dropdown - if ($form && $ipLogging) - { - /* @var \Joomla\CMS\Form\Field\ListField $field */ - $field = $form->getField('fullordering', 'list'); - $field->addOption(Text::_('COM_ACTIONLOGS_IP_ADDRESS_ASC'), array('value' => 'a.ip_address ASC')); - $field->addOption(Text::_('COM_ACTIONLOGS_IP_ADDRESS_DESC'), array('value' => 'a.ip_address DESC')); - } - - return $form; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 3.9.0 + * + * @throws Exception + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'a.id', 'id', + 'a.extension', 'extension', + 'a.user_id', 'user', + 'a.message', 'message', + 'a.log_date', 'log_date', + 'a.ip_address', 'ip_address', + 'dateRange', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.9.0 + * + * @throws Exception + */ + protected function populateState($ordering = 'a.id', $direction = 'desc') + { + parent::populateState($ordering, $direction); + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + * + * @since 3.9.0 + * + * @throws Exception + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('a.*') + ->select($db->quoteName('u.name')) + ->from($db->quoteName('#__action_logs', 'a')) + ->join('LEFT', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')); + + // Get ordering + $fullorderCol = $this->state->get('list.fullordering', 'a.id DESC'); + + // Apply ordering + if (!empty($fullorderCol)) { + $query->order($db->escape($fullorderCol)); + } + + // Get filter by user + $user = $this->getState('filter.user'); + + // Apply filter by user + if (!empty($user)) { + $user = (int) $user; + $query->where($db->quoteName('a.user_id') . ' = :userid') + ->bind(':userid', $user, ParameterType::INTEGER); + } + + // Get filter by extension + $extension = $this->getState('filter.extension'); + + // Apply filter by extension + if (!empty($extension)) { + $extension = $extension . '%'; + $query->where($db->quoteName('a.extension') . ' LIKE :extension') + ->bind(':extension', $extension); + } + + // Get filter by date range + $dateRange = $this->getState('filter.dateRange'); + + // Apply filter by date range + if (!empty($dateRange)) { + $date = $this->buildDateRange($dateRange); + + // If the chosen range is not more than a year ago + if ($date['dNow'] !== false && $date['dStart'] !== false) { + $dStart = $date['dStart']->format('Y-m-d H:i:s'); + $dNow = $date['dNow']->format('Y-m-d H:i:s'); + $query->where( + $db->quoteName('a.log_date') . ' BETWEEN :dstart AND :dnow' + ); + $query->bind(':dstart', $dStart); + $query->bind(':dnow', $dNow); + } + } + + // Filter the items over the search string if set. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $ids, ParameterType::INTEGER); + } elseif (stripos($search, 'item_id:') === 0) { + $ids = (int) substr($search, 8); + $query->where($db->quoteName('a.item_id') . ' = :itemid') + ->bind(':itemid', $ids, ParameterType::INTEGER); + } else { + $search = '%' . $search . '%'; + $query->where($db->quoteName('a.message') . ' LIKE :message') + ->bind(':message', $search); + } + } + + return $query; + } + + /** + * Construct the date range to filter on. + * + * @param string $range The textual range to construct the filter for. + * + * @return array The date range to filter on. + * + * @since 3.9.0 + * + * @throws Exception + */ + private function buildDateRange($range) + { + // Get UTC for now. + $dNow = new Date(); + $dStart = clone $dNow; + + switch ($range) { + case 'past_week': + $dStart->modify('-7 day'); + break; + + case 'past_1month': + $dStart->modify('-1 month'); + break; + + case 'past_3month': + $dStart->modify('-3 month'); + break; + + case 'past_6month': + $dStart->modify('-6 month'); + break; + + case 'past_year': + $dStart->modify('-1 year'); + break; + + case 'today': + // Ranges that need to align with local 'days' need special treatment. + $offset = Factory::getApplication()->get('offset'); + + // Reset the start time to be the beginning of today, local time. + $dStart = new Date('now', $offset); + $dStart->setTime(0, 0, 0); + + // Now change the timezone back to UTC. + $tz = new DateTimeZone('GMT'); + $dStart->setTimezone($tz); + break; + } + + return array('dNow' => $dNow, 'dStart' => $dStart); + } + + /** + * Get all log entries for an item + * + * @param string $extension The extension the item belongs to + * @param integer $itemId The item ID + * + * @return array + * + * @since 3.9.0 + */ + public function getLogsForItem($extension, $itemId) + { + $itemId = (int) $itemId; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('a.*') + ->select($db->quoteName('u.name')) + ->from($db->quoteName('#__action_logs', 'a')) + ->join('INNER', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')) + ->where($db->quoteName('a.extension') . ' = :extension') + ->where($db->quoteName('a.item_id') . ' = :itemid') + ->bind(':extension', $extension) + ->bind(':itemid', $itemId, ParameterType::INTEGER); + + // Get ordering + $fullorderCol = $this->getState('list.fullordering', 'a.id DESC'); + + // Apply ordering + if (!empty($fullorderCol)) { + $query->order($db->escape($fullorderCol)); + } + + $db->setQuery($query); + + return $db->loadObjectList(); + } + + /** + * Get logs data into Table object + * + * @param integer[]|null $pks An optional array of log record IDs to load + * + * @return array All logs in the table + * + * @since 3.9.0 + */ + public function getLogsData($pks = null) + { + $db = $this->getDatabase(); + $query = $this->getLogDataQuery($pks); + + $db->setQuery($query); + + return $db->loadObjectList(); + } + + /** + * Get logs data as a database iterator + * + * @param integer[]|null $pks An optional array of log record IDs to load + * + * @return DatabaseIterator + * + * @since 3.9.0 + */ + public function getLogDataAsIterator($pks = null) + { + $db = $this->getDatabase(); + $query = $this->getLogDataQuery($pks); + + $db->setQuery($query); + + return $db->getIterator(); + } + + /** + * Get the query for loading logs data + * + * @param integer[]|null $pks An optional array of log record IDs to load + * + * @return DatabaseQuery + * + * @since 3.9.0 + */ + private function getLogDataQuery($pks = null) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('a.*') + ->select($db->quoteName('u.name')) + ->from($db->quoteName('#__action_logs', 'a')) + ->join('INNER', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')); + + if (\is_array($pks) && \count($pks) > 0) { + $pks = ArrayHelper::toInteger($pks); + $query->whereIn($db->quoteName('a.id'), $pks); + } + + return $query; + } + + /** + * Delete logs + * + * @param array $pks Primary keys of logs + * + * @return boolean + * + * @since 3.9.0 + */ + public function delete(&$pks) + { + $keys = ArrayHelper::toInteger($pks); + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__action_logs')) + ->whereIn($db->quoteName('id'), $keys); + $db->setQuery($query); + + try { + $db->execute(); + } catch (RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + Factory::getApplication()->triggerEvent('onAfterLogPurge', array()); + + return true; + } + + /** + * Removes all of logs from the table. + * + * @return boolean result of operation + * + * @since 3.9.0 + */ + public function purge() + { + try { + $this->getDatabase()->truncateTable('#__action_logs'); + } catch (Exception $e) { + return false; + } + + Factory::getApplication()->triggerEvent('onAfterLogPurge', array()); + + return true; + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return Form|boolean The Form object or false on error + * + * @since 3.9.0 + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + $params = ComponentHelper::getParams('com_actionlogs'); + $ipLogging = (bool) $params->get('ip_logging', 0); + + // Add ip sort options to sort dropdown + if ($form && $ipLogging) { + /* @var \Joomla\CMS\Form\Field\ListField $field */ + $field = $form->getField('fullordering', 'list'); + $field->addOption(Text::_('COM_ACTIONLOGS_IP_ADDRESS_ASC'), array('value' => 'a.ip_address ASC')); + $field->addOption(Text::_('COM_ACTIONLOGS_IP_ADDRESS_DESC'), array('value' => 'a.ip_address DESC')); + } + + return $form; + } } diff --git a/administrator/components/com_actionlogs/src/Plugin/ActionLogPlugin.php b/administrator/components/com_actionlogs/src/Plugin/ActionLogPlugin.php index c6520f1360810..34f04fb0f7597 100644 --- a/administrator/components/com_actionlogs/src/Plugin/ActionLogPlugin.php +++ b/administrator/components/com_actionlogs/src/Plugin/ActionLogPlugin.php @@ -1,4 +1,5 @@ $message) - { - if (!\array_key_exists('userid', $message)) - { - $message['userid'] = $user->id; - } - - if (!\array_key_exists('username', $message)) - { - $message['username'] = $user->username; - } - - if (!\array_key_exists('accountlink', $message)) - { - $message['accountlink'] = 'index.php?option=com_users&task=user.edit&id=' . $user->id; - } - - if (\array_key_exists('type', $message)) - { - $message['type'] = strtoupper($message['type']); - } - - if (\array_key_exists('app', $message)) - { - $message['app'] = strtoupper($message['app']); - } - - $messages[$index] = $message; - } - - /** @var \Joomla\Component\Actionlogs\Administrator\Model\ActionlogModel $model */ - $model = $this->app->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - - $model->addLog($messages, strtoupper($messageLanguageKey), $context, $userId); - } + /** + * Application object. + * + * @var \Joomla\CMS\Application\CMSApplication + * @since 3.9.0 + */ + protected $app; + + /** + * Database object. + * + * @var \Joomla\Database\DatabaseDriver + * @since 3.9.0 + */ + protected $db; + + /** + * Load plugin language file automatically so that it can be used inside component + * + * @var boolean + * @since 3.9.0 + */ + protected $autoloadLanguage = true; + + /** + * Proxy for ActionlogsModelUserlog addLog method + * + * This method adds a record to #__action_logs contains (message_language_key, message, date, context, user) + * + * @param array $messages The contents of the messages to be logged + * @param string $messageLanguageKey The language key of the message + * @param string $context The context of the content passed to the plugin + * @param int $userId ID of user perform the action, usually ID of current logged in user + * + * @return void + * + * @since 3.9.0 + */ + protected function addLog($messages, $messageLanguageKey, $context, $userId = null) + { + $user = Factory::getUser(); + + foreach ($messages as $index => $message) { + if (!\array_key_exists('userid', $message)) { + $message['userid'] = $user->id; + } + + if (!\array_key_exists('username', $message)) { + $message['username'] = $user->username; + } + + if (!\array_key_exists('accountlink', $message)) { + $message['accountlink'] = 'index.php?option=com_users&task=user.edit&id=' . $user->id; + } + + if (\array_key_exists('type', $message)) { + $message['type'] = strtoupper($message['type']); + } + + if (\array_key_exists('app', $message)) { + $message['app'] = strtoupper($message['app']); + } + + $messages[$index] = $message; + } + + /** @var \Joomla\Component\Actionlogs\Administrator\Model\ActionlogModel $model */ + $model = $this->app->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + + $model->addLog($messages, strtoupper($messageLanguageKey), $context, $userId); + } } diff --git a/administrator/components/com_admin/postinstall/addnosniff.php b/administrator/components/com_admin/postinstall/addnosniff.php index b8dcab690900b..4de293a3afcbe 100644 --- a/administrator/components/com_admin/postinstall/addnosniff.php +++ b/administrator/components/com_admin/postinstall/addnosniff.php @@ -1,4 +1,5 @@ get('behind_loadbalancer', '0')) - { - return false; - } + if ($app->get('behind_loadbalancer', '0')) { + return false; + } - if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER) && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) - { - return true; - } + if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER) && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + return true; + } - if (array_key_exists('HTTP_CLIENT_IP', $_SERVER) && !empty($_SERVER['HTTP_CLIENT_IP'])) - { - return true; - } + if (array_key_exists('HTTP_CLIENT_IP', $_SERVER) && !empty($_SERVER['HTTP_CLIENT_IP'])) { + return true; + } - return false; + return false; } @@ -55,35 +53,32 @@ function admin_postinstall_behindproxy_condition() */ function behindproxy_postinstall_action() { - $prev = ArrayHelper::fromObject(new JConfig); - $data = array_merge($prev, array('behind_loadbalancer' => '1')); + $prev = ArrayHelper::fromObject(new JConfig()); + $data = array_merge($prev, array('behind_loadbalancer' => '1')); - $config = new Registry($data); + $config = new Registry($data); - // Set the configuration file path. - $file = JPATH_CONFIGURATION . '/configuration.php'; + // Set the configuration file path. + $file = JPATH_CONFIGURATION . '/configuration.php'; - // Attempt to make the file writeable - if (Path::isOwner($file) && !Path::setPermissions($file, '0644')) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTWRITABLE'), 'error'); + // Attempt to make the file writeable + if (Path::isOwner($file) && !Path::setPermissions($file, '0644')) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTWRITABLE'), 'error'); - return; - } + return; + } - // Attempt to write the configuration file as a PHP class named JConfig. - $configuration = $config->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); + // Attempt to write the configuration file as a PHP class named JConfig. + $configuration = $config->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); - if (!File::write($file, $configuration)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_WRITE_FAILED'), 'error'); + if (!File::write($file, $configuration)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_WRITE_FAILED'), 'error'); - return; - } + return; + } - // Attempt to make the file unwriteable - if (Path::isOwner($file) && !Path::setPermissions($file, '0444')) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTUNWRITABLE'), 'error'); - } + // Attempt to make the file unwriteable + if (Path::isOwner($file) && !Path::setPermissions($file, '0444')) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTUNWRITABLE'), 'error'); + } } diff --git a/administrator/components/com_admin/postinstall/htaccesssvg.php b/administrator/components/com_admin/postinstall/htaccesssvg.php index 91645751f1d39..30dbab91c095c 100644 --- a/administrator/components/com_admin/postinstall/htaccesssvg.php +++ b/administrator/components/com_admin/postinstall/htaccesssvg.php @@ -1,4 +1,5 @@ getQuery(true) - ->select($db->quoteName('access')) - ->from($db->quoteName('#__languages')) - ->where($db->quoteName('access') . ' = ' . $db->quote('0')); - $db->setQuery($query); - $db->execute(); - $numRows = $db->getNumRows(); + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('access')) + ->from($db->quoteName('#__languages')) + ->where($db->quoteName('access') . ' = ' . $db->quote('0')); + $db->setQuery($query); + $db->execute(); + $numRows = $db->getNumRows(); - if (isset($numRows) && $numRows != 0) - { - // We have rows here so we have at minimum one row with access set to 0 - return true; - } + if (isset($numRows) && $numRows != 0) { + // We have rows here so we have at minimum one row with access set to 0 + return true; + } - // All good the query return nothing. - return false; + // All good the query return nothing. + return false; } diff --git a/administrator/components/com_admin/postinstall/statscollection.php b/administrator/components/com_admin/postinstall/statscollection.php index c1cf1a3a9886a..6d29d31b873aa 100644 --- a/administrator/components/com_admin/postinstall/statscollection.php +++ b/administrator/components/com_admin/postinstall/statscollection.php @@ -1,4 +1,5 @@ extension->manifest_cache)) - { - $manifestValues = json_decode($installer->extension->manifest_cache, true); - - if (array_key_exists('version', $manifestValues)) - { - $this->fromVersion = $manifestValues['version']; - - // Ensure templates are moved to the correct mode - $this->fixTemplateMode(); - - return true; - } - } - - return false; - } - - return true; - } - - /** - * Method to update Joomla! - * - * @param Installer $installer The class calling this method - * - * @return void - */ - public function update($installer) - { - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'joomla_update.php'; - - Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); - - try - { - Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_DELETE_FILES'), Log::INFO, 'Update'); - } - catch (RuntimeException $exception) - { - // Informational log only - } - - // Uninstall plugins before removing their files and folders - $this->uninstallRepeatableFieldsPlugin(); - $this->uninstallEosPlugin(); - - // This needs to stay for 2.5 update compatibility - $this->deleteUnexistingFiles(); - $this->updateManifestCaches(); - $this->updateDatabase(); - $this->updateAssets($installer); - $this->clearStatsCache(); - $this->convertTablesToUtf8mb4(true); - $this->addUserAuthProviderColumn(); - $this->cleanJoomlaCache(); - } - - /** - * Method to clear our stats plugin cache to ensure we get fresh data on Joomla Update - * - * @return void - * - * @since 3.5 - */ - protected function clearStatsCache() - { - $db = Factory::getDbo(); - - try - { - // Get the params for the stats plugin - $params = $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('params')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('stats')) - )->loadResult(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
    '; - - return; - } - - $params = json_decode($params, true); - - // Reset the last run parameter - if (isset($params['lastrun'])) - { - $params['lastrun'] = ''; - } - - $params = json_encode($params); - - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('params') . ' = ' . $db->quote($params)) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('stats')); - - try - { - $db->setQuery($query)->execute(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
    '; - - return; - } - } - - /** - * Method to update Database - * - * @return void - */ - protected function updateDatabase() - { - if (Factory::getDbo()->getServerType() === 'mysql') - { - $this->updateDatabaseMysql(); - } - } - - /** - * Method to update MySQL Database - * - * @return void - */ - protected function updateDatabaseMysql() - { - $db = Factory::getDbo(); - - $db->setQuery('SHOW ENGINES'); - - try - { - $results = $db->loadObjectList(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
    '; - - return; - } - - foreach ($results as $result) - { - if ($result->Support != 'DEFAULT') - { - continue; - } - - $db->setQuery('ALTER TABLE #__update_sites_extensions ENGINE = ' . $result->Engine); - - try - { - $db->execute(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
    '; - - return; - } - - break; - } - } - - /** - * Uninstalls the plg_fields_repeatable plugin and transforms its custom field instances - * to instances of the plg_fields_subfields plugin. - * - * @return void - * - * @since 4.0.0 - */ - protected function uninstallRepeatableFieldsPlugin() - { - $app = Factory::getApplication(); - $db = Factory::getDbo(); - - // Check if the plg_fields_repeatable plugin is present - $extensionId = $db->setQuery( - $db->getQuery(true) - ->select('extension_id') - ->from('#__extensions') - ->where('name = ' . $db->quote('plg_fields_repeatable')) - )->loadResult(); - - // Skip uninstalling when it doesn't exist - if (!$extensionId) - { - return; - } - - // Ensure the FieldsHelper class is loaded for the Repeatable fields plugin we're about to remove - \JLoader::register('FieldsHelper', JPATH_ADMINISTRATOR . '/components/com_fields/helpers/fields.php'); - - try - { - $db->transactionStart(); - - // Get the FieldsModelField, we need it in a sec - $fieldModel = $app->bootComponent('com_fields')->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); - /** @var FieldModel $fieldModel */ - - // Now get a list of all `repeatable` custom field instances - $db->setQuery( - $db->getQuery(true) - ->select('*') - ->from('#__fields') - ->where($db->quoteName('type') . ' = ' . $db->quote('repeatable')) - ); - - // Execute the query and iterate over the `repeatable` instances - foreach ($db->loadObjectList() as $row) - { - // Skip broken rows - just a security measure, should not happen - if (!isset($row->fieldparams) || !($oldFieldparams = json_decode($row->fieldparams)) || !is_object($oldFieldparams)) - { - continue; - } - - /** - * We basically want to transform this `repeatable` type into a `subfields` type. While $oldFieldparams - * holds the `fieldparams` of the `repeatable` type, $newFieldparams shall hold the `fieldparams` - * of the `subfields` type. - */ - $newFieldparams = [ - 'repeat' => '1', - 'options' => [], - ]; - - /** - * This array is used to store the mapping between the name of form fields from Repeatable field - * with ID of the child-fields. It will then be used to migrate data later - */ - $mapping = []; - - /** - * Store name of media fields which we need to convert data from old format (string) to new - * format (json) during the migration - */ - $mediaFields = []; - - // If this repeatable fields actually had child-fields (normally this is always the case) - if (isset($oldFieldparams->fields) && is_object($oldFieldparams->fields)) - { - // Small counter for the child-fields (aka sub fields) - $newFieldCount = 0; - - // Iterate over the sub fields - foreach (get_object_vars($oldFieldparams->fields) as $oldField) - { - // Used for field name collision prevention - $fieldname_prefix = ''; - $fieldname_suffix = 0; - - // Try to save the new sub field in a loop because of field name collisions - while (true) - { - /** - * We basically want to create a completely new custom fields instance for every sub field - * of the `repeatable` instance. This is what we use $data for, we create a new custom field - * for each of the sub fields of the `repeatable` instance. - */ - $data = [ - 'context' => $row->context, - 'group_id' => $row->group_id, - 'title' => $oldField->fieldname, - 'name' => ( - $fieldname_prefix - . $oldField->fieldname - . ($fieldname_suffix > 0 ? ('_' . $fieldname_suffix) : '') - ), - 'label' => $oldField->fieldname, - 'default_value' => $row->default_value, - 'type' => $oldField->fieldtype, - 'description' => $row->description, - 'state' => '1', - 'params' => $row->params, - 'language' => '*', - 'assigned_cat_ids' => [-1], - 'only_use_in_subform' => 1, - ]; - - // `number` is not a valid custom field type, so use `text` instead. - if ($data['type'] == 'number') - { - $data['type'] = 'text'; - } - - if ($data['type'] == 'media') - { - $mediaFields[] = $oldField->fieldname; - } - - // Reset the state because else \Joomla\CMS\MVC\Model\AdminModel will take an already - // existing value (e.g. from previous save) and do an UPDATE instead of INSERT. - $fieldModel->setState('field.id', 0); - - // If an error occurred when trying to save this. - if (!$fieldModel->save($data)) - { - // If the error is, that the name collided, increase the collision prevention - $error = $fieldModel->getError(); - - if ($error == 'COM_FIELDS_ERROR_UNIQUE_NAME') - { - // If this is the first time this error occurs, set only the prefix - if ($fieldname_prefix == '') - { - $fieldname_prefix = ($row->name . '_'); - } - else - { - // Else increase the suffix - $fieldname_suffix++; - } - - // And start again with the while loop. - continue 1; - } - - // Else bail out with the error. Something is totally wrong. - throw new \Exception($error); - } - - // Break out of the while loop, saving was successful. - break 1; - } - - // Get the newly created id - $subfield_id = $fieldModel->getState('field.id'); - - // Really check that it is valid - if (!is_numeric($subfield_id) || $subfield_id < 1) - { - throw new \Exception('Something went wrong.'); - } - - // And tell our new `subfields` field about his child - $newFieldparams['options'][('option' . $newFieldCount)] = [ - 'customfield' => $subfield_id, - 'render_values' => '1', - ]; - - $newFieldCount++; - - $mapping[$oldField->fieldname] = 'field' . $subfield_id; - } - } - - // Write back the changed stuff to the database - $db->setQuery( - $db->getQuery(true) - ->update('#__fields') - ->set($db->quoteName('type') . ' = ' . $db->quote('subform')) - ->set($db->quoteName('fieldparams') . ' = ' . $db->quote(json_encode($newFieldparams))) - ->where($db->quoteName('id') . ' = ' . $db->quote($row->id)) - )->execute(); - - // Migrate data for this field - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__fields_values')) - ->where($db->quoteName('field_id') . ' = ' . $row->id); - $db->setQuery($query); - - foreach ($db->loadObjectList() as $rowFieldValue) - { - // Do not do the version if no data is entered for the custom field this item - if (!$rowFieldValue->value) - { - continue; - } - - /** - * Here we will have to update the stored value of the field to new format - * The key for each row changes from repeatable to row, for example repeatable0 to row0, and so on - * The key for each sub-field change from name of field to field + ID of the new sub-field - * Example data format stored in J3: {"repeatable0":{"id":"1","username":"admin"}} - * Example data format stored in J4: {"row0":{"field1":"1","field2":"admin"}} - */ - $newFieldValue = []; - - // Convert to array to change key - $fieldValue = json_decode($rowFieldValue->value, true); - - // If data could not be decoded for some reason, ignore - if (!$fieldValue) - { - continue; - } - - $rowIndex = 0; - - foreach ($fieldValue as $rowKey => $rowValue) - { - $rowKey = 'row' . ($rowIndex++); - $newFieldValue[$rowKey] = []; - - foreach ($rowValue as $subFieldName => $subFieldValue) - { - // This is a media field, so we need to convert data to new format required in Joomla! 4 - if (in_array($subFieldName, $mediaFields)) - { - $subFieldValue = ['imagefile' => $subFieldValue, 'alt_text' => '']; - } - - if (isset($mapping[$subFieldName])) - { - $newFieldValue[$rowKey][$mapping[$subFieldName]] = $subFieldValue; - } - else - { - // Not found, use the old key to avoid data lost - $newFieldValue[$subFieldName] = $subFieldValue; - } - } - } - - $query->clear() - ->update($db->quoteName('#__fields_values')) - ->set($db->quoteName('value') . ' = ' . $db->quote(json_encode($newFieldValue))) - ->where($db->quoteName('field_id') . ' = ' . $rowFieldValue->field_id) - ->where($db->quoteName('item_id') . ' =' . $rowFieldValue->item_id); - $db->setQuery($query) - ->execute(); - } - } - - // Now, unprotect the plugin so we can uninstall it - $db->setQuery( - $db->getQuery(true) - ->update('#__extensions') - ->set('protected = 0') - ->where($db->quoteName('extension_id') . ' = ' . $extensionId) - )->execute(); - - // And now uninstall the plugin - $installer = new Installer; - $installer->setDatabase($db); - $installer->uninstall('plugin', $extensionId); - - $db->transactionCommit(); - } - catch (\Exception $e) - { - $db->transactionRollback(); - throw $e; - } - } - - /** - * Uninstall the 3.10 EOS plugin - * - * @return void - * - * @since 4.0.0 - */ - protected function uninstallEosPlugin() - { - $db = Factory::getDbo(); - - // Check if the plg_quickicon_eos310 plugin is present - $extensionId = $db->setQuery( - $db->getQuery(true) - ->select('extension_id') - ->from('#__extensions') - ->where('name = ' . $db->quote('plg_quickicon_eos310')) - )->loadResult(); - - // Skip uninstalling if it doesn't exist - if (!$extensionId) - { - return; - } - - try - { - $db->transactionStart(); - - // Unprotect the plugin so we can uninstall it - $db->setQuery( - $db->getQuery(true) - ->update('#__extensions') - ->set('protected = 0') - ->where($db->quoteName('extension_id') . ' = ' . $extensionId) - )->execute(); - - // Uninstall the plugin - $installer = new Installer; - $installer->setDatabase($db); - $installer->uninstall('plugin', $extensionId); - - $db->transactionCommit(); - } - catch (\Exception $e) - { - $db->transactionRollback(); - throw $e; - } - } - - /** - * Update the manifest caches - * - * @return void - */ - protected function updateManifestCaches() - { - $extensions = ExtensionHelper::getCoreExtensions(); - - // If we have the search package around, it may not have a manifest cache entry after upgrades from 3.x, so add it to the list - if (File::exists(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml')) - { - $extensions[] = array('package', 'pkg_search', '', 0); - } - - // Attempt to refresh manifest caches - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('*') - ->from('#__extensions'); - - foreach ($extensions as $extension) - { - $query->where( - 'type=' . $db->quote($extension[0]) - . ' AND element=' . $db->quote($extension[1]) - . ' AND folder=' . $db->quote($extension[2]) - . ' AND client_id=' . $extension[3], 'OR' - ); - } - - $db->setQuery($query); - - try - { - $extensions = $db->loadObjectList(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
    '; - - return; - } - - $installer = new Installer; - $installer->setDatabase($db); - - foreach ($extensions as $extension) - { - if (!$installer->refreshManifestCache($extension->extension_id)) - { - echo Text::sprintf('FILES_JOOMLA_ERROR_MANIFEST', $extension->type, $extension->element, $extension->name, $extension->client_id) . '
    '; - } - } - } - - /** - * Delete files that should not exist - * - * @param bool $dryRun If set to true, will not actually delete files, but just report their status for use in CLI - * @param bool $suppressOutput Set to true to suppress echoing any errors, and just return the $status array - * - * @return array - */ - public function deleteUnexistingFiles($dryRun = false, $suppressOutput = false) - { - $status = [ - 'files_exist' => [], - 'folders_exist' => [], - 'files_deleted' => [], - 'folders_deleted' => [], - 'files_errors' => [], - 'folders_errors' => [], - 'folders_checked' => [], - 'files_checked' => [], - ]; - - $files = array( - // From 3.10 to 4.1 - '/administrator/components/com_actionlogs/actionlogs.php', - '/administrator/components/com_actionlogs/controller.php', - '/administrator/components/com_actionlogs/controllers/actionlogs.php', - '/administrator/components/com_actionlogs/helpers/actionlogs.php', - '/administrator/components/com_actionlogs/helpers/actionlogsphp55.php', - '/administrator/components/com_actionlogs/layouts/logstable.php', - '/administrator/components/com_actionlogs/libraries/actionlogplugin.php', - '/administrator/components/com_actionlogs/models/actionlog.php', - '/administrator/components/com_actionlogs/models/actionlogs.php', - '/administrator/components/com_actionlogs/models/fields/extension.php', - '/administrator/components/com_actionlogs/models/fields/logcreator.php', - '/administrator/components/com_actionlogs/models/fields/logsdaterange.php', - '/administrator/components/com_actionlogs/models/fields/logtype.php', - '/administrator/components/com_actionlogs/models/fields/plugininfo.php', - '/administrator/components/com_actionlogs/models/forms/filter_actionlogs.xml', - '/administrator/components/com_actionlogs/views/actionlogs/tmpl/default.php', - '/administrator/components/com_actionlogs/views/actionlogs/tmpl/default.xml', - '/administrator/components/com_actionlogs/views/actionlogs/view.html.php', - '/administrator/components/com_admin/admin.php', - '/administrator/components/com_admin/controller.php', - '/administrator/components/com_admin/controllers/profile.php', - '/administrator/components/com_admin/helpers/html/directory.php', - '/administrator/components/com_admin/helpers/html/phpsetting.php', - '/administrator/components/com_admin/helpers/html/system.php', - '/administrator/components/com_admin/models/forms/profile.xml', - '/administrator/components/com_admin/models/help.php', - '/administrator/components/com_admin/models/profile.php', - '/administrator/components/com_admin/models/sysinfo.php', - '/administrator/components/com_admin/postinstall/eaccelerator.php', - '/administrator/components/com_admin/postinstall/htaccess.php', - '/administrator/components/com_admin/postinstall/joomla40checks.php', - '/administrator/components/com_admin/postinstall/updatedefaultsettings.php', - '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-01.sql', - '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-02.sql', - '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-06.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-21-1.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-21-2.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-23.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2012-01-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2012-01-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.1-2012-01-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.2-2012-03-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.3-2012-03-13.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.4-2012-03-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.4-2012-03-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.5.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.6.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.7.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.0.0.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.0.1.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.0.2.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.0.3.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.0.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.1.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.2.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.3.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.4.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.5.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.10.0-2020-08-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.10.0-2021-05-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.10.7-2022-02-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.10.7-2022-03-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.0.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.1.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2013-12-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2013-12-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-08.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-23.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.3-2014-02-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.3.0-2014-02-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.3.0-2014-04-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.3.4-2014-08-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.3.6-2014-09-30.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-08-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-09-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-09-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-10-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-12-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2015-01-21.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2015-02-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-07-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-13.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-30.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-11-04.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-11-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2016-02-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2016-03-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.1-2016-03-25.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.1-2016-03-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-06.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-08.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-09.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-05-06.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-06-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-06-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.3-2016-08-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.3-2016-08-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-06.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-09-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-10-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-10-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-04.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-21.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-27.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-08.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-09.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-17.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-31.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-17.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-09.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-04-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-04-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.3-2017-06-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.4-2017-07-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.0-2017-07-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.0-2017-07-31.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.2-2017-10-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.4-2018-01-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.6-2018-02-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.8-2018-05-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.9-2018-06-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-27.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-12.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-13.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-17.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-09.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-11.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-12.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-09-04.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-21.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.10-2019-07-09.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.16-2020-02-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.16-2020-03-04.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.19-2020-05-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.19-2020-06-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.21-2020-08-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.22-2020-09-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.26-2021-04-07.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.27-2021-04-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.3-2019-01-12.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.3-2019-02-07.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-04-23.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-04-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-05-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.8-2019-06-11.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.8-2019-06-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.0.0.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.0.1.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.0.2.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.0.3.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.0.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.1.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.2.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.3.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.4.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.5.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.10.0-2020-08-10.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.10.0-2021-05-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-02-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-02-20.sql.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-03-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.0.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.1.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2013-12-22.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2013-12-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-23.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.3-2014-02-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2013-12-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2014-02-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2014-04-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.3.4-2014-08-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.3.6-2014-09-30.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-08-24.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-09-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-09-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-10-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-12-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2015-01-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2015-02-26.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.4-2015-07-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-13.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-26.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-30.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-11-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-11-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2016-03-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-09.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-05-06.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-06-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-06-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-08-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-08-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-10-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-06.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-22.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-09-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-10-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-10-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-24.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-09.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-17.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-31.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-17.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-03-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-03-09.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-04-10.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-04-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.4-2017-07-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.0-2017-07-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.0-2017-07-31.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.2-2017-10-14.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.4-2018-01-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.6-2018-02-14.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.8-2018-05-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.9-2018-06-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-24.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-27.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-12.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-13.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-14.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-17.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-09.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-10.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-12.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-09-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.10-2019-07-09.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.15-2020-01-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.16-2020-02-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.16-2020-03-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.19-2020-06-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.21-2020-08-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.22-2020-09-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.26-2021-04-07.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.27-2021-04-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.3-2019-01-12.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.3-2019-02-07.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-04-23.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-04-26.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-05-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.8-2019-06-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.8-2019-06-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.2-2012-03-05.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.3-2012-03-13.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.4-2012-03-18.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.4-2012-03-19.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.5.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.6.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.7.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.0.0.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.0.1.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.0.2.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.0.3.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.0.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.1.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.2.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.3.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.4.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.5.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.10.0-2021-05-28.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.10.1-2021-08-17.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-02-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-02-20.sql.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-03-18.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.0.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.1.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2013-12-22.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2013-12-28.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-08.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-18.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-23.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.3-2014-02-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.3.0-2014-02-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.3.0-2014-04-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.3.4-2014-08-03.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.3.6-2014-09-30.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-08-24.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-09-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-09-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-10-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-12-03.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2015-01-21.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2015-02-26.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.4-2015-07-11.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-13.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-26.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-30.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-11-04.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-11-05.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2016-03-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-06.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-08.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-09.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-05-06.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-06-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-06-05.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.3-2016-08-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.3-2016-08-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-06.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-22.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-29.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-09-29.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-10-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-10-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-04.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-19.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-24.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-08.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-09.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-17.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-31.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-17.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-03-03.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-03-09.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-04-10.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-04-19.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.4-2017-07-05.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.0-2017-07-28.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.0-2017-07-31.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.2-2017-10-14.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.4-2018-01-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.6-2018-02-14.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.8-2018-05-18.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.9-2018-06-19.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-03.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-05.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-19.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-24.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-27.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-12.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-13.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-14.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-17.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-09.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-10.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-11.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-12.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-28.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-29.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-09-04.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-21.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.10-2019-07-09.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.16-2020-03-04.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.19-2020-06-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.21-2020-08-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.22-2020-09-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.26-2021-04-07.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.27-2021-04-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.3-2019-01-12.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.3-2019-02-07.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.4-2019-03-06.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-04-23.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-04-26.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-05-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.8-2019-06-11.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.8-2019-06-15.sql', - '/administrator/components/com_admin/views/help/tmpl/default.php', - '/administrator/components/com_admin/views/help/tmpl/default.xml', - '/administrator/components/com_admin/views/help/tmpl/langforum.php', - '/administrator/components/com_admin/views/help/view.html.php', - '/administrator/components/com_admin/views/profile/tmpl/edit.php', - '/administrator/components/com_admin/views/profile/view.html.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default.xml', - '/administrator/components/com_admin/views/sysinfo/tmpl/default_config.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default_directory.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default_phpinfo.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default_phpsettings.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default_system.php', - '/administrator/components/com_admin/views/sysinfo/view.html.php', - '/administrator/components/com_admin/views/sysinfo/view.json.php', - '/administrator/components/com_admin/views/sysinfo/view.text.php', - '/administrator/components/com_associations/associations.php', - '/administrator/components/com_associations/controller.php', - '/administrator/components/com_associations/controllers/association.php', - '/administrator/components/com_associations/controllers/associations.php', - '/administrator/components/com_associations/helpers/associations.php', - '/administrator/components/com_associations/layouts/joomla/searchtools/default/bar.php', - '/administrator/components/com_associations/models/association.php', - '/administrator/components/com_associations/models/associations.php', - '/administrator/components/com_associations/models/fields/itemlanguage.php', - '/administrator/components/com_associations/models/fields/itemtype.php', - '/administrator/components/com_associations/models/fields/modalassociation.php', - '/administrator/components/com_associations/models/forms/association.xml', - '/administrator/components/com_associations/models/forms/filter_associations.xml', - '/administrator/components/com_associations/views/association/tmpl/edit.php', - '/administrator/components/com_associations/views/association/view.html.php', - '/administrator/components/com_associations/views/associations/tmpl/default.php', - '/administrator/components/com_associations/views/associations/tmpl/default.xml', - '/administrator/components/com_associations/views/associations/tmpl/modal.php', - '/administrator/components/com_associations/views/associations/view.html.php', - '/administrator/components/com_banners/banners.php', - '/administrator/components/com_banners/controller.php', - '/administrator/components/com_banners/controllers/banner.php', - '/administrator/components/com_banners/controllers/banners.php', - '/administrator/components/com_banners/controllers/client.php', - '/administrator/components/com_banners/controllers/clients.php', - '/administrator/components/com_banners/controllers/tracks.php', - '/administrator/components/com_banners/controllers/tracks.raw.php', - '/administrator/components/com_banners/helpers/html/banner.php', - '/administrator/components/com_banners/models/banner.php', - '/administrator/components/com_banners/models/banners.php', - '/administrator/components/com_banners/models/client.php', - '/administrator/components/com_banners/models/clients.php', - '/administrator/components/com_banners/models/download.php', - '/administrator/components/com_banners/models/fields/bannerclient.php', - '/administrator/components/com_banners/models/fields/clicks.php', - '/administrator/components/com_banners/models/fields/impmade.php', - '/administrator/components/com_banners/models/fields/imptotal.php', - '/administrator/components/com_banners/models/forms/banner.xml', - '/administrator/components/com_banners/models/forms/client.xml', - '/administrator/components/com_banners/models/forms/download.xml', - '/administrator/components/com_banners/models/forms/filter_banners.xml', - '/administrator/components/com_banners/models/forms/filter_clients.xml', - '/administrator/components/com_banners/models/forms/filter_tracks.xml', - '/administrator/components/com_banners/models/tracks.php', - '/administrator/components/com_banners/tables/banner.php', - '/administrator/components/com_banners/tables/client.php', - '/administrator/components/com_banners/views/banner/tmpl/edit.php', - '/administrator/components/com_banners/views/banner/view.html.php', - '/administrator/components/com_banners/views/banners/tmpl/default.php', - '/administrator/components/com_banners/views/banners/tmpl/default_batch_body.php', - '/administrator/components/com_banners/views/banners/tmpl/default_batch_footer.php', - '/administrator/components/com_banners/views/banners/view.html.php', - '/administrator/components/com_banners/views/client/tmpl/edit.php', - '/administrator/components/com_banners/views/client/view.html.php', - '/administrator/components/com_banners/views/clients/tmpl/default.php', - '/administrator/components/com_banners/views/clients/view.html.php', - '/administrator/components/com_banners/views/download/tmpl/default.php', - '/administrator/components/com_banners/views/download/view.html.php', - '/administrator/components/com_banners/views/tracks/tmpl/default.php', - '/administrator/components/com_banners/views/tracks/view.html.php', - '/administrator/components/com_banners/views/tracks/view.raw.php', - '/administrator/components/com_cache/cache.php', - '/administrator/components/com_cache/controller.php', - '/administrator/components/com_cache/helpers/cache.php', - '/administrator/components/com_cache/models/cache.php', - '/administrator/components/com_cache/models/forms/filter_cache.xml', - '/administrator/components/com_cache/views/cache/tmpl/default.php', - '/administrator/components/com_cache/views/cache/tmpl/default.xml', - '/administrator/components/com_cache/views/cache/view.html.php', - '/administrator/components/com_cache/views/purge/tmpl/default.php', - '/administrator/components/com_cache/views/purge/tmpl/default.xml', - '/administrator/components/com_cache/views/purge/view.html.php', - '/administrator/components/com_categories/categories.php', - '/administrator/components/com_categories/controller.php', - '/administrator/components/com_categories/controllers/ajax.json.php', - '/administrator/components/com_categories/controllers/categories.php', - '/administrator/components/com_categories/controllers/category.php', - '/administrator/components/com_categories/helpers/association.php', - '/administrator/components/com_categories/helpers/html/categoriesadministrator.php', - '/administrator/components/com_categories/models/categories.php', - '/administrator/components/com_categories/models/category.php', - '/administrator/components/com_categories/models/fields/categoryedit.php', - '/administrator/components/com_categories/models/fields/categoryparent.php', - '/administrator/components/com_categories/models/fields/modal/category.php', - '/administrator/components/com_categories/models/forms/category.xml', - '/administrator/components/com_categories/models/forms/filter_categories.xml', - '/administrator/components/com_categories/tables/category.php', - '/administrator/components/com_categories/views/categories/tmpl/default.php', - '/administrator/components/com_categories/views/categories/tmpl/default.xml', - '/administrator/components/com_categories/views/categories/tmpl/default_batch_body.php', - '/administrator/components/com_categories/views/categories/tmpl/default_batch_footer.php', - '/administrator/components/com_categories/views/categories/tmpl/modal.php', - '/administrator/components/com_categories/views/categories/view.html.php', - '/administrator/components/com_categories/views/category/tmpl/edit.php', - '/administrator/components/com_categories/views/category/tmpl/edit.xml', - '/administrator/components/com_categories/views/category/tmpl/edit_associations.php', - '/administrator/components/com_categories/views/category/tmpl/edit_metadata.php', - '/administrator/components/com_categories/views/category/tmpl/modal.php', - '/administrator/components/com_categories/views/category/tmpl/modal_associations.php', - '/administrator/components/com_categories/views/category/tmpl/modal_extrafields.php', - '/administrator/components/com_categories/views/category/tmpl/modal_metadata.php', - '/administrator/components/com_categories/views/category/tmpl/modal_options.php', - '/administrator/components/com_categories/views/category/view.html.php', - '/administrator/components/com_checkin/checkin.php', - '/administrator/components/com_checkin/controller.php', - '/administrator/components/com_checkin/models/checkin.php', - '/administrator/components/com_checkin/models/forms/filter_checkin.xml', - '/administrator/components/com_checkin/views/checkin/tmpl/default.php', - '/administrator/components/com_checkin/views/checkin/tmpl/default.xml', - '/administrator/components/com_checkin/views/checkin/view.html.php', - '/administrator/components/com_config/config.php', - '/administrator/components/com_config/controller.php', - '/administrator/components/com_config/controller/application/cancel.php', - '/administrator/components/com_config/controller/application/display.php', - '/administrator/components/com_config/controller/application/removeroot.php', - '/administrator/components/com_config/controller/application/save.php', - '/administrator/components/com_config/controller/application/sendtestmail.php', - '/administrator/components/com_config/controller/application/store.php', - '/administrator/components/com_config/controller/component/cancel.php', - '/administrator/components/com_config/controller/component/display.php', - '/administrator/components/com_config/controller/component/save.php', - '/administrator/components/com_config/controllers/application.php', - '/administrator/components/com_config/controllers/component.php', - '/administrator/components/com_config/helper/config.php', - '/administrator/components/com_config/model/application.php', - '/administrator/components/com_config/model/component.php', - '/administrator/components/com_config/model/field/configcomponents.php', - '/administrator/components/com_config/model/field/filters.php', - '/administrator/components/com_config/model/form/application.xml', - '/administrator/components/com_config/models/application.php', - '/administrator/components/com_config/models/component.php', - '/administrator/components/com_config/view/application/html.php', - '/administrator/components/com_config/view/application/json.php', - '/administrator/components/com_config/view/application/tmpl/default.php', - '/administrator/components/com_config/view/application/tmpl/default.xml', - '/administrator/components/com_config/view/application/tmpl/default_cache.php', - '/administrator/components/com_config/view/application/tmpl/default_cookie.php', - '/administrator/components/com_config/view/application/tmpl/default_database.php', - '/administrator/components/com_config/view/application/tmpl/default_debug.php', - '/administrator/components/com_config/view/application/tmpl/default_filters.php', - '/administrator/components/com_config/view/application/tmpl/default_ftp.php', - '/administrator/components/com_config/view/application/tmpl/default_ftplogin.php', - '/administrator/components/com_config/view/application/tmpl/default_locale.php', - '/administrator/components/com_config/view/application/tmpl/default_mail.php', - '/administrator/components/com_config/view/application/tmpl/default_metadata.php', - '/administrator/components/com_config/view/application/tmpl/default_navigation.php', - '/administrator/components/com_config/view/application/tmpl/default_permissions.php', - '/administrator/components/com_config/view/application/tmpl/default_proxy.php', - '/administrator/components/com_config/view/application/tmpl/default_seo.php', - '/administrator/components/com_config/view/application/tmpl/default_server.php', - '/administrator/components/com_config/view/application/tmpl/default_session.php', - '/administrator/components/com_config/view/application/tmpl/default_site.php', - '/administrator/components/com_config/view/application/tmpl/default_system.php', - '/administrator/components/com_config/view/component/html.php', - '/administrator/components/com_config/view/component/tmpl/default.php', - '/administrator/components/com_config/view/component/tmpl/default.xml', - '/administrator/components/com_config/view/component/tmpl/default_navigation.php', - '/administrator/components/com_contact/contact.php', - '/administrator/components/com_contact/controller.php', - '/administrator/components/com_contact/controllers/ajax.json.php', - '/administrator/components/com_contact/controllers/contact.php', - '/administrator/components/com_contact/controllers/contacts.php', - '/administrator/components/com_contact/helpers/associations.php', - '/administrator/components/com_contact/helpers/html/contact.php', - '/administrator/components/com_contact/models/contact.php', - '/administrator/components/com_contact/models/contacts.php', - '/administrator/components/com_contact/models/fields/modal/contact.php', - '/administrator/components/com_contact/models/forms/contact.xml', - '/administrator/components/com_contact/models/forms/fields/mail.xml', - '/administrator/components/com_contact/models/forms/filter_contacts.xml', - '/administrator/components/com_contact/tables/contact.php', - '/administrator/components/com_contact/views/contact/tmpl/edit.php', - '/administrator/components/com_contact/views/contact/tmpl/edit_associations.php', - '/administrator/components/com_contact/views/contact/tmpl/edit_metadata.php', - '/administrator/components/com_contact/views/contact/tmpl/edit_params.php', - '/administrator/components/com_contact/views/contact/tmpl/modal.php', - '/administrator/components/com_contact/views/contact/tmpl/modal_associations.php', - '/administrator/components/com_contact/views/contact/tmpl/modal_metadata.php', - '/administrator/components/com_contact/views/contact/tmpl/modal_params.php', - '/administrator/components/com_contact/views/contact/view.html.php', - '/administrator/components/com_contact/views/contacts/tmpl/default.php', - '/administrator/components/com_contact/views/contacts/tmpl/default_batch.php', - '/administrator/components/com_contact/views/contacts/tmpl/default_batch_body.php', - '/administrator/components/com_contact/views/contacts/tmpl/default_batch_footer.php', - '/administrator/components/com_contact/views/contacts/tmpl/modal.php', - '/administrator/components/com_contact/views/contacts/view.html.php', - '/administrator/components/com_content/content.php', - '/administrator/components/com_content/controller.php', - '/administrator/components/com_content/controllers/ajax.json.php', - '/administrator/components/com_content/controllers/article.php', - '/administrator/components/com_content/controllers/articles.php', - '/administrator/components/com_content/controllers/featured.php', - '/administrator/components/com_content/helpers/associations.php', - '/administrator/components/com_content/helpers/html/contentadministrator.php', - '/administrator/components/com_content/models/article.php', - '/administrator/components/com_content/models/articles.php', - '/administrator/components/com_content/models/feature.php', - '/administrator/components/com_content/models/featured.php', - '/administrator/components/com_content/models/fields/modal/article.php', - '/administrator/components/com_content/models/fields/voteradio.php', - '/administrator/components/com_content/models/forms/article.xml', - '/administrator/components/com_content/models/forms/filter_articles.xml', - '/administrator/components/com_content/models/forms/filter_featured.xml', - '/administrator/components/com_content/tables/featured.php', - '/administrator/components/com_content/views/article/tmpl/edit.php', - '/administrator/components/com_content/views/article/tmpl/edit.xml', - '/administrator/components/com_content/views/article/tmpl/edit_associations.php', - '/administrator/components/com_content/views/article/tmpl/edit_metadata.php', - '/administrator/components/com_content/views/article/tmpl/modal.php', - '/administrator/components/com_content/views/article/tmpl/modal_associations.php', - '/administrator/components/com_content/views/article/tmpl/modal_metadata.php', - '/administrator/components/com_content/views/article/tmpl/pagebreak.php', - '/administrator/components/com_content/views/article/view.html.php', - '/administrator/components/com_content/views/articles/tmpl/default.php', - '/administrator/components/com_content/views/articles/tmpl/default.xml', - '/administrator/components/com_content/views/articles/tmpl/default_batch_body.php', - '/administrator/components/com_content/views/articles/tmpl/default_batch_footer.php', - '/administrator/components/com_content/views/articles/tmpl/modal.php', - '/administrator/components/com_content/views/articles/view.html.php', - '/administrator/components/com_content/views/featured/tmpl/default.php', - '/administrator/components/com_content/views/featured/tmpl/default.xml', - '/administrator/components/com_content/views/featured/view.html.php', - '/administrator/components/com_contenthistory/contenthistory.php', - '/administrator/components/com_contenthistory/controller.php', - '/administrator/components/com_contenthistory/controllers/history.php', - '/administrator/components/com_contenthistory/controllers/preview.php', - '/administrator/components/com_contenthistory/helpers/html/textdiff.php', - '/administrator/components/com_contenthistory/models/compare.php', - '/administrator/components/com_contenthistory/models/history.php', - '/administrator/components/com_contenthistory/models/preview.php', - '/administrator/components/com_contenthistory/views/compare/tmpl/compare.php', - '/administrator/components/com_contenthistory/views/compare/view.html.php', - '/administrator/components/com_contenthistory/views/history/tmpl/modal.php', - '/administrator/components/com_contenthistory/views/history/view.html.php', - '/administrator/components/com_contenthistory/views/preview/tmpl/preview.php', - '/administrator/components/com_contenthistory/views/preview/view.html.php', - '/administrator/components/com_cpanel/controller.php', - '/administrator/components/com_cpanel/cpanel.php', - '/administrator/components/com_cpanel/views/cpanel/tmpl/default.php', - '/administrator/components/com_cpanel/views/cpanel/tmpl/default.xml', - '/administrator/components/com_cpanel/views/cpanel/view.html.php', - '/administrator/components/com_fields/controller.php', - '/administrator/components/com_fields/controllers/field.php', - '/administrator/components/com_fields/controllers/fields.php', - '/administrator/components/com_fields/controllers/group.php', - '/administrator/components/com_fields/controllers/groups.php', - '/administrator/components/com_fields/fields.php', - '/administrator/components/com_fields/libraries/fieldslistplugin.php', - '/administrator/components/com_fields/libraries/fieldsplugin.php', - '/administrator/components/com_fields/models/field.php', - '/administrator/components/com_fields/models/fields.php', - '/administrator/components/com_fields/models/fields/fieldcontexts.php', - '/administrator/components/com_fields/models/fields/fieldgroups.php', - '/administrator/components/com_fields/models/fields/fieldlayout.php', - '/administrator/components/com_fields/models/fields/section.php', - '/administrator/components/com_fields/models/fields/type.php', - '/administrator/components/com_fields/models/forms/field.xml', - '/administrator/components/com_fields/models/forms/filter_fields.xml', - '/administrator/components/com_fields/models/forms/filter_groups.xml', - '/administrator/components/com_fields/models/forms/group.xml', - '/administrator/components/com_fields/models/group.php', - '/administrator/components/com_fields/models/groups.php', - '/administrator/components/com_fields/tables/field.php', - '/administrator/components/com_fields/tables/group.php', - '/administrator/components/com_fields/views/field/tmpl/edit.php', - '/administrator/components/com_fields/views/field/view.html.php', - '/administrator/components/com_fields/views/fields/tmpl/default.php', - '/administrator/components/com_fields/views/fields/tmpl/default_batch_body.php', - '/administrator/components/com_fields/views/fields/tmpl/default_batch_footer.php', - '/administrator/components/com_fields/views/fields/tmpl/modal.php', - '/administrator/components/com_fields/views/fields/view.html.php', - '/administrator/components/com_fields/views/group/tmpl/edit.php', - '/administrator/components/com_fields/views/group/view.html.php', - '/administrator/components/com_fields/views/groups/tmpl/default.php', - '/administrator/components/com_fields/views/groups/tmpl/default_batch_body.php', - '/administrator/components/com_fields/views/groups/tmpl/default_batch_footer.php', - '/administrator/components/com_fields/views/groups/view.html.php', - '/administrator/components/com_finder/controller.php', - '/administrator/components/com_finder/controllers/filter.php', - '/administrator/components/com_finder/controllers/filters.php', - '/administrator/components/com_finder/controllers/index.php', - '/administrator/components/com_finder/controllers/indexer.json.php', - '/administrator/components/com_finder/controllers/maps.php', - '/administrator/components/com_finder/finder.php', - '/administrator/components/com_finder/helpers/finder.php', - '/administrator/components/com_finder/helpers/html/finder.php', - '/administrator/components/com_finder/helpers/indexer/driver/mysql.php', - '/administrator/components/com_finder/helpers/indexer/driver/postgresql.php', - '/administrator/components/com_finder/helpers/indexer/driver/sqlsrv.php', - '/administrator/components/com_finder/helpers/indexer/indexer.php', - '/administrator/components/com_finder/helpers/indexer/parser/html.php', - '/administrator/components/com_finder/helpers/indexer/parser/rtf.php', - '/administrator/components/com_finder/helpers/indexer/parser/txt.php', - '/administrator/components/com_finder/helpers/indexer/stemmer.php', - '/administrator/components/com_finder/helpers/indexer/stemmer/fr.php', - '/administrator/components/com_finder/helpers/indexer/stemmer/porter_en.php', - '/administrator/components/com_finder/helpers/indexer/stemmer/snowball.php', - '/administrator/components/com_finder/models/fields/branches.php', - '/administrator/components/com_finder/models/fields/contentmap.php', - '/administrator/components/com_finder/models/fields/contenttypes.php', - '/administrator/components/com_finder/models/fields/directories.php', - '/administrator/components/com_finder/models/fields/searchfilter.php', - '/administrator/components/com_finder/models/filter.php', - '/administrator/components/com_finder/models/filters.php', - '/administrator/components/com_finder/models/forms/filter.xml', - '/administrator/components/com_finder/models/forms/filter_filters.xml', - '/administrator/components/com_finder/models/forms/filter_index.xml', - '/administrator/components/com_finder/models/forms/filter_maps.xml', - '/administrator/components/com_finder/models/index.php', - '/administrator/components/com_finder/models/indexer.php', - '/administrator/components/com_finder/models/maps.php', - '/administrator/components/com_finder/models/statistics.php', - '/administrator/components/com_finder/tables/filter.php', - '/administrator/components/com_finder/tables/link.php', - '/administrator/components/com_finder/tables/map.php', - '/administrator/components/com_finder/views/filter/tmpl/edit.php', - '/administrator/components/com_finder/views/filter/view.html.php', - '/administrator/components/com_finder/views/filters/tmpl/default.php', - '/administrator/components/com_finder/views/filters/view.html.php', - '/administrator/components/com_finder/views/index/tmpl/default.php', - '/administrator/components/com_finder/views/index/view.html.php', - '/administrator/components/com_finder/views/indexer/tmpl/default.php', - '/administrator/components/com_finder/views/indexer/view.html.php', - '/administrator/components/com_finder/views/maps/tmpl/default.php', - '/administrator/components/com_finder/views/maps/view.html.php', - '/administrator/components/com_finder/views/statistics/tmpl/default.php', - '/administrator/components/com_finder/views/statistics/view.html.php', - '/administrator/components/com_installer/controller.php', - '/administrator/components/com_installer/controllers/database.php', - '/administrator/components/com_installer/controllers/discover.php', - '/administrator/components/com_installer/controllers/install.php', - '/administrator/components/com_installer/controllers/manage.php', - '/administrator/components/com_installer/controllers/update.php', - '/administrator/components/com_installer/controllers/updatesites.php', - '/administrator/components/com_installer/helpers/html/manage.php', - '/administrator/components/com_installer/helpers/html/updatesites.php', - '/administrator/components/com_installer/installer.php', - '/administrator/components/com_installer/models/database.php', - '/administrator/components/com_installer/models/discover.php', - '/administrator/components/com_installer/models/extension.php', - '/administrator/components/com_installer/models/fields/extensionstatus.php', - '/administrator/components/com_installer/models/fields/folder.php', - '/administrator/components/com_installer/models/fields/location.php', - '/administrator/components/com_installer/models/fields/type.php', - '/administrator/components/com_installer/models/forms/filter_discover.xml', - '/administrator/components/com_installer/models/forms/filter_languages.xml', - '/administrator/components/com_installer/models/forms/filter_manage.xml', - '/administrator/components/com_installer/models/forms/filter_update.xml', - '/administrator/components/com_installer/models/forms/filter_updatesites.xml', - '/administrator/components/com_installer/models/install.php', - '/administrator/components/com_installer/models/languages.php', - '/administrator/components/com_installer/models/manage.php', - '/administrator/components/com_installer/models/update.php', - '/administrator/components/com_installer/models/updatesites.php', - '/administrator/components/com_installer/models/warnings.php', - '/administrator/components/com_installer/views/database/tmpl/default.php', - '/administrator/components/com_installer/views/database/tmpl/default.xml', - '/administrator/components/com_installer/views/database/view.html.php', - '/administrator/components/com_installer/views/default/tmpl/default_ftp.php', - '/administrator/components/com_installer/views/default/tmpl/default_message.php', - '/administrator/components/com_installer/views/default/view.php', - '/administrator/components/com_installer/views/discover/tmpl/default.php', - '/administrator/components/com_installer/views/discover/tmpl/default.xml', - '/administrator/components/com_installer/views/discover/tmpl/default_item.php', - '/administrator/components/com_installer/views/discover/view.html.php', - '/administrator/components/com_installer/views/install/tmpl/default.php', - '/administrator/components/com_installer/views/install/tmpl/default.xml', - '/administrator/components/com_installer/views/install/view.html.php', - '/administrator/components/com_installer/views/languages/tmpl/default.php', - '/administrator/components/com_installer/views/languages/tmpl/default.xml', - '/administrator/components/com_installer/views/languages/view.html.php', - '/administrator/components/com_installer/views/manage/tmpl/default.php', - '/administrator/components/com_installer/views/manage/tmpl/default.xml', - '/administrator/components/com_installer/views/manage/view.html.php', - '/administrator/components/com_installer/views/update/tmpl/default.php', - '/administrator/components/com_installer/views/update/tmpl/default.xml', - '/administrator/components/com_installer/views/update/view.html.php', - '/administrator/components/com_installer/views/updatesites/tmpl/default.php', - '/administrator/components/com_installer/views/updatesites/tmpl/default.xml', - '/administrator/components/com_installer/views/updatesites/view.html.php', - '/administrator/components/com_installer/views/warnings/tmpl/default.php', - '/administrator/components/com_installer/views/warnings/tmpl/default.xml', - '/administrator/components/com_installer/views/warnings/view.html.php', - '/administrator/components/com_joomlaupdate/controller.php', - '/administrator/components/com_joomlaupdate/controllers/update.php', - '/administrator/components/com_joomlaupdate/helpers/joomlaupdate.php', - '/administrator/components/com_joomlaupdate/helpers/select.php', - '/administrator/components/com_joomlaupdate/joomlaupdate.php', - '/administrator/components/com_joomlaupdate/models/default.php', - '/administrator/components/com_joomlaupdate/restore.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/complete.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default.xml', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_nodownload.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_noupdate.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_preupdatecheck.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_reinstall.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_update.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_updatemefirst.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_upload.php', - '/administrator/components/com_joomlaupdate/views/default/view.html.php', - '/administrator/components/com_joomlaupdate/views/update/tmpl/default.php', - '/administrator/components/com_joomlaupdate/views/update/tmpl/finaliseconfirm.php', - '/administrator/components/com_joomlaupdate/views/update/view.html.php', - '/administrator/components/com_joomlaupdate/views/upload/tmpl/captive.php', - '/administrator/components/com_joomlaupdate/views/upload/view.html.php', - '/administrator/components/com_languages/controller.php', - '/administrator/components/com_languages/controllers/installed.php', - '/administrator/components/com_languages/controllers/language.php', - '/administrator/components/com_languages/controllers/languages.php', - '/administrator/components/com_languages/controllers/override.php', - '/administrator/components/com_languages/controllers/overrides.php', - '/administrator/components/com_languages/controllers/strings.json.php', - '/administrator/components/com_languages/helpers/html/languages.php', - '/administrator/components/com_languages/helpers/jsonresponse.php', - '/administrator/components/com_languages/helpers/languages.php', - '/administrator/components/com_languages/helpers/multilangstatus.php', - '/administrator/components/com_languages/languages.php', - '/administrator/components/com_languages/layouts/joomla/searchtools/default/bar.php', - '/administrator/components/com_languages/models/fields/languageclient.php', - '/administrator/components/com_languages/models/forms/filter_installed.xml', - '/administrator/components/com_languages/models/forms/filter_languages.xml', - '/administrator/components/com_languages/models/forms/filter_overrides.xml', - '/administrator/components/com_languages/models/forms/language.xml', - '/administrator/components/com_languages/models/forms/override.xml', - '/administrator/components/com_languages/models/installed.php', - '/administrator/components/com_languages/models/language.php', - '/administrator/components/com_languages/models/languages.php', - '/administrator/components/com_languages/models/override.php', - '/administrator/components/com_languages/models/overrides.php', - '/administrator/components/com_languages/models/strings.php', - '/administrator/components/com_languages/views/installed/tmpl/default.php', - '/administrator/components/com_languages/views/installed/tmpl/default.xml', - '/administrator/components/com_languages/views/installed/view.html.php', - '/administrator/components/com_languages/views/language/tmpl/edit.php', - '/administrator/components/com_languages/views/language/view.html.php', - '/administrator/components/com_languages/views/languages/tmpl/default.php', - '/administrator/components/com_languages/views/languages/tmpl/default.xml', - '/administrator/components/com_languages/views/languages/view.html.php', - '/administrator/components/com_languages/views/multilangstatus/tmpl/default.php', - '/administrator/components/com_languages/views/multilangstatus/view.html.php', - '/administrator/components/com_languages/views/override/tmpl/edit.php', - '/administrator/components/com_languages/views/override/view.html.php', - '/administrator/components/com_languages/views/overrides/tmpl/default.php', - '/administrator/components/com_languages/views/overrides/tmpl/default.xml', - '/administrator/components/com_languages/views/overrides/view.html.php', - '/administrator/components/com_login/controller.php', - '/administrator/components/com_login/login.php', - '/administrator/components/com_login/models/login.php', - '/administrator/components/com_login/views/login/tmpl/default.php', - '/administrator/components/com_login/views/login/view.html.php', - '/administrator/components/com_media/controller.php', - '/administrator/components/com_media/controllers/file.json.php', - '/administrator/components/com_media/controllers/file.php', - '/administrator/components/com_media/controllers/folder.php', - '/administrator/components/com_media/layouts/toolbar/deletemedia.php', - '/administrator/components/com_media/layouts/toolbar/newfolder.php', - '/administrator/components/com_media/layouts/toolbar/uploadmedia.php', - '/administrator/components/com_media/media.php', - '/administrator/components/com_media/models/list.php', - '/administrator/components/com_media/models/manager.php', - '/administrator/components/com_media/views/images/tmpl/default.php', - '/administrator/components/com_media/views/images/view.html.php', - '/administrator/components/com_media/views/imageslist/tmpl/default.php', - '/administrator/components/com_media/views/imageslist/tmpl/default_folder.php', - '/administrator/components/com_media/views/imageslist/tmpl/default_image.php', - '/administrator/components/com_media/views/imageslist/view.html.php', - '/administrator/components/com_media/views/media/tmpl/default.php', - '/administrator/components/com_media/views/media/tmpl/default.xml', - '/administrator/components/com_media/views/media/tmpl/default_folders.php', - '/administrator/components/com_media/views/media/tmpl/default_navigation.php', - '/administrator/components/com_media/views/media/view.html.php', - '/administrator/components/com_media/views/medialist/tmpl/default.php', - '/administrator/components/com_media/views/medialist/tmpl/details.php', - '/administrator/components/com_media/views/medialist/tmpl/details_doc.php', - '/administrator/components/com_media/views/medialist/tmpl/details_docs.php', - '/administrator/components/com_media/views/medialist/tmpl/details_folder.php', - '/administrator/components/com_media/views/medialist/tmpl/details_folders.php', - '/administrator/components/com_media/views/medialist/tmpl/details_img.php', - '/administrator/components/com_media/views/medialist/tmpl/details_imgs.php', - '/administrator/components/com_media/views/medialist/tmpl/details_up.php', - '/administrator/components/com_media/views/medialist/tmpl/details_video.php', - '/administrator/components/com_media/views/medialist/tmpl/details_videos.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs_docs.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs_folders.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs_imgs.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs_up.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs_videos.php', - '/administrator/components/com_media/views/medialist/view.html.php', - '/administrator/components/com_menus/controller.php', - '/administrator/components/com_menus/controllers/ajax.json.php', - '/administrator/components/com_menus/controllers/item.php', - '/administrator/components/com_menus/controllers/items.php', - '/administrator/components/com_menus/controllers/menu.php', - '/administrator/components/com_menus/controllers/menus.php', - '/administrator/components/com_menus/helpers/associations.php', - '/administrator/components/com_menus/helpers/html/menus.php', - '/administrator/components/com_menus/layouts/joomla/searchtools/default/bar.php', - '/administrator/components/com_menus/menus.php', - '/administrator/components/com_menus/models/fields/componentscategory.php', - '/administrator/components/com_menus/models/fields/menuitembytype.php', - '/administrator/components/com_menus/models/fields/menuordering.php', - '/administrator/components/com_menus/models/fields/menuparent.php', - '/administrator/components/com_menus/models/fields/menupreset.php', - '/administrator/components/com_menus/models/fields/menutype.php', - '/administrator/components/com_menus/models/fields/modal/menu.php', - '/administrator/components/com_menus/models/forms/filter_items.xml', - '/administrator/components/com_menus/models/forms/filter_itemsadmin.xml', - '/administrator/components/com_menus/models/forms/filter_menus.xml', - '/administrator/components/com_menus/models/forms/item.xml', - '/administrator/components/com_menus/models/forms/item_alias.xml', - '/administrator/components/com_menus/models/forms/item_component.xml', - '/administrator/components/com_menus/models/forms/item_heading.xml', - '/administrator/components/com_menus/models/forms/item_separator.xml', - '/administrator/components/com_menus/models/forms/item_url.xml', - '/administrator/components/com_menus/models/forms/itemadmin.xml', - '/administrator/components/com_menus/models/forms/itemadmin_alias.xml', - '/administrator/components/com_menus/models/forms/itemadmin_component.xml', - '/administrator/components/com_menus/models/forms/itemadmin_container.xml', - '/administrator/components/com_menus/models/forms/itemadmin_heading.xml', - '/administrator/components/com_menus/models/forms/itemadmin_separator.xml', - '/administrator/components/com_menus/models/forms/itemadmin_url.xml', - '/administrator/components/com_menus/models/forms/menu.xml', - '/administrator/components/com_menus/models/item.php', - '/administrator/components/com_menus/models/items.php', - '/administrator/components/com_menus/models/menu.php', - '/administrator/components/com_menus/models/menus.php', - '/administrator/components/com_menus/models/menutypes.php', - '/administrator/components/com_menus/presets/joomla.xml', - '/administrator/components/com_menus/presets/modern.xml', - '/administrator/components/com_menus/tables/menu.php', - '/administrator/components/com_menus/views/item/tmpl/edit.php', - '/administrator/components/com_menus/views/item/tmpl/edit.xml', - '/administrator/components/com_menus/views/item/tmpl/edit_associations.php', - '/administrator/components/com_menus/views/item/tmpl/edit_container.php', - '/administrator/components/com_menus/views/item/tmpl/edit_modules.php', - '/administrator/components/com_menus/views/item/tmpl/edit_options.php', - '/administrator/components/com_menus/views/item/tmpl/modal.php', - '/administrator/components/com_menus/views/item/tmpl/modal_associations.php', - '/administrator/components/com_menus/views/item/tmpl/modal_options.php', - '/administrator/components/com_menus/views/item/view.html.php', - '/administrator/components/com_menus/views/items/tmpl/default.php', - '/administrator/components/com_menus/views/items/tmpl/default.xml', - '/administrator/components/com_menus/views/items/tmpl/default_batch_body.php', - '/administrator/components/com_menus/views/items/tmpl/default_batch_footer.php', - '/administrator/components/com_menus/views/items/tmpl/modal.php', - '/administrator/components/com_menus/views/items/view.html.php', - '/administrator/components/com_menus/views/menu/tmpl/edit.php', - '/administrator/components/com_menus/views/menu/tmpl/edit.xml', - '/administrator/components/com_menus/views/menu/view.html.php', - '/administrator/components/com_menus/views/menu/view.xml.php', - '/administrator/components/com_menus/views/menus/tmpl/default.php', - '/administrator/components/com_menus/views/menus/tmpl/default.xml', - '/administrator/components/com_menus/views/menus/view.html.php', - '/administrator/components/com_menus/views/menutypes/tmpl/default.php', - '/administrator/components/com_menus/views/menutypes/view.html.php', - '/administrator/components/com_messages/controller.php', - '/administrator/components/com_messages/controllers/config.php', - '/administrator/components/com_messages/controllers/message.php', - '/administrator/components/com_messages/controllers/messages.php', - '/administrator/components/com_messages/helpers/html/messages.php', - '/administrator/components/com_messages/helpers/messages.php', - '/administrator/components/com_messages/messages.php', - '/administrator/components/com_messages/models/config.php', - '/administrator/components/com_messages/models/fields/messagestates.php', - '/administrator/components/com_messages/models/fields/usermessages.php', - '/administrator/components/com_messages/models/forms/config.xml', - '/administrator/components/com_messages/models/forms/filter_messages.xml', - '/administrator/components/com_messages/models/forms/message.xml', - '/administrator/components/com_messages/models/message.php', - '/administrator/components/com_messages/models/messages.php', - '/administrator/components/com_messages/tables/message.php', - '/administrator/components/com_messages/views/config/tmpl/default.php', - '/administrator/components/com_messages/views/config/view.html.php', - '/administrator/components/com_messages/views/message/tmpl/default.php', - '/administrator/components/com_messages/views/message/tmpl/edit.php', - '/administrator/components/com_messages/views/message/view.html.php', - '/administrator/components/com_messages/views/messages/tmpl/default.php', - '/administrator/components/com_messages/views/messages/view.html.php', - '/administrator/components/com_modules/controller.php', - '/administrator/components/com_modules/controllers/module.php', - '/administrator/components/com_modules/controllers/modules.php', - '/administrator/components/com_modules/helpers/html/modules.php', - '/administrator/components/com_modules/helpers/xml.php', - '/administrator/components/com_modules/layouts/toolbar/newmodule.php', - '/administrator/components/com_modules/models/fields/modulesmodule.php', - '/administrator/components/com_modules/models/fields/modulesposition.php', - '/administrator/components/com_modules/models/forms/advanced.xml', - '/administrator/components/com_modules/models/forms/filter_modules.xml', - '/administrator/components/com_modules/models/forms/filter_modulesadmin.xml', - '/administrator/components/com_modules/models/forms/module.xml', - '/administrator/components/com_modules/models/forms/moduleadmin.xml', - '/administrator/components/com_modules/models/module.php', - '/administrator/components/com_modules/models/modules.php', - '/administrator/components/com_modules/models/positions.php', - '/administrator/components/com_modules/models/select.php', - '/administrator/components/com_modules/modules.php', - '/administrator/components/com_modules/views/module/tmpl/edit.php', - '/administrator/components/com_modules/views/module/tmpl/edit_assignment.php', - '/administrator/components/com_modules/views/module/tmpl/edit_options.php', - '/administrator/components/com_modules/views/module/tmpl/edit_positions.php', - '/administrator/components/com_modules/views/module/tmpl/modal.php', - '/administrator/components/com_modules/views/module/view.html.php', - '/administrator/components/com_modules/views/module/view.json.php', - '/administrator/components/com_modules/views/modules/tmpl/default.php', - '/administrator/components/com_modules/views/modules/tmpl/default.xml', - '/administrator/components/com_modules/views/modules/tmpl/default_batch_body.php', - '/administrator/components/com_modules/views/modules/tmpl/default_batch_footer.php', - '/administrator/components/com_modules/views/modules/tmpl/modal.php', - '/administrator/components/com_modules/views/modules/view.html.php', - '/administrator/components/com_modules/views/positions/tmpl/modal.php', - '/administrator/components/com_modules/views/positions/view.html.php', - '/administrator/components/com_modules/views/preview/tmpl/default.php', - '/administrator/components/com_modules/views/preview/view.html.php', - '/administrator/components/com_modules/views/select/tmpl/default.php', - '/administrator/components/com_modules/views/select/view.html.php', - '/administrator/components/com_newsfeeds/controller.php', - '/administrator/components/com_newsfeeds/controllers/ajax.json.php', - '/administrator/components/com_newsfeeds/controllers/newsfeed.php', - '/administrator/components/com_newsfeeds/controllers/newsfeeds.php', - '/administrator/components/com_newsfeeds/helpers/associations.php', - '/administrator/components/com_newsfeeds/helpers/html/newsfeed.php', - '/administrator/components/com_newsfeeds/models/fields/modal/newsfeed.php', - '/administrator/components/com_newsfeeds/models/fields/newsfeeds.php', - '/administrator/components/com_newsfeeds/models/forms/filter_newsfeeds.xml', - '/administrator/components/com_newsfeeds/models/forms/newsfeed.xml', - '/administrator/components/com_newsfeeds/models/newsfeed.php', - '/administrator/components/com_newsfeeds/models/newsfeeds.php', - '/administrator/components/com_newsfeeds/newsfeeds.php', - '/administrator/components/com_newsfeeds/tables/newsfeed.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_associations.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_display.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_metadata.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_params.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_associations.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_display.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_metadata.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_params.php', - '/administrator/components/com_newsfeeds/views/newsfeed/view.html.php', - '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default.php', - '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default_batch_body.php', - '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default_batch_footer.php', - '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/modal.php', - '/administrator/components/com_newsfeeds/views/newsfeeds/view.html.php', - '/administrator/components/com_plugins/controller.php', - '/administrator/components/com_plugins/controllers/plugin.php', - '/administrator/components/com_plugins/controllers/plugins.php', - '/administrator/components/com_plugins/models/fields/pluginelement.php', - '/administrator/components/com_plugins/models/fields/pluginordering.php', - '/administrator/components/com_plugins/models/fields/plugintype.php', - '/administrator/components/com_plugins/models/forms/filter_plugins.xml', - '/administrator/components/com_plugins/models/forms/plugin.xml', - '/administrator/components/com_plugins/models/plugin.php', - '/administrator/components/com_plugins/models/plugins.php', - '/administrator/components/com_plugins/plugins.php', - '/administrator/components/com_plugins/views/plugin/tmpl/edit.php', - '/administrator/components/com_plugins/views/plugin/tmpl/edit_options.php', - '/administrator/components/com_plugins/views/plugin/tmpl/modal.php', - '/administrator/components/com_plugins/views/plugin/view.html.php', - '/administrator/components/com_plugins/views/plugins/tmpl/default.php', - '/administrator/components/com_plugins/views/plugins/tmpl/default.xml', - '/administrator/components/com_plugins/views/plugins/view.html.php', - '/administrator/components/com_postinstall/controllers/message.php', - '/administrator/components/com_postinstall/fof.xml', - '/administrator/components/com_postinstall/models/messages.php', - '/administrator/components/com_postinstall/postinstall.php', - '/administrator/components/com_postinstall/toolbar.php', - '/administrator/components/com_postinstall/views/messages/tmpl/default.php', - '/administrator/components/com_postinstall/views/messages/tmpl/default.xml', - '/administrator/components/com_postinstall/views/messages/view.html.php', - '/administrator/components/com_privacy/controller.php', - '/administrator/components/com_privacy/controllers/consents.php', - '/administrator/components/com_privacy/controllers/request.php', - '/administrator/components/com_privacy/controllers/request.xml.php', - '/administrator/components/com_privacy/controllers/requests.php', - '/administrator/components/com_privacy/helpers/export/domain.php', - '/administrator/components/com_privacy/helpers/export/field.php', - '/administrator/components/com_privacy/helpers/export/item.php', - '/administrator/components/com_privacy/helpers/html/helper.php', - '/administrator/components/com_privacy/helpers/plugin.php', - '/administrator/components/com_privacy/helpers/privacy.php', - '/administrator/components/com_privacy/helpers/removal/status.php', - '/administrator/components/com_privacy/models/capabilities.php', - '/administrator/components/com_privacy/models/consents.php', - '/administrator/components/com_privacy/models/dashboard.php', - '/administrator/components/com_privacy/models/export.php', - '/administrator/components/com_privacy/models/fields/requeststatus.php', - '/administrator/components/com_privacy/models/fields/requesttype.php', - '/administrator/components/com_privacy/models/forms/filter_consents.xml', - '/administrator/components/com_privacy/models/forms/filter_requests.xml', - '/administrator/components/com_privacy/models/forms/request.xml', - '/administrator/components/com_privacy/models/remove.php', - '/administrator/components/com_privacy/models/request.php', - '/administrator/components/com_privacy/models/requests.php', - '/administrator/components/com_privacy/privacy.php', - '/administrator/components/com_privacy/tables/consent.php', - '/administrator/components/com_privacy/tables/request.php', - '/administrator/components/com_privacy/views/capabilities/tmpl/default.php', - '/administrator/components/com_privacy/views/capabilities/view.html.php', - '/administrator/components/com_privacy/views/consents/tmpl/default.php', - '/administrator/components/com_privacy/views/consents/tmpl/default.xml', - '/administrator/components/com_privacy/views/consents/view.html.php', - '/administrator/components/com_privacy/views/dashboard/tmpl/default.php', - '/administrator/components/com_privacy/views/dashboard/tmpl/default.xml', - '/administrator/components/com_privacy/views/dashboard/view.html.php', - '/administrator/components/com_privacy/views/export/view.xml.php', - '/administrator/components/com_privacy/views/request/tmpl/default.php', - '/administrator/components/com_privacy/views/request/tmpl/edit.php', - '/administrator/components/com_privacy/views/request/view.html.php', - '/administrator/components/com_privacy/views/requests/tmpl/default.php', - '/administrator/components/com_privacy/views/requests/tmpl/default.xml', - '/administrator/components/com_privacy/views/requests/view.html.php', - '/administrator/components/com_redirect/controller.php', - '/administrator/components/com_redirect/controllers/link.php', - '/administrator/components/com_redirect/controllers/links.php', - '/administrator/components/com_redirect/helpers/html/redirect.php', - '/administrator/components/com_redirect/models/fields/redirect.php', - '/administrator/components/com_redirect/models/forms/filter_links.xml', - '/administrator/components/com_redirect/models/forms/link.xml', - '/administrator/components/com_redirect/models/link.php', - '/administrator/components/com_redirect/models/links.php', - '/administrator/components/com_redirect/redirect.php', - '/administrator/components/com_redirect/tables/link.php', - '/administrator/components/com_redirect/views/link/tmpl/edit.php', - '/administrator/components/com_redirect/views/link/view.html.php', - '/administrator/components/com_redirect/views/links/tmpl/default.php', - '/administrator/components/com_redirect/views/links/tmpl/default.xml', - '/administrator/components/com_redirect/views/links/tmpl/default_addform.php', - '/administrator/components/com_redirect/views/links/tmpl/default_batch_body.php', - '/administrator/components/com_redirect/views/links/tmpl/default_batch_footer.php', - '/administrator/components/com_redirect/views/links/view.html.php', - '/administrator/components/com_tags/controller.php', - '/administrator/components/com_tags/controllers/tag.php', - '/administrator/components/com_tags/controllers/tags.php', - '/administrator/components/com_tags/helpers/tags.php', - '/administrator/components/com_tags/models/forms/filter_tags.xml', - '/administrator/components/com_tags/models/forms/tag.xml', - '/administrator/components/com_tags/models/tag.php', - '/administrator/components/com_tags/models/tags.php', - '/administrator/components/com_tags/tables/tag.php', - '/administrator/components/com_tags/tags.php', - '/administrator/components/com_tags/views/tag/tmpl/edit.php', - '/administrator/components/com_tags/views/tag/tmpl/edit_metadata.php', - '/administrator/components/com_tags/views/tag/tmpl/edit_options.php', - '/administrator/components/com_tags/views/tag/view.html.php', - '/administrator/components/com_tags/views/tags/tmpl/default.php', - '/administrator/components/com_tags/views/tags/tmpl/default.xml', - '/administrator/components/com_tags/views/tags/tmpl/default_batch_body.php', - '/administrator/components/com_tags/views/tags/tmpl/default_batch_footer.php', - '/administrator/components/com_tags/views/tags/view.html.php', - '/administrator/components/com_templates/controller.php', - '/administrator/components/com_templates/controllers/style.php', - '/administrator/components/com_templates/controllers/styles.php', - '/administrator/components/com_templates/controllers/template.php', - '/administrator/components/com_templates/helpers/html/templates.php', - '/administrator/components/com_templates/models/fields/templatelocation.php', - '/administrator/components/com_templates/models/fields/templatename.php', - '/administrator/components/com_templates/models/forms/filter_styles.xml', - '/administrator/components/com_templates/models/forms/filter_templates.xml', - '/administrator/components/com_templates/models/forms/source.xml', - '/administrator/components/com_templates/models/forms/style.xml', - '/administrator/components/com_templates/models/forms/style_administrator.xml', - '/administrator/components/com_templates/models/forms/style_site.xml', - '/administrator/components/com_templates/models/style.php', - '/administrator/components/com_templates/models/styles.php', - '/administrator/components/com_templates/models/template.php', - '/administrator/components/com_templates/models/templates.php', - '/administrator/components/com_templates/tables/style.php', - '/administrator/components/com_templates/templates.php', - '/administrator/components/com_templates/views/style/tmpl/edit.php', - '/administrator/components/com_templates/views/style/tmpl/edit_assignment.php', - '/administrator/components/com_templates/views/style/tmpl/edit_options.php', - '/administrator/components/com_templates/views/style/view.html.php', - '/administrator/components/com_templates/views/style/view.json.php', - '/administrator/components/com_templates/views/styles/tmpl/default.php', - '/administrator/components/com_templates/views/styles/tmpl/default.xml', - '/administrator/components/com_templates/views/styles/view.html.php', - '/administrator/components/com_templates/views/template/tmpl/default.php', - '/administrator/components/com_templates/views/template/tmpl/default_description.php', - '/administrator/components/com_templates/views/template/tmpl/default_folders.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_copy_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_copy_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_delete_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_delete_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_file_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_file_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_folder_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_folder_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_rename_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_rename_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_resize_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_resize_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_tree.php', - '/administrator/components/com_templates/views/template/tmpl/readonly.php', - '/administrator/components/com_templates/views/template/view.html.php', - '/administrator/components/com_templates/views/templates/tmpl/default.php', - '/administrator/components/com_templates/views/templates/tmpl/default.xml', - '/administrator/components/com_templates/views/templates/view.html.php', - '/administrator/components/com_users/controller.php', - '/administrator/components/com_users/controllers/group.php', - '/administrator/components/com_users/controllers/groups.php', - '/administrator/components/com_users/controllers/level.php', - '/administrator/components/com_users/controllers/levels.php', - '/administrator/components/com_users/controllers/mail.php', - '/administrator/components/com_users/controllers/note.php', - '/administrator/components/com_users/controllers/notes.php', - '/administrator/components/com_users/controllers/user.php', - '/administrator/components/com_users/controllers/users.php', - '/administrator/components/com_users/helpers/html/users.php', - '/administrator/components/com_users/models/debuggroup.php', - '/administrator/components/com_users/models/debuguser.php', - '/administrator/components/com_users/models/fields/groupparent.php', - '/administrator/components/com_users/models/fields/levels.php', - '/administrator/components/com_users/models/forms/config_domain.xml', - '/administrator/components/com_users/models/forms/fields/user.xml', - '/administrator/components/com_users/models/forms/filter_debuggroup.xml', - '/administrator/components/com_users/models/forms/filter_debuguser.xml', - '/administrator/components/com_users/models/forms/filter_groups.xml', - '/administrator/components/com_users/models/forms/filter_levels.xml', - '/administrator/components/com_users/models/forms/filter_notes.xml', - '/administrator/components/com_users/models/forms/filter_users.xml', - '/administrator/components/com_users/models/forms/group.xml', - '/administrator/components/com_users/models/forms/level.xml', - '/administrator/components/com_users/models/forms/mail.xml', - '/administrator/components/com_users/models/forms/note.xml', - '/administrator/components/com_users/models/forms/user.xml', - '/administrator/components/com_users/models/group.php', - '/administrator/components/com_users/models/groups.php', - '/administrator/components/com_users/models/level.php', - '/administrator/components/com_users/models/levels.php', - '/administrator/components/com_users/models/mail.php', - '/administrator/components/com_users/models/note.php', - '/administrator/components/com_users/models/notes.php', - '/administrator/components/com_users/models/user.php', - '/administrator/components/com_users/models/users.php', - '/administrator/components/com_users/tables/note.php', - '/administrator/components/com_users/users.php', - '/administrator/components/com_users/views/debuggroup/tmpl/default.php', - '/administrator/components/com_users/views/debuggroup/view.html.php', - '/administrator/components/com_users/views/debuguser/tmpl/default.php', - '/administrator/components/com_users/views/debuguser/view.html.php', - '/administrator/components/com_users/views/group/tmpl/edit.php', - '/administrator/components/com_users/views/group/tmpl/edit.xml', - '/administrator/components/com_users/views/group/view.html.php', - '/administrator/components/com_users/views/groups/tmpl/default.php', - '/administrator/components/com_users/views/groups/tmpl/default.xml', - '/administrator/components/com_users/views/groups/view.html.php', - '/administrator/components/com_users/views/level/tmpl/edit.php', - '/administrator/components/com_users/views/level/tmpl/edit.xml', - '/administrator/components/com_users/views/level/view.html.php', - '/administrator/components/com_users/views/levels/tmpl/default.php', - '/administrator/components/com_users/views/levels/tmpl/default.xml', - '/administrator/components/com_users/views/levels/view.html.php', - '/administrator/components/com_users/views/mail/tmpl/default.php', - '/administrator/components/com_users/views/mail/tmpl/default.xml', - '/administrator/components/com_users/views/mail/view.html.php', - '/administrator/components/com_users/views/note/tmpl/edit.php', - '/administrator/components/com_users/views/note/tmpl/edit.xml', - '/administrator/components/com_users/views/note/view.html.php', - '/administrator/components/com_users/views/notes/tmpl/default.php', - '/administrator/components/com_users/views/notes/tmpl/default.xml', - '/administrator/components/com_users/views/notes/tmpl/modal.php', - '/administrator/components/com_users/views/notes/view.html.php', - '/administrator/components/com_users/views/user/tmpl/edit.php', - '/administrator/components/com_users/views/user/tmpl/edit.xml', - '/administrator/components/com_users/views/user/tmpl/edit_groups.php', - '/administrator/components/com_users/views/user/view.html.php', - '/administrator/components/com_users/views/users/tmpl/default.php', - '/administrator/components/com_users/views/users/tmpl/default.xml', - '/administrator/components/com_users/views/users/tmpl/default_batch_body.php', - '/administrator/components/com_users/views/users/tmpl/default_batch_footer.php', - '/administrator/components/com_users/views/users/tmpl/modal.php', - '/administrator/components/com_users/views/users/view.html.php', - '/administrator/help/helpsites.xml', - '/administrator/includes/helper.php', - '/administrator/includes/subtoolbar.php', - '/administrator/language/en-GB/en-GB.com_actionlogs.ini', - '/administrator/language/en-GB/en-GB.com_actionlogs.sys.ini', - '/administrator/language/en-GB/en-GB.com_admin.ini', - '/administrator/language/en-GB/en-GB.com_admin.sys.ini', - '/administrator/language/en-GB/en-GB.com_ajax.ini', - '/administrator/language/en-GB/en-GB.com_ajax.sys.ini', - '/administrator/language/en-GB/en-GB.com_associations.ini', - '/administrator/language/en-GB/en-GB.com_associations.sys.ini', - '/administrator/language/en-GB/en-GB.com_banners.ini', - '/administrator/language/en-GB/en-GB.com_banners.sys.ini', - '/administrator/language/en-GB/en-GB.com_cache.ini', - '/administrator/language/en-GB/en-GB.com_cache.sys.ini', - '/administrator/language/en-GB/en-GB.com_categories.ini', - '/administrator/language/en-GB/en-GB.com_categories.sys.ini', - '/administrator/language/en-GB/en-GB.com_checkin.ini', - '/administrator/language/en-GB/en-GB.com_checkin.sys.ini', - '/administrator/language/en-GB/en-GB.com_config.ini', - '/administrator/language/en-GB/en-GB.com_config.sys.ini', - '/administrator/language/en-GB/en-GB.com_contact.ini', - '/administrator/language/en-GB/en-GB.com_contact.sys.ini', - '/administrator/language/en-GB/en-GB.com_content.ini', - '/administrator/language/en-GB/en-GB.com_content.sys.ini', - '/administrator/language/en-GB/en-GB.com_contenthistory.ini', - '/administrator/language/en-GB/en-GB.com_contenthistory.sys.ini', - '/administrator/language/en-GB/en-GB.com_cpanel.ini', - '/administrator/language/en-GB/en-GB.com_cpanel.sys.ini', - '/administrator/language/en-GB/en-GB.com_fields.ini', - '/administrator/language/en-GB/en-GB.com_fields.sys.ini', - '/administrator/language/en-GB/en-GB.com_finder.ini', - '/administrator/language/en-GB/en-GB.com_finder.sys.ini', - '/administrator/language/en-GB/en-GB.com_installer.ini', - '/administrator/language/en-GB/en-GB.com_installer.sys.ini', - '/administrator/language/en-GB/en-GB.com_joomlaupdate.ini', - '/administrator/language/en-GB/en-GB.com_joomlaupdate.sys.ini', - '/administrator/language/en-GB/en-GB.com_languages.ini', - '/administrator/language/en-GB/en-GB.com_languages.sys.ini', - '/administrator/language/en-GB/en-GB.com_login.ini', - '/administrator/language/en-GB/en-GB.com_login.sys.ini', - '/administrator/language/en-GB/en-GB.com_mailto.sys.ini', - '/administrator/language/en-GB/en-GB.com_media.ini', - '/administrator/language/en-GB/en-GB.com_media.sys.ini', - '/administrator/language/en-GB/en-GB.com_menus.ini', - '/administrator/language/en-GB/en-GB.com_menus.sys.ini', - '/administrator/language/en-GB/en-GB.com_messages.ini', - '/administrator/language/en-GB/en-GB.com_messages.sys.ini', - '/administrator/language/en-GB/en-GB.com_modules.ini', - '/administrator/language/en-GB/en-GB.com_modules.sys.ini', - '/administrator/language/en-GB/en-GB.com_newsfeeds.ini', - '/administrator/language/en-GB/en-GB.com_newsfeeds.sys.ini', - '/administrator/language/en-GB/en-GB.com_plugins.ini', - '/administrator/language/en-GB/en-GB.com_plugins.sys.ini', - '/administrator/language/en-GB/en-GB.com_postinstall.ini', - '/administrator/language/en-GB/en-GB.com_postinstall.sys.ini', - '/administrator/language/en-GB/en-GB.com_privacy.ini', - '/administrator/language/en-GB/en-GB.com_privacy.sys.ini', - '/administrator/language/en-GB/en-GB.com_redirect.ini', - '/administrator/language/en-GB/en-GB.com_redirect.sys.ini', - '/administrator/language/en-GB/en-GB.com_tags.ini', - '/administrator/language/en-GB/en-GB.com_tags.sys.ini', - '/administrator/language/en-GB/en-GB.com_templates.ini', - '/administrator/language/en-GB/en-GB.com_templates.sys.ini', - '/administrator/language/en-GB/en-GB.com_users.ini', - '/administrator/language/en-GB/en-GB.com_users.sys.ini', - '/administrator/language/en-GB/en-GB.com_weblinks.ini', - '/administrator/language/en-GB/en-GB.com_weblinks.sys.ini', - '/administrator/language/en-GB/en-GB.com_wrapper.ini', - '/administrator/language/en-GB/en-GB.com_wrapper.sys.ini', - '/administrator/language/en-GB/en-GB.ini', - '/administrator/language/en-GB/en-GB.lib_joomla.ini', - '/administrator/language/en-GB/en-GB.localise.php', - '/administrator/language/en-GB/en-GB.mod_custom.ini', - '/administrator/language/en-GB/en-GB.mod_custom.sys.ini', - '/administrator/language/en-GB/en-GB.mod_feed.ini', - '/administrator/language/en-GB/en-GB.mod_feed.sys.ini', - '/administrator/language/en-GB/en-GB.mod_latest.ini', - '/administrator/language/en-GB/en-GB.mod_latest.sys.ini', - '/administrator/language/en-GB/en-GB.mod_latestactions.ini', - '/administrator/language/en-GB/en-GB.mod_latestactions.sys.ini', - '/administrator/language/en-GB/en-GB.mod_logged.ini', - '/administrator/language/en-GB/en-GB.mod_logged.sys.ini', - '/administrator/language/en-GB/en-GB.mod_login.ini', - '/administrator/language/en-GB/en-GB.mod_login.sys.ini', - '/administrator/language/en-GB/en-GB.mod_menu.ini', - '/administrator/language/en-GB/en-GB.mod_menu.sys.ini', - '/administrator/language/en-GB/en-GB.mod_multilangstatus.ini', - '/administrator/language/en-GB/en-GB.mod_multilangstatus.sys.ini', - '/administrator/language/en-GB/en-GB.mod_popular.ini', - '/administrator/language/en-GB/en-GB.mod_popular.sys.ini', - '/administrator/language/en-GB/en-GB.mod_privacy_dashboard.ini', - '/administrator/language/en-GB/en-GB.mod_privacy_dashboard.sys.ini', - '/administrator/language/en-GB/en-GB.mod_quickicon.ini', - '/administrator/language/en-GB/en-GB.mod_quickicon.sys.ini', - '/administrator/language/en-GB/en-GB.mod_sampledata.ini', - '/administrator/language/en-GB/en-GB.mod_sampledata.sys.ini', - '/administrator/language/en-GB/en-GB.mod_stats_admin.ini', - '/administrator/language/en-GB/en-GB.mod_stats_admin.sys.ini', - '/administrator/language/en-GB/en-GB.mod_status.ini', - '/administrator/language/en-GB/en-GB.mod_status.sys.ini', - '/administrator/language/en-GB/en-GB.mod_submenu.ini', - '/administrator/language/en-GB/en-GB.mod_submenu.sys.ini', - '/administrator/language/en-GB/en-GB.mod_title.ini', - '/administrator/language/en-GB/en-GB.mod_title.sys.ini', - '/administrator/language/en-GB/en-GB.mod_toolbar.ini', - '/administrator/language/en-GB/en-GB.mod_toolbar.sys.ini', - '/administrator/language/en-GB/en-GB.mod_version.ini', - '/administrator/language/en-GB/en-GB.mod_version.sys.ini', - '/administrator/language/en-GB/en-GB.plg_actionlog_joomla.ini', - '/administrator/language/en-GB/en-GB.plg_actionlog_joomla.sys.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_cookie.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_cookie.sys.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_gmail.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_gmail.sys.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_joomla.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_joomla.sys.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_ldap.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_ldap.sys.ini', - '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.ini', - '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.sys.ini', - '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha_invisible.ini', - '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha_invisible.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_confirmconsent.ini', - '/administrator/language/en-GB/en-GB.plg_content_confirmconsent.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_contact.ini', - '/administrator/language/en-GB/en-GB.plg_content_contact.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_emailcloak.ini', - '/administrator/language/en-GB/en-GB.plg_content_emailcloak.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_fields.ini', - '/administrator/language/en-GB/en-GB.plg_content_fields.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_finder.ini', - '/administrator/language/en-GB/en-GB.plg_content_finder.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_joomla.ini', - '/administrator/language/en-GB/en-GB.plg_content_joomla.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_loadmodule.ini', - '/administrator/language/en-GB/en-GB.plg_content_loadmodule.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_pagebreak.ini', - '/administrator/language/en-GB/en-GB.plg_content_pagebreak.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_pagenavigation.ini', - '/administrator/language/en-GB/en-GB.plg_content_pagenavigation.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_vote.ini', - '/administrator/language/en-GB/en-GB.plg_content_vote.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_article.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_article.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_contact.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_contact.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_fields.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_fields.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_image.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_image.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_menu.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_menu.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_module.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_module.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors_codemirror.ini', - '/administrator/language/en-GB/en-GB.plg_editors_codemirror.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors_none.ini', - '/administrator/language/en-GB/en-GB.plg_editors_none.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors_tinymce.ini', - '/administrator/language/en-GB/en-GB.plg_editors_tinymce.sys.ini', - '/administrator/language/en-GB/en-GB.plg_extension_joomla.ini', - '/administrator/language/en-GB/en-GB.plg_extension_joomla.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_calendar.ini', - '/administrator/language/en-GB/en-GB.plg_fields_calendar.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_checkboxes.ini', - '/administrator/language/en-GB/en-GB.plg_fields_checkboxes.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_color.ini', - '/administrator/language/en-GB/en-GB.plg_fields_color.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_editor.ini', - '/administrator/language/en-GB/en-GB.plg_fields_editor.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_image.ini', - '/administrator/language/en-GB/en-GB.plg_fields_image.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_imagelist.ini', - '/administrator/language/en-GB/en-GB.plg_fields_imagelist.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_integer.ini', - '/administrator/language/en-GB/en-GB.plg_fields_integer.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_list.ini', - '/administrator/language/en-GB/en-GB.plg_fields_list.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_media.ini', - '/administrator/language/en-GB/en-GB.plg_fields_media.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_radio.ini', - '/administrator/language/en-GB/en-GB.plg_fields_radio.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_sql.ini', - '/administrator/language/en-GB/en-GB.plg_fields_sql.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_text.ini', - '/administrator/language/en-GB/en-GB.plg_fields_text.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_textarea.ini', - '/administrator/language/en-GB/en-GB.plg_fields_textarea.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_url.ini', - '/administrator/language/en-GB/en-GB.plg_fields_url.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_user.ini', - '/administrator/language/en-GB/en-GB.plg_fields_user.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_usergrouplist.ini', - '/administrator/language/en-GB/en-GB.plg_fields_usergrouplist.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_categories.ini', - '/administrator/language/en-GB/en-GB.plg_finder_categories.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_contacts.ini', - '/administrator/language/en-GB/en-GB.plg_finder_contacts.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_content.ini', - '/administrator/language/en-GB/en-GB.plg_finder_content.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.ini', - '/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_tags.ini', - '/administrator/language/en-GB/en-GB.plg_finder_tags.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_weblinks.ini', - '/administrator/language/en-GB/en-GB.plg_finder_weblinks.sys.ini', - '/administrator/language/en-GB/en-GB.plg_installer_folderinstaller.ini', - '/administrator/language/en-GB/en-GB.plg_installer_folderinstaller.sys.ini', - '/administrator/language/en-GB/en-GB.plg_installer_packageinstaller.ini', - '/administrator/language/en-GB/en-GB.plg_installer_packageinstaller.sys.ini', - '/administrator/language/en-GB/en-GB.plg_installer_urlinstaller.ini', - '/administrator/language/en-GB/en-GB.plg_installer_urlinstaller.sys.ini', - '/administrator/language/en-GB/en-GB.plg_installer_webinstaller.ini', - '/administrator/language/en-GB/en-GB.plg_installer_webinstaller.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_actionlogs.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_actionlogs.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_consents.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_consents.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_contact.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_contact.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_content.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_content.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_message.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_message.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_user.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_user.sys.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.sys.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.sys.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_phpversioncheck.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_phpversioncheck.sys.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_privacycheck.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_privacycheck.sys.ini', - '/administrator/language/en-GB/en-GB.plg_sampledata_blog.ini', - '/administrator/language/en-GB/en-GB.plg_sampledata_blog.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_actionlogs.ini', - '/administrator/language/en-GB/en-GB.plg_system_actionlogs.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_cache.ini', - '/administrator/language/en-GB/en-GB.plg_system_cache.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_debug.ini', - '/administrator/language/en-GB/en-GB.plg_system_debug.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_fields.ini', - '/administrator/language/en-GB/en-GB.plg_system_fields.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_highlight.ini', - '/administrator/language/en-GB/en-GB.plg_system_highlight.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_languagecode.ini', - '/administrator/language/en-GB/en-GB.plg_system_languagecode.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_languagefilter.ini', - '/administrator/language/en-GB/en-GB.plg_system_languagefilter.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_log.ini', - '/administrator/language/en-GB/en-GB.plg_system_log.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_logout.ini', - '/administrator/language/en-GB/en-GB.plg_system_logout.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_logrotation.ini', - '/administrator/language/en-GB/en-GB.plg_system_logrotation.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_p3p.ini', - '/administrator/language/en-GB/en-GB.plg_system_p3p.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_privacyconsent.ini', - '/administrator/language/en-GB/en-GB.plg_system_privacyconsent.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_redirect.ini', - '/administrator/language/en-GB/en-GB.plg_system_redirect.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_remember.ini', - '/administrator/language/en-GB/en-GB.plg_system_remember.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_sef.ini', - '/administrator/language/en-GB/en-GB.plg_system_sef.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_sessiongc.ini', - '/administrator/language/en-GB/en-GB.plg_system_sessiongc.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_stats.ini', - '/administrator/language/en-GB/en-GB.plg_system_stats.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_updatenotification.ini', - '/administrator/language/en-GB/en-GB.plg_system_updatenotification.sys.ini', - '/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.ini', - '/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.sys.ini', - '/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.ini', - '/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.sys.ini', - '/administrator/language/en-GB/en-GB.plg_user_contactcreator.ini', - '/administrator/language/en-GB/en-GB.plg_user_contactcreator.sys.ini', - '/administrator/language/en-GB/en-GB.plg_user_joomla.ini', - '/administrator/language/en-GB/en-GB.plg_user_joomla.sys.ini', - '/administrator/language/en-GB/en-GB.plg_user_profile.ini', - '/administrator/language/en-GB/en-GB.plg_user_profile.sys.ini', - '/administrator/language/en-GB/en-GB.plg_user_terms.ini', - '/administrator/language/en-GB/en-GB.plg_user_terms.sys.ini', - '/administrator/language/en-GB/en-GB.tpl_hathor.ini', - '/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini', - '/administrator/language/en-GB/en-GB.tpl_isis.ini', - '/administrator/language/en-GB/en-GB.tpl_isis.sys.ini', - '/administrator/language/en-GB/en-GB.xml', - '/administrator/manifests/libraries/fof.xml', - '/administrator/manifests/libraries/idna_convert.xml', - '/administrator/manifests/libraries/phputf8.xml', - '/administrator/modules/mod_feed/helper.php', - '/administrator/modules/mod_latest/helper.php', - '/administrator/modules/mod_latestactions/helper.php', - '/administrator/modules/mod_logged/helper.php', - '/administrator/modules/mod_login/helper.php', - '/administrator/modules/mod_menu/helper.php', - '/administrator/modules/mod_menu/menu.php', - '/administrator/modules/mod_multilangstatus/language/en-GB/en-GB.mod_multilangstatus.ini', - '/administrator/modules/mod_multilangstatus/language/en-GB/en-GB.mod_multilangstatus.sys.ini', - '/administrator/modules/mod_popular/helper.php', - '/administrator/modules/mod_privacy_dashboard/helper.php', - '/administrator/modules/mod_quickicon/helper.php', - '/administrator/modules/mod_quickicon/mod_quickicon.php', - '/administrator/modules/mod_sampledata/helper.php', - '/administrator/modules/mod_stats_admin/helper.php', - '/administrator/modules/mod_stats_admin/language/en-GB.mod_stats_admin.ini', - '/administrator/modules/mod_stats_admin/language/en-GB.mod_stats_admin.sys.ini', - '/administrator/modules/mod_status/mod_status.php', - '/administrator/modules/mod_status/mod_status.xml', - '/administrator/modules/mod_status/tmpl/default.php', - '/administrator/modules/mod_version/helper.php', - '/administrator/modules/mod_version/language/en-GB/en-GB.mod_version.ini', - '/administrator/modules/mod_version/language/en-GB/en-GB.mod_version.sys.ini', - '/administrator/templates/hathor/LICENSE.txt', - '/administrator/templates/hathor/component.php', - '/administrator/templates/hathor/cpanel.php', - '/administrator/templates/hathor/css/boldtext.css', - '/administrator/templates/hathor/css/colour_blue.css', - '/administrator/templates/hathor/css/colour_blue_rtl.css', - '/administrator/templates/hathor/css/colour_brown.css', - '/administrator/templates/hathor/css/colour_brown_rtl.css', - '/administrator/templates/hathor/css/colour_highcontrast.css', - '/administrator/templates/hathor/css/colour_highcontrast_rtl.css', - '/administrator/templates/hathor/css/colour_standard.css', - '/administrator/templates/hathor/css/colour_standard_rtl.css', - '/administrator/templates/hathor/css/error.css', - '/administrator/templates/hathor/css/ie7.css', - '/administrator/templates/hathor/css/ie8.css', - '/administrator/templates/hathor/css/template.css', - '/administrator/templates/hathor/css/template_rtl.css', - '/administrator/templates/hathor/css/theme.css', - '/administrator/templates/hathor/error.php', - '/administrator/templates/hathor/favicon.ico', - '/administrator/templates/hathor/html/com_admin/help/default.php', - '/administrator/templates/hathor/html/com_admin/profile/edit.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default_config.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default_directory.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default_navigation.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default_phpsettings.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default_system.php', - '/administrator/templates/hathor/html/com_associations/associations/default.php', - '/administrator/templates/hathor/html/com_banners/banner/edit.php', - '/administrator/templates/hathor/html/com_banners/banners/default.php', - '/administrator/templates/hathor/html/com_banners/client/edit.php', - '/administrator/templates/hathor/html/com_banners/clients/default.php', - '/administrator/templates/hathor/html/com_banners/download/default.php', - '/administrator/templates/hathor/html/com_banners/tracks/default.php', - '/administrator/templates/hathor/html/com_cache/cache/default.php', - '/administrator/templates/hathor/html/com_cache/purge/default.php', - '/administrator/templates/hathor/html/com_categories/categories/default.php', - '/administrator/templates/hathor/html/com_categories/category/edit.php', - '/administrator/templates/hathor/html/com_categories/category/edit_options.php', - '/administrator/templates/hathor/html/com_checkin/checkin/default.php', - '/administrator/templates/hathor/html/com_config/application/default.php', - '/administrator/templates/hathor/html/com_config/application/default_cache.php', - '/administrator/templates/hathor/html/com_config/application/default_cookie.php', - '/administrator/templates/hathor/html/com_config/application/default_database.php', - '/administrator/templates/hathor/html/com_config/application/default_debug.php', - '/administrator/templates/hathor/html/com_config/application/default_filters.php', - '/administrator/templates/hathor/html/com_config/application/default_ftp.php', - '/administrator/templates/hathor/html/com_config/application/default_ftplogin.php', - '/administrator/templates/hathor/html/com_config/application/default_locale.php', - '/administrator/templates/hathor/html/com_config/application/default_mail.php', - '/administrator/templates/hathor/html/com_config/application/default_metadata.php', - '/administrator/templates/hathor/html/com_config/application/default_navigation.php', - '/administrator/templates/hathor/html/com_config/application/default_permissions.php', - '/administrator/templates/hathor/html/com_config/application/default_seo.php', - '/administrator/templates/hathor/html/com_config/application/default_server.php', - '/administrator/templates/hathor/html/com_config/application/default_session.php', - '/administrator/templates/hathor/html/com_config/application/default_site.php', - '/administrator/templates/hathor/html/com_config/application/default_system.php', - '/administrator/templates/hathor/html/com_config/component/default.php', - '/administrator/templates/hathor/html/com_contact/contact/edit.php', - '/administrator/templates/hathor/html/com_contact/contact/edit_params.php', - '/administrator/templates/hathor/html/com_contact/contacts/default.php', - '/administrator/templates/hathor/html/com_contact/contacts/modal.php', - '/administrator/templates/hathor/html/com_content/article/edit.php', - '/administrator/templates/hathor/html/com_content/articles/default.php', - '/administrator/templates/hathor/html/com_content/articles/modal.php', - '/administrator/templates/hathor/html/com_content/featured/default.php', - '/administrator/templates/hathor/html/com_contenthistory/history/modal.php', - '/administrator/templates/hathor/html/com_cpanel/cpanel/default.php', - '/administrator/templates/hathor/html/com_fields/field/edit.php', - '/administrator/templates/hathor/html/com_fields/fields/default.php', - '/administrator/templates/hathor/html/com_fields/group/edit.php', - '/administrator/templates/hathor/html/com_fields/groups/default.php', - '/administrator/templates/hathor/html/com_finder/filters/default.php', - '/administrator/templates/hathor/html/com_finder/index/default.php', - '/administrator/templates/hathor/html/com_finder/maps/default.php', - '/administrator/templates/hathor/html/com_installer/database/default.php', - '/administrator/templates/hathor/html/com_installer/default/default_ftp.php', - '/administrator/templates/hathor/html/com_installer/discover/default.php', - '/administrator/templates/hathor/html/com_installer/install/default.php', - '/administrator/templates/hathor/html/com_installer/install/default_form.php', - '/administrator/templates/hathor/html/com_installer/languages/default.php', - '/administrator/templates/hathor/html/com_installer/languages/default_filter.php', - '/administrator/templates/hathor/html/com_installer/manage/default.php', - '/administrator/templates/hathor/html/com_installer/manage/default_filter.php', - '/administrator/templates/hathor/html/com_installer/update/default.php', - '/administrator/templates/hathor/html/com_installer/warnings/default.php', - '/administrator/templates/hathor/html/com_joomlaupdate/default/default.php', - '/administrator/templates/hathor/html/com_languages/installed/default.php', - '/administrator/templates/hathor/html/com_languages/installed/default_ftp.php', - '/administrator/templates/hathor/html/com_languages/languages/default.php', - '/administrator/templates/hathor/html/com_languages/overrides/default.php', - '/administrator/templates/hathor/html/com_menus/item/edit.php', - '/administrator/templates/hathor/html/com_menus/item/edit_options.php', - '/administrator/templates/hathor/html/com_menus/items/default.php', - '/administrator/templates/hathor/html/com_menus/menu/edit.php', - '/administrator/templates/hathor/html/com_menus/menus/default.php', - '/administrator/templates/hathor/html/com_menus/menutypes/default.php', - '/administrator/templates/hathor/html/com_messages/message/edit.php', - '/administrator/templates/hathor/html/com_messages/messages/default.php', - '/administrator/templates/hathor/html/com_modules/module/edit.php', - '/administrator/templates/hathor/html/com_modules/module/edit_assignment.php', - '/administrator/templates/hathor/html/com_modules/module/edit_options.php', - '/administrator/templates/hathor/html/com_modules/modules/default.php', - '/administrator/templates/hathor/html/com_modules/positions/modal.php', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeed/edit.php', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeed/edit_params.php', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds/default.php', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds/modal.php', - '/administrator/templates/hathor/html/com_plugins/plugin/edit.php', - '/administrator/templates/hathor/html/com_plugins/plugin/edit_options.php', - '/administrator/templates/hathor/html/com_plugins/plugins/default.php', - '/administrator/templates/hathor/html/com_postinstall/messages/default.php', - '/administrator/templates/hathor/html/com_redirect/links/default.php', - '/administrator/templates/hathor/html/com_search/searches/default.php', - '/administrator/templates/hathor/html/com_tags/tag/edit.php', - '/administrator/templates/hathor/html/com_tags/tag/edit_metadata.php', - '/administrator/templates/hathor/html/com_tags/tag/edit_options.php', - '/administrator/templates/hathor/html/com_tags/tags/default.php', - '/administrator/templates/hathor/html/com_templates/style/edit.php', - '/administrator/templates/hathor/html/com_templates/style/edit_assignment.php', - '/administrator/templates/hathor/html/com_templates/style/edit_options.php', - '/administrator/templates/hathor/html/com_templates/styles/default.php', - '/administrator/templates/hathor/html/com_templates/template/default.php', - '/administrator/templates/hathor/html/com_templates/template/default_description.php', - '/administrator/templates/hathor/html/com_templates/template/default_folders.php', - '/administrator/templates/hathor/html/com_templates/template/default_tree.php', - '/administrator/templates/hathor/html/com_templates/templates/default.php', - '/administrator/templates/hathor/html/com_users/debuggroup/default.php', - '/administrator/templates/hathor/html/com_users/debuguser/default.php', - '/administrator/templates/hathor/html/com_users/groups/default.php', - '/administrator/templates/hathor/html/com_users/levels/default.php', - '/administrator/templates/hathor/html/com_users/note/edit.php', - '/administrator/templates/hathor/html/com_users/notes/default.php', - '/administrator/templates/hathor/html/com_users/user/edit.php', - '/administrator/templates/hathor/html/com_users/users/default.php', - '/administrator/templates/hathor/html/com_users/users/modal.php', - '/administrator/templates/hathor/html/com_weblinks/weblink/edit.php', - '/administrator/templates/hathor/html/com_weblinks/weblink/edit_params.php', - '/administrator/templates/hathor/html/com_weblinks/weblinks/default.php', - '/administrator/templates/hathor/html/layouts/com_media/toolbar/deletemedia.php', - '/administrator/templates/hathor/html/layouts/com_media/toolbar/newfolder.php', - '/administrator/templates/hathor/html/layouts/com_media/toolbar/uploadmedia.php', - '/administrator/templates/hathor/html/layouts/com_messages/toolbar/mysettings.php', - '/administrator/templates/hathor/html/layouts/com_modules/toolbar/cancelselect.php', - '/administrator/templates/hathor/html/layouts/com_modules/toolbar/newmodule.php', - '/administrator/templates/hathor/html/layouts/joomla/edit/details.php', - '/administrator/templates/hathor/html/layouts/joomla/edit/fieldset.php', - '/administrator/templates/hathor/html/layouts/joomla/edit/global.php', - '/administrator/templates/hathor/html/layouts/joomla/edit/metadata.php', - '/administrator/templates/hathor/html/layouts/joomla/edit/params.php', - '/administrator/templates/hathor/html/layouts/joomla/quickicons/icon.php', - '/administrator/templates/hathor/html/layouts/joomla/sidebars/submenu.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/base.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/batch.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/confirm.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/containerclose.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/containeropen.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/help.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/iconclass.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/link.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/modal.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/popup.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/separator.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/slider.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/standard.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/title.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/versions.php', - '/administrator/templates/hathor/html/layouts/plugins/user/profile/fields/dob.php', - '/administrator/templates/hathor/html/mod_login/default.php', - '/administrator/templates/hathor/html/mod_quickicon/default.php', - '/administrator/templates/hathor/html/modules.php', - '/administrator/templates/hathor/html/pagination.php', - '/administrator/templates/hathor/images/admin/blank.png', - '/administrator/templates/hathor/images/admin/checked_out.png', - '/administrator/templates/hathor/images/admin/collapseall.png', - '/administrator/templates/hathor/images/admin/disabled.png', - '/administrator/templates/hathor/images/admin/downarrow-1.png', - '/administrator/templates/hathor/images/admin/downarrow.png', - '/administrator/templates/hathor/images/admin/downarrow0.png', - '/administrator/templates/hathor/images/admin/expandall.png', - '/administrator/templates/hathor/images/admin/featured.png', - '/administrator/templates/hathor/images/admin/filesave.png', - '/administrator/templates/hathor/images/admin/filter_16.png', - '/administrator/templates/hathor/images/admin/icon-16-allow.png', - '/administrator/templates/hathor/images/admin/icon-16-allowinactive.png', - '/administrator/templates/hathor/images/admin/icon-16-deny.png', - '/administrator/templates/hathor/images/admin/icon-16-denyinactive.png', - '/administrator/templates/hathor/images/admin/icon-16-links.png', - '/administrator/templates/hathor/images/admin/icon-16-notice-note.png', - '/administrator/templates/hathor/images/admin/icon-16-protected.png', - '/administrator/templates/hathor/images/admin/menu_divider.png', - '/administrator/templates/hathor/images/admin/note_add_16.png', - '/administrator/templates/hathor/images/admin/publish_g.png', - '/administrator/templates/hathor/images/admin/publish_r.png', - '/administrator/templates/hathor/images/admin/publish_x.png', - '/administrator/templates/hathor/images/admin/publish_y.png', - '/administrator/templates/hathor/images/admin/sort_asc.png', - '/administrator/templates/hathor/images/admin/sort_desc.png', - '/administrator/templates/hathor/images/admin/tick.png', - '/administrator/templates/hathor/images/admin/trash.png', - '/administrator/templates/hathor/images/admin/uparrow-1.png', - '/administrator/templates/hathor/images/admin/uparrow.png', - '/administrator/templates/hathor/images/admin/uparrow0.png', - '/administrator/templates/hathor/images/arrow.png', - '/administrator/templates/hathor/images/bg-menu.gif', - '/administrator/templates/hathor/images/calendar.png', - '/administrator/templates/hathor/images/header/icon-48-alert.png', - '/administrator/templates/hathor/images/header/icon-48-apply.png', - '/administrator/templates/hathor/images/header/icon-48-archive.png', - '/administrator/templates/hathor/images/header/icon-48-article-add.png', - '/administrator/templates/hathor/images/header/icon-48-article-edit.png', - '/administrator/templates/hathor/images/header/icon-48-article.png', - '/administrator/templates/hathor/images/header/icon-48-assoc.png', - '/administrator/templates/hathor/images/header/icon-48-banner-categories.png', - '/administrator/templates/hathor/images/header/icon-48-banner-client.png', - '/administrator/templates/hathor/images/header/icon-48-banner-tracks.png', - '/administrator/templates/hathor/images/header/icon-48-banner.png', - '/administrator/templates/hathor/images/header/icon-48-calendar.png', - '/administrator/templates/hathor/images/header/icon-48-category-add.png', - '/administrator/templates/hathor/images/header/icon-48-category.png', - '/administrator/templates/hathor/images/header/icon-48-checkin.png', - '/administrator/templates/hathor/images/header/icon-48-clear.png', - '/administrator/templates/hathor/images/header/icon-48-component.png', - '/administrator/templates/hathor/images/header/icon-48-config.png', - '/administrator/templates/hathor/images/header/icon-48-contacts-categories.png', - '/administrator/templates/hathor/images/header/icon-48-contacts.png', - '/administrator/templates/hathor/images/header/icon-48-content.png', - '/administrator/templates/hathor/images/header/icon-48-cpanel.png', - '/administrator/templates/hathor/images/header/icon-48-default.png', - '/administrator/templates/hathor/images/header/icon-48-deny.png', - '/administrator/templates/hathor/images/header/icon-48-download.png', - '/administrator/templates/hathor/images/header/icon-48-edit.png', - '/administrator/templates/hathor/images/header/icon-48-extension.png', - '/administrator/templates/hathor/images/header/icon-48-featured.png', - '/administrator/templates/hathor/images/header/icon-48-frontpage.png', - '/administrator/templates/hathor/images/header/icon-48-generic.png', - '/administrator/templates/hathor/images/header/icon-48-groups-add.png', - '/administrator/templates/hathor/images/header/icon-48-groups.png', - '/administrator/templates/hathor/images/header/icon-48-help-forum.png', - '/administrator/templates/hathor/images/header/icon-48-help-this.png', - '/administrator/templates/hathor/images/header/icon-48-help_header.png', - '/administrator/templates/hathor/images/header/icon-48-inbox.png', - '/administrator/templates/hathor/images/header/icon-48-info.png', - '/administrator/templates/hathor/images/header/icon-48-install.png', - '/administrator/templates/hathor/images/header/icon-48-jupdate-updatefound.png', - '/administrator/templates/hathor/images/header/icon-48-jupdate-uptodate.png', - '/administrator/templates/hathor/images/header/icon-48-language.png', - '/administrator/templates/hathor/images/header/icon-48-levels-add.png', - '/administrator/templates/hathor/images/header/icon-48-levels.png', - '/administrator/templates/hathor/images/header/icon-48-links-cat.png', - '/administrator/templates/hathor/images/header/icon-48-links.png', - '/administrator/templates/hathor/images/header/icon-48-massmail.png', - '/administrator/templates/hathor/images/header/icon-48-media.png', - '/administrator/templates/hathor/images/header/icon-48-menu-add.png', - '/administrator/templates/hathor/images/header/icon-48-menu.png', - '/administrator/templates/hathor/images/header/icon-48-menumgr.png', - '/administrator/templates/hathor/images/header/icon-48-module.png', - '/administrator/templates/hathor/images/header/icon-48-move.png', - '/administrator/templates/hathor/images/header/icon-48-new-privatemessage.png', - '/administrator/templates/hathor/images/header/icon-48-newcategory.png', - '/administrator/templates/hathor/images/header/icon-48-newsfeeds-cat.png', - '/administrator/templates/hathor/images/header/icon-48-newsfeeds.png', - '/administrator/templates/hathor/images/header/icon-48-notice.png', - '/administrator/templates/hathor/images/header/icon-48-plugin.png', - '/administrator/templates/hathor/images/header/icon-48-preview.png', - '/administrator/templates/hathor/images/header/icon-48-print.png', - '/administrator/templates/hathor/images/header/icon-48-purge.png', - '/administrator/templates/hathor/images/header/icon-48-puzzle.png', - '/administrator/templates/hathor/images/header/icon-48-read-privatemessage.png', - '/administrator/templates/hathor/images/header/icon-48-readmess.png', - '/administrator/templates/hathor/images/header/icon-48-redirect.png', - '/administrator/templates/hathor/images/header/icon-48-revert.png', - '/administrator/templates/hathor/images/header/icon-48-search.png', - '/administrator/templates/hathor/images/header/icon-48-section.png', - '/administrator/templates/hathor/images/header/icon-48-send.png', - '/administrator/templates/hathor/images/header/icon-48-static.png', - '/administrator/templates/hathor/images/header/icon-48-stats.png', - '/administrator/templates/hathor/images/header/icon-48-tags.png', - '/administrator/templates/hathor/images/header/icon-48-themes.png', - '/administrator/templates/hathor/images/header/icon-48-trash.png', - '/administrator/templates/hathor/images/header/icon-48-unarchive.png', - '/administrator/templates/hathor/images/header/icon-48-upload.png', - '/administrator/templates/hathor/images/header/icon-48-user-add.png', - '/administrator/templates/hathor/images/header/icon-48-user-edit.png', - '/administrator/templates/hathor/images/header/icon-48-user-profile.png', - '/administrator/templates/hathor/images/header/icon-48-user.png', - '/administrator/templates/hathor/images/header/icon-48-writemess.png', - '/administrator/templates/hathor/images/header/icon-messaging.png', - '/administrator/templates/hathor/images/j_arrow.png', - '/administrator/templates/hathor/images/j_arrow_down.png', - '/administrator/templates/hathor/images/j_arrow_left.png', - '/administrator/templates/hathor/images/j_arrow_right.png', - '/administrator/templates/hathor/images/j_login_lock.png', - '/administrator/templates/hathor/images/j_logo.png', - '/administrator/templates/hathor/images/logo.png', - '/administrator/templates/hathor/images/menu/icon-16-alert.png', - '/administrator/templates/hathor/images/menu/icon-16-apply.png', - '/administrator/templates/hathor/images/menu/icon-16-archive.png', - '/administrator/templates/hathor/images/menu/icon-16-article.png', - '/administrator/templates/hathor/images/menu/icon-16-assoc.png', - '/administrator/templates/hathor/images/menu/icon-16-back-user.png', - '/administrator/templates/hathor/images/menu/icon-16-banner-categories.png', - '/administrator/templates/hathor/images/menu/icon-16-banner-client.png', - '/administrator/templates/hathor/images/menu/icon-16-banner-tracks.png', - '/administrator/templates/hathor/images/menu/icon-16-banner.png', - '/administrator/templates/hathor/images/menu/icon-16-calendar.png', - '/administrator/templates/hathor/images/menu/icon-16-category.png', - '/administrator/templates/hathor/images/menu/icon-16-checkin.png', - '/administrator/templates/hathor/images/menu/icon-16-clear.png', - '/administrator/templates/hathor/images/menu/icon-16-component.png', - '/administrator/templates/hathor/images/menu/icon-16-config.png', - '/administrator/templates/hathor/images/menu/icon-16-contacts-categories.png', - '/administrator/templates/hathor/images/menu/icon-16-contacts.png', - '/administrator/templates/hathor/images/menu/icon-16-content.png', - '/administrator/templates/hathor/images/menu/icon-16-cpanel.png', - '/administrator/templates/hathor/images/menu/icon-16-default.png', - '/administrator/templates/hathor/images/menu/icon-16-delete.png', - '/administrator/templates/hathor/images/menu/icon-16-deny.png', - '/administrator/templates/hathor/images/menu/icon-16-download.png', - '/administrator/templates/hathor/images/menu/icon-16-edit.png', - '/administrator/templates/hathor/images/menu/icon-16-featured.png', - '/administrator/templates/hathor/images/menu/icon-16-frontpage.png', - '/administrator/templates/hathor/images/menu/icon-16-generic.png', - '/administrator/templates/hathor/images/menu/icon-16-groups.png', - '/administrator/templates/hathor/images/menu/icon-16-help-community.png', - '/administrator/templates/hathor/images/menu/icon-16-help-dev.png', - '/administrator/templates/hathor/images/menu/icon-16-help-docs.png', - '/administrator/templates/hathor/images/menu/icon-16-help-forum.png', - '/administrator/templates/hathor/images/menu/icon-16-help-jed.png', - '/administrator/templates/hathor/images/menu/icon-16-help-jrd.png', - '/administrator/templates/hathor/images/menu/icon-16-help-security.png', - '/administrator/templates/hathor/images/menu/icon-16-help-shop.png', - '/administrator/templates/hathor/images/menu/icon-16-help-this.png', - '/administrator/templates/hathor/images/menu/icon-16-help-trans.png', - '/administrator/templates/hathor/images/menu/icon-16-help.png', - '/administrator/templates/hathor/images/menu/icon-16-inbox.png', - '/administrator/templates/hathor/images/menu/icon-16-info.png', - '/administrator/templates/hathor/images/menu/icon-16-install.png', - '/administrator/templates/hathor/images/menu/icon-16-language.png', - '/administrator/templates/hathor/images/menu/icon-16-levels.png', - '/administrator/templates/hathor/images/menu/icon-16-links-cat.png', - '/administrator/templates/hathor/images/menu/icon-16-links.png', - '/administrator/templates/hathor/images/menu/icon-16-logout.png', - '/administrator/templates/hathor/images/menu/icon-16-maintenance.png', - '/administrator/templates/hathor/images/menu/icon-16-massmail.png', - '/administrator/templates/hathor/images/menu/icon-16-media.png', - '/administrator/templates/hathor/images/menu/icon-16-menu.png', - '/administrator/templates/hathor/images/menu/icon-16-menumgr.png', - '/administrator/templates/hathor/images/menu/icon-16-messages.png', - '/administrator/templates/hathor/images/menu/icon-16-messaging.png', - '/administrator/templates/hathor/images/menu/icon-16-module.png', - '/administrator/templates/hathor/images/menu/icon-16-move.png', - '/administrator/templates/hathor/images/menu/icon-16-new-privatemessage.png', - '/administrator/templates/hathor/images/menu/icon-16-new.png', - '/administrator/templates/hathor/images/menu/icon-16-newarticle.png', - '/administrator/templates/hathor/images/menu/icon-16-newcategory.png', - '/administrator/templates/hathor/images/menu/icon-16-newgroup.png', - '/administrator/templates/hathor/images/menu/icon-16-newlevel.png', - '/administrator/templates/hathor/images/menu/icon-16-newsfeeds-cat.png', - '/administrator/templates/hathor/images/menu/icon-16-newsfeeds.png', - '/administrator/templates/hathor/images/menu/icon-16-newuser.png', - '/administrator/templates/hathor/images/menu/icon-16-nopreview.png', - '/administrator/templates/hathor/images/menu/icon-16-notdefault.png', - '/administrator/templates/hathor/images/menu/icon-16-notice.png', - '/administrator/templates/hathor/images/menu/icon-16-plugin.png', - '/administrator/templates/hathor/images/menu/icon-16-preview.png', - '/administrator/templates/hathor/images/menu/icon-16-print.png', - '/administrator/templates/hathor/images/menu/icon-16-purge.png', - '/administrator/templates/hathor/images/menu/icon-16-puzzle.png', - '/administrator/templates/hathor/images/menu/icon-16-read-privatemessage.png', - '/administrator/templates/hathor/images/menu/icon-16-readmess.png', - '/administrator/templates/hathor/images/menu/icon-16-redirect.png', - '/administrator/templates/hathor/images/menu/icon-16-revert.png', - '/administrator/templates/hathor/images/menu/icon-16-search.png', - '/administrator/templates/hathor/images/menu/icon-16-send.png', - '/administrator/templates/hathor/images/menu/icon-16-stats.png', - '/administrator/templates/hathor/images/menu/icon-16-tags.png', - '/administrator/templates/hathor/images/menu/icon-16-themes.png', - '/administrator/templates/hathor/images/menu/icon-16-trash.png', - '/administrator/templates/hathor/images/menu/icon-16-unarticle.png', - '/administrator/templates/hathor/images/menu/icon-16-upload.png', - '/administrator/templates/hathor/images/menu/icon-16-user-dd.png', - '/administrator/templates/hathor/images/menu/icon-16-user-note.png', - '/administrator/templates/hathor/images/menu/icon-16-user.png', - '/administrator/templates/hathor/images/menu/icon-16-viewsite.png', - '/administrator/templates/hathor/images/menu/icon-16-writemess.png', - '/administrator/templates/hathor/images/mini_icon.png', - '/administrator/templates/hathor/images/notice-alert.png', - '/administrator/templates/hathor/images/notice-info.png', - '/administrator/templates/hathor/images/notice-note.png', - '/administrator/templates/hathor/images/required.png', - '/administrator/templates/hathor/images/selector-arrow-hc.png', - '/administrator/templates/hathor/images/selector-arrow-rtl.png', - '/administrator/templates/hathor/images/selector-arrow-std.png', - '/administrator/templates/hathor/images/selector-arrow.png', - '/administrator/templates/hathor/images/system/calendar.png', - '/administrator/templates/hathor/images/system/selector-arrow.png', - '/administrator/templates/hathor/images/toolbar/icon-32-adduser.png', - '/administrator/templates/hathor/images/toolbar/icon-32-alert.png', - '/administrator/templates/hathor/images/toolbar/icon-32-apply.png', - '/administrator/templates/hathor/images/toolbar/icon-32-archive.png', - '/administrator/templates/hathor/images/toolbar/icon-32-article-add.png', - '/administrator/templates/hathor/images/toolbar/icon-32-article.png', - '/administrator/templates/hathor/images/toolbar/icon-32-back.png', - '/administrator/templates/hathor/images/toolbar/icon-32-banner-categories.png', - '/administrator/templates/hathor/images/toolbar/icon-32-banner-client.png', - '/administrator/templates/hathor/images/toolbar/icon-32-banner-tracks.png', - '/administrator/templates/hathor/images/toolbar/icon-32-banner.png', - '/administrator/templates/hathor/images/toolbar/icon-32-batch.png', - '/administrator/templates/hathor/images/toolbar/icon-32-calendar.png', - '/administrator/templates/hathor/images/toolbar/icon-32-cancel.png', - '/administrator/templates/hathor/images/toolbar/icon-32-checkin.png', - '/administrator/templates/hathor/images/toolbar/icon-32-cog.png', - '/administrator/templates/hathor/images/toolbar/icon-32-component.png', - '/administrator/templates/hathor/images/toolbar/icon-32-config.png', - '/administrator/templates/hathor/images/toolbar/icon-32-contacts-categories.png', - '/administrator/templates/hathor/images/toolbar/icon-32-contacts.png', - '/administrator/templates/hathor/images/toolbar/icon-32-copy.png', - '/administrator/templates/hathor/images/toolbar/icon-32-css.png', - '/administrator/templates/hathor/images/toolbar/icon-32-default.png', - '/administrator/templates/hathor/images/toolbar/icon-32-delete-style.png', - '/administrator/templates/hathor/images/toolbar/icon-32-delete.png', - '/administrator/templates/hathor/images/toolbar/icon-32-deny.png', - '/administrator/templates/hathor/images/toolbar/icon-32-download.png', - '/administrator/templates/hathor/images/toolbar/icon-32-edit.png', - '/administrator/templates/hathor/images/toolbar/icon-32-error.png', - '/administrator/templates/hathor/images/toolbar/icon-32-export.png', - '/administrator/templates/hathor/images/toolbar/icon-32-extension.png', - '/administrator/templates/hathor/images/toolbar/icon-32-featured.png', - '/administrator/templates/hathor/images/toolbar/icon-32-forward.png', - '/administrator/templates/hathor/images/toolbar/icon-32-help.png', - '/administrator/templates/hathor/images/toolbar/icon-32-html.png', - '/administrator/templates/hathor/images/toolbar/icon-32-inbox.png', - '/administrator/templates/hathor/images/toolbar/icon-32-info.png', - '/administrator/templates/hathor/images/toolbar/icon-32-links.png', - '/administrator/templates/hathor/images/toolbar/icon-32-lock.png', - '/administrator/templates/hathor/images/toolbar/icon-32-menu.png', - '/administrator/templates/hathor/images/toolbar/icon-32-messaging.png', - '/administrator/templates/hathor/images/toolbar/icon-32-messanging.png', - '/administrator/templates/hathor/images/toolbar/icon-32-module.png', - '/administrator/templates/hathor/images/toolbar/icon-32-move.png', - '/administrator/templates/hathor/images/toolbar/icon-32-new-privatemessage.png', - '/administrator/templates/hathor/images/toolbar/icon-32-new-style.png', - '/administrator/templates/hathor/images/toolbar/icon-32-new.png', - '/administrator/templates/hathor/images/toolbar/icon-32-notice.png', - '/administrator/templates/hathor/images/toolbar/icon-32-preview.png', - '/administrator/templates/hathor/images/toolbar/icon-32-print.png', - '/administrator/templates/hathor/images/toolbar/icon-32-publish.png', - '/administrator/templates/hathor/images/toolbar/icon-32-purge.png', - '/administrator/templates/hathor/images/toolbar/icon-32-read-privatemessage.png', - '/administrator/templates/hathor/images/toolbar/icon-32-refresh.png', - '/administrator/templates/hathor/images/toolbar/icon-32-remove.png', - '/administrator/templates/hathor/images/toolbar/icon-32-revert.png', - '/administrator/templates/hathor/images/toolbar/icon-32-save-copy.png', - '/administrator/templates/hathor/images/toolbar/icon-32-save-new.png', - '/administrator/templates/hathor/images/toolbar/icon-32-save.png', - '/administrator/templates/hathor/images/toolbar/icon-32-search.png', - '/administrator/templates/hathor/images/toolbar/icon-32-send.png', - '/administrator/templates/hathor/images/toolbar/icon-32-stats.png', - '/administrator/templates/hathor/images/toolbar/icon-32-trash.png', - '/administrator/templates/hathor/images/toolbar/icon-32-unarchive.png', - '/administrator/templates/hathor/images/toolbar/icon-32-unblock.png', - '/administrator/templates/hathor/images/toolbar/icon-32-unpublish.png', - '/administrator/templates/hathor/images/toolbar/icon-32-upload.png', - '/administrator/templates/hathor/images/toolbar/icon-32-user-add.png', - '/administrator/templates/hathor/images/toolbar/icon-32-xml.png', - '/administrator/templates/hathor/index.php', - '/administrator/templates/hathor/js/template.js', - '/administrator/templates/hathor/language/en-GB/en-GB.tpl_hathor.ini', - '/administrator/templates/hathor/language/en-GB/en-GB.tpl_hathor.sys.ini', - '/administrator/templates/hathor/less/buttons.less', - '/administrator/templates/hathor/less/colour_baseline.less', - '/administrator/templates/hathor/less/colour_blue.less', - '/administrator/templates/hathor/less/colour_brown.less', - '/administrator/templates/hathor/less/colour_standard.less', - '/administrator/templates/hathor/less/forms.less', - '/administrator/templates/hathor/less/hathor_variables.less', - '/administrator/templates/hathor/less/icomoon.less', - '/administrator/templates/hathor/less/modals.less', - '/administrator/templates/hathor/less/template.less', - '/administrator/templates/hathor/less/variables.less', - '/administrator/templates/hathor/login.php', - '/administrator/templates/hathor/postinstall/hathormessage.php', - '/administrator/templates/hathor/templateDetails.xml', - '/administrator/templates/hathor/template_preview.png', - '/administrator/templates/hathor/template_thumbnail.png', - '/administrator/templates/isis/component.php', - '/administrator/templates/isis/cpanel.php', - '/administrator/templates/isis/css/template-rtl.css', - '/administrator/templates/isis/css/template.css', - '/administrator/templates/isis/error.php', - '/administrator/templates/isis/favicon.ico', - '/administrator/templates/isis/html/com_media/imageslist/default_folder.php', - '/administrator/templates/isis/html/com_media/imageslist/default_image.php', - '/administrator/templates/isis/html/com_media/medialist/thumbs_folders.php', - '/administrator/templates/isis/html/com_media/medialist/thumbs_imgs.php', - '/administrator/templates/isis/html/editor_content.css', - '/administrator/templates/isis/html/layouts/joomla/form/field/media.php', - '/administrator/templates/isis/html/layouts/joomla/form/field/user.php', - '/administrator/templates/isis/html/layouts/joomla/pagination/link.php', - '/administrator/templates/isis/html/layouts/joomla/pagination/links.php', - '/administrator/templates/isis/html/layouts/joomla/system/message.php', - '/administrator/templates/isis/html/layouts/joomla/toolbar/versions.php', - '/administrator/templates/isis/html/mod_version/default.php', - '/administrator/templates/isis/html/modules.php', - '/administrator/templates/isis/html/pagination.php', - '/administrator/templates/isis/images/admin/blank.png', - '/administrator/templates/isis/images/admin/checked_out.png', - '/administrator/templates/isis/images/admin/collapseall.png', - '/administrator/templates/isis/images/admin/disabled.png', - '/administrator/templates/isis/images/admin/downarrow-1.png', - '/administrator/templates/isis/images/admin/downarrow.png', - '/administrator/templates/isis/images/admin/downarrow0.png', - '/administrator/templates/isis/images/admin/expandall.png', - '/administrator/templates/isis/images/admin/featured.png', - '/administrator/templates/isis/images/admin/filesave.png', - '/administrator/templates/isis/images/admin/filter_16.png', - '/administrator/templates/isis/images/admin/icon-16-add.png', - '/administrator/templates/isis/images/admin/icon-16-allow.png', - '/administrator/templates/isis/images/admin/icon-16-allowinactive.png', - '/administrator/templates/isis/images/admin/icon-16-deny.png', - '/administrator/templates/isis/images/admin/icon-16-denyinactive.png', - '/administrator/templates/isis/images/admin/icon-16-links.png', - '/administrator/templates/isis/images/admin/icon-16-notice-note.png', - '/administrator/templates/isis/images/admin/icon-16-protected.png', - '/administrator/templates/isis/images/admin/menu_divider.png', - '/administrator/templates/isis/images/admin/note_add_16.png', - '/administrator/templates/isis/images/admin/publish_g.png', - '/administrator/templates/isis/images/admin/publish_r.png', - '/administrator/templates/isis/images/admin/publish_x.png', - '/administrator/templates/isis/images/admin/publish_y.png', - '/administrator/templates/isis/images/admin/sort_asc.png', - '/administrator/templates/isis/images/admin/sort_desc.png', - '/administrator/templates/isis/images/admin/tick.png', - '/administrator/templates/isis/images/admin/trash.png', - '/administrator/templates/isis/images/admin/uparrow-1.png', - '/administrator/templates/isis/images/admin/uparrow.png', - '/administrator/templates/isis/images/admin/uparrow0.png', - '/administrator/templates/isis/images/emailButton.png', - '/administrator/templates/isis/images/joomla.png', - '/administrator/templates/isis/images/login-joomla-inverse.png', - '/administrator/templates/isis/images/login-joomla.png', - '/administrator/templates/isis/images/logo-inverse.png', - '/administrator/templates/isis/images/logo.png', - '/administrator/templates/isis/images/pdf_button.png', - '/administrator/templates/isis/images/printButton.png', - '/administrator/templates/isis/images/system/sort_asc.png', - '/administrator/templates/isis/images/system/sort_desc.png', - '/administrator/templates/isis/img/glyphicons-halflings-white.png', - '/administrator/templates/isis/img/glyphicons-halflings.png', - '/administrator/templates/isis/index.php', - '/administrator/templates/isis/js/application.js', - '/administrator/templates/isis/js/classes.js', - '/administrator/templates/isis/js/template.js', - '/administrator/templates/isis/language/en-GB/en-GB.tpl_isis.ini', - '/administrator/templates/isis/language/en-GB/en-GB.tpl_isis.sys.ini', - '/administrator/templates/isis/less/blocks/_chzn-override.less', - '/administrator/templates/isis/less/blocks/_custom.less', - '/administrator/templates/isis/less/blocks/_editors.less', - '/administrator/templates/isis/less/blocks/_forms.less', - '/administrator/templates/isis/less/blocks/_global.less', - '/administrator/templates/isis/less/blocks/_header.less', - '/administrator/templates/isis/less/blocks/_login.less', - '/administrator/templates/isis/less/blocks/_media.less', - '/administrator/templates/isis/less/blocks/_modals.less', - '/administrator/templates/isis/less/blocks/_navbar.less', - '/administrator/templates/isis/less/blocks/_quickicons.less', - '/administrator/templates/isis/less/blocks/_sidebar.less', - '/administrator/templates/isis/less/blocks/_status.less', - '/administrator/templates/isis/less/blocks/_tables.less', - '/administrator/templates/isis/less/blocks/_toolbar.less', - '/administrator/templates/isis/less/blocks/_treeselect.less', - '/administrator/templates/isis/less/blocks/_utility-classes.less', - '/administrator/templates/isis/less/bootstrap/button-groups.less', - '/administrator/templates/isis/less/bootstrap/buttons.less', - '/administrator/templates/isis/less/bootstrap/mixins.less', - '/administrator/templates/isis/less/bootstrap/responsive-1200px-min.less', - '/administrator/templates/isis/less/bootstrap/responsive-768px-979px.less', - '/administrator/templates/isis/less/bootstrap/wells.less', - '/administrator/templates/isis/less/icomoon.less', - '/administrator/templates/isis/less/pages/_com_cpanel.less', - '/administrator/templates/isis/less/pages/_com_postinstall.less', - '/administrator/templates/isis/less/pages/_com_privacy.less', - '/administrator/templates/isis/less/pages/_com_templates.less', - '/administrator/templates/isis/less/template-rtl.less', - '/administrator/templates/isis/less/template.less', - '/administrator/templates/isis/less/variables.less', - '/administrator/templates/isis/login.php', - '/administrator/templates/isis/templateDetails.xml', - '/administrator/templates/isis/template_preview.png', - '/administrator/templates/isis/template_thumbnail.png', - '/administrator/templates/system/html/modules.php', - '/bin/index.html', - '/bin/keychain.php', - '/cli/deletefiles.php', - '/cli/finder_indexer.php', - '/cli/garbagecron.php', - '/cli/sessionGc.php', - '/cli/sessionMetadataGc.php', - '/cli/update_cron.php', - '/components/com_banners/banners.php', - '/components/com_banners/controller.php', - '/components/com_banners/helpers/banner.php', - '/components/com_banners/helpers/category.php', - '/components/com_banners/models/banner.php', - '/components/com_banners/models/banners.php', - '/components/com_banners/router.php', - '/components/com_config/config.php', - '/components/com_config/controller/cancel.php', - '/components/com_config/controller/canceladmin.php', - '/components/com_config/controller/cmsbase.php', - '/components/com_config/controller/config/display.php', - '/components/com_config/controller/config/save.php', - '/components/com_config/controller/display.php', - '/components/com_config/controller/helper.php', - '/components/com_config/controller/modules/cancel.php', - '/components/com_config/controller/modules/display.php', - '/components/com_config/controller/modules/save.php', - '/components/com_config/controller/templates/display.php', - '/components/com_config/controller/templates/save.php', - '/components/com_config/model/cms.php', - '/components/com_config/model/config.php', - '/components/com_config/model/form.php', - '/components/com_config/model/form/config.xml', - '/components/com_config/model/form/modules.xml', - '/components/com_config/model/form/modules_advanced.xml', - '/components/com_config/model/form/templates.xml', - '/components/com_config/model/modules.php', - '/components/com_config/model/templates.php', - '/components/com_config/view/cms/html.php', - '/components/com_config/view/cms/json.php', - '/components/com_config/view/config/html.php', - '/components/com_config/view/config/tmpl/default.php', - '/components/com_config/view/config/tmpl/default.xml', - '/components/com_config/view/config/tmpl/default_metadata.php', - '/components/com_config/view/config/tmpl/default_seo.php', - '/components/com_config/view/config/tmpl/default_site.php', - '/components/com_config/view/modules/html.php', - '/components/com_config/view/modules/tmpl/default.php', - '/components/com_config/view/modules/tmpl/default_options.php', - '/components/com_config/view/modules/tmpl/default_positions.php', - '/components/com_config/view/templates/html.php', - '/components/com_config/view/templates/tmpl/default.php', - '/components/com_config/view/templates/tmpl/default.xml', - '/components/com_config/view/templates/tmpl/default_options.php', - '/components/com_contact/contact.php', - '/components/com_contact/controller.php', - '/components/com_contact/controllers/contact.php', - '/components/com_contact/helpers/association.php', - '/components/com_contact/helpers/category.php', - '/components/com_contact/helpers/legacyrouter.php', - '/components/com_contact/layouts/joomla/form/renderfield.php', - '/components/com_contact/models/categories.php', - '/components/com_contact/models/category.php', - '/components/com_contact/models/contact.php', - '/components/com_contact/models/featured.php', - '/components/com_contact/models/forms/contact.xml', - '/components/com_contact/models/forms/filter_contacts.xml', - '/components/com_contact/models/forms/form.xml', - '/components/com_contact/models/rules/contactemail.php', - '/components/com_contact/models/rules/contactemailmessage.php', - '/components/com_contact/models/rules/contactemailsubject.php', - '/components/com_contact/router.php', - '/components/com_contact/views/categories/tmpl/default.php', - '/components/com_contact/views/categories/tmpl/default.xml', - '/components/com_contact/views/categories/tmpl/default_items.php', - '/components/com_contact/views/categories/view.html.php', - '/components/com_contact/views/category/tmpl/default.php', - '/components/com_contact/views/category/tmpl/default.xml', - '/components/com_contact/views/category/tmpl/default_children.php', - '/components/com_contact/views/category/tmpl/default_items.php', - '/components/com_contact/views/category/view.feed.php', - '/components/com_contact/views/category/view.html.php', - '/components/com_contact/views/contact/tmpl/default.php', - '/components/com_contact/views/contact/tmpl/default.xml', - '/components/com_contact/views/contact/tmpl/default_address.php', - '/components/com_contact/views/contact/tmpl/default_articles.php', - '/components/com_contact/views/contact/tmpl/default_form.php', - '/components/com_contact/views/contact/tmpl/default_links.php', - '/components/com_contact/views/contact/tmpl/default_profile.php', - '/components/com_contact/views/contact/tmpl/default_user_custom_fields.php', - '/components/com_contact/views/contact/view.html.php', - '/components/com_contact/views/contact/view.vcf.php', - '/components/com_contact/views/featured/tmpl/default.php', - '/components/com_contact/views/featured/tmpl/default.xml', - '/components/com_contact/views/featured/tmpl/default_items.php', - '/components/com_contact/views/featured/view.html.php', - '/components/com_content/content.php', - '/components/com_content/controller.php', - '/components/com_content/controllers/article.php', - '/components/com_content/helpers/association.php', - '/components/com_content/helpers/category.php', - '/components/com_content/helpers/legacyrouter.php', - '/components/com_content/helpers/query.php', - '/components/com_content/helpers/route.php', - '/components/com_content/models/archive.php', - '/components/com_content/models/article.php', - '/components/com_content/models/articles.php', - '/components/com_content/models/categories.php', - '/components/com_content/models/category.php', - '/components/com_content/models/featured.php', - '/components/com_content/models/form.php', - '/components/com_content/models/forms/article.xml', - '/components/com_content/models/forms/filter_articles.xml', - '/components/com_content/router.php', - '/components/com_content/views/archive/tmpl/default.php', - '/components/com_content/views/archive/tmpl/default.xml', - '/components/com_content/views/archive/tmpl/default_items.php', - '/components/com_content/views/archive/view.html.php', - '/components/com_content/views/article/tmpl/default.php', - '/components/com_content/views/article/tmpl/default.xml', - '/components/com_content/views/article/tmpl/default_links.php', - '/components/com_content/views/article/view.html.php', - '/components/com_content/views/categories/tmpl/default.php', - '/components/com_content/views/categories/tmpl/default.xml', - '/components/com_content/views/categories/tmpl/default_items.php', - '/components/com_content/views/categories/view.html.php', - '/components/com_content/views/category/tmpl/blog.php', - '/components/com_content/views/category/tmpl/blog.xml', - '/components/com_content/views/category/tmpl/blog_children.php', - '/components/com_content/views/category/tmpl/blog_item.php', - '/components/com_content/views/category/tmpl/blog_links.php', - '/components/com_content/views/category/tmpl/default.php', - '/components/com_content/views/category/tmpl/default.xml', - '/components/com_content/views/category/tmpl/default_articles.php', - '/components/com_content/views/category/tmpl/default_children.php', - '/components/com_content/views/category/view.feed.php', - '/components/com_content/views/category/view.html.php', - '/components/com_content/views/featured/tmpl/default.php', - '/components/com_content/views/featured/tmpl/default.xml', - '/components/com_content/views/featured/tmpl/default_item.php', - '/components/com_content/views/featured/tmpl/default_links.php', - '/components/com_content/views/featured/view.feed.php', - '/components/com_content/views/featured/view.html.php', - '/components/com_content/views/form/tmpl/edit.php', - '/components/com_content/views/form/tmpl/edit.xml', - '/components/com_content/views/form/view.html.php', - '/components/com_contenthistory/contenthistory.php', - '/components/com_fields/controller.php', - '/components/com_fields/fields.php', - '/components/com_fields/models/forms/filter_fields.xml', - '/components/com_finder/controller.php', - '/components/com_finder/controllers/suggestions.json.php', - '/components/com_finder/finder.php', - '/components/com_finder/helpers/html/filter.php', - '/components/com_finder/helpers/html/query.php', - '/components/com_finder/models/search.php', - '/components/com_finder/models/suggestions.php', - '/components/com_finder/router.php', - '/components/com_finder/views/search/tmpl/default.php', - '/components/com_finder/views/search/tmpl/default.xml', - '/components/com_finder/views/search/tmpl/default_form.php', - '/components/com_finder/views/search/tmpl/default_result.php', - '/components/com_finder/views/search/tmpl/default_results.php', - '/components/com_finder/views/search/view.feed.php', - '/components/com_finder/views/search/view.html.php', - '/components/com_finder/views/search/view.opensearch.php', - '/components/com_mailto/controller.php', - '/components/com_mailto/helpers/mailto.php', - '/components/com_mailto/mailto.php', - '/components/com_mailto/mailto.xml', - '/components/com_mailto/models/forms/mailto.xml', - '/components/com_mailto/models/mailto.php', - '/components/com_mailto/views/mailto/tmpl/default.php', - '/components/com_mailto/views/mailto/view.html.php', - '/components/com_mailto/views/sent/tmpl/default.php', - '/components/com_mailto/views/sent/view.html.php', - '/components/com_media/media.php', - '/components/com_menus/controller.php', - '/components/com_menus/menus.php', - '/components/com_menus/models/forms/filter_items.xml', - '/components/com_modules/controller.php', - '/components/com_modules/models/forms/filter_modules.xml', - '/components/com_modules/modules.php', - '/components/com_newsfeeds/controller.php', - '/components/com_newsfeeds/helpers/association.php', - '/components/com_newsfeeds/helpers/category.php', - '/components/com_newsfeeds/helpers/legacyrouter.php', - '/components/com_newsfeeds/models/categories.php', - '/components/com_newsfeeds/models/category.php', - '/components/com_newsfeeds/models/newsfeed.php', - '/components/com_newsfeeds/newsfeeds.php', - '/components/com_newsfeeds/router.php', - '/components/com_newsfeeds/views/categories/tmpl/default.php', - '/components/com_newsfeeds/views/categories/tmpl/default.xml', - '/components/com_newsfeeds/views/categories/tmpl/default_items.php', - '/components/com_newsfeeds/views/categories/view.html.php', - '/components/com_newsfeeds/views/category/tmpl/default.php', - '/components/com_newsfeeds/views/category/tmpl/default.xml', - '/components/com_newsfeeds/views/category/tmpl/default_children.php', - '/components/com_newsfeeds/views/category/tmpl/default_items.php', - '/components/com_newsfeeds/views/category/view.html.php', - '/components/com_newsfeeds/views/newsfeed/tmpl/default.php', - '/components/com_newsfeeds/views/newsfeed/tmpl/default.xml', - '/components/com_newsfeeds/views/newsfeed/view.html.php', - '/components/com_privacy/controller.php', - '/components/com_privacy/controllers/request.php', - '/components/com_privacy/models/confirm.php', - '/components/com_privacy/models/forms/confirm.xml', - '/components/com_privacy/models/forms/remind.xml', - '/components/com_privacy/models/forms/request.xml', - '/components/com_privacy/models/remind.php', - '/components/com_privacy/models/request.php', - '/components/com_privacy/privacy.php', - '/components/com_privacy/router.php', - '/components/com_privacy/views/confirm/tmpl/default.php', - '/components/com_privacy/views/confirm/tmpl/default.xml', - '/components/com_privacy/views/confirm/view.html.php', - '/components/com_privacy/views/remind/tmpl/default.php', - '/components/com_privacy/views/remind/tmpl/default.xml', - '/components/com_privacy/views/remind/view.html.php', - '/components/com_privacy/views/request/tmpl/default.php', - '/components/com_privacy/views/request/tmpl/default.xml', - '/components/com_privacy/views/request/view.html.php', - '/components/com_tags/controller.php', - '/components/com_tags/controllers/tags.php', - '/components/com_tags/models/tag.php', - '/components/com_tags/models/tags.php', - '/components/com_tags/router.php', - '/components/com_tags/tags.php', - '/components/com_tags/views/tag/tmpl/default.php', - '/components/com_tags/views/tag/tmpl/default.xml', - '/components/com_tags/views/tag/tmpl/default_items.php', - '/components/com_tags/views/tag/tmpl/list.php', - '/components/com_tags/views/tag/tmpl/list.xml', - '/components/com_tags/views/tag/tmpl/list_items.php', - '/components/com_tags/views/tag/view.feed.php', - '/components/com_tags/views/tag/view.html.php', - '/components/com_tags/views/tags/tmpl/default.php', - '/components/com_tags/views/tags/tmpl/default.xml', - '/components/com_tags/views/tags/tmpl/default_items.php', - '/components/com_tags/views/tags/view.feed.php', - '/components/com_tags/views/tags/view.html.php', - '/components/com_users/controller.php', - '/components/com_users/controllers/profile.php', - '/components/com_users/controllers/registration.php', - '/components/com_users/controllers/remind.php', - '/components/com_users/controllers/reset.php', - '/components/com_users/controllers/user.php', - '/components/com_users/helpers/html/users.php', - '/components/com_users/helpers/legacyrouter.php', - '/components/com_users/helpers/route.php', - '/components/com_users/layouts/joomla/form/renderfield.php', - '/components/com_users/models/forms/frontend.xml', - '/components/com_users/models/forms/frontend_admin.xml', - '/components/com_users/models/forms/login.xml', - '/components/com_users/models/forms/profile.xml', - '/components/com_users/models/forms/registration.xml', - '/components/com_users/models/forms/remind.xml', - '/components/com_users/models/forms/reset_complete.xml', - '/components/com_users/models/forms/reset_confirm.xml', - '/components/com_users/models/forms/reset_request.xml', - '/components/com_users/models/forms/sitelang.xml', - '/components/com_users/models/login.php', - '/components/com_users/models/profile.php', - '/components/com_users/models/registration.php', - '/components/com_users/models/remind.php', - '/components/com_users/models/reset.php', - '/components/com_users/models/rules/loginuniquefield.php', - '/components/com_users/models/rules/logoutuniquefield.php', - '/components/com_users/router.php', - '/components/com_users/users.php', - '/components/com_users/views/login/tmpl/default.php', - '/components/com_users/views/login/tmpl/default.xml', - '/components/com_users/views/login/tmpl/default_login.php', - '/components/com_users/views/login/tmpl/default_logout.php', - '/components/com_users/views/login/tmpl/logout.xml', - '/components/com_users/views/login/view.html.php', - '/components/com_users/views/profile/tmpl/default.php', - '/components/com_users/views/profile/tmpl/default.xml', - '/components/com_users/views/profile/tmpl/default_core.php', - '/components/com_users/views/profile/tmpl/default_custom.php', - '/components/com_users/views/profile/tmpl/default_params.php', - '/components/com_users/views/profile/tmpl/edit.php', - '/components/com_users/views/profile/tmpl/edit.xml', - '/components/com_users/views/profile/view.html.php', - '/components/com_users/views/registration/tmpl/complete.php', - '/components/com_users/views/registration/tmpl/default.php', - '/components/com_users/views/registration/tmpl/default.xml', - '/components/com_users/views/registration/view.html.php', - '/components/com_users/views/remind/tmpl/default.php', - '/components/com_users/views/remind/tmpl/default.xml', - '/components/com_users/views/remind/view.html.php', - '/components/com_users/views/reset/tmpl/complete.php', - '/components/com_users/views/reset/tmpl/confirm.php', - '/components/com_users/views/reset/tmpl/default.php', - '/components/com_users/views/reset/tmpl/default.xml', - '/components/com_users/views/reset/view.html.php', - '/components/com_wrapper/controller.php', - '/components/com_wrapper/router.php', - '/components/com_wrapper/views/wrapper/tmpl/default.php', - '/components/com_wrapper/views/wrapper/tmpl/default.xml', - '/components/com_wrapper/views/wrapper/view.html.php', - '/components/com_wrapper/wrapper.php', - '/components/com_wrapper/wrapper.xml', - '/language/en-GB/en-GB.com_ajax.ini', - '/language/en-GB/en-GB.com_config.ini', - '/language/en-GB/en-GB.com_contact.ini', - '/language/en-GB/en-GB.com_content.ini', - '/language/en-GB/en-GB.com_finder.ini', - '/language/en-GB/en-GB.com_mailto.ini', - '/language/en-GB/en-GB.com_media.ini', - '/language/en-GB/en-GB.com_messages.ini', - '/language/en-GB/en-GB.com_newsfeeds.ini', - '/language/en-GB/en-GB.com_privacy.ini', - '/language/en-GB/en-GB.com_tags.ini', - '/language/en-GB/en-GB.com_users.ini', - '/language/en-GB/en-GB.com_weblinks.ini', - '/language/en-GB/en-GB.com_wrapper.ini', - '/language/en-GB/en-GB.files_joomla.sys.ini', - '/language/en-GB/en-GB.finder_cli.ini', - '/language/en-GB/en-GB.ini', - '/language/en-GB/en-GB.lib_fof.ini', - '/language/en-GB/en-GB.lib_fof.sys.ini', - '/language/en-GB/en-GB.lib_idna_convert.sys.ini', - '/language/en-GB/en-GB.lib_joomla.ini', - '/language/en-GB/en-GB.lib_joomla.sys.ini', - '/language/en-GB/en-GB.lib_phpass.sys.ini', - '/language/en-GB/en-GB.lib_phputf8.sys.ini', - '/language/en-GB/en-GB.lib_simplepie.sys.ini', - '/language/en-GB/en-GB.localise.php', - '/language/en-GB/en-GB.mod_articles_archive.ini', - '/language/en-GB/en-GB.mod_articles_archive.sys.ini', - '/language/en-GB/en-GB.mod_articles_categories.ini', - '/language/en-GB/en-GB.mod_articles_categories.sys.ini', - '/language/en-GB/en-GB.mod_articles_category.ini', - '/language/en-GB/en-GB.mod_articles_category.sys.ini', - '/language/en-GB/en-GB.mod_articles_latest.ini', - '/language/en-GB/en-GB.mod_articles_latest.sys.ini', - '/language/en-GB/en-GB.mod_articles_news.ini', - '/language/en-GB/en-GB.mod_articles_news.sys.ini', - '/language/en-GB/en-GB.mod_articles_popular.ini', - '/language/en-GB/en-GB.mod_articles_popular.sys.ini', - '/language/en-GB/en-GB.mod_banners.ini', - '/language/en-GB/en-GB.mod_banners.sys.ini', - '/language/en-GB/en-GB.mod_breadcrumbs.ini', - '/language/en-GB/en-GB.mod_breadcrumbs.sys.ini', - '/language/en-GB/en-GB.mod_custom.ini', - '/language/en-GB/en-GB.mod_custom.sys.ini', - '/language/en-GB/en-GB.mod_feed.ini', - '/language/en-GB/en-GB.mod_feed.sys.ini', - '/language/en-GB/en-GB.mod_finder.ini', - '/language/en-GB/en-GB.mod_finder.sys.ini', - '/language/en-GB/en-GB.mod_footer.ini', - '/language/en-GB/en-GB.mod_footer.sys.ini', - '/language/en-GB/en-GB.mod_languages.ini', - '/language/en-GB/en-GB.mod_languages.sys.ini', - '/language/en-GB/en-GB.mod_login.ini', - '/language/en-GB/en-GB.mod_login.sys.ini', - '/language/en-GB/en-GB.mod_menu.ini', - '/language/en-GB/en-GB.mod_menu.sys.ini', - '/language/en-GB/en-GB.mod_random_image.ini', - '/language/en-GB/en-GB.mod_random_image.sys.ini', - '/language/en-GB/en-GB.mod_related_items.ini', - '/language/en-GB/en-GB.mod_related_items.sys.ini', - '/language/en-GB/en-GB.mod_stats.ini', - '/language/en-GB/en-GB.mod_stats.sys.ini', - '/language/en-GB/en-GB.mod_syndicate.ini', - '/language/en-GB/en-GB.mod_syndicate.sys.ini', - '/language/en-GB/en-GB.mod_tags_popular.ini', - '/language/en-GB/en-GB.mod_tags_popular.sys.ini', - '/language/en-GB/en-GB.mod_tags_similar.ini', - '/language/en-GB/en-GB.mod_tags_similar.sys.ini', - '/language/en-GB/en-GB.mod_users_latest.ini', - '/language/en-GB/en-GB.mod_users_latest.sys.ini', - '/language/en-GB/en-GB.mod_weblinks.ini', - '/language/en-GB/en-GB.mod_weblinks.sys.ini', - '/language/en-GB/en-GB.mod_whosonline.ini', - '/language/en-GB/en-GB.mod_whosonline.sys.ini', - '/language/en-GB/en-GB.mod_wrapper.ini', - '/language/en-GB/en-GB.mod_wrapper.sys.ini', - '/language/en-GB/en-GB.tpl_beez3.ini', - '/language/en-GB/en-GB.tpl_beez3.sys.ini', - '/language/en-GB/en-GB.tpl_protostar.ini', - '/language/en-GB/en-GB.tpl_protostar.sys.ini', - '/language/en-GB/en-GB.xml', - '/layouts/joomla/content/blog_style_default_links.php', - '/layouts/joomla/content/icons/email.php', - '/layouts/joomla/content/icons/print_popup.php', - '/layouts/joomla/content/icons/print_screen.php', - '/layouts/joomla/content/info_block/block.php', - '/layouts/joomla/edit/details.php', - '/layouts/joomla/edit/item_title.php', - '/layouts/joomla/form/field/radio.php', - '/layouts/joomla/html/formbehavior/ajaxchosen.php', - '/layouts/joomla/html/formbehavior/chosen.php', - '/layouts/joomla/html/sortablelist.php', - '/layouts/joomla/html/tag.php', - '/layouts/joomla/modal/body.php', - '/layouts/joomla/modal/footer.php', - '/layouts/joomla/modal/header.php', - '/layouts/joomla/modal/iframe.php', - '/layouts/joomla/modal/main.php', - '/layouts/joomla/sidebars/toggle.php', - '/layouts/joomla/tinymce/buttons.php', - '/layouts/joomla/tinymce/buttons/button.php', - '/layouts/joomla/toolbar/confirm.php', - '/layouts/joomla/toolbar/help.php', - '/layouts/joomla/toolbar/modal.php', - '/layouts/joomla/toolbar/slider.php', - '/layouts/libraries/cms/html/bootstrap/addtab.php', - '/layouts/libraries/cms/html/bootstrap/addtabscript.php', - '/layouts/libraries/cms/html/bootstrap/endtab.php', - '/layouts/libraries/cms/html/bootstrap/endtabset.php', - '/layouts/libraries/cms/html/bootstrap/starttabset.php', - '/layouts/libraries/cms/html/bootstrap/starttabsetscript.php', - '/libraries/cms/class/loader.php', - '/libraries/cms/html/access.php', - '/libraries/cms/html/actionsdropdown.php', - '/libraries/cms/html/adminlanguage.php', - '/libraries/cms/html/batch.php', - '/libraries/cms/html/behavior.php', - '/libraries/cms/html/bootstrap.php', - '/libraries/cms/html/category.php', - '/libraries/cms/html/content.php', - '/libraries/cms/html/contentlanguage.php', - '/libraries/cms/html/date.php', - '/libraries/cms/html/debug.php', - '/libraries/cms/html/dropdown.php', - '/libraries/cms/html/email.php', - '/libraries/cms/html/form.php', - '/libraries/cms/html/formbehavior.php', - '/libraries/cms/html/grid.php', - '/libraries/cms/html/icons.php', - '/libraries/cms/html/jgrid.php', - '/libraries/cms/html/jquery.php', - '/libraries/cms/html/language/en-GB/en-GB.jhtmldate.ini', - '/libraries/cms/html/links.php', - '/libraries/cms/html/list.php', - '/libraries/cms/html/menu.php', - '/libraries/cms/html/number.php', - '/libraries/cms/html/rules.php', - '/libraries/cms/html/searchtools.php', - '/libraries/cms/html/select.php', - '/libraries/cms/html/sidebar.php', - '/libraries/cms/html/sliders.php', - '/libraries/cms/html/sortablelist.php', - '/libraries/cms/html/string.php', - '/libraries/cms/html/tabs.php', - '/libraries/cms/html/tag.php', - '/libraries/cms/html/tel.php', - '/libraries/cms/html/user.php', - '/libraries/cms/less/formatter/joomla.php', - '/libraries/cms/less/less.php', - '/libraries/fof/LICENSE.txt', - '/libraries/fof/autoloader/component.php', - '/libraries/fof/autoloader/fof.php', - '/libraries/fof/config/domain/dispatcher.php', - '/libraries/fof/config/domain/interface.php', - '/libraries/fof/config/domain/tables.php', - '/libraries/fof/config/domain/views.php', - '/libraries/fof/config/provider.php', - '/libraries/fof/controller/controller.php', - '/libraries/fof/database/database.php', - '/libraries/fof/database/driver.php', - '/libraries/fof/database/driver/joomla.php', - '/libraries/fof/database/driver/mysql.php', - '/libraries/fof/database/driver/mysqli.php', - '/libraries/fof/database/driver/oracle.php', - '/libraries/fof/database/driver/pdo.php', - '/libraries/fof/database/driver/pdomysql.php', - '/libraries/fof/database/driver/postgresql.php', - '/libraries/fof/database/driver/sqlazure.php', - '/libraries/fof/database/driver/sqlite.php', - '/libraries/fof/database/driver/sqlsrv.php', - '/libraries/fof/database/factory.php', - '/libraries/fof/database/installer.php', - '/libraries/fof/database/interface.php', - '/libraries/fof/database/iterator.php', - '/libraries/fof/database/iterator/azure.php', - '/libraries/fof/database/iterator/mysql.php', - '/libraries/fof/database/iterator/mysqli.php', - '/libraries/fof/database/iterator/oracle.php', - '/libraries/fof/database/iterator/pdo.php', - '/libraries/fof/database/iterator/pdomysql.php', - '/libraries/fof/database/iterator/postgresql.php', - '/libraries/fof/database/iterator/sqlite.php', - '/libraries/fof/database/iterator/sqlsrv.php', - '/libraries/fof/database/query.php', - '/libraries/fof/database/query/element.php', - '/libraries/fof/database/query/limitable.php', - '/libraries/fof/database/query/mysql.php', - '/libraries/fof/database/query/mysqli.php', - '/libraries/fof/database/query/oracle.php', - '/libraries/fof/database/query/pdo.php', - '/libraries/fof/database/query/pdomysql.php', - '/libraries/fof/database/query/postgresql.php', - '/libraries/fof/database/query/preparable.php', - '/libraries/fof/database/query/sqlazure.php', - '/libraries/fof/database/query/sqlite.php', - '/libraries/fof/database/query/sqlsrv.php', - '/libraries/fof/dispatcher/dispatcher.php', - '/libraries/fof/download/adapter/abstract.php', - '/libraries/fof/download/adapter/cacert.pem', - '/libraries/fof/download/adapter/curl.php', - '/libraries/fof/download/adapter/fopen.php', - '/libraries/fof/download/download.php', - '/libraries/fof/download/interface.php', - '/libraries/fof/encrypt/aes.php', - '/libraries/fof/encrypt/aes/abstract.php', - '/libraries/fof/encrypt/aes/interface.php', - '/libraries/fof/encrypt/aes/mcrypt.php', - '/libraries/fof/encrypt/aes/openssl.php', - '/libraries/fof/encrypt/base32.php', - '/libraries/fof/encrypt/randval.php', - '/libraries/fof/encrypt/randvalinterface.php', - '/libraries/fof/encrypt/totp.php', - '/libraries/fof/form/field.php', - '/libraries/fof/form/field/accesslevel.php', - '/libraries/fof/form/field/actions.php', - '/libraries/fof/form/field/button.php', - '/libraries/fof/form/field/cachehandler.php', - '/libraries/fof/form/field/calendar.php', - '/libraries/fof/form/field/captcha.php', - '/libraries/fof/form/field/checkbox.php', - '/libraries/fof/form/field/checkboxes.php', - '/libraries/fof/form/field/components.php', - '/libraries/fof/form/field/editor.php', - '/libraries/fof/form/field/email.php', - '/libraries/fof/form/field/groupedbutton.php', - '/libraries/fof/form/field/groupedlist.php', - '/libraries/fof/form/field/hidden.php', - '/libraries/fof/form/field/image.php', - '/libraries/fof/form/field/imagelist.php', - '/libraries/fof/form/field/integer.php', - '/libraries/fof/form/field/language.php', - '/libraries/fof/form/field/list.php', - '/libraries/fof/form/field/media.php', - '/libraries/fof/form/field/model.php', - '/libraries/fof/form/field/ordering.php', - '/libraries/fof/form/field/password.php', - '/libraries/fof/form/field/plugins.php', - '/libraries/fof/form/field/published.php', - '/libraries/fof/form/field/radio.php', - '/libraries/fof/form/field/relation.php', - '/libraries/fof/form/field/rules.php', - '/libraries/fof/form/field/selectrow.php', - '/libraries/fof/form/field/sessionhandler.php', - '/libraries/fof/form/field/spacer.php', - '/libraries/fof/form/field/sql.php', - '/libraries/fof/form/field/tag.php', - '/libraries/fof/form/field/tel.php', - '/libraries/fof/form/field/text.php', - '/libraries/fof/form/field/textarea.php', - '/libraries/fof/form/field/timezone.php', - '/libraries/fof/form/field/title.php', - '/libraries/fof/form/field/url.php', - '/libraries/fof/form/field/user.php', - '/libraries/fof/form/field/usergroup.php', - '/libraries/fof/form/form.php', - '/libraries/fof/form/header.php', - '/libraries/fof/form/header/accesslevel.php', - '/libraries/fof/form/header/field.php', - '/libraries/fof/form/header/fielddate.php', - '/libraries/fof/form/header/fieldfilterable.php', - '/libraries/fof/form/header/fieldsearchable.php', - '/libraries/fof/form/header/fieldselectable.php', - '/libraries/fof/form/header/fieldsql.php', - '/libraries/fof/form/header/filterdate.php', - '/libraries/fof/form/header/filterfilterable.php', - '/libraries/fof/form/header/filtersearchable.php', - '/libraries/fof/form/header/filterselectable.php', - '/libraries/fof/form/header/filtersql.php', - '/libraries/fof/form/header/language.php', - '/libraries/fof/form/header/model.php', - '/libraries/fof/form/header/ordering.php', - '/libraries/fof/form/header/published.php', - '/libraries/fof/form/header/rowselect.php', - '/libraries/fof/form/helper.php', - '/libraries/fof/hal/document.php', - '/libraries/fof/hal/link.php', - '/libraries/fof/hal/links.php', - '/libraries/fof/hal/render/interface.php', - '/libraries/fof/hal/render/json.php', - '/libraries/fof/include.php', - '/libraries/fof/inflector/inflector.php', - '/libraries/fof/input/input.php', - '/libraries/fof/input/jinput/cli.php', - '/libraries/fof/input/jinput/cookie.php', - '/libraries/fof/input/jinput/files.php', - '/libraries/fof/input/jinput/input.php', - '/libraries/fof/input/jinput/json.php', - '/libraries/fof/integration/joomla/filesystem/filesystem.php', - '/libraries/fof/integration/joomla/platform.php', - '/libraries/fof/layout/file.php', - '/libraries/fof/layout/helper.php', - '/libraries/fof/less/formatter/classic.php', - '/libraries/fof/less/formatter/compressed.php', - '/libraries/fof/less/formatter/joomla.php', - '/libraries/fof/less/formatter/lessjs.php', - '/libraries/fof/less/less.php', - '/libraries/fof/less/parser/parser.php', - '/libraries/fof/model/behavior.php', - '/libraries/fof/model/behavior/access.php', - '/libraries/fof/model/behavior/emptynonzero.php', - '/libraries/fof/model/behavior/enabled.php', - '/libraries/fof/model/behavior/filters.php', - '/libraries/fof/model/behavior/language.php', - '/libraries/fof/model/behavior/private.php', - '/libraries/fof/model/dispatcher/behavior.php', - '/libraries/fof/model/field.php', - '/libraries/fof/model/field/boolean.php', - '/libraries/fof/model/field/date.php', - '/libraries/fof/model/field/number.php', - '/libraries/fof/model/field/text.php', - '/libraries/fof/model/model.php', - '/libraries/fof/platform/filesystem/filesystem.php', - '/libraries/fof/platform/filesystem/interface.php', - '/libraries/fof/platform/interface.php', - '/libraries/fof/platform/platform.php', - '/libraries/fof/query/abstract.php', - '/libraries/fof/render/abstract.php', - '/libraries/fof/render/joomla.php', - '/libraries/fof/render/joomla3.php', - '/libraries/fof/render/strapper.php', - '/libraries/fof/string/utils.php', - '/libraries/fof/table/behavior.php', - '/libraries/fof/table/behavior/assets.php', - '/libraries/fof/table/behavior/contenthistory.php', - '/libraries/fof/table/behavior/tags.php', - '/libraries/fof/table/dispatcher/behavior.php', - '/libraries/fof/table/nested.php', - '/libraries/fof/table/relations.php', - '/libraries/fof/table/table.php', - '/libraries/fof/template/utils.php', - '/libraries/fof/toolbar/toolbar.php', - '/libraries/fof/utils/array/array.php', - '/libraries/fof/utils/cache/cleaner.php', - '/libraries/fof/utils/config/helper.php', - '/libraries/fof/utils/filescheck/filescheck.php', - '/libraries/fof/utils/ini/parser.php', - '/libraries/fof/utils/installscript/installscript.php', - '/libraries/fof/utils/ip/ip.php', - '/libraries/fof/utils/object/object.php', - '/libraries/fof/utils/observable/dispatcher.php', - '/libraries/fof/utils/observable/event.php', - '/libraries/fof/utils/phpfunc/phpfunc.php', - '/libraries/fof/utils/timer/timer.php', - '/libraries/fof/utils/update/collection.php', - '/libraries/fof/utils/update/extension.php', - '/libraries/fof/utils/update/joomla.php', - '/libraries/fof/utils/update/update.php', - '/libraries/fof/version.txt', - '/libraries/fof/view/csv.php', - '/libraries/fof/view/form.php', - '/libraries/fof/view/html.php', - '/libraries/fof/view/json.php', - '/libraries/fof/view/raw.php', - '/libraries/fof/view/view.php', - '/libraries/idna_convert/LICENCE', - '/libraries/idna_convert/ReadMe.txt', - '/libraries/idna_convert/idna_convert.class.php', - '/libraries/idna_convert/transcode_wrapper.php', - '/libraries/idna_convert/uctc.php', - '/libraries/joomla/application/web/router.php', - '/libraries/joomla/application/web/router/base.php', - '/libraries/joomla/application/web/router/rest.php', - '/libraries/joomla/archive/archive.php', - '/libraries/joomla/archive/bzip2.php', - '/libraries/joomla/archive/extractable.php', - '/libraries/joomla/archive/gzip.php', - '/libraries/joomla/archive/tar.php', - '/libraries/joomla/archive/wrapper/archive.php', - '/libraries/joomla/archive/zip.php', - '/libraries/joomla/controller/base.php', - '/libraries/joomla/controller/controller.php', - '/libraries/joomla/database/database.php', - '/libraries/joomla/database/driver.php', - '/libraries/joomla/database/driver/mysql.php', - '/libraries/joomla/database/driver/mysqli.php', - '/libraries/joomla/database/driver/oracle.php', - '/libraries/joomla/database/driver/pdo.php', - '/libraries/joomla/database/driver/pdomysql.php', - '/libraries/joomla/database/driver/pgsql.php', - '/libraries/joomla/database/driver/postgresql.php', - '/libraries/joomla/database/driver/sqlazure.php', - '/libraries/joomla/database/driver/sqlite.php', - '/libraries/joomla/database/driver/sqlsrv.php', - '/libraries/joomla/database/exception/connecting.php', - '/libraries/joomla/database/exception/executing.php', - '/libraries/joomla/database/exception/unsupported.php', - '/libraries/joomla/database/exporter.php', - '/libraries/joomla/database/exporter/mysql.php', - '/libraries/joomla/database/exporter/mysqli.php', - '/libraries/joomla/database/exporter/pdomysql.php', - '/libraries/joomla/database/exporter/pgsql.php', - '/libraries/joomla/database/exporter/postgresql.php', - '/libraries/joomla/database/factory.php', - '/libraries/joomla/database/importer.php', - '/libraries/joomla/database/importer/mysql.php', - '/libraries/joomla/database/importer/mysqli.php', - '/libraries/joomla/database/importer/pdomysql.php', - '/libraries/joomla/database/importer/pgsql.php', - '/libraries/joomla/database/importer/postgresql.php', - '/libraries/joomla/database/interface.php', - '/libraries/joomla/database/iterator.php', - '/libraries/joomla/database/iterator/mysql.php', - '/libraries/joomla/database/iterator/mysqli.php', - '/libraries/joomla/database/iterator/oracle.php', - '/libraries/joomla/database/iterator/pdo.php', - '/libraries/joomla/database/iterator/pdomysql.php', - '/libraries/joomla/database/iterator/pgsql.php', - '/libraries/joomla/database/iterator/postgresql.php', - '/libraries/joomla/database/iterator/sqlazure.php', - '/libraries/joomla/database/iterator/sqlite.php', - '/libraries/joomla/database/iterator/sqlsrv.php', - '/libraries/joomla/database/query.php', - '/libraries/joomla/database/query/element.php', - '/libraries/joomla/database/query/limitable.php', - '/libraries/joomla/database/query/mysql.php', - '/libraries/joomla/database/query/mysqli.php', - '/libraries/joomla/database/query/oracle.php', - '/libraries/joomla/database/query/pdo.php', - '/libraries/joomla/database/query/pdomysql.php', - '/libraries/joomla/database/query/pgsql.php', - '/libraries/joomla/database/query/postgresql.php', - '/libraries/joomla/database/query/preparable.php', - '/libraries/joomla/database/query/sqlazure.php', - '/libraries/joomla/database/query/sqlite.php', - '/libraries/joomla/database/query/sqlsrv.php', - '/libraries/joomla/event/dispatcher.php', - '/libraries/joomla/event/event.php', - '/libraries/joomla/facebook/album.php', - '/libraries/joomla/facebook/checkin.php', - '/libraries/joomla/facebook/comment.php', - '/libraries/joomla/facebook/event.php', - '/libraries/joomla/facebook/facebook.php', - '/libraries/joomla/facebook/group.php', - '/libraries/joomla/facebook/link.php', - '/libraries/joomla/facebook/note.php', - '/libraries/joomla/facebook/oauth.php', - '/libraries/joomla/facebook/object.php', - '/libraries/joomla/facebook/photo.php', - '/libraries/joomla/facebook/post.php', - '/libraries/joomla/facebook/status.php', - '/libraries/joomla/facebook/user.php', - '/libraries/joomla/facebook/video.php', - '/libraries/joomla/form/fields/accesslevel.php', - '/libraries/joomla/form/fields/aliastag.php', - '/libraries/joomla/form/fields/cachehandler.php', - '/libraries/joomla/form/fields/calendar.php', - '/libraries/joomla/form/fields/checkbox.php', - '/libraries/joomla/form/fields/checkboxes.php', - '/libraries/joomla/form/fields/color.php', - '/libraries/joomla/form/fields/combo.php', - '/libraries/joomla/form/fields/components.php', - '/libraries/joomla/form/fields/databaseconnection.php', - '/libraries/joomla/form/fields/email.php', - '/libraries/joomla/form/fields/file.php', - '/libraries/joomla/form/fields/filelist.php', - '/libraries/joomla/form/fields/folderlist.php', - '/libraries/joomla/form/fields/groupedlist.php', - '/libraries/joomla/form/fields/hidden.php', - '/libraries/joomla/form/fields/imagelist.php', - '/libraries/joomla/form/fields/integer.php', - '/libraries/joomla/form/fields/language.php', - '/libraries/joomla/form/fields/list.php', - '/libraries/joomla/form/fields/meter.php', - '/libraries/joomla/form/fields/note.php', - '/libraries/joomla/form/fields/number.php', - '/libraries/joomla/form/fields/password.php', - '/libraries/joomla/form/fields/plugins.php', - '/libraries/joomla/form/fields/predefinedlist.php', - '/libraries/joomla/form/fields/radio.php', - '/libraries/joomla/form/fields/range.php', - '/libraries/joomla/form/fields/repeatable.php', - '/libraries/joomla/form/fields/rules.php', - '/libraries/joomla/form/fields/sessionhandler.php', - '/libraries/joomla/form/fields/spacer.php', - '/libraries/joomla/form/fields/sql.php', - '/libraries/joomla/form/fields/subform.php', - '/libraries/joomla/form/fields/tel.php', - '/libraries/joomla/form/fields/text.php', - '/libraries/joomla/form/fields/textarea.php', - '/libraries/joomla/form/fields/timezone.php', - '/libraries/joomla/form/fields/url.php', - '/libraries/joomla/form/fields/usergroup.php', - '/libraries/joomla/github/account.php', - '/libraries/joomla/github/commits.php', - '/libraries/joomla/github/forks.php', - '/libraries/joomla/github/github.php', - '/libraries/joomla/github/hooks.php', - '/libraries/joomla/github/http.php', - '/libraries/joomla/github/meta.php', - '/libraries/joomla/github/milestones.php', - '/libraries/joomla/github/object.php', - '/libraries/joomla/github/package.php', - '/libraries/joomla/github/package/activity.php', - '/libraries/joomla/github/package/activity/events.php', - '/libraries/joomla/github/package/activity/notifications.php', - '/libraries/joomla/github/package/activity/starring.php', - '/libraries/joomla/github/package/activity/watching.php', - '/libraries/joomla/github/package/authorization.php', - '/libraries/joomla/github/package/data.php', - '/libraries/joomla/github/package/data/blobs.php', - '/libraries/joomla/github/package/data/commits.php', - '/libraries/joomla/github/package/data/refs.php', - '/libraries/joomla/github/package/data/tags.php', - '/libraries/joomla/github/package/data/trees.php', - '/libraries/joomla/github/package/gists.php', - '/libraries/joomla/github/package/gists/comments.php', - '/libraries/joomla/github/package/gitignore.php', - '/libraries/joomla/github/package/issues.php', - '/libraries/joomla/github/package/issues/assignees.php', - '/libraries/joomla/github/package/issues/comments.php', - '/libraries/joomla/github/package/issues/events.php', - '/libraries/joomla/github/package/issues/labels.php', - '/libraries/joomla/github/package/issues/milestones.php', - '/libraries/joomla/github/package/markdown.php', - '/libraries/joomla/github/package/orgs.php', - '/libraries/joomla/github/package/orgs/members.php', - '/libraries/joomla/github/package/orgs/teams.php', - '/libraries/joomla/github/package/pulls.php', - '/libraries/joomla/github/package/pulls/comments.php', - '/libraries/joomla/github/package/repositories.php', - '/libraries/joomla/github/package/repositories/collaborators.php', - '/libraries/joomla/github/package/repositories/comments.php', - '/libraries/joomla/github/package/repositories/commits.php', - '/libraries/joomla/github/package/repositories/contents.php', - '/libraries/joomla/github/package/repositories/downloads.php', - '/libraries/joomla/github/package/repositories/forks.php', - '/libraries/joomla/github/package/repositories/hooks.php', - '/libraries/joomla/github/package/repositories/keys.php', - '/libraries/joomla/github/package/repositories/merging.php', - '/libraries/joomla/github/package/repositories/statistics.php', - '/libraries/joomla/github/package/repositories/statuses.php', - '/libraries/joomla/github/package/search.php', - '/libraries/joomla/github/package/users.php', - '/libraries/joomla/github/package/users/emails.php', - '/libraries/joomla/github/package/users/followers.php', - '/libraries/joomla/github/package/users/keys.php', - '/libraries/joomla/github/refs.php', - '/libraries/joomla/github/statuses.php', - '/libraries/joomla/google/auth.php', - '/libraries/joomla/google/auth/oauth2.php', - '/libraries/joomla/google/data.php', - '/libraries/joomla/google/data/adsense.php', - '/libraries/joomla/google/data/calendar.php', - '/libraries/joomla/google/data/picasa.php', - '/libraries/joomla/google/data/picasa/album.php', - '/libraries/joomla/google/data/picasa/photo.php', - '/libraries/joomla/google/data/plus.php', - '/libraries/joomla/google/data/plus/activities.php', - '/libraries/joomla/google/data/plus/comments.php', - '/libraries/joomla/google/data/plus/people.php', - '/libraries/joomla/google/embed.php', - '/libraries/joomla/google/embed/analytics.php', - '/libraries/joomla/google/embed/maps.php', - '/libraries/joomla/google/google.php', - '/libraries/joomla/grid/grid.php', - '/libraries/joomla/keychain/keychain.php', - '/libraries/joomla/linkedin/communications.php', - '/libraries/joomla/linkedin/companies.php', - '/libraries/joomla/linkedin/groups.php', - '/libraries/joomla/linkedin/jobs.php', - '/libraries/joomla/linkedin/linkedin.php', - '/libraries/joomla/linkedin/oauth.php', - '/libraries/joomla/linkedin/object.php', - '/libraries/joomla/linkedin/people.php', - '/libraries/joomla/linkedin/stream.php', - '/libraries/joomla/mediawiki/categories.php', - '/libraries/joomla/mediawiki/http.php', - '/libraries/joomla/mediawiki/images.php', - '/libraries/joomla/mediawiki/links.php', - '/libraries/joomla/mediawiki/mediawiki.php', - '/libraries/joomla/mediawiki/object.php', - '/libraries/joomla/mediawiki/pages.php', - '/libraries/joomla/mediawiki/search.php', - '/libraries/joomla/mediawiki/sites.php', - '/libraries/joomla/mediawiki/users.php', - '/libraries/joomla/model/base.php', - '/libraries/joomla/model/database.php', - '/libraries/joomla/model/model.php', - '/libraries/joomla/oauth1/client.php', - '/libraries/joomla/oauth2/client.php', - '/libraries/joomla/observable/interface.php', - '/libraries/joomla/observer/interface.php', - '/libraries/joomla/observer/mapper.php', - '/libraries/joomla/observer/updater.php', - '/libraries/joomla/observer/updater/interface.php', - '/libraries/joomla/observer/wrapper/mapper.php', - '/libraries/joomla/openstreetmap/changesets.php', - '/libraries/joomla/openstreetmap/elements.php', - '/libraries/joomla/openstreetmap/gps.php', - '/libraries/joomla/openstreetmap/info.php', - '/libraries/joomla/openstreetmap/oauth.php', - '/libraries/joomla/openstreetmap/object.php', - '/libraries/joomla/openstreetmap/openstreetmap.php', - '/libraries/joomla/openstreetmap/user.php', - '/libraries/joomla/platform.php', - '/libraries/joomla/route/wrapper/route.php', - '/libraries/joomla/session/handler/interface.php', - '/libraries/joomla/session/handler/joomla.php', - '/libraries/joomla/session/handler/native.php', - '/libraries/joomla/session/storage.php', - '/libraries/joomla/session/storage/apc.php', - '/libraries/joomla/session/storage/apcu.php', - '/libraries/joomla/session/storage/database.php', - '/libraries/joomla/session/storage/memcache.php', - '/libraries/joomla/session/storage/memcached.php', - '/libraries/joomla/session/storage/none.php', - '/libraries/joomla/session/storage/redis.php', - '/libraries/joomla/session/storage/wincache.php', - '/libraries/joomla/session/storage/xcache.php', - '/libraries/joomla/string/string.php', - '/libraries/joomla/string/wrapper/normalise.php', - '/libraries/joomla/string/wrapper/punycode.php', - '/libraries/joomla/twitter/block.php', - '/libraries/joomla/twitter/directmessages.php', - '/libraries/joomla/twitter/favorites.php', - '/libraries/joomla/twitter/friends.php', - '/libraries/joomla/twitter/help.php', - '/libraries/joomla/twitter/lists.php', - '/libraries/joomla/twitter/oauth.php', - '/libraries/joomla/twitter/object.php', - '/libraries/joomla/twitter/places.php', - '/libraries/joomla/twitter/profile.php', - '/libraries/joomla/twitter/search.php', - '/libraries/joomla/twitter/statuses.php', - '/libraries/joomla/twitter/trends.php', - '/libraries/joomla/twitter/twitter.php', - '/libraries/joomla/twitter/users.php', - '/libraries/joomla/utilities/arrayhelper.php', - '/libraries/joomla/view/base.php', - '/libraries/joomla/view/html.php', - '/libraries/joomla/view/view.php', - '/libraries/legacy/application/application.php', - '/libraries/legacy/base/node.php', - '/libraries/legacy/base/observable.php', - '/libraries/legacy/base/observer.php', - '/libraries/legacy/base/tree.php', - '/libraries/legacy/database/exception.php', - '/libraries/legacy/database/mysql.php', - '/libraries/legacy/database/mysqli.php', - '/libraries/legacy/database/sqlazure.php', - '/libraries/legacy/database/sqlsrv.php', - '/libraries/legacy/dispatcher/dispatcher.php', - '/libraries/legacy/error/error.php', - '/libraries/legacy/exception/exception.php', - '/libraries/legacy/form/field/category.php', - '/libraries/legacy/form/field/componentlayout.php', - '/libraries/legacy/form/field/modulelayout.php', - '/libraries/legacy/log/logexception.php', - '/libraries/legacy/request/request.php', - '/libraries/legacy/response/response.php', - '/libraries/legacy/simplecrypt/simplecrypt.php', - '/libraries/legacy/simplepie/factory.php', - '/libraries/legacy/table/session.php', - '/libraries/legacy/utilities/xmlelement.php', - '/libraries/phputf8/LICENSE', - '/libraries/phputf8/README', - '/libraries/phputf8/mbstring/core.php', - '/libraries/phputf8/native/core.php', - '/libraries/phputf8/ord.php', - '/libraries/phputf8/str_ireplace.php', - '/libraries/phputf8/str_pad.php', - '/libraries/phputf8/str_split.php', - '/libraries/phputf8/strcasecmp.php', - '/libraries/phputf8/strcspn.php', - '/libraries/phputf8/stristr.php', - '/libraries/phputf8/strrev.php', - '/libraries/phputf8/strspn.php', - '/libraries/phputf8/substr_replace.php', - '/libraries/phputf8/trim.php', - '/libraries/phputf8/ucfirst.php', - '/libraries/phputf8/ucwords.php', - '/libraries/phputf8/utf8.php', - '/libraries/phputf8/utils/ascii.php', - '/libraries/phputf8/utils/bad.php', - '/libraries/phputf8/utils/patterns.php', - '/libraries/phputf8/utils/position.php', - '/libraries/phputf8/utils/specials.php', - '/libraries/phputf8/utils/unicode.php', - '/libraries/phputf8/utils/validation.php', - '/libraries/src/Access/Wrapper/Access.php', - '/libraries/src/Cache/Storage/ApcStorage.php', - '/libraries/src/Cache/Storage/CacheliteStorage.php', - '/libraries/src/Cache/Storage/MemcacheStorage.php', - '/libraries/src/Cache/Storage/XcacheStorage.php', - '/libraries/src/Client/ClientWrapper.php', - '/libraries/src/Crypt/Cipher/BlowfishCipher.php', - '/libraries/src/Crypt/Cipher/McryptCipher.php', - '/libraries/src/Crypt/Cipher/Rijndael256Cipher.php', - '/libraries/src/Crypt/Cipher/SimpleCipher.php', - '/libraries/src/Crypt/Cipher/TripleDesCipher.php', - '/libraries/src/Crypt/CipherInterface.php', - '/libraries/src/Crypt/CryptPassword.php', - '/libraries/src/Crypt/Key.php', - '/libraries/src/Crypt/Password/SimpleCryptPassword.php', - '/libraries/src/Crypt/README.md', - '/libraries/src/Filesystem/Wrapper/FileWrapper.php', - '/libraries/src/Filesystem/Wrapper/FolderWrapper.php', - '/libraries/src/Filesystem/Wrapper/PathWrapper.php', - '/libraries/src/Filter/Wrapper/OutputFilterWrapper.php', - '/libraries/src/Form/Field/HelpsiteField.php', - '/libraries/src/Form/FormWrapper.php', - '/libraries/src/Helper/ContentHistoryHelper.php', - '/libraries/src/Helper/SearchHelper.php', - '/libraries/src/Http/Transport/cacert.pem', - '/libraries/src/Http/Wrapper/FactoryWrapper.php', - '/libraries/src/Language/LanguageStemmer.php', - '/libraries/src/Language/Stemmer/Porteren.php', - '/libraries/src/Language/Wrapper/JTextWrapper.php', - '/libraries/src/Language/Wrapper/LanguageHelperWrapper.php', - '/libraries/src/Language/Wrapper/TransliterateWrapper.php', - '/libraries/src/Mail/MailWrapper.php', - '/libraries/src/Menu/MenuHelper.php', - '/libraries/src/Menu/Node.php', - '/libraries/src/Menu/Node/Component.php', - '/libraries/src/Menu/Node/Container.php', - '/libraries/src/Menu/Node/Heading.php', - '/libraries/src/Menu/Node/Separator.php', - '/libraries/src/Menu/Node/Url.php', - '/libraries/src/Menu/Tree.php', - '/libraries/src/Table/Observer/AbstractObserver.php', - '/libraries/src/Table/Observer/ContentHistory.php', - '/libraries/src/Table/Observer/Tags.php', - '/libraries/src/Toolbar/Button/SliderButton.php', - '/libraries/src/User/UserWrapper.php', - '/libraries/vendor/.htaccess', - '/libraries/vendor/brumann/polyfill-unserialize/LICENSE', - '/libraries/vendor/brumann/polyfill-unserialize/composer.json', - '/libraries/vendor/brumann/polyfill-unserialize/src/DisallowedClassesSubstitutor.php', - '/libraries/vendor/brumann/polyfill-unserialize/src/Unserialize.php', - '/libraries/vendor/ircmaxell/password-compat/LICENSE.md', - '/libraries/vendor/ircmaxell/password-compat/lib/password.php', - '/libraries/vendor/joomla/application/src/AbstractCliApplication.php', - '/libraries/vendor/joomla/application/src/AbstractDaemonApplication.php', - '/libraries/vendor/joomla/application/src/Cli/CliInput.php', - '/libraries/vendor/joomla/application/src/Cli/CliOutput.php', - '/libraries/vendor/joomla/application/src/Cli/ColorProcessor.php', - '/libraries/vendor/joomla/application/src/Cli/ColorStyle.php', - '/libraries/vendor/joomla/application/src/Cli/Output/Processor/ColorProcessor.php', - '/libraries/vendor/joomla/application/src/Cli/Output/Processor/ProcessorInterface.php', - '/libraries/vendor/joomla/application/src/Cli/Output/Stdout.php', - '/libraries/vendor/joomla/application/src/Cli/Output/Xml.php', - '/libraries/vendor/joomla/compat/LICENSE', - '/libraries/vendor/joomla/compat/src/CallbackFilterIterator.php', - '/libraries/vendor/joomla/compat/src/JsonSerializable.php', - '/libraries/vendor/joomla/event/src/DelegatingDispatcher.php', - '/libraries/vendor/joomla/filesystem/src/Stream/String.php', - '/libraries/vendor/joomla/image/LICENSE', - '/libraries/vendor/joomla/image/src/Filter/Backgroundfill.php', - '/libraries/vendor/joomla/image/src/Filter/Brightness.php', - '/libraries/vendor/joomla/image/src/Filter/Contrast.php', - '/libraries/vendor/joomla/image/src/Filter/Edgedetect.php', - '/libraries/vendor/joomla/image/src/Filter/Emboss.php', - '/libraries/vendor/joomla/image/src/Filter/Grayscale.php', - '/libraries/vendor/joomla/image/src/Filter/Negate.php', - '/libraries/vendor/joomla/image/src/Filter/Sketchy.php', - '/libraries/vendor/joomla/image/src/Filter/Smooth.php', - '/libraries/vendor/joomla/image/src/Image.php', - '/libraries/vendor/joomla/image/src/ImageFilter.php', - '/libraries/vendor/joomla/input/src/Cli.php', - '/libraries/vendor/joomla/registry/src/AbstractRegistryFormat.php', - '/libraries/vendor/joomla/session/Joomla/Session/LICENSE', - '/libraries/vendor/joomla/session/Joomla/Session/Session.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Apc.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Apcu.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Database.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Memcache.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Memcached.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/None.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Wincache.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Xcache.php', - '/libraries/vendor/joomla/string/src/String.php', - '/libraries/vendor/leafo/lessphp/LICENSE', - '/libraries/vendor/leafo/lessphp/lessc.inc.php', - '/libraries/vendor/leafo/lessphp/lessify', - '/libraries/vendor/leafo/lessphp/lessify.inc.php', - '/libraries/vendor/leafo/lessphp/plessc', - '/libraries/vendor/paragonie/random_compat/LICENSE', - '/libraries/vendor/paragonie/random_compat/lib/byte_safe_strings.php', - '/libraries/vendor/paragonie/random_compat/lib/cast_to_int.php', - '/libraries/vendor/paragonie/random_compat/lib/error_polyfill.php', - '/libraries/vendor/paragonie/random_compat/lib/random.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_com_dotnet.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_dev_urandom.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_libsodium.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_libsodium_legacy.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_mcrypt.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_openssl.php', - '/libraries/vendor/paragonie/random_compat/lib/random_int.php', - '/libraries/vendor/paragonie/sodium_compat/src/Core32/Curve25519/README.md', - '/libraries/vendor/phpmailer/phpmailer/PHPMailerAutoload.php', - '/libraries/vendor/phpmailer/phpmailer/class.phpmailer.php', - '/libraries/vendor/phpmailer/phpmailer/class.phpmaileroauth.php', - '/libraries/vendor/phpmailer/phpmailer/class.phpmaileroauthgoogle.php', - '/libraries/vendor/phpmailer/phpmailer/class.pop3.php', - '/libraries/vendor/phpmailer/phpmailer/class.smtp.php', - '/libraries/vendor/phpmailer/phpmailer/extras/EasyPeasyICS.php', - '/libraries/vendor/phpmailer/phpmailer/extras/htmlfilter.php', - '/libraries/vendor/phpmailer/phpmailer/extras/ntlm_sasl_client.php', - '/libraries/vendor/simplepie/simplepie/LICENSE.txt', - '/libraries/vendor/simplepie/simplepie/autoloader.php', - '/libraries/vendor/simplepie/simplepie/db.sql', - '/libraries/vendor/simplepie/simplepie/idn/LICENCE', - '/libraries/vendor/simplepie/simplepie/idn/idna_convert.class.php', - '/libraries/vendor/simplepie/simplepie/idn/npdata.ser', - '/libraries/vendor/simplepie/simplepie/library/SimplePie.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Author.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/Base.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/DB.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/File.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/Memcache.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/MySQL.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Caption.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Category.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content/Type/Sniffer.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Copyright.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Core.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Credit.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode/HTML/Entities.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Enclosure.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Exception.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/File.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/HTTP/Parser.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/IRI.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Item.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Locator.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Misc.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Net/IPv6.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parse/Date.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parser.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Rating.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Registry.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Restriction.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Sanitize.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Source.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML/Declaration/Parser.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/gzdecode.php', - '/libraries/vendor/symfony/polyfill-php55/LICENSE', - '/libraries/vendor/symfony/polyfill-php55/Php55.php', - '/libraries/vendor/symfony/polyfill-php55/Php55ArrayColumn.php', - '/libraries/vendor/symfony/polyfill-php55/bootstrap.php', - '/libraries/vendor/symfony/polyfill-php56/LICENSE', - '/libraries/vendor/symfony/polyfill-php56/Php56.php', - '/libraries/vendor/symfony/polyfill-php56/bootstrap.php', - '/libraries/vendor/symfony/polyfill-php71/LICENSE', - '/libraries/vendor/symfony/polyfill-php71/Php71.php', - '/libraries/vendor/symfony/polyfill-php71/bootstrap.php', - '/libraries/vendor/symfony/polyfill-util/Binary.php', - '/libraries/vendor/symfony/polyfill-util/BinaryNoFuncOverload.php', - '/libraries/vendor/symfony/polyfill-util/BinaryOnFuncOverload.php', - '/libraries/vendor/symfony/polyfill-util/LICENSE', - '/libraries/vendor/typo3/phar-stream-wrapper/composer.json', - '/libraries/vendor/web.config', - '/media/cms/css/debug.css', - '/media/com_associations/js/sidebyside-uncompressed.js', - '/media/com_contenthistory/css/jquery.pretty-text-diff.css', - '/media/com_contenthistory/js/diff_match_patch.js', - '/media/com_contenthistory/js/jquery.pretty-text-diff.js', - '/media/com_contenthistory/js/jquery.pretty-text-diff.min.js', - '/media/com_finder/js/autocompleter.js', - '/media/com_joomlaupdate/js/encryption.js', - '/media/com_joomlaupdate/js/encryption.min.js', - '/media/com_joomlaupdate/js/json2.js', - '/media/com_joomlaupdate/js/json2.min.js', - '/media/com_joomlaupdate/js/update.js', - '/media/com_joomlaupdate/js/update.min.js', - '/media/contacts/images/con_address.png', - '/media/contacts/images/con_fax.png', - '/media/contacts/images/con_info.png', - '/media/contacts/images/con_mobile.png', - '/media/contacts/images/con_tel.png', - '/media/contacts/images/emailButton.png', - '/media/editors/codemirror/LICENSE', - '/media/editors/codemirror/addon/comment/comment.js', - '/media/editors/codemirror/addon/comment/comment.min.js', - '/media/editors/codemirror/addon/comment/continuecomment.js', - '/media/editors/codemirror/addon/comment/continuecomment.min.js', - '/media/editors/codemirror/addon/dialog/dialog.css', - '/media/editors/codemirror/addon/dialog/dialog.js', - '/media/editors/codemirror/addon/dialog/dialog.min.css', - '/media/editors/codemirror/addon/dialog/dialog.min.js', - '/media/editors/codemirror/addon/display/autorefresh.js', - '/media/editors/codemirror/addon/display/autorefresh.min.js', - '/media/editors/codemirror/addon/display/fullscreen.css', - '/media/editors/codemirror/addon/display/fullscreen.js', - '/media/editors/codemirror/addon/display/fullscreen.min.css', - '/media/editors/codemirror/addon/display/fullscreen.min.js', - '/media/editors/codemirror/addon/display/panel.js', - '/media/editors/codemirror/addon/display/panel.min.js', - '/media/editors/codemirror/addon/display/placeholder.js', - '/media/editors/codemirror/addon/display/placeholder.min.js', - '/media/editors/codemirror/addon/display/rulers.js', - '/media/editors/codemirror/addon/display/rulers.min.js', - '/media/editors/codemirror/addon/edit/closebrackets.js', - '/media/editors/codemirror/addon/edit/closebrackets.min.js', - '/media/editors/codemirror/addon/edit/closetag.js', - '/media/editors/codemirror/addon/edit/closetag.min.js', - '/media/editors/codemirror/addon/edit/continuelist.js', - '/media/editors/codemirror/addon/edit/continuelist.min.js', - '/media/editors/codemirror/addon/edit/matchbrackets.js', - '/media/editors/codemirror/addon/edit/matchbrackets.min.js', - '/media/editors/codemirror/addon/edit/matchtags.js', - '/media/editors/codemirror/addon/edit/matchtags.min.js', - '/media/editors/codemirror/addon/edit/trailingspace.js', - '/media/editors/codemirror/addon/edit/trailingspace.min.js', - '/media/editors/codemirror/addon/fold/brace-fold.js', - '/media/editors/codemirror/addon/fold/brace-fold.min.js', - '/media/editors/codemirror/addon/fold/comment-fold.js', - '/media/editors/codemirror/addon/fold/comment-fold.min.js', - '/media/editors/codemirror/addon/fold/foldcode.js', - '/media/editors/codemirror/addon/fold/foldcode.min.js', - '/media/editors/codemirror/addon/fold/foldgutter.css', - '/media/editors/codemirror/addon/fold/foldgutter.js', - '/media/editors/codemirror/addon/fold/foldgutter.min.css', - '/media/editors/codemirror/addon/fold/foldgutter.min.js', - '/media/editors/codemirror/addon/fold/indent-fold.js', - '/media/editors/codemirror/addon/fold/indent-fold.min.js', - '/media/editors/codemirror/addon/fold/markdown-fold.js', - '/media/editors/codemirror/addon/fold/markdown-fold.min.js', - '/media/editors/codemirror/addon/fold/xml-fold.js', - '/media/editors/codemirror/addon/fold/xml-fold.min.js', - '/media/editors/codemirror/addon/hint/anyword-hint.js', - '/media/editors/codemirror/addon/hint/anyword-hint.min.js', - '/media/editors/codemirror/addon/hint/css-hint.js', - '/media/editors/codemirror/addon/hint/css-hint.min.js', - '/media/editors/codemirror/addon/hint/html-hint.js', - '/media/editors/codemirror/addon/hint/html-hint.min.js', - '/media/editors/codemirror/addon/hint/javascript-hint.js', - '/media/editors/codemirror/addon/hint/javascript-hint.min.js', - '/media/editors/codemirror/addon/hint/show-hint.css', - '/media/editors/codemirror/addon/hint/show-hint.js', - '/media/editors/codemirror/addon/hint/show-hint.min.css', - '/media/editors/codemirror/addon/hint/show-hint.min.js', - '/media/editors/codemirror/addon/hint/sql-hint.js', - '/media/editors/codemirror/addon/hint/sql-hint.min.js', - '/media/editors/codemirror/addon/hint/xml-hint.js', - '/media/editors/codemirror/addon/hint/xml-hint.min.js', - '/media/editors/codemirror/addon/lint/coffeescript-lint.js', - '/media/editors/codemirror/addon/lint/coffeescript-lint.min.js', - '/media/editors/codemirror/addon/lint/css-lint.js', - '/media/editors/codemirror/addon/lint/css-lint.min.js', - '/media/editors/codemirror/addon/lint/html-lint.js', - '/media/editors/codemirror/addon/lint/html-lint.min.js', - '/media/editors/codemirror/addon/lint/javascript-lint.js', - '/media/editors/codemirror/addon/lint/javascript-lint.min.js', - '/media/editors/codemirror/addon/lint/json-lint.js', - '/media/editors/codemirror/addon/lint/json-lint.min.js', - '/media/editors/codemirror/addon/lint/lint.css', - '/media/editors/codemirror/addon/lint/lint.js', - '/media/editors/codemirror/addon/lint/lint.min.css', - '/media/editors/codemirror/addon/lint/lint.min.js', - '/media/editors/codemirror/addon/lint/yaml-lint.js', - '/media/editors/codemirror/addon/lint/yaml-lint.min.js', - '/media/editors/codemirror/addon/merge/merge.css', - '/media/editors/codemirror/addon/merge/merge.js', - '/media/editors/codemirror/addon/merge/merge.min.css', - '/media/editors/codemirror/addon/merge/merge.min.js', - '/media/editors/codemirror/addon/mode/loadmode.js', - '/media/editors/codemirror/addon/mode/loadmode.min.js', - '/media/editors/codemirror/addon/mode/multiplex.js', - '/media/editors/codemirror/addon/mode/multiplex.min.js', - '/media/editors/codemirror/addon/mode/multiplex_test.js', - '/media/editors/codemirror/addon/mode/multiplex_test.min.js', - '/media/editors/codemirror/addon/mode/overlay.js', - '/media/editors/codemirror/addon/mode/overlay.min.js', - '/media/editors/codemirror/addon/mode/simple.js', - '/media/editors/codemirror/addon/mode/simple.min.js', - '/media/editors/codemirror/addon/runmode/colorize.js', - '/media/editors/codemirror/addon/runmode/colorize.min.js', - '/media/editors/codemirror/addon/runmode/runmode-standalone.js', - '/media/editors/codemirror/addon/runmode/runmode-standalone.min.js', - '/media/editors/codemirror/addon/runmode/runmode.js', - '/media/editors/codemirror/addon/runmode/runmode.min.js', - '/media/editors/codemirror/addon/runmode/runmode.node.js', - '/media/editors/codemirror/addon/scroll/annotatescrollbar.js', - '/media/editors/codemirror/addon/scroll/annotatescrollbar.min.js', - '/media/editors/codemirror/addon/scroll/scrollpastend.js', - '/media/editors/codemirror/addon/scroll/scrollpastend.min.js', - '/media/editors/codemirror/addon/scroll/simplescrollbars.css', - '/media/editors/codemirror/addon/scroll/simplescrollbars.js', - '/media/editors/codemirror/addon/scroll/simplescrollbars.min.css', - '/media/editors/codemirror/addon/scroll/simplescrollbars.min.js', - '/media/editors/codemirror/addon/search/jump-to-line.js', - '/media/editors/codemirror/addon/search/jump-to-line.min.js', - '/media/editors/codemirror/addon/search/match-highlighter.js', - '/media/editors/codemirror/addon/search/match-highlighter.min.js', - '/media/editors/codemirror/addon/search/matchesonscrollbar.css', - '/media/editors/codemirror/addon/search/matchesonscrollbar.js', - '/media/editors/codemirror/addon/search/matchesonscrollbar.min.css', - '/media/editors/codemirror/addon/search/matchesonscrollbar.min.js', - '/media/editors/codemirror/addon/search/search.js', - '/media/editors/codemirror/addon/search/search.min.js', - '/media/editors/codemirror/addon/search/searchcursor.js', - '/media/editors/codemirror/addon/search/searchcursor.min.js', - '/media/editors/codemirror/addon/selection/active-line.js', - '/media/editors/codemirror/addon/selection/active-line.min.js', - '/media/editors/codemirror/addon/selection/mark-selection.js', - '/media/editors/codemirror/addon/selection/mark-selection.min.js', - '/media/editors/codemirror/addon/selection/selection-pointer.js', - '/media/editors/codemirror/addon/selection/selection-pointer.min.js', - '/media/editors/codemirror/addon/tern/tern.css', - '/media/editors/codemirror/addon/tern/tern.js', - '/media/editors/codemirror/addon/tern/tern.min.css', - '/media/editors/codemirror/addon/tern/tern.min.js', - '/media/editors/codemirror/addon/tern/worker.js', - '/media/editors/codemirror/addon/tern/worker.min.js', - '/media/editors/codemirror/addon/wrap/hardwrap.js', - '/media/editors/codemirror/addon/wrap/hardwrap.min.js', - '/media/editors/codemirror/keymap/emacs.js', - '/media/editors/codemirror/keymap/emacs.min.js', - '/media/editors/codemirror/keymap/sublime.js', - '/media/editors/codemirror/keymap/sublime.min.js', - '/media/editors/codemirror/keymap/vim.js', - '/media/editors/codemirror/keymap/vim.min.js', - '/media/editors/codemirror/lib/addons.css', - '/media/editors/codemirror/lib/addons.js', - '/media/editors/codemirror/lib/addons.min.css', - '/media/editors/codemirror/lib/addons.min.js', - '/media/editors/codemirror/lib/codemirror.css', - '/media/editors/codemirror/lib/codemirror.js', - '/media/editors/codemirror/lib/codemirror.min.css', - '/media/editors/codemirror/lib/codemirror.min.js', - '/media/editors/codemirror/mode/apl/apl.js', - '/media/editors/codemirror/mode/apl/apl.min.js', - '/media/editors/codemirror/mode/asciiarmor/asciiarmor.js', - '/media/editors/codemirror/mode/asciiarmor/asciiarmor.min.js', - '/media/editors/codemirror/mode/asn.1/asn.1.js', - '/media/editors/codemirror/mode/asn.1/asn.min.js', - '/media/editors/codemirror/mode/asterisk/asterisk.js', - '/media/editors/codemirror/mode/asterisk/asterisk.min.js', - '/media/editors/codemirror/mode/brainfuck/brainfuck.js', - '/media/editors/codemirror/mode/brainfuck/brainfuck.min.js', - '/media/editors/codemirror/mode/clike/clike.js', - '/media/editors/codemirror/mode/clike/clike.min.js', - '/media/editors/codemirror/mode/clojure/clojure.js', - '/media/editors/codemirror/mode/clojure/clojure.min.js', - '/media/editors/codemirror/mode/cmake/cmake.js', - '/media/editors/codemirror/mode/cmake/cmake.min.js', - '/media/editors/codemirror/mode/cobol/cobol.js', - '/media/editors/codemirror/mode/cobol/cobol.min.js', - '/media/editors/codemirror/mode/coffeescript/coffeescript.js', - '/media/editors/codemirror/mode/coffeescript/coffeescript.min.js', - '/media/editors/codemirror/mode/commonlisp/commonlisp.js', - '/media/editors/codemirror/mode/commonlisp/commonlisp.min.js', - '/media/editors/codemirror/mode/crystal/crystal.js', - '/media/editors/codemirror/mode/crystal/crystal.min.js', - '/media/editors/codemirror/mode/css/css.js', - '/media/editors/codemirror/mode/css/css.min.js', - '/media/editors/codemirror/mode/cypher/cypher.js', - '/media/editors/codemirror/mode/cypher/cypher.min.js', - '/media/editors/codemirror/mode/d/d.js', - '/media/editors/codemirror/mode/d/d.min.js', - '/media/editors/codemirror/mode/dart/dart.js', - '/media/editors/codemirror/mode/dart/dart.min.js', - '/media/editors/codemirror/mode/diff/diff.js', - '/media/editors/codemirror/mode/diff/diff.min.js', - '/media/editors/codemirror/mode/django/django.js', - '/media/editors/codemirror/mode/django/django.min.js', - '/media/editors/codemirror/mode/dockerfile/dockerfile.js', - '/media/editors/codemirror/mode/dockerfile/dockerfile.min.js', - '/media/editors/codemirror/mode/dtd/dtd.js', - '/media/editors/codemirror/mode/dtd/dtd.min.js', - '/media/editors/codemirror/mode/dylan/dylan.js', - '/media/editors/codemirror/mode/dylan/dylan.min.js', - '/media/editors/codemirror/mode/ebnf/ebnf.js', - '/media/editors/codemirror/mode/ebnf/ebnf.min.js', - '/media/editors/codemirror/mode/ecl/ecl.js', - '/media/editors/codemirror/mode/ecl/ecl.min.js', - '/media/editors/codemirror/mode/eiffel/eiffel.js', - '/media/editors/codemirror/mode/eiffel/eiffel.min.js', - '/media/editors/codemirror/mode/elm/elm.js', - '/media/editors/codemirror/mode/elm/elm.min.js', - '/media/editors/codemirror/mode/erlang/erlang.js', - '/media/editors/codemirror/mode/erlang/erlang.min.js', - '/media/editors/codemirror/mode/factor/factor.js', - '/media/editors/codemirror/mode/factor/factor.min.js', - '/media/editors/codemirror/mode/fcl/fcl.js', - '/media/editors/codemirror/mode/fcl/fcl.min.js', - '/media/editors/codemirror/mode/forth/forth.js', - '/media/editors/codemirror/mode/forth/forth.min.js', - '/media/editors/codemirror/mode/fortran/fortran.js', - '/media/editors/codemirror/mode/fortran/fortran.min.js', - '/media/editors/codemirror/mode/gas/gas.js', - '/media/editors/codemirror/mode/gas/gas.min.js', - '/media/editors/codemirror/mode/gfm/gfm.js', - '/media/editors/codemirror/mode/gfm/gfm.min.js', - '/media/editors/codemirror/mode/gherkin/gherkin.js', - '/media/editors/codemirror/mode/gherkin/gherkin.min.js', - '/media/editors/codemirror/mode/go/go.js', - '/media/editors/codemirror/mode/go/go.min.js', - '/media/editors/codemirror/mode/groovy/groovy.js', - '/media/editors/codemirror/mode/groovy/groovy.min.js', - '/media/editors/codemirror/mode/haml/haml.js', - '/media/editors/codemirror/mode/haml/haml.min.js', - '/media/editors/codemirror/mode/handlebars/handlebars.js', - '/media/editors/codemirror/mode/handlebars/handlebars.min.js', - '/media/editors/codemirror/mode/haskell-literate/haskell-literate.js', - '/media/editors/codemirror/mode/haskell-literate/haskell-literate.min.js', - '/media/editors/codemirror/mode/haskell/haskell.js', - '/media/editors/codemirror/mode/haskell/haskell.min.js', - '/media/editors/codemirror/mode/haxe/haxe.js', - '/media/editors/codemirror/mode/haxe/haxe.min.js', - '/media/editors/codemirror/mode/htmlembedded/htmlembedded.js', - '/media/editors/codemirror/mode/htmlembedded/htmlembedded.min.js', - '/media/editors/codemirror/mode/htmlmixed/htmlmixed.js', - '/media/editors/codemirror/mode/htmlmixed/htmlmixed.min.js', - '/media/editors/codemirror/mode/http/http.js', - '/media/editors/codemirror/mode/http/http.min.js', - '/media/editors/codemirror/mode/idl/idl.js', - '/media/editors/codemirror/mode/idl/idl.min.js', - '/media/editors/codemirror/mode/javascript/javascript.js', - '/media/editors/codemirror/mode/javascript/javascript.min.js', - '/media/editors/codemirror/mode/jinja2/jinja2.js', - '/media/editors/codemirror/mode/jinja2/jinja2.min.js', - '/media/editors/codemirror/mode/jsx/jsx.js', - '/media/editors/codemirror/mode/jsx/jsx.min.js', - '/media/editors/codemirror/mode/julia/julia.js', - '/media/editors/codemirror/mode/julia/julia.min.js', - '/media/editors/codemirror/mode/livescript/livescript.js', - '/media/editors/codemirror/mode/livescript/livescript.min.js', - '/media/editors/codemirror/mode/lua/lua.js', - '/media/editors/codemirror/mode/lua/lua.min.js', - '/media/editors/codemirror/mode/markdown/markdown.js', - '/media/editors/codemirror/mode/markdown/markdown.min.js', - '/media/editors/codemirror/mode/mathematica/mathematica.js', - '/media/editors/codemirror/mode/mathematica/mathematica.min.js', - '/media/editors/codemirror/mode/mbox/mbox.js', - '/media/editors/codemirror/mode/mbox/mbox.min.js', - '/media/editors/codemirror/mode/meta.js', - '/media/editors/codemirror/mode/meta.min.js', - '/media/editors/codemirror/mode/mirc/mirc.js', - '/media/editors/codemirror/mode/mirc/mirc.min.js', - '/media/editors/codemirror/mode/mllike/mllike.js', - '/media/editors/codemirror/mode/mllike/mllike.min.js', - '/media/editors/codemirror/mode/modelica/modelica.js', - '/media/editors/codemirror/mode/modelica/modelica.min.js', - '/media/editors/codemirror/mode/mscgen/mscgen.js', - '/media/editors/codemirror/mode/mscgen/mscgen.min.js', - '/media/editors/codemirror/mode/mumps/mumps.js', - '/media/editors/codemirror/mode/mumps/mumps.min.js', - '/media/editors/codemirror/mode/nginx/nginx.js', - '/media/editors/codemirror/mode/nginx/nginx.min.js', - '/media/editors/codemirror/mode/nsis/nsis.js', - '/media/editors/codemirror/mode/nsis/nsis.min.js', - '/media/editors/codemirror/mode/ntriples/ntriples.js', - '/media/editors/codemirror/mode/ntriples/ntriples.min.js', - '/media/editors/codemirror/mode/octave/octave.js', - '/media/editors/codemirror/mode/octave/octave.min.js', - '/media/editors/codemirror/mode/oz/oz.js', - '/media/editors/codemirror/mode/oz/oz.min.js', - '/media/editors/codemirror/mode/pascal/pascal.js', - '/media/editors/codemirror/mode/pascal/pascal.min.js', - '/media/editors/codemirror/mode/pegjs/pegjs.js', - '/media/editors/codemirror/mode/pegjs/pegjs.min.js', - '/media/editors/codemirror/mode/perl/perl.js', - '/media/editors/codemirror/mode/perl/perl.min.js', - '/media/editors/codemirror/mode/php/php.js', - '/media/editors/codemirror/mode/php/php.min.js', - '/media/editors/codemirror/mode/pig/pig.js', - '/media/editors/codemirror/mode/pig/pig.min.js', - '/media/editors/codemirror/mode/powershell/powershell.js', - '/media/editors/codemirror/mode/powershell/powershell.min.js', - '/media/editors/codemirror/mode/properties/properties.js', - '/media/editors/codemirror/mode/properties/properties.min.js', - '/media/editors/codemirror/mode/protobuf/protobuf.js', - '/media/editors/codemirror/mode/protobuf/protobuf.min.js', - '/media/editors/codemirror/mode/pug/pug.js', - '/media/editors/codemirror/mode/pug/pug.min.js', - '/media/editors/codemirror/mode/puppet/puppet.js', - '/media/editors/codemirror/mode/puppet/puppet.min.js', - '/media/editors/codemirror/mode/python/python.js', - '/media/editors/codemirror/mode/python/python.min.js', - '/media/editors/codemirror/mode/q/q.js', - '/media/editors/codemirror/mode/q/q.min.js', - '/media/editors/codemirror/mode/r/r.js', - '/media/editors/codemirror/mode/r/r.min.js', - '/media/editors/codemirror/mode/rpm/changes/index.html', - '/media/editors/codemirror/mode/rpm/rpm.js', - '/media/editors/codemirror/mode/rpm/rpm.min.js', - '/media/editors/codemirror/mode/rst/rst.js', - '/media/editors/codemirror/mode/rst/rst.min.js', - '/media/editors/codemirror/mode/ruby/ruby.js', - '/media/editors/codemirror/mode/ruby/ruby.min.js', - '/media/editors/codemirror/mode/rust/rust.js', - '/media/editors/codemirror/mode/rust/rust.min.js', - '/media/editors/codemirror/mode/sas/sas.js', - '/media/editors/codemirror/mode/sas/sas.min.js', - '/media/editors/codemirror/mode/sass/sass.js', - '/media/editors/codemirror/mode/sass/sass.min.js', - '/media/editors/codemirror/mode/scheme/scheme.js', - '/media/editors/codemirror/mode/scheme/scheme.min.js', - '/media/editors/codemirror/mode/shell/shell.js', - '/media/editors/codemirror/mode/shell/shell.min.js', - '/media/editors/codemirror/mode/sieve/sieve.js', - '/media/editors/codemirror/mode/sieve/sieve.min.js', - '/media/editors/codemirror/mode/slim/slim.js', - '/media/editors/codemirror/mode/slim/slim.min.js', - '/media/editors/codemirror/mode/smalltalk/smalltalk.js', - '/media/editors/codemirror/mode/smalltalk/smalltalk.min.js', - '/media/editors/codemirror/mode/smarty/smarty.js', - '/media/editors/codemirror/mode/smarty/smarty.min.js', - '/media/editors/codemirror/mode/solr/solr.js', - '/media/editors/codemirror/mode/solr/solr.min.js', - '/media/editors/codemirror/mode/soy/soy.js', - '/media/editors/codemirror/mode/soy/soy.min.js', - '/media/editors/codemirror/mode/sparql/sparql.js', - '/media/editors/codemirror/mode/sparql/sparql.min.js', - '/media/editors/codemirror/mode/spreadsheet/spreadsheet.js', - '/media/editors/codemirror/mode/spreadsheet/spreadsheet.min.js', - '/media/editors/codemirror/mode/sql/sql.js', - '/media/editors/codemirror/mode/sql/sql.min.js', - '/media/editors/codemirror/mode/stex/stex.js', - '/media/editors/codemirror/mode/stex/stex.min.js', - '/media/editors/codemirror/mode/stylus/stylus.js', - '/media/editors/codemirror/mode/stylus/stylus.min.js', - '/media/editors/codemirror/mode/swift/swift.js', - '/media/editors/codemirror/mode/swift/swift.min.js', - '/media/editors/codemirror/mode/tcl/tcl.js', - '/media/editors/codemirror/mode/tcl/tcl.min.js', - '/media/editors/codemirror/mode/textile/textile.js', - '/media/editors/codemirror/mode/textile/textile.min.js', - '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.css', - '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.js', - '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.min.css', - '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.min.js', - '/media/editors/codemirror/mode/tiki/tiki.css', - '/media/editors/codemirror/mode/tiki/tiki.js', - '/media/editors/codemirror/mode/tiki/tiki.min.css', - '/media/editors/codemirror/mode/tiki/tiki.min.js', - '/media/editors/codemirror/mode/toml/toml.js', - '/media/editors/codemirror/mode/toml/toml.min.js', - '/media/editors/codemirror/mode/tornado/tornado.js', - '/media/editors/codemirror/mode/tornado/tornado.min.js', - '/media/editors/codemirror/mode/troff/troff.js', - '/media/editors/codemirror/mode/troff/troff.min.js', - '/media/editors/codemirror/mode/ttcn-cfg/ttcn-cfg.js', - '/media/editors/codemirror/mode/ttcn-cfg/ttcn-cfg.min.js', - '/media/editors/codemirror/mode/ttcn/ttcn.js', - '/media/editors/codemirror/mode/ttcn/ttcn.min.js', - '/media/editors/codemirror/mode/turtle/turtle.js', - '/media/editors/codemirror/mode/turtle/turtle.min.js', - '/media/editors/codemirror/mode/twig/twig.js', - '/media/editors/codemirror/mode/twig/twig.min.js', - '/media/editors/codemirror/mode/vb/vb.js', - '/media/editors/codemirror/mode/vb/vb.min.js', - '/media/editors/codemirror/mode/vbscript/vbscript.js', - '/media/editors/codemirror/mode/vbscript/vbscript.min.js', - '/media/editors/codemirror/mode/velocity/velocity.js', - '/media/editors/codemirror/mode/velocity/velocity.min.js', - '/media/editors/codemirror/mode/verilog/verilog.js', - '/media/editors/codemirror/mode/verilog/verilog.min.js', - '/media/editors/codemirror/mode/vhdl/vhdl.js', - '/media/editors/codemirror/mode/vhdl/vhdl.min.js', - '/media/editors/codemirror/mode/vue/vue.js', - '/media/editors/codemirror/mode/vue/vue.min.js', - '/media/editors/codemirror/mode/wast/wast.js', - '/media/editors/codemirror/mode/wast/wast.min.js', - '/media/editors/codemirror/mode/webidl/webidl.js', - '/media/editors/codemirror/mode/webidl/webidl.min.js', - '/media/editors/codemirror/mode/xml/xml.js', - '/media/editors/codemirror/mode/xml/xml.min.js', - '/media/editors/codemirror/mode/xquery/xquery.js', - '/media/editors/codemirror/mode/xquery/xquery.min.js', - '/media/editors/codemirror/mode/yacas/yacas.js', - '/media/editors/codemirror/mode/yacas/yacas.min.js', - '/media/editors/codemirror/mode/yaml-frontmatter/yaml-frontmatter.js', - '/media/editors/codemirror/mode/yaml-frontmatter/yaml-frontmatter.min.js', - '/media/editors/codemirror/mode/yaml/yaml.js', - '/media/editors/codemirror/mode/yaml/yaml.min.js', - '/media/editors/codemirror/mode/z80/z80.js', - '/media/editors/codemirror/mode/z80/z80.min.js', - '/media/editors/codemirror/theme/3024-day.css', - '/media/editors/codemirror/theme/3024-night.css', - '/media/editors/codemirror/theme/abcdef.css', - '/media/editors/codemirror/theme/ambiance-mobile.css', - '/media/editors/codemirror/theme/ambiance.css', - '/media/editors/codemirror/theme/ayu-dark.css', - '/media/editors/codemirror/theme/ayu-mirage.css', - '/media/editors/codemirror/theme/base16-dark.css', - '/media/editors/codemirror/theme/base16-light.css', - '/media/editors/codemirror/theme/bespin.css', - '/media/editors/codemirror/theme/blackboard.css', - '/media/editors/codemirror/theme/cobalt.css', - '/media/editors/codemirror/theme/colorforth.css', - '/media/editors/codemirror/theme/darcula.css', - '/media/editors/codemirror/theme/dracula.css', - '/media/editors/codemirror/theme/duotone-dark.css', - '/media/editors/codemirror/theme/duotone-light.css', - '/media/editors/codemirror/theme/eclipse.css', - '/media/editors/codemirror/theme/elegant.css', - '/media/editors/codemirror/theme/erlang-dark.css', - '/media/editors/codemirror/theme/gruvbox-dark.css', - '/media/editors/codemirror/theme/hopscotch.css', - '/media/editors/codemirror/theme/icecoder.css', - '/media/editors/codemirror/theme/idea.css', - '/media/editors/codemirror/theme/isotope.css', - '/media/editors/codemirror/theme/lesser-dark.css', - '/media/editors/codemirror/theme/liquibyte.css', - '/media/editors/codemirror/theme/lucario.css', - '/media/editors/codemirror/theme/material-darker.css', - '/media/editors/codemirror/theme/material-ocean.css', - '/media/editors/codemirror/theme/material-palenight.css', - '/media/editors/codemirror/theme/material.css', - '/media/editors/codemirror/theme/mbo.css', - '/media/editors/codemirror/theme/mdn-like.css', - '/media/editors/codemirror/theme/midnight.css', - '/media/editors/codemirror/theme/monokai.css', - '/media/editors/codemirror/theme/moxer.css', - '/media/editors/codemirror/theme/neat.css', - '/media/editors/codemirror/theme/neo.css', - '/media/editors/codemirror/theme/night.css', - '/media/editors/codemirror/theme/nord.css', - '/media/editors/codemirror/theme/oceanic-next.css', - '/media/editors/codemirror/theme/panda-syntax.css', - '/media/editors/codemirror/theme/paraiso-dark.css', - '/media/editors/codemirror/theme/paraiso-light.css', - '/media/editors/codemirror/theme/pastel-on-dark.css', - '/media/editors/codemirror/theme/railscasts.css', - '/media/editors/codemirror/theme/rubyblue.css', - '/media/editors/codemirror/theme/seti.css', - '/media/editors/codemirror/theme/shadowfox.css', - '/media/editors/codemirror/theme/solarized.css', - '/media/editors/codemirror/theme/ssms.css', - '/media/editors/codemirror/theme/the-matrix.css', - '/media/editors/codemirror/theme/tomorrow-night-bright.css', - '/media/editors/codemirror/theme/tomorrow-night-eighties.css', - '/media/editors/codemirror/theme/ttcn.css', - '/media/editors/codemirror/theme/twilight.css', - '/media/editors/codemirror/theme/vibrant-ink.css', - '/media/editors/codemirror/theme/xq-dark.css', - '/media/editors/codemirror/theme/xq-light.css', - '/media/editors/codemirror/theme/yeti.css', - '/media/editors/codemirror/theme/yonce.css', - '/media/editors/codemirror/theme/zenburn.css', - '/media/editors/none/js/none.js', - '/media/editors/none/js/none.min.js', - '/media/editors/tinymce/changelog.txt', - '/media/editors/tinymce/js/plugins/dragdrop/plugin.js', - '/media/editors/tinymce/js/plugins/dragdrop/plugin.min.js', - '/media/editors/tinymce/js/tiny-close.js', - '/media/editors/tinymce/js/tiny-close.min.js', - '/media/editors/tinymce/js/tinymce-builder.js', - '/media/editors/tinymce/js/tinymce.js', - '/media/editors/tinymce/js/tinymce.min.js', - '/media/editors/tinymce/langs/af.js', - '/media/editors/tinymce/langs/ar.js', - '/media/editors/tinymce/langs/be.js', - '/media/editors/tinymce/langs/bg.js', - '/media/editors/tinymce/langs/bs.js', - '/media/editors/tinymce/langs/ca.js', - '/media/editors/tinymce/langs/cs.js', - '/media/editors/tinymce/langs/cy.js', - '/media/editors/tinymce/langs/da.js', - '/media/editors/tinymce/langs/de.js', - '/media/editors/tinymce/langs/el.js', - '/media/editors/tinymce/langs/es.js', - '/media/editors/tinymce/langs/et.js', - '/media/editors/tinymce/langs/eu.js', - '/media/editors/tinymce/langs/fa.js', - '/media/editors/tinymce/langs/fi.js', - '/media/editors/tinymce/langs/fo.js', - '/media/editors/tinymce/langs/fr.js', - '/media/editors/tinymce/langs/ga.js', - '/media/editors/tinymce/langs/gl.js', - '/media/editors/tinymce/langs/he.js', - '/media/editors/tinymce/langs/hr.js', - '/media/editors/tinymce/langs/hu.js', - '/media/editors/tinymce/langs/id.js', - '/media/editors/tinymce/langs/it.js', - '/media/editors/tinymce/langs/ja.js', - '/media/editors/tinymce/langs/ka.js', - '/media/editors/tinymce/langs/kk.js', - '/media/editors/tinymce/langs/km.js', - '/media/editors/tinymce/langs/ko.js', - '/media/editors/tinymce/langs/lb.js', - '/media/editors/tinymce/langs/lt.js', - '/media/editors/tinymce/langs/lv.js', - '/media/editors/tinymce/langs/mk.js', - '/media/editors/tinymce/langs/ms.js', - '/media/editors/tinymce/langs/nb.js', - '/media/editors/tinymce/langs/nl.js', - '/media/editors/tinymce/langs/pl.js', - '/media/editors/tinymce/langs/pt-BR.js', - '/media/editors/tinymce/langs/pt-PT.js', - '/media/editors/tinymce/langs/readme.md', - '/media/editors/tinymce/langs/ro.js', - '/media/editors/tinymce/langs/ru.js', - '/media/editors/tinymce/langs/si-LK.js', - '/media/editors/tinymce/langs/sk.js', - '/media/editors/tinymce/langs/sl.js', - '/media/editors/tinymce/langs/sr.js', - '/media/editors/tinymce/langs/sv.js', - '/media/editors/tinymce/langs/sw.js', - '/media/editors/tinymce/langs/sy.js', - '/media/editors/tinymce/langs/ta.js', - '/media/editors/tinymce/langs/th.js', - '/media/editors/tinymce/langs/tr.js', - '/media/editors/tinymce/langs/ug.js', - '/media/editors/tinymce/langs/uk.js', - '/media/editors/tinymce/langs/vi.js', - '/media/editors/tinymce/langs/zh-CN.js', - '/media/editors/tinymce/langs/zh-TW.js', - '/media/editors/tinymce/license.txt', - '/media/editors/tinymce/plugins/advlist/plugin.min.js', - '/media/editors/tinymce/plugins/anchor/plugin.min.js', - '/media/editors/tinymce/plugins/autolink/plugin.min.js', - '/media/editors/tinymce/plugins/autoresize/plugin.min.js', - '/media/editors/tinymce/plugins/autosave/plugin.min.js', - '/media/editors/tinymce/plugins/bbcode/plugin.min.js', - '/media/editors/tinymce/plugins/charmap/plugin.min.js', - '/media/editors/tinymce/plugins/code/plugin.min.js', - '/media/editors/tinymce/plugins/codesample/css/prism.css', - '/media/editors/tinymce/plugins/codesample/plugin.min.js', - '/media/editors/tinymce/plugins/colorpicker/plugin.min.js', - '/media/editors/tinymce/plugins/contextmenu/plugin.min.js', - '/media/editors/tinymce/plugins/directionality/plugin.min.js', - '/media/editors/tinymce/plugins/emoticons/img/smiley-cool.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-cry.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-embarassed.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-foot-in-mouth.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-frown.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-innocent.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-kiss.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-laughing.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-money-mouth.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-sealed.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-smile.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-surprised.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-tongue-out.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-undecided.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-wink.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-yell.gif', - '/media/editors/tinymce/plugins/emoticons/plugin.min.js', - '/media/editors/tinymce/plugins/example/dialog.html', - '/media/editors/tinymce/plugins/example/plugin.min.js', - '/media/editors/tinymce/plugins/example_dependency/plugin.min.js', - '/media/editors/tinymce/plugins/fullpage/plugin.min.js', - '/media/editors/tinymce/plugins/fullscreen/plugin.min.js', - '/media/editors/tinymce/plugins/hr/plugin.min.js', - '/media/editors/tinymce/plugins/image/plugin.min.js', - '/media/editors/tinymce/plugins/imagetools/plugin.min.js', - '/media/editors/tinymce/plugins/importcss/plugin.min.js', - '/media/editors/tinymce/plugins/insertdatetime/plugin.min.js', - '/media/editors/tinymce/plugins/layer/plugin.min.js', - '/media/editors/tinymce/plugins/legacyoutput/plugin.min.js', - '/media/editors/tinymce/plugins/link/plugin.min.js', - '/media/editors/tinymce/plugins/lists/plugin.min.js', - '/media/editors/tinymce/plugins/media/plugin.min.js', - '/media/editors/tinymce/plugins/nonbreaking/plugin.min.js', - '/media/editors/tinymce/plugins/noneditable/plugin.min.js', - '/media/editors/tinymce/plugins/pagebreak/plugin.min.js', - '/media/editors/tinymce/plugins/paste/plugin.min.js', - '/media/editors/tinymce/plugins/preview/plugin.min.js', - '/media/editors/tinymce/plugins/print/plugin.min.js', - '/media/editors/tinymce/plugins/save/plugin.min.js', - '/media/editors/tinymce/plugins/searchreplace/plugin.min.js', - '/media/editors/tinymce/plugins/spellchecker/plugin.min.js', - '/media/editors/tinymce/plugins/tabfocus/plugin.min.js', - '/media/editors/tinymce/plugins/table/plugin.min.js', - '/media/editors/tinymce/plugins/template/plugin.min.js', - '/media/editors/tinymce/plugins/textcolor/plugin.min.js', - '/media/editors/tinymce/plugins/textpattern/plugin.min.js', - '/media/editors/tinymce/plugins/toc/plugin.min.js', - '/media/editors/tinymce/plugins/visualblocks/css/visualblocks.css', - '/media/editors/tinymce/plugins/visualblocks/plugin.min.js', - '/media/editors/tinymce/plugins/visualchars/plugin.min.js', - '/media/editors/tinymce/plugins/wordcount/plugin.min.js', - '/media/editors/tinymce/skins/lightgray/content.inline.min.css', - '/media/editors/tinymce/skins/lightgray/content.min.css', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.eot', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.svg', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.ttf', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.woff', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce.eot', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce.svg', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce.ttf', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce.woff', - '/media/editors/tinymce/skins/lightgray/img/anchor.gif', - '/media/editors/tinymce/skins/lightgray/img/loader.gif', - '/media/editors/tinymce/skins/lightgray/img/object.gif', - '/media/editors/tinymce/skins/lightgray/img/trans.gif', - '/media/editors/tinymce/skins/lightgray/skin.ie7.min.css', - '/media/editors/tinymce/skins/lightgray/skin.min.css', - '/media/editors/tinymce/templates/layout1.html', - '/media/editors/tinymce/templates/snippet1.html', - '/media/editors/tinymce/themes/modern/theme.min.js', - '/media/editors/tinymce/tinymce.min.js', - '/media/jui/css/bootstrap-extended.css', - '/media/jui/css/bootstrap-responsive.css', - '/media/jui/css/bootstrap-responsive.min.css', - '/media/jui/css/bootstrap-rtl.css', - '/media/jui/css/bootstrap-tooltip-extended.css', - '/media/jui/css/bootstrap.css', - '/media/jui/css/bootstrap.min.css', - '/media/jui/css/chosen-sprite.png', - '/media/jui/css/chosen-sprite@2x.png', - '/media/jui/css/chosen.css', - '/media/jui/css/icomoon.css', - '/media/jui/css/jquery.minicolors.css', - '/media/jui/css/jquery.searchtools.css', - '/media/jui/css/jquery.simplecolors.css', - '/media/jui/css/sortablelist.css', - '/media/jui/fonts/IcoMoon.dev.commented.svg', - '/media/jui/fonts/IcoMoon.dev.svg', - '/media/jui/fonts/IcoMoon.eot', - '/media/jui/fonts/IcoMoon.svg', - '/media/jui/fonts/IcoMoon.ttf', - '/media/jui/fonts/IcoMoon.woff', - '/media/jui/fonts/icomoon-license.txt', - '/media/jui/images/ajax-loader.gif', - '/media/jui/img/ajax-loader.gif', - '/media/jui/img/alpha.png', - '/media/jui/img/bg-overlay.png', - '/media/jui/img/glyphicons-halflings-white.png', - '/media/jui/img/glyphicons-halflings.png', - '/media/jui/img/hue.png', - '/media/jui/img/joomla.png', - '/media/jui/img/jquery.minicolors.png', - '/media/jui/img/saturation.png', - '/media/jui/js/ajax-chosen.js', - '/media/jui/js/ajax-chosen.min.js', - '/media/jui/js/bootstrap-tooltip-extended.js', - '/media/jui/js/bootstrap-tooltip-extended.min.js', - '/media/jui/js/bootstrap.js', - '/media/jui/js/bootstrap.min.js', - '/media/jui/js/chosen.jquery.js', - '/media/jui/js/chosen.jquery.min.js', - '/media/jui/js/cms-uncompressed.js', - '/media/jui/js/cms.js', - '/media/jui/js/fielduser.js', - '/media/jui/js/fielduser.min.js', - '/media/jui/js/html5-uncompressed.js', - '/media/jui/js/html5.js', - '/media/jui/js/icomoon-lte-ie7.js', - '/media/jui/js/jquery-migrate.js', - '/media/jui/js/jquery-migrate.min.js', - '/media/jui/js/jquery-noconflict.js', - '/media/jui/js/jquery.autocomplete.js', - '/media/jui/js/jquery.autocomplete.min.js', - '/media/jui/js/jquery.js', - '/media/jui/js/jquery.min.js', - '/media/jui/js/jquery.minicolors.js', - '/media/jui/js/jquery.minicolors.min.js', - '/media/jui/js/jquery.searchtools.js', - '/media/jui/js/jquery.searchtools.min.js', - '/media/jui/js/jquery.simplecolors.js', - '/media/jui/js/jquery.simplecolors.min.js', - '/media/jui/js/jquery.ui.core.js', - '/media/jui/js/jquery.ui.core.min.js', - '/media/jui/js/jquery.ui.sortable.js', - '/media/jui/js/jquery.ui.sortable.min.js', - '/media/jui/js/sortablelist.js', - '/media/jui/js/treeselectmenu.jquery.js', - '/media/jui/js/treeselectmenu.jquery.min.js', - '/media/jui/less/accordion.less', - '/media/jui/less/alerts.less', - '/media/jui/less/bootstrap-extended.less', - '/media/jui/less/bootstrap-rtl.less', - '/media/jui/less/bootstrap.less', - '/media/jui/less/breadcrumbs.less', - '/media/jui/less/button-groups.less', - '/media/jui/less/buttons.less', - '/media/jui/less/carousel.less', - '/media/jui/less/close.less', - '/media/jui/less/code.less', - '/media/jui/less/component-animations.less', - '/media/jui/less/dropdowns.less', - '/media/jui/less/forms.less', - '/media/jui/less/grid.less', - '/media/jui/less/hero-unit.less', - '/media/jui/less/icomoon.less', - '/media/jui/less/labels-badges.less', - '/media/jui/less/layouts.less', - '/media/jui/less/media.less', - '/media/jui/less/mixins.less', - '/media/jui/less/modals.joomla.less', - '/media/jui/less/modals.less', - '/media/jui/less/navbar.less', - '/media/jui/less/navs.less', - '/media/jui/less/pager.less', - '/media/jui/less/pagination.less', - '/media/jui/less/popovers.less', - '/media/jui/less/progress-bars.less', - '/media/jui/less/reset.less', - '/media/jui/less/responsive-1200px-min.less', - '/media/jui/less/responsive-767px-max.joomla.less', - '/media/jui/less/responsive-767px-max.less', - '/media/jui/less/responsive-768px-979px.less', - '/media/jui/less/responsive-navbar.less', - '/media/jui/less/responsive-utilities.less', - '/media/jui/less/responsive.less', - '/media/jui/less/scaffolding.less', - '/media/jui/less/sprites.less', - '/media/jui/less/tables.less', - '/media/jui/less/thumbnails.less', - '/media/jui/less/tooltip.less', - '/media/jui/less/type.less', - '/media/jui/less/utilities.less', - '/media/jui/less/variables.less', - '/media/jui/less/wells.less', - '/media/media/css/background.png', - '/media/media/css/bigplay.fw.png', - '/media/media/css/bigplay.png', - '/media/media/css/bigplay.svg', - '/media/media/css/controls-ted.png', - '/media/media/css/controls-wmp-bg.png', - '/media/media/css/controls-wmp.png', - '/media/media/css/controls.fw.png', - '/media/media/css/controls.png', - '/media/media/css/controls.svg', - '/media/media/css/jumpforward.png', - '/media/media/css/loading.gif', - '/media/media/css/mediaelementplayer.css', - '/media/media/css/mediaelementplayer.min.css', - '/media/media/css/medialist-details.css', - '/media/media/css/medialist-details_rtl.css', - '/media/media/css/medialist-thumbs.css', - '/media/media/css/medialist-thumbs_rtl.css', - '/media/media/css/mediamanager.css', - '/media/media/css/mediamanager_rtl.css', - '/media/media/css/mejs-skins.css', - '/media/media/css/popup-imagelist.css', - '/media/media/css/popup-imagelist_rtl.css', - '/media/media/css/popup-imagemanager.css', - '/media/media/css/popup-imagemanager_rtl.css', - '/media/media/css/skipback.png', - '/media/media/images/bar.gif', - '/media/media/images/con_info.png', - '/media/media/images/delete.png', - '/media/media/images/dots.gif', - '/media/media/images/failed.png', - '/media/media/images/folder.gif', - '/media/media/images/folder.png', - '/media/media/images/folder_sm.png', - '/media/media/images/folderup_16.png', - '/media/media/images/folderup_32.png', - '/media/media/images/mime-icon-16/avi.png', - '/media/media/images/mime-icon-16/doc.png', - '/media/media/images/mime-icon-16/mov.png', - '/media/media/images/mime-icon-16/mp3.png', - '/media/media/images/mime-icon-16/mp4.png', - '/media/media/images/mime-icon-16/odc.png', - '/media/media/images/mime-icon-16/odd.png', - '/media/media/images/mime-icon-16/odt.png', - '/media/media/images/mime-icon-16/ogg.png', - '/media/media/images/mime-icon-16/pdf.png', - '/media/media/images/mime-icon-16/ppt.png', - '/media/media/images/mime-icon-16/rar.png', - '/media/media/images/mime-icon-16/rtf.png', - '/media/media/images/mime-icon-16/svg.png', - '/media/media/images/mime-icon-16/sxd.png', - '/media/media/images/mime-icon-16/tar.png', - '/media/media/images/mime-icon-16/tgz.png', - '/media/media/images/mime-icon-16/wma.png', - '/media/media/images/mime-icon-16/wmv.png', - '/media/media/images/mime-icon-16/xls.png', - '/media/media/images/mime-icon-16/zip.png', - '/media/media/images/mime-icon-32/avi.png', - '/media/media/images/mime-icon-32/doc.png', - '/media/media/images/mime-icon-32/mov.png', - '/media/media/images/mime-icon-32/mp3.png', - '/media/media/images/mime-icon-32/mp4.png', - '/media/media/images/mime-icon-32/odc.png', - '/media/media/images/mime-icon-32/odd.png', - '/media/media/images/mime-icon-32/odt.png', - '/media/media/images/mime-icon-32/ogg.png', - '/media/media/images/mime-icon-32/pdf.png', - '/media/media/images/mime-icon-32/ppt.png', - '/media/media/images/mime-icon-32/rar.png', - '/media/media/images/mime-icon-32/rtf.png', - '/media/media/images/mime-icon-32/svg.png', - '/media/media/images/mime-icon-32/sxd.png', - '/media/media/images/mime-icon-32/tar.png', - '/media/media/images/mime-icon-32/tgz.png', - '/media/media/images/mime-icon-32/wma.png', - '/media/media/images/mime-icon-32/wmv.png', - '/media/media/images/mime-icon-32/xls.png', - '/media/media/images/mime-icon-32/zip.png', - '/media/media/images/progress.gif', - '/media/media/images/remove.png', - '/media/media/images/success.png', - '/media/media/images/upload.png', - '/media/media/images/uploading.png', - '/media/media/js/flashmediaelement-cdn.swf', - '/media/media/js/flashmediaelement.swf', - '/media/media/js/mediaelement-and-player.js', - '/media/media/js/mediaelement-and-player.min.js', - '/media/media/js/mediafield-mootools.js', - '/media/media/js/mediafield-mootools.min.js', - '/media/media/js/mediafield.js', - '/media/media/js/mediafield.min.js', - '/media/media/js/mediamanager.js', - '/media/media/js/mediamanager.min.js', - '/media/media/js/popup-imagemanager.js', - '/media/media/js/popup-imagemanager.min.js', - '/media/media/js/silverlightmediaelement.xap', - '/media/overrider/css/overrider.css', - '/media/overrider/js/overrider.js', - '/media/overrider/js/overrider.min.js', - '/media/plg_system_highlight/highlight.css', - '/media/plg_twofactorauth_totp/js/qrcode.js', - '/media/plg_twofactorauth_totp/js/qrcode.min.js', - '/media/plg_twofactorauth_totp/js/qrcode_SJIS.js', - '/media/plg_twofactorauth_totp/js/qrcode_UTF8.js', - '/media/system/css/adminlist.css', - '/media/system/css/jquery.Jcrop.min.css', - '/media/system/css/modal.css', - '/media/system/css/system.css', - '/media/system/js/associations-edit-uncompressed.js', - '/media/system/js/associations-edit.js', - '/media/system/js/calendar-setup-uncompressed.js', - '/media/system/js/calendar-setup.js', - '/media/system/js/calendar-uncompressed.js', - '/media/system/js/calendar.js', - '/media/system/js/caption-uncompressed.js', - '/media/system/js/caption.js', - '/media/system/js/color-field-adv-init.js', - '/media/system/js/color-field-adv-init.min.js', - '/media/system/js/color-field-init.js', - '/media/system/js/color-field-init.min.js', - '/media/system/js/combobox-uncompressed.js', - '/media/system/js/combobox.js', - '/media/system/js/core-uncompressed.js', - '/media/system/js/fields/calendar-locales/af.js', - '/media/system/js/fields/calendar-locales/ar.js', - '/media/system/js/fields/calendar-locales/bg.js', - '/media/system/js/fields/calendar-locales/bn.js', - '/media/system/js/fields/calendar-locales/bs.js', - '/media/system/js/fields/calendar-locales/ca.js', - '/media/system/js/fields/calendar-locales/cs.js', - '/media/system/js/fields/calendar-locales/cy.js', - '/media/system/js/fields/calendar-locales/da.js', - '/media/system/js/fields/calendar-locales/de.js', - '/media/system/js/fields/calendar-locales/el.js', - '/media/system/js/fields/calendar-locales/en.js', - '/media/system/js/fields/calendar-locales/es.js', - '/media/system/js/fields/calendar-locales/eu.js', - '/media/system/js/fields/calendar-locales/fa-ir.js', - '/media/system/js/fields/calendar-locales/fi.js', - '/media/system/js/fields/calendar-locales/fr.js', - '/media/system/js/fields/calendar-locales/ga.js', - '/media/system/js/fields/calendar-locales/hr.js', - '/media/system/js/fields/calendar-locales/hu.js', - '/media/system/js/fields/calendar-locales/it.js', - '/media/system/js/fields/calendar-locales/ja.js', - '/media/system/js/fields/calendar-locales/ka.js', - '/media/system/js/fields/calendar-locales/kk.js', - '/media/system/js/fields/calendar-locales/ko.js', - '/media/system/js/fields/calendar-locales/lt.js', - '/media/system/js/fields/calendar-locales/mk.js', - '/media/system/js/fields/calendar-locales/nb.js', - '/media/system/js/fields/calendar-locales/nl.js', - '/media/system/js/fields/calendar-locales/pl.js', - '/media/system/js/fields/calendar-locales/prs-af.js', - '/media/system/js/fields/calendar-locales/pt.js', - '/media/system/js/fields/calendar-locales/ru.js', - '/media/system/js/fields/calendar-locales/sk.js', - '/media/system/js/fields/calendar-locales/sl.js', - '/media/system/js/fields/calendar-locales/sr-rs.js', - '/media/system/js/fields/calendar-locales/sr-yu.js', - '/media/system/js/fields/calendar-locales/sv.js', - '/media/system/js/fields/calendar-locales/sw.js', - '/media/system/js/fields/calendar-locales/ta.js', - '/media/system/js/fields/calendar-locales/th.js', - '/media/system/js/fields/calendar-locales/uk.js', - '/media/system/js/fields/calendar-locales/zh-CN.js', - '/media/system/js/fields/calendar-locales/zh-TW.js', - '/media/system/js/frontediting-uncompressed.js', - '/media/system/js/frontediting.js', - '/media/system/js/helpsite.js', - '/media/system/js/highlighter-uncompressed.js', - '/media/system/js/highlighter.js', - '/media/system/js/html5fallback-uncompressed.js', - '/media/system/js/html5fallback.js', - '/media/system/js/jquery.Jcrop.js', - '/media/system/js/jquery.Jcrop.min.js', - '/media/system/js/keepalive-uncompressed.js', - '/media/system/js/modal-fields-uncompressed.js', - '/media/system/js/modal-fields.js', - '/media/system/js/modal-uncompressed.js', - '/media/system/js/modal.js', - '/media/system/js/moduleorder.js', - '/media/system/js/mootools-core-uncompressed.js', - '/media/system/js/mootools-core.js', - '/media/system/js/mootools-more-uncompressed.js', - '/media/system/js/mootools-more.js', - '/media/system/js/mootree-uncompressed.js', - '/media/system/js/mootree.js', - '/media/system/js/multiselect-uncompressed.js', - '/media/system/js/passwordstrength.js', - '/media/system/js/permissions-uncompressed.js', - '/media/system/js/permissions.js', - '/media/system/js/polyfill.classlist-uncompressed.js', - '/media/system/js/polyfill.classlist.js', - '/media/system/js/polyfill.event-uncompressed.js', - '/media/system/js/polyfill.event.js', - '/media/system/js/polyfill.filter-uncompressed.js', - '/media/system/js/polyfill.filter.js', - '/media/system/js/polyfill.map-uncompressed.js', - '/media/system/js/polyfill.map.js', - '/media/system/js/polyfill.xpath-uncompressed.js', - '/media/system/js/polyfill.xpath.js', - '/media/system/js/progressbar-uncompressed.js', - '/media/system/js/progressbar.js', - '/media/system/js/punycode-uncompressed.js', - '/media/system/js/punycode.js', - '/media/system/js/repeatable-uncompressed.js', - '/media/system/js/repeatable.js', - '/media/system/js/sendtestmail-uncompressed.js', - '/media/system/js/sendtestmail.js', - '/media/system/js/subform-repeatable-uncompressed.js', - '/media/system/js/subform-repeatable.js', - '/media/system/js/switcher-uncompressed.js', - '/media/system/js/switcher.js', - '/media/system/js/tabs-state-uncompressed.js', - '/media/system/js/tabs-state.js', - '/media/system/js/tabs.js', - '/media/system/js/validate-uncompressed.js', - '/media/system/js/validate.js', - '/modules/mod_articles_archive/helper.php', - '/modules/mod_articles_categories/helper.php', - '/modules/mod_articles_category/helper.php', - '/modules/mod_articles_latest/helper.php', - '/modules/mod_articles_news/helper.php', - '/modules/mod_articles_popular/helper.php', - '/modules/mod_banners/helper.php', - '/modules/mod_breadcrumbs/helper.php', - '/modules/mod_feed/helper.php', - '/modules/mod_finder/helper.php', - '/modules/mod_languages/helper.php', - '/modules/mod_login/helper.php', - '/modules/mod_menu/helper.php', - '/modules/mod_random_image/helper.php', - '/modules/mod_related_items/helper.php', - '/modules/mod_stats/helper.php', - '/modules/mod_syndicate/helper.php', - '/modules/mod_tags_popular/helper.php', - '/modules/mod_tags_similar/helper.php', - '/modules/mod_users_latest/helper.php', - '/modules/mod_whosonline/helper.php', - '/modules/mod_wrapper/helper.php', - '/plugins/authentication/gmail/gmail.php', - '/plugins/authentication/gmail/gmail.xml', - '/plugins/captcha/recaptcha/postinstall/actions.php', - '/plugins/content/confirmconsent/fields/consentbox.php', - '/plugins/editors/codemirror/fonts.php', - '/plugins/editors/codemirror/layouts/editors/codemirror/init.php', - '/plugins/editors/tinymce/field/skins.php', - '/plugins/editors/tinymce/field/tinymcebuilder.php', - '/plugins/editors/tinymce/field/uploaddirs.php', - '/plugins/editors/tinymce/form/setoptions.xml', - '/plugins/quickicon/joomlaupdate/joomlaupdate.php', - '/plugins/system/languagecode/language/en-GB/en-GB.plg_system_languagecode.ini', - '/plugins/system/languagecode/language/en-GB/en-GB.plg_system_languagecode.sys.ini', - '/plugins/system/p3p/p3p.php', - '/plugins/system/p3p/p3p.xml', - '/plugins/system/privacyconsent/field/privacy.php', - '/plugins/system/privacyconsent/privacyconsent/privacyconsent.xml', - '/plugins/system/stats/field/base.php', - '/plugins/system/stats/field/data.php', - '/plugins/system/stats/field/uniqueid.php', - '/plugins/user/profile/field/dob.php', - '/plugins/user/profile/field/tos.php', - '/plugins/user/profile/profiles/profile.xml', - '/plugins/user/terms/field/terms.php', - '/plugins/user/terms/terms/terms.xml', - '/templates/beez3/component.php', - '/templates/beez3/css/general.css', - '/templates/beez3/css/ie7only.css', - '/templates/beez3/css/ieonly.css', - '/templates/beez3/css/layout.css', - '/templates/beez3/css/nature.css', - '/templates/beez3/css/nature_rtl.css', - '/templates/beez3/css/personal.css', - '/templates/beez3/css/personal_rtl.css', - '/templates/beez3/css/position.css', - '/templates/beez3/css/print.css', - '/templates/beez3/css/red.css', - '/templates/beez3/css/template.css', - '/templates/beez3/css/template_rtl.css', - '/templates/beez3/css/turq.css', - '/templates/beez3/css/turq.less', - '/templates/beez3/error.php', - '/templates/beez3/favicon.ico', - '/templates/beez3/html/com_contact/categories/default.php', - '/templates/beez3/html/com_contact/categories/default_items.php', - '/templates/beez3/html/com_contact/category/default.php', - '/templates/beez3/html/com_contact/category/default_children.php', - '/templates/beez3/html/com_contact/category/default_items.php', - '/templates/beez3/html/com_contact/contact/default.php', - '/templates/beez3/html/com_contact/contact/default_address.php', - '/templates/beez3/html/com_contact/contact/default_articles.php', - '/templates/beez3/html/com_contact/contact/default_form.php', - '/templates/beez3/html/com_contact/contact/default_links.php', - '/templates/beez3/html/com_contact/contact/default_profile.php', - '/templates/beez3/html/com_contact/contact/default_user_custom_fields.php', - '/templates/beez3/html/com_contact/contact/encyclopedia.php', - '/templates/beez3/html/com_content/archive/default.php', - '/templates/beez3/html/com_content/archive/default_items.php', - '/templates/beez3/html/com_content/article/default.php', - '/templates/beez3/html/com_content/article/default_links.php', - '/templates/beez3/html/com_content/categories/default.php', - '/templates/beez3/html/com_content/categories/default_items.php', - '/templates/beez3/html/com_content/category/blog.php', - '/templates/beez3/html/com_content/category/blog_children.php', - '/templates/beez3/html/com_content/category/blog_item.php', - '/templates/beez3/html/com_content/category/blog_links.php', - '/templates/beez3/html/com_content/category/default.php', - '/templates/beez3/html/com_content/category/default_articles.php', - '/templates/beez3/html/com_content/category/default_children.php', - '/templates/beez3/html/com_content/featured/default.php', - '/templates/beez3/html/com_content/featured/default_item.php', - '/templates/beez3/html/com_content/featured/default_links.php', - '/templates/beez3/html/com_content/form/edit.php', - '/templates/beez3/html/com_newsfeeds/categories/default.php', - '/templates/beez3/html/com_newsfeeds/categories/default_items.php', - '/templates/beez3/html/com_newsfeeds/category/default.php', - '/templates/beez3/html/com_newsfeeds/category/default_children.php', - '/templates/beez3/html/com_newsfeeds/category/default_items.php', - '/templates/beez3/html/com_weblinks/categories/default.php', - '/templates/beez3/html/com_weblinks/categories/default_items.php', - '/templates/beez3/html/com_weblinks/category/default.php', - '/templates/beez3/html/com_weblinks/category/default_children.php', - '/templates/beez3/html/com_weblinks/category/default_items.php', - '/templates/beez3/html/com_weblinks/form/edit.php', - '/templates/beez3/html/layouts/joomla/system/message.php', - '/templates/beez3/html/mod_breadcrumbs/default.php', - '/templates/beez3/html/mod_languages/default.php', - '/templates/beez3/html/mod_login/default.php', - '/templates/beez3/html/mod_login/default_logout.php', - '/templates/beez3/html/modules.php', - '/templates/beez3/images/all_bg.gif', - '/templates/beez3/images/arrow.png', - '/templates/beez3/images/arrow2_grey.png', - '/templates/beez3/images/arrow_white_grey.png', - '/templates/beez3/images/blog_more.gif', - '/templates/beez3/images/blog_more_hover.gif', - '/templates/beez3/images/close.png', - '/templates/beez3/images/content_bg.gif', - '/templates/beez3/images/footer_bg.gif', - '/templates/beez3/images/footer_bg.png', - '/templates/beez3/images/header-bg.gif', - '/templates/beez3/images/minus.png', - '/templates/beez3/images/nature/arrow1.gif', - '/templates/beez3/images/nature/arrow1_rtl.gif', - '/templates/beez3/images/nature/arrow2.gif', - '/templates/beez3/images/nature/arrow2_grey.png', - '/templates/beez3/images/nature/arrow2_rtl.gif', - '/templates/beez3/images/nature/arrow_nav.gif', - '/templates/beez3/images/nature/arrow_small.png', - '/templates/beez3/images/nature/arrow_small_rtl.png', - '/templates/beez3/images/nature/blog_more.gif', - '/templates/beez3/images/nature/box.png', - '/templates/beez3/images/nature/box1.png', - '/templates/beez3/images/nature/grey_bg.png', - '/templates/beez3/images/nature/headingback.png', - '/templates/beez3/images/nature/karo.gif', - '/templates/beez3/images/nature/level4.png', - '/templates/beez3/images/nature/nav_level1_a.gif', - '/templates/beez3/images/nature/nav_level_1.gif', - '/templates/beez3/images/nature/pfeil.gif', - '/templates/beez3/images/nature/readmore_arrow.png', - '/templates/beez3/images/nature/searchbutton.png', - '/templates/beez3/images/nature/tabs.gif', - '/templates/beez3/images/nav_level_1.gif', - '/templates/beez3/images/news.gif', - '/templates/beez3/images/personal/arrow2_grey.jpg', - '/templates/beez3/images/personal/arrow2_grey.png', - '/templates/beez3/images/personal/bg2.png', - '/templates/beez3/images/personal/button.png', - '/templates/beez3/images/personal/dot.png', - '/templates/beez3/images/personal/ecke.gif', - '/templates/beez3/images/personal/footer.jpg', - '/templates/beez3/images/personal/grey_bg.png', - '/templates/beez3/images/personal/navi_active.png', - '/templates/beez3/images/personal/personal2.png', - '/templates/beez3/images/personal/readmore_arrow.png', - '/templates/beez3/images/personal/readmore_arrow_hover.png', - '/templates/beez3/images/personal/tabs_back.png', - '/templates/beez3/images/plus.png', - '/templates/beez3/images/req.png', - '/templates/beez3/images/slider_minus.png', - '/templates/beez3/images/slider_minus_rtl.png', - '/templates/beez3/images/slider_plus.png', - '/templates/beez3/images/slider_plus_rtl.png', - '/templates/beez3/images/system/arrow.png', - '/templates/beez3/images/system/arrow_rtl.png', - '/templates/beez3/images/system/calendar.png', - '/templates/beez3/images/system/j_button2_blank.png', - '/templates/beez3/images/system/j_button2_image.png', - '/templates/beez3/images/system/j_button2_left.png', - '/templates/beez3/images/system/j_button2_pagebreak.png', - '/templates/beez3/images/system/j_button2_readmore.png', - '/templates/beez3/images/system/notice-alert.png', - '/templates/beez3/images/system/notice-alert_rtl.png', - '/templates/beez3/images/system/notice-info.png', - '/templates/beez3/images/system/notice-info_rtl.png', - '/templates/beez3/images/system/notice-note.png', - '/templates/beez3/images/system/notice-note_rtl.png', - '/templates/beez3/images/system/selector-arrow.png', - '/templates/beez3/images/table_footer.gif', - '/templates/beez3/images/trans.gif', - '/templates/beez3/index.php', - '/templates/beez3/javascript/hide.js', - '/templates/beez3/javascript/md_stylechanger.js', - '/templates/beez3/javascript/respond.js', - '/templates/beez3/javascript/respond.src.js', - '/templates/beez3/javascript/template.js', - '/templates/beez3/jsstrings.php', - '/templates/beez3/language/en-GB/en-GB.tpl_beez3.ini', - '/templates/beez3/language/en-GB/en-GB.tpl_beez3.sys.ini', - '/templates/beez3/templateDetails.xml', - '/templates/beez3/template_preview.png', - '/templates/beez3/template_thumbnail.png', - '/templates/protostar/component.php', - '/templates/protostar/css/offline.css', - '/templates/protostar/css/template.css', - '/templates/protostar/error.php', - '/templates/protostar/favicon.ico', - '/templates/protostar/html/com_media/imageslist/default_folder.php', - '/templates/protostar/html/com_media/imageslist/default_image.php', - '/templates/protostar/html/layouts/joomla/form/field/contenthistory.php', - '/templates/protostar/html/layouts/joomla/form/field/media.php', - '/templates/protostar/html/layouts/joomla/form/field/user.php', - '/templates/protostar/html/layouts/joomla/system/message.php', - '/templates/protostar/html/modules.php', - '/templates/protostar/html/pagination.php', - '/templates/protostar/images/logo.png', - '/templates/protostar/images/system/rating_star.png', - '/templates/protostar/images/system/rating_star_blank.png', - '/templates/protostar/images/system/sort_asc.png', - '/templates/protostar/images/system/sort_desc.png', - '/templates/protostar/img/glyphicons-halflings-white.png', - '/templates/protostar/img/glyphicons-halflings.png', - '/templates/protostar/index.php', - '/templates/protostar/js/application.js', - '/templates/protostar/js/classes.js', - '/templates/protostar/js/template.js', - '/templates/protostar/language/en-GB/en-GB.tpl_protostar.ini', - '/templates/protostar/language/en-GB/en-GB.tpl_protostar.sys.ini', - '/templates/protostar/less/icomoon.less', - '/templates/protostar/less/template.less', - '/templates/protostar/less/template_rtl.less', - '/templates/protostar/less/variables.less', - '/templates/protostar/offline.php', - '/templates/protostar/templateDetails.xml', - '/templates/protostar/template_preview.png', - '/templates/protostar/template_thumbnail.png', - '/templates/system/css/system.css', - '/templates/system/css/toolbar.css', - '/templates/system/html/modules.php', - '/templates/system/images/calendar.png', - '/templates/system/images/j_button2_blank.png', - '/templates/system/images/j_button2_image.png', - '/templates/system/images/j_button2_left.png', - '/templates/system/images/j_button2_pagebreak.png', - '/templates/system/images/j_button2_readmore.png', - '/templates/system/images/j_button2_right.png', - '/templates/system/images/selector-arrow.png', - // 4.0 from Beta 1 to Beta 2 - '/administrator/components/com_finder/src/Indexer/Driver/Mysql.php', - '/administrator/components/com_finder/src/Indexer/Driver/Postgresql.php', - '/administrator/components/com_workflow/access.xml', - '/api/components/com_installer/src/Controller/LanguagesController.php', - '/api/components/com_installer/src/View/Languages/JsonapiView.php', - '/libraries/vendor/joomla/controller/LICENSE', - '/libraries/vendor/joomla/controller/src/AbstractController.php', - '/libraries/vendor/joomla/controller/src/ControllerInterface.php', - '/media/com_users/js/admin-users-user.es6.js', - '/media/com_users/js/admin-users-user.es6.min.js', - '/media/com_users/js/admin-users-user.es6.min.js.gz', - '/media/com_users/js/admin-users-user.js', - '/media/com_users/js/admin-users-user.min.js', - '/media/com_users/js/admin-users-user.min.js.gz', - // 4.0 from Beta 2 to Beta 3 - '/administrator/templates/atum/images/logo-blue.svg', - '/administrator/templates/atum/images/logo-joomla-blue.svg', - '/administrator/templates/atum/images/logo-joomla-white.svg', - '/administrator/templates/atum/images/logo.svg', - // 4.0 from Beta 3 to Beta 4 - '/components/com_config/src/Model/CmsModel.php', - // 4.0 from Beta 4 to Beta 5 - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-11.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-18.sql', - '/administrator/components/com_config/tmpl/application/default_system.php', - '/administrator/language/en-GB/plg_content_imagelazyload.sys.ini', - '/administrator/language/en-GB/plg_fields_image.ini', - '/administrator/language/en-GB/plg_fields_image.sys.ini', - '/administrator/templates/atum/scss/vendor/bootstrap/_nav.scss', - '/libraries/vendor/spomky-labs/base64url/phpstan.neon', - '/media/plg_system_webauthn/images/webauthn-black.png', - '/media/plg_system_webauthn/images/webauthn-color.png', - '/media/plg_system_webauthn/images/webauthn-white.png', - '/media/system/css/system.min.css', - '/media/system/css/system.min.css.gz', - '/plugins/content/imagelazyload/imagelazyload.php', - '/plugins/content/imagelazyload/imagelazyload.xml', - '/templates/cassiopeia/html/layouts/chromes/cardGrey.php', - '/templates/cassiopeia/html/layouts/chromes/default.php', - '/templates/cassiopeia/scss/vendor/bootstrap/_card.scss', - // 4.0 from Beta 5 to Beta 6 - '/administrator/modules/mod_multilangstatus/src/Helper/MultilangstatusAdminHelper.php', - '/administrator/templates/atum/favicon.ico', - '/libraries/vendor/nyholm/psr7/phpstan.baseline.dist', - '/libraries/vendor/spomky-labs/base64url/.php_cs.dist', - '/libraries/vendor/spomky-labs/base64url/infection.json.dist', - '/media/layouts/js/joomla/html/batch/batch-language.es6.js', - '/media/layouts/js/joomla/html/batch/batch-language.es6.min.js', - '/media/layouts/js/joomla/html/batch/batch-language.es6.min.js.gz', - '/media/layouts/js/joomla/html/batch/batch-language.js', - '/media/layouts/js/joomla/html/batch/batch-language.min.js', - '/media/layouts/js/joomla/html/batch/batch-language.min.js.gz', - '/media/plg_system_webauthn/images/webauthn-black.svg', - '/media/plg_system_webauthn/images/webauthn-white.svg', - '/media/system/js/core.es6/ajax.es6', - '/media/system/js/core.es6/customevent.es6', - '/media/system/js/core.es6/event.es6', - '/media/system/js/core.es6/form.es6', - '/media/system/js/core.es6/message.es6', - '/media/system/js/core.es6/options.es6', - '/media/system/js/core.es6/text.es6', - '/media/system/js/core.es6/token.es6', - '/media/system/js/core.es6/webcomponent.es6', - '/templates/cassiopeia/favicon.ico', - '/templates/cassiopeia/scss/_mixin.scss', - '/templates/cassiopeia/scss/_variables.scss', - '/templates/cassiopeia/scss/blocks/_demo-styling.scss', - // 4.0 from Beta 6 to Beta 7 - '/media/legacy/js/bootstrap-init.js', - '/media/legacy/js/bootstrap-init.min.js', - '/media/legacy/js/bootstrap-init.min.js.gz', - '/media/legacy/js/frontediting.js', - '/media/legacy/js/frontediting.min.js', - '/media/legacy/js/frontediting.min.js.gz', - '/media/vendor/bootstrap/js/bootstrap.bundle.js', - '/media/vendor/bootstrap/js/bootstrap.bundle.min.js', - '/media/vendor/bootstrap/js/bootstrap.bundle.min.js.gz', - '/media/vendor/bootstrap/js/bootstrap.bundle.min.js.map', - '/media/vendor/bootstrap/js/bootstrap.js', - '/media/vendor/bootstrap/js/bootstrap.min.js', - '/media/vendor/bootstrap/js/bootstrap.min.js.gz', - '/media/vendor/bootstrap/scss/_code.scss', - '/media/vendor/bootstrap/scss/_custom-forms.scss', - '/media/vendor/bootstrap/scss/_input-group.scss', - '/media/vendor/bootstrap/scss/_jumbotron.scss', - '/media/vendor/bootstrap/scss/_media.scss', - '/media/vendor/bootstrap/scss/_print.scss', - '/media/vendor/bootstrap/scss/mixins/_background-variant.scss', - '/media/vendor/bootstrap/scss/mixins/_badge.scss', - '/media/vendor/bootstrap/scss/mixins/_float.scss', - '/media/vendor/bootstrap/scss/mixins/_grid-framework.scss', - '/media/vendor/bootstrap/scss/mixins/_hover.scss', - '/media/vendor/bootstrap/scss/mixins/_nav-divider.scss', - '/media/vendor/bootstrap/scss/mixins/_screen-reader.scss', - '/media/vendor/bootstrap/scss/mixins/_size.scss', - '/media/vendor/bootstrap/scss/mixins/_table-row.scss', - '/media/vendor/bootstrap/scss/mixins/_text-emphasis.scss', - '/media/vendor/bootstrap/scss/mixins/_text-hide.scss', - '/media/vendor/bootstrap/scss/mixins/_visibility.scss', - '/media/vendor/bootstrap/scss/utilities/_align.scss', - '/media/vendor/bootstrap/scss/utilities/_background.scss', - '/media/vendor/bootstrap/scss/utilities/_borders.scss', - '/media/vendor/bootstrap/scss/utilities/_clearfix.scss', - '/media/vendor/bootstrap/scss/utilities/_display.scss', - '/media/vendor/bootstrap/scss/utilities/_embed.scss', - '/media/vendor/bootstrap/scss/utilities/_flex.scss', - '/media/vendor/bootstrap/scss/utilities/_float.scss', - '/media/vendor/bootstrap/scss/utilities/_interactions.scss', - '/media/vendor/bootstrap/scss/utilities/_overflow.scss', - '/media/vendor/bootstrap/scss/utilities/_position.scss', - '/media/vendor/bootstrap/scss/utilities/_screenreaders.scss', - '/media/vendor/bootstrap/scss/utilities/_shadows.scss', - '/media/vendor/bootstrap/scss/utilities/_sizing.scss', - '/media/vendor/bootstrap/scss/utilities/_spacing.scss', - '/media/vendor/bootstrap/scss/utilities/_stretched-link.scss', - '/media/vendor/bootstrap/scss/utilities/_text.scss', - '/media/vendor/bootstrap/scss/utilities/_visibility.scss', - '/media/vendor/skipto/css/SkipTo.css', - '/media/vendor/skipto/js/dropMenu.js', - // 4.0 from Beta 7 to RC 1 - '/administrator/components/com_admin/forms/profile.xml', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-07-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-09-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-09-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-10-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-10-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-03-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-04-25.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-05-31.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-06-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-10-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-02-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-07-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-08-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-09-12.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-10-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-01-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-01-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-02-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-03-31.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-05-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-06-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-21.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-23.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-25.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-27.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-10-13.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-10-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-11-07.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-11-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-08.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-11.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-05-21.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-09-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-09-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-12-08.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-12-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-02-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-04-11.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-04-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-04.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-07.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-07-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-09-22.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-09-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-10-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-10-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-03-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-04-25.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-05-31.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-06-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-10-10.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-02-24.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-26.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-07-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-08-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-09-12.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-10-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-01-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-01-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-02-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-03-31.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-05-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-06-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-14.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-14.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-23.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-24.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-25.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-26.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-27.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-10-13.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-10-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-11-07.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-11-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-22.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-05-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-09-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-09-22.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-12-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-12-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-02-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-04-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-04-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-07.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-10.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-21.sql', - '/administrator/components/com_admin/src/Controller/ProfileController.php', - '/administrator/components/com_admin/src/Model/ProfileModel.php', - '/administrator/components/com_admin/src/View/Profile/HtmlView.php', - '/administrator/components/com_admin/tmpl/profile/edit.php', - '/administrator/components/com_config/tmpl/application/default_ftp.php', - '/administrator/components/com_config/tmpl/application/default_ftplogin.php', - '/administrator/components/com_csp/access.xml', - '/administrator/components/com_csp/config.xml', - '/administrator/components/com_csp/csp.xml', - '/administrator/components/com_csp/forms/filter_reports.xml', - '/administrator/components/com_csp/services/provider.php', - '/administrator/components/com_csp/src/Controller/DisplayController.php', - '/administrator/components/com_csp/src/Controller/ReportsController.php', - '/administrator/components/com_csp/src/Helper/ReporterHelper.php', - '/administrator/components/com_csp/src/Model/ReportModel.php', - '/administrator/components/com_csp/src/Model/ReportsModel.php', - '/administrator/components/com_csp/src/Table/ReportTable.php', - '/administrator/components/com_csp/src/View/Reports/HtmlView.php', - '/administrator/components/com_csp/tmpl/reports/default.php', - '/administrator/components/com_csp/tmpl/reports/default.xml', - '/administrator/components/com_fields/src/Field/SubfieldstypeField.php', - '/administrator/components/com_installer/tmpl/installer/default_ftp.php', - '/administrator/components/com_joomlaupdate/src/Helper/Select.php', - '/administrator/language/en-GB/com_csp.ini', - '/administrator/language/en-GB/com_csp.sys.ini', - '/administrator/language/en-GB/plg_fields_subfields.ini', - '/administrator/language/en-GB/plg_fields_subfields.sys.ini', - '/administrator/templates/atum/Service/HTML/Atum.php', - '/components/com_csp/src/Controller/ReportController.php', - '/components/com_menus/src/Controller/DisplayController.php', - '/libraries/vendor/algo26-matthias/idna-convert/CODE_OF_CONDUCT.md', - '/libraries/vendor/algo26-matthias/idna-convert/UPGRADING.md', - '/libraries/vendor/algo26-matthias/idna-convert/docker-compose.yml', - '/libraries/vendor/beberlei/assert/phpstan-code.neon', - '/libraries/vendor/beberlei/assert/phpstan-tests.neon', - '/libraries/vendor/bin/generate-defuse-key', - '/libraries/vendor/bin/var-dump-server', - '/libraries/vendor/bin/yaml-lint', - '/libraries/vendor/brick/math/psalm-baseline.xml', - '/libraries/vendor/doctrine/inflector/phpstan.neon.dist', - '/libraries/vendor/jakeasmith/http_build_url/readme.md', - '/libraries/vendor/nyholm/psr7/src/LowercaseTrait.php', - '/libraries/vendor/ozdemirburak/iris/LICENSE.md', - '/libraries/vendor/ozdemirburak/iris/src/BaseColor.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Factory.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Hex.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Hsl.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Hsla.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Hsv.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Rgb.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Rgba.php', - '/libraries/vendor/ozdemirburak/iris/src/Exceptions/AmbiguousColorString.php', - '/libraries/vendor/ozdemirburak/iris/src/Exceptions/InvalidColorException.php', - '/libraries/vendor/ozdemirburak/iris/src/Helpers/DefinedColor.php', - '/libraries/vendor/ozdemirburak/iris/src/Traits/AlphaTrait.php', - '/libraries/vendor/ozdemirburak/iris/src/Traits/HsTrait.php', - '/libraries/vendor/ozdemirburak/iris/src/Traits/HslTrait.php', - '/libraries/vendor/ozdemirburak/iris/src/Traits/RgbTrait.php', - '/libraries/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey', - '/libraries/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey.asc', - '/libraries/vendor/psr/http-factory/.pullapprove.yml', - '/libraries/vendor/spomky-labs/cbor-php/.php_cs.dist', - '/libraries/vendor/spomky-labs/cbor-php/CODE_OF_CONDUCT.md', - '/libraries/vendor/spomky-labs/cbor-php/infection.json.dist', - '/libraries/vendor/spomky-labs/cbor-php/phpstan.neon', - '/libraries/vendor/typo3/phar-stream-wrapper/_config.yml', - '/libraries/vendor/voku/portable-utf8/SUMMARY.md', - '/libraries/vendor/willdurand/negotiation/src/Negotiation/Match.php', - '/media/com_actionlogs/js/admin-actionlogs-default.es6.js', - '/media/com_actionlogs/js/admin-actionlogs-default.es6.min.js', - '/media/com_actionlogs/js/admin-actionlogs-default.es6.min.js.gz', - '/media/com_associations/js/admin-associations-default.es6.js', - '/media/com_associations/js/admin-associations-default.es6.min.js', - '/media/com_associations/js/admin-associations-default.es6.min.js.gz', - '/media/com_associations/js/admin-associations-modal.es6.js', - '/media/com_associations/js/admin-associations-modal.es6.min.js', - '/media/com_associations/js/admin-associations-modal.es6.min.js.gz', - '/media/com_associations/js/associations-edit.es6.js', - '/media/com_associations/js/associations-edit.es6.min.js', - '/media/com_associations/js/associations-edit.es6.min.js.gz', - '/media/com_banners/js/admin-banner-edit.es6.js', - '/media/com_banners/js/admin-banner-edit.es6.min.js', - '/media/com_banners/js/admin-banner-edit.es6.min.js.gz', - '/media/com_cache/js/admin-cache-default.es6.js', - '/media/com_cache/js/admin-cache-default.es6.min.js', - '/media/com_cache/js/admin-cache-default.es6.min.js.gz', - '/media/com_categories/js/shared-categories-accordion.es6.js', - '/media/com_categories/js/shared-categories-accordion.es6.min.js', - '/media/com_categories/js/shared-categories-accordion.es6.min.js.gz', - '/media/com_config/js/config-default.es6.js', - '/media/com_config/js/config-default.es6.min.js', - '/media/com_config/js/config-default.es6.min.js.gz', - '/media/com_config/js/modules-default.es6.js', - '/media/com_config/js/modules-default.es6.min.js', - '/media/com_config/js/modules-default.es6.min.js.gz', - '/media/com_config/js/templates-default.es6.js', - '/media/com_config/js/templates-default.es6.min.js', - '/media/com_config/js/templates-default.es6.min.js.gz', - '/media/com_contact/js/admin-contacts-modal.es6.js', - '/media/com_contact/js/admin-contacts-modal.es6.min.js', - '/media/com_contact/js/admin-contacts-modal.es6.min.js.gz', - '/media/com_contact/js/contacts-list.es6.js', - '/media/com_contact/js/contacts-list.es6.min.js', - '/media/com_contact/js/contacts-list.es6.min.js.gz', - '/media/com_content/js/admin-article-pagebreak.es6.js', - '/media/com_content/js/admin-article-pagebreak.es6.min.js', - '/media/com_content/js/admin-article-pagebreak.es6.min.js.gz', - '/media/com_content/js/admin-article-readmore.es6.js', - '/media/com_content/js/admin-article-readmore.es6.min.js', - '/media/com_content/js/admin-article-readmore.es6.min.js.gz', - '/media/com_content/js/admin-articles-default-batch-footer.es6.js', - '/media/com_content/js/admin-articles-default-batch-footer.es6.min.js', - '/media/com_content/js/admin-articles-default-batch-footer.es6.min.js.gz', - '/media/com_content/js/admin-articles-default-stage-footer.es6.js', - '/media/com_content/js/admin-articles-default-stage-footer.es6.min.js', - '/media/com_content/js/admin-articles-default-stage-footer.es6.min.js.gz', - '/media/com_content/js/admin-articles-modal.es6.js', - '/media/com_content/js/admin-articles-modal.es6.min.js', - '/media/com_content/js/admin-articles-modal.es6.min.js.gz', - '/media/com_content/js/articles-list.es6.js', - '/media/com_content/js/articles-list.es6.min.js', - '/media/com_content/js/articles-list.es6.min.js.gz', - '/media/com_content/js/form-edit.es6.js', - '/media/com_content/js/form-edit.es6.min.js', - '/media/com_content/js/form-edit.es6.min.js.gz', - '/media/com_contenthistory/js/admin-compare-compare.es6.js', - '/media/com_contenthistory/js/admin-compare-compare.es6.min.js', - '/media/com_contenthistory/js/admin-compare-compare.es6.min.js.gz', - '/media/com_contenthistory/js/admin-history-modal.es6.js', - '/media/com_contenthistory/js/admin-history-modal.es6.min.js', - '/media/com_contenthistory/js/admin-history-modal.es6.min.js.gz', - '/media/com_contenthistory/js/admin-history-versions.es6.js', - '/media/com_contenthistory/js/admin-history-versions.es6.min.js', - '/media/com_contenthistory/js/admin-history-versions.es6.min.js.gz', - '/media/com_cpanel/js/admin-add_module.es6.js', - '/media/com_cpanel/js/admin-add_module.es6.min.js', - '/media/com_cpanel/js/admin-add_module.es6.min.js.gz', - '/media/com_cpanel/js/admin-cpanel-default.es6.js', - '/media/com_cpanel/js/admin-cpanel-default.es6.min.js', - '/media/com_cpanel/js/admin-cpanel-default.es6.min.js.gz', - '/media/com_cpanel/js/admin-system-loader.es6.js', - '/media/com_cpanel/js/admin-system-loader.es6.min.js', - '/media/com_cpanel/js/admin-system-loader.es6.min.js.gz', - '/media/com_fields/js/admin-field-changecontext.es6.js', - '/media/com_fields/js/admin-field-changecontext.es6.min.js', - '/media/com_fields/js/admin-field-changecontext.es6.min.js.gz', - '/media/com_fields/js/admin-field-edit-modal.es6.js', - '/media/com_fields/js/admin-field-edit-modal.es6.min.js', - '/media/com_fields/js/admin-field-edit-modal.es6.min.js.gz', - '/media/com_fields/js/admin-field-edit.es6.js', - '/media/com_fields/js/admin-field-edit.es6.min.js', - '/media/com_fields/js/admin-field-edit.es6.min.js.gz', - '/media/com_fields/js/admin-field-typehaschanged.es6.js', - '/media/com_fields/js/admin-field-typehaschanged.es6.min.js', - '/media/com_fields/js/admin-field-typehaschanged.es6.min.js.gz', - '/media/com_fields/js/admin-fields-default-batch.es6.js', - '/media/com_fields/js/admin-fields-default-batch.es6.min.js', - '/media/com_fields/js/admin-fields-default-batch.es6.min.js.gz', - '/media/com_fields/js/admin-fields-modal.es6.js', - '/media/com_fields/js/admin-fields-modal.es6.min.js', - '/media/com_fields/js/admin-fields-modal.es6.min.js.gz', - '/media/com_finder/js/filters.es6.js', - '/media/com_finder/js/filters.es6.min.js', - '/media/com_finder/js/filters.es6.min.js.gz', - '/media/com_finder/js/finder-edit.es6.js', - '/media/com_finder/js/finder-edit.es6.min.js', - '/media/com_finder/js/finder-edit.es6.min.js.gz', - '/media/com_finder/js/finder.es6.js', - '/media/com_finder/js/finder.es6.min.js', - '/media/com_finder/js/finder.es6.min.js.gz', - '/media/com_finder/js/index.es6.js', - '/media/com_finder/js/index.es6.min.js', - '/media/com_finder/js/index.es6.min.js.gz', - '/media/com_finder/js/indexer.es6.js', - '/media/com_finder/js/indexer.es6.min.js', - '/media/com_finder/js/indexer.es6.min.js.gz', - '/media/com_finder/js/maps.es6.js', - '/media/com_finder/js/maps.es6.min.js', - '/media/com_finder/js/maps.es6.min.js.gz', - '/media/com_installer/js/changelog.es6.js', - '/media/com_installer/js/changelog.es6.min.js', - '/media/com_installer/js/changelog.es6.min.js.gz', - '/media/com_installer/js/installer.es6.js', - '/media/com_installer/js/installer.es6.min.js', - '/media/com_installer/js/installer.es6.min.js.gz', - '/media/com_joomlaupdate/js/admin-update-default.es6.js', - '/media/com_joomlaupdate/js/admin-update-default.es6.min.js', - '/media/com_joomlaupdate/js/admin-update-default.es6.min.js.gz', - '/media/com_languages/js/admin-language-edit-change-flag.es6.js', - '/media/com_languages/js/admin-language-edit-change-flag.es6.min.js', - '/media/com_languages/js/admin-language-edit-change-flag.es6.min.js.gz', - '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.js', - '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.min.js', - '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.min.js.gz', - '/media/com_languages/js/overrider.es6.js', - '/media/com_languages/js/overrider.es6.min.js', - '/media/com_languages/js/overrider.es6.min.js.gz', - '/media/com_mails/js/admin-email-template-edit.es6.js', - '/media/com_mails/js/admin-email-template-edit.es6.min.js', - '/media/com_mails/js/admin-email-template-edit.es6.min.js.gz', - '/media/com_media/css/mediamanager.min.css', - '/media/com_media/css/mediamanager.min.css.gz', - '/media/com_media/css/mediamanager.min.css.map', - '/media/com_media/js/edit-images.es6.js', - '/media/com_media/js/edit-images.es6.min.js', - '/media/com_media/js/mediamanager.min.js', - '/media/com_media/js/mediamanager.min.js.gz', - '/media/com_media/js/mediamanager.min.js.map', - '/media/com_menus/js/admin-item-edit.es6.js', - '/media/com_menus/js/admin-item-edit.es6.min.js', - '/media/com_menus/js/admin-item-edit.es6.min.js.gz', - '/media/com_menus/js/admin-item-edit_container.es6.js', - '/media/com_menus/js/admin-item-edit_container.es6.min.js', - '/media/com_menus/js/admin-item-edit_container.es6.min.js.gz', - '/media/com_menus/js/admin-item-edit_modules.es6.js', - '/media/com_menus/js/admin-item-edit_modules.es6.min.js', - '/media/com_menus/js/admin-item-edit_modules.es6.min.js.gz', - '/media/com_menus/js/admin-item-modal.es6.js', - '/media/com_menus/js/admin-item-modal.es6.min.js', - '/media/com_menus/js/admin-item-modal.es6.min.js.gz', - '/media/com_menus/js/admin-items-modal.es6.js', - '/media/com_menus/js/admin-items-modal.es6.min.js', - '/media/com_menus/js/admin-items-modal.es6.min.js.gz', - '/media/com_menus/js/admin-menus-default.es6.js', - '/media/com_menus/js/admin-menus-default.es6.min.js', - '/media/com_menus/js/admin-menus-default.es6.min.js.gz', - '/media/com_menus/js/default-batch-body.es6.js', - '/media/com_menus/js/default-batch-body.es6.min.js', - '/media/com_menus/js/default-batch-body.es6.min.js.gz', - '/media/com_modules/js/admin-module-edit.es6.js', - '/media/com_modules/js/admin-module-edit.es6.min.js', - '/media/com_modules/js/admin-module-edit.es6.min.js.gz', - '/media/com_modules/js/admin-module-edit_assignment.es6.js', - '/media/com_modules/js/admin-module-edit_assignment.es6.min.js', - '/media/com_modules/js/admin-module-edit_assignment.es6.min.js.gz', - '/media/com_modules/js/admin-module-search.es6.js', - '/media/com_modules/js/admin-module-search.es6.min.js', - '/media/com_modules/js/admin-module-search.es6.min.js.gz', - '/media/com_modules/js/admin-modules-modal.es6.js', - '/media/com_modules/js/admin-modules-modal.es6.min.js', - '/media/com_modules/js/admin-modules-modal.es6.min.js.gz', - '/media/com_modules/js/admin-select-modal.es6.js', - '/media/com_modules/js/admin-select-modal.es6.min.js', - '/media/com_modules/js/admin-select-modal.es6.min.js.gz', - '/media/com_tags/js/tag-default.es6.js', - '/media/com_tags/js/tag-default.es6.min.js', - '/media/com_tags/js/tag-default.es6.min.js.gz', - '/media/com_tags/js/tag-list.es6.js', - '/media/com_tags/js/tag-list.es6.min.js', - '/media/com_tags/js/tag-list.es6.min.js.gz', - '/media/com_tags/js/tags-default.es6.js', - '/media/com_tags/js/tags-default.es6.min.js', - '/media/com_tags/js/tags-default.es6.min.js.gz', - '/media/com_templates/js/admin-template-compare.es6.js', - '/media/com_templates/js/admin-template-compare.es6.min.js', - '/media/com_templates/js/admin-template-compare.es6.min.js.gz', - '/media/com_templates/js/admin-template-toggle-assignment.es6.js', - '/media/com_templates/js/admin-template-toggle-assignment.es6.min.js', - '/media/com_templates/js/admin-template-toggle-assignment.es6.min.js.gz', - '/media/com_templates/js/admin-template-toggle-switch.es6.js', - '/media/com_templates/js/admin-template-toggle-switch.es6.min.js', - '/media/com_templates/js/admin-template-toggle-switch.es6.min.js.gz', - '/media/com_templates/js/admin-templates-default.es6.js', - '/media/com_templates/js/admin-templates-default.es6.min.js', - '/media/com_templates/js/admin-templates-default.es6.min.js.gz', - '/media/com_users/js/admin-users-groups.es6.js', - '/media/com_users/js/admin-users-groups.es6.min.js', - '/media/com_users/js/admin-users-groups.es6.min.js.gz', - '/media/com_users/js/admin-users-mail.es6.js', - '/media/com_users/js/admin-users-mail.es6.min.js', - '/media/com_users/js/admin-users-mail.es6.min.js.gz', - '/media/com_users/js/two-factor-switcher.es6.js', - '/media/com_users/js/two-factor-switcher.es6.min.js', - '/media/com_users/js/two-factor-switcher.es6.min.js.gz', - '/media/com_workflow/js/admin-items-workflow-buttons.es6.js', - '/media/com_workflow/js/admin-items-workflow-buttons.es6.min.js', - '/media/com_workflow/js/admin-items-workflow-buttons.es6.min.js.gz', - '/media/com_wrapper/js/iframe-height.es6.js', - '/media/com_wrapper/js/iframe-height.es6.min.js', - '/media/com_wrapper/js/iframe-height.es6.min.js.gz', - '/media/layouts/js/joomla/form/field/category-change.es6.js', - '/media/layouts/js/joomla/form/field/category-change.es6.min.js', - '/media/layouts/js/joomla/form/field/category-change.es6.min.js.gz', - '/media/layouts/js/joomla/html/batch/batch-copymove.es6.js', - '/media/layouts/js/joomla/html/batch/batch-copymove.es6.min.js', - '/media/layouts/js/joomla/html/batch/batch-copymove.es6.min.js.gz', - '/media/legacy/js/highlighter.js', - '/media/legacy/js/highlighter.min.js', - '/media/legacy/js/highlighter.min.js.gz', - '/media/mod_login/js/admin-login.es6.js', - '/media/mod_login/js/admin-login.es6.min.js', - '/media/mod_login/js/admin-login.es6.min.js.gz', - '/media/mod_menu/js/admin-menu.es6.js', - '/media/mod_menu/js/admin-menu.es6.min.js', - '/media/mod_menu/js/admin-menu.es6.min.js.gz', - '/media/mod_menu/js/menu.es6.js', - '/media/mod_menu/js/menu.es6.min.js', - '/media/mod_menu/js/menu.es6.min.js.gz', - '/media/mod_multilangstatus/js/admin-multilangstatus.es6.js', - '/media/mod_multilangstatus/js/admin-multilangstatus.es6.min.js', - '/media/mod_multilangstatus/js/admin-multilangstatus.es6.min.js.gz', - '/media/mod_quickicon/js/quickicon.es6.js', - '/media/mod_quickicon/js/quickicon.es6.min.js', - '/media/mod_quickicon/js/quickicon.es6.min.js.gz', - '/media/mod_sampledata/js/sampledata-process.es6.js', - '/media/mod_sampledata/js/sampledata-process.es6.min.js', - '/media/mod_sampledata/js/sampledata-process.es6.min.js.gz', - '/media/plg_captcha_recaptcha/js/recaptcha.es6.js', - '/media/plg_captcha_recaptcha/js/recaptcha.es6.min.js', - '/media/plg_captcha_recaptcha/js/recaptcha.es6.min.js.gz', - '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.js', - '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.min.js', - '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.min.js.gz', - '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.js', - '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.min.js', - '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.min.js.gz', - '/media/plg_editors_tinymce/js/tinymce-builder.es6.js', - '/media/plg_editors_tinymce/js/tinymce-builder.es6.min.js', - '/media/plg_editors_tinymce/js/tinymce-builder.es6.min.js.gz', - '/media/plg_editors_tinymce/js/tinymce.es6.js', - '/media/plg_editors_tinymce/js/tinymce.es6.min.js', - '/media/plg_editors_tinymce/js/tinymce.es6.min.js.gz', - '/media/plg_installer_folderinstaller/js/folderinstaller.es6.js', - '/media/plg_installer_folderinstaller/js/folderinstaller.es6.min.js', - '/media/plg_installer_folderinstaller/js/folderinstaller.es6.min.js.gz', - '/media/plg_installer_packageinstaller/js/packageinstaller.es6.js', - '/media/plg_installer_packageinstaller/js/packageinstaller.es6.min.js', - '/media/plg_installer_packageinstaller/js/packageinstaller.es6.min.js.gz', - '/media/plg_installer_urlinstaller/js/urlinstaller.es6.js', - '/media/plg_installer_urlinstaller/js/urlinstaller.es6.min.js', - '/media/plg_installer_urlinstaller/js/urlinstaller.es6.min.js.gz', - '/media/plg_installer_webinstaller/js/client.es6.js', - '/media/plg_installer_webinstaller/js/client.es6.min.js', - '/media/plg_installer_webinstaller/js/client.es6.min.js.gz', - '/media/plg_media-action_crop/js/crop.es6.js', - '/media/plg_media-action_crop/js/crop.es6.min.js', - '/media/plg_media-action_crop/js/crop.es6.min.js.gz', - '/media/plg_media-action_resize/js/resize.es6.js', - '/media/plg_media-action_resize/js/resize.es6.min.js', - '/media/plg_media-action_resize/js/resize.es6.min.js.gz', - '/media/plg_media-action_rotate/js/rotate.es6.js', - '/media/plg_media-action_rotate/js/rotate.es6.min.js', - '/media/plg_media-action_rotate/js/rotate.es6.min.js.gz', - '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.js', - '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.min.js', - '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.min.js.gz', - '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.js', - '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.min.js', - '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.min.js.gz', - '/media/plg_quickicon_overridecheck/js/overridecheck.es6.js', - '/media/plg_quickicon_overridecheck/js/overridecheck.es6.min.js', - '/media/plg_quickicon_overridecheck/js/overridecheck.es6.min.js.gz', - '/media/plg_quickicon_privacycheck/js/privacycheck.es6.js', - '/media/plg_quickicon_privacycheck/js/privacycheck.es6.min.js', - '/media/plg_quickicon_privacycheck/js/privacycheck.es6.min.js.gz', - '/media/plg_system_debug/js/debug.es6.js', - '/media/plg_system_debug/js/debug.es6.min.js', - '/media/plg_system_debug/js/debug.es6.min.js.gz', - '/media/plg_system_highlight/highlight.min.css', - '/media/plg_system_highlight/highlight.min.css.gz', - '/media/plg_system_stats/js/stats-message.es6.js', - '/media/plg_system_stats/js/stats-message.es6.min.js', - '/media/plg_system_stats/js/stats-message.es6.min.js.gz', - '/media/plg_system_stats/js/stats.es6.js', - '/media/plg_system_stats/js/stats.es6.min.js', - '/media/plg_system_stats/js/stats.es6.min.js.gz', - '/media/plg_system_webauthn/js/login.es6.js', - '/media/plg_system_webauthn/js/login.es6.min.js', - '/media/plg_system_webauthn/js/login.es6.min.js.gz', - '/media/plg_system_webauthn/js/management.es6.js', - '/media/plg_system_webauthn/js/management.es6.min.js', - '/media/plg_system_webauthn/js/management.es6.min.js.gz', - '/media/plg_user_token/js/token.es6.js', - '/media/plg_user_token/js/token.es6.min.js', - '/media/plg_user_token/js/token.es6.min.js.gz', - '/media/system/js/core.es6.js', - '/media/system/js/core.es6.min.js', - '/media/system/js/core.es6.min.js.gz', - '/media/system/js/draggable.es6.js', - '/media/system/js/draggable.es6.min.js', - '/media/system/js/draggable.es6.min.js.gz', - '/media/system/js/fields/joomla-field-color-slider.es6.js', - '/media/system/js/fields/joomla-field-color-slider.es6.min.js', - '/media/system/js/fields/joomla-field-color-slider.es6.min.js.gz', - '/media/system/js/fields/passwordstrength.es6.js', - '/media/system/js/fields/passwordstrength.es6.min.js', - '/media/system/js/fields/passwordstrength.es6.min.js.gz', - '/media/system/js/fields/passwordview.es6.js', - '/media/system/js/fields/passwordview.es6.min.js', - '/media/system/js/fields/passwordview.es6.min.js.gz', - '/media/system/js/fields/select-colour.es6.js', - '/media/system/js/fields/select-colour.es6.min.js', - '/media/system/js/fields/select-colour.es6.min.js.gz', - '/media/system/js/fields/validate.es6.js', - '/media/system/js/fields/validate.es6.min.js', - '/media/system/js/fields/validate.es6.min.js.gz', - '/media/system/js/keepalive.es6.js', - '/media/system/js/keepalive.es6.min.js', - '/media/system/js/keepalive.es6.min.js.gz', - '/media/system/js/multiselect.es6.js', - '/media/system/js/multiselect.es6.min.js', - '/media/system/js/multiselect.es6.min.js.gz', - '/media/system/js/searchtools.es6.js', - '/media/system/js/searchtools.es6.min.js', - '/media/system/js/searchtools.es6.min.js.gz', - '/media/system/js/showon.es6.js', - '/media/system/js/showon.es6.min.js', - '/media/system/js/showon.es6.min.js.gz', - '/media/templates/atum/js/template.es6.js', - '/media/templates/atum/js/template.es6.min.js', - '/media/templates/atum/js/template.es6.min.js.gz', - '/media/templates/atum/js/template.js', - '/media/templates/atum/js/template.min.js', - '/media/templates/atum/js/template.min.js.gz', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.min.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.min.js.gz', - '/media/vendor/bootstrap/js/alert.es6.js', - '/media/vendor/bootstrap/js/alert.es6.min.js', - '/media/vendor/bootstrap/js/alert.es6.min.js.gz', - '/media/vendor/bootstrap/js/bootstrap.es5.js', - '/media/vendor/bootstrap/js/bootstrap.es5.min.js', - '/media/vendor/bootstrap/js/bootstrap.es5.min.js.gz', - '/media/vendor/bootstrap/js/button.es6.js', - '/media/vendor/bootstrap/js/button.es6.min.js', - '/media/vendor/bootstrap/js/button.es6.min.js.gz', - '/media/vendor/bootstrap/js/carousel.es6.js', - '/media/vendor/bootstrap/js/carousel.es6.min.js', - '/media/vendor/bootstrap/js/carousel.es6.min.js.gz', - '/media/vendor/bootstrap/js/collapse.es6.js', - '/media/vendor/bootstrap/js/collapse.es6.min.js', - '/media/vendor/bootstrap/js/collapse.es6.min.js.gz', - '/media/vendor/bootstrap/js/dom-8eef6b5f.js', - '/media/vendor/bootstrap/js/dropdown.es6.js', - '/media/vendor/bootstrap/js/dropdown.es6.min.js', - '/media/vendor/bootstrap/js/dropdown.es6.min.js.gz', - '/media/vendor/bootstrap/js/modal.es6.js', - '/media/vendor/bootstrap/js/modal.es6.min.js', - '/media/vendor/bootstrap/js/modal.es6.min.js.gz', - '/media/vendor/bootstrap/js/popover.es6.js', - '/media/vendor/bootstrap/js/popover.es6.min.js', - '/media/vendor/bootstrap/js/popover.es6.min.js.gz', - '/media/vendor/bootstrap/js/popper-5304749a.js', - '/media/vendor/bootstrap/js/scrollspy.es6.js', - '/media/vendor/bootstrap/js/scrollspy.es6.min.js', - '/media/vendor/bootstrap/js/scrollspy.es6.min.js.gz', - '/media/vendor/bootstrap/js/tab.es6.js', - '/media/vendor/bootstrap/js/tab.es6.min.js', - '/media/vendor/bootstrap/js/tab.es6.min.js.gz', - '/media/vendor/bootstrap/js/toast.es6.js', - '/media/vendor/bootstrap/js/toast.es6.min.js', - '/media/vendor/bootstrap/js/toast.es6.min.js.gz', - '/media/vendor/codemirror/lib/codemirror-ce.js', - '/media/vendor/codemirror/lib/codemirror-ce.min.js', - '/media/vendor/codemirror/lib/codemirror-ce.min.js.gz', - '/media/vendor/punycode/js/punycode.js', - '/media/vendor/punycode/js/punycode.min.js', - '/media/vendor/punycode/js/punycode.min.js.gz', - '/media/vendor/tinymce/changelog.txt', - '/media/vendor/webcomponentsjs/js/webcomponents-ce.js', - '/media/vendor/webcomponentsjs/js/webcomponents-ce.min.js', - '/media/vendor/webcomponentsjs/js/webcomponents-ce.min.js.gz', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.min.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.min.js.gz', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.min.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.min.js.gz', - '/media/vendor/webcomponentsjs/js/webcomponents-sd.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd.min.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd.min.js.gz', - '/plugins/fields/subfields/params/subfields.xml', - '/plugins/fields/subfields/subfields.php', - '/plugins/fields/subfields/subfields.xml', - '/plugins/fields/subfields/tmpl/subfields.php', - '/templates/cassiopeia/images/system/rating_star.png', - '/templates/cassiopeia/images/system/rating_star_blank.png', - '/templates/cassiopeia/scss/tools/mixins/_margin.scss', - '/templates/cassiopeia/scss/tools/mixins/_visually-hidden.scss', - '/templates/system/js/error-locales.js', - // 4.0 from RC 1 to RC 2 - '/administrator/components/com_fields/tmpl/field/modal.php', - '/administrator/templates/atum/scss/pages/_com_admin.scss', - '/administrator/templates/atum/scss/pages/_com_finder.scss', - '/libraries/src/Error/JsonApi/InstallLanguageExceptionHandler.php', - '/libraries/src/MVC/Controller/Exception/InstallLanguage.php', - '/media/com_fields/js/admin-field-edit-modal-es5.js', - '/media/com_fields/js/admin-field-edit-modal-es5.min.js', - '/media/com_fields/js/admin-field-edit-modal-es5.min.js.gz', - '/media/com_fields/js/admin-field-edit-modal.js', - '/media/com_fields/js/admin-field-edit-modal.min.js', - '/media/com_fields/js/admin-field-edit-modal.min.js.gz', - // 4.0 from RC 3 to RC 4 - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_nodownload.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_noupdate.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_preupdatecheck.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_reinstall.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_update.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_updatemefirst.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_upload.php', - '/language/en-GB/com_messages.ini', - '/media/system/css/fields/joomla-image-select.css', - '/media/system/css/fields/joomla-image-select.min.css', - '/media/system/css/fields/joomla-image-select.min.css.gz', - '/media/system/js/fields/joomla-image-select-es5.js', - '/media/system/js/fields/joomla-image-select-es5.min.js', - '/media/system/js/fields/joomla-image-select-es5.min.js.gz', - '/media/system/js/fields/joomla-image-select.js', - '/media/system/js/fields/joomla-image-select.min.js', - '/media/system/js/fields/joomla-image-select.min.js.gz', - // 4.0 from RC 4 to RC 5 - '/media/system/js/fields/calendar-locales/af.min.js', - '/media/system/js/fields/calendar-locales/af.min.js.gz', - '/media/system/js/fields/calendar-locales/ar.min.js', - '/media/system/js/fields/calendar-locales/ar.min.js.gz', - '/media/system/js/fields/calendar-locales/bg.min.js', - '/media/system/js/fields/calendar-locales/bg.min.js.gz', - '/media/system/js/fields/calendar-locales/bn.min.js', - '/media/system/js/fields/calendar-locales/bn.min.js.gz', - '/media/system/js/fields/calendar-locales/bs.min.js', - '/media/system/js/fields/calendar-locales/bs.min.js.gz', - '/media/system/js/fields/calendar-locales/ca.min.js', - '/media/system/js/fields/calendar-locales/ca.min.js.gz', - '/media/system/js/fields/calendar-locales/cs.min.js', - '/media/system/js/fields/calendar-locales/cs.min.js.gz', - '/media/system/js/fields/calendar-locales/cy.min.js', - '/media/system/js/fields/calendar-locales/cy.min.js.gz', - '/media/system/js/fields/calendar-locales/da.min.js', - '/media/system/js/fields/calendar-locales/da.min.js.gz', - '/media/system/js/fields/calendar-locales/de.min.js', - '/media/system/js/fields/calendar-locales/de.min.js.gz', - '/media/system/js/fields/calendar-locales/el.min.js', - '/media/system/js/fields/calendar-locales/el.min.js.gz', - '/media/system/js/fields/calendar-locales/en.min.js', - '/media/system/js/fields/calendar-locales/en.min.js.gz', - '/media/system/js/fields/calendar-locales/es.min.js', - '/media/system/js/fields/calendar-locales/es.min.js.gz', - '/media/system/js/fields/calendar-locales/eu.min.js', - '/media/system/js/fields/calendar-locales/eu.min.js.gz', - '/media/system/js/fields/calendar-locales/fa-ir.min.js', - '/media/system/js/fields/calendar-locales/fa-ir.min.js.gz', - '/media/system/js/fields/calendar-locales/fi.min.js', - '/media/system/js/fields/calendar-locales/fi.min.js.gz', - '/media/system/js/fields/calendar-locales/fr.min.js', - '/media/system/js/fields/calendar-locales/fr.min.js.gz', - '/media/system/js/fields/calendar-locales/ga.min.js', - '/media/system/js/fields/calendar-locales/ga.min.js.gz', - '/media/system/js/fields/calendar-locales/hr.min.js', - '/media/system/js/fields/calendar-locales/hr.min.js.gz', - '/media/system/js/fields/calendar-locales/hu.min.js', - '/media/system/js/fields/calendar-locales/hu.min.js.gz', - '/media/system/js/fields/calendar-locales/it.min.js', - '/media/system/js/fields/calendar-locales/it.min.js.gz', - '/media/system/js/fields/calendar-locales/ja.min.js', - '/media/system/js/fields/calendar-locales/ja.min.js.gz', - '/media/system/js/fields/calendar-locales/ka.min.js', - '/media/system/js/fields/calendar-locales/ka.min.js.gz', - '/media/system/js/fields/calendar-locales/kk.min.js', - '/media/system/js/fields/calendar-locales/kk.min.js.gz', - '/media/system/js/fields/calendar-locales/ko.min.js', - '/media/system/js/fields/calendar-locales/ko.min.js.gz', - '/media/system/js/fields/calendar-locales/lt.min.js', - '/media/system/js/fields/calendar-locales/lt.min.js.gz', - '/media/system/js/fields/calendar-locales/mk.min.js', - '/media/system/js/fields/calendar-locales/mk.min.js.gz', - '/media/system/js/fields/calendar-locales/nb.min.js', - '/media/system/js/fields/calendar-locales/nb.min.js.gz', - '/media/system/js/fields/calendar-locales/nl.min.js', - '/media/system/js/fields/calendar-locales/nl.min.js.gz', - '/media/system/js/fields/calendar-locales/pl.min.js', - '/media/system/js/fields/calendar-locales/pl.min.js.gz', - '/media/system/js/fields/calendar-locales/prs-af.min.js', - '/media/system/js/fields/calendar-locales/prs-af.min.js.gz', - '/media/system/js/fields/calendar-locales/pt.min.js', - '/media/system/js/fields/calendar-locales/pt.min.js.gz', - '/media/system/js/fields/calendar-locales/ru.min.js', - '/media/system/js/fields/calendar-locales/ru.min.js.gz', - '/media/system/js/fields/calendar-locales/sk.min.js', - '/media/system/js/fields/calendar-locales/sk.min.js.gz', - '/media/system/js/fields/calendar-locales/sl.min.js', - '/media/system/js/fields/calendar-locales/sl.min.js.gz', - '/media/system/js/fields/calendar-locales/sr-rs.min.js', - '/media/system/js/fields/calendar-locales/sr-rs.min.js.gz', - '/media/system/js/fields/calendar-locales/sr-yu.min.js', - '/media/system/js/fields/calendar-locales/sr-yu.min.js.gz', - '/media/system/js/fields/calendar-locales/sv.min.js', - '/media/system/js/fields/calendar-locales/sv.min.js.gz', - '/media/system/js/fields/calendar-locales/sw.min.js', - '/media/system/js/fields/calendar-locales/sw.min.js.gz', - '/media/system/js/fields/calendar-locales/ta.min.js', - '/media/system/js/fields/calendar-locales/ta.min.js.gz', - '/media/system/js/fields/calendar-locales/th.min.js', - '/media/system/js/fields/calendar-locales/th.min.js.gz', - '/media/system/js/fields/calendar-locales/uk.min.js', - '/media/system/js/fields/calendar-locales/uk.min.js.gz', - '/media/system/js/fields/calendar-locales/zh-CN.min.js', - '/media/system/js/fields/calendar-locales/zh-CN.min.js.gz', - '/media/system/js/fields/calendar-locales/zh-TW.min.js', - '/media/system/js/fields/calendar-locales/zh-TW.min.js.gz', - // 4.0 from RC 5 to RC 6 - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.min.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.min.js.gz', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.min.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.min.js.gz', - '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.css', - '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.min.css', - '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.min.css.gz', - '/templates/cassiopeia/scss/vendor/fontawesome-free/fontawesome.scss', - // 4.0 from RC 6 to 4.0.0 (stable) - '/libraries/vendor/algo26-matthias/idna-convert/tests/integration/ToIdnTest.php', - '/libraries/vendor/algo26-matthias/idna-convert/tests/integration/ToUnicodeTest.php', - '/libraries/vendor/algo26-matthias/idna-convert/tests/unit/.gitkeep', - '/libraries/vendor/algo26-matthias/idna-convert/tests/unit/namePrepTest.php', - '/libraries/vendor/doctrine/inflector/docs/en/index.rst', - '/libraries/vendor/jakeasmith/http_build_url/tests/HttpBuildUrlTest.php', - '/libraries/vendor/jakeasmith/http_build_url/tests/bootstrap.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/AcceptLanguageTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/AcceptTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/BaseAcceptTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/CharsetNegotiatorTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/EncodingNegotiatorTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/LanguageNegotiatorTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/MatchTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/NegotiatorTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/TestCase.php', - '/libraries/vendor/willdurand/negotiation/tests/bootstrap.php', - // From 4.0.2 to 4.0.3 - '/templates/cassiopeia/css/global/fonts-web_fira-sans.css', - '/templates/cassiopeia/css/global/fonts-web_fira-sans.min.css', - '/templates/cassiopeia/css/global/fonts-web_fira-sans.min.css.gz', - '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.css', - '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.min.css', - '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.min.css.gz', - '/templates/cassiopeia/scss/global/fonts-web_fira-sans.scss', - '/templates/cassiopeia/scss/global/fonts-web_roboto+noto-sans.scss', - // From 4.0.3 to 4.0.4 - '/administrator/templates/atum/scss/_mixin.scss', - '/media/com_joomlaupdate/js/encryption.min.js.gz', - '/media/com_joomlaupdate/js/update.min.js.gz', - '/templates/cassiopeia/images/system/sort_asc.png', - '/templates/cassiopeia/images/system/sort_desc.png', - // From 4.0.4 to 4.0.5 - '/media/vendor/codemirror/lib/#codemirror.js#', - // From 4.0.5 to 4.0.6 - '/media/vendor/mediaelement/css/mejs-controls.png', - // From 4.0.x to 4.1.0-beta1 - '/administrator/templates/atum/css/system/searchtools/searchtools.css', - '/administrator/templates/atum/css/system/searchtools/searchtools.min.css', - '/administrator/templates/atum/css/system/searchtools/searchtools.min.css.gz', - '/administrator/templates/atum/css/template-rtl.css', - '/administrator/templates/atum/css/template-rtl.min.css', - '/administrator/templates/atum/css/template-rtl.min.css.gz', - '/administrator/templates/atum/css/template.css', - '/administrator/templates/atum/css/template.min.css', - '/administrator/templates/atum/css/template.min.css.gz', - '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.css', - '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.min.css', - '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.min.css.gz', - '/administrator/templates/atum/css/vendor/choicesjs/choices.css', - '/administrator/templates/atum/css/vendor/choicesjs/choices.min.css', - '/administrator/templates/atum/css/vendor/choicesjs/choices.min.css.gz', - '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.css', - '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.min.css', - '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.min.css.gz', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.css', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.min.css', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.min.css.gz', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.css', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.min.css', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.min.css.gz', - '/administrator/templates/atum/css/vendor/minicolors/minicolors.css', - '/administrator/templates/atum/css/vendor/minicolors/minicolors.min.css', - '/administrator/templates/atum/css/vendor/minicolors/minicolors.min.css.gz', - '/administrator/templates/atum/images/joomla-pattern.svg', - '/administrator/templates/atum/images/logos/brand-large.svg', - '/administrator/templates/atum/images/logos/brand-small.svg', - '/administrator/templates/atum/images/logos/login.svg', - '/administrator/templates/atum/images/select-bg-active-rtl.svg', - '/administrator/templates/atum/images/select-bg-active.svg', - '/administrator/templates/atum/images/select-bg-rtl.svg', - '/administrator/templates/atum/images/select-bg.svg', - '/administrator/templates/atum/scss/_root.scss', - '/administrator/templates/atum/scss/_variables.scss', - '/administrator/templates/atum/scss/blocks/_alerts.scss', - '/administrator/templates/atum/scss/blocks/_edit.scss', - '/administrator/templates/atum/scss/blocks/_form.scss', - '/administrator/templates/atum/scss/blocks/_global.scss', - '/administrator/templates/atum/scss/blocks/_header.scss', - '/administrator/templates/atum/scss/blocks/_icons.scss', - '/administrator/templates/atum/scss/blocks/_iframe.scss', - '/administrator/templates/atum/scss/blocks/_layout.scss', - '/administrator/templates/atum/scss/blocks/_lists.scss', - '/administrator/templates/atum/scss/blocks/_login.scss', - '/administrator/templates/atum/scss/blocks/_modals.scss', - '/administrator/templates/atum/scss/blocks/_quickicons.scss', - '/administrator/templates/atum/scss/blocks/_sidebar-nav.scss', - '/administrator/templates/atum/scss/blocks/_sidebar.scss', - '/administrator/templates/atum/scss/blocks/_switcher.scss', - '/administrator/templates/atum/scss/blocks/_toolbar.scss', - '/administrator/templates/atum/scss/blocks/_treeselect.scss', - '/administrator/templates/atum/scss/blocks/_utilities.scss', - '/administrator/templates/atum/scss/pages/_com_config.scss', - '/administrator/templates/atum/scss/pages/_com_content.scss', - '/administrator/templates/atum/scss/pages/_com_cpanel.scss', - '/administrator/templates/atum/scss/pages/_com_joomlaupdate.scss', - '/administrator/templates/atum/scss/pages/_com_modules.scss', - '/administrator/templates/atum/scss/pages/_com_privacy.scss', - '/administrator/templates/atum/scss/pages/_com_tags.scss', - '/administrator/templates/atum/scss/pages/_com_templates.scss', - '/administrator/templates/atum/scss/pages/_com_users.scss', - '/administrator/templates/atum/scss/system/searchtools/searchtools.scss', - '/administrator/templates/atum/scss/template-rtl.scss', - '/administrator/templates/atum/scss/template.scss', - '/administrator/templates/atum/scss/vendor/_bootstrap.scss', - '/administrator/templates/atum/scss/vendor/_codemirror.scss', - '/administrator/templates/atum/scss/vendor/_dragula.scss', - '/administrator/templates/atum/scss/vendor/_tinymce.scss', - '/administrator/templates/atum/scss/vendor/awesomplete/awesomplete.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_badge.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_bootstrap-rtl.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_buttons.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_card.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_collapse.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_custom-forms.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_dropdown.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_form.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_lists.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_modal.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_pagination.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_reboot.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_table.scss', - '/administrator/templates/atum/scss/vendor/choicesjs/choices.scss', - '/administrator/templates/atum/scss/vendor/fontawesome-free/fontawesome.scss', - '/administrator/templates/atum/scss/vendor/joomla-custom-elements/joomla-alert.scss', - '/administrator/templates/atum/scss/vendor/joomla-custom-elements/joomla-tab.scss', - '/administrator/templates/atum/scss/vendor/minicolors/minicolors.scss', - '/administrator/templates/atum/template_preview.png', - '/administrator/templates/atum/template_thumbnail.png', - '/administrator/templates/system/css/error.css', - '/administrator/templates/system/css/error.min.css', - '/administrator/templates/system/css/error.min.css.gz', - '/administrator/templates/system/css/system.css', - '/administrator/templates/system/css/system.min.css', - '/administrator/templates/system/css/system.min.css.gz', - '/administrator/templates/system/images/calendar.png', - '/administrator/templates/system/scss/error.scss', - '/administrator/templates/system/scss/system.scss', - '/templates/cassiopeia/css/editor.css', - '/templates/cassiopeia/css/editor.min.css', - '/templates/cassiopeia/css/editor.min.css.gz', - '/templates/cassiopeia/css/global/colors_alternative.css', - '/templates/cassiopeia/css/global/colors_alternative.min.css', - '/templates/cassiopeia/css/global/colors_alternative.min.css.gz', - '/templates/cassiopeia/css/global/colors_standard.css', - '/templates/cassiopeia/css/global/colors_standard.min.css', - '/templates/cassiopeia/css/global/colors_standard.min.css.gz', - '/templates/cassiopeia/css/global/fonts-local_roboto.css', - '/templates/cassiopeia/css/global/fonts-local_roboto.min.css', - '/templates/cassiopeia/css/global/fonts-local_roboto.min.css.gz', - '/templates/cassiopeia/css/offline.css', - '/templates/cassiopeia/css/offline.min.css', - '/templates/cassiopeia/css/offline.min.css.gz', - '/templates/cassiopeia/css/system/searchtools/searchtools.css', - '/templates/cassiopeia/css/system/searchtools/searchtools.min.css', - '/templates/cassiopeia/css/system/searchtools/searchtools.min.css.gz', - '/templates/cassiopeia/css/template-rtl.css', - '/templates/cassiopeia/css/template-rtl.min.css', - '/templates/cassiopeia/css/template-rtl.min.css.gz', - '/templates/cassiopeia/css/template.css', - '/templates/cassiopeia/css/template.min.css', - '/templates/cassiopeia/css/template.min.css.gz', - '/templates/cassiopeia/css/vendor/choicesjs/choices.css', - '/templates/cassiopeia/css/vendor/choicesjs/choices.min.css', - '/templates/cassiopeia/css/vendor/choicesjs/choices.min.css.gz', - '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.css', - '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.min.css', - '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.min.css.gz', - '/templates/cassiopeia/images/logo.svg', - '/templates/cassiopeia/images/select-bg-active-rtl.svg', - '/templates/cassiopeia/images/select-bg-active.svg', - '/templates/cassiopeia/images/select-bg-rtl.svg', - '/templates/cassiopeia/images/select-bg.svg', - '/templates/cassiopeia/js/template.es5.js', - '/templates/cassiopeia/js/template.js', - '/templates/cassiopeia/js/template.min.js', - '/templates/cassiopeia/js/template.min.js.gz', - '/templates/cassiopeia/scss/blocks/_alerts.scss', - '/templates/cassiopeia/scss/blocks/_back-to-top.scss', - '/templates/cassiopeia/scss/blocks/_banner.scss', - '/templates/cassiopeia/scss/blocks/_css-grid.scss', - '/templates/cassiopeia/scss/blocks/_footer.scss', - '/templates/cassiopeia/scss/blocks/_form.scss', - '/templates/cassiopeia/scss/blocks/_frontend-edit.scss', - '/templates/cassiopeia/scss/blocks/_global.scss', - '/templates/cassiopeia/scss/blocks/_header.scss', - '/templates/cassiopeia/scss/blocks/_icons.scss', - '/templates/cassiopeia/scss/blocks/_iframe.scss', - '/templates/cassiopeia/scss/blocks/_layout.scss', - '/templates/cassiopeia/scss/blocks/_legacy.scss', - '/templates/cassiopeia/scss/blocks/_modals.scss', - '/templates/cassiopeia/scss/blocks/_modifiers.scss', - '/templates/cassiopeia/scss/blocks/_tags.scss', - '/templates/cassiopeia/scss/blocks/_toolbar.scss', - '/templates/cassiopeia/scss/blocks/_utilities.scss', - '/templates/cassiopeia/scss/editor.scss', - '/templates/cassiopeia/scss/global/colors_alternative.scss', - '/templates/cassiopeia/scss/global/colors_standard.scss', - '/templates/cassiopeia/scss/global/fonts-local_roboto.scss', - '/templates/cassiopeia/scss/offline.scss', - '/templates/cassiopeia/scss/system/searchtools/searchtools.scss', - '/templates/cassiopeia/scss/template-rtl.scss', - '/templates/cassiopeia/scss/template.scss', - '/templates/cassiopeia/scss/tools/_tools.scss', - '/templates/cassiopeia/scss/tools/functions/_max-width.scss', - '/templates/cassiopeia/scss/tools/variables/_variables.scss', - '/templates/cassiopeia/scss/vendor/_awesomplete.scss', - '/templates/cassiopeia/scss/vendor/_chosen.scss', - '/templates/cassiopeia/scss/vendor/_dragula.scss', - '/templates/cassiopeia/scss/vendor/_minicolors.scss', - '/templates/cassiopeia/scss/vendor/_tinymce.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_bootstrap-rtl.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_buttons.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_collapse.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_custom-forms.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_dropdown.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_forms.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_lists.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_modal.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_nav.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_pagination.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_table.scss', - '/templates/cassiopeia/scss/vendor/choicesjs/choices.scss', - '/templates/cassiopeia/scss/vendor/joomla-custom-elements/joomla-alert.scss', - '/templates/cassiopeia/scss/vendor/metismenu/_metismenu.scss', - '/templates/cassiopeia/template_preview.png', - '/templates/cassiopeia/template_thumbnail.png', - '/templates/system/css/editor.css', - '/templates/system/css/editor.min.css', - '/templates/system/css/editor.min.css.gz', - '/templates/system/css/error.css', - '/templates/system/css/error.min.css', - '/templates/system/css/error.min.css.gz', - '/templates/system/css/error_rtl.css', - '/templates/system/css/error_rtl.min.css', - '/templates/system/css/error_rtl.min.css.gz', - '/templates/system/css/general.css', - '/templates/system/css/general.min.css', - '/templates/system/css/general.min.css.gz', - '/templates/system/css/offline.css', - '/templates/system/css/offline.min.css', - '/templates/system/css/offline.min.css.gz', - '/templates/system/css/offline_rtl.css', - '/templates/system/css/offline_rtl.min.css', - '/templates/system/css/offline_rtl.min.css.gz', - '/templates/system/scss/editor.scss', - '/templates/system/scss/error.scss', - '/templates/system/scss/error_rtl.scss', - '/templates/system/scss/general.scss', - '/templates/system/scss/offline.scss', - '/templates/system/scss/offline_rtl.scss', - // From 4.1.0-beta3 to 4.1.0-rc1 - '/api/components/com_media/src/Helper/AdapterTrait.php', - // From 4.1.0 to 4.1.1 - '/libraries/vendor/tobscure/json-api/.git/HEAD', - '/libraries/vendor/tobscure/json-api/.git/ORIG_HEAD', - '/libraries/vendor/tobscure/json-api/.git/config', - '/libraries/vendor/tobscure/json-api/.git/description', - '/libraries/vendor/tobscure/json-api/.git/hooks/applypatch-msg.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/commit-msg.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/fsmonitor-watchman.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/post-update.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-applypatch.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-commit.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-merge-commit.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-push.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-rebase.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-receive.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/prepare-commit-msg.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/push-to-checkout.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/update.sample', - '/libraries/vendor/tobscure/json-api/.git/index', - '/libraries/vendor/tobscure/json-api/.git/info/exclude', - '/libraries/vendor/tobscure/json-api/.git/info/refs', - '/libraries/vendor/tobscure/json-api/.git/logs/HEAD', - '/libraries/vendor/tobscure/json-api/.git/logs/refs/heads/joomla-backports', - '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes/origin/HEAD', - '/libraries/vendor/tobscure/json-api/.git/objects/info/packs', - '/libraries/vendor/tobscure/json-api/.git/objects/pack/pack-51530cba04703b17f3c11b9e8458a171092cf5e3.idx', - '/libraries/vendor/tobscure/json-api/.git/objects/pack/pack-51530cba04703b17f3c11b9e8458a171092cf5e3.pack', - '/libraries/vendor/tobscure/json-api/.git/packed-refs', - '/libraries/vendor/tobscure/json-api/.git/refs/heads/joomla-backports', - '/libraries/vendor/tobscure/json-api/.git/refs/remotes/origin/HEAD', - '/libraries/vendor/tobscure/json-api/.php_cs', - '/libraries/vendor/tobscure/json-api/tests/AbstractSerializerTest.php', - '/libraries/vendor/tobscure/json-api/tests/AbstractTestCase.php', - '/libraries/vendor/tobscure/json-api/tests/CollectionTest.php', - '/libraries/vendor/tobscure/json-api/tests/DocumentTest.php', - '/libraries/vendor/tobscure/json-api/tests/ErrorHandlerTest.php', - '/libraries/vendor/tobscure/json-api/tests/Exception/Handler/FallbackExceptionHandlerTest.php', - '/libraries/vendor/tobscure/json-api/tests/Exception/Handler/InvalidParameterExceptionHandlerTest.php', - '/libraries/vendor/tobscure/json-api/tests/LinksTraitTest.php', - '/libraries/vendor/tobscure/json-api/tests/ParametersTest.php', - '/libraries/vendor/tobscure/json-api/tests/ResourceTest.php', - '/libraries/vendor/tobscure/json-api/tests/UtilTest.php', - // From 4.1.1 to 4.1.2 - '/administrator/components/com_users/src/Field/PrimaryauthprovidersField.php', - // From 4.1.2 to 4.1.3 - '/libraries/vendor/webmozart/assert/.php_cs', - // From 4.1.3 to 4.1.4 - '/libraries/vendor/maximebf/debugbar/.bowerrc', - '/libraries/vendor/maximebf/debugbar/bower.json', - '/libraries/vendor/maximebf/debugbar/build/namespaceFontAwesome.php', - '/libraries/vendor/maximebf/debugbar/demo/ajax.php', - '/libraries/vendor/maximebf/debugbar/demo/ajax_exception.php', - '/libraries/vendor/maximebf/debugbar/demo/bootstrap.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/cachecache/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/bootstrap.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/build.sh', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/cli-config.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src/Demo/Product.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/monolog/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/build.properties', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/build.sh', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/runtime-conf.xml', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/schema.xml', - '/libraries/vendor/maximebf/debugbar/demo/bridge/slim/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/swiftmailer/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/foobar.html', - '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/hello.html', - '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/index.php', - '/libraries/vendor/maximebf/debugbar/demo/dump_assets.php', - '/libraries/vendor/maximebf/debugbar/demo/index.php', - '/libraries/vendor/maximebf/debugbar/demo/open.php', - '/libraries/vendor/maximebf/debugbar/demo/pdo.php', - '/libraries/vendor/maximebf/debugbar/demo/stack.php', - '/libraries/vendor/maximebf/debugbar/docs/ajax_and_stack.md', - '/libraries/vendor/maximebf/debugbar/docs/base_collectors.md', - '/libraries/vendor/maximebf/debugbar/docs/bridge_collectors.md', - '/libraries/vendor/maximebf/debugbar/docs/data_collectors.md', - '/libraries/vendor/maximebf/debugbar/docs/data_formatter.md', - '/libraries/vendor/maximebf/debugbar/docs/http_drivers.md', - '/libraries/vendor/maximebf/debugbar/docs/javascript_bar.md', - '/libraries/vendor/maximebf/debugbar/docs/manifest.json', - '/libraries/vendor/maximebf/debugbar/docs/openhandler.md', - '/libraries/vendor/maximebf/debugbar/docs/rendering.md', - '/libraries/vendor/maximebf/debugbar/docs/screenshot.png', - '/libraries/vendor/maximebf/debugbar/docs/storage.md', - '/libraries/vendor/maximebf/debugbar/docs/style.css', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/AggregatedCollectorTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/ConfigCollectorTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/MessagesCollectorTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/MockCollector.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/Propel2CollectorTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/TimeDataCollectorTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter/DataFormatterTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter/DebugBarVarDumperTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DebugBarTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DebugBarTestCase.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/JavascriptRendererTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/MockHttpDriver.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/OpenHandlerTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage/FileStorageTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage/MockStorage.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/TracedStatementTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/full_init.html', - '/libraries/vendor/maximebf/debugbar/tests/bootstrap.php', - // From 4.1 to 4.2.0-beta1 - '/libraries/src/Service/Provider/ApiRouter.php', - '/libraries/vendor/nyholm/psr7/doc/final.md', - '/media/com_finder/js/index-es5.js', - '/media/com_finder/js/index-es5.min.js', - '/media/com_finder/js/index-es5.min.js.gz', - '/media/com_finder/js/index.js', - '/media/com_finder/js/index.min.js', - '/media/com_finder/js/index.min.js.gz', - '/media/com_users/js/two-factor-switcher-es5.js', - '/media/com_users/js/two-factor-switcher-es5.min.js', - '/media/com_users/js/two-factor-switcher-es5.min.js.gz', - '/media/com_users/js/two-factor-switcher.js', - '/media/com_users/js/two-factor-switcher.min.js', - '/media/com_users/js/two-factor-switcher.min.js.gz', - '/modules/mod_articles_news/mod_articles_news.php', - '/plugins/actionlog/joomla/joomla.php', - '/plugins/api-authentication/basic/basic.php', - '/plugins/api-authentication/token/token.php', - '/plugins/system/cache/cache.php', - '/plugins/twofactorauth/totp/postinstall/actions.php', - '/plugins/twofactorauth/totp/tmpl/form.php', - '/plugins/twofactorauth/totp/totp.php', - '/plugins/twofactorauth/totp/totp.xml', - '/plugins/twofactorauth/yubikey/tmpl/form.php', - '/plugins/twofactorauth/yubikey/yubikey.php', - '/plugins/twofactorauth/yubikey/yubikey.xml', - // From 4.2.0-beta1 to 4.2.0-beta2 - '/layouts/plugins/user/profile/fields/dob.php', - '/modules/mod_articles_latest/mod_articles_latest.php', - '/plugins/behaviour/taggable/taggable.php', - '/plugins/behaviour/versionable/versionable.php', - '/plugins/task/requests/requests.php', - '/plugins/task/sitestatus/sitestatus.php', - '/plugins/user/profile/src/Field/DobField.php', - ); - - $folders = array( - // From 3.10 to 4.1 - '/templates/system/images', - '/templates/system/html', - '/templates/protostar/less', - '/templates/protostar/language/en-GB', - '/templates/protostar/language', - '/templates/protostar/js', - '/templates/protostar/img', - '/templates/protostar/images/system', - '/templates/protostar/images', - '/templates/protostar/html/layouts/joomla/system', - '/templates/protostar/html/layouts/joomla/form/field', - '/templates/protostar/html/layouts/joomla/form', - '/templates/protostar/html/layouts/joomla', - '/templates/protostar/html/layouts', - '/templates/protostar/html/com_media/imageslist', - '/templates/protostar/html/com_media', - '/templates/protostar/html', - '/templates/protostar/css', - '/templates/protostar', - '/templates/beez3/language/en-GB', - '/templates/beez3/language', - '/templates/beez3/javascript', - '/templates/beez3/images/system', - '/templates/beez3/images/personal', - '/templates/beez3/images/nature', - '/templates/beez3/images', - '/templates/beez3/html/mod_login', - '/templates/beez3/html/mod_languages', - '/templates/beez3/html/mod_breadcrumbs', - '/templates/beez3/html/layouts/joomla/system', - '/templates/beez3/html/layouts/joomla', - '/templates/beez3/html/layouts', - '/templates/beez3/html/com_weblinks/form', - '/templates/beez3/html/com_weblinks/category', - '/templates/beez3/html/com_weblinks/categories', - '/templates/beez3/html/com_weblinks', - '/templates/beez3/html/com_newsfeeds/category', - '/templates/beez3/html/com_newsfeeds/categories', - '/templates/beez3/html/com_newsfeeds', - '/templates/beez3/html/com_content/form', - '/templates/beez3/html/com_content/featured', - '/templates/beez3/html/com_content/category', - '/templates/beez3/html/com_content/categories', - '/templates/beez3/html/com_content/article', - '/templates/beez3/html/com_content/archive', - '/templates/beez3/html/com_content', - '/templates/beez3/html/com_contact/contact', - '/templates/beez3/html/com_contact/category', - '/templates/beez3/html/com_contact/categories', - '/templates/beez3/html/com_contact', - '/templates/beez3/html', - '/templates/beez3/css', - '/templates/beez3', - '/plugins/user/terms/terms', - '/plugins/user/terms/field', - '/plugins/user/profile/profiles', - '/plugins/user/profile/field', - '/plugins/system/stats/field', - '/plugins/system/privacyconsent/privacyconsent', - '/plugins/system/privacyconsent/field', - '/plugins/system/p3p', - '/plugins/system/languagecode/language/en-GB', - '/plugins/system/languagecode/language', - '/plugins/editors/tinymce/form', - '/plugins/editors/tinymce/field', - '/plugins/content/confirmconsent/fields', - '/plugins/captcha/recaptcha/postinstall', - '/plugins/authentication/gmail', - '/media/plg_twofactorauth_totp/js', - '/media/plg_twofactorauth_totp', - '/media/plg_system_highlight', - '/media/overrider/js', - '/media/overrider/css', - '/media/overrider', - '/media/media/js', - '/media/media/images/mime-icon-32', - '/media/media/images/mime-icon-16', - '/media/media/images', - '/media/media/css', - '/media/media', - '/media/jui/less', - '/media/jui/js', - '/media/jui/img', - '/media/jui/images', - '/media/jui/fonts', - '/media/jui/css', - '/media/jui', - '/media/editors/tinymce/themes/modern', - '/media/editors/tinymce/themes', - '/media/editors/tinymce/templates', - '/media/editors/tinymce/skins/lightgray/img', - '/media/editors/tinymce/skins/lightgray/fonts', - '/media/editors/tinymce/skins/lightgray', - '/media/editors/tinymce/skins', - '/media/editors/tinymce/plugins/wordcount', - '/media/editors/tinymce/plugins/visualchars', - '/media/editors/tinymce/plugins/visualblocks/css', - '/media/editors/tinymce/plugins/visualblocks', - '/media/editors/tinymce/plugins/toc', - '/media/editors/tinymce/plugins/textpattern', - '/media/editors/tinymce/plugins/textcolor', - '/media/editors/tinymce/plugins/template', - '/media/editors/tinymce/plugins/table', - '/media/editors/tinymce/plugins/tabfocus', - '/media/editors/tinymce/plugins/spellchecker', - '/media/editors/tinymce/plugins/searchreplace', - '/media/editors/tinymce/plugins/save', - '/media/editors/tinymce/plugins/print', - '/media/editors/tinymce/plugins/preview', - '/media/editors/tinymce/plugins/paste', - '/media/editors/tinymce/plugins/pagebreak', - '/media/editors/tinymce/plugins/noneditable', - '/media/editors/tinymce/plugins/nonbreaking', - '/media/editors/tinymce/plugins/media', - '/media/editors/tinymce/plugins/lists', - '/media/editors/tinymce/plugins/link', - '/media/editors/tinymce/plugins/legacyoutput', - '/media/editors/tinymce/plugins/layer', - '/media/editors/tinymce/plugins/insertdatetime', - '/media/editors/tinymce/plugins/importcss', - '/media/editors/tinymce/plugins/imagetools', - '/media/editors/tinymce/plugins/image', - '/media/editors/tinymce/plugins/hr', - '/media/editors/tinymce/plugins/fullscreen', - '/media/editors/tinymce/plugins/fullpage', - '/media/editors/tinymce/plugins/example_dependency', - '/media/editors/tinymce/plugins/example', - '/media/editors/tinymce/plugins/emoticons/img', - '/media/editors/tinymce/plugins/emoticons', - '/media/editors/tinymce/plugins/directionality', - '/media/editors/tinymce/plugins/contextmenu', - '/media/editors/tinymce/plugins/colorpicker', - '/media/editors/tinymce/plugins/codesample/css', - '/media/editors/tinymce/plugins/codesample', - '/media/editors/tinymce/plugins/code', - '/media/editors/tinymce/plugins/charmap', - '/media/editors/tinymce/plugins/bbcode', - '/media/editors/tinymce/plugins/autosave', - '/media/editors/tinymce/plugins/autoresize', - '/media/editors/tinymce/plugins/autolink', - '/media/editors/tinymce/plugins/anchor', - '/media/editors/tinymce/plugins/advlist', - '/media/editors/tinymce/plugins', - '/media/editors/tinymce/langs', - '/media/editors/tinymce/js/plugins/dragdrop', - '/media/editors/tinymce/js/plugins', - '/media/editors/tinymce/js', - '/media/editors/tinymce', - '/media/editors/none/js', - '/media/editors/none', - '/media/editors/codemirror/theme', - '/media/editors/codemirror/mode/z80', - '/media/editors/codemirror/mode/yaml-frontmatter', - '/media/editors/codemirror/mode/yaml', - '/media/editors/codemirror/mode/yacas', - '/media/editors/codemirror/mode/xquery', - '/media/editors/codemirror/mode/xml', - '/media/editors/codemirror/mode/webidl', - '/media/editors/codemirror/mode/wast', - '/media/editors/codemirror/mode/vue', - '/media/editors/codemirror/mode/vhdl', - '/media/editors/codemirror/mode/verilog', - '/media/editors/codemirror/mode/velocity', - '/media/editors/codemirror/mode/vbscript', - '/media/editors/codemirror/mode/vb', - '/media/editors/codemirror/mode/twig', - '/media/editors/codemirror/mode/turtle', - '/media/editors/codemirror/mode/ttcn-cfg', - '/media/editors/codemirror/mode/ttcn', - '/media/editors/codemirror/mode/troff', - '/media/editors/codemirror/mode/tornado', - '/media/editors/codemirror/mode/toml', - '/media/editors/codemirror/mode/tiki', - '/media/editors/codemirror/mode/tiddlywiki', - '/media/editors/codemirror/mode/textile', - '/media/editors/codemirror/mode/tcl', - '/media/editors/codemirror/mode/swift', - '/media/editors/codemirror/mode/stylus', - '/media/editors/codemirror/mode/stex', - '/media/editors/codemirror/mode/sql', - '/media/editors/codemirror/mode/spreadsheet', - '/media/editors/codemirror/mode/sparql', - '/media/editors/codemirror/mode/soy', - '/media/editors/codemirror/mode/solr', - '/media/editors/codemirror/mode/smarty', - '/media/editors/codemirror/mode/smalltalk', - '/media/editors/codemirror/mode/slim', - '/media/editors/codemirror/mode/sieve', - '/media/editors/codemirror/mode/shell', - '/media/editors/codemirror/mode/scheme', - '/media/editors/codemirror/mode/sass', - '/media/editors/codemirror/mode/sas', - '/media/editors/codemirror/mode/rust', - '/media/editors/codemirror/mode/ruby', - '/media/editors/codemirror/mode/rst', - '/media/editors/codemirror/mode/rpm/changes', - '/media/editors/codemirror/mode/rpm', - '/media/editors/codemirror/mode/r', - '/media/editors/codemirror/mode/q', - '/media/editors/codemirror/mode/python', - '/media/editors/codemirror/mode/puppet', - '/media/editors/codemirror/mode/pug', - '/media/editors/codemirror/mode/protobuf', - '/media/editors/codemirror/mode/properties', - '/media/editors/codemirror/mode/powershell', - '/media/editors/codemirror/mode/pig', - '/media/editors/codemirror/mode/php', - '/media/editors/codemirror/mode/perl', - '/media/editors/codemirror/mode/pegjs', - '/media/editors/codemirror/mode/pascal', - '/media/editors/codemirror/mode/oz', - '/media/editors/codemirror/mode/octave', - '/media/editors/codemirror/mode/ntriples', - '/media/editors/codemirror/mode/nsis', - '/media/editors/codemirror/mode/nginx', - '/media/editors/codemirror/mode/mumps', - '/media/editors/codemirror/mode/mscgen', - '/media/editors/codemirror/mode/modelica', - '/media/editors/codemirror/mode/mllike', - '/media/editors/codemirror/mode/mirc', - '/media/editors/codemirror/mode/mbox', - '/media/editors/codemirror/mode/mathematica', - '/media/editors/codemirror/mode/markdown', - '/media/editors/codemirror/mode/lua', - '/media/editors/codemirror/mode/livescript', - '/media/editors/codemirror/mode/julia', - '/media/editors/codemirror/mode/jsx', - '/media/editors/codemirror/mode/jinja2', - '/media/editors/codemirror/mode/javascript', - '/media/editors/codemirror/mode/idl', - '/media/editors/codemirror/mode/http', - '/media/editors/codemirror/mode/htmlmixed', - '/media/editors/codemirror/mode/htmlembedded', - '/media/editors/codemirror/mode/haxe', - '/media/editors/codemirror/mode/haskell-literate', - '/media/editors/codemirror/mode/haskell', - '/media/editors/codemirror/mode/handlebars', - '/media/editors/codemirror/mode/haml', - '/media/editors/codemirror/mode/groovy', - '/media/editors/codemirror/mode/go', - '/media/editors/codemirror/mode/gherkin', - '/media/editors/codemirror/mode/gfm', - '/media/editors/codemirror/mode/gas', - '/media/editors/codemirror/mode/fortran', - '/media/editors/codemirror/mode/forth', - '/media/editors/codemirror/mode/fcl', - '/media/editors/codemirror/mode/factor', - '/media/editors/codemirror/mode/erlang', - '/media/editors/codemirror/mode/elm', - '/media/editors/codemirror/mode/eiffel', - '/media/editors/codemirror/mode/ecl', - '/media/editors/codemirror/mode/ebnf', - '/media/editors/codemirror/mode/dylan', - '/media/editors/codemirror/mode/dtd', - '/media/editors/codemirror/mode/dockerfile', - '/media/editors/codemirror/mode/django', - '/media/editors/codemirror/mode/diff', - '/media/editors/codemirror/mode/dart', - '/media/editors/codemirror/mode/d', - '/media/editors/codemirror/mode/cypher', - '/media/editors/codemirror/mode/css', - '/media/editors/codemirror/mode/crystal', - '/media/editors/codemirror/mode/commonlisp', - '/media/editors/codemirror/mode/coffeescript', - '/media/editors/codemirror/mode/cobol', - '/media/editors/codemirror/mode/cmake', - '/media/editors/codemirror/mode/clojure', - '/media/editors/codemirror/mode/clike', - '/media/editors/codemirror/mode/brainfuck', - '/media/editors/codemirror/mode/asterisk', - '/media/editors/codemirror/mode/asn.1', - '/media/editors/codemirror/mode/asciiarmor', - '/media/editors/codemirror/mode/apl', - '/media/editors/codemirror/mode', - '/media/editors/codemirror/lib', - '/media/editors/codemirror/keymap', - '/media/editors/codemirror/addon/wrap', - '/media/editors/codemirror/addon/tern', - '/media/editors/codemirror/addon/selection', - '/media/editors/codemirror/addon/search', - '/media/editors/codemirror/addon/scroll', - '/media/editors/codemirror/addon/runmode', - '/media/editors/codemirror/addon/mode', - '/media/editors/codemirror/addon/merge', - '/media/editors/codemirror/addon/lint', - '/media/editors/codemirror/addon/hint', - '/media/editors/codemirror/addon/fold', - '/media/editors/codemirror/addon/edit', - '/media/editors/codemirror/addon/display', - '/media/editors/codemirror/addon/dialog', - '/media/editors/codemirror/addon/comment', - '/media/editors/codemirror/addon', - '/media/editors/codemirror', - '/media/editors', - '/media/contacts/images', - '/media/contacts', - '/media/com_contenthistory/css', - '/media/cms/css', - '/media/cms', - '/libraries/vendor/symfony/polyfill-util', - '/libraries/vendor/symfony/polyfill-php71', - '/libraries/vendor/symfony/polyfill-php56', - '/libraries/vendor/symfony/polyfill-php55', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML/Declaration', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parse', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Net', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/HTTP', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode/HTML', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content/Type', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache', - '/libraries/vendor/simplepie/simplepie/library/SimplePie', - '/libraries/vendor/simplepie/simplepie/library', - '/libraries/vendor/simplepie/simplepie/idn', - '/libraries/vendor/simplepie/simplepie', - '/libraries/vendor/simplepie', - '/libraries/vendor/phpmailer/phpmailer/extras', - '/libraries/vendor/paragonie/random_compat/lib', - '/libraries/vendor/leafo/lessphp', - '/libraries/vendor/leafo', - '/libraries/vendor/joomla/session/Joomla/Session/Storage', - '/libraries/vendor/joomla/session/Joomla/Session', - '/libraries/vendor/joomla/session/Joomla', - '/libraries/vendor/joomla/image/src/Filter', - '/libraries/vendor/joomla/image/src', - '/libraries/vendor/joomla/image', - '/libraries/vendor/joomla/compat/src', - '/libraries/vendor/joomla/compat', - '/libraries/vendor/joomla/application/src/Cli/Output/Processor', - '/libraries/vendor/joomla/application/src/Cli/Output', - '/libraries/vendor/joomla/application/src/Cli', - '/libraries/vendor/ircmaxell/password-compat/lib', - '/libraries/vendor/ircmaxell/password-compat', - '/libraries/vendor/ircmaxell', - '/libraries/vendor/brumann/polyfill-unserialize/src', - '/libraries/vendor/brumann/polyfill-unserialize', - '/libraries/vendor/brumann', - '/libraries/src/Table/Observer', - '/libraries/src/Menu/Node', - '/libraries/src/Language/Wrapper', - '/libraries/src/Language/Stemmer', - '/libraries/src/Http/Wrapper', - '/libraries/src/Filter/Wrapper', - '/libraries/src/Filesystem/Wrapper', - '/libraries/src/Crypt/Password', - '/libraries/src/Access/Wrapper', - '/libraries/phputf8/utils', - '/libraries/phputf8/native', - '/libraries/phputf8/mbstring', - '/libraries/phputf8', - '/libraries/legacy/utilities', - '/libraries/legacy/table', - '/libraries/legacy/simplepie', - '/libraries/legacy/simplecrypt', - '/libraries/legacy/response', - '/libraries/legacy/request', - '/libraries/legacy/log', - '/libraries/legacy/form/field', - '/libraries/legacy/form', - '/libraries/legacy/exception', - '/libraries/legacy/error', - '/libraries/legacy/dispatcher', - '/libraries/legacy/database', - '/libraries/legacy/base', - '/libraries/legacy/application', - '/libraries/legacy', - '/libraries/joomla/view', - '/libraries/joomla/utilities', - '/libraries/joomla/twitter', - '/libraries/joomla/string/wrapper', - '/libraries/joomla/string', - '/libraries/joomla/session/storage', - '/libraries/joomla/session/handler', - '/libraries/joomla/session', - '/libraries/joomla/route/wrapper', - '/libraries/joomla/route', - '/libraries/joomla/openstreetmap', - '/libraries/joomla/observer/wrapper', - '/libraries/joomla/observer/updater', - '/libraries/joomla/observer', - '/libraries/joomla/observable', - '/libraries/joomla/oauth2', - '/libraries/joomla/oauth1', - '/libraries/joomla/model', - '/libraries/joomla/mediawiki', - '/libraries/joomla/linkedin', - '/libraries/joomla/keychain', - '/libraries/joomla/grid', - '/libraries/joomla/google/embed', - '/libraries/joomla/google/data/plus', - '/libraries/joomla/google/data/picasa', - '/libraries/joomla/google/data', - '/libraries/joomla/google/auth', - '/libraries/joomla/google', - '/libraries/joomla/github/package/users', - '/libraries/joomla/github/package/repositories', - '/libraries/joomla/github/package/pulls', - '/libraries/joomla/github/package/orgs', - '/libraries/joomla/github/package/issues', - '/libraries/joomla/github/package/gists', - '/libraries/joomla/github/package/data', - '/libraries/joomla/github/package/activity', - '/libraries/joomla/github/package', - '/libraries/joomla/github', - '/libraries/joomla/form/fields', - '/libraries/joomla/form', - '/libraries/joomla/facebook', - '/libraries/joomla/event', - '/libraries/joomla/database/query', - '/libraries/joomla/database/iterator', - '/libraries/joomla/database/importer', - '/libraries/joomla/database/exporter', - '/libraries/joomla/database/exception', - '/libraries/joomla/database/driver', - '/libraries/joomla/database', - '/libraries/joomla/controller', - '/libraries/joomla/archive/wrapper', - '/libraries/joomla/archive', - '/libraries/joomla/application/web/router', - '/libraries/joomla/application/web', - '/libraries/joomla/application', - '/libraries/joomla', - '/libraries/idna_convert', - '/libraries/fof/view', - '/libraries/fof/utils/update', - '/libraries/fof/utils/timer', - '/libraries/fof/utils/phpfunc', - '/libraries/fof/utils/observable', - '/libraries/fof/utils/object', - '/libraries/fof/utils/ip', - '/libraries/fof/utils/installscript', - '/libraries/fof/utils/ini', - '/libraries/fof/utils/filescheck', - '/libraries/fof/utils/config', - '/libraries/fof/utils/cache', - '/libraries/fof/utils/array', - '/libraries/fof/utils', - '/libraries/fof/toolbar', - '/libraries/fof/template', - '/libraries/fof/table/dispatcher', - '/libraries/fof/table/behavior', - '/libraries/fof/table', - '/libraries/fof/string', - '/libraries/fof/render', - '/libraries/fof/query', - '/libraries/fof/platform/filesystem', - '/libraries/fof/platform', - '/libraries/fof/model/field', - '/libraries/fof/model/dispatcher', - '/libraries/fof/model/behavior', - '/libraries/fof/model', - '/libraries/fof/less/parser', - '/libraries/fof/less/formatter', - '/libraries/fof/less', - '/libraries/fof/layout', - '/libraries/fof/integration/joomla/filesystem', - '/libraries/fof/integration/joomla', - '/libraries/fof/integration', - '/libraries/fof/input/jinput', - '/libraries/fof/input', - '/libraries/fof/inflector', - '/libraries/fof/hal/render', - '/libraries/fof/hal', - '/libraries/fof/form/header', - '/libraries/fof/form/field', - '/libraries/fof/form', - '/libraries/fof/encrypt/aes', - '/libraries/fof/encrypt', - '/libraries/fof/download/adapter', - '/libraries/fof/download', - '/libraries/fof/dispatcher', - '/libraries/fof/database/query', - '/libraries/fof/database/iterator', - '/libraries/fof/database/driver', - '/libraries/fof/database', - '/libraries/fof/controller', - '/libraries/fof/config/domain', - '/libraries/fof/config', - '/libraries/fof/autoloader', - '/libraries/fof', - '/libraries/cms/less/formatter', - '/libraries/cms/less', - '/libraries/cms/html/language/en-GB', - '/libraries/cms/html/language', - '/libraries/cms/html', - '/libraries/cms/class', - '/libraries/cms', - '/layouts/libraries/cms/html/bootstrap', - '/layouts/libraries/cms/html', - '/layouts/libraries/cms', - '/layouts/joomla/tinymce/buttons', - '/layouts/joomla/modal', - '/layouts/joomla/html/formbehavior', - '/components/com_wrapper/views/wrapper/tmpl', - '/components/com_wrapper/views/wrapper', - '/components/com_wrapper/views', - '/components/com_users/views/reset/tmpl', - '/components/com_users/views/reset', - '/components/com_users/views/remind/tmpl', - '/components/com_users/views/remind', - '/components/com_users/views/registration/tmpl', - '/components/com_users/views/registration', - '/components/com_users/views/profile/tmpl', - '/components/com_users/views/profile', - '/components/com_users/views/login/tmpl', - '/components/com_users/views/login', - '/components/com_users/views', - '/components/com_users/models/rules', - '/components/com_users/models/forms', - '/components/com_users/models', - '/components/com_users/layouts/joomla/form', - '/components/com_users/layouts/joomla', - '/components/com_users/layouts', - '/components/com_users/helpers/html', - '/components/com_users/helpers', - '/components/com_users/controllers', - '/components/com_tags/views/tags/tmpl', - '/components/com_tags/views/tags', - '/components/com_tags/views/tag/tmpl', - '/components/com_tags/views/tag', - '/components/com_tags/views', - '/components/com_tags/models', - '/components/com_tags/controllers', - '/components/com_privacy/views/request/tmpl', - '/components/com_privacy/views/request', - '/components/com_privacy/views/remind/tmpl', - '/components/com_privacy/views/remind', - '/components/com_privacy/views/confirm/tmpl', - '/components/com_privacy/views/confirm', - '/components/com_privacy/views', - '/components/com_privacy/models/forms', - '/components/com_privacy/models', - '/components/com_privacy/controllers', - '/components/com_newsfeeds/views/newsfeed/tmpl', - '/components/com_newsfeeds/views/newsfeed', - '/components/com_newsfeeds/views/category/tmpl', - '/components/com_newsfeeds/views/category', - '/components/com_newsfeeds/views/categories/tmpl', - '/components/com_newsfeeds/views/categories', - '/components/com_newsfeeds/views', - '/components/com_newsfeeds/models', - '/components/com_modules/models/forms', - '/components/com_modules/models', - '/components/com_menus/models/forms', - '/components/com_menus/models', - '/components/com_mailto/views/sent/tmpl', - '/components/com_mailto/views/sent', - '/components/com_mailto/views/mailto/tmpl', - '/components/com_mailto/views/mailto', - '/components/com_mailto/views', - '/components/com_mailto/models/forms', - '/components/com_mailto/models', - '/components/com_mailto/helpers', - '/components/com_mailto', - '/components/com_finder/views/search/tmpl', - '/components/com_finder/views/search', - '/components/com_finder/views', - '/components/com_finder/models', - '/components/com_finder/helpers/html', - '/components/com_finder/controllers', - '/components/com_fields/models/forms', - '/components/com_fields/models', - '/components/com_content/views/form/tmpl', - '/components/com_content/views/form', - '/components/com_content/views/featured/tmpl', - '/components/com_content/views/featured', - '/components/com_content/views/category/tmpl', - '/components/com_content/views/category', - '/components/com_content/views/categories/tmpl', - '/components/com_content/views/categories', - '/components/com_content/views/article/tmpl', - '/components/com_content/views/article', - '/components/com_content/views/archive/tmpl', - '/components/com_content/views/archive', - '/components/com_content/views', - '/components/com_content/models/forms', - '/components/com_content/models', - '/components/com_content/controllers', - '/components/com_contact/views/featured/tmpl', - '/components/com_contact/views/featured', - '/components/com_contact/views/contact/tmpl', - '/components/com_contact/views/contact', - '/components/com_contact/views/category/tmpl', - '/components/com_contact/views/category', - '/components/com_contact/views/categories/tmpl', - '/components/com_contact/views/categories', - '/components/com_contact/views', - '/components/com_contact/models/rules', - '/components/com_contact/models/forms', - '/components/com_contact/models', - '/components/com_contact/layouts/joomla/form', - '/components/com_contact/layouts/joomla', - '/components/com_contact/controllers', - '/components/com_config/view/templates/tmpl', - '/components/com_config/view/templates', - '/components/com_config/view/modules/tmpl', - '/components/com_config/view/modules', - '/components/com_config/view/config/tmpl', - '/components/com_config/view/config', - '/components/com_config/view/cms', - '/components/com_config/view', - '/components/com_config/model/form', - '/components/com_config/model', - '/components/com_config/controller/templates', - '/components/com_config/controller/modules', - '/components/com_config/controller/config', - '/components/com_config/controller', - '/components/com_banners/models', - '/components/com_banners/helpers', - '/administrator/templates/system/html', - '/administrator/templates/isis/less/pages', - '/administrator/templates/isis/less/bootstrap', - '/administrator/templates/isis/less/blocks', - '/administrator/templates/isis/less', - '/administrator/templates/isis/language/en-GB', - '/administrator/templates/isis/language', - '/administrator/templates/isis/js', - '/administrator/templates/isis/img', - '/administrator/templates/isis/images/system', - '/administrator/templates/isis/images/admin', - '/administrator/templates/isis/images', - '/administrator/templates/isis/html/mod_version', - '/administrator/templates/isis/html/layouts/joomla/toolbar', - '/administrator/templates/isis/html/layouts/joomla/system', - '/administrator/templates/isis/html/layouts/joomla/pagination', - '/administrator/templates/isis/html/layouts/joomla/form/field', - '/administrator/templates/isis/html/layouts/joomla/form', - '/administrator/templates/isis/html/layouts/joomla', - '/administrator/templates/isis/html/layouts', - '/administrator/templates/isis/html/com_media/medialist', - '/administrator/templates/isis/html/com_media/imageslist', - '/administrator/templates/isis/html/com_media', - '/administrator/templates/isis/html', - '/administrator/templates/isis/css', - '/administrator/templates/isis', - '/administrator/templates/hathor/postinstall', - '/administrator/templates/hathor/less', - '/administrator/templates/hathor/language/en-GB', - '/administrator/templates/hathor/language', - '/administrator/templates/hathor/js', - '/administrator/templates/hathor/images/toolbar', - '/administrator/templates/hathor/images/system', - '/administrator/templates/hathor/images/menu', - '/administrator/templates/hathor/images/header', - '/administrator/templates/hathor/images/admin', - '/administrator/templates/hathor/images', - '/administrator/templates/hathor/html/mod_quickicon', - '/administrator/templates/hathor/html/mod_login', - '/administrator/templates/hathor/html/layouts/plugins/user/profile/fields', - '/administrator/templates/hathor/html/layouts/plugins/user/profile', - '/administrator/templates/hathor/html/layouts/plugins/user', - '/administrator/templates/hathor/html/layouts/plugins', - '/administrator/templates/hathor/html/layouts/joomla/toolbar', - '/administrator/templates/hathor/html/layouts/joomla/sidebars', - '/administrator/templates/hathor/html/layouts/joomla/quickicons', - '/administrator/templates/hathor/html/layouts/joomla/edit', - '/administrator/templates/hathor/html/layouts/joomla', - '/administrator/templates/hathor/html/layouts/com_modules/toolbar', - '/administrator/templates/hathor/html/layouts/com_modules', - '/administrator/templates/hathor/html/layouts/com_messages/toolbar', - '/administrator/templates/hathor/html/layouts/com_messages', - '/administrator/templates/hathor/html/layouts/com_media/toolbar', - '/administrator/templates/hathor/html/layouts/com_media', - '/administrator/templates/hathor/html/layouts', - '/administrator/templates/hathor/html/com_weblinks/weblinks', - '/administrator/templates/hathor/html/com_weblinks/weblink', - '/administrator/templates/hathor/html/com_weblinks', - '/administrator/templates/hathor/html/com_users/users', - '/administrator/templates/hathor/html/com_users/user', - '/administrator/templates/hathor/html/com_users/notes', - '/administrator/templates/hathor/html/com_users/note', - '/administrator/templates/hathor/html/com_users/levels', - '/administrator/templates/hathor/html/com_users/groups', - '/administrator/templates/hathor/html/com_users/debuguser', - '/administrator/templates/hathor/html/com_users/debuggroup', - '/administrator/templates/hathor/html/com_users', - '/administrator/templates/hathor/html/com_templates/templates', - '/administrator/templates/hathor/html/com_templates/template', - '/administrator/templates/hathor/html/com_templates/styles', - '/administrator/templates/hathor/html/com_templates/style', - '/administrator/templates/hathor/html/com_templates', - '/administrator/templates/hathor/html/com_tags/tags', - '/administrator/templates/hathor/html/com_tags/tag', - '/administrator/templates/hathor/html/com_tags', - '/administrator/templates/hathor/html/com_search/searches', - '/administrator/templates/hathor/html/com_search', - '/administrator/templates/hathor/html/com_redirect/links', - '/administrator/templates/hathor/html/com_redirect', - '/administrator/templates/hathor/html/com_postinstall/messages', - '/administrator/templates/hathor/html/com_postinstall', - '/administrator/templates/hathor/html/com_plugins/plugins', - '/administrator/templates/hathor/html/com_plugins/plugin', - '/administrator/templates/hathor/html/com_plugins', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeed', - '/administrator/templates/hathor/html/com_newsfeeds', - '/administrator/templates/hathor/html/com_modules/positions', - '/administrator/templates/hathor/html/com_modules/modules', - '/administrator/templates/hathor/html/com_modules/module', - '/administrator/templates/hathor/html/com_modules', - '/administrator/templates/hathor/html/com_messages/messages', - '/administrator/templates/hathor/html/com_messages/message', - '/administrator/templates/hathor/html/com_messages', - '/administrator/templates/hathor/html/com_menus/menutypes', - '/administrator/templates/hathor/html/com_menus/menus', - '/administrator/templates/hathor/html/com_menus/menu', - '/administrator/templates/hathor/html/com_menus/items', - '/administrator/templates/hathor/html/com_menus/item', - '/administrator/templates/hathor/html/com_menus', - '/administrator/templates/hathor/html/com_languages/overrides', - '/administrator/templates/hathor/html/com_languages/languages', - '/administrator/templates/hathor/html/com_languages/installed', - '/administrator/templates/hathor/html/com_languages', - '/administrator/templates/hathor/html/com_joomlaupdate/default', - '/administrator/templates/hathor/html/com_joomlaupdate', - '/administrator/templates/hathor/html/com_installer/warnings', - '/administrator/templates/hathor/html/com_installer/update', - '/administrator/templates/hathor/html/com_installer/manage', - '/administrator/templates/hathor/html/com_installer/languages', - '/administrator/templates/hathor/html/com_installer/install', - '/administrator/templates/hathor/html/com_installer/discover', - '/administrator/templates/hathor/html/com_installer/default', - '/administrator/templates/hathor/html/com_installer/database', - '/administrator/templates/hathor/html/com_installer', - '/administrator/templates/hathor/html/com_finder/maps', - '/administrator/templates/hathor/html/com_finder/index', - '/administrator/templates/hathor/html/com_finder/filters', - '/administrator/templates/hathor/html/com_finder', - '/administrator/templates/hathor/html/com_fields/groups', - '/administrator/templates/hathor/html/com_fields/group', - '/administrator/templates/hathor/html/com_fields/fields', - '/administrator/templates/hathor/html/com_fields/field', - '/administrator/templates/hathor/html/com_fields', - '/administrator/templates/hathor/html/com_cpanel/cpanel', - '/administrator/templates/hathor/html/com_cpanel', - '/administrator/templates/hathor/html/com_contenthistory/history', - '/administrator/templates/hathor/html/com_contenthistory', - '/administrator/templates/hathor/html/com_content/featured', - '/administrator/templates/hathor/html/com_content/articles', - '/administrator/templates/hathor/html/com_content/article', - '/administrator/templates/hathor/html/com_content', - '/administrator/templates/hathor/html/com_contact/contacts', - '/administrator/templates/hathor/html/com_contact/contact', - '/administrator/templates/hathor/html/com_contact', - '/administrator/templates/hathor/html/com_config/component', - '/administrator/templates/hathor/html/com_config/application', - '/administrator/templates/hathor/html/com_config', - '/administrator/templates/hathor/html/com_checkin/checkin', - '/administrator/templates/hathor/html/com_checkin', - '/administrator/templates/hathor/html/com_categories/category', - '/administrator/templates/hathor/html/com_categories/categories', - '/administrator/templates/hathor/html/com_categories', - '/administrator/templates/hathor/html/com_cache/purge', - '/administrator/templates/hathor/html/com_cache/cache', - '/administrator/templates/hathor/html/com_cache', - '/administrator/templates/hathor/html/com_banners/tracks', - '/administrator/templates/hathor/html/com_banners/download', - '/administrator/templates/hathor/html/com_banners/clients', - '/administrator/templates/hathor/html/com_banners/client', - '/administrator/templates/hathor/html/com_banners/banners', - '/administrator/templates/hathor/html/com_banners/banner', - '/administrator/templates/hathor/html/com_banners', - '/administrator/templates/hathor/html/com_associations/associations', - '/administrator/templates/hathor/html/com_associations', - '/administrator/templates/hathor/html/com_admin/sysinfo', - '/administrator/templates/hathor/html/com_admin/profile', - '/administrator/templates/hathor/html/com_admin/help', - '/administrator/templates/hathor/html/com_admin', - '/administrator/templates/hathor/html', - '/administrator/templates/hathor/css', - '/administrator/templates/hathor', - '/administrator/modules/mod_version/language/en-GB', - '/administrator/modules/mod_version/language', - '/administrator/modules/mod_status/tmpl', - '/administrator/modules/mod_status', - '/administrator/modules/mod_stats_admin/language', - '/administrator/modules/mod_multilangstatus/language/en-GB', - '/administrator/modules/mod_multilangstatus/language', - '/administrator/components/com_users/views/users/tmpl', - '/administrator/components/com_users/views/users', - '/administrator/components/com_users/views/user/tmpl', - '/administrator/components/com_users/views/user', - '/administrator/components/com_users/views/notes/tmpl', - '/administrator/components/com_users/views/notes', - '/administrator/components/com_users/views/note/tmpl', - '/administrator/components/com_users/views/note', - '/administrator/components/com_users/views/mail/tmpl', - '/administrator/components/com_users/views/mail', - '/administrator/components/com_users/views/levels/tmpl', - '/administrator/components/com_users/views/levels', - '/administrator/components/com_users/views/level/tmpl', - '/administrator/components/com_users/views/level', - '/administrator/components/com_users/views/groups/tmpl', - '/administrator/components/com_users/views/groups', - '/administrator/components/com_users/views/group/tmpl', - '/administrator/components/com_users/views/group', - '/administrator/components/com_users/views/debuguser/tmpl', - '/administrator/components/com_users/views/debuguser', - '/administrator/components/com_users/views/debuggroup/tmpl', - '/administrator/components/com_users/views/debuggroup', - '/administrator/components/com_users/views', - '/administrator/components/com_users/tables', - '/administrator/components/com_users/models/forms/fields', - '/administrator/components/com_users/models/forms', - '/administrator/components/com_users/models/fields', - '/administrator/components/com_users/models', - '/administrator/components/com_users/helpers/html', - '/administrator/components/com_users/controllers', - '/administrator/components/com_templates/views/templates/tmpl', - '/administrator/components/com_templates/views/templates', - '/administrator/components/com_templates/views/template/tmpl', - '/administrator/components/com_templates/views/template', - '/administrator/components/com_templates/views/styles/tmpl', - '/administrator/components/com_templates/views/styles', - '/administrator/components/com_templates/views/style/tmpl', - '/administrator/components/com_templates/views/style', - '/administrator/components/com_templates/views', - '/administrator/components/com_templates/tables', - '/administrator/components/com_templates/models/forms', - '/administrator/components/com_templates/models/fields', - '/administrator/components/com_templates/models', - '/administrator/components/com_templates/helpers/html', - '/administrator/components/com_templates/controllers', - '/administrator/components/com_tags/views/tags/tmpl', - '/administrator/components/com_tags/views/tags', - '/administrator/components/com_tags/views/tag/tmpl', - '/administrator/components/com_tags/views/tag', - '/administrator/components/com_tags/views', - '/administrator/components/com_tags/tables', - '/administrator/components/com_tags/models/forms', - '/administrator/components/com_tags/models', - '/administrator/components/com_tags/helpers', - '/administrator/components/com_tags/controllers', - '/administrator/components/com_redirect/views/links/tmpl', - '/administrator/components/com_redirect/views/links', - '/administrator/components/com_redirect/views/link/tmpl', - '/administrator/components/com_redirect/views/link', - '/administrator/components/com_redirect/views', - '/administrator/components/com_redirect/tables', - '/administrator/components/com_redirect/models/forms', - '/administrator/components/com_redirect/models/fields', - '/administrator/components/com_redirect/models', - '/administrator/components/com_redirect/helpers/html', - '/administrator/components/com_redirect/controllers', - '/administrator/components/com_privacy/views/requests/tmpl', - '/administrator/components/com_privacy/views/requests', - '/administrator/components/com_privacy/views/request/tmpl', - '/administrator/components/com_privacy/views/request', - '/administrator/components/com_privacy/views/export', - '/administrator/components/com_privacy/views/dashboard/tmpl', - '/administrator/components/com_privacy/views/dashboard', - '/administrator/components/com_privacy/views/consents/tmpl', - '/administrator/components/com_privacy/views/consents', - '/administrator/components/com_privacy/views/capabilities/tmpl', - '/administrator/components/com_privacy/views/capabilities', - '/administrator/components/com_privacy/views', - '/administrator/components/com_privacy/tables', - '/administrator/components/com_privacy/models/forms', - '/administrator/components/com_privacy/models/fields', - '/administrator/components/com_privacy/models', - '/administrator/components/com_privacy/helpers/removal', - '/administrator/components/com_privacy/helpers/html', - '/administrator/components/com_privacy/helpers/export', - '/administrator/components/com_privacy/helpers', - '/administrator/components/com_privacy/controllers', - '/administrator/components/com_postinstall/views/messages/tmpl', - '/administrator/components/com_postinstall/views/messages', - '/administrator/components/com_postinstall/views', - '/administrator/components/com_postinstall/models', - '/administrator/components/com_postinstall/controllers', - '/administrator/components/com_plugins/views/plugins/tmpl', - '/administrator/components/com_plugins/views/plugins', - '/administrator/components/com_plugins/views/plugin/tmpl', - '/administrator/components/com_plugins/views/plugin', - '/administrator/components/com_plugins/views', - '/administrator/components/com_plugins/models/forms', - '/administrator/components/com_plugins/models/fields', - '/administrator/components/com_plugins/models', - '/administrator/components/com_plugins/controllers', - '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl', - '/administrator/components/com_newsfeeds/views/newsfeeds', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl', - '/administrator/components/com_newsfeeds/views/newsfeed', - '/administrator/components/com_newsfeeds/views', - '/administrator/components/com_newsfeeds/tables', - '/administrator/components/com_newsfeeds/models/forms', - '/administrator/components/com_newsfeeds/models/fields/modal', - '/administrator/components/com_newsfeeds/models/fields', - '/administrator/components/com_newsfeeds/models', - '/administrator/components/com_newsfeeds/helpers/html', - '/administrator/components/com_newsfeeds/controllers', - '/administrator/components/com_modules/views/select/tmpl', - '/administrator/components/com_modules/views/select', - '/administrator/components/com_modules/views/preview/tmpl', - '/administrator/components/com_modules/views/preview', - '/administrator/components/com_modules/views/positions/tmpl', - '/administrator/components/com_modules/views/positions', - '/administrator/components/com_modules/views/modules/tmpl', - '/administrator/components/com_modules/views/modules', - '/administrator/components/com_modules/views/module/tmpl', - '/administrator/components/com_modules/views/module', - '/administrator/components/com_modules/views', - '/administrator/components/com_modules/models/forms', - '/administrator/components/com_modules/models/fields', - '/administrator/components/com_modules/models', - '/administrator/components/com_modules/helpers/html', - '/administrator/components/com_modules/controllers', - '/administrator/components/com_messages/views/messages/tmpl', - '/administrator/components/com_messages/views/messages', - '/administrator/components/com_messages/views/message/tmpl', - '/administrator/components/com_messages/views/message', - '/administrator/components/com_messages/views/config/tmpl', - '/administrator/components/com_messages/views/config', - '/administrator/components/com_messages/views', - '/administrator/components/com_messages/tables', - '/administrator/components/com_messages/models/forms', - '/administrator/components/com_messages/models/fields', - '/administrator/components/com_messages/models', - '/administrator/components/com_messages/helpers/html', - '/administrator/components/com_messages/helpers', - '/administrator/components/com_messages/controllers', - '/administrator/components/com_menus/views/menutypes/tmpl', - '/administrator/components/com_menus/views/menutypes', - '/administrator/components/com_menus/views/menus/tmpl', - '/administrator/components/com_menus/views/menus', - '/administrator/components/com_menus/views/menu/tmpl', - '/administrator/components/com_menus/views/menu', - '/administrator/components/com_menus/views/items/tmpl', - '/administrator/components/com_menus/views/items', - '/administrator/components/com_menus/views/item/tmpl', - '/administrator/components/com_menus/views/item', - '/administrator/components/com_menus/views', - '/administrator/components/com_menus/tables', - '/administrator/components/com_menus/models/forms', - '/administrator/components/com_menus/models/fields/modal', - '/administrator/components/com_menus/models/fields', - '/administrator/components/com_menus/models', - '/administrator/components/com_menus/layouts/joomla/searchtools/default', - '/administrator/components/com_menus/helpers/html', - '/administrator/components/com_menus/controllers', - '/administrator/components/com_media/views/medialist/tmpl', - '/administrator/components/com_media/views/medialist', - '/administrator/components/com_media/views/media/tmpl', - '/administrator/components/com_media/views/media', - '/administrator/components/com_media/views/imageslist/tmpl', - '/administrator/components/com_media/views/imageslist', - '/administrator/components/com_media/views/images/tmpl', - '/administrator/components/com_media/views/images', - '/administrator/components/com_media/views', - '/administrator/components/com_media/models', - '/administrator/components/com_media/controllers', - '/administrator/components/com_login/views/login/tmpl', - '/administrator/components/com_login/views/login', - '/administrator/components/com_login/views', - '/administrator/components/com_login/models', - '/administrator/components/com_languages/views/overrides/tmpl', - '/administrator/components/com_languages/views/overrides', - '/administrator/components/com_languages/views/override/tmpl', - '/administrator/components/com_languages/views/override', - '/administrator/components/com_languages/views/multilangstatus/tmpl', - '/administrator/components/com_languages/views/multilangstatus', - '/administrator/components/com_languages/views/languages/tmpl', - '/administrator/components/com_languages/views/languages', - '/administrator/components/com_languages/views/language/tmpl', - '/administrator/components/com_languages/views/language', - '/administrator/components/com_languages/views/installed/tmpl', - '/administrator/components/com_languages/views/installed', - '/administrator/components/com_languages/views', - '/administrator/components/com_languages/models/forms', - '/administrator/components/com_languages/models/fields', - '/administrator/components/com_languages/models', - '/administrator/components/com_languages/layouts/joomla/searchtools/default', - '/administrator/components/com_languages/layouts/joomla/searchtools', - '/administrator/components/com_languages/layouts/joomla', - '/administrator/components/com_languages/layouts', - '/administrator/components/com_languages/helpers/html', - '/administrator/components/com_languages/helpers', - '/administrator/components/com_languages/controllers', - '/administrator/components/com_joomlaupdate/views/upload/tmpl', - '/administrator/components/com_joomlaupdate/views/upload', - '/administrator/components/com_joomlaupdate/views/update/tmpl', - '/administrator/components/com_joomlaupdate/views/update', - '/administrator/components/com_joomlaupdate/views/default/tmpl', - '/administrator/components/com_joomlaupdate/views/default', - '/administrator/components/com_joomlaupdate/views', - '/administrator/components/com_joomlaupdate/models', - '/administrator/components/com_joomlaupdate/helpers', - '/administrator/components/com_joomlaupdate/controllers', - '/administrator/components/com_installer/views/warnings/tmpl', - '/administrator/components/com_installer/views/warnings', - '/administrator/components/com_installer/views/updatesites/tmpl', - '/administrator/components/com_installer/views/updatesites', - '/administrator/components/com_installer/views/update/tmpl', - '/administrator/components/com_installer/views/update', - '/administrator/components/com_installer/views/manage/tmpl', - '/administrator/components/com_installer/views/manage', - '/administrator/components/com_installer/views/languages/tmpl', - '/administrator/components/com_installer/views/languages', - '/administrator/components/com_installer/views/install/tmpl', - '/administrator/components/com_installer/views/install', - '/administrator/components/com_installer/views/discover/tmpl', - '/administrator/components/com_installer/views/discover', - '/administrator/components/com_installer/views/default/tmpl', - '/administrator/components/com_installer/views/default', - '/administrator/components/com_installer/views/database/tmpl', - '/administrator/components/com_installer/views/database', - '/administrator/components/com_installer/views', - '/administrator/components/com_installer/models/forms', - '/administrator/components/com_installer/models/fields', - '/administrator/components/com_installer/models', - '/administrator/components/com_installer/helpers/html', - '/administrator/components/com_installer/controllers', - '/administrator/components/com_finder/views/statistics/tmpl', - '/administrator/components/com_finder/views/statistics', - '/administrator/components/com_finder/views/maps/tmpl', - '/administrator/components/com_finder/views/maps', - '/administrator/components/com_finder/views/indexer/tmpl', - '/administrator/components/com_finder/views/indexer', - '/administrator/components/com_finder/views/index/tmpl', - '/administrator/components/com_finder/views/index', - '/administrator/components/com_finder/views/filters/tmpl', - '/administrator/components/com_finder/views/filters', - '/administrator/components/com_finder/views/filter/tmpl', - '/administrator/components/com_finder/views/filter', - '/administrator/components/com_finder/views', - '/administrator/components/com_finder/tables', - '/administrator/components/com_finder/models/forms', - '/administrator/components/com_finder/models/fields', - '/administrator/components/com_finder/models', - '/administrator/components/com_finder/helpers/indexer/stemmer', - '/administrator/components/com_finder/helpers/indexer/parser', - '/administrator/components/com_finder/helpers/indexer/driver', - '/administrator/components/com_finder/helpers/html', - '/administrator/components/com_finder/controllers', - '/administrator/components/com_fields/views/groups/tmpl', - '/administrator/components/com_fields/views/groups', - '/administrator/components/com_fields/views/group/tmpl', - '/administrator/components/com_fields/views/group', - '/administrator/components/com_fields/views/fields/tmpl', - '/administrator/components/com_fields/views/fields', - '/administrator/components/com_fields/views/field/tmpl', - '/administrator/components/com_fields/views/field', - '/administrator/components/com_fields/views', - '/administrator/components/com_fields/tables', - '/administrator/components/com_fields/models/forms', - '/administrator/components/com_fields/models/fields', - '/administrator/components/com_fields/models', - '/administrator/components/com_fields/libraries', - '/administrator/components/com_fields/controllers', - '/administrator/components/com_cpanel/views/cpanel/tmpl', - '/administrator/components/com_cpanel/views/cpanel', - '/administrator/components/com_cpanel/views', - '/administrator/components/com_contenthistory/views/preview/tmpl', - '/administrator/components/com_contenthistory/views/preview', - '/administrator/components/com_contenthistory/views/history/tmpl', - '/administrator/components/com_contenthistory/views/history', - '/administrator/components/com_contenthistory/views/compare/tmpl', - '/administrator/components/com_contenthistory/views/compare', - '/administrator/components/com_contenthistory/views', - '/administrator/components/com_contenthistory/models', - '/administrator/components/com_contenthistory/helpers/html', - '/administrator/components/com_contenthistory/controllers', - '/administrator/components/com_content/views/featured/tmpl', - '/administrator/components/com_content/views/featured', - '/administrator/components/com_content/views/articles/tmpl', - '/administrator/components/com_content/views/articles', - '/administrator/components/com_content/views/article/tmpl', - '/administrator/components/com_content/views/article', - '/administrator/components/com_content/views', - '/administrator/components/com_content/tables', - '/administrator/components/com_content/models/forms', - '/administrator/components/com_content/models/fields/modal', - '/administrator/components/com_content/models/fields', - '/administrator/components/com_content/models', - '/administrator/components/com_content/helpers/html', - '/administrator/components/com_content/controllers', - '/administrator/components/com_contact/views/contacts/tmpl', - '/administrator/components/com_contact/views/contacts', - '/administrator/components/com_contact/views/contact/tmpl', - '/administrator/components/com_contact/views/contact', - '/administrator/components/com_contact/views', - '/administrator/components/com_contact/tables', - '/administrator/components/com_contact/models/forms/fields', - '/administrator/components/com_contact/models/forms', - '/administrator/components/com_contact/models/fields/modal', - '/administrator/components/com_contact/models/fields', - '/administrator/components/com_contact/models', - '/administrator/components/com_contact/helpers/html', - '/administrator/components/com_contact/controllers', - '/administrator/components/com_config/view/component/tmpl', - '/administrator/components/com_config/view/component', - '/administrator/components/com_config/view/application/tmpl', - '/administrator/components/com_config/view/application', - '/administrator/components/com_config/view', - '/administrator/components/com_config/models', - '/administrator/components/com_config/model/form', - '/administrator/components/com_config/model/field', - '/administrator/components/com_config/model', - '/administrator/components/com_config/helper', - '/administrator/components/com_config/controllers', - '/administrator/components/com_config/controller/component', - '/administrator/components/com_config/controller/application', - '/administrator/components/com_config/controller', - '/administrator/components/com_checkin/views/checkin/tmpl', - '/administrator/components/com_checkin/views/checkin', - '/administrator/components/com_checkin/views', - '/administrator/components/com_checkin/models/forms', - '/administrator/components/com_checkin/models', - '/administrator/components/com_categories/views/category/tmpl', - '/administrator/components/com_categories/views/category', - '/administrator/components/com_categories/views/categories/tmpl', - '/administrator/components/com_categories/views/categories', - '/administrator/components/com_categories/views', - '/administrator/components/com_categories/tables', - '/administrator/components/com_categories/models/forms', - '/administrator/components/com_categories/models/fields/modal', - '/administrator/components/com_categories/models/fields', - '/administrator/components/com_categories/models', - '/administrator/components/com_categories/helpers/html', - '/administrator/components/com_categories/controllers', - '/administrator/components/com_cache/views/purge/tmpl', - '/administrator/components/com_cache/views/purge', - '/administrator/components/com_cache/views/cache/tmpl', - '/administrator/components/com_cache/views/cache', - '/administrator/components/com_cache/views', - '/administrator/components/com_cache/models/forms', - '/administrator/components/com_cache/models', - '/administrator/components/com_cache/helpers', - '/administrator/components/com_banners/views/tracks/tmpl', - '/administrator/components/com_banners/views/tracks', - '/administrator/components/com_banners/views/download/tmpl', - '/administrator/components/com_banners/views/download', - '/administrator/components/com_banners/views/clients/tmpl', - '/administrator/components/com_banners/views/clients', - '/administrator/components/com_banners/views/client/tmpl', - '/administrator/components/com_banners/views/client', - '/administrator/components/com_banners/views/banners/tmpl', - '/administrator/components/com_banners/views/banners', - '/administrator/components/com_banners/views/banner/tmpl', - '/administrator/components/com_banners/views/banner', - '/administrator/components/com_banners/views', - '/administrator/components/com_banners/tables', - '/administrator/components/com_banners/models/forms', - '/administrator/components/com_banners/models/fields', - '/administrator/components/com_banners/models', - '/administrator/components/com_banners/helpers/html', - '/administrator/components/com_banners/controllers', - '/administrator/components/com_associations/views/associations/tmpl', - '/administrator/components/com_associations/views/associations', - '/administrator/components/com_associations/views/association/tmpl', - '/administrator/components/com_associations/views/association', - '/administrator/components/com_associations/views', - '/administrator/components/com_associations/models/forms', - '/administrator/components/com_associations/models/fields', - '/administrator/components/com_associations/models', - '/administrator/components/com_associations/layouts/joomla/searchtools/default', - '/administrator/components/com_associations/helpers', - '/administrator/components/com_associations/controllers', - '/administrator/components/com_admin/views/sysinfo/tmpl', - '/administrator/components/com_admin/views/sysinfo', - '/administrator/components/com_admin/views/profile/tmpl', - '/administrator/components/com_admin/views/profile', - '/administrator/components/com_admin/views/help/tmpl', - '/administrator/components/com_admin/views/help', - '/administrator/components/com_admin/views', - '/administrator/components/com_admin/sql/updates/sqlazure', - '/administrator/components/com_admin/models/forms', - '/administrator/components/com_admin/models', - '/administrator/components/com_admin/helpers/html', - '/administrator/components/com_admin/helpers', - '/administrator/components/com_admin/controllers', - '/administrator/components/com_actionlogs/views/actionlogs/tmpl', - '/administrator/components/com_actionlogs/views/actionlogs', - '/administrator/components/com_actionlogs/views', - '/administrator/components/com_actionlogs/models/forms', - '/administrator/components/com_actionlogs/models/fields', - '/administrator/components/com_actionlogs/models', - '/administrator/components/com_actionlogs/libraries', - '/administrator/components/com_actionlogs/layouts', - '/administrator/components/com_actionlogs/helpers', - '/administrator/components/com_actionlogs/controllers', - // 4.0 from Beta 1 to Beta 2 - '/libraries/vendor/joomla/controller/src', - '/libraries/vendor/joomla/controller', - '/api/components/com_installer/src/View/Languages', - '/administrator/components/com_finder/src/Indexer/Driver', - // 4.0 from Beta 4 to Beta 5 - '/plugins/content/imagelazyload', - // 4.0 from Beta 5 to Beta 6 - '/media/system/js/core.es6', - '/administrator/modules/mod_multilangstatus/src/Helper', - '/administrator/modules/mod_multilangstatus/src', - // 4.0 from Beta 6 to Beta 7 - '/media/vendor/skipto/css', - // 4.0 from Beta 7 to RC 1 - '/templates/system/js', - '/templates/cassiopeia/scss/tools/mixins', - '/plugins/fields/subfields/tmpl', - '/plugins/fields/subfields/params', - '/plugins/fields/subfields', - '/media/vendor/punycode/js', - '/media/templates/atum/js', - '/media/templates/atum', - '/libraries/vendor/paragonie/random_compat/dist', - '/libraries/vendor/paragonie/random_compat', - '/libraries/vendor/ozdemirburak/iris/src/Traits', - '/libraries/vendor/ozdemirburak/iris/src/Helpers', - '/libraries/vendor/ozdemirburak/iris/src/Exceptions', - '/libraries/vendor/ozdemirburak/iris/src/Color', - '/libraries/vendor/ozdemirburak/iris/src', - '/libraries/vendor/ozdemirburak/iris', - '/libraries/vendor/ozdemirburak', - '/libraries/vendor/bin', - '/components/com_menus/src/Controller', - '/components/com_csp/src/Controller', - '/components/com_csp/src', - '/components/com_csp', - '/administrator/templates/atum/Service/HTML', - '/administrator/templates/atum/Service', - '/administrator/components/com_joomlaupdate/src/Helper', - '/administrator/components/com_csp/tmpl/reports', - '/administrator/components/com_csp/tmpl', - '/administrator/components/com_csp/src/View/Reports', - '/administrator/components/com_csp/src/View', - '/administrator/components/com_csp/src/Table', - '/administrator/components/com_csp/src/Model', - '/administrator/components/com_csp/src/Helper', - '/administrator/components/com_csp/src/Controller', - '/administrator/components/com_csp/src', - '/administrator/components/com_csp/services', - '/administrator/components/com_csp/forms', - '/administrator/components/com_csp', - '/administrator/components/com_admin/tmpl/profile', - '/administrator/components/com_admin/src/View/Profile', - '/administrator/components/com_admin/forms', - // 4.0 from RC 5 to RC 6 - '/templates/cassiopeia/scss/vendor/fontawesome-free', - '/templates/cassiopeia/css/vendor/fontawesome-free', - '/media/templates/cassiopeia/js/mod_menu', - '/media/templates/cassiopeia/js', - '/media/templates/cassiopeia', - // 4.0 from RC 6 to 4.0.0 (stable) - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation', - '/libraries/vendor/willdurand/negotiation/tests', - '/libraries/vendor/jakeasmith/http_build_url/tests', - '/libraries/vendor/doctrine/inflector/docs/en', - '/libraries/vendor/doctrine/inflector/docs', - '/libraries/vendor/algo26-matthias/idna-convert/tests/unit', - '/libraries/vendor/algo26-matthias/idna-convert/tests/integration', - '/libraries/vendor/algo26-matthias/idna-convert/tests', - // From 4.0.3 to 4.0.4 - '/templates/cassiopeia/images/system', - // From 4.0.x to 4.1.0-beta1 - '/templates/system/scss', - '/templates/system/css', - '/templates/cassiopeia/scss/vendor/metismenu', - '/templates/cassiopeia/scss/vendor/joomla-custom-elements', - '/templates/cassiopeia/scss/vendor/choicesjs', - '/templates/cassiopeia/scss/vendor/bootstrap', - '/templates/cassiopeia/scss/vendor', - '/templates/cassiopeia/scss/tools/variables', - '/templates/cassiopeia/scss/tools/functions', - '/templates/cassiopeia/scss/tools', - '/templates/cassiopeia/scss/system/searchtools', - '/templates/cassiopeia/scss/system', - '/templates/cassiopeia/scss/global', - '/templates/cassiopeia/scss/blocks', - '/templates/cassiopeia/scss', - '/templates/cassiopeia/js', - '/templates/cassiopeia/images', - '/templates/cassiopeia/css/vendor/joomla-custom-elements', - '/templates/cassiopeia/css/vendor/choicesjs', - '/templates/cassiopeia/css/vendor', - '/templates/cassiopeia/css/system/searchtools', - '/templates/cassiopeia/css/system', - '/templates/cassiopeia/css/global', - '/templates/cassiopeia/css', - '/administrator/templates/system/scss', - '/administrator/templates/system/images', - '/administrator/templates/system/css', - '/administrator/templates/atum/scss/vendor/minicolors', - '/administrator/templates/atum/scss/vendor/joomla-custom-elements', - '/administrator/templates/atum/scss/vendor/fontawesome-free', - '/administrator/templates/atum/scss/vendor/choicesjs', - '/administrator/templates/atum/scss/vendor/bootstrap', - '/administrator/templates/atum/scss/vendor/awesomplete', - '/administrator/templates/atum/scss/vendor', - '/administrator/templates/atum/scss/system/searchtools', - '/administrator/templates/atum/scss/system', - '/administrator/templates/atum/scss/pages', - '/administrator/templates/atum/scss/blocks', - '/administrator/templates/atum/scss', - '/administrator/templates/atum/images/logos', - '/administrator/templates/atum/images', - '/administrator/templates/atum/css/vendor/minicolors', - '/administrator/templates/atum/css/vendor/joomla-custom-elements', - '/administrator/templates/atum/css/vendor/fontawesome-free', - '/administrator/templates/atum/css/vendor/choicesjs', - '/administrator/templates/atum/css/vendor/awesomplete', - '/administrator/templates/atum/css/vendor', - '/administrator/templates/atum/css/system/searchtools', - '/administrator/templates/atum/css/system', - '/administrator/templates/atum/css', - // From 4.1.0-beta3 to 4.1.0-rc1 - '/api/components/com_media/src/Helper', - // From 4.1.0 to 4.1.1 - '/libraries/vendor/tobscure/json-api/tests/Exception/Handler', - '/libraries/vendor/tobscure/json-api/tests/Exception', - '/libraries/vendor/tobscure/json-api/tests', - '/libraries/vendor/tobscure/json-api/.git/refs/tags', - '/libraries/vendor/tobscure/json-api/.git/refs/remotes/origin', - '/libraries/vendor/tobscure/json-api/.git/refs/remotes', - '/libraries/vendor/tobscure/json-api/.git/refs/heads', - '/libraries/vendor/tobscure/json-api/.git/refs', - '/libraries/vendor/tobscure/json-api/.git/objects/pack', - '/libraries/vendor/tobscure/json-api/.git/objects/info', - '/libraries/vendor/tobscure/json-api/.git/objects', - '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes/origin', - '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes', - '/libraries/vendor/tobscure/json-api/.git/logs/refs/heads', - '/libraries/vendor/tobscure/json-api/.git/logs/refs', - '/libraries/vendor/tobscure/json-api/.git/logs', - '/libraries/vendor/tobscure/json-api/.git/info', - '/libraries/vendor/tobscure/json-api/.git/hooks', - '/libraries/vendor/tobscure/json-api/.git/branches', - '/libraries/vendor/tobscure/json-api/.git', - // From 4.1.3 to 4.1.4 - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar', - '/libraries/vendor/maximebf/debugbar/tests', - '/libraries/vendor/maximebf/debugbar/docs', - '/libraries/vendor/maximebf/debugbar/demo/bridge/twig', - '/libraries/vendor/maximebf/debugbar/demo/bridge/swiftmailer', - '/libraries/vendor/maximebf/debugbar/demo/bridge/slim', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel', - '/libraries/vendor/maximebf/debugbar/demo/bridge/monolog', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src/Demo', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine', - '/libraries/vendor/maximebf/debugbar/demo/bridge/cachecache', - '/libraries/vendor/maximebf/debugbar/demo/bridge', - '/libraries/vendor/maximebf/debugbar/demo', - '/libraries/vendor/maximebf/debugbar/build', - // From 4.1 to 4.2.0-beta1 - '/plugins/twofactorauth/yubikey/tmpl', - '/plugins/twofactorauth/yubikey', - '/plugins/twofactorauth/totp/tmpl', - '/plugins/twofactorauth/totp/postinstall', - '/plugins/twofactorauth/totp', - '/plugins/twofactorauth', - '/libraries/vendor/nyholm/psr7/doc', - // From 4.2.0-beta1 to 4.2.0-beta2 - '/layouts/plugins/user/profile/fields', - '/layouts/plugins/user/profile', - ); - - $status['files_checked'] = $files; - $status['folders_checked'] = $folders; - - foreach ($files as $file) - { - if ($fileExists = File::exists(JPATH_ROOT . $file)) - { - $status['files_exist'][] = $file; - - if ($dryRun === false) - { - if (File::delete(JPATH_ROOT . $file)) - { - $status['files_deleted'][] = $file; - } - else - { - $status['files_errors'][] = Text::sprintf('FILES_JOOMLA_ERROR_FILE_FOLDER', $file); - } - } - } - } - - $this->moveRemainingTemplateFiles(); - - foreach ($folders as $folder) - { - if ($folderExists = Folder::exists(JPATH_ROOT . $folder)) - { - $status['folders_exist'][] = $folder; - - if ($dryRun === false) - { - if (Folder::delete(JPATH_ROOT . $folder)) - { - $status['folders_deleted'][] = $folder; - } - else - { - $status['folders_errors'][] = Text::sprintf('FILES_JOOMLA_ERROR_FILE_FOLDER', $folder); - } - } - } - } - - $this->fixFilenameCasing(); - - /* - * Needed for updates from 3.10 - * If com_search doesn't exist then assume we can delete the search package manifest (included in the update packages) - * We deliberately check for the presence of the files in case people have previously uninstalled their search extension - * but an update has put the files back. In that case it exists even if they don't believe in it! - */ - if (!File::exists(JPATH_ROOT . '/administrator/components/com_search/search.php') - && File::exists(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml')) - { - File::delete(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml'); - } - - if ($suppressOutput === false && count($status['folders_errors'])) - { - echo implode('
    ', $status['folders_errors']); - } - - if ($suppressOutput === false && count($status['files_errors'])) - { - echo implode('
    ', $status['files_errors']); - } - - return $status; - } - - /** - * Method to create assets for newly installed components - * - * @param Installer $installer The class calling this method - * - * @return boolean - * - * @since 3.2 - */ - public function updateAssets($installer) - { - // List all components added since 4.0 - $newComponents = array( - // Components to be added here - ); - - foreach ($newComponents as $component) - { - /** @var \Joomla\CMS\Table\Asset $asset */ - $asset = Table::getInstance('Asset'); - - if ($asset->loadByName($component)) - { - continue; - } - - $asset->name = $component; - $asset->parent_id = 1; - $asset->rules = '{}'; - $asset->title = $component; - $asset->setLocation(1, 'last-child'); - - if (!$asset->store()) - { - // Install failed, roll back changes - $installer->abort(Text::sprintf('JLIB_INSTALLER_ABORT_COMP_INSTALL_ROLLBACK', $asset->getError(true))); - - return false; - } - } - - return true; - } - - /** - * Converts the site's database tables to support UTF-8 Multibyte. - * - * @param boolean $doDbFixMsg Flag if message to be shown to check db fix - * - * @return void - * - * @since 3.5 - */ - public function convertTablesToUtf8mb4($doDbFixMsg = false) - { - $db = Factory::getDbo(); - - if ($db->getServerType() !== 'mysql') - { - return; - } - - // Check if the #__utf8_conversion table exists - $db->setQuery('SHOW TABLES LIKE ' . $db->quote($db->getPrefix() . 'utf8_conversion')); - - try - { - $rows = $db->loadRowList(0); - } - catch (Exception $e) - { - // Render the error message from the Exception object - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - if ($doDbFixMsg) - { - // Show an error message telling to check database problems - Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); - } - - return; - } - - // Nothing to do if the table doesn't exist because the CMS has never been updated from a pre-4.0 version - if (count($rows) === 0) - { - return; - } - - // Set required conversion status - $converted = 5; - - // Check conversion status in database - $db->setQuery( - 'SELECT ' . $db->quoteName('converted') - . ' FROM ' . $db->quoteName('#__utf8_conversion') - ); - - try - { - $convertedDB = $db->loadResult(); - } - catch (Exception $e) - { - // Render the error message from the Exception object - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - if ($doDbFixMsg) - { - // Show an error message telling to check database problems - Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); - } - - return; - } - - // If conversion status from DB is equal to required final status, try to drop the #__utf8_conversion table - if ($convertedDB === $converted) - { - $this->dropUtf8ConversionTable(); - - return; - } - - // Perform the required conversions of core tables if not done already in a previous step - if ($convertedDB !== 99) - { - $fileName1 = JPATH_ROOT . '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion.sql'; - - if (is_file($fileName1)) - { - $fileContents1 = @file_get_contents($fileName1); - $queries1 = $db->splitSql($fileContents1); - - if (!empty($queries1)) - { - foreach ($queries1 as $query1) - { - try - { - $db->setQuery($query1)->execute(); - } - catch (Exception $e) - { - $converted = $convertedDB; - - // Still render the error message from the Exception object - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - } - } - } - - // If no error before, perform the optional conversions of tables which might or might not exist - if ($converted === 5) - { - $fileName2 = JPATH_ROOT . '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion_optional.sql'; - - if (is_file($fileName2)) - { - $fileContents2 = @file_get_contents($fileName2); - $queries2 = $db->splitSql($fileContents2); - - if (!empty($queries2)) - { - foreach ($queries2 as $query2) - { - // Get table name from query - if (preg_match('/^ALTER\s+TABLE\s+([^\s]+)\s+/i', $query2, $matches) === 1) - { - $tableName = str_replace('`', '', $matches[1]); - $tableName = str_replace('#__', $db->getPrefix(), $tableName); - - // Check if the table exists and if yes, run the query - try - { - $db->setQuery('SHOW TABLES LIKE ' . $db->quote($tableName)); - - $rows = $db->loadRowList(0); - - if (count($rows) > 0) - { - $db->setQuery($query2)->execute(); - } - } - catch (Exception $e) - { - $converted = 99; - - // Still render the error message from the Exception object - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - } - } - } - } - - if ($doDbFixMsg && $converted !== 5) - { - // Show an error message telling to check database problems - Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); - } - - // If the conversion was successful try to drop the #__utf8_conversion table - if ($converted === 5 && $this->dropUtf8ConversionTable()) - { - // Table successfully dropped - return; - } - - // Set flag in database if the conversion status has changed. - if ($converted !== $convertedDB) - { - $db->setQuery('UPDATE ' . $db->quoteName('#__utf8_conversion') - . ' SET ' . $db->quoteName('converted') . ' = ' . $converted . ';' - )->execute(); - } - } - - /** - * This method clean the Joomla Cache using the method `clean` from the com_cache model - * - * @return void - * - * @since 3.5.1 - */ - private function cleanJoomlaCache() - { - /** @var \Joomla\Component\Cache\Administrator\Model\CacheModel $model */ - $model = Factory::getApplication()->bootComponent('com_cache')->getMVCFactory() - ->createModel('Cache', 'Administrator', ['ignore_request' => true]); - - // Clean frontend cache - $model->clean(); - - // Clean admin cache - $model->setState('client_id', 1); - $model->clean(); - } - - /** - * This method drops the #__utf8_conversion table - * - * @return boolean True on success - * - * @since 4.0.0 - */ - private function dropUtf8ConversionTable() - { - $db = Factory::getDbo(); - - try - { - $db->setQuery('DROP TABLE ' . $db->quoteName('#__utf8_conversion') . ';' - )->execute(); - } - catch (Exception $e) - { - return false; - } - - return true; - } - - /** - * Called after any type of action - * - * @param string $action Which action is happening (install|uninstall|discover_install|update) - * @param Installer $installer The class calling this method - * - * @return boolean True on success - * - * @since 4.0.0 - */ - public function postflight($action, $installer) - { - if ($action !== 'update') - { - return true; - } - - if (empty($this->fromVersion) || version_compare($this->fromVersion, '4.0.0', 'ge')) - { - return true; - } - - // Update UCM content types. - $this->updateContentTypes(); - - $db = Factory::getDbo(); - Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/Table/'); - - $tableItem = new \Joomla\Component\Menus\Administrator\Table\MenuTable($db); - - $contactItems = $this->contactItems($tableItem); - $finderItems = $this->finderItems($tableItem); - - $menuItems = array_merge($contactItems, $finderItems); - - foreach ($menuItems as $menuItem) - { - // Check an existing record - $keys = [ - 'menutype' => $menuItem['menutype'], - 'type' => $menuItem['type'], - 'title' => $menuItem['title'], - 'parent_id' => $menuItem['parent_id'], - 'client_id' => $menuItem['client_id'], - ]; - - if ($tableItem->load($keys)) - { - continue; - } - - $newTableItem = new \Joomla\Component\Menus\Administrator\Table\MenuTable($db); - - // Bind the data. - if (!$newTableItem->bind($menuItem)) - { - return false; - } - - $newTableItem->setLocation($menuItem['parent_id'], 'last-child'); - - // Check the data. - if (!$newTableItem->check()) - { - return false; - } - - // Store the data. - if (!$newTableItem->store()) - { - return false; - } - - // Rebuild the tree path. - if (!$newTableItem->rebuildPath($newTableItem->id)) - { - return false; - } - } - - return true; - } - - /** - * Prepare the contact menu items - * - * @return array Menu items - * - * @since 4.0.0 - */ - private function contactItems(Table $tableItem): array - { - // Check for the Contact parent Id Menu Item - $keys = [ - 'menutype' => 'main', - 'type' => 'component', - 'title' => 'com_contact', - 'parent_id' => 1, - 'client_id' => 1, - ]; - - $contactMenuitem = $tableItem->load($keys); - - if (!$contactMenuitem) - { - return []; - } - - $parentId = $tableItem->id; - $componentId = ExtensionHelper::getExtensionRecord('com_fields', 'component')->extension_id; - - // Add Contact Fields Menu Items. - $menuItems = [ - [ - 'menutype' => 'main', - 'title' => '-', - 'alias' => microtime(true), - 'note' => '', - 'path' => '', - 'link' => '#', - 'type' => 'separator', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'mod_menu_fields', - 'alias' => 'Contact Custom Fields', - 'note' => '', - 'path' => 'contact/Custom Fields', - 'link' => 'index.php?option=com_fields&context=com_contact.contact', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'mod_menu_fields_group', - 'alias' => 'Contact Custom Fields Group', - 'note' => '', - 'path' => 'contact/Custom Fields Group', - 'link' => 'index.php?option=com_fields&view=groups&context=com_contact.contact', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ] - ]; - - return $menuItems; - } - - /** - * Prepare the finder menu items - * - * @return array Menu items - * - * @since 4.0.0 - */ - private function finderItems(Table $tableItem): array - { - // Check for the Finder parent Id Menu Item - $keys = [ - 'menutype' => 'main', - 'type' => 'component', - 'title' => 'com_finder', - 'parent_id' => 1, - 'client_id' => 1, - ]; - - $finderMenuitem = $tableItem->load($keys); - - if (!$finderMenuitem) - { - return []; - } - - $parentId = $tableItem->id; - $componentId = ExtensionHelper::getExtensionRecord('com_finder', 'component')->extension_id; - - // Add Finder Fields Menu Items. - $menuItems = [ - [ - 'menutype' => 'main', - 'title' => '-', - 'alias' => microtime(true), - 'note' => '', - 'path' => '', - 'link' => '#', - 'type' => 'separator', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'com_finder_index', - 'alias' => 'Smart-Search-Index', - 'note' => '', - 'path' => 'Smart Search/Index', - 'link' => 'index.php?option=com_finder&view=index', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'com_finder_maps', - 'alias' => 'Smart-Search-Maps', - 'note' => '', - 'path' => 'Smart Search/Maps', - 'link' => 'index.php?option=com_finder&view=maps', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'com_finder_filters', - 'alias' => 'Smart-Search-Filters', - 'note' => '', - 'path' => 'Smart Search/Filters', - 'link' => 'index.php?option=com_finder&view=filters', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'com_finder_searches', - 'alias' => 'Smart-Search-Searches', - 'note' => '', - 'path' => 'Smart Search/Searches', - 'link' => 'index.php?option=com_finder&view=searches', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ] - ]; - - return $menuItems; - } - - /** - * Updates content type table classes. - * - * @return void - * - * @since 4.0.0 - */ - private function updateContentTypes(): void - { - // Content types to update. - $contentTypes = [ - 'com_content.article', - 'com_contact.contact', - 'com_newsfeeds.newsfeed', - 'com_tags.tag', - 'com_banners.banner', - 'com_banners.client', - 'com_users.note', - 'com_content.category', - 'com_contact.category', - 'com_newsfeeds.category', - 'com_banners.category', - 'com_users.category', - 'com_users.user', - ]; - - // Get table definitions. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('type_alias'), - $db->quoteName('table'), - ] - ) - ->from($db->quoteName('#__content_types')) - ->whereIn($db->quoteName('type_alias'), $contentTypes, ParameterType::STRING); - - $db->setQuery($query); - $contentTypes = $db->loadObjectList(); - - // Prepare the update query. - $query = $db->getQuery(true) - ->update($db->quoteName('#__content_types')) - ->set($db->quoteName('table') . ' = :table') - ->where($db->quoteName('type_alias') . ' = :typeAlias') - ->bind(':table', $table) - ->bind(':typeAlias', $typeAlias); - - $db->setQuery($query); - - foreach ($contentTypes as $contentType) - { - list($component, $tableType) = explode('.', $contentType->type_alias); - - // Special case for core table classes. - if ($contentType->type_alias === 'com_users.users' || $tableType === 'category') - { - $tablePrefix = 'Joomla\\CMS\Table\\'; - $tableType = ucfirst($tableType); - } - else - { - $tablePrefix = 'Joomla\\Component\\' . ucfirst(substr($component, 4)) . '\\Administrator\\Table\\'; - $tableType = ucfirst($tableType) . 'Table'; - } - - // Bind type alias. - $typeAlias = $contentType->type_alias; - - $table = json_decode($contentType->table); - - // Update table definitions. - $table->special->type = $tableType; - $table->special->prefix = $tablePrefix; - - // Some content types don't have this property. - if (!empty($table->common->prefix)) - { - $table->common->prefix = 'Joomla\\CMS\\Table\\'; - } - - $table = json_encode($table); - - // Execute the query. - $db->execute(); - } - } - - /** - * Renames or removes incorrectly cased files. - * - * @return void - * - * @since 3.9.25 - */ - protected function fixFilenameCasing() - { - $files = array( - // 3.10 changes - '/libraries/src/Filesystem/Support/Stringcontroller.php' => '/libraries/src/Filesystem/Support/StringController.php', - '/libraries/src/Form/Rule/SubFormRule.php' => '/libraries/src/Form/Rule/SubformRule.php', - // 4.0.0 - '/media/vendor/skipto/js/skipTo.js' => '/media/vendor/skipto/js/skipto.js', - ); - - foreach ($files as $old => $expected) - { - $oldRealpath = realpath(JPATH_ROOT . $old); - - // On Unix without incorrectly cased file. - if ($oldRealpath === false) - { - continue; - } - - $oldBasename = basename($oldRealpath); - $newRealpath = realpath(JPATH_ROOT . $expected); - $newBasename = basename($newRealpath); - $expectedBasename = basename($expected); - - // On Windows or Unix with only the incorrectly cased file. - if ($newBasename !== $expectedBasename) - { - // Rename the file. - File::move(JPATH_ROOT . $old, JPATH_ROOT . $old . '.tmp'); - File::move(JPATH_ROOT . $old . '.tmp', JPATH_ROOT . $expected); - - continue; - } - - // There might still be an incorrectly cased file on other OS than Windows. - if ($oldBasename === basename($old)) - { - // Check if case-insensitive file system, eg on OSX. - if (fileinode($oldRealpath) === fileinode($newRealpath)) - { - // Check deeper because even realpath or glob might not return the actual case. - if (!in_array($expectedBasename, scandir(dirname($newRealpath)))) - { - // Rename the file. - File::move(JPATH_ROOT . $old, JPATH_ROOT . $old . '.tmp'); - File::move(JPATH_ROOT . $old . '.tmp', JPATH_ROOT . $expected); - } - } - else - { - // On Unix with both files: Delete the incorrectly cased file. - File::delete(JPATH_ROOT . $old); - } - } - } - } - - /** - * Move core template (s)css or js or image files which are left after deleting - * obsolete core files to the right place in media folder. - * - * @return void - * - * @since 4.1.0 - */ - protected function moveRemainingTemplateFiles() - { - $folders = [ - '/administrator/templates/atum/css' => '/media/templates/administrator/atum/css', - '/administrator/templates/atum/images' => '/media/templates/administrator/atum/images', - '/administrator/templates/atum/js' => '/media/templates/administrator/atum/js', - '/administrator/templates/atum/scss' => '/media/templates/administrator/atum/scss', - '/templates/cassiopeia/css' => '/media/templates/site/cassiopeia/css', - '/templates/cassiopeia/images' => '/media/templates/site/cassiopeia/images', - '/templates/cassiopeia/js' => '/media/templates/site/cassiopeia/js', - '/templates/cassiopeia/scss' => '/media/templates/site/cassiopeia/scss', - ]; - - foreach ($folders as $oldFolder => $newFolder) - { - if (Folder::exists(JPATH_ROOT . $oldFolder)) - { - $oldPath = realpath(JPATH_ROOT . $oldFolder); - $newPath = realpath(JPATH_ROOT . $newFolder); - $directory = new \RecursiveDirectoryIterator($oldPath); - $directory->setFlags(RecursiveDirectoryIterator::SKIP_DOTS); - $iterator = new \RecursiveIteratorIterator($directory); - - // Handle all files in this folder and all sub-folders - foreach ($iterator as $oldFile) - { - if ($oldFile->isDir()) - { - continue; - } - - $newFile = $newPath . substr($oldFile, strlen($oldPath)); - - // Create target folder and parent folders if they don't exist yet - if (is_dir(dirname($newFile)) || @mkdir(dirname($newFile), 0755, true)) - { - File::move($oldFile, $newFile); - } - } - } - } - } - - /** - * Ensure the core templates are correctly moved to the new mode. - * - * @return void - * - * @since 4.1.0 - */ - protected function fixTemplateMode(): void - { - $db = Factory::getContainer()->get('DatabaseDriver'); - - array_map( - function ($template) use ($db) - { - $clientId = $template === 'atum' ? 1 : 0; - $query = $db->getQuery(true) - ->update($db->quoteName('#__template_styles')) - ->set($db->quoteName('inheritable') . ' = 1') - ->where($db->quoteName('template') . ' = ' . $db->quote($template)) - ->where($db->quoteName('client_id') . ' = ' . $clientId); - - try - { - $db->setQuery($query)->execute(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
    '; - - return; - } - }, - ['atum', 'cassiopeia'] - ); - } - - /** - * Add the user Auth Provider Column as it could be present from 3.10 already - * - * @return void - * - * @since 4.1.1 - */ - protected function addUserAuthProviderColumn(): void - { - $db = Factory::getContainer()->get('DatabaseDriver'); - - // Check if the column already exists - $fields = $db->getTableColumns('#__users'); - - // Column exists, skip - if (isset($fields['authProvider'])) - { - return; - } - - $query = 'ALTER TABLE ' . $db->quoteName('#__users') - . ' ADD COLUMN ' . $db->quoteName('authProvider') . ' varchar(100) DEFAULT ' . $db->quote('') . ' NOT NULL'; - - // Add column - try - { - $db->setQuery($query)->execute(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
    '; - - return; - } - } + /** + * The Joomla Version we are updating from + * + * @var string + * @since 3.7 + */ + protected $fromVersion = null; + + /** + * Function to act prior to installation process begins + * + * @param string $action Which action is happening (install|uninstall|discover_install|update) + * @param Installer $installer The class calling this method + * + * @return boolean True on success + * + * @since 3.7.0 + */ + public function preflight($action, $installer) + { + if ($action === 'update') { + // Get the version we are updating from + if (!empty($installer->extension->manifest_cache)) { + $manifestValues = json_decode($installer->extension->manifest_cache, true); + + if (array_key_exists('version', $manifestValues)) { + $this->fromVersion = $manifestValues['version']; + + // Ensure templates are moved to the correct mode + $this->fixTemplateMode(); + + return true; + } + } + + return false; + } + + return true; + } + + /** + * Method to update Joomla! + * + * @param Installer $installer The class calling this method + * + * @return void + */ + public function update($installer) + { + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'joomla_update.php'; + + Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); + + try { + Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_DELETE_FILES'), Log::INFO, 'Update'); + } catch (RuntimeException $exception) { + // Informational log only + } + + // Uninstall plugins before removing their files and folders + $this->uninstallRepeatableFieldsPlugin(); + $this->uninstallEosPlugin(); + + // This needs to stay for 2.5 update compatibility + $this->deleteUnexistingFiles(); + $this->updateManifestCaches(); + $this->updateDatabase(); + $this->updateAssets($installer); + $this->clearStatsCache(); + $this->convertTablesToUtf8mb4(true); + $this->addUserAuthProviderColumn(); + $this->cleanJoomlaCache(); + } + + /** + * Method to clear our stats plugin cache to ensure we get fresh data on Joomla Update + * + * @return void + * + * @since 3.5 + */ + protected function clearStatsCache() + { + $db = Factory::getDbo(); + + try { + // Get the params for the stats plugin + $params = $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('params')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('stats')) + )->loadResult(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
    '; + + return; + } + + $params = json_decode($params, true); + + // Reset the last run parameter + if (isset($params['lastrun'])) { + $params['lastrun'] = ''; + } + + $params = json_encode($params); + + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote($params)) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('stats')); + + try { + $db->setQuery($query)->execute(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
    '; + + return; + } + } + + /** + * Method to update Database + * + * @return void + */ + protected function updateDatabase() + { + if (Factory::getDbo()->getServerType() === 'mysql') { + $this->updateDatabaseMysql(); + } + } + + /** + * Method to update MySQL Database + * + * @return void + */ + protected function updateDatabaseMysql() + { + $db = Factory::getDbo(); + + $db->setQuery('SHOW ENGINES'); + + try { + $results = $db->loadObjectList(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
    '; + + return; + } + + foreach ($results as $result) { + if ($result->Support != 'DEFAULT') { + continue; + } + + $db->setQuery('ALTER TABLE #__update_sites_extensions ENGINE = ' . $result->Engine); + + try { + $db->execute(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
    '; + + return; + } + + break; + } + } + + /** + * Uninstalls the plg_fields_repeatable plugin and transforms its custom field instances + * to instances of the plg_fields_subfields plugin. + * + * @return void + * + * @since 4.0.0 + */ + protected function uninstallRepeatableFieldsPlugin() + { + $app = Factory::getApplication(); + $db = Factory::getDbo(); + + // Check if the plg_fields_repeatable plugin is present + $extensionId = $db->setQuery( + $db->getQuery(true) + ->select('extension_id') + ->from('#__extensions') + ->where('name = ' . $db->quote('plg_fields_repeatable')) + )->loadResult(); + + // Skip uninstalling when it doesn't exist + if (!$extensionId) { + return; + } + + // Ensure the FieldsHelper class is loaded for the Repeatable fields plugin we're about to remove + \JLoader::register('FieldsHelper', JPATH_ADMINISTRATOR . '/components/com_fields/helpers/fields.php'); + + try { + $db->transactionStart(); + + // Get the FieldsModelField, we need it in a sec + $fieldModel = $app->bootComponent('com_fields')->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); + /** @var FieldModel $fieldModel */ + + // Now get a list of all `repeatable` custom field instances + $db->setQuery( + $db->getQuery(true) + ->select('*') + ->from('#__fields') + ->where($db->quoteName('type') . ' = ' . $db->quote('repeatable')) + ); + + // Execute the query and iterate over the `repeatable` instances + foreach ($db->loadObjectList() as $row) { + // Skip broken rows - just a security measure, should not happen + if (!isset($row->fieldparams) || !($oldFieldparams = json_decode($row->fieldparams)) || !is_object($oldFieldparams)) { + continue; + } + + /** + * We basically want to transform this `repeatable` type into a `subfields` type. While $oldFieldparams + * holds the `fieldparams` of the `repeatable` type, $newFieldparams shall hold the `fieldparams` + * of the `subfields` type. + */ + $newFieldparams = [ + 'repeat' => '1', + 'options' => [], + ]; + + /** + * This array is used to store the mapping between the name of form fields from Repeatable field + * with ID of the child-fields. It will then be used to migrate data later + */ + $mapping = []; + + /** + * Store name of media fields which we need to convert data from old format (string) to new + * format (json) during the migration + */ + $mediaFields = []; + + // If this repeatable fields actually had child-fields (normally this is always the case) + if (isset($oldFieldparams->fields) && is_object($oldFieldparams->fields)) { + // Small counter for the child-fields (aka sub fields) + $newFieldCount = 0; + + // Iterate over the sub fields + foreach (get_object_vars($oldFieldparams->fields) as $oldField) { + // Used for field name collision prevention + $fieldname_prefix = ''; + $fieldname_suffix = 0; + + // Try to save the new sub field in a loop because of field name collisions + while (true) { + /** + * We basically want to create a completely new custom fields instance for every sub field + * of the `repeatable` instance. This is what we use $data for, we create a new custom field + * for each of the sub fields of the `repeatable` instance. + */ + $data = [ + 'context' => $row->context, + 'group_id' => $row->group_id, + 'title' => $oldField->fieldname, + 'name' => ( + $fieldname_prefix + . $oldField->fieldname + . ($fieldname_suffix > 0 ? ('_' . $fieldname_suffix) : '') + ), + 'label' => $oldField->fieldname, + 'default_value' => $row->default_value, + 'type' => $oldField->fieldtype, + 'description' => $row->description, + 'state' => '1', + 'params' => $row->params, + 'language' => '*', + 'assigned_cat_ids' => [-1], + 'only_use_in_subform' => 1, + ]; + + // `number` is not a valid custom field type, so use `text` instead. + if ($data['type'] == 'number') { + $data['type'] = 'text'; + } + + if ($data['type'] == 'media') { + $mediaFields[] = $oldField->fieldname; + } + + // Reset the state because else \Joomla\CMS\MVC\Model\AdminModel will take an already + // existing value (e.g. from previous save) and do an UPDATE instead of INSERT. + $fieldModel->setState('field.id', 0); + + // If an error occurred when trying to save this. + if (!$fieldModel->save($data)) { + // If the error is, that the name collided, increase the collision prevention + $error = $fieldModel->getError(); + + if ($error == 'COM_FIELDS_ERROR_UNIQUE_NAME') { + // If this is the first time this error occurs, set only the prefix + if ($fieldname_prefix == '') { + $fieldname_prefix = ($row->name . '_'); + } else { + // Else increase the suffix + $fieldname_suffix++; + } + + // And start again with the while loop. + continue 1; + } + + // Else bail out with the error. Something is totally wrong. + throw new \Exception($error); + } + + // Break out of the while loop, saving was successful. + break 1; + } + + // Get the newly created id + $subfield_id = $fieldModel->getState('field.id'); + + // Really check that it is valid + if (!is_numeric($subfield_id) || $subfield_id < 1) { + throw new \Exception('Something went wrong.'); + } + + // And tell our new `subfields` field about his child + $newFieldparams['options'][('option' . $newFieldCount)] = [ + 'customfield' => $subfield_id, + 'render_values' => '1', + ]; + + $newFieldCount++; + + $mapping[$oldField->fieldname] = 'field' . $subfield_id; + } + } + + // Write back the changed stuff to the database + $db->setQuery( + $db->getQuery(true) + ->update('#__fields') + ->set($db->quoteName('type') . ' = ' . $db->quote('subform')) + ->set($db->quoteName('fieldparams') . ' = ' . $db->quote(json_encode($newFieldparams))) + ->where($db->quoteName('id') . ' = ' . $db->quote($row->id)) + )->execute(); + + // Migrate data for this field + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__fields_values')) + ->where($db->quoteName('field_id') . ' = ' . $row->id); + $db->setQuery($query); + + foreach ($db->loadObjectList() as $rowFieldValue) { + // Do not do the version if no data is entered for the custom field this item + if (!$rowFieldValue->value) { + continue; + } + + /** + * Here we will have to update the stored value of the field to new format + * The key for each row changes from repeatable to row, for example repeatable0 to row0, and so on + * The key for each sub-field change from name of field to field + ID of the new sub-field + * Example data format stored in J3: {"repeatable0":{"id":"1","username":"admin"}} + * Example data format stored in J4: {"row0":{"field1":"1","field2":"admin"}} + */ + $newFieldValue = []; + + // Convert to array to change key + $fieldValue = json_decode($rowFieldValue->value, true); + + // If data could not be decoded for some reason, ignore + if (!$fieldValue) { + continue; + } + + $rowIndex = 0; + + foreach ($fieldValue as $rowKey => $rowValue) { + $rowKey = 'row' . ($rowIndex++); + $newFieldValue[$rowKey] = []; + + foreach ($rowValue as $subFieldName => $subFieldValue) { + // This is a media field, so we need to convert data to new format required in Joomla! 4 + if (in_array($subFieldName, $mediaFields)) { + $subFieldValue = ['imagefile' => $subFieldValue, 'alt_text' => '']; + } + + if (isset($mapping[$subFieldName])) { + $newFieldValue[$rowKey][$mapping[$subFieldName]] = $subFieldValue; + } else { + // Not found, use the old key to avoid data lost + $newFieldValue[$subFieldName] = $subFieldValue; + } + } + } + + $query->clear() + ->update($db->quoteName('#__fields_values')) + ->set($db->quoteName('value') . ' = ' . $db->quote(json_encode($newFieldValue))) + ->where($db->quoteName('field_id') . ' = ' . $rowFieldValue->field_id) + ->where($db->quoteName('item_id') . ' =' . $rowFieldValue->item_id); + $db->setQuery($query) + ->execute(); + } + } + + // Now, unprotect the plugin so we can uninstall it + $db->setQuery( + $db->getQuery(true) + ->update('#__extensions') + ->set('protected = 0') + ->where($db->quoteName('extension_id') . ' = ' . $extensionId) + )->execute(); + + // And now uninstall the plugin + $installer = new Installer(); + $installer->setDatabase($db); + $installer->uninstall('plugin', $extensionId); + + $db->transactionCommit(); + } catch (\Exception $e) { + $db->transactionRollback(); + throw $e; + } + } + + /** + * Uninstall the 3.10 EOS plugin + * + * @return void + * + * @since 4.0.0 + */ + protected function uninstallEosPlugin() + { + $db = Factory::getDbo(); + + // Check if the plg_quickicon_eos310 plugin is present + $extensionId = $db->setQuery( + $db->getQuery(true) + ->select('extension_id') + ->from('#__extensions') + ->where('name = ' . $db->quote('plg_quickicon_eos310')) + )->loadResult(); + + // Skip uninstalling if it doesn't exist + if (!$extensionId) { + return; + } + + try { + $db->transactionStart(); + + // Unprotect the plugin so we can uninstall it + $db->setQuery( + $db->getQuery(true) + ->update('#__extensions') + ->set('protected = 0') + ->where($db->quoteName('extension_id') . ' = ' . $extensionId) + )->execute(); + + // Uninstall the plugin + $installer = new Installer(); + $installer->setDatabase($db); + $installer->uninstall('plugin', $extensionId); + + $db->transactionCommit(); + } catch (\Exception $e) { + $db->transactionRollback(); + throw $e; + } + } + + /** + * Update the manifest caches + * + * @return void + */ + protected function updateManifestCaches() + { + $extensions = ExtensionHelper::getCoreExtensions(); + + // If we have the search package around, it may not have a manifest cache entry after upgrades from 3.x, so add it to the list + if (File::exists(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml')) { + $extensions[] = array('package', 'pkg_search', '', 0); + } + + // Attempt to refresh manifest caches + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('*') + ->from('#__extensions'); + + foreach ($extensions as $extension) { + $query->where( + 'type=' . $db->quote($extension[0]) + . ' AND element=' . $db->quote($extension[1]) + . ' AND folder=' . $db->quote($extension[2]) + . ' AND client_id=' . $extension[3], + 'OR' + ); + } + + $db->setQuery($query); + + try { + $extensions = $db->loadObjectList(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
    '; + + return; + } + + $installer = new Installer(); + $installer->setDatabase($db); + + foreach ($extensions as $extension) { + if (!$installer->refreshManifestCache($extension->extension_id)) { + echo Text::sprintf('FILES_JOOMLA_ERROR_MANIFEST', $extension->type, $extension->element, $extension->name, $extension->client_id) . '
    '; + } + } + } + + /** + * Delete files that should not exist + * + * @param bool $dryRun If set to true, will not actually delete files, but just report their status for use in CLI + * @param bool $suppressOutput Set to true to suppress echoing any errors, and just return the $status array + * + * @return array + */ + public function deleteUnexistingFiles($dryRun = false, $suppressOutput = false) + { + $status = [ + 'files_exist' => [], + 'folders_exist' => [], + 'files_deleted' => [], + 'folders_deleted' => [], + 'files_errors' => [], + 'folders_errors' => [], + 'folders_checked' => [], + 'files_checked' => [], + ]; + + $files = array( + // From 3.10 to 4.1 + '/administrator/components/com_actionlogs/actionlogs.php', + '/administrator/components/com_actionlogs/controller.php', + '/administrator/components/com_actionlogs/controllers/actionlogs.php', + '/administrator/components/com_actionlogs/helpers/actionlogs.php', + '/administrator/components/com_actionlogs/helpers/actionlogsphp55.php', + '/administrator/components/com_actionlogs/layouts/logstable.php', + '/administrator/components/com_actionlogs/libraries/actionlogplugin.php', + '/administrator/components/com_actionlogs/models/actionlog.php', + '/administrator/components/com_actionlogs/models/actionlogs.php', + '/administrator/components/com_actionlogs/models/fields/extension.php', + '/administrator/components/com_actionlogs/models/fields/logcreator.php', + '/administrator/components/com_actionlogs/models/fields/logsdaterange.php', + '/administrator/components/com_actionlogs/models/fields/logtype.php', + '/administrator/components/com_actionlogs/models/fields/plugininfo.php', + '/administrator/components/com_actionlogs/models/forms/filter_actionlogs.xml', + '/administrator/components/com_actionlogs/views/actionlogs/tmpl/default.php', + '/administrator/components/com_actionlogs/views/actionlogs/tmpl/default.xml', + '/administrator/components/com_actionlogs/views/actionlogs/view.html.php', + '/administrator/components/com_admin/admin.php', + '/administrator/components/com_admin/controller.php', + '/administrator/components/com_admin/controllers/profile.php', + '/administrator/components/com_admin/helpers/html/directory.php', + '/administrator/components/com_admin/helpers/html/phpsetting.php', + '/administrator/components/com_admin/helpers/html/system.php', + '/administrator/components/com_admin/models/forms/profile.xml', + '/administrator/components/com_admin/models/help.php', + '/administrator/components/com_admin/models/profile.php', + '/administrator/components/com_admin/models/sysinfo.php', + '/administrator/components/com_admin/postinstall/eaccelerator.php', + '/administrator/components/com_admin/postinstall/htaccess.php', + '/administrator/components/com_admin/postinstall/joomla40checks.php', + '/administrator/components/com_admin/postinstall/updatedefaultsettings.php', + '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-01.sql', + '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-02.sql', + '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-06.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-21-1.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-21-2.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-23.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2012-01-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2012-01-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.1-2012-01-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.2-2012-03-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.3-2012-03-13.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.4-2012-03-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.4-2012-03-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.5.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.6.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.7.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.0.0.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.0.1.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.0.2.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.0.3.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.0.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.1.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.2.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.3.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.4.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.5.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.10.0-2020-08-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.10.0-2021-05-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.10.7-2022-02-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.10.7-2022-03-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.0.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.1.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2013-12-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2013-12-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-08.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-23.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.3-2014-02-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.3.0-2014-02-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.3.0-2014-04-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.3.4-2014-08-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.3.6-2014-09-30.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-08-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-09-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-09-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-10-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-12-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2015-01-21.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2015-02-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-07-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-13.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-30.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-11-04.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-11-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2016-02-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2016-03-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.1-2016-03-25.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.1-2016-03-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-06.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-08.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-09.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-05-06.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-06-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-06-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.3-2016-08-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.3-2016-08-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-06.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-09-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-10-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-10-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-04.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-21.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-27.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-08.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-09.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-17.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-31.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-17.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-09.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-04-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-04-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.3-2017-06-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.4-2017-07-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.0-2017-07-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.0-2017-07-31.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.2-2017-10-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.4-2018-01-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.6-2018-02-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.8-2018-05-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.9-2018-06-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-27.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-12.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-13.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-17.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-09.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-11.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-12.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-09-04.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-21.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.10-2019-07-09.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.16-2020-02-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.16-2020-03-04.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.19-2020-05-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.19-2020-06-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.21-2020-08-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.22-2020-09-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.26-2021-04-07.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.27-2021-04-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.3-2019-01-12.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.3-2019-02-07.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-04-23.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-04-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-05-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.8-2019-06-11.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.8-2019-06-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.0.0.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.0.1.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.0.2.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.0.3.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.0.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.1.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.2.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.3.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.4.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.5.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.10.0-2020-08-10.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.10.0-2021-05-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-02-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-02-20.sql.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-03-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.0.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.1.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2013-12-22.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2013-12-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-23.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.3-2014-02-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2013-12-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2014-02-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2014-04-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.3.4-2014-08-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.3.6-2014-09-30.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-08-24.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-09-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-09-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-10-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-12-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2015-01-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2015-02-26.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.4-2015-07-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-13.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-26.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-30.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-11-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-11-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2016-03-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-09.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-05-06.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-06-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-06-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-08-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-08-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-10-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-06.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-22.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-09-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-10-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-10-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-24.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-09.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-17.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-31.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-17.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-03-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-03-09.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-04-10.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-04-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.4-2017-07-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.0-2017-07-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.0-2017-07-31.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.2-2017-10-14.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.4-2018-01-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.6-2018-02-14.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.8-2018-05-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.9-2018-06-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-24.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-27.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-12.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-13.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-14.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-17.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-09.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-10.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-12.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-09-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.10-2019-07-09.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.15-2020-01-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.16-2020-02-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.16-2020-03-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.19-2020-06-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.21-2020-08-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.22-2020-09-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.26-2021-04-07.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.27-2021-04-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.3-2019-01-12.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.3-2019-02-07.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-04-23.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-04-26.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-05-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.8-2019-06-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.8-2019-06-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.2-2012-03-05.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.3-2012-03-13.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.4-2012-03-18.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.4-2012-03-19.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.5.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.6.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.7.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.0.0.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.0.1.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.0.2.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.0.3.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.0.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.1.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.2.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.3.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.4.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.5.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.10.0-2021-05-28.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.10.1-2021-08-17.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-02-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-02-20.sql.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-03-18.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.0.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.1.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2013-12-22.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2013-12-28.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-08.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-18.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-23.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.3-2014-02-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.3.0-2014-02-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.3.0-2014-04-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.3.4-2014-08-03.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.3.6-2014-09-30.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-08-24.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-09-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-09-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-10-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-12-03.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2015-01-21.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2015-02-26.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.4-2015-07-11.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-13.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-26.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-30.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-11-04.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-11-05.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2016-03-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-06.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-08.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-09.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-05-06.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-06-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-06-05.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.3-2016-08-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.3-2016-08-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-06.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-22.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-29.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-09-29.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-10-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-10-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-04.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-19.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-24.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-08.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-09.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-17.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-31.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-17.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-03-03.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-03-09.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-04-10.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-04-19.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.4-2017-07-05.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.0-2017-07-28.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.0-2017-07-31.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.2-2017-10-14.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.4-2018-01-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.6-2018-02-14.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.8-2018-05-18.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.9-2018-06-19.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-03.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-05.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-19.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-24.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-27.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-12.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-13.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-14.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-17.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-09.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-10.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-11.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-12.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-28.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-29.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-09-04.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-21.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.10-2019-07-09.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.16-2020-03-04.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.19-2020-06-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.21-2020-08-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.22-2020-09-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.26-2021-04-07.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.27-2021-04-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.3-2019-01-12.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.3-2019-02-07.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.4-2019-03-06.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-04-23.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-04-26.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-05-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.8-2019-06-11.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.8-2019-06-15.sql', + '/administrator/components/com_admin/views/help/tmpl/default.php', + '/administrator/components/com_admin/views/help/tmpl/default.xml', + '/administrator/components/com_admin/views/help/tmpl/langforum.php', + '/administrator/components/com_admin/views/help/view.html.php', + '/administrator/components/com_admin/views/profile/tmpl/edit.php', + '/administrator/components/com_admin/views/profile/view.html.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default.xml', + '/administrator/components/com_admin/views/sysinfo/tmpl/default_config.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default_directory.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default_phpinfo.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default_phpsettings.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default_system.php', + '/administrator/components/com_admin/views/sysinfo/view.html.php', + '/administrator/components/com_admin/views/sysinfo/view.json.php', + '/administrator/components/com_admin/views/sysinfo/view.text.php', + '/administrator/components/com_associations/associations.php', + '/administrator/components/com_associations/controller.php', + '/administrator/components/com_associations/controllers/association.php', + '/administrator/components/com_associations/controllers/associations.php', + '/administrator/components/com_associations/helpers/associations.php', + '/administrator/components/com_associations/layouts/joomla/searchtools/default/bar.php', + '/administrator/components/com_associations/models/association.php', + '/administrator/components/com_associations/models/associations.php', + '/administrator/components/com_associations/models/fields/itemlanguage.php', + '/administrator/components/com_associations/models/fields/itemtype.php', + '/administrator/components/com_associations/models/fields/modalassociation.php', + '/administrator/components/com_associations/models/forms/association.xml', + '/administrator/components/com_associations/models/forms/filter_associations.xml', + '/administrator/components/com_associations/views/association/tmpl/edit.php', + '/administrator/components/com_associations/views/association/view.html.php', + '/administrator/components/com_associations/views/associations/tmpl/default.php', + '/administrator/components/com_associations/views/associations/tmpl/default.xml', + '/administrator/components/com_associations/views/associations/tmpl/modal.php', + '/administrator/components/com_associations/views/associations/view.html.php', + '/administrator/components/com_banners/banners.php', + '/administrator/components/com_banners/controller.php', + '/administrator/components/com_banners/controllers/banner.php', + '/administrator/components/com_banners/controllers/banners.php', + '/administrator/components/com_banners/controllers/client.php', + '/administrator/components/com_banners/controllers/clients.php', + '/administrator/components/com_banners/controllers/tracks.php', + '/administrator/components/com_banners/controllers/tracks.raw.php', + '/administrator/components/com_banners/helpers/html/banner.php', + '/administrator/components/com_banners/models/banner.php', + '/administrator/components/com_banners/models/banners.php', + '/administrator/components/com_banners/models/client.php', + '/administrator/components/com_banners/models/clients.php', + '/administrator/components/com_banners/models/download.php', + '/administrator/components/com_banners/models/fields/bannerclient.php', + '/administrator/components/com_banners/models/fields/clicks.php', + '/administrator/components/com_banners/models/fields/impmade.php', + '/administrator/components/com_banners/models/fields/imptotal.php', + '/administrator/components/com_banners/models/forms/banner.xml', + '/administrator/components/com_banners/models/forms/client.xml', + '/administrator/components/com_banners/models/forms/download.xml', + '/administrator/components/com_banners/models/forms/filter_banners.xml', + '/administrator/components/com_banners/models/forms/filter_clients.xml', + '/administrator/components/com_banners/models/forms/filter_tracks.xml', + '/administrator/components/com_banners/models/tracks.php', + '/administrator/components/com_banners/tables/banner.php', + '/administrator/components/com_banners/tables/client.php', + '/administrator/components/com_banners/views/banner/tmpl/edit.php', + '/administrator/components/com_banners/views/banner/view.html.php', + '/administrator/components/com_banners/views/banners/tmpl/default.php', + '/administrator/components/com_banners/views/banners/tmpl/default_batch_body.php', + '/administrator/components/com_banners/views/banners/tmpl/default_batch_footer.php', + '/administrator/components/com_banners/views/banners/view.html.php', + '/administrator/components/com_banners/views/client/tmpl/edit.php', + '/administrator/components/com_banners/views/client/view.html.php', + '/administrator/components/com_banners/views/clients/tmpl/default.php', + '/administrator/components/com_banners/views/clients/view.html.php', + '/administrator/components/com_banners/views/download/tmpl/default.php', + '/administrator/components/com_banners/views/download/view.html.php', + '/administrator/components/com_banners/views/tracks/tmpl/default.php', + '/administrator/components/com_banners/views/tracks/view.html.php', + '/administrator/components/com_banners/views/tracks/view.raw.php', + '/administrator/components/com_cache/cache.php', + '/administrator/components/com_cache/controller.php', + '/administrator/components/com_cache/helpers/cache.php', + '/administrator/components/com_cache/models/cache.php', + '/administrator/components/com_cache/models/forms/filter_cache.xml', + '/administrator/components/com_cache/views/cache/tmpl/default.php', + '/administrator/components/com_cache/views/cache/tmpl/default.xml', + '/administrator/components/com_cache/views/cache/view.html.php', + '/administrator/components/com_cache/views/purge/tmpl/default.php', + '/administrator/components/com_cache/views/purge/tmpl/default.xml', + '/administrator/components/com_cache/views/purge/view.html.php', + '/administrator/components/com_categories/categories.php', + '/administrator/components/com_categories/controller.php', + '/administrator/components/com_categories/controllers/ajax.json.php', + '/administrator/components/com_categories/controllers/categories.php', + '/administrator/components/com_categories/controllers/category.php', + '/administrator/components/com_categories/helpers/association.php', + '/administrator/components/com_categories/helpers/html/categoriesadministrator.php', + '/administrator/components/com_categories/models/categories.php', + '/administrator/components/com_categories/models/category.php', + '/administrator/components/com_categories/models/fields/categoryedit.php', + '/administrator/components/com_categories/models/fields/categoryparent.php', + '/administrator/components/com_categories/models/fields/modal/category.php', + '/administrator/components/com_categories/models/forms/category.xml', + '/administrator/components/com_categories/models/forms/filter_categories.xml', + '/administrator/components/com_categories/tables/category.php', + '/administrator/components/com_categories/views/categories/tmpl/default.php', + '/administrator/components/com_categories/views/categories/tmpl/default.xml', + '/administrator/components/com_categories/views/categories/tmpl/default_batch_body.php', + '/administrator/components/com_categories/views/categories/tmpl/default_batch_footer.php', + '/administrator/components/com_categories/views/categories/tmpl/modal.php', + '/administrator/components/com_categories/views/categories/view.html.php', + '/administrator/components/com_categories/views/category/tmpl/edit.php', + '/administrator/components/com_categories/views/category/tmpl/edit.xml', + '/administrator/components/com_categories/views/category/tmpl/edit_associations.php', + '/administrator/components/com_categories/views/category/tmpl/edit_metadata.php', + '/administrator/components/com_categories/views/category/tmpl/modal.php', + '/administrator/components/com_categories/views/category/tmpl/modal_associations.php', + '/administrator/components/com_categories/views/category/tmpl/modal_extrafields.php', + '/administrator/components/com_categories/views/category/tmpl/modal_metadata.php', + '/administrator/components/com_categories/views/category/tmpl/modal_options.php', + '/administrator/components/com_categories/views/category/view.html.php', + '/administrator/components/com_checkin/checkin.php', + '/administrator/components/com_checkin/controller.php', + '/administrator/components/com_checkin/models/checkin.php', + '/administrator/components/com_checkin/models/forms/filter_checkin.xml', + '/administrator/components/com_checkin/views/checkin/tmpl/default.php', + '/administrator/components/com_checkin/views/checkin/tmpl/default.xml', + '/administrator/components/com_checkin/views/checkin/view.html.php', + '/administrator/components/com_config/config.php', + '/administrator/components/com_config/controller.php', + '/administrator/components/com_config/controller/application/cancel.php', + '/administrator/components/com_config/controller/application/display.php', + '/administrator/components/com_config/controller/application/removeroot.php', + '/administrator/components/com_config/controller/application/save.php', + '/administrator/components/com_config/controller/application/sendtestmail.php', + '/administrator/components/com_config/controller/application/store.php', + '/administrator/components/com_config/controller/component/cancel.php', + '/administrator/components/com_config/controller/component/display.php', + '/administrator/components/com_config/controller/component/save.php', + '/administrator/components/com_config/controllers/application.php', + '/administrator/components/com_config/controllers/component.php', + '/administrator/components/com_config/helper/config.php', + '/administrator/components/com_config/model/application.php', + '/administrator/components/com_config/model/component.php', + '/administrator/components/com_config/model/field/configcomponents.php', + '/administrator/components/com_config/model/field/filters.php', + '/administrator/components/com_config/model/form/application.xml', + '/administrator/components/com_config/models/application.php', + '/administrator/components/com_config/models/component.php', + '/administrator/components/com_config/view/application/html.php', + '/administrator/components/com_config/view/application/json.php', + '/administrator/components/com_config/view/application/tmpl/default.php', + '/administrator/components/com_config/view/application/tmpl/default.xml', + '/administrator/components/com_config/view/application/tmpl/default_cache.php', + '/administrator/components/com_config/view/application/tmpl/default_cookie.php', + '/administrator/components/com_config/view/application/tmpl/default_database.php', + '/administrator/components/com_config/view/application/tmpl/default_debug.php', + '/administrator/components/com_config/view/application/tmpl/default_filters.php', + '/administrator/components/com_config/view/application/tmpl/default_ftp.php', + '/administrator/components/com_config/view/application/tmpl/default_ftplogin.php', + '/administrator/components/com_config/view/application/tmpl/default_locale.php', + '/administrator/components/com_config/view/application/tmpl/default_mail.php', + '/administrator/components/com_config/view/application/tmpl/default_metadata.php', + '/administrator/components/com_config/view/application/tmpl/default_navigation.php', + '/administrator/components/com_config/view/application/tmpl/default_permissions.php', + '/administrator/components/com_config/view/application/tmpl/default_proxy.php', + '/administrator/components/com_config/view/application/tmpl/default_seo.php', + '/administrator/components/com_config/view/application/tmpl/default_server.php', + '/administrator/components/com_config/view/application/tmpl/default_session.php', + '/administrator/components/com_config/view/application/tmpl/default_site.php', + '/administrator/components/com_config/view/application/tmpl/default_system.php', + '/administrator/components/com_config/view/component/html.php', + '/administrator/components/com_config/view/component/tmpl/default.php', + '/administrator/components/com_config/view/component/tmpl/default.xml', + '/administrator/components/com_config/view/component/tmpl/default_navigation.php', + '/administrator/components/com_contact/contact.php', + '/administrator/components/com_contact/controller.php', + '/administrator/components/com_contact/controllers/ajax.json.php', + '/administrator/components/com_contact/controllers/contact.php', + '/administrator/components/com_contact/controllers/contacts.php', + '/administrator/components/com_contact/helpers/associations.php', + '/administrator/components/com_contact/helpers/html/contact.php', + '/administrator/components/com_contact/models/contact.php', + '/administrator/components/com_contact/models/contacts.php', + '/administrator/components/com_contact/models/fields/modal/contact.php', + '/administrator/components/com_contact/models/forms/contact.xml', + '/administrator/components/com_contact/models/forms/fields/mail.xml', + '/administrator/components/com_contact/models/forms/filter_contacts.xml', + '/administrator/components/com_contact/tables/contact.php', + '/administrator/components/com_contact/views/contact/tmpl/edit.php', + '/administrator/components/com_contact/views/contact/tmpl/edit_associations.php', + '/administrator/components/com_contact/views/contact/tmpl/edit_metadata.php', + '/administrator/components/com_contact/views/contact/tmpl/edit_params.php', + '/administrator/components/com_contact/views/contact/tmpl/modal.php', + '/administrator/components/com_contact/views/contact/tmpl/modal_associations.php', + '/administrator/components/com_contact/views/contact/tmpl/modal_metadata.php', + '/administrator/components/com_contact/views/contact/tmpl/modal_params.php', + '/administrator/components/com_contact/views/contact/view.html.php', + '/administrator/components/com_contact/views/contacts/tmpl/default.php', + '/administrator/components/com_contact/views/contacts/tmpl/default_batch.php', + '/administrator/components/com_contact/views/contacts/tmpl/default_batch_body.php', + '/administrator/components/com_contact/views/contacts/tmpl/default_batch_footer.php', + '/administrator/components/com_contact/views/contacts/tmpl/modal.php', + '/administrator/components/com_contact/views/contacts/view.html.php', + '/administrator/components/com_content/content.php', + '/administrator/components/com_content/controller.php', + '/administrator/components/com_content/controllers/ajax.json.php', + '/administrator/components/com_content/controllers/article.php', + '/administrator/components/com_content/controllers/articles.php', + '/administrator/components/com_content/controllers/featured.php', + '/administrator/components/com_content/helpers/associations.php', + '/administrator/components/com_content/helpers/html/contentadministrator.php', + '/administrator/components/com_content/models/article.php', + '/administrator/components/com_content/models/articles.php', + '/administrator/components/com_content/models/feature.php', + '/administrator/components/com_content/models/featured.php', + '/administrator/components/com_content/models/fields/modal/article.php', + '/administrator/components/com_content/models/fields/voteradio.php', + '/administrator/components/com_content/models/forms/article.xml', + '/administrator/components/com_content/models/forms/filter_articles.xml', + '/administrator/components/com_content/models/forms/filter_featured.xml', + '/administrator/components/com_content/tables/featured.php', + '/administrator/components/com_content/views/article/tmpl/edit.php', + '/administrator/components/com_content/views/article/tmpl/edit.xml', + '/administrator/components/com_content/views/article/tmpl/edit_associations.php', + '/administrator/components/com_content/views/article/tmpl/edit_metadata.php', + '/administrator/components/com_content/views/article/tmpl/modal.php', + '/administrator/components/com_content/views/article/tmpl/modal_associations.php', + '/administrator/components/com_content/views/article/tmpl/modal_metadata.php', + '/administrator/components/com_content/views/article/tmpl/pagebreak.php', + '/administrator/components/com_content/views/article/view.html.php', + '/administrator/components/com_content/views/articles/tmpl/default.php', + '/administrator/components/com_content/views/articles/tmpl/default.xml', + '/administrator/components/com_content/views/articles/tmpl/default_batch_body.php', + '/administrator/components/com_content/views/articles/tmpl/default_batch_footer.php', + '/administrator/components/com_content/views/articles/tmpl/modal.php', + '/administrator/components/com_content/views/articles/view.html.php', + '/administrator/components/com_content/views/featured/tmpl/default.php', + '/administrator/components/com_content/views/featured/tmpl/default.xml', + '/administrator/components/com_content/views/featured/view.html.php', + '/administrator/components/com_contenthistory/contenthistory.php', + '/administrator/components/com_contenthistory/controller.php', + '/administrator/components/com_contenthistory/controllers/history.php', + '/administrator/components/com_contenthistory/controllers/preview.php', + '/administrator/components/com_contenthistory/helpers/html/textdiff.php', + '/administrator/components/com_contenthistory/models/compare.php', + '/administrator/components/com_contenthistory/models/history.php', + '/administrator/components/com_contenthistory/models/preview.php', + '/administrator/components/com_contenthistory/views/compare/tmpl/compare.php', + '/administrator/components/com_contenthistory/views/compare/view.html.php', + '/administrator/components/com_contenthistory/views/history/tmpl/modal.php', + '/administrator/components/com_contenthistory/views/history/view.html.php', + '/administrator/components/com_contenthistory/views/preview/tmpl/preview.php', + '/administrator/components/com_contenthistory/views/preview/view.html.php', + '/administrator/components/com_cpanel/controller.php', + '/administrator/components/com_cpanel/cpanel.php', + '/administrator/components/com_cpanel/views/cpanel/tmpl/default.php', + '/administrator/components/com_cpanel/views/cpanel/tmpl/default.xml', + '/administrator/components/com_cpanel/views/cpanel/view.html.php', + '/administrator/components/com_fields/controller.php', + '/administrator/components/com_fields/controllers/field.php', + '/administrator/components/com_fields/controllers/fields.php', + '/administrator/components/com_fields/controllers/group.php', + '/administrator/components/com_fields/controllers/groups.php', + '/administrator/components/com_fields/fields.php', + '/administrator/components/com_fields/libraries/fieldslistplugin.php', + '/administrator/components/com_fields/libraries/fieldsplugin.php', + '/administrator/components/com_fields/models/field.php', + '/administrator/components/com_fields/models/fields.php', + '/administrator/components/com_fields/models/fields/fieldcontexts.php', + '/administrator/components/com_fields/models/fields/fieldgroups.php', + '/administrator/components/com_fields/models/fields/fieldlayout.php', + '/administrator/components/com_fields/models/fields/section.php', + '/administrator/components/com_fields/models/fields/type.php', + '/administrator/components/com_fields/models/forms/field.xml', + '/administrator/components/com_fields/models/forms/filter_fields.xml', + '/administrator/components/com_fields/models/forms/filter_groups.xml', + '/administrator/components/com_fields/models/forms/group.xml', + '/administrator/components/com_fields/models/group.php', + '/administrator/components/com_fields/models/groups.php', + '/administrator/components/com_fields/tables/field.php', + '/administrator/components/com_fields/tables/group.php', + '/administrator/components/com_fields/views/field/tmpl/edit.php', + '/administrator/components/com_fields/views/field/view.html.php', + '/administrator/components/com_fields/views/fields/tmpl/default.php', + '/administrator/components/com_fields/views/fields/tmpl/default_batch_body.php', + '/administrator/components/com_fields/views/fields/tmpl/default_batch_footer.php', + '/administrator/components/com_fields/views/fields/tmpl/modal.php', + '/administrator/components/com_fields/views/fields/view.html.php', + '/administrator/components/com_fields/views/group/tmpl/edit.php', + '/administrator/components/com_fields/views/group/view.html.php', + '/administrator/components/com_fields/views/groups/tmpl/default.php', + '/administrator/components/com_fields/views/groups/tmpl/default_batch_body.php', + '/administrator/components/com_fields/views/groups/tmpl/default_batch_footer.php', + '/administrator/components/com_fields/views/groups/view.html.php', + '/administrator/components/com_finder/controller.php', + '/administrator/components/com_finder/controllers/filter.php', + '/administrator/components/com_finder/controllers/filters.php', + '/administrator/components/com_finder/controllers/index.php', + '/administrator/components/com_finder/controllers/indexer.json.php', + '/administrator/components/com_finder/controllers/maps.php', + '/administrator/components/com_finder/finder.php', + '/administrator/components/com_finder/helpers/finder.php', + '/administrator/components/com_finder/helpers/html/finder.php', + '/administrator/components/com_finder/helpers/indexer/driver/mysql.php', + '/administrator/components/com_finder/helpers/indexer/driver/postgresql.php', + '/administrator/components/com_finder/helpers/indexer/driver/sqlsrv.php', + '/administrator/components/com_finder/helpers/indexer/indexer.php', + '/administrator/components/com_finder/helpers/indexer/parser/html.php', + '/administrator/components/com_finder/helpers/indexer/parser/rtf.php', + '/administrator/components/com_finder/helpers/indexer/parser/txt.php', + '/administrator/components/com_finder/helpers/indexer/stemmer.php', + '/administrator/components/com_finder/helpers/indexer/stemmer/fr.php', + '/administrator/components/com_finder/helpers/indexer/stemmer/porter_en.php', + '/administrator/components/com_finder/helpers/indexer/stemmer/snowball.php', + '/administrator/components/com_finder/models/fields/branches.php', + '/administrator/components/com_finder/models/fields/contentmap.php', + '/administrator/components/com_finder/models/fields/contenttypes.php', + '/administrator/components/com_finder/models/fields/directories.php', + '/administrator/components/com_finder/models/fields/searchfilter.php', + '/administrator/components/com_finder/models/filter.php', + '/administrator/components/com_finder/models/filters.php', + '/administrator/components/com_finder/models/forms/filter.xml', + '/administrator/components/com_finder/models/forms/filter_filters.xml', + '/administrator/components/com_finder/models/forms/filter_index.xml', + '/administrator/components/com_finder/models/forms/filter_maps.xml', + '/administrator/components/com_finder/models/index.php', + '/administrator/components/com_finder/models/indexer.php', + '/administrator/components/com_finder/models/maps.php', + '/administrator/components/com_finder/models/statistics.php', + '/administrator/components/com_finder/tables/filter.php', + '/administrator/components/com_finder/tables/link.php', + '/administrator/components/com_finder/tables/map.php', + '/administrator/components/com_finder/views/filter/tmpl/edit.php', + '/administrator/components/com_finder/views/filter/view.html.php', + '/administrator/components/com_finder/views/filters/tmpl/default.php', + '/administrator/components/com_finder/views/filters/view.html.php', + '/administrator/components/com_finder/views/index/tmpl/default.php', + '/administrator/components/com_finder/views/index/view.html.php', + '/administrator/components/com_finder/views/indexer/tmpl/default.php', + '/administrator/components/com_finder/views/indexer/view.html.php', + '/administrator/components/com_finder/views/maps/tmpl/default.php', + '/administrator/components/com_finder/views/maps/view.html.php', + '/administrator/components/com_finder/views/statistics/tmpl/default.php', + '/administrator/components/com_finder/views/statistics/view.html.php', + '/administrator/components/com_installer/controller.php', + '/administrator/components/com_installer/controllers/database.php', + '/administrator/components/com_installer/controllers/discover.php', + '/administrator/components/com_installer/controllers/install.php', + '/administrator/components/com_installer/controllers/manage.php', + '/administrator/components/com_installer/controllers/update.php', + '/administrator/components/com_installer/controllers/updatesites.php', + '/administrator/components/com_installer/helpers/html/manage.php', + '/administrator/components/com_installer/helpers/html/updatesites.php', + '/administrator/components/com_installer/installer.php', + '/administrator/components/com_installer/models/database.php', + '/administrator/components/com_installer/models/discover.php', + '/administrator/components/com_installer/models/extension.php', + '/administrator/components/com_installer/models/fields/extensionstatus.php', + '/administrator/components/com_installer/models/fields/folder.php', + '/administrator/components/com_installer/models/fields/location.php', + '/administrator/components/com_installer/models/fields/type.php', + '/administrator/components/com_installer/models/forms/filter_discover.xml', + '/administrator/components/com_installer/models/forms/filter_languages.xml', + '/administrator/components/com_installer/models/forms/filter_manage.xml', + '/administrator/components/com_installer/models/forms/filter_update.xml', + '/administrator/components/com_installer/models/forms/filter_updatesites.xml', + '/administrator/components/com_installer/models/install.php', + '/administrator/components/com_installer/models/languages.php', + '/administrator/components/com_installer/models/manage.php', + '/administrator/components/com_installer/models/update.php', + '/administrator/components/com_installer/models/updatesites.php', + '/administrator/components/com_installer/models/warnings.php', + '/administrator/components/com_installer/views/database/tmpl/default.php', + '/administrator/components/com_installer/views/database/tmpl/default.xml', + '/administrator/components/com_installer/views/database/view.html.php', + '/administrator/components/com_installer/views/default/tmpl/default_ftp.php', + '/administrator/components/com_installer/views/default/tmpl/default_message.php', + '/administrator/components/com_installer/views/default/view.php', + '/administrator/components/com_installer/views/discover/tmpl/default.php', + '/administrator/components/com_installer/views/discover/tmpl/default.xml', + '/administrator/components/com_installer/views/discover/tmpl/default_item.php', + '/administrator/components/com_installer/views/discover/view.html.php', + '/administrator/components/com_installer/views/install/tmpl/default.php', + '/administrator/components/com_installer/views/install/tmpl/default.xml', + '/administrator/components/com_installer/views/install/view.html.php', + '/administrator/components/com_installer/views/languages/tmpl/default.php', + '/administrator/components/com_installer/views/languages/tmpl/default.xml', + '/administrator/components/com_installer/views/languages/view.html.php', + '/administrator/components/com_installer/views/manage/tmpl/default.php', + '/administrator/components/com_installer/views/manage/tmpl/default.xml', + '/administrator/components/com_installer/views/manage/view.html.php', + '/administrator/components/com_installer/views/update/tmpl/default.php', + '/administrator/components/com_installer/views/update/tmpl/default.xml', + '/administrator/components/com_installer/views/update/view.html.php', + '/administrator/components/com_installer/views/updatesites/tmpl/default.php', + '/administrator/components/com_installer/views/updatesites/tmpl/default.xml', + '/administrator/components/com_installer/views/updatesites/view.html.php', + '/administrator/components/com_installer/views/warnings/tmpl/default.php', + '/administrator/components/com_installer/views/warnings/tmpl/default.xml', + '/administrator/components/com_installer/views/warnings/view.html.php', + '/administrator/components/com_joomlaupdate/controller.php', + '/administrator/components/com_joomlaupdate/controllers/update.php', + '/administrator/components/com_joomlaupdate/helpers/joomlaupdate.php', + '/administrator/components/com_joomlaupdate/helpers/select.php', + '/administrator/components/com_joomlaupdate/joomlaupdate.php', + '/administrator/components/com_joomlaupdate/models/default.php', + '/administrator/components/com_joomlaupdate/restore.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/complete.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default.xml', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_nodownload.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_noupdate.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_preupdatecheck.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_reinstall.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_update.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_updatemefirst.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_upload.php', + '/administrator/components/com_joomlaupdate/views/default/view.html.php', + '/administrator/components/com_joomlaupdate/views/update/tmpl/default.php', + '/administrator/components/com_joomlaupdate/views/update/tmpl/finaliseconfirm.php', + '/administrator/components/com_joomlaupdate/views/update/view.html.php', + '/administrator/components/com_joomlaupdate/views/upload/tmpl/captive.php', + '/administrator/components/com_joomlaupdate/views/upload/view.html.php', + '/administrator/components/com_languages/controller.php', + '/administrator/components/com_languages/controllers/installed.php', + '/administrator/components/com_languages/controllers/language.php', + '/administrator/components/com_languages/controllers/languages.php', + '/administrator/components/com_languages/controllers/override.php', + '/administrator/components/com_languages/controllers/overrides.php', + '/administrator/components/com_languages/controllers/strings.json.php', + '/administrator/components/com_languages/helpers/html/languages.php', + '/administrator/components/com_languages/helpers/jsonresponse.php', + '/administrator/components/com_languages/helpers/languages.php', + '/administrator/components/com_languages/helpers/multilangstatus.php', + '/administrator/components/com_languages/languages.php', + '/administrator/components/com_languages/layouts/joomla/searchtools/default/bar.php', + '/administrator/components/com_languages/models/fields/languageclient.php', + '/administrator/components/com_languages/models/forms/filter_installed.xml', + '/administrator/components/com_languages/models/forms/filter_languages.xml', + '/administrator/components/com_languages/models/forms/filter_overrides.xml', + '/administrator/components/com_languages/models/forms/language.xml', + '/administrator/components/com_languages/models/forms/override.xml', + '/administrator/components/com_languages/models/installed.php', + '/administrator/components/com_languages/models/language.php', + '/administrator/components/com_languages/models/languages.php', + '/administrator/components/com_languages/models/override.php', + '/administrator/components/com_languages/models/overrides.php', + '/administrator/components/com_languages/models/strings.php', + '/administrator/components/com_languages/views/installed/tmpl/default.php', + '/administrator/components/com_languages/views/installed/tmpl/default.xml', + '/administrator/components/com_languages/views/installed/view.html.php', + '/administrator/components/com_languages/views/language/tmpl/edit.php', + '/administrator/components/com_languages/views/language/view.html.php', + '/administrator/components/com_languages/views/languages/tmpl/default.php', + '/administrator/components/com_languages/views/languages/tmpl/default.xml', + '/administrator/components/com_languages/views/languages/view.html.php', + '/administrator/components/com_languages/views/multilangstatus/tmpl/default.php', + '/administrator/components/com_languages/views/multilangstatus/view.html.php', + '/administrator/components/com_languages/views/override/tmpl/edit.php', + '/administrator/components/com_languages/views/override/view.html.php', + '/administrator/components/com_languages/views/overrides/tmpl/default.php', + '/administrator/components/com_languages/views/overrides/tmpl/default.xml', + '/administrator/components/com_languages/views/overrides/view.html.php', + '/administrator/components/com_login/controller.php', + '/administrator/components/com_login/login.php', + '/administrator/components/com_login/models/login.php', + '/administrator/components/com_login/views/login/tmpl/default.php', + '/administrator/components/com_login/views/login/view.html.php', + '/administrator/components/com_media/controller.php', + '/administrator/components/com_media/controllers/file.json.php', + '/administrator/components/com_media/controllers/file.php', + '/administrator/components/com_media/controllers/folder.php', + '/administrator/components/com_media/layouts/toolbar/deletemedia.php', + '/administrator/components/com_media/layouts/toolbar/newfolder.php', + '/administrator/components/com_media/layouts/toolbar/uploadmedia.php', + '/administrator/components/com_media/media.php', + '/administrator/components/com_media/models/list.php', + '/administrator/components/com_media/models/manager.php', + '/administrator/components/com_media/views/images/tmpl/default.php', + '/administrator/components/com_media/views/images/view.html.php', + '/administrator/components/com_media/views/imageslist/tmpl/default.php', + '/administrator/components/com_media/views/imageslist/tmpl/default_folder.php', + '/administrator/components/com_media/views/imageslist/tmpl/default_image.php', + '/administrator/components/com_media/views/imageslist/view.html.php', + '/administrator/components/com_media/views/media/tmpl/default.php', + '/administrator/components/com_media/views/media/tmpl/default.xml', + '/administrator/components/com_media/views/media/tmpl/default_folders.php', + '/administrator/components/com_media/views/media/tmpl/default_navigation.php', + '/administrator/components/com_media/views/media/view.html.php', + '/administrator/components/com_media/views/medialist/tmpl/default.php', + '/administrator/components/com_media/views/medialist/tmpl/details.php', + '/administrator/components/com_media/views/medialist/tmpl/details_doc.php', + '/administrator/components/com_media/views/medialist/tmpl/details_docs.php', + '/administrator/components/com_media/views/medialist/tmpl/details_folder.php', + '/administrator/components/com_media/views/medialist/tmpl/details_folders.php', + '/administrator/components/com_media/views/medialist/tmpl/details_img.php', + '/administrator/components/com_media/views/medialist/tmpl/details_imgs.php', + '/administrator/components/com_media/views/medialist/tmpl/details_up.php', + '/administrator/components/com_media/views/medialist/tmpl/details_video.php', + '/administrator/components/com_media/views/medialist/tmpl/details_videos.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs_docs.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs_folders.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs_imgs.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs_up.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs_videos.php', + '/administrator/components/com_media/views/medialist/view.html.php', + '/administrator/components/com_menus/controller.php', + '/administrator/components/com_menus/controllers/ajax.json.php', + '/administrator/components/com_menus/controllers/item.php', + '/administrator/components/com_menus/controllers/items.php', + '/administrator/components/com_menus/controllers/menu.php', + '/administrator/components/com_menus/controllers/menus.php', + '/administrator/components/com_menus/helpers/associations.php', + '/administrator/components/com_menus/helpers/html/menus.php', + '/administrator/components/com_menus/layouts/joomla/searchtools/default/bar.php', + '/administrator/components/com_menus/menus.php', + '/administrator/components/com_menus/models/fields/componentscategory.php', + '/administrator/components/com_menus/models/fields/menuitembytype.php', + '/administrator/components/com_menus/models/fields/menuordering.php', + '/administrator/components/com_menus/models/fields/menuparent.php', + '/administrator/components/com_menus/models/fields/menupreset.php', + '/administrator/components/com_menus/models/fields/menutype.php', + '/administrator/components/com_menus/models/fields/modal/menu.php', + '/administrator/components/com_menus/models/forms/filter_items.xml', + '/administrator/components/com_menus/models/forms/filter_itemsadmin.xml', + '/administrator/components/com_menus/models/forms/filter_menus.xml', + '/administrator/components/com_menus/models/forms/item.xml', + '/administrator/components/com_menus/models/forms/item_alias.xml', + '/administrator/components/com_menus/models/forms/item_component.xml', + '/administrator/components/com_menus/models/forms/item_heading.xml', + '/administrator/components/com_menus/models/forms/item_separator.xml', + '/administrator/components/com_menus/models/forms/item_url.xml', + '/administrator/components/com_menus/models/forms/itemadmin.xml', + '/administrator/components/com_menus/models/forms/itemadmin_alias.xml', + '/administrator/components/com_menus/models/forms/itemadmin_component.xml', + '/administrator/components/com_menus/models/forms/itemadmin_container.xml', + '/administrator/components/com_menus/models/forms/itemadmin_heading.xml', + '/administrator/components/com_menus/models/forms/itemadmin_separator.xml', + '/administrator/components/com_menus/models/forms/itemadmin_url.xml', + '/administrator/components/com_menus/models/forms/menu.xml', + '/administrator/components/com_menus/models/item.php', + '/administrator/components/com_menus/models/items.php', + '/administrator/components/com_menus/models/menu.php', + '/administrator/components/com_menus/models/menus.php', + '/administrator/components/com_menus/models/menutypes.php', + '/administrator/components/com_menus/presets/joomla.xml', + '/administrator/components/com_menus/presets/modern.xml', + '/administrator/components/com_menus/tables/menu.php', + '/administrator/components/com_menus/views/item/tmpl/edit.php', + '/administrator/components/com_menus/views/item/tmpl/edit.xml', + '/administrator/components/com_menus/views/item/tmpl/edit_associations.php', + '/administrator/components/com_menus/views/item/tmpl/edit_container.php', + '/administrator/components/com_menus/views/item/tmpl/edit_modules.php', + '/administrator/components/com_menus/views/item/tmpl/edit_options.php', + '/administrator/components/com_menus/views/item/tmpl/modal.php', + '/administrator/components/com_menus/views/item/tmpl/modal_associations.php', + '/administrator/components/com_menus/views/item/tmpl/modal_options.php', + '/administrator/components/com_menus/views/item/view.html.php', + '/administrator/components/com_menus/views/items/tmpl/default.php', + '/administrator/components/com_menus/views/items/tmpl/default.xml', + '/administrator/components/com_menus/views/items/tmpl/default_batch_body.php', + '/administrator/components/com_menus/views/items/tmpl/default_batch_footer.php', + '/administrator/components/com_menus/views/items/tmpl/modal.php', + '/administrator/components/com_menus/views/items/view.html.php', + '/administrator/components/com_menus/views/menu/tmpl/edit.php', + '/administrator/components/com_menus/views/menu/tmpl/edit.xml', + '/administrator/components/com_menus/views/menu/view.html.php', + '/administrator/components/com_menus/views/menu/view.xml.php', + '/administrator/components/com_menus/views/menus/tmpl/default.php', + '/administrator/components/com_menus/views/menus/tmpl/default.xml', + '/administrator/components/com_menus/views/menus/view.html.php', + '/administrator/components/com_menus/views/menutypes/tmpl/default.php', + '/administrator/components/com_menus/views/menutypes/view.html.php', + '/administrator/components/com_messages/controller.php', + '/administrator/components/com_messages/controllers/config.php', + '/administrator/components/com_messages/controllers/message.php', + '/administrator/components/com_messages/controllers/messages.php', + '/administrator/components/com_messages/helpers/html/messages.php', + '/administrator/components/com_messages/helpers/messages.php', + '/administrator/components/com_messages/messages.php', + '/administrator/components/com_messages/models/config.php', + '/administrator/components/com_messages/models/fields/messagestates.php', + '/administrator/components/com_messages/models/fields/usermessages.php', + '/administrator/components/com_messages/models/forms/config.xml', + '/administrator/components/com_messages/models/forms/filter_messages.xml', + '/administrator/components/com_messages/models/forms/message.xml', + '/administrator/components/com_messages/models/message.php', + '/administrator/components/com_messages/models/messages.php', + '/administrator/components/com_messages/tables/message.php', + '/administrator/components/com_messages/views/config/tmpl/default.php', + '/administrator/components/com_messages/views/config/view.html.php', + '/administrator/components/com_messages/views/message/tmpl/default.php', + '/administrator/components/com_messages/views/message/tmpl/edit.php', + '/administrator/components/com_messages/views/message/view.html.php', + '/administrator/components/com_messages/views/messages/tmpl/default.php', + '/administrator/components/com_messages/views/messages/view.html.php', + '/administrator/components/com_modules/controller.php', + '/administrator/components/com_modules/controllers/module.php', + '/administrator/components/com_modules/controllers/modules.php', + '/administrator/components/com_modules/helpers/html/modules.php', + '/administrator/components/com_modules/helpers/xml.php', + '/administrator/components/com_modules/layouts/toolbar/newmodule.php', + '/administrator/components/com_modules/models/fields/modulesmodule.php', + '/administrator/components/com_modules/models/fields/modulesposition.php', + '/administrator/components/com_modules/models/forms/advanced.xml', + '/administrator/components/com_modules/models/forms/filter_modules.xml', + '/administrator/components/com_modules/models/forms/filter_modulesadmin.xml', + '/administrator/components/com_modules/models/forms/module.xml', + '/administrator/components/com_modules/models/forms/moduleadmin.xml', + '/administrator/components/com_modules/models/module.php', + '/administrator/components/com_modules/models/modules.php', + '/administrator/components/com_modules/models/positions.php', + '/administrator/components/com_modules/models/select.php', + '/administrator/components/com_modules/modules.php', + '/administrator/components/com_modules/views/module/tmpl/edit.php', + '/administrator/components/com_modules/views/module/tmpl/edit_assignment.php', + '/administrator/components/com_modules/views/module/tmpl/edit_options.php', + '/administrator/components/com_modules/views/module/tmpl/edit_positions.php', + '/administrator/components/com_modules/views/module/tmpl/modal.php', + '/administrator/components/com_modules/views/module/view.html.php', + '/administrator/components/com_modules/views/module/view.json.php', + '/administrator/components/com_modules/views/modules/tmpl/default.php', + '/administrator/components/com_modules/views/modules/tmpl/default.xml', + '/administrator/components/com_modules/views/modules/tmpl/default_batch_body.php', + '/administrator/components/com_modules/views/modules/tmpl/default_batch_footer.php', + '/administrator/components/com_modules/views/modules/tmpl/modal.php', + '/administrator/components/com_modules/views/modules/view.html.php', + '/administrator/components/com_modules/views/positions/tmpl/modal.php', + '/administrator/components/com_modules/views/positions/view.html.php', + '/administrator/components/com_modules/views/preview/tmpl/default.php', + '/administrator/components/com_modules/views/preview/view.html.php', + '/administrator/components/com_modules/views/select/tmpl/default.php', + '/administrator/components/com_modules/views/select/view.html.php', + '/administrator/components/com_newsfeeds/controller.php', + '/administrator/components/com_newsfeeds/controllers/ajax.json.php', + '/administrator/components/com_newsfeeds/controllers/newsfeed.php', + '/administrator/components/com_newsfeeds/controllers/newsfeeds.php', + '/administrator/components/com_newsfeeds/helpers/associations.php', + '/administrator/components/com_newsfeeds/helpers/html/newsfeed.php', + '/administrator/components/com_newsfeeds/models/fields/modal/newsfeed.php', + '/administrator/components/com_newsfeeds/models/fields/newsfeeds.php', + '/administrator/components/com_newsfeeds/models/forms/filter_newsfeeds.xml', + '/administrator/components/com_newsfeeds/models/forms/newsfeed.xml', + '/administrator/components/com_newsfeeds/models/newsfeed.php', + '/administrator/components/com_newsfeeds/models/newsfeeds.php', + '/administrator/components/com_newsfeeds/newsfeeds.php', + '/administrator/components/com_newsfeeds/tables/newsfeed.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_associations.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_display.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_metadata.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_params.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_associations.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_display.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_metadata.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_params.php', + '/administrator/components/com_newsfeeds/views/newsfeed/view.html.php', + '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default.php', + '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default_batch_body.php', + '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default_batch_footer.php', + '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/modal.php', + '/administrator/components/com_newsfeeds/views/newsfeeds/view.html.php', + '/administrator/components/com_plugins/controller.php', + '/administrator/components/com_plugins/controllers/plugin.php', + '/administrator/components/com_plugins/controllers/plugins.php', + '/administrator/components/com_plugins/models/fields/pluginelement.php', + '/administrator/components/com_plugins/models/fields/pluginordering.php', + '/administrator/components/com_plugins/models/fields/plugintype.php', + '/administrator/components/com_plugins/models/forms/filter_plugins.xml', + '/administrator/components/com_plugins/models/forms/plugin.xml', + '/administrator/components/com_plugins/models/plugin.php', + '/administrator/components/com_plugins/models/plugins.php', + '/administrator/components/com_plugins/plugins.php', + '/administrator/components/com_plugins/views/plugin/tmpl/edit.php', + '/administrator/components/com_plugins/views/plugin/tmpl/edit_options.php', + '/administrator/components/com_plugins/views/plugin/tmpl/modal.php', + '/administrator/components/com_plugins/views/plugin/view.html.php', + '/administrator/components/com_plugins/views/plugins/tmpl/default.php', + '/administrator/components/com_plugins/views/plugins/tmpl/default.xml', + '/administrator/components/com_plugins/views/plugins/view.html.php', + '/administrator/components/com_postinstall/controllers/message.php', + '/administrator/components/com_postinstall/fof.xml', + '/administrator/components/com_postinstall/models/messages.php', + '/administrator/components/com_postinstall/postinstall.php', + '/administrator/components/com_postinstall/toolbar.php', + '/administrator/components/com_postinstall/views/messages/tmpl/default.php', + '/administrator/components/com_postinstall/views/messages/tmpl/default.xml', + '/administrator/components/com_postinstall/views/messages/view.html.php', + '/administrator/components/com_privacy/controller.php', + '/administrator/components/com_privacy/controllers/consents.php', + '/administrator/components/com_privacy/controllers/request.php', + '/administrator/components/com_privacy/controllers/request.xml.php', + '/administrator/components/com_privacy/controllers/requests.php', + '/administrator/components/com_privacy/helpers/export/domain.php', + '/administrator/components/com_privacy/helpers/export/field.php', + '/administrator/components/com_privacy/helpers/export/item.php', + '/administrator/components/com_privacy/helpers/html/helper.php', + '/administrator/components/com_privacy/helpers/plugin.php', + '/administrator/components/com_privacy/helpers/privacy.php', + '/administrator/components/com_privacy/helpers/removal/status.php', + '/administrator/components/com_privacy/models/capabilities.php', + '/administrator/components/com_privacy/models/consents.php', + '/administrator/components/com_privacy/models/dashboard.php', + '/administrator/components/com_privacy/models/export.php', + '/administrator/components/com_privacy/models/fields/requeststatus.php', + '/administrator/components/com_privacy/models/fields/requesttype.php', + '/administrator/components/com_privacy/models/forms/filter_consents.xml', + '/administrator/components/com_privacy/models/forms/filter_requests.xml', + '/administrator/components/com_privacy/models/forms/request.xml', + '/administrator/components/com_privacy/models/remove.php', + '/administrator/components/com_privacy/models/request.php', + '/administrator/components/com_privacy/models/requests.php', + '/administrator/components/com_privacy/privacy.php', + '/administrator/components/com_privacy/tables/consent.php', + '/administrator/components/com_privacy/tables/request.php', + '/administrator/components/com_privacy/views/capabilities/tmpl/default.php', + '/administrator/components/com_privacy/views/capabilities/view.html.php', + '/administrator/components/com_privacy/views/consents/tmpl/default.php', + '/administrator/components/com_privacy/views/consents/tmpl/default.xml', + '/administrator/components/com_privacy/views/consents/view.html.php', + '/administrator/components/com_privacy/views/dashboard/tmpl/default.php', + '/administrator/components/com_privacy/views/dashboard/tmpl/default.xml', + '/administrator/components/com_privacy/views/dashboard/view.html.php', + '/administrator/components/com_privacy/views/export/view.xml.php', + '/administrator/components/com_privacy/views/request/tmpl/default.php', + '/administrator/components/com_privacy/views/request/tmpl/edit.php', + '/administrator/components/com_privacy/views/request/view.html.php', + '/administrator/components/com_privacy/views/requests/tmpl/default.php', + '/administrator/components/com_privacy/views/requests/tmpl/default.xml', + '/administrator/components/com_privacy/views/requests/view.html.php', + '/administrator/components/com_redirect/controller.php', + '/administrator/components/com_redirect/controllers/link.php', + '/administrator/components/com_redirect/controllers/links.php', + '/administrator/components/com_redirect/helpers/html/redirect.php', + '/administrator/components/com_redirect/models/fields/redirect.php', + '/administrator/components/com_redirect/models/forms/filter_links.xml', + '/administrator/components/com_redirect/models/forms/link.xml', + '/administrator/components/com_redirect/models/link.php', + '/administrator/components/com_redirect/models/links.php', + '/administrator/components/com_redirect/redirect.php', + '/administrator/components/com_redirect/tables/link.php', + '/administrator/components/com_redirect/views/link/tmpl/edit.php', + '/administrator/components/com_redirect/views/link/view.html.php', + '/administrator/components/com_redirect/views/links/tmpl/default.php', + '/administrator/components/com_redirect/views/links/tmpl/default.xml', + '/administrator/components/com_redirect/views/links/tmpl/default_addform.php', + '/administrator/components/com_redirect/views/links/tmpl/default_batch_body.php', + '/administrator/components/com_redirect/views/links/tmpl/default_batch_footer.php', + '/administrator/components/com_redirect/views/links/view.html.php', + '/administrator/components/com_tags/controller.php', + '/administrator/components/com_tags/controllers/tag.php', + '/administrator/components/com_tags/controllers/tags.php', + '/administrator/components/com_tags/helpers/tags.php', + '/administrator/components/com_tags/models/forms/filter_tags.xml', + '/administrator/components/com_tags/models/forms/tag.xml', + '/administrator/components/com_tags/models/tag.php', + '/administrator/components/com_tags/models/tags.php', + '/administrator/components/com_tags/tables/tag.php', + '/administrator/components/com_tags/tags.php', + '/administrator/components/com_tags/views/tag/tmpl/edit.php', + '/administrator/components/com_tags/views/tag/tmpl/edit_metadata.php', + '/administrator/components/com_tags/views/tag/tmpl/edit_options.php', + '/administrator/components/com_tags/views/tag/view.html.php', + '/administrator/components/com_tags/views/tags/tmpl/default.php', + '/administrator/components/com_tags/views/tags/tmpl/default.xml', + '/administrator/components/com_tags/views/tags/tmpl/default_batch_body.php', + '/administrator/components/com_tags/views/tags/tmpl/default_batch_footer.php', + '/administrator/components/com_tags/views/tags/view.html.php', + '/administrator/components/com_templates/controller.php', + '/administrator/components/com_templates/controllers/style.php', + '/administrator/components/com_templates/controllers/styles.php', + '/administrator/components/com_templates/controllers/template.php', + '/administrator/components/com_templates/helpers/html/templates.php', + '/administrator/components/com_templates/models/fields/templatelocation.php', + '/administrator/components/com_templates/models/fields/templatename.php', + '/administrator/components/com_templates/models/forms/filter_styles.xml', + '/administrator/components/com_templates/models/forms/filter_templates.xml', + '/administrator/components/com_templates/models/forms/source.xml', + '/administrator/components/com_templates/models/forms/style.xml', + '/administrator/components/com_templates/models/forms/style_administrator.xml', + '/administrator/components/com_templates/models/forms/style_site.xml', + '/administrator/components/com_templates/models/style.php', + '/administrator/components/com_templates/models/styles.php', + '/administrator/components/com_templates/models/template.php', + '/administrator/components/com_templates/models/templates.php', + '/administrator/components/com_templates/tables/style.php', + '/administrator/components/com_templates/templates.php', + '/administrator/components/com_templates/views/style/tmpl/edit.php', + '/administrator/components/com_templates/views/style/tmpl/edit_assignment.php', + '/administrator/components/com_templates/views/style/tmpl/edit_options.php', + '/administrator/components/com_templates/views/style/view.html.php', + '/administrator/components/com_templates/views/style/view.json.php', + '/administrator/components/com_templates/views/styles/tmpl/default.php', + '/administrator/components/com_templates/views/styles/tmpl/default.xml', + '/administrator/components/com_templates/views/styles/view.html.php', + '/administrator/components/com_templates/views/template/tmpl/default.php', + '/administrator/components/com_templates/views/template/tmpl/default_description.php', + '/administrator/components/com_templates/views/template/tmpl/default_folders.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_copy_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_copy_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_delete_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_delete_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_file_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_file_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_folder_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_folder_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_rename_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_rename_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_resize_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_resize_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_tree.php', + '/administrator/components/com_templates/views/template/tmpl/readonly.php', + '/administrator/components/com_templates/views/template/view.html.php', + '/administrator/components/com_templates/views/templates/tmpl/default.php', + '/administrator/components/com_templates/views/templates/tmpl/default.xml', + '/administrator/components/com_templates/views/templates/view.html.php', + '/administrator/components/com_users/controller.php', + '/administrator/components/com_users/controllers/group.php', + '/administrator/components/com_users/controllers/groups.php', + '/administrator/components/com_users/controllers/level.php', + '/administrator/components/com_users/controllers/levels.php', + '/administrator/components/com_users/controllers/mail.php', + '/administrator/components/com_users/controllers/note.php', + '/administrator/components/com_users/controllers/notes.php', + '/administrator/components/com_users/controllers/user.php', + '/administrator/components/com_users/controllers/users.php', + '/administrator/components/com_users/helpers/html/users.php', + '/administrator/components/com_users/models/debuggroup.php', + '/administrator/components/com_users/models/debuguser.php', + '/administrator/components/com_users/models/fields/groupparent.php', + '/administrator/components/com_users/models/fields/levels.php', + '/administrator/components/com_users/models/forms/config_domain.xml', + '/administrator/components/com_users/models/forms/fields/user.xml', + '/administrator/components/com_users/models/forms/filter_debuggroup.xml', + '/administrator/components/com_users/models/forms/filter_debuguser.xml', + '/administrator/components/com_users/models/forms/filter_groups.xml', + '/administrator/components/com_users/models/forms/filter_levels.xml', + '/administrator/components/com_users/models/forms/filter_notes.xml', + '/administrator/components/com_users/models/forms/filter_users.xml', + '/administrator/components/com_users/models/forms/group.xml', + '/administrator/components/com_users/models/forms/level.xml', + '/administrator/components/com_users/models/forms/mail.xml', + '/administrator/components/com_users/models/forms/note.xml', + '/administrator/components/com_users/models/forms/user.xml', + '/administrator/components/com_users/models/group.php', + '/administrator/components/com_users/models/groups.php', + '/administrator/components/com_users/models/level.php', + '/administrator/components/com_users/models/levels.php', + '/administrator/components/com_users/models/mail.php', + '/administrator/components/com_users/models/note.php', + '/administrator/components/com_users/models/notes.php', + '/administrator/components/com_users/models/user.php', + '/administrator/components/com_users/models/users.php', + '/administrator/components/com_users/tables/note.php', + '/administrator/components/com_users/users.php', + '/administrator/components/com_users/views/debuggroup/tmpl/default.php', + '/administrator/components/com_users/views/debuggroup/view.html.php', + '/administrator/components/com_users/views/debuguser/tmpl/default.php', + '/administrator/components/com_users/views/debuguser/view.html.php', + '/administrator/components/com_users/views/group/tmpl/edit.php', + '/administrator/components/com_users/views/group/tmpl/edit.xml', + '/administrator/components/com_users/views/group/view.html.php', + '/administrator/components/com_users/views/groups/tmpl/default.php', + '/administrator/components/com_users/views/groups/tmpl/default.xml', + '/administrator/components/com_users/views/groups/view.html.php', + '/administrator/components/com_users/views/level/tmpl/edit.php', + '/administrator/components/com_users/views/level/tmpl/edit.xml', + '/administrator/components/com_users/views/level/view.html.php', + '/administrator/components/com_users/views/levels/tmpl/default.php', + '/administrator/components/com_users/views/levels/tmpl/default.xml', + '/administrator/components/com_users/views/levels/view.html.php', + '/administrator/components/com_users/views/mail/tmpl/default.php', + '/administrator/components/com_users/views/mail/tmpl/default.xml', + '/administrator/components/com_users/views/mail/view.html.php', + '/administrator/components/com_users/views/note/tmpl/edit.php', + '/administrator/components/com_users/views/note/tmpl/edit.xml', + '/administrator/components/com_users/views/note/view.html.php', + '/administrator/components/com_users/views/notes/tmpl/default.php', + '/administrator/components/com_users/views/notes/tmpl/default.xml', + '/administrator/components/com_users/views/notes/tmpl/modal.php', + '/administrator/components/com_users/views/notes/view.html.php', + '/administrator/components/com_users/views/user/tmpl/edit.php', + '/administrator/components/com_users/views/user/tmpl/edit.xml', + '/administrator/components/com_users/views/user/tmpl/edit_groups.php', + '/administrator/components/com_users/views/user/view.html.php', + '/administrator/components/com_users/views/users/tmpl/default.php', + '/administrator/components/com_users/views/users/tmpl/default.xml', + '/administrator/components/com_users/views/users/tmpl/default_batch_body.php', + '/administrator/components/com_users/views/users/tmpl/default_batch_footer.php', + '/administrator/components/com_users/views/users/tmpl/modal.php', + '/administrator/components/com_users/views/users/view.html.php', + '/administrator/help/helpsites.xml', + '/administrator/includes/helper.php', + '/administrator/includes/subtoolbar.php', + '/administrator/language/en-GB/en-GB.com_actionlogs.ini', + '/administrator/language/en-GB/en-GB.com_actionlogs.sys.ini', + '/administrator/language/en-GB/en-GB.com_admin.ini', + '/administrator/language/en-GB/en-GB.com_admin.sys.ini', + '/administrator/language/en-GB/en-GB.com_ajax.ini', + '/administrator/language/en-GB/en-GB.com_ajax.sys.ini', + '/administrator/language/en-GB/en-GB.com_associations.ini', + '/administrator/language/en-GB/en-GB.com_associations.sys.ini', + '/administrator/language/en-GB/en-GB.com_banners.ini', + '/administrator/language/en-GB/en-GB.com_banners.sys.ini', + '/administrator/language/en-GB/en-GB.com_cache.ini', + '/administrator/language/en-GB/en-GB.com_cache.sys.ini', + '/administrator/language/en-GB/en-GB.com_categories.ini', + '/administrator/language/en-GB/en-GB.com_categories.sys.ini', + '/administrator/language/en-GB/en-GB.com_checkin.ini', + '/administrator/language/en-GB/en-GB.com_checkin.sys.ini', + '/administrator/language/en-GB/en-GB.com_config.ini', + '/administrator/language/en-GB/en-GB.com_config.sys.ini', + '/administrator/language/en-GB/en-GB.com_contact.ini', + '/administrator/language/en-GB/en-GB.com_contact.sys.ini', + '/administrator/language/en-GB/en-GB.com_content.ini', + '/administrator/language/en-GB/en-GB.com_content.sys.ini', + '/administrator/language/en-GB/en-GB.com_contenthistory.ini', + '/administrator/language/en-GB/en-GB.com_contenthistory.sys.ini', + '/administrator/language/en-GB/en-GB.com_cpanel.ini', + '/administrator/language/en-GB/en-GB.com_cpanel.sys.ini', + '/administrator/language/en-GB/en-GB.com_fields.ini', + '/administrator/language/en-GB/en-GB.com_fields.sys.ini', + '/administrator/language/en-GB/en-GB.com_finder.ini', + '/administrator/language/en-GB/en-GB.com_finder.sys.ini', + '/administrator/language/en-GB/en-GB.com_installer.ini', + '/administrator/language/en-GB/en-GB.com_installer.sys.ini', + '/administrator/language/en-GB/en-GB.com_joomlaupdate.ini', + '/administrator/language/en-GB/en-GB.com_joomlaupdate.sys.ini', + '/administrator/language/en-GB/en-GB.com_languages.ini', + '/administrator/language/en-GB/en-GB.com_languages.sys.ini', + '/administrator/language/en-GB/en-GB.com_login.ini', + '/administrator/language/en-GB/en-GB.com_login.sys.ini', + '/administrator/language/en-GB/en-GB.com_mailto.sys.ini', + '/administrator/language/en-GB/en-GB.com_media.ini', + '/administrator/language/en-GB/en-GB.com_media.sys.ini', + '/administrator/language/en-GB/en-GB.com_menus.ini', + '/administrator/language/en-GB/en-GB.com_menus.sys.ini', + '/administrator/language/en-GB/en-GB.com_messages.ini', + '/administrator/language/en-GB/en-GB.com_messages.sys.ini', + '/administrator/language/en-GB/en-GB.com_modules.ini', + '/administrator/language/en-GB/en-GB.com_modules.sys.ini', + '/administrator/language/en-GB/en-GB.com_newsfeeds.ini', + '/administrator/language/en-GB/en-GB.com_newsfeeds.sys.ini', + '/administrator/language/en-GB/en-GB.com_plugins.ini', + '/administrator/language/en-GB/en-GB.com_plugins.sys.ini', + '/administrator/language/en-GB/en-GB.com_postinstall.ini', + '/administrator/language/en-GB/en-GB.com_postinstall.sys.ini', + '/administrator/language/en-GB/en-GB.com_privacy.ini', + '/administrator/language/en-GB/en-GB.com_privacy.sys.ini', + '/administrator/language/en-GB/en-GB.com_redirect.ini', + '/administrator/language/en-GB/en-GB.com_redirect.sys.ini', + '/administrator/language/en-GB/en-GB.com_tags.ini', + '/administrator/language/en-GB/en-GB.com_tags.sys.ini', + '/administrator/language/en-GB/en-GB.com_templates.ini', + '/administrator/language/en-GB/en-GB.com_templates.sys.ini', + '/administrator/language/en-GB/en-GB.com_users.ini', + '/administrator/language/en-GB/en-GB.com_users.sys.ini', + '/administrator/language/en-GB/en-GB.com_weblinks.ini', + '/administrator/language/en-GB/en-GB.com_weblinks.sys.ini', + '/administrator/language/en-GB/en-GB.com_wrapper.ini', + '/administrator/language/en-GB/en-GB.com_wrapper.sys.ini', + '/administrator/language/en-GB/en-GB.ini', + '/administrator/language/en-GB/en-GB.lib_joomla.ini', + '/administrator/language/en-GB/en-GB.localise.php', + '/administrator/language/en-GB/en-GB.mod_custom.ini', + '/administrator/language/en-GB/en-GB.mod_custom.sys.ini', + '/administrator/language/en-GB/en-GB.mod_feed.ini', + '/administrator/language/en-GB/en-GB.mod_feed.sys.ini', + '/administrator/language/en-GB/en-GB.mod_latest.ini', + '/administrator/language/en-GB/en-GB.mod_latest.sys.ini', + '/administrator/language/en-GB/en-GB.mod_latestactions.ini', + '/administrator/language/en-GB/en-GB.mod_latestactions.sys.ini', + '/administrator/language/en-GB/en-GB.mod_logged.ini', + '/administrator/language/en-GB/en-GB.mod_logged.sys.ini', + '/administrator/language/en-GB/en-GB.mod_login.ini', + '/administrator/language/en-GB/en-GB.mod_login.sys.ini', + '/administrator/language/en-GB/en-GB.mod_menu.ini', + '/administrator/language/en-GB/en-GB.mod_menu.sys.ini', + '/administrator/language/en-GB/en-GB.mod_multilangstatus.ini', + '/administrator/language/en-GB/en-GB.mod_multilangstatus.sys.ini', + '/administrator/language/en-GB/en-GB.mod_popular.ini', + '/administrator/language/en-GB/en-GB.mod_popular.sys.ini', + '/administrator/language/en-GB/en-GB.mod_privacy_dashboard.ini', + '/administrator/language/en-GB/en-GB.mod_privacy_dashboard.sys.ini', + '/administrator/language/en-GB/en-GB.mod_quickicon.ini', + '/administrator/language/en-GB/en-GB.mod_quickicon.sys.ini', + '/administrator/language/en-GB/en-GB.mod_sampledata.ini', + '/administrator/language/en-GB/en-GB.mod_sampledata.sys.ini', + '/administrator/language/en-GB/en-GB.mod_stats_admin.ini', + '/administrator/language/en-GB/en-GB.mod_stats_admin.sys.ini', + '/administrator/language/en-GB/en-GB.mod_status.ini', + '/administrator/language/en-GB/en-GB.mod_status.sys.ini', + '/administrator/language/en-GB/en-GB.mod_submenu.ini', + '/administrator/language/en-GB/en-GB.mod_submenu.sys.ini', + '/administrator/language/en-GB/en-GB.mod_title.ini', + '/administrator/language/en-GB/en-GB.mod_title.sys.ini', + '/administrator/language/en-GB/en-GB.mod_toolbar.ini', + '/administrator/language/en-GB/en-GB.mod_toolbar.sys.ini', + '/administrator/language/en-GB/en-GB.mod_version.ini', + '/administrator/language/en-GB/en-GB.mod_version.sys.ini', + '/administrator/language/en-GB/en-GB.plg_actionlog_joomla.ini', + '/administrator/language/en-GB/en-GB.plg_actionlog_joomla.sys.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_cookie.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_cookie.sys.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_gmail.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_gmail.sys.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_joomla.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_joomla.sys.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_ldap.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_ldap.sys.ini', + '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.ini', + '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.sys.ini', + '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha_invisible.ini', + '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha_invisible.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_confirmconsent.ini', + '/administrator/language/en-GB/en-GB.plg_content_confirmconsent.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_contact.ini', + '/administrator/language/en-GB/en-GB.plg_content_contact.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_emailcloak.ini', + '/administrator/language/en-GB/en-GB.plg_content_emailcloak.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_fields.ini', + '/administrator/language/en-GB/en-GB.plg_content_fields.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_finder.ini', + '/administrator/language/en-GB/en-GB.plg_content_finder.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_joomla.ini', + '/administrator/language/en-GB/en-GB.plg_content_joomla.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_loadmodule.ini', + '/administrator/language/en-GB/en-GB.plg_content_loadmodule.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_pagebreak.ini', + '/administrator/language/en-GB/en-GB.plg_content_pagebreak.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_pagenavigation.ini', + '/administrator/language/en-GB/en-GB.plg_content_pagenavigation.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_vote.ini', + '/administrator/language/en-GB/en-GB.plg_content_vote.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_article.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_article.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_contact.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_contact.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_fields.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_fields.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_image.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_image.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_menu.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_menu.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_module.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_module.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors_codemirror.ini', + '/administrator/language/en-GB/en-GB.plg_editors_codemirror.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors_none.ini', + '/administrator/language/en-GB/en-GB.plg_editors_none.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors_tinymce.ini', + '/administrator/language/en-GB/en-GB.plg_editors_tinymce.sys.ini', + '/administrator/language/en-GB/en-GB.plg_extension_joomla.ini', + '/administrator/language/en-GB/en-GB.plg_extension_joomla.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_calendar.ini', + '/administrator/language/en-GB/en-GB.plg_fields_calendar.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_checkboxes.ini', + '/administrator/language/en-GB/en-GB.plg_fields_checkboxes.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_color.ini', + '/administrator/language/en-GB/en-GB.plg_fields_color.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_editor.ini', + '/administrator/language/en-GB/en-GB.plg_fields_editor.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_image.ini', + '/administrator/language/en-GB/en-GB.plg_fields_image.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_imagelist.ini', + '/administrator/language/en-GB/en-GB.plg_fields_imagelist.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_integer.ini', + '/administrator/language/en-GB/en-GB.plg_fields_integer.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_list.ini', + '/administrator/language/en-GB/en-GB.plg_fields_list.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_media.ini', + '/administrator/language/en-GB/en-GB.plg_fields_media.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_radio.ini', + '/administrator/language/en-GB/en-GB.plg_fields_radio.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_sql.ini', + '/administrator/language/en-GB/en-GB.plg_fields_sql.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_text.ini', + '/administrator/language/en-GB/en-GB.plg_fields_text.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_textarea.ini', + '/administrator/language/en-GB/en-GB.plg_fields_textarea.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_url.ini', + '/administrator/language/en-GB/en-GB.plg_fields_url.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_user.ini', + '/administrator/language/en-GB/en-GB.plg_fields_user.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_usergrouplist.ini', + '/administrator/language/en-GB/en-GB.plg_fields_usergrouplist.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_categories.ini', + '/administrator/language/en-GB/en-GB.plg_finder_categories.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_contacts.ini', + '/administrator/language/en-GB/en-GB.plg_finder_contacts.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_content.ini', + '/administrator/language/en-GB/en-GB.plg_finder_content.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.ini', + '/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_tags.ini', + '/administrator/language/en-GB/en-GB.plg_finder_tags.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_weblinks.ini', + '/administrator/language/en-GB/en-GB.plg_finder_weblinks.sys.ini', + '/administrator/language/en-GB/en-GB.plg_installer_folderinstaller.ini', + '/administrator/language/en-GB/en-GB.plg_installer_folderinstaller.sys.ini', + '/administrator/language/en-GB/en-GB.plg_installer_packageinstaller.ini', + '/administrator/language/en-GB/en-GB.plg_installer_packageinstaller.sys.ini', + '/administrator/language/en-GB/en-GB.plg_installer_urlinstaller.ini', + '/administrator/language/en-GB/en-GB.plg_installer_urlinstaller.sys.ini', + '/administrator/language/en-GB/en-GB.plg_installer_webinstaller.ini', + '/administrator/language/en-GB/en-GB.plg_installer_webinstaller.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_actionlogs.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_actionlogs.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_consents.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_consents.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_contact.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_contact.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_content.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_content.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_message.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_message.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_user.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_user.sys.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.sys.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.sys.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_phpversioncheck.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_phpversioncheck.sys.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_privacycheck.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_privacycheck.sys.ini', + '/administrator/language/en-GB/en-GB.plg_sampledata_blog.ini', + '/administrator/language/en-GB/en-GB.plg_sampledata_blog.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_actionlogs.ini', + '/administrator/language/en-GB/en-GB.plg_system_actionlogs.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_cache.ini', + '/administrator/language/en-GB/en-GB.plg_system_cache.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_debug.ini', + '/administrator/language/en-GB/en-GB.plg_system_debug.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_fields.ini', + '/administrator/language/en-GB/en-GB.plg_system_fields.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_highlight.ini', + '/administrator/language/en-GB/en-GB.plg_system_highlight.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_languagecode.ini', + '/administrator/language/en-GB/en-GB.plg_system_languagecode.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_languagefilter.ini', + '/administrator/language/en-GB/en-GB.plg_system_languagefilter.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_log.ini', + '/administrator/language/en-GB/en-GB.plg_system_log.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_logout.ini', + '/administrator/language/en-GB/en-GB.plg_system_logout.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_logrotation.ini', + '/administrator/language/en-GB/en-GB.plg_system_logrotation.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_p3p.ini', + '/administrator/language/en-GB/en-GB.plg_system_p3p.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_privacyconsent.ini', + '/administrator/language/en-GB/en-GB.plg_system_privacyconsent.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_redirect.ini', + '/administrator/language/en-GB/en-GB.plg_system_redirect.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_remember.ini', + '/administrator/language/en-GB/en-GB.plg_system_remember.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_sef.ini', + '/administrator/language/en-GB/en-GB.plg_system_sef.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_sessiongc.ini', + '/administrator/language/en-GB/en-GB.plg_system_sessiongc.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_stats.ini', + '/administrator/language/en-GB/en-GB.plg_system_stats.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_updatenotification.ini', + '/administrator/language/en-GB/en-GB.plg_system_updatenotification.sys.ini', + '/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.ini', + '/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.sys.ini', + '/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.ini', + '/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.sys.ini', + '/administrator/language/en-GB/en-GB.plg_user_contactcreator.ini', + '/administrator/language/en-GB/en-GB.plg_user_contactcreator.sys.ini', + '/administrator/language/en-GB/en-GB.plg_user_joomla.ini', + '/administrator/language/en-GB/en-GB.plg_user_joomla.sys.ini', + '/administrator/language/en-GB/en-GB.plg_user_profile.ini', + '/administrator/language/en-GB/en-GB.plg_user_profile.sys.ini', + '/administrator/language/en-GB/en-GB.plg_user_terms.ini', + '/administrator/language/en-GB/en-GB.plg_user_terms.sys.ini', + '/administrator/language/en-GB/en-GB.tpl_hathor.ini', + '/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini', + '/administrator/language/en-GB/en-GB.tpl_isis.ini', + '/administrator/language/en-GB/en-GB.tpl_isis.sys.ini', + '/administrator/language/en-GB/en-GB.xml', + '/administrator/manifests/libraries/fof.xml', + '/administrator/manifests/libraries/idna_convert.xml', + '/administrator/manifests/libraries/phputf8.xml', + '/administrator/modules/mod_feed/helper.php', + '/administrator/modules/mod_latest/helper.php', + '/administrator/modules/mod_latestactions/helper.php', + '/administrator/modules/mod_logged/helper.php', + '/administrator/modules/mod_login/helper.php', + '/administrator/modules/mod_menu/helper.php', + '/administrator/modules/mod_menu/menu.php', + '/administrator/modules/mod_multilangstatus/language/en-GB/en-GB.mod_multilangstatus.ini', + '/administrator/modules/mod_multilangstatus/language/en-GB/en-GB.mod_multilangstatus.sys.ini', + '/administrator/modules/mod_popular/helper.php', + '/administrator/modules/mod_privacy_dashboard/helper.php', + '/administrator/modules/mod_quickicon/helper.php', + '/administrator/modules/mod_quickicon/mod_quickicon.php', + '/administrator/modules/mod_sampledata/helper.php', + '/administrator/modules/mod_stats_admin/helper.php', + '/administrator/modules/mod_stats_admin/language/en-GB.mod_stats_admin.ini', + '/administrator/modules/mod_stats_admin/language/en-GB.mod_stats_admin.sys.ini', + '/administrator/modules/mod_status/mod_status.php', + '/administrator/modules/mod_status/mod_status.xml', + '/administrator/modules/mod_status/tmpl/default.php', + '/administrator/modules/mod_version/helper.php', + '/administrator/modules/mod_version/language/en-GB/en-GB.mod_version.ini', + '/administrator/modules/mod_version/language/en-GB/en-GB.mod_version.sys.ini', + '/administrator/templates/hathor/LICENSE.txt', + '/administrator/templates/hathor/component.php', + '/administrator/templates/hathor/cpanel.php', + '/administrator/templates/hathor/css/boldtext.css', + '/administrator/templates/hathor/css/colour_blue.css', + '/administrator/templates/hathor/css/colour_blue_rtl.css', + '/administrator/templates/hathor/css/colour_brown.css', + '/administrator/templates/hathor/css/colour_brown_rtl.css', + '/administrator/templates/hathor/css/colour_highcontrast.css', + '/administrator/templates/hathor/css/colour_highcontrast_rtl.css', + '/administrator/templates/hathor/css/colour_standard.css', + '/administrator/templates/hathor/css/colour_standard_rtl.css', + '/administrator/templates/hathor/css/error.css', + '/administrator/templates/hathor/css/ie7.css', + '/administrator/templates/hathor/css/ie8.css', + '/administrator/templates/hathor/css/template.css', + '/administrator/templates/hathor/css/template_rtl.css', + '/administrator/templates/hathor/css/theme.css', + '/administrator/templates/hathor/error.php', + '/administrator/templates/hathor/favicon.ico', + '/administrator/templates/hathor/html/com_admin/help/default.php', + '/administrator/templates/hathor/html/com_admin/profile/edit.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default_config.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default_directory.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default_navigation.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default_phpsettings.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default_system.php', + '/administrator/templates/hathor/html/com_associations/associations/default.php', + '/administrator/templates/hathor/html/com_banners/banner/edit.php', + '/administrator/templates/hathor/html/com_banners/banners/default.php', + '/administrator/templates/hathor/html/com_banners/client/edit.php', + '/administrator/templates/hathor/html/com_banners/clients/default.php', + '/administrator/templates/hathor/html/com_banners/download/default.php', + '/administrator/templates/hathor/html/com_banners/tracks/default.php', + '/administrator/templates/hathor/html/com_cache/cache/default.php', + '/administrator/templates/hathor/html/com_cache/purge/default.php', + '/administrator/templates/hathor/html/com_categories/categories/default.php', + '/administrator/templates/hathor/html/com_categories/category/edit.php', + '/administrator/templates/hathor/html/com_categories/category/edit_options.php', + '/administrator/templates/hathor/html/com_checkin/checkin/default.php', + '/administrator/templates/hathor/html/com_config/application/default.php', + '/administrator/templates/hathor/html/com_config/application/default_cache.php', + '/administrator/templates/hathor/html/com_config/application/default_cookie.php', + '/administrator/templates/hathor/html/com_config/application/default_database.php', + '/administrator/templates/hathor/html/com_config/application/default_debug.php', + '/administrator/templates/hathor/html/com_config/application/default_filters.php', + '/administrator/templates/hathor/html/com_config/application/default_ftp.php', + '/administrator/templates/hathor/html/com_config/application/default_ftplogin.php', + '/administrator/templates/hathor/html/com_config/application/default_locale.php', + '/administrator/templates/hathor/html/com_config/application/default_mail.php', + '/administrator/templates/hathor/html/com_config/application/default_metadata.php', + '/administrator/templates/hathor/html/com_config/application/default_navigation.php', + '/administrator/templates/hathor/html/com_config/application/default_permissions.php', + '/administrator/templates/hathor/html/com_config/application/default_seo.php', + '/administrator/templates/hathor/html/com_config/application/default_server.php', + '/administrator/templates/hathor/html/com_config/application/default_session.php', + '/administrator/templates/hathor/html/com_config/application/default_site.php', + '/administrator/templates/hathor/html/com_config/application/default_system.php', + '/administrator/templates/hathor/html/com_config/component/default.php', + '/administrator/templates/hathor/html/com_contact/contact/edit.php', + '/administrator/templates/hathor/html/com_contact/contact/edit_params.php', + '/administrator/templates/hathor/html/com_contact/contacts/default.php', + '/administrator/templates/hathor/html/com_contact/contacts/modal.php', + '/administrator/templates/hathor/html/com_content/article/edit.php', + '/administrator/templates/hathor/html/com_content/articles/default.php', + '/administrator/templates/hathor/html/com_content/articles/modal.php', + '/administrator/templates/hathor/html/com_content/featured/default.php', + '/administrator/templates/hathor/html/com_contenthistory/history/modal.php', + '/administrator/templates/hathor/html/com_cpanel/cpanel/default.php', + '/administrator/templates/hathor/html/com_fields/field/edit.php', + '/administrator/templates/hathor/html/com_fields/fields/default.php', + '/administrator/templates/hathor/html/com_fields/group/edit.php', + '/administrator/templates/hathor/html/com_fields/groups/default.php', + '/administrator/templates/hathor/html/com_finder/filters/default.php', + '/administrator/templates/hathor/html/com_finder/index/default.php', + '/administrator/templates/hathor/html/com_finder/maps/default.php', + '/administrator/templates/hathor/html/com_installer/database/default.php', + '/administrator/templates/hathor/html/com_installer/default/default_ftp.php', + '/administrator/templates/hathor/html/com_installer/discover/default.php', + '/administrator/templates/hathor/html/com_installer/install/default.php', + '/administrator/templates/hathor/html/com_installer/install/default_form.php', + '/administrator/templates/hathor/html/com_installer/languages/default.php', + '/administrator/templates/hathor/html/com_installer/languages/default_filter.php', + '/administrator/templates/hathor/html/com_installer/manage/default.php', + '/administrator/templates/hathor/html/com_installer/manage/default_filter.php', + '/administrator/templates/hathor/html/com_installer/update/default.php', + '/administrator/templates/hathor/html/com_installer/warnings/default.php', + '/administrator/templates/hathor/html/com_joomlaupdate/default/default.php', + '/administrator/templates/hathor/html/com_languages/installed/default.php', + '/administrator/templates/hathor/html/com_languages/installed/default_ftp.php', + '/administrator/templates/hathor/html/com_languages/languages/default.php', + '/administrator/templates/hathor/html/com_languages/overrides/default.php', + '/administrator/templates/hathor/html/com_menus/item/edit.php', + '/administrator/templates/hathor/html/com_menus/item/edit_options.php', + '/administrator/templates/hathor/html/com_menus/items/default.php', + '/administrator/templates/hathor/html/com_menus/menu/edit.php', + '/administrator/templates/hathor/html/com_menus/menus/default.php', + '/administrator/templates/hathor/html/com_menus/menutypes/default.php', + '/administrator/templates/hathor/html/com_messages/message/edit.php', + '/administrator/templates/hathor/html/com_messages/messages/default.php', + '/administrator/templates/hathor/html/com_modules/module/edit.php', + '/administrator/templates/hathor/html/com_modules/module/edit_assignment.php', + '/administrator/templates/hathor/html/com_modules/module/edit_options.php', + '/administrator/templates/hathor/html/com_modules/modules/default.php', + '/administrator/templates/hathor/html/com_modules/positions/modal.php', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeed/edit.php', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeed/edit_params.php', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds/default.php', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds/modal.php', + '/administrator/templates/hathor/html/com_plugins/plugin/edit.php', + '/administrator/templates/hathor/html/com_plugins/plugin/edit_options.php', + '/administrator/templates/hathor/html/com_plugins/plugins/default.php', + '/administrator/templates/hathor/html/com_postinstall/messages/default.php', + '/administrator/templates/hathor/html/com_redirect/links/default.php', + '/administrator/templates/hathor/html/com_search/searches/default.php', + '/administrator/templates/hathor/html/com_tags/tag/edit.php', + '/administrator/templates/hathor/html/com_tags/tag/edit_metadata.php', + '/administrator/templates/hathor/html/com_tags/tag/edit_options.php', + '/administrator/templates/hathor/html/com_tags/tags/default.php', + '/administrator/templates/hathor/html/com_templates/style/edit.php', + '/administrator/templates/hathor/html/com_templates/style/edit_assignment.php', + '/administrator/templates/hathor/html/com_templates/style/edit_options.php', + '/administrator/templates/hathor/html/com_templates/styles/default.php', + '/administrator/templates/hathor/html/com_templates/template/default.php', + '/administrator/templates/hathor/html/com_templates/template/default_description.php', + '/administrator/templates/hathor/html/com_templates/template/default_folders.php', + '/administrator/templates/hathor/html/com_templates/template/default_tree.php', + '/administrator/templates/hathor/html/com_templates/templates/default.php', + '/administrator/templates/hathor/html/com_users/debuggroup/default.php', + '/administrator/templates/hathor/html/com_users/debuguser/default.php', + '/administrator/templates/hathor/html/com_users/groups/default.php', + '/administrator/templates/hathor/html/com_users/levels/default.php', + '/administrator/templates/hathor/html/com_users/note/edit.php', + '/administrator/templates/hathor/html/com_users/notes/default.php', + '/administrator/templates/hathor/html/com_users/user/edit.php', + '/administrator/templates/hathor/html/com_users/users/default.php', + '/administrator/templates/hathor/html/com_users/users/modal.php', + '/administrator/templates/hathor/html/com_weblinks/weblink/edit.php', + '/administrator/templates/hathor/html/com_weblinks/weblink/edit_params.php', + '/administrator/templates/hathor/html/com_weblinks/weblinks/default.php', + '/administrator/templates/hathor/html/layouts/com_media/toolbar/deletemedia.php', + '/administrator/templates/hathor/html/layouts/com_media/toolbar/newfolder.php', + '/administrator/templates/hathor/html/layouts/com_media/toolbar/uploadmedia.php', + '/administrator/templates/hathor/html/layouts/com_messages/toolbar/mysettings.php', + '/administrator/templates/hathor/html/layouts/com_modules/toolbar/cancelselect.php', + '/administrator/templates/hathor/html/layouts/com_modules/toolbar/newmodule.php', + '/administrator/templates/hathor/html/layouts/joomla/edit/details.php', + '/administrator/templates/hathor/html/layouts/joomla/edit/fieldset.php', + '/administrator/templates/hathor/html/layouts/joomla/edit/global.php', + '/administrator/templates/hathor/html/layouts/joomla/edit/metadata.php', + '/administrator/templates/hathor/html/layouts/joomla/edit/params.php', + '/administrator/templates/hathor/html/layouts/joomla/quickicons/icon.php', + '/administrator/templates/hathor/html/layouts/joomla/sidebars/submenu.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/base.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/batch.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/confirm.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/containerclose.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/containeropen.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/help.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/iconclass.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/link.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/modal.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/popup.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/separator.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/slider.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/standard.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/title.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/versions.php', + '/administrator/templates/hathor/html/layouts/plugins/user/profile/fields/dob.php', + '/administrator/templates/hathor/html/mod_login/default.php', + '/administrator/templates/hathor/html/mod_quickicon/default.php', + '/administrator/templates/hathor/html/modules.php', + '/administrator/templates/hathor/html/pagination.php', + '/administrator/templates/hathor/images/admin/blank.png', + '/administrator/templates/hathor/images/admin/checked_out.png', + '/administrator/templates/hathor/images/admin/collapseall.png', + '/administrator/templates/hathor/images/admin/disabled.png', + '/administrator/templates/hathor/images/admin/downarrow-1.png', + '/administrator/templates/hathor/images/admin/downarrow.png', + '/administrator/templates/hathor/images/admin/downarrow0.png', + '/administrator/templates/hathor/images/admin/expandall.png', + '/administrator/templates/hathor/images/admin/featured.png', + '/administrator/templates/hathor/images/admin/filesave.png', + '/administrator/templates/hathor/images/admin/filter_16.png', + '/administrator/templates/hathor/images/admin/icon-16-allow.png', + '/administrator/templates/hathor/images/admin/icon-16-allowinactive.png', + '/administrator/templates/hathor/images/admin/icon-16-deny.png', + '/administrator/templates/hathor/images/admin/icon-16-denyinactive.png', + '/administrator/templates/hathor/images/admin/icon-16-links.png', + '/administrator/templates/hathor/images/admin/icon-16-notice-note.png', + '/administrator/templates/hathor/images/admin/icon-16-protected.png', + '/administrator/templates/hathor/images/admin/menu_divider.png', + '/administrator/templates/hathor/images/admin/note_add_16.png', + '/administrator/templates/hathor/images/admin/publish_g.png', + '/administrator/templates/hathor/images/admin/publish_r.png', + '/administrator/templates/hathor/images/admin/publish_x.png', + '/administrator/templates/hathor/images/admin/publish_y.png', + '/administrator/templates/hathor/images/admin/sort_asc.png', + '/administrator/templates/hathor/images/admin/sort_desc.png', + '/administrator/templates/hathor/images/admin/tick.png', + '/administrator/templates/hathor/images/admin/trash.png', + '/administrator/templates/hathor/images/admin/uparrow-1.png', + '/administrator/templates/hathor/images/admin/uparrow.png', + '/administrator/templates/hathor/images/admin/uparrow0.png', + '/administrator/templates/hathor/images/arrow.png', + '/administrator/templates/hathor/images/bg-menu.gif', + '/administrator/templates/hathor/images/calendar.png', + '/administrator/templates/hathor/images/header/icon-48-alert.png', + '/administrator/templates/hathor/images/header/icon-48-apply.png', + '/administrator/templates/hathor/images/header/icon-48-archive.png', + '/administrator/templates/hathor/images/header/icon-48-article-add.png', + '/administrator/templates/hathor/images/header/icon-48-article-edit.png', + '/administrator/templates/hathor/images/header/icon-48-article.png', + '/administrator/templates/hathor/images/header/icon-48-assoc.png', + '/administrator/templates/hathor/images/header/icon-48-banner-categories.png', + '/administrator/templates/hathor/images/header/icon-48-banner-client.png', + '/administrator/templates/hathor/images/header/icon-48-banner-tracks.png', + '/administrator/templates/hathor/images/header/icon-48-banner.png', + '/administrator/templates/hathor/images/header/icon-48-calendar.png', + '/administrator/templates/hathor/images/header/icon-48-category-add.png', + '/administrator/templates/hathor/images/header/icon-48-category.png', + '/administrator/templates/hathor/images/header/icon-48-checkin.png', + '/administrator/templates/hathor/images/header/icon-48-clear.png', + '/administrator/templates/hathor/images/header/icon-48-component.png', + '/administrator/templates/hathor/images/header/icon-48-config.png', + '/administrator/templates/hathor/images/header/icon-48-contacts-categories.png', + '/administrator/templates/hathor/images/header/icon-48-contacts.png', + '/administrator/templates/hathor/images/header/icon-48-content.png', + '/administrator/templates/hathor/images/header/icon-48-cpanel.png', + '/administrator/templates/hathor/images/header/icon-48-default.png', + '/administrator/templates/hathor/images/header/icon-48-deny.png', + '/administrator/templates/hathor/images/header/icon-48-download.png', + '/administrator/templates/hathor/images/header/icon-48-edit.png', + '/administrator/templates/hathor/images/header/icon-48-extension.png', + '/administrator/templates/hathor/images/header/icon-48-featured.png', + '/administrator/templates/hathor/images/header/icon-48-frontpage.png', + '/administrator/templates/hathor/images/header/icon-48-generic.png', + '/administrator/templates/hathor/images/header/icon-48-groups-add.png', + '/administrator/templates/hathor/images/header/icon-48-groups.png', + '/administrator/templates/hathor/images/header/icon-48-help-forum.png', + '/administrator/templates/hathor/images/header/icon-48-help-this.png', + '/administrator/templates/hathor/images/header/icon-48-help_header.png', + '/administrator/templates/hathor/images/header/icon-48-inbox.png', + '/administrator/templates/hathor/images/header/icon-48-info.png', + '/administrator/templates/hathor/images/header/icon-48-install.png', + '/administrator/templates/hathor/images/header/icon-48-jupdate-updatefound.png', + '/administrator/templates/hathor/images/header/icon-48-jupdate-uptodate.png', + '/administrator/templates/hathor/images/header/icon-48-language.png', + '/administrator/templates/hathor/images/header/icon-48-levels-add.png', + '/administrator/templates/hathor/images/header/icon-48-levels.png', + '/administrator/templates/hathor/images/header/icon-48-links-cat.png', + '/administrator/templates/hathor/images/header/icon-48-links.png', + '/administrator/templates/hathor/images/header/icon-48-massmail.png', + '/administrator/templates/hathor/images/header/icon-48-media.png', + '/administrator/templates/hathor/images/header/icon-48-menu-add.png', + '/administrator/templates/hathor/images/header/icon-48-menu.png', + '/administrator/templates/hathor/images/header/icon-48-menumgr.png', + '/administrator/templates/hathor/images/header/icon-48-module.png', + '/administrator/templates/hathor/images/header/icon-48-move.png', + '/administrator/templates/hathor/images/header/icon-48-new-privatemessage.png', + '/administrator/templates/hathor/images/header/icon-48-newcategory.png', + '/administrator/templates/hathor/images/header/icon-48-newsfeeds-cat.png', + '/administrator/templates/hathor/images/header/icon-48-newsfeeds.png', + '/administrator/templates/hathor/images/header/icon-48-notice.png', + '/administrator/templates/hathor/images/header/icon-48-plugin.png', + '/administrator/templates/hathor/images/header/icon-48-preview.png', + '/administrator/templates/hathor/images/header/icon-48-print.png', + '/administrator/templates/hathor/images/header/icon-48-purge.png', + '/administrator/templates/hathor/images/header/icon-48-puzzle.png', + '/administrator/templates/hathor/images/header/icon-48-read-privatemessage.png', + '/administrator/templates/hathor/images/header/icon-48-readmess.png', + '/administrator/templates/hathor/images/header/icon-48-redirect.png', + '/administrator/templates/hathor/images/header/icon-48-revert.png', + '/administrator/templates/hathor/images/header/icon-48-search.png', + '/administrator/templates/hathor/images/header/icon-48-section.png', + '/administrator/templates/hathor/images/header/icon-48-send.png', + '/administrator/templates/hathor/images/header/icon-48-static.png', + '/administrator/templates/hathor/images/header/icon-48-stats.png', + '/administrator/templates/hathor/images/header/icon-48-tags.png', + '/administrator/templates/hathor/images/header/icon-48-themes.png', + '/administrator/templates/hathor/images/header/icon-48-trash.png', + '/administrator/templates/hathor/images/header/icon-48-unarchive.png', + '/administrator/templates/hathor/images/header/icon-48-upload.png', + '/administrator/templates/hathor/images/header/icon-48-user-add.png', + '/administrator/templates/hathor/images/header/icon-48-user-edit.png', + '/administrator/templates/hathor/images/header/icon-48-user-profile.png', + '/administrator/templates/hathor/images/header/icon-48-user.png', + '/administrator/templates/hathor/images/header/icon-48-writemess.png', + '/administrator/templates/hathor/images/header/icon-messaging.png', + '/administrator/templates/hathor/images/j_arrow.png', + '/administrator/templates/hathor/images/j_arrow_down.png', + '/administrator/templates/hathor/images/j_arrow_left.png', + '/administrator/templates/hathor/images/j_arrow_right.png', + '/administrator/templates/hathor/images/j_login_lock.png', + '/administrator/templates/hathor/images/j_logo.png', + '/administrator/templates/hathor/images/logo.png', + '/administrator/templates/hathor/images/menu/icon-16-alert.png', + '/administrator/templates/hathor/images/menu/icon-16-apply.png', + '/administrator/templates/hathor/images/menu/icon-16-archive.png', + '/administrator/templates/hathor/images/menu/icon-16-article.png', + '/administrator/templates/hathor/images/menu/icon-16-assoc.png', + '/administrator/templates/hathor/images/menu/icon-16-back-user.png', + '/administrator/templates/hathor/images/menu/icon-16-banner-categories.png', + '/administrator/templates/hathor/images/menu/icon-16-banner-client.png', + '/administrator/templates/hathor/images/menu/icon-16-banner-tracks.png', + '/administrator/templates/hathor/images/menu/icon-16-banner.png', + '/administrator/templates/hathor/images/menu/icon-16-calendar.png', + '/administrator/templates/hathor/images/menu/icon-16-category.png', + '/administrator/templates/hathor/images/menu/icon-16-checkin.png', + '/administrator/templates/hathor/images/menu/icon-16-clear.png', + '/administrator/templates/hathor/images/menu/icon-16-component.png', + '/administrator/templates/hathor/images/menu/icon-16-config.png', + '/administrator/templates/hathor/images/menu/icon-16-contacts-categories.png', + '/administrator/templates/hathor/images/menu/icon-16-contacts.png', + '/administrator/templates/hathor/images/menu/icon-16-content.png', + '/administrator/templates/hathor/images/menu/icon-16-cpanel.png', + '/administrator/templates/hathor/images/menu/icon-16-default.png', + '/administrator/templates/hathor/images/menu/icon-16-delete.png', + '/administrator/templates/hathor/images/menu/icon-16-deny.png', + '/administrator/templates/hathor/images/menu/icon-16-download.png', + '/administrator/templates/hathor/images/menu/icon-16-edit.png', + '/administrator/templates/hathor/images/menu/icon-16-featured.png', + '/administrator/templates/hathor/images/menu/icon-16-frontpage.png', + '/administrator/templates/hathor/images/menu/icon-16-generic.png', + '/administrator/templates/hathor/images/menu/icon-16-groups.png', + '/administrator/templates/hathor/images/menu/icon-16-help-community.png', + '/administrator/templates/hathor/images/menu/icon-16-help-dev.png', + '/administrator/templates/hathor/images/menu/icon-16-help-docs.png', + '/administrator/templates/hathor/images/menu/icon-16-help-forum.png', + '/administrator/templates/hathor/images/menu/icon-16-help-jed.png', + '/administrator/templates/hathor/images/menu/icon-16-help-jrd.png', + '/administrator/templates/hathor/images/menu/icon-16-help-security.png', + '/administrator/templates/hathor/images/menu/icon-16-help-shop.png', + '/administrator/templates/hathor/images/menu/icon-16-help-this.png', + '/administrator/templates/hathor/images/menu/icon-16-help-trans.png', + '/administrator/templates/hathor/images/menu/icon-16-help.png', + '/administrator/templates/hathor/images/menu/icon-16-inbox.png', + '/administrator/templates/hathor/images/menu/icon-16-info.png', + '/administrator/templates/hathor/images/menu/icon-16-install.png', + '/administrator/templates/hathor/images/menu/icon-16-language.png', + '/administrator/templates/hathor/images/menu/icon-16-levels.png', + '/administrator/templates/hathor/images/menu/icon-16-links-cat.png', + '/administrator/templates/hathor/images/menu/icon-16-links.png', + '/administrator/templates/hathor/images/menu/icon-16-logout.png', + '/administrator/templates/hathor/images/menu/icon-16-maintenance.png', + '/administrator/templates/hathor/images/menu/icon-16-massmail.png', + '/administrator/templates/hathor/images/menu/icon-16-media.png', + '/administrator/templates/hathor/images/menu/icon-16-menu.png', + '/administrator/templates/hathor/images/menu/icon-16-menumgr.png', + '/administrator/templates/hathor/images/menu/icon-16-messages.png', + '/administrator/templates/hathor/images/menu/icon-16-messaging.png', + '/administrator/templates/hathor/images/menu/icon-16-module.png', + '/administrator/templates/hathor/images/menu/icon-16-move.png', + '/administrator/templates/hathor/images/menu/icon-16-new-privatemessage.png', + '/administrator/templates/hathor/images/menu/icon-16-new.png', + '/administrator/templates/hathor/images/menu/icon-16-newarticle.png', + '/administrator/templates/hathor/images/menu/icon-16-newcategory.png', + '/administrator/templates/hathor/images/menu/icon-16-newgroup.png', + '/administrator/templates/hathor/images/menu/icon-16-newlevel.png', + '/administrator/templates/hathor/images/menu/icon-16-newsfeeds-cat.png', + '/administrator/templates/hathor/images/menu/icon-16-newsfeeds.png', + '/administrator/templates/hathor/images/menu/icon-16-newuser.png', + '/administrator/templates/hathor/images/menu/icon-16-nopreview.png', + '/administrator/templates/hathor/images/menu/icon-16-notdefault.png', + '/administrator/templates/hathor/images/menu/icon-16-notice.png', + '/administrator/templates/hathor/images/menu/icon-16-plugin.png', + '/administrator/templates/hathor/images/menu/icon-16-preview.png', + '/administrator/templates/hathor/images/menu/icon-16-print.png', + '/administrator/templates/hathor/images/menu/icon-16-purge.png', + '/administrator/templates/hathor/images/menu/icon-16-puzzle.png', + '/administrator/templates/hathor/images/menu/icon-16-read-privatemessage.png', + '/administrator/templates/hathor/images/menu/icon-16-readmess.png', + '/administrator/templates/hathor/images/menu/icon-16-redirect.png', + '/administrator/templates/hathor/images/menu/icon-16-revert.png', + '/administrator/templates/hathor/images/menu/icon-16-search.png', + '/administrator/templates/hathor/images/menu/icon-16-send.png', + '/administrator/templates/hathor/images/menu/icon-16-stats.png', + '/administrator/templates/hathor/images/menu/icon-16-tags.png', + '/administrator/templates/hathor/images/menu/icon-16-themes.png', + '/administrator/templates/hathor/images/menu/icon-16-trash.png', + '/administrator/templates/hathor/images/menu/icon-16-unarticle.png', + '/administrator/templates/hathor/images/menu/icon-16-upload.png', + '/administrator/templates/hathor/images/menu/icon-16-user-dd.png', + '/administrator/templates/hathor/images/menu/icon-16-user-note.png', + '/administrator/templates/hathor/images/menu/icon-16-user.png', + '/administrator/templates/hathor/images/menu/icon-16-viewsite.png', + '/administrator/templates/hathor/images/menu/icon-16-writemess.png', + '/administrator/templates/hathor/images/mini_icon.png', + '/administrator/templates/hathor/images/notice-alert.png', + '/administrator/templates/hathor/images/notice-info.png', + '/administrator/templates/hathor/images/notice-note.png', + '/administrator/templates/hathor/images/required.png', + '/administrator/templates/hathor/images/selector-arrow-hc.png', + '/administrator/templates/hathor/images/selector-arrow-rtl.png', + '/administrator/templates/hathor/images/selector-arrow-std.png', + '/administrator/templates/hathor/images/selector-arrow.png', + '/administrator/templates/hathor/images/system/calendar.png', + '/administrator/templates/hathor/images/system/selector-arrow.png', + '/administrator/templates/hathor/images/toolbar/icon-32-adduser.png', + '/administrator/templates/hathor/images/toolbar/icon-32-alert.png', + '/administrator/templates/hathor/images/toolbar/icon-32-apply.png', + '/administrator/templates/hathor/images/toolbar/icon-32-archive.png', + '/administrator/templates/hathor/images/toolbar/icon-32-article-add.png', + '/administrator/templates/hathor/images/toolbar/icon-32-article.png', + '/administrator/templates/hathor/images/toolbar/icon-32-back.png', + '/administrator/templates/hathor/images/toolbar/icon-32-banner-categories.png', + '/administrator/templates/hathor/images/toolbar/icon-32-banner-client.png', + '/administrator/templates/hathor/images/toolbar/icon-32-banner-tracks.png', + '/administrator/templates/hathor/images/toolbar/icon-32-banner.png', + '/administrator/templates/hathor/images/toolbar/icon-32-batch.png', + '/administrator/templates/hathor/images/toolbar/icon-32-calendar.png', + '/administrator/templates/hathor/images/toolbar/icon-32-cancel.png', + '/administrator/templates/hathor/images/toolbar/icon-32-checkin.png', + '/administrator/templates/hathor/images/toolbar/icon-32-cog.png', + '/administrator/templates/hathor/images/toolbar/icon-32-component.png', + '/administrator/templates/hathor/images/toolbar/icon-32-config.png', + '/administrator/templates/hathor/images/toolbar/icon-32-contacts-categories.png', + '/administrator/templates/hathor/images/toolbar/icon-32-contacts.png', + '/administrator/templates/hathor/images/toolbar/icon-32-copy.png', + '/administrator/templates/hathor/images/toolbar/icon-32-css.png', + '/administrator/templates/hathor/images/toolbar/icon-32-default.png', + '/administrator/templates/hathor/images/toolbar/icon-32-delete-style.png', + '/administrator/templates/hathor/images/toolbar/icon-32-delete.png', + '/administrator/templates/hathor/images/toolbar/icon-32-deny.png', + '/administrator/templates/hathor/images/toolbar/icon-32-download.png', + '/administrator/templates/hathor/images/toolbar/icon-32-edit.png', + '/administrator/templates/hathor/images/toolbar/icon-32-error.png', + '/administrator/templates/hathor/images/toolbar/icon-32-export.png', + '/administrator/templates/hathor/images/toolbar/icon-32-extension.png', + '/administrator/templates/hathor/images/toolbar/icon-32-featured.png', + '/administrator/templates/hathor/images/toolbar/icon-32-forward.png', + '/administrator/templates/hathor/images/toolbar/icon-32-help.png', + '/administrator/templates/hathor/images/toolbar/icon-32-html.png', + '/administrator/templates/hathor/images/toolbar/icon-32-inbox.png', + '/administrator/templates/hathor/images/toolbar/icon-32-info.png', + '/administrator/templates/hathor/images/toolbar/icon-32-links.png', + '/administrator/templates/hathor/images/toolbar/icon-32-lock.png', + '/administrator/templates/hathor/images/toolbar/icon-32-menu.png', + '/administrator/templates/hathor/images/toolbar/icon-32-messaging.png', + '/administrator/templates/hathor/images/toolbar/icon-32-messanging.png', + '/administrator/templates/hathor/images/toolbar/icon-32-module.png', + '/administrator/templates/hathor/images/toolbar/icon-32-move.png', + '/administrator/templates/hathor/images/toolbar/icon-32-new-privatemessage.png', + '/administrator/templates/hathor/images/toolbar/icon-32-new-style.png', + '/administrator/templates/hathor/images/toolbar/icon-32-new.png', + '/administrator/templates/hathor/images/toolbar/icon-32-notice.png', + '/administrator/templates/hathor/images/toolbar/icon-32-preview.png', + '/administrator/templates/hathor/images/toolbar/icon-32-print.png', + '/administrator/templates/hathor/images/toolbar/icon-32-publish.png', + '/administrator/templates/hathor/images/toolbar/icon-32-purge.png', + '/administrator/templates/hathor/images/toolbar/icon-32-read-privatemessage.png', + '/administrator/templates/hathor/images/toolbar/icon-32-refresh.png', + '/administrator/templates/hathor/images/toolbar/icon-32-remove.png', + '/administrator/templates/hathor/images/toolbar/icon-32-revert.png', + '/administrator/templates/hathor/images/toolbar/icon-32-save-copy.png', + '/administrator/templates/hathor/images/toolbar/icon-32-save-new.png', + '/administrator/templates/hathor/images/toolbar/icon-32-save.png', + '/administrator/templates/hathor/images/toolbar/icon-32-search.png', + '/administrator/templates/hathor/images/toolbar/icon-32-send.png', + '/administrator/templates/hathor/images/toolbar/icon-32-stats.png', + '/administrator/templates/hathor/images/toolbar/icon-32-trash.png', + '/administrator/templates/hathor/images/toolbar/icon-32-unarchive.png', + '/administrator/templates/hathor/images/toolbar/icon-32-unblock.png', + '/administrator/templates/hathor/images/toolbar/icon-32-unpublish.png', + '/administrator/templates/hathor/images/toolbar/icon-32-upload.png', + '/administrator/templates/hathor/images/toolbar/icon-32-user-add.png', + '/administrator/templates/hathor/images/toolbar/icon-32-xml.png', + '/administrator/templates/hathor/index.php', + '/administrator/templates/hathor/js/template.js', + '/administrator/templates/hathor/language/en-GB/en-GB.tpl_hathor.ini', + '/administrator/templates/hathor/language/en-GB/en-GB.tpl_hathor.sys.ini', + '/administrator/templates/hathor/less/buttons.less', + '/administrator/templates/hathor/less/colour_baseline.less', + '/administrator/templates/hathor/less/colour_blue.less', + '/administrator/templates/hathor/less/colour_brown.less', + '/administrator/templates/hathor/less/colour_standard.less', + '/administrator/templates/hathor/less/forms.less', + '/administrator/templates/hathor/less/hathor_variables.less', + '/administrator/templates/hathor/less/icomoon.less', + '/administrator/templates/hathor/less/modals.less', + '/administrator/templates/hathor/less/template.less', + '/administrator/templates/hathor/less/variables.less', + '/administrator/templates/hathor/login.php', + '/administrator/templates/hathor/postinstall/hathormessage.php', + '/administrator/templates/hathor/templateDetails.xml', + '/administrator/templates/hathor/template_preview.png', + '/administrator/templates/hathor/template_thumbnail.png', + '/administrator/templates/isis/component.php', + '/administrator/templates/isis/cpanel.php', + '/administrator/templates/isis/css/template-rtl.css', + '/administrator/templates/isis/css/template.css', + '/administrator/templates/isis/error.php', + '/administrator/templates/isis/favicon.ico', + '/administrator/templates/isis/html/com_media/imageslist/default_folder.php', + '/administrator/templates/isis/html/com_media/imageslist/default_image.php', + '/administrator/templates/isis/html/com_media/medialist/thumbs_folders.php', + '/administrator/templates/isis/html/com_media/medialist/thumbs_imgs.php', + '/administrator/templates/isis/html/editor_content.css', + '/administrator/templates/isis/html/layouts/joomla/form/field/media.php', + '/administrator/templates/isis/html/layouts/joomla/form/field/user.php', + '/administrator/templates/isis/html/layouts/joomla/pagination/link.php', + '/administrator/templates/isis/html/layouts/joomla/pagination/links.php', + '/administrator/templates/isis/html/layouts/joomla/system/message.php', + '/administrator/templates/isis/html/layouts/joomla/toolbar/versions.php', + '/administrator/templates/isis/html/mod_version/default.php', + '/administrator/templates/isis/html/modules.php', + '/administrator/templates/isis/html/pagination.php', + '/administrator/templates/isis/images/admin/blank.png', + '/administrator/templates/isis/images/admin/checked_out.png', + '/administrator/templates/isis/images/admin/collapseall.png', + '/administrator/templates/isis/images/admin/disabled.png', + '/administrator/templates/isis/images/admin/downarrow-1.png', + '/administrator/templates/isis/images/admin/downarrow.png', + '/administrator/templates/isis/images/admin/downarrow0.png', + '/administrator/templates/isis/images/admin/expandall.png', + '/administrator/templates/isis/images/admin/featured.png', + '/administrator/templates/isis/images/admin/filesave.png', + '/administrator/templates/isis/images/admin/filter_16.png', + '/administrator/templates/isis/images/admin/icon-16-add.png', + '/administrator/templates/isis/images/admin/icon-16-allow.png', + '/administrator/templates/isis/images/admin/icon-16-allowinactive.png', + '/administrator/templates/isis/images/admin/icon-16-deny.png', + '/administrator/templates/isis/images/admin/icon-16-denyinactive.png', + '/administrator/templates/isis/images/admin/icon-16-links.png', + '/administrator/templates/isis/images/admin/icon-16-notice-note.png', + '/administrator/templates/isis/images/admin/icon-16-protected.png', + '/administrator/templates/isis/images/admin/menu_divider.png', + '/administrator/templates/isis/images/admin/note_add_16.png', + '/administrator/templates/isis/images/admin/publish_g.png', + '/administrator/templates/isis/images/admin/publish_r.png', + '/administrator/templates/isis/images/admin/publish_x.png', + '/administrator/templates/isis/images/admin/publish_y.png', + '/administrator/templates/isis/images/admin/sort_asc.png', + '/administrator/templates/isis/images/admin/sort_desc.png', + '/administrator/templates/isis/images/admin/tick.png', + '/administrator/templates/isis/images/admin/trash.png', + '/administrator/templates/isis/images/admin/uparrow-1.png', + '/administrator/templates/isis/images/admin/uparrow.png', + '/administrator/templates/isis/images/admin/uparrow0.png', + '/administrator/templates/isis/images/emailButton.png', + '/administrator/templates/isis/images/joomla.png', + '/administrator/templates/isis/images/login-joomla-inverse.png', + '/administrator/templates/isis/images/login-joomla.png', + '/administrator/templates/isis/images/logo-inverse.png', + '/administrator/templates/isis/images/logo.png', + '/administrator/templates/isis/images/pdf_button.png', + '/administrator/templates/isis/images/printButton.png', + '/administrator/templates/isis/images/system/sort_asc.png', + '/administrator/templates/isis/images/system/sort_desc.png', + '/administrator/templates/isis/img/glyphicons-halflings-white.png', + '/administrator/templates/isis/img/glyphicons-halflings.png', + '/administrator/templates/isis/index.php', + '/administrator/templates/isis/js/application.js', + '/administrator/templates/isis/js/classes.js', + '/administrator/templates/isis/js/template.js', + '/administrator/templates/isis/language/en-GB/en-GB.tpl_isis.ini', + '/administrator/templates/isis/language/en-GB/en-GB.tpl_isis.sys.ini', + '/administrator/templates/isis/less/blocks/_chzn-override.less', + '/administrator/templates/isis/less/blocks/_custom.less', + '/administrator/templates/isis/less/blocks/_editors.less', + '/administrator/templates/isis/less/blocks/_forms.less', + '/administrator/templates/isis/less/blocks/_global.less', + '/administrator/templates/isis/less/blocks/_header.less', + '/administrator/templates/isis/less/blocks/_login.less', + '/administrator/templates/isis/less/blocks/_media.less', + '/administrator/templates/isis/less/blocks/_modals.less', + '/administrator/templates/isis/less/blocks/_navbar.less', + '/administrator/templates/isis/less/blocks/_quickicons.less', + '/administrator/templates/isis/less/blocks/_sidebar.less', + '/administrator/templates/isis/less/blocks/_status.less', + '/administrator/templates/isis/less/blocks/_tables.less', + '/administrator/templates/isis/less/blocks/_toolbar.less', + '/administrator/templates/isis/less/blocks/_treeselect.less', + '/administrator/templates/isis/less/blocks/_utility-classes.less', + '/administrator/templates/isis/less/bootstrap/button-groups.less', + '/administrator/templates/isis/less/bootstrap/buttons.less', + '/administrator/templates/isis/less/bootstrap/mixins.less', + '/administrator/templates/isis/less/bootstrap/responsive-1200px-min.less', + '/administrator/templates/isis/less/bootstrap/responsive-768px-979px.less', + '/administrator/templates/isis/less/bootstrap/wells.less', + '/administrator/templates/isis/less/icomoon.less', + '/administrator/templates/isis/less/pages/_com_cpanel.less', + '/administrator/templates/isis/less/pages/_com_postinstall.less', + '/administrator/templates/isis/less/pages/_com_privacy.less', + '/administrator/templates/isis/less/pages/_com_templates.less', + '/administrator/templates/isis/less/template-rtl.less', + '/administrator/templates/isis/less/template.less', + '/administrator/templates/isis/less/variables.less', + '/administrator/templates/isis/login.php', + '/administrator/templates/isis/templateDetails.xml', + '/administrator/templates/isis/template_preview.png', + '/administrator/templates/isis/template_thumbnail.png', + '/administrator/templates/system/html/modules.php', + '/bin/index.html', + '/bin/keychain.php', + '/cli/deletefiles.php', + '/cli/finder_indexer.php', + '/cli/garbagecron.php', + '/cli/sessionGc.php', + '/cli/sessionMetadataGc.php', + '/cli/update_cron.php', + '/components/com_banners/banners.php', + '/components/com_banners/controller.php', + '/components/com_banners/helpers/banner.php', + '/components/com_banners/helpers/category.php', + '/components/com_banners/models/banner.php', + '/components/com_banners/models/banners.php', + '/components/com_banners/router.php', + '/components/com_config/config.php', + '/components/com_config/controller/cancel.php', + '/components/com_config/controller/canceladmin.php', + '/components/com_config/controller/cmsbase.php', + '/components/com_config/controller/config/display.php', + '/components/com_config/controller/config/save.php', + '/components/com_config/controller/display.php', + '/components/com_config/controller/helper.php', + '/components/com_config/controller/modules/cancel.php', + '/components/com_config/controller/modules/display.php', + '/components/com_config/controller/modules/save.php', + '/components/com_config/controller/templates/display.php', + '/components/com_config/controller/templates/save.php', + '/components/com_config/model/cms.php', + '/components/com_config/model/config.php', + '/components/com_config/model/form.php', + '/components/com_config/model/form/config.xml', + '/components/com_config/model/form/modules.xml', + '/components/com_config/model/form/modules_advanced.xml', + '/components/com_config/model/form/templates.xml', + '/components/com_config/model/modules.php', + '/components/com_config/model/templates.php', + '/components/com_config/view/cms/html.php', + '/components/com_config/view/cms/json.php', + '/components/com_config/view/config/html.php', + '/components/com_config/view/config/tmpl/default.php', + '/components/com_config/view/config/tmpl/default.xml', + '/components/com_config/view/config/tmpl/default_metadata.php', + '/components/com_config/view/config/tmpl/default_seo.php', + '/components/com_config/view/config/tmpl/default_site.php', + '/components/com_config/view/modules/html.php', + '/components/com_config/view/modules/tmpl/default.php', + '/components/com_config/view/modules/tmpl/default_options.php', + '/components/com_config/view/modules/tmpl/default_positions.php', + '/components/com_config/view/templates/html.php', + '/components/com_config/view/templates/tmpl/default.php', + '/components/com_config/view/templates/tmpl/default.xml', + '/components/com_config/view/templates/tmpl/default_options.php', + '/components/com_contact/contact.php', + '/components/com_contact/controller.php', + '/components/com_contact/controllers/contact.php', + '/components/com_contact/helpers/association.php', + '/components/com_contact/helpers/category.php', + '/components/com_contact/helpers/legacyrouter.php', + '/components/com_contact/layouts/joomla/form/renderfield.php', + '/components/com_contact/models/categories.php', + '/components/com_contact/models/category.php', + '/components/com_contact/models/contact.php', + '/components/com_contact/models/featured.php', + '/components/com_contact/models/forms/contact.xml', + '/components/com_contact/models/forms/filter_contacts.xml', + '/components/com_contact/models/forms/form.xml', + '/components/com_contact/models/rules/contactemail.php', + '/components/com_contact/models/rules/contactemailmessage.php', + '/components/com_contact/models/rules/contactemailsubject.php', + '/components/com_contact/router.php', + '/components/com_contact/views/categories/tmpl/default.php', + '/components/com_contact/views/categories/tmpl/default.xml', + '/components/com_contact/views/categories/tmpl/default_items.php', + '/components/com_contact/views/categories/view.html.php', + '/components/com_contact/views/category/tmpl/default.php', + '/components/com_contact/views/category/tmpl/default.xml', + '/components/com_contact/views/category/tmpl/default_children.php', + '/components/com_contact/views/category/tmpl/default_items.php', + '/components/com_contact/views/category/view.feed.php', + '/components/com_contact/views/category/view.html.php', + '/components/com_contact/views/contact/tmpl/default.php', + '/components/com_contact/views/contact/tmpl/default.xml', + '/components/com_contact/views/contact/tmpl/default_address.php', + '/components/com_contact/views/contact/tmpl/default_articles.php', + '/components/com_contact/views/contact/tmpl/default_form.php', + '/components/com_contact/views/contact/tmpl/default_links.php', + '/components/com_contact/views/contact/tmpl/default_profile.php', + '/components/com_contact/views/contact/tmpl/default_user_custom_fields.php', + '/components/com_contact/views/contact/view.html.php', + '/components/com_contact/views/contact/view.vcf.php', + '/components/com_contact/views/featured/tmpl/default.php', + '/components/com_contact/views/featured/tmpl/default.xml', + '/components/com_contact/views/featured/tmpl/default_items.php', + '/components/com_contact/views/featured/view.html.php', + '/components/com_content/content.php', + '/components/com_content/controller.php', + '/components/com_content/controllers/article.php', + '/components/com_content/helpers/association.php', + '/components/com_content/helpers/category.php', + '/components/com_content/helpers/legacyrouter.php', + '/components/com_content/helpers/query.php', + '/components/com_content/helpers/route.php', + '/components/com_content/models/archive.php', + '/components/com_content/models/article.php', + '/components/com_content/models/articles.php', + '/components/com_content/models/categories.php', + '/components/com_content/models/category.php', + '/components/com_content/models/featured.php', + '/components/com_content/models/form.php', + '/components/com_content/models/forms/article.xml', + '/components/com_content/models/forms/filter_articles.xml', + '/components/com_content/router.php', + '/components/com_content/views/archive/tmpl/default.php', + '/components/com_content/views/archive/tmpl/default.xml', + '/components/com_content/views/archive/tmpl/default_items.php', + '/components/com_content/views/archive/view.html.php', + '/components/com_content/views/article/tmpl/default.php', + '/components/com_content/views/article/tmpl/default.xml', + '/components/com_content/views/article/tmpl/default_links.php', + '/components/com_content/views/article/view.html.php', + '/components/com_content/views/categories/tmpl/default.php', + '/components/com_content/views/categories/tmpl/default.xml', + '/components/com_content/views/categories/tmpl/default_items.php', + '/components/com_content/views/categories/view.html.php', + '/components/com_content/views/category/tmpl/blog.php', + '/components/com_content/views/category/tmpl/blog.xml', + '/components/com_content/views/category/tmpl/blog_children.php', + '/components/com_content/views/category/tmpl/blog_item.php', + '/components/com_content/views/category/tmpl/blog_links.php', + '/components/com_content/views/category/tmpl/default.php', + '/components/com_content/views/category/tmpl/default.xml', + '/components/com_content/views/category/tmpl/default_articles.php', + '/components/com_content/views/category/tmpl/default_children.php', + '/components/com_content/views/category/view.feed.php', + '/components/com_content/views/category/view.html.php', + '/components/com_content/views/featured/tmpl/default.php', + '/components/com_content/views/featured/tmpl/default.xml', + '/components/com_content/views/featured/tmpl/default_item.php', + '/components/com_content/views/featured/tmpl/default_links.php', + '/components/com_content/views/featured/view.feed.php', + '/components/com_content/views/featured/view.html.php', + '/components/com_content/views/form/tmpl/edit.php', + '/components/com_content/views/form/tmpl/edit.xml', + '/components/com_content/views/form/view.html.php', + '/components/com_contenthistory/contenthistory.php', + '/components/com_fields/controller.php', + '/components/com_fields/fields.php', + '/components/com_fields/models/forms/filter_fields.xml', + '/components/com_finder/controller.php', + '/components/com_finder/controllers/suggestions.json.php', + '/components/com_finder/finder.php', + '/components/com_finder/helpers/html/filter.php', + '/components/com_finder/helpers/html/query.php', + '/components/com_finder/models/search.php', + '/components/com_finder/models/suggestions.php', + '/components/com_finder/router.php', + '/components/com_finder/views/search/tmpl/default.php', + '/components/com_finder/views/search/tmpl/default.xml', + '/components/com_finder/views/search/tmpl/default_form.php', + '/components/com_finder/views/search/tmpl/default_result.php', + '/components/com_finder/views/search/tmpl/default_results.php', + '/components/com_finder/views/search/view.feed.php', + '/components/com_finder/views/search/view.html.php', + '/components/com_finder/views/search/view.opensearch.php', + '/components/com_mailto/controller.php', + '/components/com_mailto/helpers/mailto.php', + '/components/com_mailto/mailto.php', + '/components/com_mailto/mailto.xml', + '/components/com_mailto/models/forms/mailto.xml', + '/components/com_mailto/models/mailto.php', + '/components/com_mailto/views/mailto/tmpl/default.php', + '/components/com_mailto/views/mailto/view.html.php', + '/components/com_mailto/views/sent/tmpl/default.php', + '/components/com_mailto/views/sent/view.html.php', + '/components/com_media/media.php', + '/components/com_menus/controller.php', + '/components/com_menus/menus.php', + '/components/com_menus/models/forms/filter_items.xml', + '/components/com_modules/controller.php', + '/components/com_modules/models/forms/filter_modules.xml', + '/components/com_modules/modules.php', + '/components/com_newsfeeds/controller.php', + '/components/com_newsfeeds/helpers/association.php', + '/components/com_newsfeeds/helpers/category.php', + '/components/com_newsfeeds/helpers/legacyrouter.php', + '/components/com_newsfeeds/models/categories.php', + '/components/com_newsfeeds/models/category.php', + '/components/com_newsfeeds/models/newsfeed.php', + '/components/com_newsfeeds/newsfeeds.php', + '/components/com_newsfeeds/router.php', + '/components/com_newsfeeds/views/categories/tmpl/default.php', + '/components/com_newsfeeds/views/categories/tmpl/default.xml', + '/components/com_newsfeeds/views/categories/tmpl/default_items.php', + '/components/com_newsfeeds/views/categories/view.html.php', + '/components/com_newsfeeds/views/category/tmpl/default.php', + '/components/com_newsfeeds/views/category/tmpl/default.xml', + '/components/com_newsfeeds/views/category/tmpl/default_children.php', + '/components/com_newsfeeds/views/category/tmpl/default_items.php', + '/components/com_newsfeeds/views/category/view.html.php', + '/components/com_newsfeeds/views/newsfeed/tmpl/default.php', + '/components/com_newsfeeds/views/newsfeed/tmpl/default.xml', + '/components/com_newsfeeds/views/newsfeed/view.html.php', + '/components/com_privacy/controller.php', + '/components/com_privacy/controllers/request.php', + '/components/com_privacy/models/confirm.php', + '/components/com_privacy/models/forms/confirm.xml', + '/components/com_privacy/models/forms/remind.xml', + '/components/com_privacy/models/forms/request.xml', + '/components/com_privacy/models/remind.php', + '/components/com_privacy/models/request.php', + '/components/com_privacy/privacy.php', + '/components/com_privacy/router.php', + '/components/com_privacy/views/confirm/tmpl/default.php', + '/components/com_privacy/views/confirm/tmpl/default.xml', + '/components/com_privacy/views/confirm/view.html.php', + '/components/com_privacy/views/remind/tmpl/default.php', + '/components/com_privacy/views/remind/tmpl/default.xml', + '/components/com_privacy/views/remind/view.html.php', + '/components/com_privacy/views/request/tmpl/default.php', + '/components/com_privacy/views/request/tmpl/default.xml', + '/components/com_privacy/views/request/view.html.php', + '/components/com_tags/controller.php', + '/components/com_tags/controllers/tags.php', + '/components/com_tags/models/tag.php', + '/components/com_tags/models/tags.php', + '/components/com_tags/router.php', + '/components/com_tags/tags.php', + '/components/com_tags/views/tag/tmpl/default.php', + '/components/com_tags/views/tag/tmpl/default.xml', + '/components/com_tags/views/tag/tmpl/default_items.php', + '/components/com_tags/views/tag/tmpl/list.php', + '/components/com_tags/views/tag/tmpl/list.xml', + '/components/com_tags/views/tag/tmpl/list_items.php', + '/components/com_tags/views/tag/view.feed.php', + '/components/com_tags/views/tag/view.html.php', + '/components/com_tags/views/tags/tmpl/default.php', + '/components/com_tags/views/tags/tmpl/default.xml', + '/components/com_tags/views/tags/tmpl/default_items.php', + '/components/com_tags/views/tags/view.feed.php', + '/components/com_tags/views/tags/view.html.php', + '/components/com_users/controller.php', + '/components/com_users/controllers/profile.php', + '/components/com_users/controllers/registration.php', + '/components/com_users/controllers/remind.php', + '/components/com_users/controllers/reset.php', + '/components/com_users/controllers/user.php', + '/components/com_users/helpers/html/users.php', + '/components/com_users/helpers/legacyrouter.php', + '/components/com_users/helpers/route.php', + '/components/com_users/layouts/joomla/form/renderfield.php', + '/components/com_users/models/forms/frontend.xml', + '/components/com_users/models/forms/frontend_admin.xml', + '/components/com_users/models/forms/login.xml', + '/components/com_users/models/forms/profile.xml', + '/components/com_users/models/forms/registration.xml', + '/components/com_users/models/forms/remind.xml', + '/components/com_users/models/forms/reset_complete.xml', + '/components/com_users/models/forms/reset_confirm.xml', + '/components/com_users/models/forms/reset_request.xml', + '/components/com_users/models/forms/sitelang.xml', + '/components/com_users/models/login.php', + '/components/com_users/models/profile.php', + '/components/com_users/models/registration.php', + '/components/com_users/models/remind.php', + '/components/com_users/models/reset.php', + '/components/com_users/models/rules/loginuniquefield.php', + '/components/com_users/models/rules/logoutuniquefield.php', + '/components/com_users/router.php', + '/components/com_users/users.php', + '/components/com_users/views/login/tmpl/default.php', + '/components/com_users/views/login/tmpl/default.xml', + '/components/com_users/views/login/tmpl/default_login.php', + '/components/com_users/views/login/tmpl/default_logout.php', + '/components/com_users/views/login/tmpl/logout.xml', + '/components/com_users/views/login/view.html.php', + '/components/com_users/views/profile/tmpl/default.php', + '/components/com_users/views/profile/tmpl/default.xml', + '/components/com_users/views/profile/tmpl/default_core.php', + '/components/com_users/views/profile/tmpl/default_custom.php', + '/components/com_users/views/profile/tmpl/default_params.php', + '/components/com_users/views/profile/tmpl/edit.php', + '/components/com_users/views/profile/tmpl/edit.xml', + '/components/com_users/views/profile/view.html.php', + '/components/com_users/views/registration/tmpl/complete.php', + '/components/com_users/views/registration/tmpl/default.php', + '/components/com_users/views/registration/tmpl/default.xml', + '/components/com_users/views/registration/view.html.php', + '/components/com_users/views/remind/tmpl/default.php', + '/components/com_users/views/remind/tmpl/default.xml', + '/components/com_users/views/remind/view.html.php', + '/components/com_users/views/reset/tmpl/complete.php', + '/components/com_users/views/reset/tmpl/confirm.php', + '/components/com_users/views/reset/tmpl/default.php', + '/components/com_users/views/reset/tmpl/default.xml', + '/components/com_users/views/reset/view.html.php', + '/components/com_wrapper/controller.php', + '/components/com_wrapper/router.php', + '/components/com_wrapper/views/wrapper/tmpl/default.php', + '/components/com_wrapper/views/wrapper/tmpl/default.xml', + '/components/com_wrapper/views/wrapper/view.html.php', + '/components/com_wrapper/wrapper.php', + '/components/com_wrapper/wrapper.xml', + '/language/en-GB/en-GB.com_ajax.ini', + '/language/en-GB/en-GB.com_config.ini', + '/language/en-GB/en-GB.com_contact.ini', + '/language/en-GB/en-GB.com_content.ini', + '/language/en-GB/en-GB.com_finder.ini', + '/language/en-GB/en-GB.com_mailto.ini', + '/language/en-GB/en-GB.com_media.ini', + '/language/en-GB/en-GB.com_messages.ini', + '/language/en-GB/en-GB.com_newsfeeds.ini', + '/language/en-GB/en-GB.com_privacy.ini', + '/language/en-GB/en-GB.com_tags.ini', + '/language/en-GB/en-GB.com_users.ini', + '/language/en-GB/en-GB.com_weblinks.ini', + '/language/en-GB/en-GB.com_wrapper.ini', + '/language/en-GB/en-GB.files_joomla.sys.ini', + '/language/en-GB/en-GB.finder_cli.ini', + '/language/en-GB/en-GB.ini', + '/language/en-GB/en-GB.lib_fof.ini', + '/language/en-GB/en-GB.lib_fof.sys.ini', + '/language/en-GB/en-GB.lib_idna_convert.sys.ini', + '/language/en-GB/en-GB.lib_joomla.ini', + '/language/en-GB/en-GB.lib_joomla.sys.ini', + '/language/en-GB/en-GB.lib_phpass.sys.ini', + '/language/en-GB/en-GB.lib_phputf8.sys.ini', + '/language/en-GB/en-GB.lib_simplepie.sys.ini', + '/language/en-GB/en-GB.localise.php', + '/language/en-GB/en-GB.mod_articles_archive.ini', + '/language/en-GB/en-GB.mod_articles_archive.sys.ini', + '/language/en-GB/en-GB.mod_articles_categories.ini', + '/language/en-GB/en-GB.mod_articles_categories.sys.ini', + '/language/en-GB/en-GB.mod_articles_category.ini', + '/language/en-GB/en-GB.mod_articles_category.sys.ini', + '/language/en-GB/en-GB.mod_articles_latest.ini', + '/language/en-GB/en-GB.mod_articles_latest.sys.ini', + '/language/en-GB/en-GB.mod_articles_news.ini', + '/language/en-GB/en-GB.mod_articles_news.sys.ini', + '/language/en-GB/en-GB.mod_articles_popular.ini', + '/language/en-GB/en-GB.mod_articles_popular.sys.ini', + '/language/en-GB/en-GB.mod_banners.ini', + '/language/en-GB/en-GB.mod_banners.sys.ini', + '/language/en-GB/en-GB.mod_breadcrumbs.ini', + '/language/en-GB/en-GB.mod_breadcrumbs.sys.ini', + '/language/en-GB/en-GB.mod_custom.ini', + '/language/en-GB/en-GB.mod_custom.sys.ini', + '/language/en-GB/en-GB.mod_feed.ini', + '/language/en-GB/en-GB.mod_feed.sys.ini', + '/language/en-GB/en-GB.mod_finder.ini', + '/language/en-GB/en-GB.mod_finder.sys.ini', + '/language/en-GB/en-GB.mod_footer.ini', + '/language/en-GB/en-GB.mod_footer.sys.ini', + '/language/en-GB/en-GB.mod_languages.ini', + '/language/en-GB/en-GB.mod_languages.sys.ini', + '/language/en-GB/en-GB.mod_login.ini', + '/language/en-GB/en-GB.mod_login.sys.ini', + '/language/en-GB/en-GB.mod_menu.ini', + '/language/en-GB/en-GB.mod_menu.sys.ini', + '/language/en-GB/en-GB.mod_random_image.ini', + '/language/en-GB/en-GB.mod_random_image.sys.ini', + '/language/en-GB/en-GB.mod_related_items.ini', + '/language/en-GB/en-GB.mod_related_items.sys.ini', + '/language/en-GB/en-GB.mod_stats.ini', + '/language/en-GB/en-GB.mod_stats.sys.ini', + '/language/en-GB/en-GB.mod_syndicate.ini', + '/language/en-GB/en-GB.mod_syndicate.sys.ini', + '/language/en-GB/en-GB.mod_tags_popular.ini', + '/language/en-GB/en-GB.mod_tags_popular.sys.ini', + '/language/en-GB/en-GB.mod_tags_similar.ini', + '/language/en-GB/en-GB.mod_tags_similar.sys.ini', + '/language/en-GB/en-GB.mod_users_latest.ini', + '/language/en-GB/en-GB.mod_users_latest.sys.ini', + '/language/en-GB/en-GB.mod_weblinks.ini', + '/language/en-GB/en-GB.mod_weblinks.sys.ini', + '/language/en-GB/en-GB.mod_whosonline.ini', + '/language/en-GB/en-GB.mod_whosonline.sys.ini', + '/language/en-GB/en-GB.mod_wrapper.ini', + '/language/en-GB/en-GB.mod_wrapper.sys.ini', + '/language/en-GB/en-GB.tpl_beez3.ini', + '/language/en-GB/en-GB.tpl_beez3.sys.ini', + '/language/en-GB/en-GB.tpl_protostar.ini', + '/language/en-GB/en-GB.tpl_protostar.sys.ini', + '/language/en-GB/en-GB.xml', + '/layouts/joomla/content/blog_style_default_links.php', + '/layouts/joomla/content/icons/email.php', + '/layouts/joomla/content/icons/print_popup.php', + '/layouts/joomla/content/icons/print_screen.php', + '/layouts/joomla/content/info_block/block.php', + '/layouts/joomla/edit/details.php', + '/layouts/joomla/edit/item_title.php', + '/layouts/joomla/form/field/radio.php', + '/layouts/joomla/html/formbehavior/ajaxchosen.php', + '/layouts/joomla/html/formbehavior/chosen.php', + '/layouts/joomla/html/sortablelist.php', + '/layouts/joomla/html/tag.php', + '/layouts/joomla/modal/body.php', + '/layouts/joomla/modal/footer.php', + '/layouts/joomla/modal/header.php', + '/layouts/joomla/modal/iframe.php', + '/layouts/joomla/modal/main.php', + '/layouts/joomla/sidebars/toggle.php', + '/layouts/joomla/tinymce/buttons.php', + '/layouts/joomla/tinymce/buttons/button.php', + '/layouts/joomla/toolbar/confirm.php', + '/layouts/joomla/toolbar/help.php', + '/layouts/joomla/toolbar/modal.php', + '/layouts/joomla/toolbar/slider.php', + '/layouts/libraries/cms/html/bootstrap/addtab.php', + '/layouts/libraries/cms/html/bootstrap/addtabscript.php', + '/layouts/libraries/cms/html/bootstrap/endtab.php', + '/layouts/libraries/cms/html/bootstrap/endtabset.php', + '/layouts/libraries/cms/html/bootstrap/starttabset.php', + '/layouts/libraries/cms/html/bootstrap/starttabsetscript.php', + '/libraries/cms/class/loader.php', + '/libraries/cms/html/access.php', + '/libraries/cms/html/actionsdropdown.php', + '/libraries/cms/html/adminlanguage.php', + '/libraries/cms/html/batch.php', + '/libraries/cms/html/behavior.php', + '/libraries/cms/html/bootstrap.php', + '/libraries/cms/html/category.php', + '/libraries/cms/html/content.php', + '/libraries/cms/html/contentlanguage.php', + '/libraries/cms/html/date.php', + '/libraries/cms/html/debug.php', + '/libraries/cms/html/dropdown.php', + '/libraries/cms/html/email.php', + '/libraries/cms/html/form.php', + '/libraries/cms/html/formbehavior.php', + '/libraries/cms/html/grid.php', + '/libraries/cms/html/icons.php', + '/libraries/cms/html/jgrid.php', + '/libraries/cms/html/jquery.php', + '/libraries/cms/html/language/en-GB/en-GB.jhtmldate.ini', + '/libraries/cms/html/links.php', + '/libraries/cms/html/list.php', + '/libraries/cms/html/menu.php', + '/libraries/cms/html/number.php', + '/libraries/cms/html/rules.php', + '/libraries/cms/html/searchtools.php', + '/libraries/cms/html/select.php', + '/libraries/cms/html/sidebar.php', + '/libraries/cms/html/sliders.php', + '/libraries/cms/html/sortablelist.php', + '/libraries/cms/html/string.php', + '/libraries/cms/html/tabs.php', + '/libraries/cms/html/tag.php', + '/libraries/cms/html/tel.php', + '/libraries/cms/html/user.php', + '/libraries/cms/less/formatter/joomla.php', + '/libraries/cms/less/less.php', + '/libraries/fof/LICENSE.txt', + '/libraries/fof/autoloader/component.php', + '/libraries/fof/autoloader/fof.php', + '/libraries/fof/config/domain/dispatcher.php', + '/libraries/fof/config/domain/interface.php', + '/libraries/fof/config/domain/tables.php', + '/libraries/fof/config/domain/views.php', + '/libraries/fof/config/provider.php', + '/libraries/fof/controller/controller.php', + '/libraries/fof/database/database.php', + '/libraries/fof/database/driver.php', + '/libraries/fof/database/driver/joomla.php', + '/libraries/fof/database/driver/mysql.php', + '/libraries/fof/database/driver/mysqli.php', + '/libraries/fof/database/driver/oracle.php', + '/libraries/fof/database/driver/pdo.php', + '/libraries/fof/database/driver/pdomysql.php', + '/libraries/fof/database/driver/postgresql.php', + '/libraries/fof/database/driver/sqlazure.php', + '/libraries/fof/database/driver/sqlite.php', + '/libraries/fof/database/driver/sqlsrv.php', + '/libraries/fof/database/factory.php', + '/libraries/fof/database/installer.php', + '/libraries/fof/database/interface.php', + '/libraries/fof/database/iterator.php', + '/libraries/fof/database/iterator/azure.php', + '/libraries/fof/database/iterator/mysql.php', + '/libraries/fof/database/iterator/mysqli.php', + '/libraries/fof/database/iterator/oracle.php', + '/libraries/fof/database/iterator/pdo.php', + '/libraries/fof/database/iterator/pdomysql.php', + '/libraries/fof/database/iterator/postgresql.php', + '/libraries/fof/database/iterator/sqlite.php', + '/libraries/fof/database/iterator/sqlsrv.php', + '/libraries/fof/database/query.php', + '/libraries/fof/database/query/element.php', + '/libraries/fof/database/query/limitable.php', + '/libraries/fof/database/query/mysql.php', + '/libraries/fof/database/query/mysqli.php', + '/libraries/fof/database/query/oracle.php', + '/libraries/fof/database/query/pdo.php', + '/libraries/fof/database/query/pdomysql.php', + '/libraries/fof/database/query/postgresql.php', + '/libraries/fof/database/query/preparable.php', + '/libraries/fof/database/query/sqlazure.php', + '/libraries/fof/database/query/sqlite.php', + '/libraries/fof/database/query/sqlsrv.php', + '/libraries/fof/dispatcher/dispatcher.php', + '/libraries/fof/download/adapter/abstract.php', + '/libraries/fof/download/adapter/cacert.pem', + '/libraries/fof/download/adapter/curl.php', + '/libraries/fof/download/adapter/fopen.php', + '/libraries/fof/download/download.php', + '/libraries/fof/download/interface.php', + '/libraries/fof/encrypt/aes.php', + '/libraries/fof/encrypt/aes/abstract.php', + '/libraries/fof/encrypt/aes/interface.php', + '/libraries/fof/encrypt/aes/mcrypt.php', + '/libraries/fof/encrypt/aes/openssl.php', + '/libraries/fof/encrypt/base32.php', + '/libraries/fof/encrypt/randval.php', + '/libraries/fof/encrypt/randvalinterface.php', + '/libraries/fof/encrypt/totp.php', + '/libraries/fof/form/field.php', + '/libraries/fof/form/field/accesslevel.php', + '/libraries/fof/form/field/actions.php', + '/libraries/fof/form/field/button.php', + '/libraries/fof/form/field/cachehandler.php', + '/libraries/fof/form/field/calendar.php', + '/libraries/fof/form/field/captcha.php', + '/libraries/fof/form/field/checkbox.php', + '/libraries/fof/form/field/checkboxes.php', + '/libraries/fof/form/field/components.php', + '/libraries/fof/form/field/editor.php', + '/libraries/fof/form/field/email.php', + '/libraries/fof/form/field/groupedbutton.php', + '/libraries/fof/form/field/groupedlist.php', + '/libraries/fof/form/field/hidden.php', + '/libraries/fof/form/field/image.php', + '/libraries/fof/form/field/imagelist.php', + '/libraries/fof/form/field/integer.php', + '/libraries/fof/form/field/language.php', + '/libraries/fof/form/field/list.php', + '/libraries/fof/form/field/media.php', + '/libraries/fof/form/field/model.php', + '/libraries/fof/form/field/ordering.php', + '/libraries/fof/form/field/password.php', + '/libraries/fof/form/field/plugins.php', + '/libraries/fof/form/field/published.php', + '/libraries/fof/form/field/radio.php', + '/libraries/fof/form/field/relation.php', + '/libraries/fof/form/field/rules.php', + '/libraries/fof/form/field/selectrow.php', + '/libraries/fof/form/field/sessionhandler.php', + '/libraries/fof/form/field/spacer.php', + '/libraries/fof/form/field/sql.php', + '/libraries/fof/form/field/tag.php', + '/libraries/fof/form/field/tel.php', + '/libraries/fof/form/field/text.php', + '/libraries/fof/form/field/textarea.php', + '/libraries/fof/form/field/timezone.php', + '/libraries/fof/form/field/title.php', + '/libraries/fof/form/field/url.php', + '/libraries/fof/form/field/user.php', + '/libraries/fof/form/field/usergroup.php', + '/libraries/fof/form/form.php', + '/libraries/fof/form/header.php', + '/libraries/fof/form/header/accesslevel.php', + '/libraries/fof/form/header/field.php', + '/libraries/fof/form/header/fielddate.php', + '/libraries/fof/form/header/fieldfilterable.php', + '/libraries/fof/form/header/fieldsearchable.php', + '/libraries/fof/form/header/fieldselectable.php', + '/libraries/fof/form/header/fieldsql.php', + '/libraries/fof/form/header/filterdate.php', + '/libraries/fof/form/header/filterfilterable.php', + '/libraries/fof/form/header/filtersearchable.php', + '/libraries/fof/form/header/filterselectable.php', + '/libraries/fof/form/header/filtersql.php', + '/libraries/fof/form/header/language.php', + '/libraries/fof/form/header/model.php', + '/libraries/fof/form/header/ordering.php', + '/libraries/fof/form/header/published.php', + '/libraries/fof/form/header/rowselect.php', + '/libraries/fof/form/helper.php', + '/libraries/fof/hal/document.php', + '/libraries/fof/hal/link.php', + '/libraries/fof/hal/links.php', + '/libraries/fof/hal/render/interface.php', + '/libraries/fof/hal/render/json.php', + '/libraries/fof/include.php', + '/libraries/fof/inflector/inflector.php', + '/libraries/fof/input/input.php', + '/libraries/fof/input/jinput/cli.php', + '/libraries/fof/input/jinput/cookie.php', + '/libraries/fof/input/jinput/files.php', + '/libraries/fof/input/jinput/input.php', + '/libraries/fof/input/jinput/json.php', + '/libraries/fof/integration/joomla/filesystem/filesystem.php', + '/libraries/fof/integration/joomla/platform.php', + '/libraries/fof/layout/file.php', + '/libraries/fof/layout/helper.php', + '/libraries/fof/less/formatter/classic.php', + '/libraries/fof/less/formatter/compressed.php', + '/libraries/fof/less/formatter/joomla.php', + '/libraries/fof/less/formatter/lessjs.php', + '/libraries/fof/less/less.php', + '/libraries/fof/less/parser/parser.php', + '/libraries/fof/model/behavior.php', + '/libraries/fof/model/behavior/access.php', + '/libraries/fof/model/behavior/emptynonzero.php', + '/libraries/fof/model/behavior/enabled.php', + '/libraries/fof/model/behavior/filters.php', + '/libraries/fof/model/behavior/language.php', + '/libraries/fof/model/behavior/private.php', + '/libraries/fof/model/dispatcher/behavior.php', + '/libraries/fof/model/field.php', + '/libraries/fof/model/field/boolean.php', + '/libraries/fof/model/field/date.php', + '/libraries/fof/model/field/number.php', + '/libraries/fof/model/field/text.php', + '/libraries/fof/model/model.php', + '/libraries/fof/platform/filesystem/filesystem.php', + '/libraries/fof/platform/filesystem/interface.php', + '/libraries/fof/platform/interface.php', + '/libraries/fof/platform/platform.php', + '/libraries/fof/query/abstract.php', + '/libraries/fof/render/abstract.php', + '/libraries/fof/render/joomla.php', + '/libraries/fof/render/joomla3.php', + '/libraries/fof/render/strapper.php', + '/libraries/fof/string/utils.php', + '/libraries/fof/table/behavior.php', + '/libraries/fof/table/behavior/assets.php', + '/libraries/fof/table/behavior/contenthistory.php', + '/libraries/fof/table/behavior/tags.php', + '/libraries/fof/table/dispatcher/behavior.php', + '/libraries/fof/table/nested.php', + '/libraries/fof/table/relations.php', + '/libraries/fof/table/table.php', + '/libraries/fof/template/utils.php', + '/libraries/fof/toolbar/toolbar.php', + '/libraries/fof/utils/array/array.php', + '/libraries/fof/utils/cache/cleaner.php', + '/libraries/fof/utils/config/helper.php', + '/libraries/fof/utils/filescheck/filescheck.php', + '/libraries/fof/utils/ini/parser.php', + '/libraries/fof/utils/installscript/installscript.php', + '/libraries/fof/utils/ip/ip.php', + '/libraries/fof/utils/object/object.php', + '/libraries/fof/utils/observable/dispatcher.php', + '/libraries/fof/utils/observable/event.php', + '/libraries/fof/utils/phpfunc/phpfunc.php', + '/libraries/fof/utils/timer/timer.php', + '/libraries/fof/utils/update/collection.php', + '/libraries/fof/utils/update/extension.php', + '/libraries/fof/utils/update/joomla.php', + '/libraries/fof/utils/update/update.php', + '/libraries/fof/version.txt', + '/libraries/fof/view/csv.php', + '/libraries/fof/view/form.php', + '/libraries/fof/view/html.php', + '/libraries/fof/view/json.php', + '/libraries/fof/view/raw.php', + '/libraries/fof/view/view.php', + '/libraries/idna_convert/LICENCE', + '/libraries/idna_convert/ReadMe.txt', + '/libraries/idna_convert/idna_convert.class.php', + '/libraries/idna_convert/transcode_wrapper.php', + '/libraries/idna_convert/uctc.php', + '/libraries/joomla/application/web/router.php', + '/libraries/joomla/application/web/router/base.php', + '/libraries/joomla/application/web/router/rest.php', + '/libraries/joomla/archive/archive.php', + '/libraries/joomla/archive/bzip2.php', + '/libraries/joomla/archive/extractable.php', + '/libraries/joomla/archive/gzip.php', + '/libraries/joomla/archive/tar.php', + '/libraries/joomla/archive/wrapper/archive.php', + '/libraries/joomla/archive/zip.php', + '/libraries/joomla/controller/base.php', + '/libraries/joomla/controller/controller.php', + '/libraries/joomla/database/database.php', + '/libraries/joomla/database/driver.php', + '/libraries/joomla/database/driver/mysql.php', + '/libraries/joomla/database/driver/mysqli.php', + '/libraries/joomla/database/driver/oracle.php', + '/libraries/joomla/database/driver/pdo.php', + '/libraries/joomla/database/driver/pdomysql.php', + '/libraries/joomla/database/driver/pgsql.php', + '/libraries/joomla/database/driver/postgresql.php', + '/libraries/joomla/database/driver/sqlazure.php', + '/libraries/joomla/database/driver/sqlite.php', + '/libraries/joomla/database/driver/sqlsrv.php', + '/libraries/joomla/database/exception/connecting.php', + '/libraries/joomla/database/exception/executing.php', + '/libraries/joomla/database/exception/unsupported.php', + '/libraries/joomla/database/exporter.php', + '/libraries/joomla/database/exporter/mysql.php', + '/libraries/joomla/database/exporter/mysqli.php', + '/libraries/joomla/database/exporter/pdomysql.php', + '/libraries/joomla/database/exporter/pgsql.php', + '/libraries/joomla/database/exporter/postgresql.php', + '/libraries/joomla/database/factory.php', + '/libraries/joomla/database/importer.php', + '/libraries/joomla/database/importer/mysql.php', + '/libraries/joomla/database/importer/mysqli.php', + '/libraries/joomla/database/importer/pdomysql.php', + '/libraries/joomla/database/importer/pgsql.php', + '/libraries/joomla/database/importer/postgresql.php', + '/libraries/joomla/database/interface.php', + '/libraries/joomla/database/iterator.php', + '/libraries/joomla/database/iterator/mysql.php', + '/libraries/joomla/database/iterator/mysqli.php', + '/libraries/joomla/database/iterator/oracle.php', + '/libraries/joomla/database/iterator/pdo.php', + '/libraries/joomla/database/iterator/pdomysql.php', + '/libraries/joomla/database/iterator/pgsql.php', + '/libraries/joomla/database/iterator/postgresql.php', + '/libraries/joomla/database/iterator/sqlazure.php', + '/libraries/joomla/database/iterator/sqlite.php', + '/libraries/joomla/database/iterator/sqlsrv.php', + '/libraries/joomla/database/query.php', + '/libraries/joomla/database/query/element.php', + '/libraries/joomla/database/query/limitable.php', + '/libraries/joomla/database/query/mysql.php', + '/libraries/joomla/database/query/mysqli.php', + '/libraries/joomla/database/query/oracle.php', + '/libraries/joomla/database/query/pdo.php', + '/libraries/joomla/database/query/pdomysql.php', + '/libraries/joomla/database/query/pgsql.php', + '/libraries/joomla/database/query/postgresql.php', + '/libraries/joomla/database/query/preparable.php', + '/libraries/joomla/database/query/sqlazure.php', + '/libraries/joomla/database/query/sqlite.php', + '/libraries/joomla/database/query/sqlsrv.php', + '/libraries/joomla/event/dispatcher.php', + '/libraries/joomla/event/event.php', + '/libraries/joomla/facebook/album.php', + '/libraries/joomla/facebook/checkin.php', + '/libraries/joomla/facebook/comment.php', + '/libraries/joomla/facebook/event.php', + '/libraries/joomla/facebook/facebook.php', + '/libraries/joomla/facebook/group.php', + '/libraries/joomla/facebook/link.php', + '/libraries/joomla/facebook/note.php', + '/libraries/joomla/facebook/oauth.php', + '/libraries/joomla/facebook/object.php', + '/libraries/joomla/facebook/photo.php', + '/libraries/joomla/facebook/post.php', + '/libraries/joomla/facebook/status.php', + '/libraries/joomla/facebook/user.php', + '/libraries/joomla/facebook/video.php', + '/libraries/joomla/form/fields/accesslevel.php', + '/libraries/joomla/form/fields/aliastag.php', + '/libraries/joomla/form/fields/cachehandler.php', + '/libraries/joomla/form/fields/calendar.php', + '/libraries/joomla/form/fields/checkbox.php', + '/libraries/joomla/form/fields/checkboxes.php', + '/libraries/joomla/form/fields/color.php', + '/libraries/joomla/form/fields/combo.php', + '/libraries/joomla/form/fields/components.php', + '/libraries/joomla/form/fields/databaseconnection.php', + '/libraries/joomla/form/fields/email.php', + '/libraries/joomla/form/fields/file.php', + '/libraries/joomla/form/fields/filelist.php', + '/libraries/joomla/form/fields/folderlist.php', + '/libraries/joomla/form/fields/groupedlist.php', + '/libraries/joomla/form/fields/hidden.php', + '/libraries/joomla/form/fields/imagelist.php', + '/libraries/joomla/form/fields/integer.php', + '/libraries/joomla/form/fields/language.php', + '/libraries/joomla/form/fields/list.php', + '/libraries/joomla/form/fields/meter.php', + '/libraries/joomla/form/fields/note.php', + '/libraries/joomla/form/fields/number.php', + '/libraries/joomla/form/fields/password.php', + '/libraries/joomla/form/fields/plugins.php', + '/libraries/joomla/form/fields/predefinedlist.php', + '/libraries/joomla/form/fields/radio.php', + '/libraries/joomla/form/fields/range.php', + '/libraries/joomla/form/fields/repeatable.php', + '/libraries/joomla/form/fields/rules.php', + '/libraries/joomla/form/fields/sessionhandler.php', + '/libraries/joomla/form/fields/spacer.php', + '/libraries/joomla/form/fields/sql.php', + '/libraries/joomla/form/fields/subform.php', + '/libraries/joomla/form/fields/tel.php', + '/libraries/joomla/form/fields/text.php', + '/libraries/joomla/form/fields/textarea.php', + '/libraries/joomla/form/fields/timezone.php', + '/libraries/joomla/form/fields/url.php', + '/libraries/joomla/form/fields/usergroup.php', + '/libraries/joomla/github/account.php', + '/libraries/joomla/github/commits.php', + '/libraries/joomla/github/forks.php', + '/libraries/joomla/github/github.php', + '/libraries/joomla/github/hooks.php', + '/libraries/joomla/github/http.php', + '/libraries/joomla/github/meta.php', + '/libraries/joomla/github/milestones.php', + '/libraries/joomla/github/object.php', + '/libraries/joomla/github/package.php', + '/libraries/joomla/github/package/activity.php', + '/libraries/joomla/github/package/activity/events.php', + '/libraries/joomla/github/package/activity/notifications.php', + '/libraries/joomla/github/package/activity/starring.php', + '/libraries/joomla/github/package/activity/watching.php', + '/libraries/joomla/github/package/authorization.php', + '/libraries/joomla/github/package/data.php', + '/libraries/joomla/github/package/data/blobs.php', + '/libraries/joomla/github/package/data/commits.php', + '/libraries/joomla/github/package/data/refs.php', + '/libraries/joomla/github/package/data/tags.php', + '/libraries/joomla/github/package/data/trees.php', + '/libraries/joomla/github/package/gists.php', + '/libraries/joomla/github/package/gists/comments.php', + '/libraries/joomla/github/package/gitignore.php', + '/libraries/joomla/github/package/issues.php', + '/libraries/joomla/github/package/issues/assignees.php', + '/libraries/joomla/github/package/issues/comments.php', + '/libraries/joomla/github/package/issues/events.php', + '/libraries/joomla/github/package/issues/labels.php', + '/libraries/joomla/github/package/issues/milestones.php', + '/libraries/joomla/github/package/markdown.php', + '/libraries/joomla/github/package/orgs.php', + '/libraries/joomla/github/package/orgs/members.php', + '/libraries/joomla/github/package/orgs/teams.php', + '/libraries/joomla/github/package/pulls.php', + '/libraries/joomla/github/package/pulls/comments.php', + '/libraries/joomla/github/package/repositories.php', + '/libraries/joomla/github/package/repositories/collaborators.php', + '/libraries/joomla/github/package/repositories/comments.php', + '/libraries/joomla/github/package/repositories/commits.php', + '/libraries/joomla/github/package/repositories/contents.php', + '/libraries/joomla/github/package/repositories/downloads.php', + '/libraries/joomla/github/package/repositories/forks.php', + '/libraries/joomla/github/package/repositories/hooks.php', + '/libraries/joomla/github/package/repositories/keys.php', + '/libraries/joomla/github/package/repositories/merging.php', + '/libraries/joomla/github/package/repositories/statistics.php', + '/libraries/joomla/github/package/repositories/statuses.php', + '/libraries/joomla/github/package/search.php', + '/libraries/joomla/github/package/users.php', + '/libraries/joomla/github/package/users/emails.php', + '/libraries/joomla/github/package/users/followers.php', + '/libraries/joomla/github/package/users/keys.php', + '/libraries/joomla/github/refs.php', + '/libraries/joomla/github/statuses.php', + '/libraries/joomla/google/auth.php', + '/libraries/joomla/google/auth/oauth2.php', + '/libraries/joomla/google/data.php', + '/libraries/joomla/google/data/adsense.php', + '/libraries/joomla/google/data/calendar.php', + '/libraries/joomla/google/data/picasa.php', + '/libraries/joomla/google/data/picasa/album.php', + '/libraries/joomla/google/data/picasa/photo.php', + '/libraries/joomla/google/data/plus.php', + '/libraries/joomla/google/data/plus/activities.php', + '/libraries/joomla/google/data/plus/comments.php', + '/libraries/joomla/google/data/plus/people.php', + '/libraries/joomla/google/embed.php', + '/libraries/joomla/google/embed/analytics.php', + '/libraries/joomla/google/embed/maps.php', + '/libraries/joomla/google/google.php', + '/libraries/joomla/grid/grid.php', + '/libraries/joomla/keychain/keychain.php', + '/libraries/joomla/linkedin/communications.php', + '/libraries/joomla/linkedin/companies.php', + '/libraries/joomla/linkedin/groups.php', + '/libraries/joomla/linkedin/jobs.php', + '/libraries/joomla/linkedin/linkedin.php', + '/libraries/joomla/linkedin/oauth.php', + '/libraries/joomla/linkedin/object.php', + '/libraries/joomla/linkedin/people.php', + '/libraries/joomla/linkedin/stream.php', + '/libraries/joomla/mediawiki/categories.php', + '/libraries/joomla/mediawiki/http.php', + '/libraries/joomla/mediawiki/images.php', + '/libraries/joomla/mediawiki/links.php', + '/libraries/joomla/mediawiki/mediawiki.php', + '/libraries/joomla/mediawiki/object.php', + '/libraries/joomla/mediawiki/pages.php', + '/libraries/joomla/mediawiki/search.php', + '/libraries/joomla/mediawiki/sites.php', + '/libraries/joomla/mediawiki/users.php', + '/libraries/joomla/model/base.php', + '/libraries/joomla/model/database.php', + '/libraries/joomla/model/model.php', + '/libraries/joomla/oauth1/client.php', + '/libraries/joomla/oauth2/client.php', + '/libraries/joomla/observable/interface.php', + '/libraries/joomla/observer/interface.php', + '/libraries/joomla/observer/mapper.php', + '/libraries/joomla/observer/updater.php', + '/libraries/joomla/observer/updater/interface.php', + '/libraries/joomla/observer/wrapper/mapper.php', + '/libraries/joomla/openstreetmap/changesets.php', + '/libraries/joomla/openstreetmap/elements.php', + '/libraries/joomla/openstreetmap/gps.php', + '/libraries/joomla/openstreetmap/info.php', + '/libraries/joomla/openstreetmap/oauth.php', + '/libraries/joomla/openstreetmap/object.php', + '/libraries/joomla/openstreetmap/openstreetmap.php', + '/libraries/joomla/openstreetmap/user.php', + '/libraries/joomla/platform.php', + '/libraries/joomla/route/wrapper/route.php', + '/libraries/joomla/session/handler/interface.php', + '/libraries/joomla/session/handler/joomla.php', + '/libraries/joomla/session/handler/native.php', + '/libraries/joomla/session/storage.php', + '/libraries/joomla/session/storage/apc.php', + '/libraries/joomla/session/storage/apcu.php', + '/libraries/joomla/session/storage/database.php', + '/libraries/joomla/session/storage/memcache.php', + '/libraries/joomla/session/storage/memcached.php', + '/libraries/joomla/session/storage/none.php', + '/libraries/joomla/session/storage/redis.php', + '/libraries/joomla/session/storage/wincache.php', + '/libraries/joomla/session/storage/xcache.php', + '/libraries/joomla/string/string.php', + '/libraries/joomla/string/wrapper/normalise.php', + '/libraries/joomla/string/wrapper/punycode.php', + '/libraries/joomla/twitter/block.php', + '/libraries/joomla/twitter/directmessages.php', + '/libraries/joomla/twitter/favorites.php', + '/libraries/joomla/twitter/friends.php', + '/libraries/joomla/twitter/help.php', + '/libraries/joomla/twitter/lists.php', + '/libraries/joomla/twitter/oauth.php', + '/libraries/joomla/twitter/object.php', + '/libraries/joomla/twitter/places.php', + '/libraries/joomla/twitter/profile.php', + '/libraries/joomla/twitter/search.php', + '/libraries/joomla/twitter/statuses.php', + '/libraries/joomla/twitter/trends.php', + '/libraries/joomla/twitter/twitter.php', + '/libraries/joomla/twitter/users.php', + '/libraries/joomla/utilities/arrayhelper.php', + '/libraries/joomla/view/base.php', + '/libraries/joomla/view/html.php', + '/libraries/joomla/view/view.php', + '/libraries/legacy/application/application.php', + '/libraries/legacy/base/node.php', + '/libraries/legacy/base/observable.php', + '/libraries/legacy/base/observer.php', + '/libraries/legacy/base/tree.php', + '/libraries/legacy/database/exception.php', + '/libraries/legacy/database/mysql.php', + '/libraries/legacy/database/mysqli.php', + '/libraries/legacy/database/sqlazure.php', + '/libraries/legacy/database/sqlsrv.php', + '/libraries/legacy/dispatcher/dispatcher.php', + '/libraries/legacy/error/error.php', + '/libraries/legacy/exception/exception.php', + '/libraries/legacy/form/field/category.php', + '/libraries/legacy/form/field/componentlayout.php', + '/libraries/legacy/form/field/modulelayout.php', + '/libraries/legacy/log/logexception.php', + '/libraries/legacy/request/request.php', + '/libraries/legacy/response/response.php', + '/libraries/legacy/simplecrypt/simplecrypt.php', + '/libraries/legacy/simplepie/factory.php', + '/libraries/legacy/table/session.php', + '/libraries/legacy/utilities/xmlelement.php', + '/libraries/phputf8/LICENSE', + '/libraries/phputf8/README', + '/libraries/phputf8/mbstring/core.php', + '/libraries/phputf8/native/core.php', + '/libraries/phputf8/ord.php', + '/libraries/phputf8/str_ireplace.php', + '/libraries/phputf8/str_pad.php', + '/libraries/phputf8/str_split.php', + '/libraries/phputf8/strcasecmp.php', + '/libraries/phputf8/strcspn.php', + '/libraries/phputf8/stristr.php', + '/libraries/phputf8/strrev.php', + '/libraries/phputf8/strspn.php', + '/libraries/phputf8/substr_replace.php', + '/libraries/phputf8/trim.php', + '/libraries/phputf8/ucfirst.php', + '/libraries/phputf8/ucwords.php', + '/libraries/phputf8/utf8.php', + '/libraries/phputf8/utils/ascii.php', + '/libraries/phputf8/utils/bad.php', + '/libraries/phputf8/utils/patterns.php', + '/libraries/phputf8/utils/position.php', + '/libraries/phputf8/utils/specials.php', + '/libraries/phputf8/utils/unicode.php', + '/libraries/phputf8/utils/validation.php', + '/libraries/src/Access/Wrapper/Access.php', + '/libraries/src/Cache/Storage/ApcStorage.php', + '/libraries/src/Cache/Storage/CacheliteStorage.php', + '/libraries/src/Cache/Storage/MemcacheStorage.php', + '/libraries/src/Cache/Storage/XcacheStorage.php', + '/libraries/src/Client/ClientWrapper.php', + '/libraries/src/Crypt/Cipher/BlowfishCipher.php', + '/libraries/src/Crypt/Cipher/McryptCipher.php', + '/libraries/src/Crypt/Cipher/Rijndael256Cipher.php', + '/libraries/src/Crypt/Cipher/SimpleCipher.php', + '/libraries/src/Crypt/Cipher/TripleDesCipher.php', + '/libraries/src/Crypt/CipherInterface.php', + '/libraries/src/Crypt/CryptPassword.php', + '/libraries/src/Crypt/Key.php', + '/libraries/src/Crypt/Password/SimpleCryptPassword.php', + '/libraries/src/Crypt/README.md', + '/libraries/src/Filesystem/Wrapper/FileWrapper.php', + '/libraries/src/Filesystem/Wrapper/FolderWrapper.php', + '/libraries/src/Filesystem/Wrapper/PathWrapper.php', + '/libraries/src/Filter/Wrapper/OutputFilterWrapper.php', + '/libraries/src/Form/Field/HelpsiteField.php', + '/libraries/src/Form/FormWrapper.php', + '/libraries/src/Helper/ContentHistoryHelper.php', + '/libraries/src/Helper/SearchHelper.php', + '/libraries/src/Http/Transport/cacert.pem', + '/libraries/src/Http/Wrapper/FactoryWrapper.php', + '/libraries/src/Language/LanguageStemmer.php', + '/libraries/src/Language/Stemmer/Porteren.php', + '/libraries/src/Language/Wrapper/JTextWrapper.php', + '/libraries/src/Language/Wrapper/LanguageHelperWrapper.php', + '/libraries/src/Language/Wrapper/TransliterateWrapper.php', + '/libraries/src/Mail/MailWrapper.php', + '/libraries/src/Menu/MenuHelper.php', + '/libraries/src/Menu/Node.php', + '/libraries/src/Menu/Node/Component.php', + '/libraries/src/Menu/Node/Container.php', + '/libraries/src/Menu/Node/Heading.php', + '/libraries/src/Menu/Node/Separator.php', + '/libraries/src/Menu/Node/Url.php', + '/libraries/src/Menu/Tree.php', + '/libraries/src/Table/Observer/AbstractObserver.php', + '/libraries/src/Table/Observer/ContentHistory.php', + '/libraries/src/Table/Observer/Tags.php', + '/libraries/src/Toolbar/Button/SliderButton.php', + '/libraries/src/User/UserWrapper.php', + '/libraries/vendor/.htaccess', + '/libraries/vendor/brumann/polyfill-unserialize/LICENSE', + '/libraries/vendor/brumann/polyfill-unserialize/composer.json', + '/libraries/vendor/brumann/polyfill-unserialize/src/DisallowedClassesSubstitutor.php', + '/libraries/vendor/brumann/polyfill-unserialize/src/Unserialize.php', + '/libraries/vendor/ircmaxell/password-compat/LICENSE.md', + '/libraries/vendor/ircmaxell/password-compat/lib/password.php', + '/libraries/vendor/joomla/application/src/AbstractCliApplication.php', + '/libraries/vendor/joomla/application/src/AbstractDaemonApplication.php', + '/libraries/vendor/joomla/application/src/Cli/CliInput.php', + '/libraries/vendor/joomla/application/src/Cli/CliOutput.php', + '/libraries/vendor/joomla/application/src/Cli/ColorProcessor.php', + '/libraries/vendor/joomla/application/src/Cli/ColorStyle.php', + '/libraries/vendor/joomla/application/src/Cli/Output/Processor/ColorProcessor.php', + '/libraries/vendor/joomla/application/src/Cli/Output/Processor/ProcessorInterface.php', + '/libraries/vendor/joomla/application/src/Cli/Output/Stdout.php', + '/libraries/vendor/joomla/application/src/Cli/Output/Xml.php', + '/libraries/vendor/joomla/compat/LICENSE', + '/libraries/vendor/joomla/compat/src/CallbackFilterIterator.php', + '/libraries/vendor/joomla/compat/src/JsonSerializable.php', + '/libraries/vendor/joomla/event/src/DelegatingDispatcher.php', + '/libraries/vendor/joomla/filesystem/src/Stream/String.php', + '/libraries/vendor/joomla/image/LICENSE', + '/libraries/vendor/joomla/image/src/Filter/Backgroundfill.php', + '/libraries/vendor/joomla/image/src/Filter/Brightness.php', + '/libraries/vendor/joomla/image/src/Filter/Contrast.php', + '/libraries/vendor/joomla/image/src/Filter/Edgedetect.php', + '/libraries/vendor/joomla/image/src/Filter/Emboss.php', + '/libraries/vendor/joomla/image/src/Filter/Grayscale.php', + '/libraries/vendor/joomla/image/src/Filter/Negate.php', + '/libraries/vendor/joomla/image/src/Filter/Sketchy.php', + '/libraries/vendor/joomla/image/src/Filter/Smooth.php', + '/libraries/vendor/joomla/image/src/Image.php', + '/libraries/vendor/joomla/image/src/ImageFilter.php', + '/libraries/vendor/joomla/input/src/Cli.php', + '/libraries/vendor/joomla/registry/src/AbstractRegistryFormat.php', + '/libraries/vendor/joomla/session/Joomla/Session/LICENSE', + '/libraries/vendor/joomla/session/Joomla/Session/Session.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Apc.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Apcu.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Database.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Memcache.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Memcached.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/None.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Wincache.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Xcache.php', + '/libraries/vendor/joomla/string/src/String.php', + '/libraries/vendor/leafo/lessphp/LICENSE', + '/libraries/vendor/leafo/lessphp/lessc.inc.php', + '/libraries/vendor/leafo/lessphp/lessify', + '/libraries/vendor/leafo/lessphp/lessify.inc.php', + '/libraries/vendor/leafo/lessphp/plessc', + '/libraries/vendor/paragonie/random_compat/LICENSE', + '/libraries/vendor/paragonie/random_compat/lib/byte_safe_strings.php', + '/libraries/vendor/paragonie/random_compat/lib/cast_to_int.php', + '/libraries/vendor/paragonie/random_compat/lib/error_polyfill.php', + '/libraries/vendor/paragonie/random_compat/lib/random.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_com_dotnet.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_dev_urandom.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_libsodium.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_libsodium_legacy.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_mcrypt.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_openssl.php', + '/libraries/vendor/paragonie/random_compat/lib/random_int.php', + '/libraries/vendor/paragonie/sodium_compat/src/Core32/Curve25519/README.md', + '/libraries/vendor/phpmailer/phpmailer/PHPMailerAutoload.php', + '/libraries/vendor/phpmailer/phpmailer/class.phpmailer.php', + '/libraries/vendor/phpmailer/phpmailer/class.phpmaileroauth.php', + '/libraries/vendor/phpmailer/phpmailer/class.phpmaileroauthgoogle.php', + '/libraries/vendor/phpmailer/phpmailer/class.pop3.php', + '/libraries/vendor/phpmailer/phpmailer/class.smtp.php', + '/libraries/vendor/phpmailer/phpmailer/extras/EasyPeasyICS.php', + '/libraries/vendor/phpmailer/phpmailer/extras/htmlfilter.php', + '/libraries/vendor/phpmailer/phpmailer/extras/ntlm_sasl_client.php', + '/libraries/vendor/simplepie/simplepie/LICENSE.txt', + '/libraries/vendor/simplepie/simplepie/autoloader.php', + '/libraries/vendor/simplepie/simplepie/db.sql', + '/libraries/vendor/simplepie/simplepie/idn/LICENCE', + '/libraries/vendor/simplepie/simplepie/idn/idna_convert.class.php', + '/libraries/vendor/simplepie/simplepie/idn/npdata.ser', + '/libraries/vendor/simplepie/simplepie/library/SimplePie.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Author.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/Base.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/DB.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/File.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/Memcache.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/MySQL.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Caption.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Category.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content/Type/Sniffer.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Copyright.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Core.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Credit.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode/HTML/Entities.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Enclosure.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Exception.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/File.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/HTTP/Parser.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/IRI.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Item.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Locator.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Misc.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Net/IPv6.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parse/Date.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parser.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Rating.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Registry.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Restriction.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Sanitize.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Source.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML/Declaration/Parser.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/gzdecode.php', + '/libraries/vendor/symfony/polyfill-php55/LICENSE', + '/libraries/vendor/symfony/polyfill-php55/Php55.php', + '/libraries/vendor/symfony/polyfill-php55/Php55ArrayColumn.php', + '/libraries/vendor/symfony/polyfill-php55/bootstrap.php', + '/libraries/vendor/symfony/polyfill-php56/LICENSE', + '/libraries/vendor/symfony/polyfill-php56/Php56.php', + '/libraries/vendor/symfony/polyfill-php56/bootstrap.php', + '/libraries/vendor/symfony/polyfill-php71/LICENSE', + '/libraries/vendor/symfony/polyfill-php71/Php71.php', + '/libraries/vendor/symfony/polyfill-php71/bootstrap.php', + '/libraries/vendor/symfony/polyfill-util/Binary.php', + '/libraries/vendor/symfony/polyfill-util/BinaryNoFuncOverload.php', + '/libraries/vendor/symfony/polyfill-util/BinaryOnFuncOverload.php', + '/libraries/vendor/symfony/polyfill-util/LICENSE', + '/libraries/vendor/typo3/phar-stream-wrapper/composer.json', + '/libraries/vendor/web.config', + '/media/cms/css/debug.css', + '/media/com_associations/js/sidebyside-uncompressed.js', + '/media/com_contenthistory/css/jquery.pretty-text-diff.css', + '/media/com_contenthistory/js/diff_match_patch.js', + '/media/com_contenthistory/js/jquery.pretty-text-diff.js', + '/media/com_contenthistory/js/jquery.pretty-text-diff.min.js', + '/media/com_finder/js/autocompleter.js', + '/media/com_joomlaupdate/js/encryption.js', + '/media/com_joomlaupdate/js/encryption.min.js', + '/media/com_joomlaupdate/js/json2.js', + '/media/com_joomlaupdate/js/json2.min.js', + '/media/com_joomlaupdate/js/update.js', + '/media/com_joomlaupdate/js/update.min.js', + '/media/contacts/images/con_address.png', + '/media/contacts/images/con_fax.png', + '/media/contacts/images/con_info.png', + '/media/contacts/images/con_mobile.png', + '/media/contacts/images/con_tel.png', + '/media/contacts/images/emailButton.png', + '/media/editors/codemirror/LICENSE', + '/media/editors/codemirror/addon/comment/comment.js', + '/media/editors/codemirror/addon/comment/comment.min.js', + '/media/editors/codemirror/addon/comment/continuecomment.js', + '/media/editors/codemirror/addon/comment/continuecomment.min.js', + '/media/editors/codemirror/addon/dialog/dialog.css', + '/media/editors/codemirror/addon/dialog/dialog.js', + '/media/editors/codemirror/addon/dialog/dialog.min.css', + '/media/editors/codemirror/addon/dialog/dialog.min.js', + '/media/editors/codemirror/addon/display/autorefresh.js', + '/media/editors/codemirror/addon/display/autorefresh.min.js', + '/media/editors/codemirror/addon/display/fullscreen.css', + '/media/editors/codemirror/addon/display/fullscreen.js', + '/media/editors/codemirror/addon/display/fullscreen.min.css', + '/media/editors/codemirror/addon/display/fullscreen.min.js', + '/media/editors/codemirror/addon/display/panel.js', + '/media/editors/codemirror/addon/display/panel.min.js', + '/media/editors/codemirror/addon/display/placeholder.js', + '/media/editors/codemirror/addon/display/placeholder.min.js', + '/media/editors/codemirror/addon/display/rulers.js', + '/media/editors/codemirror/addon/display/rulers.min.js', + '/media/editors/codemirror/addon/edit/closebrackets.js', + '/media/editors/codemirror/addon/edit/closebrackets.min.js', + '/media/editors/codemirror/addon/edit/closetag.js', + '/media/editors/codemirror/addon/edit/closetag.min.js', + '/media/editors/codemirror/addon/edit/continuelist.js', + '/media/editors/codemirror/addon/edit/continuelist.min.js', + '/media/editors/codemirror/addon/edit/matchbrackets.js', + '/media/editors/codemirror/addon/edit/matchbrackets.min.js', + '/media/editors/codemirror/addon/edit/matchtags.js', + '/media/editors/codemirror/addon/edit/matchtags.min.js', + '/media/editors/codemirror/addon/edit/trailingspace.js', + '/media/editors/codemirror/addon/edit/trailingspace.min.js', + '/media/editors/codemirror/addon/fold/brace-fold.js', + '/media/editors/codemirror/addon/fold/brace-fold.min.js', + '/media/editors/codemirror/addon/fold/comment-fold.js', + '/media/editors/codemirror/addon/fold/comment-fold.min.js', + '/media/editors/codemirror/addon/fold/foldcode.js', + '/media/editors/codemirror/addon/fold/foldcode.min.js', + '/media/editors/codemirror/addon/fold/foldgutter.css', + '/media/editors/codemirror/addon/fold/foldgutter.js', + '/media/editors/codemirror/addon/fold/foldgutter.min.css', + '/media/editors/codemirror/addon/fold/foldgutter.min.js', + '/media/editors/codemirror/addon/fold/indent-fold.js', + '/media/editors/codemirror/addon/fold/indent-fold.min.js', + '/media/editors/codemirror/addon/fold/markdown-fold.js', + '/media/editors/codemirror/addon/fold/markdown-fold.min.js', + '/media/editors/codemirror/addon/fold/xml-fold.js', + '/media/editors/codemirror/addon/fold/xml-fold.min.js', + '/media/editors/codemirror/addon/hint/anyword-hint.js', + '/media/editors/codemirror/addon/hint/anyword-hint.min.js', + '/media/editors/codemirror/addon/hint/css-hint.js', + '/media/editors/codemirror/addon/hint/css-hint.min.js', + '/media/editors/codemirror/addon/hint/html-hint.js', + '/media/editors/codemirror/addon/hint/html-hint.min.js', + '/media/editors/codemirror/addon/hint/javascript-hint.js', + '/media/editors/codemirror/addon/hint/javascript-hint.min.js', + '/media/editors/codemirror/addon/hint/show-hint.css', + '/media/editors/codemirror/addon/hint/show-hint.js', + '/media/editors/codemirror/addon/hint/show-hint.min.css', + '/media/editors/codemirror/addon/hint/show-hint.min.js', + '/media/editors/codemirror/addon/hint/sql-hint.js', + '/media/editors/codemirror/addon/hint/sql-hint.min.js', + '/media/editors/codemirror/addon/hint/xml-hint.js', + '/media/editors/codemirror/addon/hint/xml-hint.min.js', + '/media/editors/codemirror/addon/lint/coffeescript-lint.js', + '/media/editors/codemirror/addon/lint/coffeescript-lint.min.js', + '/media/editors/codemirror/addon/lint/css-lint.js', + '/media/editors/codemirror/addon/lint/css-lint.min.js', + '/media/editors/codemirror/addon/lint/html-lint.js', + '/media/editors/codemirror/addon/lint/html-lint.min.js', + '/media/editors/codemirror/addon/lint/javascript-lint.js', + '/media/editors/codemirror/addon/lint/javascript-lint.min.js', + '/media/editors/codemirror/addon/lint/json-lint.js', + '/media/editors/codemirror/addon/lint/json-lint.min.js', + '/media/editors/codemirror/addon/lint/lint.css', + '/media/editors/codemirror/addon/lint/lint.js', + '/media/editors/codemirror/addon/lint/lint.min.css', + '/media/editors/codemirror/addon/lint/lint.min.js', + '/media/editors/codemirror/addon/lint/yaml-lint.js', + '/media/editors/codemirror/addon/lint/yaml-lint.min.js', + '/media/editors/codemirror/addon/merge/merge.css', + '/media/editors/codemirror/addon/merge/merge.js', + '/media/editors/codemirror/addon/merge/merge.min.css', + '/media/editors/codemirror/addon/merge/merge.min.js', + '/media/editors/codemirror/addon/mode/loadmode.js', + '/media/editors/codemirror/addon/mode/loadmode.min.js', + '/media/editors/codemirror/addon/mode/multiplex.js', + '/media/editors/codemirror/addon/mode/multiplex.min.js', + '/media/editors/codemirror/addon/mode/multiplex_test.js', + '/media/editors/codemirror/addon/mode/multiplex_test.min.js', + '/media/editors/codemirror/addon/mode/overlay.js', + '/media/editors/codemirror/addon/mode/overlay.min.js', + '/media/editors/codemirror/addon/mode/simple.js', + '/media/editors/codemirror/addon/mode/simple.min.js', + '/media/editors/codemirror/addon/runmode/colorize.js', + '/media/editors/codemirror/addon/runmode/colorize.min.js', + '/media/editors/codemirror/addon/runmode/runmode-standalone.js', + '/media/editors/codemirror/addon/runmode/runmode-standalone.min.js', + '/media/editors/codemirror/addon/runmode/runmode.js', + '/media/editors/codemirror/addon/runmode/runmode.min.js', + '/media/editors/codemirror/addon/runmode/runmode.node.js', + '/media/editors/codemirror/addon/scroll/annotatescrollbar.js', + '/media/editors/codemirror/addon/scroll/annotatescrollbar.min.js', + '/media/editors/codemirror/addon/scroll/scrollpastend.js', + '/media/editors/codemirror/addon/scroll/scrollpastend.min.js', + '/media/editors/codemirror/addon/scroll/simplescrollbars.css', + '/media/editors/codemirror/addon/scroll/simplescrollbars.js', + '/media/editors/codemirror/addon/scroll/simplescrollbars.min.css', + '/media/editors/codemirror/addon/scroll/simplescrollbars.min.js', + '/media/editors/codemirror/addon/search/jump-to-line.js', + '/media/editors/codemirror/addon/search/jump-to-line.min.js', + '/media/editors/codemirror/addon/search/match-highlighter.js', + '/media/editors/codemirror/addon/search/match-highlighter.min.js', + '/media/editors/codemirror/addon/search/matchesonscrollbar.css', + '/media/editors/codemirror/addon/search/matchesonscrollbar.js', + '/media/editors/codemirror/addon/search/matchesonscrollbar.min.css', + '/media/editors/codemirror/addon/search/matchesonscrollbar.min.js', + '/media/editors/codemirror/addon/search/search.js', + '/media/editors/codemirror/addon/search/search.min.js', + '/media/editors/codemirror/addon/search/searchcursor.js', + '/media/editors/codemirror/addon/search/searchcursor.min.js', + '/media/editors/codemirror/addon/selection/active-line.js', + '/media/editors/codemirror/addon/selection/active-line.min.js', + '/media/editors/codemirror/addon/selection/mark-selection.js', + '/media/editors/codemirror/addon/selection/mark-selection.min.js', + '/media/editors/codemirror/addon/selection/selection-pointer.js', + '/media/editors/codemirror/addon/selection/selection-pointer.min.js', + '/media/editors/codemirror/addon/tern/tern.css', + '/media/editors/codemirror/addon/tern/tern.js', + '/media/editors/codemirror/addon/tern/tern.min.css', + '/media/editors/codemirror/addon/tern/tern.min.js', + '/media/editors/codemirror/addon/tern/worker.js', + '/media/editors/codemirror/addon/tern/worker.min.js', + '/media/editors/codemirror/addon/wrap/hardwrap.js', + '/media/editors/codemirror/addon/wrap/hardwrap.min.js', + '/media/editors/codemirror/keymap/emacs.js', + '/media/editors/codemirror/keymap/emacs.min.js', + '/media/editors/codemirror/keymap/sublime.js', + '/media/editors/codemirror/keymap/sublime.min.js', + '/media/editors/codemirror/keymap/vim.js', + '/media/editors/codemirror/keymap/vim.min.js', + '/media/editors/codemirror/lib/addons.css', + '/media/editors/codemirror/lib/addons.js', + '/media/editors/codemirror/lib/addons.min.css', + '/media/editors/codemirror/lib/addons.min.js', + '/media/editors/codemirror/lib/codemirror.css', + '/media/editors/codemirror/lib/codemirror.js', + '/media/editors/codemirror/lib/codemirror.min.css', + '/media/editors/codemirror/lib/codemirror.min.js', + '/media/editors/codemirror/mode/apl/apl.js', + '/media/editors/codemirror/mode/apl/apl.min.js', + '/media/editors/codemirror/mode/asciiarmor/asciiarmor.js', + '/media/editors/codemirror/mode/asciiarmor/asciiarmor.min.js', + '/media/editors/codemirror/mode/asn.1/asn.1.js', + '/media/editors/codemirror/mode/asn.1/asn.min.js', + '/media/editors/codemirror/mode/asterisk/asterisk.js', + '/media/editors/codemirror/mode/asterisk/asterisk.min.js', + '/media/editors/codemirror/mode/brainfuck/brainfuck.js', + '/media/editors/codemirror/mode/brainfuck/brainfuck.min.js', + '/media/editors/codemirror/mode/clike/clike.js', + '/media/editors/codemirror/mode/clike/clike.min.js', + '/media/editors/codemirror/mode/clojure/clojure.js', + '/media/editors/codemirror/mode/clojure/clojure.min.js', + '/media/editors/codemirror/mode/cmake/cmake.js', + '/media/editors/codemirror/mode/cmake/cmake.min.js', + '/media/editors/codemirror/mode/cobol/cobol.js', + '/media/editors/codemirror/mode/cobol/cobol.min.js', + '/media/editors/codemirror/mode/coffeescript/coffeescript.js', + '/media/editors/codemirror/mode/coffeescript/coffeescript.min.js', + '/media/editors/codemirror/mode/commonlisp/commonlisp.js', + '/media/editors/codemirror/mode/commonlisp/commonlisp.min.js', + '/media/editors/codemirror/mode/crystal/crystal.js', + '/media/editors/codemirror/mode/crystal/crystal.min.js', + '/media/editors/codemirror/mode/css/css.js', + '/media/editors/codemirror/mode/css/css.min.js', + '/media/editors/codemirror/mode/cypher/cypher.js', + '/media/editors/codemirror/mode/cypher/cypher.min.js', + '/media/editors/codemirror/mode/d/d.js', + '/media/editors/codemirror/mode/d/d.min.js', + '/media/editors/codemirror/mode/dart/dart.js', + '/media/editors/codemirror/mode/dart/dart.min.js', + '/media/editors/codemirror/mode/diff/diff.js', + '/media/editors/codemirror/mode/diff/diff.min.js', + '/media/editors/codemirror/mode/django/django.js', + '/media/editors/codemirror/mode/django/django.min.js', + '/media/editors/codemirror/mode/dockerfile/dockerfile.js', + '/media/editors/codemirror/mode/dockerfile/dockerfile.min.js', + '/media/editors/codemirror/mode/dtd/dtd.js', + '/media/editors/codemirror/mode/dtd/dtd.min.js', + '/media/editors/codemirror/mode/dylan/dylan.js', + '/media/editors/codemirror/mode/dylan/dylan.min.js', + '/media/editors/codemirror/mode/ebnf/ebnf.js', + '/media/editors/codemirror/mode/ebnf/ebnf.min.js', + '/media/editors/codemirror/mode/ecl/ecl.js', + '/media/editors/codemirror/mode/ecl/ecl.min.js', + '/media/editors/codemirror/mode/eiffel/eiffel.js', + '/media/editors/codemirror/mode/eiffel/eiffel.min.js', + '/media/editors/codemirror/mode/elm/elm.js', + '/media/editors/codemirror/mode/elm/elm.min.js', + '/media/editors/codemirror/mode/erlang/erlang.js', + '/media/editors/codemirror/mode/erlang/erlang.min.js', + '/media/editors/codemirror/mode/factor/factor.js', + '/media/editors/codemirror/mode/factor/factor.min.js', + '/media/editors/codemirror/mode/fcl/fcl.js', + '/media/editors/codemirror/mode/fcl/fcl.min.js', + '/media/editors/codemirror/mode/forth/forth.js', + '/media/editors/codemirror/mode/forth/forth.min.js', + '/media/editors/codemirror/mode/fortran/fortran.js', + '/media/editors/codemirror/mode/fortran/fortran.min.js', + '/media/editors/codemirror/mode/gas/gas.js', + '/media/editors/codemirror/mode/gas/gas.min.js', + '/media/editors/codemirror/mode/gfm/gfm.js', + '/media/editors/codemirror/mode/gfm/gfm.min.js', + '/media/editors/codemirror/mode/gherkin/gherkin.js', + '/media/editors/codemirror/mode/gherkin/gherkin.min.js', + '/media/editors/codemirror/mode/go/go.js', + '/media/editors/codemirror/mode/go/go.min.js', + '/media/editors/codemirror/mode/groovy/groovy.js', + '/media/editors/codemirror/mode/groovy/groovy.min.js', + '/media/editors/codemirror/mode/haml/haml.js', + '/media/editors/codemirror/mode/haml/haml.min.js', + '/media/editors/codemirror/mode/handlebars/handlebars.js', + '/media/editors/codemirror/mode/handlebars/handlebars.min.js', + '/media/editors/codemirror/mode/haskell-literate/haskell-literate.js', + '/media/editors/codemirror/mode/haskell-literate/haskell-literate.min.js', + '/media/editors/codemirror/mode/haskell/haskell.js', + '/media/editors/codemirror/mode/haskell/haskell.min.js', + '/media/editors/codemirror/mode/haxe/haxe.js', + '/media/editors/codemirror/mode/haxe/haxe.min.js', + '/media/editors/codemirror/mode/htmlembedded/htmlembedded.js', + '/media/editors/codemirror/mode/htmlembedded/htmlembedded.min.js', + '/media/editors/codemirror/mode/htmlmixed/htmlmixed.js', + '/media/editors/codemirror/mode/htmlmixed/htmlmixed.min.js', + '/media/editors/codemirror/mode/http/http.js', + '/media/editors/codemirror/mode/http/http.min.js', + '/media/editors/codemirror/mode/idl/idl.js', + '/media/editors/codemirror/mode/idl/idl.min.js', + '/media/editors/codemirror/mode/javascript/javascript.js', + '/media/editors/codemirror/mode/javascript/javascript.min.js', + '/media/editors/codemirror/mode/jinja2/jinja2.js', + '/media/editors/codemirror/mode/jinja2/jinja2.min.js', + '/media/editors/codemirror/mode/jsx/jsx.js', + '/media/editors/codemirror/mode/jsx/jsx.min.js', + '/media/editors/codemirror/mode/julia/julia.js', + '/media/editors/codemirror/mode/julia/julia.min.js', + '/media/editors/codemirror/mode/livescript/livescript.js', + '/media/editors/codemirror/mode/livescript/livescript.min.js', + '/media/editors/codemirror/mode/lua/lua.js', + '/media/editors/codemirror/mode/lua/lua.min.js', + '/media/editors/codemirror/mode/markdown/markdown.js', + '/media/editors/codemirror/mode/markdown/markdown.min.js', + '/media/editors/codemirror/mode/mathematica/mathematica.js', + '/media/editors/codemirror/mode/mathematica/mathematica.min.js', + '/media/editors/codemirror/mode/mbox/mbox.js', + '/media/editors/codemirror/mode/mbox/mbox.min.js', + '/media/editors/codemirror/mode/meta.js', + '/media/editors/codemirror/mode/meta.min.js', + '/media/editors/codemirror/mode/mirc/mirc.js', + '/media/editors/codemirror/mode/mirc/mirc.min.js', + '/media/editors/codemirror/mode/mllike/mllike.js', + '/media/editors/codemirror/mode/mllike/mllike.min.js', + '/media/editors/codemirror/mode/modelica/modelica.js', + '/media/editors/codemirror/mode/modelica/modelica.min.js', + '/media/editors/codemirror/mode/mscgen/mscgen.js', + '/media/editors/codemirror/mode/mscgen/mscgen.min.js', + '/media/editors/codemirror/mode/mumps/mumps.js', + '/media/editors/codemirror/mode/mumps/mumps.min.js', + '/media/editors/codemirror/mode/nginx/nginx.js', + '/media/editors/codemirror/mode/nginx/nginx.min.js', + '/media/editors/codemirror/mode/nsis/nsis.js', + '/media/editors/codemirror/mode/nsis/nsis.min.js', + '/media/editors/codemirror/mode/ntriples/ntriples.js', + '/media/editors/codemirror/mode/ntriples/ntriples.min.js', + '/media/editors/codemirror/mode/octave/octave.js', + '/media/editors/codemirror/mode/octave/octave.min.js', + '/media/editors/codemirror/mode/oz/oz.js', + '/media/editors/codemirror/mode/oz/oz.min.js', + '/media/editors/codemirror/mode/pascal/pascal.js', + '/media/editors/codemirror/mode/pascal/pascal.min.js', + '/media/editors/codemirror/mode/pegjs/pegjs.js', + '/media/editors/codemirror/mode/pegjs/pegjs.min.js', + '/media/editors/codemirror/mode/perl/perl.js', + '/media/editors/codemirror/mode/perl/perl.min.js', + '/media/editors/codemirror/mode/php/php.js', + '/media/editors/codemirror/mode/php/php.min.js', + '/media/editors/codemirror/mode/pig/pig.js', + '/media/editors/codemirror/mode/pig/pig.min.js', + '/media/editors/codemirror/mode/powershell/powershell.js', + '/media/editors/codemirror/mode/powershell/powershell.min.js', + '/media/editors/codemirror/mode/properties/properties.js', + '/media/editors/codemirror/mode/properties/properties.min.js', + '/media/editors/codemirror/mode/protobuf/protobuf.js', + '/media/editors/codemirror/mode/protobuf/protobuf.min.js', + '/media/editors/codemirror/mode/pug/pug.js', + '/media/editors/codemirror/mode/pug/pug.min.js', + '/media/editors/codemirror/mode/puppet/puppet.js', + '/media/editors/codemirror/mode/puppet/puppet.min.js', + '/media/editors/codemirror/mode/python/python.js', + '/media/editors/codemirror/mode/python/python.min.js', + '/media/editors/codemirror/mode/q/q.js', + '/media/editors/codemirror/mode/q/q.min.js', + '/media/editors/codemirror/mode/r/r.js', + '/media/editors/codemirror/mode/r/r.min.js', + '/media/editors/codemirror/mode/rpm/changes/index.html', + '/media/editors/codemirror/mode/rpm/rpm.js', + '/media/editors/codemirror/mode/rpm/rpm.min.js', + '/media/editors/codemirror/mode/rst/rst.js', + '/media/editors/codemirror/mode/rst/rst.min.js', + '/media/editors/codemirror/mode/ruby/ruby.js', + '/media/editors/codemirror/mode/ruby/ruby.min.js', + '/media/editors/codemirror/mode/rust/rust.js', + '/media/editors/codemirror/mode/rust/rust.min.js', + '/media/editors/codemirror/mode/sas/sas.js', + '/media/editors/codemirror/mode/sas/sas.min.js', + '/media/editors/codemirror/mode/sass/sass.js', + '/media/editors/codemirror/mode/sass/sass.min.js', + '/media/editors/codemirror/mode/scheme/scheme.js', + '/media/editors/codemirror/mode/scheme/scheme.min.js', + '/media/editors/codemirror/mode/shell/shell.js', + '/media/editors/codemirror/mode/shell/shell.min.js', + '/media/editors/codemirror/mode/sieve/sieve.js', + '/media/editors/codemirror/mode/sieve/sieve.min.js', + '/media/editors/codemirror/mode/slim/slim.js', + '/media/editors/codemirror/mode/slim/slim.min.js', + '/media/editors/codemirror/mode/smalltalk/smalltalk.js', + '/media/editors/codemirror/mode/smalltalk/smalltalk.min.js', + '/media/editors/codemirror/mode/smarty/smarty.js', + '/media/editors/codemirror/mode/smarty/smarty.min.js', + '/media/editors/codemirror/mode/solr/solr.js', + '/media/editors/codemirror/mode/solr/solr.min.js', + '/media/editors/codemirror/mode/soy/soy.js', + '/media/editors/codemirror/mode/soy/soy.min.js', + '/media/editors/codemirror/mode/sparql/sparql.js', + '/media/editors/codemirror/mode/sparql/sparql.min.js', + '/media/editors/codemirror/mode/spreadsheet/spreadsheet.js', + '/media/editors/codemirror/mode/spreadsheet/spreadsheet.min.js', + '/media/editors/codemirror/mode/sql/sql.js', + '/media/editors/codemirror/mode/sql/sql.min.js', + '/media/editors/codemirror/mode/stex/stex.js', + '/media/editors/codemirror/mode/stex/stex.min.js', + '/media/editors/codemirror/mode/stylus/stylus.js', + '/media/editors/codemirror/mode/stylus/stylus.min.js', + '/media/editors/codemirror/mode/swift/swift.js', + '/media/editors/codemirror/mode/swift/swift.min.js', + '/media/editors/codemirror/mode/tcl/tcl.js', + '/media/editors/codemirror/mode/tcl/tcl.min.js', + '/media/editors/codemirror/mode/textile/textile.js', + '/media/editors/codemirror/mode/textile/textile.min.js', + '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.css', + '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.js', + '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.min.css', + '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.min.js', + '/media/editors/codemirror/mode/tiki/tiki.css', + '/media/editors/codemirror/mode/tiki/tiki.js', + '/media/editors/codemirror/mode/tiki/tiki.min.css', + '/media/editors/codemirror/mode/tiki/tiki.min.js', + '/media/editors/codemirror/mode/toml/toml.js', + '/media/editors/codemirror/mode/toml/toml.min.js', + '/media/editors/codemirror/mode/tornado/tornado.js', + '/media/editors/codemirror/mode/tornado/tornado.min.js', + '/media/editors/codemirror/mode/troff/troff.js', + '/media/editors/codemirror/mode/troff/troff.min.js', + '/media/editors/codemirror/mode/ttcn-cfg/ttcn-cfg.js', + '/media/editors/codemirror/mode/ttcn-cfg/ttcn-cfg.min.js', + '/media/editors/codemirror/mode/ttcn/ttcn.js', + '/media/editors/codemirror/mode/ttcn/ttcn.min.js', + '/media/editors/codemirror/mode/turtle/turtle.js', + '/media/editors/codemirror/mode/turtle/turtle.min.js', + '/media/editors/codemirror/mode/twig/twig.js', + '/media/editors/codemirror/mode/twig/twig.min.js', + '/media/editors/codemirror/mode/vb/vb.js', + '/media/editors/codemirror/mode/vb/vb.min.js', + '/media/editors/codemirror/mode/vbscript/vbscript.js', + '/media/editors/codemirror/mode/vbscript/vbscript.min.js', + '/media/editors/codemirror/mode/velocity/velocity.js', + '/media/editors/codemirror/mode/velocity/velocity.min.js', + '/media/editors/codemirror/mode/verilog/verilog.js', + '/media/editors/codemirror/mode/verilog/verilog.min.js', + '/media/editors/codemirror/mode/vhdl/vhdl.js', + '/media/editors/codemirror/mode/vhdl/vhdl.min.js', + '/media/editors/codemirror/mode/vue/vue.js', + '/media/editors/codemirror/mode/vue/vue.min.js', + '/media/editors/codemirror/mode/wast/wast.js', + '/media/editors/codemirror/mode/wast/wast.min.js', + '/media/editors/codemirror/mode/webidl/webidl.js', + '/media/editors/codemirror/mode/webidl/webidl.min.js', + '/media/editors/codemirror/mode/xml/xml.js', + '/media/editors/codemirror/mode/xml/xml.min.js', + '/media/editors/codemirror/mode/xquery/xquery.js', + '/media/editors/codemirror/mode/xquery/xquery.min.js', + '/media/editors/codemirror/mode/yacas/yacas.js', + '/media/editors/codemirror/mode/yacas/yacas.min.js', + '/media/editors/codemirror/mode/yaml-frontmatter/yaml-frontmatter.js', + '/media/editors/codemirror/mode/yaml-frontmatter/yaml-frontmatter.min.js', + '/media/editors/codemirror/mode/yaml/yaml.js', + '/media/editors/codemirror/mode/yaml/yaml.min.js', + '/media/editors/codemirror/mode/z80/z80.js', + '/media/editors/codemirror/mode/z80/z80.min.js', + '/media/editors/codemirror/theme/3024-day.css', + '/media/editors/codemirror/theme/3024-night.css', + '/media/editors/codemirror/theme/abcdef.css', + '/media/editors/codemirror/theme/ambiance-mobile.css', + '/media/editors/codemirror/theme/ambiance.css', + '/media/editors/codemirror/theme/ayu-dark.css', + '/media/editors/codemirror/theme/ayu-mirage.css', + '/media/editors/codemirror/theme/base16-dark.css', + '/media/editors/codemirror/theme/base16-light.css', + '/media/editors/codemirror/theme/bespin.css', + '/media/editors/codemirror/theme/blackboard.css', + '/media/editors/codemirror/theme/cobalt.css', + '/media/editors/codemirror/theme/colorforth.css', + '/media/editors/codemirror/theme/darcula.css', + '/media/editors/codemirror/theme/dracula.css', + '/media/editors/codemirror/theme/duotone-dark.css', + '/media/editors/codemirror/theme/duotone-light.css', + '/media/editors/codemirror/theme/eclipse.css', + '/media/editors/codemirror/theme/elegant.css', + '/media/editors/codemirror/theme/erlang-dark.css', + '/media/editors/codemirror/theme/gruvbox-dark.css', + '/media/editors/codemirror/theme/hopscotch.css', + '/media/editors/codemirror/theme/icecoder.css', + '/media/editors/codemirror/theme/idea.css', + '/media/editors/codemirror/theme/isotope.css', + '/media/editors/codemirror/theme/lesser-dark.css', + '/media/editors/codemirror/theme/liquibyte.css', + '/media/editors/codemirror/theme/lucario.css', + '/media/editors/codemirror/theme/material-darker.css', + '/media/editors/codemirror/theme/material-ocean.css', + '/media/editors/codemirror/theme/material-palenight.css', + '/media/editors/codemirror/theme/material.css', + '/media/editors/codemirror/theme/mbo.css', + '/media/editors/codemirror/theme/mdn-like.css', + '/media/editors/codemirror/theme/midnight.css', + '/media/editors/codemirror/theme/monokai.css', + '/media/editors/codemirror/theme/moxer.css', + '/media/editors/codemirror/theme/neat.css', + '/media/editors/codemirror/theme/neo.css', + '/media/editors/codemirror/theme/night.css', + '/media/editors/codemirror/theme/nord.css', + '/media/editors/codemirror/theme/oceanic-next.css', + '/media/editors/codemirror/theme/panda-syntax.css', + '/media/editors/codemirror/theme/paraiso-dark.css', + '/media/editors/codemirror/theme/paraiso-light.css', + '/media/editors/codemirror/theme/pastel-on-dark.css', + '/media/editors/codemirror/theme/railscasts.css', + '/media/editors/codemirror/theme/rubyblue.css', + '/media/editors/codemirror/theme/seti.css', + '/media/editors/codemirror/theme/shadowfox.css', + '/media/editors/codemirror/theme/solarized.css', + '/media/editors/codemirror/theme/ssms.css', + '/media/editors/codemirror/theme/the-matrix.css', + '/media/editors/codemirror/theme/tomorrow-night-bright.css', + '/media/editors/codemirror/theme/tomorrow-night-eighties.css', + '/media/editors/codemirror/theme/ttcn.css', + '/media/editors/codemirror/theme/twilight.css', + '/media/editors/codemirror/theme/vibrant-ink.css', + '/media/editors/codemirror/theme/xq-dark.css', + '/media/editors/codemirror/theme/xq-light.css', + '/media/editors/codemirror/theme/yeti.css', + '/media/editors/codemirror/theme/yonce.css', + '/media/editors/codemirror/theme/zenburn.css', + '/media/editors/none/js/none.js', + '/media/editors/none/js/none.min.js', + '/media/editors/tinymce/changelog.txt', + '/media/editors/tinymce/js/plugins/dragdrop/plugin.js', + '/media/editors/tinymce/js/plugins/dragdrop/plugin.min.js', + '/media/editors/tinymce/js/tiny-close.js', + '/media/editors/tinymce/js/tiny-close.min.js', + '/media/editors/tinymce/js/tinymce-builder.js', + '/media/editors/tinymce/js/tinymce.js', + '/media/editors/tinymce/js/tinymce.min.js', + '/media/editors/tinymce/langs/af.js', + '/media/editors/tinymce/langs/ar.js', + '/media/editors/tinymce/langs/be.js', + '/media/editors/tinymce/langs/bg.js', + '/media/editors/tinymce/langs/bs.js', + '/media/editors/tinymce/langs/ca.js', + '/media/editors/tinymce/langs/cs.js', + '/media/editors/tinymce/langs/cy.js', + '/media/editors/tinymce/langs/da.js', + '/media/editors/tinymce/langs/de.js', + '/media/editors/tinymce/langs/el.js', + '/media/editors/tinymce/langs/es.js', + '/media/editors/tinymce/langs/et.js', + '/media/editors/tinymce/langs/eu.js', + '/media/editors/tinymce/langs/fa.js', + '/media/editors/tinymce/langs/fi.js', + '/media/editors/tinymce/langs/fo.js', + '/media/editors/tinymce/langs/fr.js', + '/media/editors/tinymce/langs/ga.js', + '/media/editors/tinymce/langs/gl.js', + '/media/editors/tinymce/langs/he.js', + '/media/editors/tinymce/langs/hr.js', + '/media/editors/tinymce/langs/hu.js', + '/media/editors/tinymce/langs/id.js', + '/media/editors/tinymce/langs/it.js', + '/media/editors/tinymce/langs/ja.js', + '/media/editors/tinymce/langs/ka.js', + '/media/editors/tinymce/langs/kk.js', + '/media/editors/tinymce/langs/km.js', + '/media/editors/tinymce/langs/ko.js', + '/media/editors/tinymce/langs/lb.js', + '/media/editors/tinymce/langs/lt.js', + '/media/editors/tinymce/langs/lv.js', + '/media/editors/tinymce/langs/mk.js', + '/media/editors/tinymce/langs/ms.js', + '/media/editors/tinymce/langs/nb.js', + '/media/editors/tinymce/langs/nl.js', + '/media/editors/tinymce/langs/pl.js', + '/media/editors/tinymce/langs/pt-BR.js', + '/media/editors/tinymce/langs/pt-PT.js', + '/media/editors/tinymce/langs/readme.md', + '/media/editors/tinymce/langs/ro.js', + '/media/editors/tinymce/langs/ru.js', + '/media/editors/tinymce/langs/si-LK.js', + '/media/editors/tinymce/langs/sk.js', + '/media/editors/tinymce/langs/sl.js', + '/media/editors/tinymce/langs/sr.js', + '/media/editors/tinymce/langs/sv.js', + '/media/editors/tinymce/langs/sw.js', + '/media/editors/tinymce/langs/sy.js', + '/media/editors/tinymce/langs/ta.js', + '/media/editors/tinymce/langs/th.js', + '/media/editors/tinymce/langs/tr.js', + '/media/editors/tinymce/langs/ug.js', + '/media/editors/tinymce/langs/uk.js', + '/media/editors/tinymce/langs/vi.js', + '/media/editors/tinymce/langs/zh-CN.js', + '/media/editors/tinymce/langs/zh-TW.js', + '/media/editors/tinymce/license.txt', + '/media/editors/tinymce/plugins/advlist/plugin.min.js', + '/media/editors/tinymce/plugins/anchor/plugin.min.js', + '/media/editors/tinymce/plugins/autolink/plugin.min.js', + '/media/editors/tinymce/plugins/autoresize/plugin.min.js', + '/media/editors/tinymce/plugins/autosave/plugin.min.js', + '/media/editors/tinymce/plugins/bbcode/plugin.min.js', + '/media/editors/tinymce/plugins/charmap/plugin.min.js', + '/media/editors/tinymce/plugins/code/plugin.min.js', + '/media/editors/tinymce/plugins/codesample/css/prism.css', + '/media/editors/tinymce/plugins/codesample/plugin.min.js', + '/media/editors/tinymce/plugins/colorpicker/plugin.min.js', + '/media/editors/tinymce/plugins/contextmenu/plugin.min.js', + '/media/editors/tinymce/plugins/directionality/plugin.min.js', + '/media/editors/tinymce/plugins/emoticons/img/smiley-cool.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-cry.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-embarassed.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-foot-in-mouth.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-frown.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-innocent.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-kiss.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-laughing.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-money-mouth.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-sealed.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-smile.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-surprised.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-tongue-out.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-undecided.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-wink.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-yell.gif', + '/media/editors/tinymce/plugins/emoticons/plugin.min.js', + '/media/editors/tinymce/plugins/example/dialog.html', + '/media/editors/tinymce/plugins/example/plugin.min.js', + '/media/editors/tinymce/plugins/example_dependency/plugin.min.js', + '/media/editors/tinymce/plugins/fullpage/plugin.min.js', + '/media/editors/tinymce/plugins/fullscreen/plugin.min.js', + '/media/editors/tinymce/plugins/hr/plugin.min.js', + '/media/editors/tinymce/plugins/image/plugin.min.js', + '/media/editors/tinymce/plugins/imagetools/plugin.min.js', + '/media/editors/tinymce/plugins/importcss/plugin.min.js', + '/media/editors/tinymce/plugins/insertdatetime/plugin.min.js', + '/media/editors/tinymce/plugins/layer/plugin.min.js', + '/media/editors/tinymce/plugins/legacyoutput/plugin.min.js', + '/media/editors/tinymce/plugins/link/plugin.min.js', + '/media/editors/tinymce/plugins/lists/plugin.min.js', + '/media/editors/tinymce/plugins/media/plugin.min.js', + '/media/editors/tinymce/plugins/nonbreaking/plugin.min.js', + '/media/editors/tinymce/plugins/noneditable/plugin.min.js', + '/media/editors/tinymce/plugins/pagebreak/plugin.min.js', + '/media/editors/tinymce/plugins/paste/plugin.min.js', + '/media/editors/tinymce/plugins/preview/plugin.min.js', + '/media/editors/tinymce/plugins/print/plugin.min.js', + '/media/editors/tinymce/plugins/save/plugin.min.js', + '/media/editors/tinymce/plugins/searchreplace/plugin.min.js', + '/media/editors/tinymce/plugins/spellchecker/plugin.min.js', + '/media/editors/tinymce/plugins/tabfocus/plugin.min.js', + '/media/editors/tinymce/plugins/table/plugin.min.js', + '/media/editors/tinymce/plugins/template/plugin.min.js', + '/media/editors/tinymce/plugins/textcolor/plugin.min.js', + '/media/editors/tinymce/plugins/textpattern/plugin.min.js', + '/media/editors/tinymce/plugins/toc/plugin.min.js', + '/media/editors/tinymce/plugins/visualblocks/css/visualblocks.css', + '/media/editors/tinymce/plugins/visualblocks/plugin.min.js', + '/media/editors/tinymce/plugins/visualchars/plugin.min.js', + '/media/editors/tinymce/plugins/wordcount/plugin.min.js', + '/media/editors/tinymce/skins/lightgray/content.inline.min.css', + '/media/editors/tinymce/skins/lightgray/content.min.css', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.eot', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.svg', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.ttf', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.woff', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce.eot', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce.svg', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce.ttf', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce.woff', + '/media/editors/tinymce/skins/lightgray/img/anchor.gif', + '/media/editors/tinymce/skins/lightgray/img/loader.gif', + '/media/editors/tinymce/skins/lightgray/img/object.gif', + '/media/editors/tinymce/skins/lightgray/img/trans.gif', + '/media/editors/tinymce/skins/lightgray/skin.ie7.min.css', + '/media/editors/tinymce/skins/lightgray/skin.min.css', + '/media/editors/tinymce/templates/layout1.html', + '/media/editors/tinymce/templates/snippet1.html', + '/media/editors/tinymce/themes/modern/theme.min.js', + '/media/editors/tinymce/tinymce.min.js', + '/media/jui/css/bootstrap-extended.css', + '/media/jui/css/bootstrap-responsive.css', + '/media/jui/css/bootstrap-responsive.min.css', + '/media/jui/css/bootstrap-rtl.css', + '/media/jui/css/bootstrap-tooltip-extended.css', + '/media/jui/css/bootstrap.css', + '/media/jui/css/bootstrap.min.css', + '/media/jui/css/chosen-sprite.png', + '/media/jui/css/chosen-sprite@2x.png', + '/media/jui/css/chosen.css', + '/media/jui/css/icomoon.css', + '/media/jui/css/jquery.minicolors.css', + '/media/jui/css/jquery.searchtools.css', + '/media/jui/css/jquery.simplecolors.css', + '/media/jui/css/sortablelist.css', + '/media/jui/fonts/IcoMoon.dev.commented.svg', + '/media/jui/fonts/IcoMoon.dev.svg', + '/media/jui/fonts/IcoMoon.eot', + '/media/jui/fonts/IcoMoon.svg', + '/media/jui/fonts/IcoMoon.ttf', + '/media/jui/fonts/IcoMoon.woff', + '/media/jui/fonts/icomoon-license.txt', + '/media/jui/images/ajax-loader.gif', + '/media/jui/img/ajax-loader.gif', + '/media/jui/img/alpha.png', + '/media/jui/img/bg-overlay.png', + '/media/jui/img/glyphicons-halflings-white.png', + '/media/jui/img/glyphicons-halflings.png', + '/media/jui/img/hue.png', + '/media/jui/img/joomla.png', + '/media/jui/img/jquery.minicolors.png', + '/media/jui/img/saturation.png', + '/media/jui/js/ajax-chosen.js', + '/media/jui/js/ajax-chosen.min.js', + '/media/jui/js/bootstrap-tooltip-extended.js', + '/media/jui/js/bootstrap-tooltip-extended.min.js', + '/media/jui/js/bootstrap.js', + '/media/jui/js/bootstrap.min.js', + '/media/jui/js/chosen.jquery.js', + '/media/jui/js/chosen.jquery.min.js', + '/media/jui/js/cms-uncompressed.js', + '/media/jui/js/cms.js', + '/media/jui/js/fielduser.js', + '/media/jui/js/fielduser.min.js', + '/media/jui/js/html5-uncompressed.js', + '/media/jui/js/html5.js', + '/media/jui/js/icomoon-lte-ie7.js', + '/media/jui/js/jquery-migrate.js', + '/media/jui/js/jquery-migrate.min.js', + '/media/jui/js/jquery-noconflict.js', + '/media/jui/js/jquery.autocomplete.js', + '/media/jui/js/jquery.autocomplete.min.js', + '/media/jui/js/jquery.js', + '/media/jui/js/jquery.min.js', + '/media/jui/js/jquery.minicolors.js', + '/media/jui/js/jquery.minicolors.min.js', + '/media/jui/js/jquery.searchtools.js', + '/media/jui/js/jquery.searchtools.min.js', + '/media/jui/js/jquery.simplecolors.js', + '/media/jui/js/jquery.simplecolors.min.js', + '/media/jui/js/jquery.ui.core.js', + '/media/jui/js/jquery.ui.core.min.js', + '/media/jui/js/jquery.ui.sortable.js', + '/media/jui/js/jquery.ui.sortable.min.js', + '/media/jui/js/sortablelist.js', + '/media/jui/js/treeselectmenu.jquery.js', + '/media/jui/js/treeselectmenu.jquery.min.js', + '/media/jui/less/accordion.less', + '/media/jui/less/alerts.less', + '/media/jui/less/bootstrap-extended.less', + '/media/jui/less/bootstrap-rtl.less', + '/media/jui/less/bootstrap.less', + '/media/jui/less/breadcrumbs.less', + '/media/jui/less/button-groups.less', + '/media/jui/less/buttons.less', + '/media/jui/less/carousel.less', + '/media/jui/less/close.less', + '/media/jui/less/code.less', + '/media/jui/less/component-animations.less', + '/media/jui/less/dropdowns.less', + '/media/jui/less/forms.less', + '/media/jui/less/grid.less', + '/media/jui/less/hero-unit.less', + '/media/jui/less/icomoon.less', + '/media/jui/less/labels-badges.less', + '/media/jui/less/layouts.less', + '/media/jui/less/media.less', + '/media/jui/less/mixins.less', + '/media/jui/less/modals.joomla.less', + '/media/jui/less/modals.less', + '/media/jui/less/navbar.less', + '/media/jui/less/navs.less', + '/media/jui/less/pager.less', + '/media/jui/less/pagination.less', + '/media/jui/less/popovers.less', + '/media/jui/less/progress-bars.less', + '/media/jui/less/reset.less', + '/media/jui/less/responsive-1200px-min.less', + '/media/jui/less/responsive-767px-max.joomla.less', + '/media/jui/less/responsive-767px-max.less', + '/media/jui/less/responsive-768px-979px.less', + '/media/jui/less/responsive-navbar.less', + '/media/jui/less/responsive-utilities.less', + '/media/jui/less/responsive.less', + '/media/jui/less/scaffolding.less', + '/media/jui/less/sprites.less', + '/media/jui/less/tables.less', + '/media/jui/less/thumbnails.less', + '/media/jui/less/tooltip.less', + '/media/jui/less/type.less', + '/media/jui/less/utilities.less', + '/media/jui/less/variables.less', + '/media/jui/less/wells.less', + '/media/media/css/background.png', + '/media/media/css/bigplay.fw.png', + '/media/media/css/bigplay.png', + '/media/media/css/bigplay.svg', + '/media/media/css/controls-ted.png', + '/media/media/css/controls-wmp-bg.png', + '/media/media/css/controls-wmp.png', + '/media/media/css/controls.fw.png', + '/media/media/css/controls.png', + '/media/media/css/controls.svg', + '/media/media/css/jumpforward.png', + '/media/media/css/loading.gif', + '/media/media/css/mediaelementplayer.css', + '/media/media/css/mediaelementplayer.min.css', + '/media/media/css/medialist-details.css', + '/media/media/css/medialist-details_rtl.css', + '/media/media/css/medialist-thumbs.css', + '/media/media/css/medialist-thumbs_rtl.css', + '/media/media/css/mediamanager.css', + '/media/media/css/mediamanager_rtl.css', + '/media/media/css/mejs-skins.css', + '/media/media/css/popup-imagelist.css', + '/media/media/css/popup-imagelist_rtl.css', + '/media/media/css/popup-imagemanager.css', + '/media/media/css/popup-imagemanager_rtl.css', + '/media/media/css/skipback.png', + '/media/media/images/bar.gif', + '/media/media/images/con_info.png', + '/media/media/images/delete.png', + '/media/media/images/dots.gif', + '/media/media/images/failed.png', + '/media/media/images/folder.gif', + '/media/media/images/folder.png', + '/media/media/images/folder_sm.png', + '/media/media/images/folderup_16.png', + '/media/media/images/folderup_32.png', + '/media/media/images/mime-icon-16/avi.png', + '/media/media/images/mime-icon-16/doc.png', + '/media/media/images/mime-icon-16/mov.png', + '/media/media/images/mime-icon-16/mp3.png', + '/media/media/images/mime-icon-16/mp4.png', + '/media/media/images/mime-icon-16/odc.png', + '/media/media/images/mime-icon-16/odd.png', + '/media/media/images/mime-icon-16/odt.png', + '/media/media/images/mime-icon-16/ogg.png', + '/media/media/images/mime-icon-16/pdf.png', + '/media/media/images/mime-icon-16/ppt.png', + '/media/media/images/mime-icon-16/rar.png', + '/media/media/images/mime-icon-16/rtf.png', + '/media/media/images/mime-icon-16/svg.png', + '/media/media/images/mime-icon-16/sxd.png', + '/media/media/images/mime-icon-16/tar.png', + '/media/media/images/mime-icon-16/tgz.png', + '/media/media/images/mime-icon-16/wma.png', + '/media/media/images/mime-icon-16/wmv.png', + '/media/media/images/mime-icon-16/xls.png', + '/media/media/images/mime-icon-16/zip.png', + '/media/media/images/mime-icon-32/avi.png', + '/media/media/images/mime-icon-32/doc.png', + '/media/media/images/mime-icon-32/mov.png', + '/media/media/images/mime-icon-32/mp3.png', + '/media/media/images/mime-icon-32/mp4.png', + '/media/media/images/mime-icon-32/odc.png', + '/media/media/images/mime-icon-32/odd.png', + '/media/media/images/mime-icon-32/odt.png', + '/media/media/images/mime-icon-32/ogg.png', + '/media/media/images/mime-icon-32/pdf.png', + '/media/media/images/mime-icon-32/ppt.png', + '/media/media/images/mime-icon-32/rar.png', + '/media/media/images/mime-icon-32/rtf.png', + '/media/media/images/mime-icon-32/svg.png', + '/media/media/images/mime-icon-32/sxd.png', + '/media/media/images/mime-icon-32/tar.png', + '/media/media/images/mime-icon-32/tgz.png', + '/media/media/images/mime-icon-32/wma.png', + '/media/media/images/mime-icon-32/wmv.png', + '/media/media/images/mime-icon-32/xls.png', + '/media/media/images/mime-icon-32/zip.png', + '/media/media/images/progress.gif', + '/media/media/images/remove.png', + '/media/media/images/success.png', + '/media/media/images/upload.png', + '/media/media/images/uploading.png', + '/media/media/js/flashmediaelement-cdn.swf', + '/media/media/js/flashmediaelement.swf', + '/media/media/js/mediaelement-and-player.js', + '/media/media/js/mediaelement-and-player.min.js', + '/media/media/js/mediafield-mootools.js', + '/media/media/js/mediafield-mootools.min.js', + '/media/media/js/mediafield.js', + '/media/media/js/mediafield.min.js', + '/media/media/js/mediamanager.js', + '/media/media/js/mediamanager.min.js', + '/media/media/js/popup-imagemanager.js', + '/media/media/js/popup-imagemanager.min.js', + '/media/media/js/silverlightmediaelement.xap', + '/media/overrider/css/overrider.css', + '/media/overrider/js/overrider.js', + '/media/overrider/js/overrider.min.js', + '/media/plg_system_highlight/highlight.css', + '/media/plg_twofactorauth_totp/js/qrcode.js', + '/media/plg_twofactorauth_totp/js/qrcode.min.js', + '/media/plg_twofactorauth_totp/js/qrcode_SJIS.js', + '/media/plg_twofactorauth_totp/js/qrcode_UTF8.js', + '/media/system/css/adminlist.css', + '/media/system/css/jquery.Jcrop.min.css', + '/media/system/css/modal.css', + '/media/system/css/system.css', + '/media/system/js/associations-edit-uncompressed.js', + '/media/system/js/associations-edit.js', + '/media/system/js/calendar-setup-uncompressed.js', + '/media/system/js/calendar-setup.js', + '/media/system/js/calendar-uncompressed.js', + '/media/system/js/calendar.js', + '/media/system/js/caption-uncompressed.js', + '/media/system/js/caption.js', + '/media/system/js/color-field-adv-init.js', + '/media/system/js/color-field-adv-init.min.js', + '/media/system/js/color-field-init.js', + '/media/system/js/color-field-init.min.js', + '/media/system/js/combobox-uncompressed.js', + '/media/system/js/combobox.js', + '/media/system/js/core-uncompressed.js', + '/media/system/js/fields/calendar-locales/af.js', + '/media/system/js/fields/calendar-locales/ar.js', + '/media/system/js/fields/calendar-locales/bg.js', + '/media/system/js/fields/calendar-locales/bn.js', + '/media/system/js/fields/calendar-locales/bs.js', + '/media/system/js/fields/calendar-locales/ca.js', + '/media/system/js/fields/calendar-locales/cs.js', + '/media/system/js/fields/calendar-locales/cy.js', + '/media/system/js/fields/calendar-locales/da.js', + '/media/system/js/fields/calendar-locales/de.js', + '/media/system/js/fields/calendar-locales/el.js', + '/media/system/js/fields/calendar-locales/en.js', + '/media/system/js/fields/calendar-locales/es.js', + '/media/system/js/fields/calendar-locales/eu.js', + '/media/system/js/fields/calendar-locales/fa-ir.js', + '/media/system/js/fields/calendar-locales/fi.js', + '/media/system/js/fields/calendar-locales/fr.js', + '/media/system/js/fields/calendar-locales/ga.js', + '/media/system/js/fields/calendar-locales/hr.js', + '/media/system/js/fields/calendar-locales/hu.js', + '/media/system/js/fields/calendar-locales/it.js', + '/media/system/js/fields/calendar-locales/ja.js', + '/media/system/js/fields/calendar-locales/ka.js', + '/media/system/js/fields/calendar-locales/kk.js', + '/media/system/js/fields/calendar-locales/ko.js', + '/media/system/js/fields/calendar-locales/lt.js', + '/media/system/js/fields/calendar-locales/mk.js', + '/media/system/js/fields/calendar-locales/nb.js', + '/media/system/js/fields/calendar-locales/nl.js', + '/media/system/js/fields/calendar-locales/pl.js', + '/media/system/js/fields/calendar-locales/prs-af.js', + '/media/system/js/fields/calendar-locales/pt.js', + '/media/system/js/fields/calendar-locales/ru.js', + '/media/system/js/fields/calendar-locales/sk.js', + '/media/system/js/fields/calendar-locales/sl.js', + '/media/system/js/fields/calendar-locales/sr-rs.js', + '/media/system/js/fields/calendar-locales/sr-yu.js', + '/media/system/js/fields/calendar-locales/sv.js', + '/media/system/js/fields/calendar-locales/sw.js', + '/media/system/js/fields/calendar-locales/ta.js', + '/media/system/js/fields/calendar-locales/th.js', + '/media/system/js/fields/calendar-locales/uk.js', + '/media/system/js/fields/calendar-locales/zh-CN.js', + '/media/system/js/fields/calendar-locales/zh-TW.js', + '/media/system/js/frontediting-uncompressed.js', + '/media/system/js/frontediting.js', + '/media/system/js/helpsite.js', + '/media/system/js/highlighter-uncompressed.js', + '/media/system/js/highlighter.js', + '/media/system/js/html5fallback-uncompressed.js', + '/media/system/js/html5fallback.js', + '/media/system/js/jquery.Jcrop.js', + '/media/system/js/jquery.Jcrop.min.js', + '/media/system/js/keepalive-uncompressed.js', + '/media/system/js/modal-fields-uncompressed.js', + '/media/system/js/modal-fields.js', + '/media/system/js/modal-uncompressed.js', + '/media/system/js/modal.js', + '/media/system/js/moduleorder.js', + '/media/system/js/mootools-core-uncompressed.js', + '/media/system/js/mootools-core.js', + '/media/system/js/mootools-more-uncompressed.js', + '/media/system/js/mootools-more.js', + '/media/system/js/mootree-uncompressed.js', + '/media/system/js/mootree.js', + '/media/system/js/multiselect-uncompressed.js', + '/media/system/js/passwordstrength.js', + '/media/system/js/permissions-uncompressed.js', + '/media/system/js/permissions.js', + '/media/system/js/polyfill.classlist-uncompressed.js', + '/media/system/js/polyfill.classlist.js', + '/media/system/js/polyfill.event-uncompressed.js', + '/media/system/js/polyfill.event.js', + '/media/system/js/polyfill.filter-uncompressed.js', + '/media/system/js/polyfill.filter.js', + '/media/system/js/polyfill.map-uncompressed.js', + '/media/system/js/polyfill.map.js', + '/media/system/js/polyfill.xpath-uncompressed.js', + '/media/system/js/polyfill.xpath.js', + '/media/system/js/progressbar-uncompressed.js', + '/media/system/js/progressbar.js', + '/media/system/js/punycode-uncompressed.js', + '/media/system/js/punycode.js', + '/media/system/js/repeatable-uncompressed.js', + '/media/system/js/repeatable.js', + '/media/system/js/sendtestmail-uncompressed.js', + '/media/system/js/sendtestmail.js', + '/media/system/js/subform-repeatable-uncompressed.js', + '/media/system/js/subform-repeatable.js', + '/media/system/js/switcher-uncompressed.js', + '/media/system/js/switcher.js', + '/media/system/js/tabs-state-uncompressed.js', + '/media/system/js/tabs-state.js', + '/media/system/js/tabs.js', + '/media/system/js/validate-uncompressed.js', + '/media/system/js/validate.js', + '/modules/mod_articles_archive/helper.php', + '/modules/mod_articles_categories/helper.php', + '/modules/mod_articles_category/helper.php', + '/modules/mod_articles_latest/helper.php', + '/modules/mod_articles_news/helper.php', + '/modules/mod_articles_popular/helper.php', + '/modules/mod_banners/helper.php', + '/modules/mod_breadcrumbs/helper.php', + '/modules/mod_feed/helper.php', + '/modules/mod_finder/helper.php', + '/modules/mod_languages/helper.php', + '/modules/mod_login/helper.php', + '/modules/mod_menu/helper.php', + '/modules/mod_random_image/helper.php', + '/modules/mod_related_items/helper.php', + '/modules/mod_stats/helper.php', + '/modules/mod_syndicate/helper.php', + '/modules/mod_tags_popular/helper.php', + '/modules/mod_tags_similar/helper.php', + '/modules/mod_users_latest/helper.php', + '/modules/mod_whosonline/helper.php', + '/modules/mod_wrapper/helper.php', + '/plugins/authentication/gmail/gmail.php', + '/plugins/authentication/gmail/gmail.xml', + '/plugins/captcha/recaptcha/postinstall/actions.php', + '/plugins/content/confirmconsent/fields/consentbox.php', + '/plugins/editors/codemirror/fonts.php', + '/plugins/editors/codemirror/layouts/editors/codemirror/init.php', + '/plugins/editors/tinymce/field/skins.php', + '/plugins/editors/tinymce/field/tinymcebuilder.php', + '/plugins/editors/tinymce/field/uploaddirs.php', + '/plugins/editors/tinymce/form/setoptions.xml', + '/plugins/quickicon/joomlaupdate/joomlaupdate.php', + '/plugins/system/languagecode/language/en-GB/en-GB.plg_system_languagecode.ini', + '/plugins/system/languagecode/language/en-GB/en-GB.plg_system_languagecode.sys.ini', + '/plugins/system/p3p/p3p.php', + '/plugins/system/p3p/p3p.xml', + '/plugins/system/privacyconsent/field/privacy.php', + '/plugins/system/privacyconsent/privacyconsent/privacyconsent.xml', + '/plugins/system/stats/field/base.php', + '/plugins/system/stats/field/data.php', + '/plugins/system/stats/field/uniqueid.php', + '/plugins/user/profile/field/dob.php', + '/plugins/user/profile/field/tos.php', + '/plugins/user/profile/profiles/profile.xml', + '/plugins/user/terms/field/terms.php', + '/plugins/user/terms/terms/terms.xml', + '/templates/beez3/component.php', + '/templates/beez3/css/general.css', + '/templates/beez3/css/ie7only.css', + '/templates/beez3/css/ieonly.css', + '/templates/beez3/css/layout.css', + '/templates/beez3/css/nature.css', + '/templates/beez3/css/nature_rtl.css', + '/templates/beez3/css/personal.css', + '/templates/beez3/css/personal_rtl.css', + '/templates/beez3/css/position.css', + '/templates/beez3/css/print.css', + '/templates/beez3/css/red.css', + '/templates/beez3/css/template.css', + '/templates/beez3/css/template_rtl.css', + '/templates/beez3/css/turq.css', + '/templates/beez3/css/turq.less', + '/templates/beez3/error.php', + '/templates/beez3/favicon.ico', + '/templates/beez3/html/com_contact/categories/default.php', + '/templates/beez3/html/com_contact/categories/default_items.php', + '/templates/beez3/html/com_contact/category/default.php', + '/templates/beez3/html/com_contact/category/default_children.php', + '/templates/beez3/html/com_contact/category/default_items.php', + '/templates/beez3/html/com_contact/contact/default.php', + '/templates/beez3/html/com_contact/contact/default_address.php', + '/templates/beez3/html/com_contact/contact/default_articles.php', + '/templates/beez3/html/com_contact/contact/default_form.php', + '/templates/beez3/html/com_contact/contact/default_links.php', + '/templates/beez3/html/com_contact/contact/default_profile.php', + '/templates/beez3/html/com_contact/contact/default_user_custom_fields.php', + '/templates/beez3/html/com_contact/contact/encyclopedia.php', + '/templates/beez3/html/com_content/archive/default.php', + '/templates/beez3/html/com_content/archive/default_items.php', + '/templates/beez3/html/com_content/article/default.php', + '/templates/beez3/html/com_content/article/default_links.php', + '/templates/beez3/html/com_content/categories/default.php', + '/templates/beez3/html/com_content/categories/default_items.php', + '/templates/beez3/html/com_content/category/blog.php', + '/templates/beez3/html/com_content/category/blog_children.php', + '/templates/beez3/html/com_content/category/blog_item.php', + '/templates/beez3/html/com_content/category/blog_links.php', + '/templates/beez3/html/com_content/category/default.php', + '/templates/beez3/html/com_content/category/default_articles.php', + '/templates/beez3/html/com_content/category/default_children.php', + '/templates/beez3/html/com_content/featured/default.php', + '/templates/beez3/html/com_content/featured/default_item.php', + '/templates/beez3/html/com_content/featured/default_links.php', + '/templates/beez3/html/com_content/form/edit.php', + '/templates/beez3/html/com_newsfeeds/categories/default.php', + '/templates/beez3/html/com_newsfeeds/categories/default_items.php', + '/templates/beez3/html/com_newsfeeds/category/default.php', + '/templates/beez3/html/com_newsfeeds/category/default_children.php', + '/templates/beez3/html/com_newsfeeds/category/default_items.php', + '/templates/beez3/html/com_weblinks/categories/default.php', + '/templates/beez3/html/com_weblinks/categories/default_items.php', + '/templates/beez3/html/com_weblinks/category/default.php', + '/templates/beez3/html/com_weblinks/category/default_children.php', + '/templates/beez3/html/com_weblinks/category/default_items.php', + '/templates/beez3/html/com_weblinks/form/edit.php', + '/templates/beez3/html/layouts/joomla/system/message.php', + '/templates/beez3/html/mod_breadcrumbs/default.php', + '/templates/beez3/html/mod_languages/default.php', + '/templates/beez3/html/mod_login/default.php', + '/templates/beez3/html/mod_login/default_logout.php', + '/templates/beez3/html/modules.php', + '/templates/beez3/images/all_bg.gif', + '/templates/beez3/images/arrow.png', + '/templates/beez3/images/arrow2_grey.png', + '/templates/beez3/images/arrow_white_grey.png', + '/templates/beez3/images/blog_more.gif', + '/templates/beez3/images/blog_more_hover.gif', + '/templates/beez3/images/close.png', + '/templates/beez3/images/content_bg.gif', + '/templates/beez3/images/footer_bg.gif', + '/templates/beez3/images/footer_bg.png', + '/templates/beez3/images/header-bg.gif', + '/templates/beez3/images/minus.png', + '/templates/beez3/images/nature/arrow1.gif', + '/templates/beez3/images/nature/arrow1_rtl.gif', + '/templates/beez3/images/nature/arrow2.gif', + '/templates/beez3/images/nature/arrow2_grey.png', + '/templates/beez3/images/nature/arrow2_rtl.gif', + '/templates/beez3/images/nature/arrow_nav.gif', + '/templates/beez3/images/nature/arrow_small.png', + '/templates/beez3/images/nature/arrow_small_rtl.png', + '/templates/beez3/images/nature/blog_more.gif', + '/templates/beez3/images/nature/box.png', + '/templates/beez3/images/nature/box1.png', + '/templates/beez3/images/nature/grey_bg.png', + '/templates/beez3/images/nature/headingback.png', + '/templates/beez3/images/nature/karo.gif', + '/templates/beez3/images/nature/level4.png', + '/templates/beez3/images/nature/nav_level1_a.gif', + '/templates/beez3/images/nature/nav_level_1.gif', + '/templates/beez3/images/nature/pfeil.gif', + '/templates/beez3/images/nature/readmore_arrow.png', + '/templates/beez3/images/nature/searchbutton.png', + '/templates/beez3/images/nature/tabs.gif', + '/templates/beez3/images/nav_level_1.gif', + '/templates/beez3/images/news.gif', + '/templates/beez3/images/personal/arrow2_grey.jpg', + '/templates/beez3/images/personal/arrow2_grey.png', + '/templates/beez3/images/personal/bg2.png', + '/templates/beez3/images/personal/button.png', + '/templates/beez3/images/personal/dot.png', + '/templates/beez3/images/personal/ecke.gif', + '/templates/beez3/images/personal/footer.jpg', + '/templates/beez3/images/personal/grey_bg.png', + '/templates/beez3/images/personal/navi_active.png', + '/templates/beez3/images/personal/personal2.png', + '/templates/beez3/images/personal/readmore_arrow.png', + '/templates/beez3/images/personal/readmore_arrow_hover.png', + '/templates/beez3/images/personal/tabs_back.png', + '/templates/beez3/images/plus.png', + '/templates/beez3/images/req.png', + '/templates/beez3/images/slider_minus.png', + '/templates/beez3/images/slider_minus_rtl.png', + '/templates/beez3/images/slider_plus.png', + '/templates/beez3/images/slider_plus_rtl.png', + '/templates/beez3/images/system/arrow.png', + '/templates/beez3/images/system/arrow_rtl.png', + '/templates/beez3/images/system/calendar.png', + '/templates/beez3/images/system/j_button2_blank.png', + '/templates/beez3/images/system/j_button2_image.png', + '/templates/beez3/images/system/j_button2_left.png', + '/templates/beez3/images/system/j_button2_pagebreak.png', + '/templates/beez3/images/system/j_button2_readmore.png', + '/templates/beez3/images/system/notice-alert.png', + '/templates/beez3/images/system/notice-alert_rtl.png', + '/templates/beez3/images/system/notice-info.png', + '/templates/beez3/images/system/notice-info_rtl.png', + '/templates/beez3/images/system/notice-note.png', + '/templates/beez3/images/system/notice-note_rtl.png', + '/templates/beez3/images/system/selector-arrow.png', + '/templates/beez3/images/table_footer.gif', + '/templates/beez3/images/trans.gif', + '/templates/beez3/index.php', + '/templates/beez3/javascript/hide.js', + '/templates/beez3/javascript/md_stylechanger.js', + '/templates/beez3/javascript/respond.js', + '/templates/beez3/javascript/respond.src.js', + '/templates/beez3/javascript/template.js', + '/templates/beez3/jsstrings.php', + '/templates/beez3/language/en-GB/en-GB.tpl_beez3.ini', + '/templates/beez3/language/en-GB/en-GB.tpl_beez3.sys.ini', + '/templates/beez3/templateDetails.xml', + '/templates/beez3/template_preview.png', + '/templates/beez3/template_thumbnail.png', + '/templates/protostar/component.php', + '/templates/protostar/css/offline.css', + '/templates/protostar/css/template.css', + '/templates/protostar/error.php', + '/templates/protostar/favicon.ico', + '/templates/protostar/html/com_media/imageslist/default_folder.php', + '/templates/protostar/html/com_media/imageslist/default_image.php', + '/templates/protostar/html/layouts/joomla/form/field/contenthistory.php', + '/templates/protostar/html/layouts/joomla/form/field/media.php', + '/templates/protostar/html/layouts/joomla/form/field/user.php', + '/templates/protostar/html/layouts/joomla/system/message.php', + '/templates/protostar/html/modules.php', + '/templates/protostar/html/pagination.php', + '/templates/protostar/images/logo.png', + '/templates/protostar/images/system/rating_star.png', + '/templates/protostar/images/system/rating_star_blank.png', + '/templates/protostar/images/system/sort_asc.png', + '/templates/protostar/images/system/sort_desc.png', + '/templates/protostar/img/glyphicons-halflings-white.png', + '/templates/protostar/img/glyphicons-halflings.png', + '/templates/protostar/index.php', + '/templates/protostar/js/application.js', + '/templates/protostar/js/classes.js', + '/templates/protostar/js/template.js', + '/templates/protostar/language/en-GB/en-GB.tpl_protostar.ini', + '/templates/protostar/language/en-GB/en-GB.tpl_protostar.sys.ini', + '/templates/protostar/less/icomoon.less', + '/templates/protostar/less/template.less', + '/templates/protostar/less/template_rtl.less', + '/templates/protostar/less/variables.less', + '/templates/protostar/offline.php', + '/templates/protostar/templateDetails.xml', + '/templates/protostar/template_preview.png', + '/templates/protostar/template_thumbnail.png', + '/templates/system/css/system.css', + '/templates/system/css/toolbar.css', + '/templates/system/html/modules.php', + '/templates/system/images/calendar.png', + '/templates/system/images/j_button2_blank.png', + '/templates/system/images/j_button2_image.png', + '/templates/system/images/j_button2_left.png', + '/templates/system/images/j_button2_pagebreak.png', + '/templates/system/images/j_button2_readmore.png', + '/templates/system/images/j_button2_right.png', + '/templates/system/images/selector-arrow.png', + // 4.0 from Beta 1 to Beta 2 + '/administrator/components/com_finder/src/Indexer/Driver/Mysql.php', + '/administrator/components/com_finder/src/Indexer/Driver/Postgresql.php', + '/administrator/components/com_workflow/access.xml', + '/api/components/com_installer/src/Controller/LanguagesController.php', + '/api/components/com_installer/src/View/Languages/JsonapiView.php', + '/libraries/vendor/joomla/controller/LICENSE', + '/libraries/vendor/joomla/controller/src/AbstractController.php', + '/libraries/vendor/joomla/controller/src/ControllerInterface.php', + '/media/com_users/js/admin-users-user.es6.js', + '/media/com_users/js/admin-users-user.es6.min.js', + '/media/com_users/js/admin-users-user.es6.min.js.gz', + '/media/com_users/js/admin-users-user.js', + '/media/com_users/js/admin-users-user.min.js', + '/media/com_users/js/admin-users-user.min.js.gz', + // 4.0 from Beta 2 to Beta 3 + '/administrator/templates/atum/images/logo-blue.svg', + '/administrator/templates/atum/images/logo-joomla-blue.svg', + '/administrator/templates/atum/images/logo-joomla-white.svg', + '/administrator/templates/atum/images/logo.svg', + // 4.0 from Beta 3 to Beta 4 + '/components/com_config/src/Model/CmsModel.php', + // 4.0 from Beta 4 to Beta 5 + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-11.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-18.sql', + '/administrator/components/com_config/tmpl/application/default_system.php', + '/administrator/language/en-GB/plg_content_imagelazyload.sys.ini', + '/administrator/language/en-GB/plg_fields_image.ini', + '/administrator/language/en-GB/plg_fields_image.sys.ini', + '/administrator/templates/atum/scss/vendor/bootstrap/_nav.scss', + '/libraries/vendor/spomky-labs/base64url/phpstan.neon', + '/media/plg_system_webauthn/images/webauthn-black.png', + '/media/plg_system_webauthn/images/webauthn-color.png', + '/media/plg_system_webauthn/images/webauthn-white.png', + '/media/system/css/system.min.css', + '/media/system/css/system.min.css.gz', + '/plugins/content/imagelazyload/imagelazyload.php', + '/plugins/content/imagelazyload/imagelazyload.xml', + '/templates/cassiopeia/html/layouts/chromes/cardGrey.php', + '/templates/cassiopeia/html/layouts/chromes/default.php', + '/templates/cassiopeia/scss/vendor/bootstrap/_card.scss', + // 4.0 from Beta 5 to Beta 6 + '/administrator/modules/mod_multilangstatus/src/Helper/MultilangstatusAdminHelper.php', + '/administrator/templates/atum/favicon.ico', + '/libraries/vendor/nyholm/psr7/phpstan.baseline.dist', + '/libraries/vendor/spomky-labs/base64url/.php_cs.dist', + '/libraries/vendor/spomky-labs/base64url/infection.json.dist', + '/media/layouts/js/joomla/html/batch/batch-language.es6.js', + '/media/layouts/js/joomla/html/batch/batch-language.es6.min.js', + '/media/layouts/js/joomla/html/batch/batch-language.es6.min.js.gz', + '/media/layouts/js/joomla/html/batch/batch-language.js', + '/media/layouts/js/joomla/html/batch/batch-language.min.js', + '/media/layouts/js/joomla/html/batch/batch-language.min.js.gz', + '/media/plg_system_webauthn/images/webauthn-black.svg', + '/media/plg_system_webauthn/images/webauthn-white.svg', + '/media/system/js/core.es6/ajax.es6', + '/media/system/js/core.es6/customevent.es6', + '/media/system/js/core.es6/event.es6', + '/media/system/js/core.es6/form.es6', + '/media/system/js/core.es6/message.es6', + '/media/system/js/core.es6/options.es6', + '/media/system/js/core.es6/text.es6', + '/media/system/js/core.es6/token.es6', + '/media/system/js/core.es6/webcomponent.es6', + '/templates/cassiopeia/favicon.ico', + '/templates/cassiopeia/scss/_mixin.scss', + '/templates/cassiopeia/scss/_variables.scss', + '/templates/cassiopeia/scss/blocks/_demo-styling.scss', + // 4.0 from Beta 6 to Beta 7 + '/media/legacy/js/bootstrap-init.js', + '/media/legacy/js/bootstrap-init.min.js', + '/media/legacy/js/bootstrap-init.min.js.gz', + '/media/legacy/js/frontediting.js', + '/media/legacy/js/frontediting.min.js', + '/media/legacy/js/frontediting.min.js.gz', + '/media/vendor/bootstrap/js/bootstrap.bundle.js', + '/media/vendor/bootstrap/js/bootstrap.bundle.min.js', + '/media/vendor/bootstrap/js/bootstrap.bundle.min.js.gz', + '/media/vendor/bootstrap/js/bootstrap.bundle.min.js.map', + '/media/vendor/bootstrap/js/bootstrap.js', + '/media/vendor/bootstrap/js/bootstrap.min.js', + '/media/vendor/bootstrap/js/bootstrap.min.js.gz', + '/media/vendor/bootstrap/scss/_code.scss', + '/media/vendor/bootstrap/scss/_custom-forms.scss', + '/media/vendor/bootstrap/scss/_input-group.scss', + '/media/vendor/bootstrap/scss/_jumbotron.scss', + '/media/vendor/bootstrap/scss/_media.scss', + '/media/vendor/bootstrap/scss/_print.scss', + '/media/vendor/bootstrap/scss/mixins/_background-variant.scss', + '/media/vendor/bootstrap/scss/mixins/_badge.scss', + '/media/vendor/bootstrap/scss/mixins/_float.scss', + '/media/vendor/bootstrap/scss/mixins/_grid-framework.scss', + '/media/vendor/bootstrap/scss/mixins/_hover.scss', + '/media/vendor/bootstrap/scss/mixins/_nav-divider.scss', + '/media/vendor/bootstrap/scss/mixins/_screen-reader.scss', + '/media/vendor/bootstrap/scss/mixins/_size.scss', + '/media/vendor/bootstrap/scss/mixins/_table-row.scss', + '/media/vendor/bootstrap/scss/mixins/_text-emphasis.scss', + '/media/vendor/bootstrap/scss/mixins/_text-hide.scss', + '/media/vendor/bootstrap/scss/mixins/_visibility.scss', + '/media/vendor/bootstrap/scss/utilities/_align.scss', + '/media/vendor/bootstrap/scss/utilities/_background.scss', + '/media/vendor/bootstrap/scss/utilities/_borders.scss', + '/media/vendor/bootstrap/scss/utilities/_clearfix.scss', + '/media/vendor/bootstrap/scss/utilities/_display.scss', + '/media/vendor/bootstrap/scss/utilities/_embed.scss', + '/media/vendor/bootstrap/scss/utilities/_flex.scss', + '/media/vendor/bootstrap/scss/utilities/_float.scss', + '/media/vendor/bootstrap/scss/utilities/_interactions.scss', + '/media/vendor/bootstrap/scss/utilities/_overflow.scss', + '/media/vendor/bootstrap/scss/utilities/_position.scss', + '/media/vendor/bootstrap/scss/utilities/_screenreaders.scss', + '/media/vendor/bootstrap/scss/utilities/_shadows.scss', + '/media/vendor/bootstrap/scss/utilities/_sizing.scss', + '/media/vendor/bootstrap/scss/utilities/_spacing.scss', + '/media/vendor/bootstrap/scss/utilities/_stretched-link.scss', + '/media/vendor/bootstrap/scss/utilities/_text.scss', + '/media/vendor/bootstrap/scss/utilities/_visibility.scss', + '/media/vendor/skipto/css/SkipTo.css', + '/media/vendor/skipto/js/dropMenu.js', + // 4.0 from Beta 7 to RC 1 + '/administrator/components/com_admin/forms/profile.xml', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-07-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-09-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-09-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-10-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-10-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-03-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-04-25.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-05-31.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-06-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-10-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-02-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-07-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-08-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-09-12.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-10-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-01-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-01-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-02-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-03-31.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-05-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-06-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-21.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-23.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-25.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-27.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-10-13.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-10-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-11-07.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-11-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-08.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-11.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-05-21.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-09-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-09-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-12-08.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-12-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-02-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-04-11.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-04-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-04.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-07.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-07-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-09-22.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-09-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-10-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-10-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-03-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-04-25.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-05-31.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-06-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-10-10.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-02-24.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-26.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-07-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-08-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-09-12.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-10-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-01-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-01-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-02-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-03-31.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-05-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-06-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-14.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-14.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-23.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-24.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-25.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-26.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-27.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-10-13.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-10-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-11-07.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-11-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-22.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-05-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-09-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-09-22.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-12-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-12-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-02-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-04-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-04-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-07.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-10.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-21.sql', + '/administrator/components/com_admin/src/Controller/ProfileController.php', + '/administrator/components/com_admin/src/Model/ProfileModel.php', + '/administrator/components/com_admin/src/View/Profile/HtmlView.php', + '/administrator/components/com_admin/tmpl/profile/edit.php', + '/administrator/components/com_config/tmpl/application/default_ftp.php', + '/administrator/components/com_config/tmpl/application/default_ftplogin.php', + '/administrator/components/com_csp/access.xml', + '/administrator/components/com_csp/config.xml', + '/administrator/components/com_csp/csp.xml', + '/administrator/components/com_csp/forms/filter_reports.xml', + '/administrator/components/com_csp/services/provider.php', + '/administrator/components/com_csp/src/Controller/DisplayController.php', + '/administrator/components/com_csp/src/Controller/ReportsController.php', + '/administrator/components/com_csp/src/Helper/ReporterHelper.php', + '/administrator/components/com_csp/src/Model/ReportModel.php', + '/administrator/components/com_csp/src/Model/ReportsModel.php', + '/administrator/components/com_csp/src/Table/ReportTable.php', + '/administrator/components/com_csp/src/View/Reports/HtmlView.php', + '/administrator/components/com_csp/tmpl/reports/default.php', + '/administrator/components/com_csp/tmpl/reports/default.xml', + '/administrator/components/com_fields/src/Field/SubfieldstypeField.php', + '/administrator/components/com_installer/tmpl/installer/default_ftp.php', + '/administrator/components/com_joomlaupdate/src/Helper/Select.php', + '/administrator/language/en-GB/com_csp.ini', + '/administrator/language/en-GB/com_csp.sys.ini', + '/administrator/language/en-GB/plg_fields_subfields.ini', + '/administrator/language/en-GB/plg_fields_subfields.sys.ini', + '/administrator/templates/atum/Service/HTML/Atum.php', + '/components/com_csp/src/Controller/ReportController.php', + '/components/com_menus/src/Controller/DisplayController.php', + '/libraries/vendor/algo26-matthias/idna-convert/CODE_OF_CONDUCT.md', + '/libraries/vendor/algo26-matthias/idna-convert/UPGRADING.md', + '/libraries/vendor/algo26-matthias/idna-convert/docker-compose.yml', + '/libraries/vendor/beberlei/assert/phpstan-code.neon', + '/libraries/vendor/beberlei/assert/phpstan-tests.neon', + '/libraries/vendor/bin/generate-defuse-key', + '/libraries/vendor/bin/var-dump-server', + '/libraries/vendor/bin/yaml-lint', + '/libraries/vendor/brick/math/psalm-baseline.xml', + '/libraries/vendor/doctrine/inflector/phpstan.neon.dist', + '/libraries/vendor/jakeasmith/http_build_url/readme.md', + '/libraries/vendor/nyholm/psr7/src/LowercaseTrait.php', + '/libraries/vendor/ozdemirburak/iris/LICENSE.md', + '/libraries/vendor/ozdemirburak/iris/src/BaseColor.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Factory.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Hex.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Hsl.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Hsla.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Hsv.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Rgb.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Rgba.php', + '/libraries/vendor/ozdemirburak/iris/src/Exceptions/AmbiguousColorString.php', + '/libraries/vendor/ozdemirburak/iris/src/Exceptions/InvalidColorException.php', + '/libraries/vendor/ozdemirburak/iris/src/Helpers/DefinedColor.php', + '/libraries/vendor/ozdemirburak/iris/src/Traits/AlphaTrait.php', + '/libraries/vendor/ozdemirburak/iris/src/Traits/HsTrait.php', + '/libraries/vendor/ozdemirburak/iris/src/Traits/HslTrait.php', + '/libraries/vendor/ozdemirburak/iris/src/Traits/RgbTrait.php', + '/libraries/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey', + '/libraries/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey.asc', + '/libraries/vendor/psr/http-factory/.pullapprove.yml', + '/libraries/vendor/spomky-labs/cbor-php/.php_cs.dist', + '/libraries/vendor/spomky-labs/cbor-php/CODE_OF_CONDUCT.md', + '/libraries/vendor/spomky-labs/cbor-php/infection.json.dist', + '/libraries/vendor/spomky-labs/cbor-php/phpstan.neon', + '/libraries/vendor/typo3/phar-stream-wrapper/_config.yml', + '/libraries/vendor/voku/portable-utf8/SUMMARY.md', + '/libraries/vendor/willdurand/negotiation/src/Negotiation/Match.php', + '/media/com_actionlogs/js/admin-actionlogs-default.es6.js', + '/media/com_actionlogs/js/admin-actionlogs-default.es6.min.js', + '/media/com_actionlogs/js/admin-actionlogs-default.es6.min.js.gz', + '/media/com_associations/js/admin-associations-default.es6.js', + '/media/com_associations/js/admin-associations-default.es6.min.js', + '/media/com_associations/js/admin-associations-default.es6.min.js.gz', + '/media/com_associations/js/admin-associations-modal.es6.js', + '/media/com_associations/js/admin-associations-modal.es6.min.js', + '/media/com_associations/js/admin-associations-modal.es6.min.js.gz', + '/media/com_associations/js/associations-edit.es6.js', + '/media/com_associations/js/associations-edit.es6.min.js', + '/media/com_associations/js/associations-edit.es6.min.js.gz', + '/media/com_banners/js/admin-banner-edit.es6.js', + '/media/com_banners/js/admin-banner-edit.es6.min.js', + '/media/com_banners/js/admin-banner-edit.es6.min.js.gz', + '/media/com_cache/js/admin-cache-default.es6.js', + '/media/com_cache/js/admin-cache-default.es6.min.js', + '/media/com_cache/js/admin-cache-default.es6.min.js.gz', + '/media/com_categories/js/shared-categories-accordion.es6.js', + '/media/com_categories/js/shared-categories-accordion.es6.min.js', + '/media/com_categories/js/shared-categories-accordion.es6.min.js.gz', + '/media/com_config/js/config-default.es6.js', + '/media/com_config/js/config-default.es6.min.js', + '/media/com_config/js/config-default.es6.min.js.gz', + '/media/com_config/js/modules-default.es6.js', + '/media/com_config/js/modules-default.es6.min.js', + '/media/com_config/js/modules-default.es6.min.js.gz', + '/media/com_config/js/templates-default.es6.js', + '/media/com_config/js/templates-default.es6.min.js', + '/media/com_config/js/templates-default.es6.min.js.gz', + '/media/com_contact/js/admin-contacts-modal.es6.js', + '/media/com_contact/js/admin-contacts-modal.es6.min.js', + '/media/com_contact/js/admin-contacts-modal.es6.min.js.gz', + '/media/com_contact/js/contacts-list.es6.js', + '/media/com_contact/js/contacts-list.es6.min.js', + '/media/com_contact/js/contacts-list.es6.min.js.gz', + '/media/com_content/js/admin-article-pagebreak.es6.js', + '/media/com_content/js/admin-article-pagebreak.es6.min.js', + '/media/com_content/js/admin-article-pagebreak.es6.min.js.gz', + '/media/com_content/js/admin-article-readmore.es6.js', + '/media/com_content/js/admin-article-readmore.es6.min.js', + '/media/com_content/js/admin-article-readmore.es6.min.js.gz', + '/media/com_content/js/admin-articles-default-batch-footer.es6.js', + '/media/com_content/js/admin-articles-default-batch-footer.es6.min.js', + '/media/com_content/js/admin-articles-default-batch-footer.es6.min.js.gz', + '/media/com_content/js/admin-articles-default-stage-footer.es6.js', + '/media/com_content/js/admin-articles-default-stage-footer.es6.min.js', + '/media/com_content/js/admin-articles-default-stage-footer.es6.min.js.gz', + '/media/com_content/js/admin-articles-modal.es6.js', + '/media/com_content/js/admin-articles-modal.es6.min.js', + '/media/com_content/js/admin-articles-modal.es6.min.js.gz', + '/media/com_content/js/articles-list.es6.js', + '/media/com_content/js/articles-list.es6.min.js', + '/media/com_content/js/articles-list.es6.min.js.gz', + '/media/com_content/js/form-edit.es6.js', + '/media/com_content/js/form-edit.es6.min.js', + '/media/com_content/js/form-edit.es6.min.js.gz', + '/media/com_contenthistory/js/admin-compare-compare.es6.js', + '/media/com_contenthistory/js/admin-compare-compare.es6.min.js', + '/media/com_contenthistory/js/admin-compare-compare.es6.min.js.gz', + '/media/com_contenthistory/js/admin-history-modal.es6.js', + '/media/com_contenthistory/js/admin-history-modal.es6.min.js', + '/media/com_contenthistory/js/admin-history-modal.es6.min.js.gz', + '/media/com_contenthistory/js/admin-history-versions.es6.js', + '/media/com_contenthistory/js/admin-history-versions.es6.min.js', + '/media/com_contenthistory/js/admin-history-versions.es6.min.js.gz', + '/media/com_cpanel/js/admin-add_module.es6.js', + '/media/com_cpanel/js/admin-add_module.es6.min.js', + '/media/com_cpanel/js/admin-add_module.es6.min.js.gz', + '/media/com_cpanel/js/admin-cpanel-default.es6.js', + '/media/com_cpanel/js/admin-cpanel-default.es6.min.js', + '/media/com_cpanel/js/admin-cpanel-default.es6.min.js.gz', + '/media/com_cpanel/js/admin-system-loader.es6.js', + '/media/com_cpanel/js/admin-system-loader.es6.min.js', + '/media/com_cpanel/js/admin-system-loader.es6.min.js.gz', + '/media/com_fields/js/admin-field-changecontext.es6.js', + '/media/com_fields/js/admin-field-changecontext.es6.min.js', + '/media/com_fields/js/admin-field-changecontext.es6.min.js.gz', + '/media/com_fields/js/admin-field-edit-modal.es6.js', + '/media/com_fields/js/admin-field-edit-modal.es6.min.js', + '/media/com_fields/js/admin-field-edit-modal.es6.min.js.gz', + '/media/com_fields/js/admin-field-edit.es6.js', + '/media/com_fields/js/admin-field-edit.es6.min.js', + '/media/com_fields/js/admin-field-edit.es6.min.js.gz', + '/media/com_fields/js/admin-field-typehaschanged.es6.js', + '/media/com_fields/js/admin-field-typehaschanged.es6.min.js', + '/media/com_fields/js/admin-field-typehaschanged.es6.min.js.gz', + '/media/com_fields/js/admin-fields-default-batch.es6.js', + '/media/com_fields/js/admin-fields-default-batch.es6.min.js', + '/media/com_fields/js/admin-fields-default-batch.es6.min.js.gz', + '/media/com_fields/js/admin-fields-modal.es6.js', + '/media/com_fields/js/admin-fields-modal.es6.min.js', + '/media/com_fields/js/admin-fields-modal.es6.min.js.gz', + '/media/com_finder/js/filters.es6.js', + '/media/com_finder/js/filters.es6.min.js', + '/media/com_finder/js/filters.es6.min.js.gz', + '/media/com_finder/js/finder-edit.es6.js', + '/media/com_finder/js/finder-edit.es6.min.js', + '/media/com_finder/js/finder-edit.es6.min.js.gz', + '/media/com_finder/js/finder.es6.js', + '/media/com_finder/js/finder.es6.min.js', + '/media/com_finder/js/finder.es6.min.js.gz', + '/media/com_finder/js/index.es6.js', + '/media/com_finder/js/index.es6.min.js', + '/media/com_finder/js/index.es6.min.js.gz', + '/media/com_finder/js/indexer.es6.js', + '/media/com_finder/js/indexer.es6.min.js', + '/media/com_finder/js/indexer.es6.min.js.gz', + '/media/com_finder/js/maps.es6.js', + '/media/com_finder/js/maps.es6.min.js', + '/media/com_finder/js/maps.es6.min.js.gz', + '/media/com_installer/js/changelog.es6.js', + '/media/com_installer/js/changelog.es6.min.js', + '/media/com_installer/js/changelog.es6.min.js.gz', + '/media/com_installer/js/installer.es6.js', + '/media/com_installer/js/installer.es6.min.js', + '/media/com_installer/js/installer.es6.min.js.gz', + '/media/com_joomlaupdate/js/admin-update-default.es6.js', + '/media/com_joomlaupdate/js/admin-update-default.es6.min.js', + '/media/com_joomlaupdate/js/admin-update-default.es6.min.js.gz', + '/media/com_languages/js/admin-language-edit-change-flag.es6.js', + '/media/com_languages/js/admin-language-edit-change-flag.es6.min.js', + '/media/com_languages/js/admin-language-edit-change-flag.es6.min.js.gz', + '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.js', + '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.min.js', + '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.min.js.gz', + '/media/com_languages/js/overrider.es6.js', + '/media/com_languages/js/overrider.es6.min.js', + '/media/com_languages/js/overrider.es6.min.js.gz', + '/media/com_mails/js/admin-email-template-edit.es6.js', + '/media/com_mails/js/admin-email-template-edit.es6.min.js', + '/media/com_mails/js/admin-email-template-edit.es6.min.js.gz', + '/media/com_media/css/mediamanager.min.css', + '/media/com_media/css/mediamanager.min.css.gz', + '/media/com_media/css/mediamanager.min.css.map', + '/media/com_media/js/edit-images.es6.js', + '/media/com_media/js/edit-images.es6.min.js', + '/media/com_media/js/mediamanager.min.js', + '/media/com_media/js/mediamanager.min.js.gz', + '/media/com_media/js/mediamanager.min.js.map', + '/media/com_menus/js/admin-item-edit.es6.js', + '/media/com_menus/js/admin-item-edit.es6.min.js', + '/media/com_menus/js/admin-item-edit.es6.min.js.gz', + '/media/com_menus/js/admin-item-edit_container.es6.js', + '/media/com_menus/js/admin-item-edit_container.es6.min.js', + '/media/com_menus/js/admin-item-edit_container.es6.min.js.gz', + '/media/com_menus/js/admin-item-edit_modules.es6.js', + '/media/com_menus/js/admin-item-edit_modules.es6.min.js', + '/media/com_menus/js/admin-item-edit_modules.es6.min.js.gz', + '/media/com_menus/js/admin-item-modal.es6.js', + '/media/com_menus/js/admin-item-modal.es6.min.js', + '/media/com_menus/js/admin-item-modal.es6.min.js.gz', + '/media/com_menus/js/admin-items-modal.es6.js', + '/media/com_menus/js/admin-items-modal.es6.min.js', + '/media/com_menus/js/admin-items-modal.es6.min.js.gz', + '/media/com_menus/js/admin-menus-default.es6.js', + '/media/com_menus/js/admin-menus-default.es6.min.js', + '/media/com_menus/js/admin-menus-default.es6.min.js.gz', + '/media/com_menus/js/default-batch-body.es6.js', + '/media/com_menus/js/default-batch-body.es6.min.js', + '/media/com_menus/js/default-batch-body.es6.min.js.gz', + '/media/com_modules/js/admin-module-edit.es6.js', + '/media/com_modules/js/admin-module-edit.es6.min.js', + '/media/com_modules/js/admin-module-edit.es6.min.js.gz', + '/media/com_modules/js/admin-module-edit_assignment.es6.js', + '/media/com_modules/js/admin-module-edit_assignment.es6.min.js', + '/media/com_modules/js/admin-module-edit_assignment.es6.min.js.gz', + '/media/com_modules/js/admin-module-search.es6.js', + '/media/com_modules/js/admin-module-search.es6.min.js', + '/media/com_modules/js/admin-module-search.es6.min.js.gz', + '/media/com_modules/js/admin-modules-modal.es6.js', + '/media/com_modules/js/admin-modules-modal.es6.min.js', + '/media/com_modules/js/admin-modules-modal.es6.min.js.gz', + '/media/com_modules/js/admin-select-modal.es6.js', + '/media/com_modules/js/admin-select-modal.es6.min.js', + '/media/com_modules/js/admin-select-modal.es6.min.js.gz', + '/media/com_tags/js/tag-default.es6.js', + '/media/com_tags/js/tag-default.es6.min.js', + '/media/com_tags/js/tag-default.es6.min.js.gz', + '/media/com_tags/js/tag-list.es6.js', + '/media/com_tags/js/tag-list.es6.min.js', + '/media/com_tags/js/tag-list.es6.min.js.gz', + '/media/com_tags/js/tags-default.es6.js', + '/media/com_tags/js/tags-default.es6.min.js', + '/media/com_tags/js/tags-default.es6.min.js.gz', + '/media/com_templates/js/admin-template-compare.es6.js', + '/media/com_templates/js/admin-template-compare.es6.min.js', + '/media/com_templates/js/admin-template-compare.es6.min.js.gz', + '/media/com_templates/js/admin-template-toggle-assignment.es6.js', + '/media/com_templates/js/admin-template-toggle-assignment.es6.min.js', + '/media/com_templates/js/admin-template-toggle-assignment.es6.min.js.gz', + '/media/com_templates/js/admin-template-toggle-switch.es6.js', + '/media/com_templates/js/admin-template-toggle-switch.es6.min.js', + '/media/com_templates/js/admin-template-toggle-switch.es6.min.js.gz', + '/media/com_templates/js/admin-templates-default.es6.js', + '/media/com_templates/js/admin-templates-default.es6.min.js', + '/media/com_templates/js/admin-templates-default.es6.min.js.gz', + '/media/com_users/js/admin-users-groups.es6.js', + '/media/com_users/js/admin-users-groups.es6.min.js', + '/media/com_users/js/admin-users-groups.es6.min.js.gz', + '/media/com_users/js/admin-users-mail.es6.js', + '/media/com_users/js/admin-users-mail.es6.min.js', + '/media/com_users/js/admin-users-mail.es6.min.js.gz', + '/media/com_users/js/two-factor-switcher.es6.js', + '/media/com_users/js/two-factor-switcher.es6.min.js', + '/media/com_users/js/two-factor-switcher.es6.min.js.gz', + '/media/com_workflow/js/admin-items-workflow-buttons.es6.js', + '/media/com_workflow/js/admin-items-workflow-buttons.es6.min.js', + '/media/com_workflow/js/admin-items-workflow-buttons.es6.min.js.gz', + '/media/com_wrapper/js/iframe-height.es6.js', + '/media/com_wrapper/js/iframe-height.es6.min.js', + '/media/com_wrapper/js/iframe-height.es6.min.js.gz', + '/media/layouts/js/joomla/form/field/category-change.es6.js', + '/media/layouts/js/joomla/form/field/category-change.es6.min.js', + '/media/layouts/js/joomla/form/field/category-change.es6.min.js.gz', + '/media/layouts/js/joomla/html/batch/batch-copymove.es6.js', + '/media/layouts/js/joomla/html/batch/batch-copymove.es6.min.js', + '/media/layouts/js/joomla/html/batch/batch-copymove.es6.min.js.gz', + '/media/legacy/js/highlighter.js', + '/media/legacy/js/highlighter.min.js', + '/media/legacy/js/highlighter.min.js.gz', + '/media/mod_login/js/admin-login.es6.js', + '/media/mod_login/js/admin-login.es6.min.js', + '/media/mod_login/js/admin-login.es6.min.js.gz', + '/media/mod_menu/js/admin-menu.es6.js', + '/media/mod_menu/js/admin-menu.es6.min.js', + '/media/mod_menu/js/admin-menu.es6.min.js.gz', + '/media/mod_menu/js/menu.es6.js', + '/media/mod_menu/js/menu.es6.min.js', + '/media/mod_menu/js/menu.es6.min.js.gz', + '/media/mod_multilangstatus/js/admin-multilangstatus.es6.js', + '/media/mod_multilangstatus/js/admin-multilangstatus.es6.min.js', + '/media/mod_multilangstatus/js/admin-multilangstatus.es6.min.js.gz', + '/media/mod_quickicon/js/quickicon.es6.js', + '/media/mod_quickicon/js/quickicon.es6.min.js', + '/media/mod_quickicon/js/quickicon.es6.min.js.gz', + '/media/mod_sampledata/js/sampledata-process.es6.js', + '/media/mod_sampledata/js/sampledata-process.es6.min.js', + '/media/mod_sampledata/js/sampledata-process.es6.min.js.gz', + '/media/plg_captcha_recaptcha/js/recaptcha.es6.js', + '/media/plg_captcha_recaptcha/js/recaptcha.es6.min.js', + '/media/plg_captcha_recaptcha/js/recaptcha.es6.min.js.gz', + '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.js', + '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.min.js', + '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.min.js.gz', + '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.js', + '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.min.js', + '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.min.js.gz', + '/media/plg_editors_tinymce/js/tinymce-builder.es6.js', + '/media/plg_editors_tinymce/js/tinymce-builder.es6.min.js', + '/media/plg_editors_tinymce/js/tinymce-builder.es6.min.js.gz', + '/media/plg_editors_tinymce/js/tinymce.es6.js', + '/media/plg_editors_tinymce/js/tinymce.es6.min.js', + '/media/plg_editors_tinymce/js/tinymce.es6.min.js.gz', + '/media/plg_installer_folderinstaller/js/folderinstaller.es6.js', + '/media/plg_installer_folderinstaller/js/folderinstaller.es6.min.js', + '/media/plg_installer_folderinstaller/js/folderinstaller.es6.min.js.gz', + '/media/plg_installer_packageinstaller/js/packageinstaller.es6.js', + '/media/plg_installer_packageinstaller/js/packageinstaller.es6.min.js', + '/media/plg_installer_packageinstaller/js/packageinstaller.es6.min.js.gz', + '/media/plg_installer_urlinstaller/js/urlinstaller.es6.js', + '/media/plg_installer_urlinstaller/js/urlinstaller.es6.min.js', + '/media/plg_installer_urlinstaller/js/urlinstaller.es6.min.js.gz', + '/media/plg_installer_webinstaller/js/client.es6.js', + '/media/plg_installer_webinstaller/js/client.es6.min.js', + '/media/plg_installer_webinstaller/js/client.es6.min.js.gz', + '/media/plg_media-action_crop/js/crop.es6.js', + '/media/plg_media-action_crop/js/crop.es6.min.js', + '/media/plg_media-action_crop/js/crop.es6.min.js.gz', + '/media/plg_media-action_resize/js/resize.es6.js', + '/media/plg_media-action_resize/js/resize.es6.min.js', + '/media/plg_media-action_resize/js/resize.es6.min.js.gz', + '/media/plg_media-action_rotate/js/rotate.es6.js', + '/media/plg_media-action_rotate/js/rotate.es6.min.js', + '/media/plg_media-action_rotate/js/rotate.es6.min.js.gz', + '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.js', + '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.min.js', + '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.min.js.gz', + '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.js', + '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.min.js', + '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.min.js.gz', + '/media/plg_quickicon_overridecheck/js/overridecheck.es6.js', + '/media/plg_quickicon_overridecheck/js/overridecheck.es6.min.js', + '/media/plg_quickicon_overridecheck/js/overridecheck.es6.min.js.gz', + '/media/plg_quickicon_privacycheck/js/privacycheck.es6.js', + '/media/plg_quickicon_privacycheck/js/privacycheck.es6.min.js', + '/media/plg_quickicon_privacycheck/js/privacycheck.es6.min.js.gz', + '/media/plg_system_debug/js/debug.es6.js', + '/media/plg_system_debug/js/debug.es6.min.js', + '/media/plg_system_debug/js/debug.es6.min.js.gz', + '/media/plg_system_highlight/highlight.min.css', + '/media/plg_system_highlight/highlight.min.css.gz', + '/media/plg_system_stats/js/stats-message.es6.js', + '/media/plg_system_stats/js/stats-message.es6.min.js', + '/media/plg_system_stats/js/stats-message.es6.min.js.gz', + '/media/plg_system_stats/js/stats.es6.js', + '/media/plg_system_stats/js/stats.es6.min.js', + '/media/plg_system_stats/js/stats.es6.min.js.gz', + '/media/plg_system_webauthn/js/login.es6.js', + '/media/plg_system_webauthn/js/login.es6.min.js', + '/media/plg_system_webauthn/js/login.es6.min.js.gz', + '/media/plg_system_webauthn/js/management.es6.js', + '/media/plg_system_webauthn/js/management.es6.min.js', + '/media/plg_system_webauthn/js/management.es6.min.js.gz', + '/media/plg_user_token/js/token.es6.js', + '/media/plg_user_token/js/token.es6.min.js', + '/media/plg_user_token/js/token.es6.min.js.gz', + '/media/system/js/core.es6.js', + '/media/system/js/core.es6.min.js', + '/media/system/js/core.es6.min.js.gz', + '/media/system/js/draggable.es6.js', + '/media/system/js/draggable.es6.min.js', + '/media/system/js/draggable.es6.min.js.gz', + '/media/system/js/fields/joomla-field-color-slider.es6.js', + '/media/system/js/fields/joomla-field-color-slider.es6.min.js', + '/media/system/js/fields/joomla-field-color-slider.es6.min.js.gz', + '/media/system/js/fields/passwordstrength.es6.js', + '/media/system/js/fields/passwordstrength.es6.min.js', + '/media/system/js/fields/passwordstrength.es6.min.js.gz', + '/media/system/js/fields/passwordview.es6.js', + '/media/system/js/fields/passwordview.es6.min.js', + '/media/system/js/fields/passwordview.es6.min.js.gz', + '/media/system/js/fields/select-colour.es6.js', + '/media/system/js/fields/select-colour.es6.min.js', + '/media/system/js/fields/select-colour.es6.min.js.gz', + '/media/system/js/fields/validate.es6.js', + '/media/system/js/fields/validate.es6.min.js', + '/media/system/js/fields/validate.es6.min.js.gz', + '/media/system/js/keepalive.es6.js', + '/media/system/js/keepalive.es6.min.js', + '/media/system/js/keepalive.es6.min.js.gz', + '/media/system/js/multiselect.es6.js', + '/media/system/js/multiselect.es6.min.js', + '/media/system/js/multiselect.es6.min.js.gz', + '/media/system/js/searchtools.es6.js', + '/media/system/js/searchtools.es6.min.js', + '/media/system/js/searchtools.es6.min.js.gz', + '/media/system/js/showon.es6.js', + '/media/system/js/showon.es6.min.js', + '/media/system/js/showon.es6.min.js.gz', + '/media/templates/atum/js/template.es6.js', + '/media/templates/atum/js/template.es6.min.js', + '/media/templates/atum/js/template.es6.min.js.gz', + '/media/templates/atum/js/template.js', + '/media/templates/atum/js/template.min.js', + '/media/templates/atum/js/template.min.js.gz', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.min.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.min.js.gz', + '/media/vendor/bootstrap/js/alert.es6.js', + '/media/vendor/bootstrap/js/alert.es6.min.js', + '/media/vendor/bootstrap/js/alert.es6.min.js.gz', + '/media/vendor/bootstrap/js/bootstrap.es5.js', + '/media/vendor/bootstrap/js/bootstrap.es5.min.js', + '/media/vendor/bootstrap/js/bootstrap.es5.min.js.gz', + '/media/vendor/bootstrap/js/button.es6.js', + '/media/vendor/bootstrap/js/button.es6.min.js', + '/media/vendor/bootstrap/js/button.es6.min.js.gz', + '/media/vendor/bootstrap/js/carousel.es6.js', + '/media/vendor/bootstrap/js/carousel.es6.min.js', + '/media/vendor/bootstrap/js/carousel.es6.min.js.gz', + '/media/vendor/bootstrap/js/collapse.es6.js', + '/media/vendor/bootstrap/js/collapse.es6.min.js', + '/media/vendor/bootstrap/js/collapse.es6.min.js.gz', + '/media/vendor/bootstrap/js/dom-8eef6b5f.js', + '/media/vendor/bootstrap/js/dropdown.es6.js', + '/media/vendor/bootstrap/js/dropdown.es6.min.js', + '/media/vendor/bootstrap/js/dropdown.es6.min.js.gz', + '/media/vendor/bootstrap/js/modal.es6.js', + '/media/vendor/bootstrap/js/modal.es6.min.js', + '/media/vendor/bootstrap/js/modal.es6.min.js.gz', + '/media/vendor/bootstrap/js/popover.es6.js', + '/media/vendor/bootstrap/js/popover.es6.min.js', + '/media/vendor/bootstrap/js/popover.es6.min.js.gz', + '/media/vendor/bootstrap/js/popper-5304749a.js', + '/media/vendor/bootstrap/js/scrollspy.es6.js', + '/media/vendor/bootstrap/js/scrollspy.es6.min.js', + '/media/vendor/bootstrap/js/scrollspy.es6.min.js.gz', + '/media/vendor/bootstrap/js/tab.es6.js', + '/media/vendor/bootstrap/js/tab.es6.min.js', + '/media/vendor/bootstrap/js/tab.es6.min.js.gz', + '/media/vendor/bootstrap/js/toast.es6.js', + '/media/vendor/bootstrap/js/toast.es6.min.js', + '/media/vendor/bootstrap/js/toast.es6.min.js.gz', + '/media/vendor/codemirror/lib/codemirror-ce.js', + '/media/vendor/codemirror/lib/codemirror-ce.min.js', + '/media/vendor/codemirror/lib/codemirror-ce.min.js.gz', + '/media/vendor/punycode/js/punycode.js', + '/media/vendor/punycode/js/punycode.min.js', + '/media/vendor/punycode/js/punycode.min.js.gz', + '/media/vendor/tinymce/changelog.txt', + '/media/vendor/webcomponentsjs/js/webcomponents-ce.js', + '/media/vendor/webcomponentsjs/js/webcomponents-ce.min.js', + '/media/vendor/webcomponentsjs/js/webcomponents-ce.min.js.gz', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.min.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.min.js.gz', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.min.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.min.js.gz', + '/media/vendor/webcomponentsjs/js/webcomponents-sd.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd.min.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd.min.js.gz', + '/plugins/fields/subfields/params/subfields.xml', + '/plugins/fields/subfields/subfields.php', + '/plugins/fields/subfields/subfields.xml', + '/plugins/fields/subfields/tmpl/subfields.php', + '/templates/cassiopeia/images/system/rating_star.png', + '/templates/cassiopeia/images/system/rating_star_blank.png', + '/templates/cassiopeia/scss/tools/mixins/_margin.scss', + '/templates/cassiopeia/scss/tools/mixins/_visually-hidden.scss', + '/templates/system/js/error-locales.js', + // 4.0 from RC 1 to RC 2 + '/administrator/components/com_fields/tmpl/field/modal.php', + '/administrator/templates/atum/scss/pages/_com_admin.scss', + '/administrator/templates/atum/scss/pages/_com_finder.scss', + '/libraries/src/Error/JsonApi/InstallLanguageExceptionHandler.php', + '/libraries/src/MVC/Controller/Exception/InstallLanguage.php', + '/media/com_fields/js/admin-field-edit-modal-es5.js', + '/media/com_fields/js/admin-field-edit-modal-es5.min.js', + '/media/com_fields/js/admin-field-edit-modal-es5.min.js.gz', + '/media/com_fields/js/admin-field-edit-modal.js', + '/media/com_fields/js/admin-field-edit-modal.min.js', + '/media/com_fields/js/admin-field-edit-modal.min.js.gz', + // 4.0 from RC 3 to RC 4 + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_nodownload.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_noupdate.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_preupdatecheck.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_reinstall.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_update.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_updatemefirst.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_upload.php', + '/language/en-GB/com_messages.ini', + '/media/system/css/fields/joomla-image-select.css', + '/media/system/css/fields/joomla-image-select.min.css', + '/media/system/css/fields/joomla-image-select.min.css.gz', + '/media/system/js/fields/joomla-image-select-es5.js', + '/media/system/js/fields/joomla-image-select-es5.min.js', + '/media/system/js/fields/joomla-image-select-es5.min.js.gz', + '/media/system/js/fields/joomla-image-select.js', + '/media/system/js/fields/joomla-image-select.min.js', + '/media/system/js/fields/joomla-image-select.min.js.gz', + // 4.0 from RC 4 to RC 5 + '/media/system/js/fields/calendar-locales/af.min.js', + '/media/system/js/fields/calendar-locales/af.min.js.gz', + '/media/system/js/fields/calendar-locales/ar.min.js', + '/media/system/js/fields/calendar-locales/ar.min.js.gz', + '/media/system/js/fields/calendar-locales/bg.min.js', + '/media/system/js/fields/calendar-locales/bg.min.js.gz', + '/media/system/js/fields/calendar-locales/bn.min.js', + '/media/system/js/fields/calendar-locales/bn.min.js.gz', + '/media/system/js/fields/calendar-locales/bs.min.js', + '/media/system/js/fields/calendar-locales/bs.min.js.gz', + '/media/system/js/fields/calendar-locales/ca.min.js', + '/media/system/js/fields/calendar-locales/ca.min.js.gz', + '/media/system/js/fields/calendar-locales/cs.min.js', + '/media/system/js/fields/calendar-locales/cs.min.js.gz', + '/media/system/js/fields/calendar-locales/cy.min.js', + '/media/system/js/fields/calendar-locales/cy.min.js.gz', + '/media/system/js/fields/calendar-locales/da.min.js', + '/media/system/js/fields/calendar-locales/da.min.js.gz', + '/media/system/js/fields/calendar-locales/de.min.js', + '/media/system/js/fields/calendar-locales/de.min.js.gz', + '/media/system/js/fields/calendar-locales/el.min.js', + '/media/system/js/fields/calendar-locales/el.min.js.gz', + '/media/system/js/fields/calendar-locales/en.min.js', + '/media/system/js/fields/calendar-locales/en.min.js.gz', + '/media/system/js/fields/calendar-locales/es.min.js', + '/media/system/js/fields/calendar-locales/es.min.js.gz', + '/media/system/js/fields/calendar-locales/eu.min.js', + '/media/system/js/fields/calendar-locales/eu.min.js.gz', + '/media/system/js/fields/calendar-locales/fa-ir.min.js', + '/media/system/js/fields/calendar-locales/fa-ir.min.js.gz', + '/media/system/js/fields/calendar-locales/fi.min.js', + '/media/system/js/fields/calendar-locales/fi.min.js.gz', + '/media/system/js/fields/calendar-locales/fr.min.js', + '/media/system/js/fields/calendar-locales/fr.min.js.gz', + '/media/system/js/fields/calendar-locales/ga.min.js', + '/media/system/js/fields/calendar-locales/ga.min.js.gz', + '/media/system/js/fields/calendar-locales/hr.min.js', + '/media/system/js/fields/calendar-locales/hr.min.js.gz', + '/media/system/js/fields/calendar-locales/hu.min.js', + '/media/system/js/fields/calendar-locales/hu.min.js.gz', + '/media/system/js/fields/calendar-locales/it.min.js', + '/media/system/js/fields/calendar-locales/it.min.js.gz', + '/media/system/js/fields/calendar-locales/ja.min.js', + '/media/system/js/fields/calendar-locales/ja.min.js.gz', + '/media/system/js/fields/calendar-locales/ka.min.js', + '/media/system/js/fields/calendar-locales/ka.min.js.gz', + '/media/system/js/fields/calendar-locales/kk.min.js', + '/media/system/js/fields/calendar-locales/kk.min.js.gz', + '/media/system/js/fields/calendar-locales/ko.min.js', + '/media/system/js/fields/calendar-locales/ko.min.js.gz', + '/media/system/js/fields/calendar-locales/lt.min.js', + '/media/system/js/fields/calendar-locales/lt.min.js.gz', + '/media/system/js/fields/calendar-locales/mk.min.js', + '/media/system/js/fields/calendar-locales/mk.min.js.gz', + '/media/system/js/fields/calendar-locales/nb.min.js', + '/media/system/js/fields/calendar-locales/nb.min.js.gz', + '/media/system/js/fields/calendar-locales/nl.min.js', + '/media/system/js/fields/calendar-locales/nl.min.js.gz', + '/media/system/js/fields/calendar-locales/pl.min.js', + '/media/system/js/fields/calendar-locales/pl.min.js.gz', + '/media/system/js/fields/calendar-locales/prs-af.min.js', + '/media/system/js/fields/calendar-locales/prs-af.min.js.gz', + '/media/system/js/fields/calendar-locales/pt.min.js', + '/media/system/js/fields/calendar-locales/pt.min.js.gz', + '/media/system/js/fields/calendar-locales/ru.min.js', + '/media/system/js/fields/calendar-locales/ru.min.js.gz', + '/media/system/js/fields/calendar-locales/sk.min.js', + '/media/system/js/fields/calendar-locales/sk.min.js.gz', + '/media/system/js/fields/calendar-locales/sl.min.js', + '/media/system/js/fields/calendar-locales/sl.min.js.gz', + '/media/system/js/fields/calendar-locales/sr-rs.min.js', + '/media/system/js/fields/calendar-locales/sr-rs.min.js.gz', + '/media/system/js/fields/calendar-locales/sr-yu.min.js', + '/media/system/js/fields/calendar-locales/sr-yu.min.js.gz', + '/media/system/js/fields/calendar-locales/sv.min.js', + '/media/system/js/fields/calendar-locales/sv.min.js.gz', + '/media/system/js/fields/calendar-locales/sw.min.js', + '/media/system/js/fields/calendar-locales/sw.min.js.gz', + '/media/system/js/fields/calendar-locales/ta.min.js', + '/media/system/js/fields/calendar-locales/ta.min.js.gz', + '/media/system/js/fields/calendar-locales/th.min.js', + '/media/system/js/fields/calendar-locales/th.min.js.gz', + '/media/system/js/fields/calendar-locales/uk.min.js', + '/media/system/js/fields/calendar-locales/uk.min.js.gz', + '/media/system/js/fields/calendar-locales/zh-CN.min.js', + '/media/system/js/fields/calendar-locales/zh-CN.min.js.gz', + '/media/system/js/fields/calendar-locales/zh-TW.min.js', + '/media/system/js/fields/calendar-locales/zh-TW.min.js.gz', + // 4.0 from RC 5 to RC 6 + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.min.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.min.js.gz', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.min.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.min.js.gz', + '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.css', + '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.min.css', + '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.min.css.gz', + '/templates/cassiopeia/scss/vendor/fontawesome-free/fontawesome.scss', + // 4.0 from RC 6 to 4.0.0 (stable) + '/libraries/vendor/algo26-matthias/idna-convert/tests/integration/ToIdnTest.php', + '/libraries/vendor/algo26-matthias/idna-convert/tests/integration/ToUnicodeTest.php', + '/libraries/vendor/algo26-matthias/idna-convert/tests/unit/.gitkeep', + '/libraries/vendor/algo26-matthias/idna-convert/tests/unit/namePrepTest.php', + '/libraries/vendor/doctrine/inflector/docs/en/index.rst', + '/libraries/vendor/jakeasmith/http_build_url/tests/HttpBuildUrlTest.php', + '/libraries/vendor/jakeasmith/http_build_url/tests/bootstrap.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/AcceptLanguageTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/AcceptTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/BaseAcceptTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/CharsetNegotiatorTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/EncodingNegotiatorTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/LanguageNegotiatorTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/MatchTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/NegotiatorTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/TestCase.php', + '/libraries/vendor/willdurand/negotiation/tests/bootstrap.php', + // From 4.0.2 to 4.0.3 + '/templates/cassiopeia/css/global/fonts-web_fira-sans.css', + '/templates/cassiopeia/css/global/fonts-web_fira-sans.min.css', + '/templates/cassiopeia/css/global/fonts-web_fira-sans.min.css.gz', + '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.css', + '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.min.css', + '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.min.css.gz', + '/templates/cassiopeia/scss/global/fonts-web_fira-sans.scss', + '/templates/cassiopeia/scss/global/fonts-web_roboto+noto-sans.scss', + // From 4.0.3 to 4.0.4 + '/administrator/templates/atum/scss/_mixin.scss', + '/media/com_joomlaupdate/js/encryption.min.js.gz', + '/media/com_joomlaupdate/js/update.min.js.gz', + '/templates/cassiopeia/images/system/sort_asc.png', + '/templates/cassiopeia/images/system/sort_desc.png', + // From 4.0.4 to 4.0.5 + '/media/vendor/codemirror/lib/#codemirror.js#', + // From 4.0.5 to 4.0.6 + '/media/vendor/mediaelement/css/mejs-controls.png', + // From 4.0.x to 4.1.0-beta1 + '/administrator/templates/atum/css/system/searchtools/searchtools.css', + '/administrator/templates/atum/css/system/searchtools/searchtools.min.css', + '/administrator/templates/atum/css/system/searchtools/searchtools.min.css.gz', + '/administrator/templates/atum/css/template-rtl.css', + '/administrator/templates/atum/css/template-rtl.min.css', + '/administrator/templates/atum/css/template-rtl.min.css.gz', + '/administrator/templates/atum/css/template.css', + '/administrator/templates/atum/css/template.min.css', + '/administrator/templates/atum/css/template.min.css.gz', + '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.css', + '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.min.css', + '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.min.css.gz', + '/administrator/templates/atum/css/vendor/choicesjs/choices.css', + '/administrator/templates/atum/css/vendor/choicesjs/choices.min.css', + '/administrator/templates/atum/css/vendor/choicesjs/choices.min.css.gz', + '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.css', + '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.min.css', + '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.min.css.gz', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.css', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.min.css', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.min.css.gz', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.css', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.min.css', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.min.css.gz', + '/administrator/templates/atum/css/vendor/minicolors/minicolors.css', + '/administrator/templates/atum/css/vendor/minicolors/minicolors.min.css', + '/administrator/templates/atum/css/vendor/minicolors/minicolors.min.css.gz', + '/administrator/templates/atum/images/joomla-pattern.svg', + '/administrator/templates/atum/images/logos/brand-large.svg', + '/administrator/templates/atum/images/logos/brand-small.svg', + '/administrator/templates/atum/images/logos/login.svg', + '/administrator/templates/atum/images/select-bg-active-rtl.svg', + '/administrator/templates/atum/images/select-bg-active.svg', + '/administrator/templates/atum/images/select-bg-rtl.svg', + '/administrator/templates/atum/images/select-bg.svg', + '/administrator/templates/atum/scss/_root.scss', + '/administrator/templates/atum/scss/_variables.scss', + '/administrator/templates/atum/scss/blocks/_alerts.scss', + '/administrator/templates/atum/scss/blocks/_edit.scss', + '/administrator/templates/atum/scss/blocks/_form.scss', + '/administrator/templates/atum/scss/blocks/_global.scss', + '/administrator/templates/atum/scss/blocks/_header.scss', + '/administrator/templates/atum/scss/blocks/_icons.scss', + '/administrator/templates/atum/scss/blocks/_iframe.scss', + '/administrator/templates/atum/scss/blocks/_layout.scss', + '/administrator/templates/atum/scss/blocks/_lists.scss', + '/administrator/templates/atum/scss/blocks/_login.scss', + '/administrator/templates/atum/scss/blocks/_modals.scss', + '/administrator/templates/atum/scss/blocks/_quickicons.scss', + '/administrator/templates/atum/scss/blocks/_sidebar-nav.scss', + '/administrator/templates/atum/scss/blocks/_sidebar.scss', + '/administrator/templates/atum/scss/blocks/_switcher.scss', + '/administrator/templates/atum/scss/blocks/_toolbar.scss', + '/administrator/templates/atum/scss/blocks/_treeselect.scss', + '/administrator/templates/atum/scss/blocks/_utilities.scss', + '/administrator/templates/atum/scss/pages/_com_config.scss', + '/administrator/templates/atum/scss/pages/_com_content.scss', + '/administrator/templates/atum/scss/pages/_com_cpanel.scss', + '/administrator/templates/atum/scss/pages/_com_joomlaupdate.scss', + '/administrator/templates/atum/scss/pages/_com_modules.scss', + '/administrator/templates/atum/scss/pages/_com_privacy.scss', + '/administrator/templates/atum/scss/pages/_com_tags.scss', + '/administrator/templates/atum/scss/pages/_com_templates.scss', + '/administrator/templates/atum/scss/pages/_com_users.scss', + '/administrator/templates/atum/scss/system/searchtools/searchtools.scss', + '/administrator/templates/atum/scss/template-rtl.scss', + '/administrator/templates/atum/scss/template.scss', + '/administrator/templates/atum/scss/vendor/_bootstrap.scss', + '/administrator/templates/atum/scss/vendor/_codemirror.scss', + '/administrator/templates/atum/scss/vendor/_dragula.scss', + '/administrator/templates/atum/scss/vendor/_tinymce.scss', + '/administrator/templates/atum/scss/vendor/awesomplete/awesomplete.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_badge.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_bootstrap-rtl.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_buttons.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_card.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_collapse.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_custom-forms.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_dropdown.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_form.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_lists.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_modal.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_pagination.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_reboot.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_table.scss', + '/administrator/templates/atum/scss/vendor/choicesjs/choices.scss', + '/administrator/templates/atum/scss/vendor/fontawesome-free/fontawesome.scss', + '/administrator/templates/atum/scss/vendor/joomla-custom-elements/joomla-alert.scss', + '/administrator/templates/atum/scss/vendor/joomla-custom-elements/joomla-tab.scss', + '/administrator/templates/atum/scss/vendor/minicolors/minicolors.scss', + '/administrator/templates/atum/template_preview.png', + '/administrator/templates/atum/template_thumbnail.png', + '/administrator/templates/system/css/error.css', + '/administrator/templates/system/css/error.min.css', + '/administrator/templates/system/css/error.min.css.gz', + '/administrator/templates/system/css/system.css', + '/administrator/templates/system/css/system.min.css', + '/administrator/templates/system/css/system.min.css.gz', + '/administrator/templates/system/images/calendar.png', + '/administrator/templates/system/scss/error.scss', + '/administrator/templates/system/scss/system.scss', + '/templates/cassiopeia/css/editor.css', + '/templates/cassiopeia/css/editor.min.css', + '/templates/cassiopeia/css/editor.min.css.gz', + '/templates/cassiopeia/css/global/colors_alternative.css', + '/templates/cassiopeia/css/global/colors_alternative.min.css', + '/templates/cassiopeia/css/global/colors_alternative.min.css.gz', + '/templates/cassiopeia/css/global/colors_standard.css', + '/templates/cassiopeia/css/global/colors_standard.min.css', + '/templates/cassiopeia/css/global/colors_standard.min.css.gz', + '/templates/cassiopeia/css/global/fonts-local_roboto.css', + '/templates/cassiopeia/css/global/fonts-local_roboto.min.css', + '/templates/cassiopeia/css/global/fonts-local_roboto.min.css.gz', + '/templates/cassiopeia/css/offline.css', + '/templates/cassiopeia/css/offline.min.css', + '/templates/cassiopeia/css/offline.min.css.gz', + '/templates/cassiopeia/css/system/searchtools/searchtools.css', + '/templates/cassiopeia/css/system/searchtools/searchtools.min.css', + '/templates/cassiopeia/css/system/searchtools/searchtools.min.css.gz', + '/templates/cassiopeia/css/template-rtl.css', + '/templates/cassiopeia/css/template-rtl.min.css', + '/templates/cassiopeia/css/template-rtl.min.css.gz', + '/templates/cassiopeia/css/template.css', + '/templates/cassiopeia/css/template.min.css', + '/templates/cassiopeia/css/template.min.css.gz', + '/templates/cassiopeia/css/vendor/choicesjs/choices.css', + '/templates/cassiopeia/css/vendor/choicesjs/choices.min.css', + '/templates/cassiopeia/css/vendor/choicesjs/choices.min.css.gz', + '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.css', + '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.min.css', + '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.min.css.gz', + '/templates/cassiopeia/images/logo.svg', + '/templates/cassiopeia/images/select-bg-active-rtl.svg', + '/templates/cassiopeia/images/select-bg-active.svg', + '/templates/cassiopeia/images/select-bg-rtl.svg', + '/templates/cassiopeia/images/select-bg.svg', + '/templates/cassiopeia/js/template.es5.js', + '/templates/cassiopeia/js/template.js', + '/templates/cassiopeia/js/template.min.js', + '/templates/cassiopeia/js/template.min.js.gz', + '/templates/cassiopeia/scss/blocks/_alerts.scss', + '/templates/cassiopeia/scss/blocks/_back-to-top.scss', + '/templates/cassiopeia/scss/blocks/_banner.scss', + '/templates/cassiopeia/scss/blocks/_css-grid.scss', + '/templates/cassiopeia/scss/blocks/_footer.scss', + '/templates/cassiopeia/scss/blocks/_form.scss', + '/templates/cassiopeia/scss/blocks/_frontend-edit.scss', + '/templates/cassiopeia/scss/blocks/_global.scss', + '/templates/cassiopeia/scss/blocks/_header.scss', + '/templates/cassiopeia/scss/blocks/_icons.scss', + '/templates/cassiopeia/scss/blocks/_iframe.scss', + '/templates/cassiopeia/scss/blocks/_layout.scss', + '/templates/cassiopeia/scss/blocks/_legacy.scss', + '/templates/cassiopeia/scss/blocks/_modals.scss', + '/templates/cassiopeia/scss/blocks/_modifiers.scss', + '/templates/cassiopeia/scss/blocks/_tags.scss', + '/templates/cassiopeia/scss/blocks/_toolbar.scss', + '/templates/cassiopeia/scss/blocks/_utilities.scss', + '/templates/cassiopeia/scss/editor.scss', + '/templates/cassiopeia/scss/global/colors_alternative.scss', + '/templates/cassiopeia/scss/global/colors_standard.scss', + '/templates/cassiopeia/scss/global/fonts-local_roboto.scss', + '/templates/cassiopeia/scss/offline.scss', + '/templates/cassiopeia/scss/system/searchtools/searchtools.scss', + '/templates/cassiopeia/scss/template-rtl.scss', + '/templates/cassiopeia/scss/template.scss', + '/templates/cassiopeia/scss/tools/_tools.scss', + '/templates/cassiopeia/scss/tools/functions/_max-width.scss', + '/templates/cassiopeia/scss/tools/variables/_variables.scss', + '/templates/cassiopeia/scss/vendor/_awesomplete.scss', + '/templates/cassiopeia/scss/vendor/_chosen.scss', + '/templates/cassiopeia/scss/vendor/_dragula.scss', + '/templates/cassiopeia/scss/vendor/_minicolors.scss', + '/templates/cassiopeia/scss/vendor/_tinymce.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_bootstrap-rtl.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_buttons.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_collapse.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_custom-forms.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_dropdown.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_forms.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_lists.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_modal.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_nav.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_pagination.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_table.scss', + '/templates/cassiopeia/scss/vendor/choicesjs/choices.scss', + '/templates/cassiopeia/scss/vendor/joomla-custom-elements/joomla-alert.scss', + '/templates/cassiopeia/scss/vendor/metismenu/_metismenu.scss', + '/templates/cassiopeia/template_preview.png', + '/templates/cassiopeia/template_thumbnail.png', + '/templates/system/css/editor.css', + '/templates/system/css/editor.min.css', + '/templates/system/css/editor.min.css.gz', + '/templates/system/css/error.css', + '/templates/system/css/error.min.css', + '/templates/system/css/error.min.css.gz', + '/templates/system/css/error_rtl.css', + '/templates/system/css/error_rtl.min.css', + '/templates/system/css/error_rtl.min.css.gz', + '/templates/system/css/general.css', + '/templates/system/css/general.min.css', + '/templates/system/css/general.min.css.gz', + '/templates/system/css/offline.css', + '/templates/system/css/offline.min.css', + '/templates/system/css/offline.min.css.gz', + '/templates/system/css/offline_rtl.css', + '/templates/system/css/offline_rtl.min.css', + '/templates/system/css/offline_rtl.min.css.gz', + '/templates/system/scss/editor.scss', + '/templates/system/scss/error.scss', + '/templates/system/scss/error_rtl.scss', + '/templates/system/scss/general.scss', + '/templates/system/scss/offline.scss', + '/templates/system/scss/offline_rtl.scss', + // From 4.1.0-beta3 to 4.1.0-rc1 + '/api/components/com_media/src/Helper/AdapterTrait.php', + // From 4.1.0 to 4.1.1 + '/libraries/vendor/tobscure/json-api/.git/HEAD', + '/libraries/vendor/tobscure/json-api/.git/ORIG_HEAD', + '/libraries/vendor/tobscure/json-api/.git/config', + '/libraries/vendor/tobscure/json-api/.git/description', + '/libraries/vendor/tobscure/json-api/.git/hooks/applypatch-msg.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/commit-msg.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/fsmonitor-watchman.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/post-update.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-applypatch.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-commit.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-merge-commit.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-push.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-rebase.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-receive.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/prepare-commit-msg.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/push-to-checkout.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/update.sample', + '/libraries/vendor/tobscure/json-api/.git/index', + '/libraries/vendor/tobscure/json-api/.git/info/exclude', + '/libraries/vendor/tobscure/json-api/.git/info/refs', + '/libraries/vendor/tobscure/json-api/.git/logs/HEAD', + '/libraries/vendor/tobscure/json-api/.git/logs/refs/heads/joomla-backports', + '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes/origin/HEAD', + '/libraries/vendor/tobscure/json-api/.git/objects/info/packs', + '/libraries/vendor/tobscure/json-api/.git/objects/pack/pack-51530cba04703b17f3c11b9e8458a171092cf5e3.idx', + '/libraries/vendor/tobscure/json-api/.git/objects/pack/pack-51530cba04703b17f3c11b9e8458a171092cf5e3.pack', + '/libraries/vendor/tobscure/json-api/.git/packed-refs', + '/libraries/vendor/tobscure/json-api/.git/refs/heads/joomla-backports', + '/libraries/vendor/tobscure/json-api/.git/refs/remotes/origin/HEAD', + '/libraries/vendor/tobscure/json-api/.php_cs', + '/libraries/vendor/tobscure/json-api/tests/AbstractSerializerTest.php', + '/libraries/vendor/tobscure/json-api/tests/AbstractTestCase.php', + '/libraries/vendor/tobscure/json-api/tests/CollectionTest.php', + '/libraries/vendor/tobscure/json-api/tests/DocumentTest.php', + '/libraries/vendor/tobscure/json-api/tests/ErrorHandlerTest.php', + '/libraries/vendor/tobscure/json-api/tests/Exception/Handler/FallbackExceptionHandlerTest.php', + '/libraries/vendor/tobscure/json-api/tests/Exception/Handler/InvalidParameterExceptionHandlerTest.php', + '/libraries/vendor/tobscure/json-api/tests/LinksTraitTest.php', + '/libraries/vendor/tobscure/json-api/tests/ParametersTest.php', + '/libraries/vendor/tobscure/json-api/tests/ResourceTest.php', + '/libraries/vendor/tobscure/json-api/tests/UtilTest.php', + // From 4.1.1 to 4.1.2 + '/administrator/components/com_users/src/Field/PrimaryauthprovidersField.php', + // From 4.1.2 to 4.1.3 + '/libraries/vendor/webmozart/assert/.php_cs', + // From 4.1.3 to 4.1.4 + '/libraries/vendor/maximebf/debugbar/.bowerrc', + '/libraries/vendor/maximebf/debugbar/bower.json', + '/libraries/vendor/maximebf/debugbar/build/namespaceFontAwesome.php', + '/libraries/vendor/maximebf/debugbar/demo/ajax.php', + '/libraries/vendor/maximebf/debugbar/demo/ajax_exception.php', + '/libraries/vendor/maximebf/debugbar/demo/bootstrap.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/cachecache/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/bootstrap.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/build.sh', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/cli-config.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src/Demo/Product.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/monolog/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/build.properties', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/build.sh', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/runtime-conf.xml', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/schema.xml', + '/libraries/vendor/maximebf/debugbar/demo/bridge/slim/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/swiftmailer/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/foobar.html', + '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/hello.html', + '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/index.php', + '/libraries/vendor/maximebf/debugbar/demo/dump_assets.php', + '/libraries/vendor/maximebf/debugbar/demo/index.php', + '/libraries/vendor/maximebf/debugbar/demo/open.php', + '/libraries/vendor/maximebf/debugbar/demo/pdo.php', + '/libraries/vendor/maximebf/debugbar/demo/stack.php', + '/libraries/vendor/maximebf/debugbar/docs/ajax_and_stack.md', + '/libraries/vendor/maximebf/debugbar/docs/base_collectors.md', + '/libraries/vendor/maximebf/debugbar/docs/bridge_collectors.md', + '/libraries/vendor/maximebf/debugbar/docs/data_collectors.md', + '/libraries/vendor/maximebf/debugbar/docs/data_formatter.md', + '/libraries/vendor/maximebf/debugbar/docs/http_drivers.md', + '/libraries/vendor/maximebf/debugbar/docs/javascript_bar.md', + '/libraries/vendor/maximebf/debugbar/docs/manifest.json', + '/libraries/vendor/maximebf/debugbar/docs/openhandler.md', + '/libraries/vendor/maximebf/debugbar/docs/rendering.md', + '/libraries/vendor/maximebf/debugbar/docs/screenshot.png', + '/libraries/vendor/maximebf/debugbar/docs/storage.md', + '/libraries/vendor/maximebf/debugbar/docs/style.css', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/AggregatedCollectorTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/ConfigCollectorTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/MessagesCollectorTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/MockCollector.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/Propel2CollectorTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/TimeDataCollectorTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter/DataFormatterTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter/DebugBarVarDumperTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DebugBarTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DebugBarTestCase.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/JavascriptRendererTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/MockHttpDriver.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/OpenHandlerTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage/FileStorageTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage/MockStorage.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/TracedStatementTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/full_init.html', + '/libraries/vendor/maximebf/debugbar/tests/bootstrap.php', + // From 4.1 to 4.2.0-beta1 + '/libraries/src/Service/Provider/ApiRouter.php', + '/libraries/vendor/nyholm/psr7/doc/final.md', + '/media/com_finder/js/index-es5.js', + '/media/com_finder/js/index-es5.min.js', + '/media/com_finder/js/index-es5.min.js.gz', + '/media/com_finder/js/index.js', + '/media/com_finder/js/index.min.js', + '/media/com_finder/js/index.min.js.gz', + '/media/com_users/js/two-factor-switcher-es5.js', + '/media/com_users/js/two-factor-switcher-es5.min.js', + '/media/com_users/js/two-factor-switcher-es5.min.js.gz', + '/media/com_users/js/two-factor-switcher.js', + '/media/com_users/js/two-factor-switcher.min.js', + '/media/com_users/js/two-factor-switcher.min.js.gz', + '/modules/mod_articles_news/mod_articles_news.php', + '/plugins/actionlog/joomla/joomla.php', + '/plugins/api-authentication/basic/basic.php', + '/plugins/api-authentication/token/token.php', + '/plugins/system/cache/cache.php', + '/plugins/twofactorauth/totp/postinstall/actions.php', + '/plugins/twofactorauth/totp/tmpl/form.php', + '/plugins/twofactorauth/totp/totp.php', + '/plugins/twofactorauth/totp/totp.xml', + '/plugins/twofactorauth/yubikey/tmpl/form.php', + '/plugins/twofactorauth/yubikey/yubikey.php', + '/plugins/twofactorauth/yubikey/yubikey.xml', + // From 4.2.0-beta1 to 4.2.0-beta2 + '/layouts/plugins/user/profile/fields/dob.php', + '/modules/mod_articles_latest/mod_articles_latest.php', + '/plugins/behaviour/taggable/taggable.php', + '/plugins/behaviour/versionable/versionable.php', + '/plugins/task/requests/requests.php', + '/plugins/task/sitestatus/sitestatus.php', + '/plugins/user/profile/src/Field/DobField.php', + ); + + $folders = array( + // From 3.10 to 4.1 + '/templates/system/images', + '/templates/system/html', + '/templates/protostar/less', + '/templates/protostar/language/en-GB', + '/templates/protostar/language', + '/templates/protostar/js', + '/templates/protostar/img', + '/templates/protostar/images/system', + '/templates/protostar/images', + '/templates/protostar/html/layouts/joomla/system', + '/templates/protostar/html/layouts/joomla/form/field', + '/templates/protostar/html/layouts/joomla/form', + '/templates/protostar/html/layouts/joomla', + '/templates/protostar/html/layouts', + '/templates/protostar/html/com_media/imageslist', + '/templates/protostar/html/com_media', + '/templates/protostar/html', + '/templates/protostar/css', + '/templates/protostar', + '/templates/beez3/language/en-GB', + '/templates/beez3/language', + '/templates/beez3/javascript', + '/templates/beez3/images/system', + '/templates/beez3/images/personal', + '/templates/beez3/images/nature', + '/templates/beez3/images', + '/templates/beez3/html/mod_login', + '/templates/beez3/html/mod_languages', + '/templates/beez3/html/mod_breadcrumbs', + '/templates/beez3/html/layouts/joomla/system', + '/templates/beez3/html/layouts/joomla', + '/templates/beez3/html/layouts', + '/templates/beez3/html/com_weblinks/form', + '/templates/beez3/html/com_weblinks/category', + '/templates/beez3/html/com_weblinks/categories', + '/templates/beez3/html/com_weblinks', + '/templates/beez3/html/com_newsfeeds/category', + '/templates/beez3/html/com_newsfeeds/categories', + '/templates/beez3/html/com_newsfeeds', + '/templates/beez3/html/com_content/form', + '/templates/beez3/html/com_content/featured', + '/templates/beez3/html/com_content/category', + '/templates/beez3/html/com_content/categories', + '/templates/beez3/html/com_content/article', + '/templates/beez3/html/com_content/archive', + '/templates/beez3/html/com_content', + '/templates/beez3/html/com_contact/contact', + '/templates/beez3/html/com_contact/category', + '/templates/beez3/html/com_contact/categories', + '/templates/beez3/html/com_contact', + '/templates/beez3/html', + '/templates/beez3/css', + '/templates/beez3', + '/plugins/user/terms/terms', + '/plugins/user/terms/field', + '/plugins/user/profile/profiles', + '/plugins/user/profile/field', + '/plugins/system/stats/field', + '/plugins/system/privacyconsent/privacyconsent', + '/plugins/system/privacyconsent/field', + '/plugins/system/p3p', + '/plugins/system/languagecode/language/en-GB', + '/plugins/system/languagecode/language', + '/plugins/editors/tinymce/form', + '/plugins/editors/tinymce/field', + '/plugins/content/confirmconsent/fields', + '/plugins/captcha/recaptcha/postinstall', + '/plugins/authentication/gmail', + '/media/plg_twofactorauth_totp/js', + '/media/plg_twofactorauth_totp', + '/media/plg_system_highlight', + '/media/overrider/js', + '/media/overrider/css', + '/media/overrider', + '/media/media/js', + '/media/media/images/mime-icon-32', + '/media/media/images/mime-icon-16', + '/media/media/images', + '/media/media/css', + '/media/media', + '/media/jui/less', + '/media/jui/js', + '/media/jui/img', + '/media/jui/images', + '/media/jui/fonts', + '/media/jui/css', + '/media/jui', + '/media/editors/tinymce/themes/modern', + '/media/editors/tinymce/themes', + '/media/editors/tinymce/templates', + '/media/editors/tinymce/skins/lightgray/img', + '/media/editors/tinymce/skins/lightgray/fonts', + '/media/editors/tinymce/skins/lightgray', + '/media/editors/tinymce/skins', + '/media/editors/tinymce/plugins/wordcount', + '/media/editors/tinymce/plugins/visualchars', + '/media/editors/tinymce/plugins/visualblocks/css', + '/media/editors/tinymce/plugins/visualblocks', + '/media/editors/tinymce/plugins/toc', + '/media/editors/tinymce/plugins/textpattern', + '/media/editors/tinymce/plugins/textcolor', + '/media/editors/tinymce/plugins/template', + '/media/editors/tinymce/plugins/table', + '/media/editors/tinymce/plugins/tabfocus', + '/media/editors/tinymce/plugins/spellchecker', + '/media/editors/tinymce/plugins/searchreplace', + '/media/editors/tinymce/plugins/save', + '/media/editors/tinymce/plugins/print', + '/media/editors/tinymce/plugins/preview', + '/media/editors/tinymce/plugins/paste', + '/media/editors/tinymce/plugins/pagebreak', + '/media/editors/tinymce/plugins/noneditable', + '/media/editors/tinymce/plugins/nonbreaking', + '/media/editors/tinymce/plugins/media', + '/media/editors/tinymce/plugins/lists', + '/media/editors/tinymce/plugins/link', + '/media/editors/tinymce/plugins/legacyoutput', + '/media/editors/tinymce/plugins/layer', + '/media/editors/tinymce/plugins/insertdatetime', + '/media/editors/tinymce/plugins/importcss', + '/media/editors/tinymce/plugins/imagetools', + '/media/editors/tinymce/plugins/image', + '/media/editors/tinymce/plugins/hr', + '/media/editors/tinymce/plugins/fullscreen', + '/media/editors/tinymce/plugins/fullpage', + '/media/editors/tinymce/plugins/example_dependency', + '/media/editors/tinymce/plugins/example', + '/media/editors/tinymce/plugins/emoticons/img', + '/media/editors/tinymce/plugins/emoticons', + '/media/editors/tinymce/plugins/directionality', + '/media/editors/tinymce/plugins/contextmenu', + '/media/editors/tinymce/plugins/colorpicker', + '/media/editors/tinymce/plugins/codesample/css', + '/media/editors/tinymce/plugins/codesample', + '/media/editors/tinymce/plugins/code', + '/media/editors/tinymce/plugins/charmap', + '/media/editors/tinymce/plugins/bbcode', + '/media/editors/tinymce/plugins/autosave', + '/media/editors/tinymce/plugins/autoresize', + '/media/editors/tinymce/plugins/autolink', + '/media/editors/tinymce/plugins/anchor', + '/media/editors/tinymce/plugins/advlist', + '/media/editors/tinymce/plugins', + '/media/editors/tinymce/langs', + '/media/editors/tinymce/js/plugins/dragdrop', + '/media/editors/tinymce/js/plugins', + '/media/editors/tinymce/js', + '/media/editors/tinymce', + '/media/editors/none/js', + '/media/editors/none', + '/media/editors/codemirror/theme', + '/media/editors/codemirror/mode/z80', + '/media/editors/codemirror/mode/yaml-frontmatter', + '/media/editors/codemirror/mode/yaml', + '/media/editors/codemirror/mode/yacas', + '/media/editors/codemirror/mode/xquery', + '/media/editors/codemirror/mode/xml', + '/media/editors/codemirror/mode/webidl', + '/media/editors/codemirror/mode/wast', + '/media/editors/codemirror/mode/vue', + '/media/editors/codemirror/mode/vhdl', + '/media/editors/codemirror/mode/verilog', + '/media/editors/codemirror/mode/velocity', + '/media/editors/codemirror/mode/vbscript', + '/media/editors/codemirror/mode/vb', + '/media/editors/codemirror/mode/twig', + '/media/editors/codemirror/mode/turtle', + '/media/editors/codemirror/mode/ttcn-cfg', + '/media/editors/codemirror/mode/ttcn', + '/media/editors/codemirror/mode/troff', + '/media/editors/codemirror/mode/tornado', + '/media/editors/codemirror/mode/toml', + '/media/editors/codemirror/mode/tiki', + '/media/editors/codemirror/mode/tiddlywiki', + '/media/editors/codemirror/mode/textile', + '/media/editors/codemirror/mode/tcl', + '/media/editors/codemirror/mode/swift', + '/media/editors/codemirror/mode/stylus', + '/media/editors/codemirror/mode/stex', + '/media/editors/codemirror/mode/sql', + '/media/editors/codemirror/mode/spreadsheet', + '/media/editors/codemirror/mode/sparql', + '/media/editors/codemirror/mode/soy', + '/media/editors/codemirror/mode/solr', + '/media/editors/codemirror/mode/smarty', + '/media/editors/codemirror/mode/smalltalk', + '/media/editors/codemirror/mode/slim', + '/media/editors/codemirror/mode/sieve', + '/media/editors/codemirror/mode/shell', + '/media/editors/codemirror/mode/scheme', + '/media/editors/codemirror/mode/sass', + '/media/editors/codemirror/mode/sas', + '/media/editors/codemirror/mode/rust', + '/media/editors/codemirror/mode/ruby', + '/media/editors/codemirror/mode/rst', + '/media/editors/codemirror/mode/rpm/changes', + '/media/editors/codemirror/mode/rpm', + '/media/editors/codemirror/mode/r', + '/media/editors/codemirror/mode/q', + '/media/editors/codemirror/mode/python', + '/media/editors/codemirror/mode/puppet', + '/media/editors/codemirror/mode/pug', + '/media/editors/codemirror/mode/protobuf', + '/media/editors/codemirror/mode/properties', + '/media/editors/codemirror/mode/powershell', + '/media/editors/codemirror/mode/pig', + '/media/editors/codemirror/mode/php', + '/media/editors/codemirror/mode/perl', + '/media/editors/codemirror/mode/pegjs', + '/media/editors/codemirror/mode/pascal', + '/media/editors/codemirror/mode/oz', + '/media/editors/codemirror/mode/octave', + '/media/editors/codemirror/mode/ntriples', + '/media/editors/codemirror/mode/nsis', + '/media/editors/codemirror/mode/nginx', + '/media/editors/codemirror/mode/mumps', + '/media/editors/codemirror/mode/mscgen', + '/media/editors/codemirror/mode/modelica', + '/media/editors/codemirror/mode/mllike', + '/media/editors/codemirror/mode/mirc', + '/media/editors/codemirror/mode/mbox', + '/media/editors/codemirror/mode/mathematica', + '/media/editors/codemirror/mode/markdown', + '/media/editors/codemirror/mode/lua', + '/media/editors/codemirror/mode/livescript', + '/media/editors/codemirror/mode/julia', + '/media/editors/codemirror/mode/jsx', + '/media/editors/codemirror/mode/jinja2', + '/media/editors/codemirror/mode/javascript', + '/media/editors/codemirror/mode/idl', + '/media/editors/codemirror/mode/http', + '/media/editors/codemirror/mode/htmlmixed', + '/media/editors/codemirror/mode/htmlembedded', + '/media/editors/codemirror/mode/haxe', + '/media/editors/codemirror/mode/haskell-literate', + '/media/editors/codemirror/mode/haskell', + '/media/editors/codemirror/mode/handlebars', + '/media/editors/codemirror/mode/haml', + '/media/editors/codemirror/mode/groovy', + '/media/editors/codemirror/mode/go', + '/media/editors/codemirror/mode/gherkin', + '/media/editors/codemirror/mode/gfm', + '/media/editors/codemirror/mode/gas', + '/media/editors/codemirror/mode/fortran', + '/media/editors/codemirror/mode/forth', + '/media/editors/codemirror/mode/fcl', + '/media/editors/codemirror/mode/factor', + '/media/editors/codemirror/mode/erlang', + '/media/editors/codemirror/mode/elm', + '/media/editors/codemirror/mode/eiffel', + '/media/editors/codemirror/mode/ecl', + '/media/editors/codemirror/mode/ebnf', + '/media/editors/codemirror/mode/dylan', + '/media/editors/codemirror/mode/dtd', + '/media/editors/codemirror/mode/dockerfile', + '/media/editors/codemirror/mode/django', + '/media/editors/codemirror/mode/diff', + '/media/editors/codemirror/mode/dart', + '/media/editors/codemirror/mode/d', + '/media/editors/codemirror/mode/cypher', + '/media/editors/codemirror/mode/css', + '/media/editors/codemirror/mode/crystal', + '/media/editors/codemirror/mode/commonlisp', + '/media/editors/codemirror/mode/coffeescript', + '/media/editors/codemirror/mode/cobol', + '/media/editors/codemirror/mode/cmake', + '/media/editors/codemirror/mode/clojure', + '/media/editors/codemirror/mode/clike', + '/media/editors/codemirror/mode/brainfuck', + '/media/editors/codemirror/mode/asterisk', + '/media/editors/codemirror/mode/asn.1', + '/media/editors/codemirror/mode/asciiarmor', + '/media/editors/codemirror/mode/apl', + '/media/editors/codemirror/mode', + '/media/editors/codemirror/lib', + '/media/editors/codemirror/keymap', + '/media/editors/codemirror/addon/wrap', + '/media/editors/codemirror/addon/tern', + '/media/editors/codemirror/addon/selection', + '/media/editors/codemirror/addon/search', + '/media/editors/codemirror/addon/scroll', + '/media/editors/codemirror/addon/runmode', + '/media/editors/codemirror/addon/mode', + '/media/editors/codemirror/addon/merge', + '/media/editors/codemirror/addon/lint', + '/media/editors/codemirror/addon/hint', + '/media/editors/codemirror/addon/fold', + '/media/editors/codemirror/addon/edit', + '/media/editors/codemirror/addon/display', + '/media/editors/codemirror/addon/dialog', + '/media/editors/codemirror/addon/comment', + '/media/editors/codemirror/addon', + '/media/editors/codemirror', + '/media/editors', + '/media/contacts/images', + '/media/contacts', + '/media/com_contenthistory/css', + '/media/cms/css', + '/media/cms', + '/libraries/vendor/symfony/polyfill-util', + '/libraries/vendor/symfony/polyfill-php71', + '/libraries/vendor/symfony/polyfill-php56', + '/libraries/vendor/symfony/polyfill-php55', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML/Declaration', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parse', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Net', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/HTTP', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode/HTML', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content/Type', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache', + '/libraries/vendor/simplepie/simplepie/library/SimplePie', + '/libraries/vendor/simplepie/simplepie/library', + '/libraries/vendor/simplepie/simplepie/idn', + '/libraries/vendor/simplepie/simplepie', + '/libraries/vendor/simplepie', + '/libraries/vendor/phpmailer/phpmailer/extras', + '/libraries/vendor/paragonie/random_compat/lib', + '/libraries/vendor/leafo/lessphp', + '/libraries/vendor/leafo', + '/libraries/vendor/joomla/session/Joomla/Session/Storage', + '/libraries/vendor/joomla/session/Joomla/Session', + '/libraries/vendor/joomla/session/Joomla', + '/libraries/vendor/joomla/image/src/Filter', + '/libraries/vendor/joomla/image/src', + '/libraries/vendor/joomla/image', + '/libraries/vendor/joomla/compat/src', + '/libraries/vendor/joomla/compat', + '/libraries/vendor/joomla/application/src/Cli/Output/Processor', + '/libraries/vendor/joomla/application/src/Cli/Output', + '/libraries/vendor/joomla/application/src/Cli', + '/libraries/vendor/ircmaxell/password-compat/lib', + '/libraries/vendor/ircmaxell/password-compat', + '/libraries/vendor/ircmaxell', + '/libraries/vendor/brumann/polyfill-unserialize/src', + '/libraries/vendor/brumann/polyfill-unserialize', + '/libraries/vendor/brumann', + '/libraries/src/Table/Observer', + '/libraries/src/Menu/Node', + '/libraries/src/Language/Wrapper', + '/libraries/src/Language/Stemmer', + '/libraries/src/Http/Wrapper', + '/libraries/src/Filter/Wrapper', + '/libraries/src/Filesystem/Wrapper', + '/libraries/src/Crypt/Password', + '/libraries/src/Access/Wrapper', + '/libraries/phputf8/utils', + '/libraries/phputf8/native', + '/libraries/phputf8/mbstring', + '/libraries/phputf8', + '/libraries/legacy/utilities', + '/libraries/legacy/table', + '/libraries/legacy/simplepie', + '/libraries/legacy/simplecrypt', + '/libraries/legacy/response', + '/libraries/legacy/request', + '/libraries/legacy/log', + '/libraries/legacy/form/field', + '/libraries/legacy/form', + '/libraries/legacy/exception', + '/libraries/legacy/error', + '/libraries/legacy/dispatcher', + '/libraries/legacy/database', + '/libraries/legacy/base', + '/libraries/legacy/application', + '/libraries/legacy', + '/libraries/joomla/view', + '/libraries/joomla/utilities', + '/libraries/joomla/twitter', + '/libraries/joomla/string/wrapper', + '/libraries/joomla/string', + '/libraries/joomla/session/storage', + '/libraries/joomla/session/handler', + '/libraries/joomla/session', + '/libraries/joomla/route/wrapper', + '/libraries/joomla/route', + '/libraries/joomla/openstreetmap', + '/libraries/joomla/observer/wrapper', + '/libraries/joomla/observer/updater', + '/libraries/joomla/observer', + '/libraries/joomla/observable', + '/libraries/joomla/oauth2', + '/libraries/joomla/oauth1', + '/libraries/joomla/model', + '/libraries/joomla/mediawiki', + '/libraries/joomla/linkedin', + '/libraries/joomla/keychain', + '/libraries/joomla/grid', + '/libraries/joomla/google/embed', + '/libraries/joomla/google/data/plus', + '/libraries/joomla/google/data/picasa', + '/libraries/joomla/google/data', + '/libraries/joomla/google/auth', + '/libraries/joomla/google', + '/libraries/joomla/github/package/users', + '/libraries/joomla/github/package/repositories', + '/libraries/joomla/github/package/pulls', + '/libraries/joomla/github/package/orgs', + '/libraries/joomla/github/package/issues', + '/libraries/joomla/github/package/gists', + '/libraries/joomla/github/package/data', + '/libraries/joomla/github/package/activity', + '/libraries/joomla/github/package', + '/libraries/joomla/github', + '/libraries/joomla/form/fields', + '/libraries/joomla/form', + '/libraries/joomla/facebook', + '/libraries/joomla/event', + '/libraries/joomla/database/query', + '/libraries/joomla/database/iterator', + '/libraries/joomla/database/importer', + '/libraries/joomla/database/exporter', + '/libraries/joomla/database/exception', + '/libraries/joomla/database/driver', + '/libraries/joomla/database', + '/libraries/joomla/controller', + '/libraries/joomla/archive/wrapper', + '/libraries/joomla/archive', + '/libraries/joomla/application/web/router', + '/libraries/joomla/application/web', + '/libraries/joomla/application', + '/libraries/joomla', + '/libraries/idna_convert', + '/libraries/fof/view', + '/libraries/fof/utils/update', + '/libraries/fof/utils/timer', + '/libraries/fof/utils/phpfunc', + '/libraries/fof/utils/observable', + '/libraries/fof/utils/object', + '/libraries/fof/utils/ip', + '/libraries/fof/utils/installscript', + '/libraries/fof/utils/ini', + '/libraries/fof/utils/filescheck', + '/libraries/fof/utils/config', + '/libraries/fof/utils/cache', + '/libraries/fof/utils/array', + '/libraries/fof/utils', + '/libraries/fof/toolbar', + '/libraries/fof/template', + '/libraries/fof/table/dispatcher', + '/libraries/fof/table/behavior', + '/libraries/fof/table', + '/libraries/fof/string', + '/libraries/fof/render', + '/libraries/fof/query', + '/libraries/fof/platform/filesystem', + '/libraries/fof/platform', + '/libraries/fof/model/field', + '/libraries/fof/model/dispatcher', + '/libraries/fof/model/behavior', + '/libraries/fof/model', + '/libraries/fof/less/parser', + '/libraries/fof/less/formatter', + '/libraries/fof/less', + '/libraries/fof/layout', + '/libraries/fof/integration/joomla/filesystem', + '/libraries/fof/integration/joomla', + '/libraries/fof/integration', + '/libraries/fof/input/jinput', + '/libraries/fof/input', + '/libraries/fof/inflector', + '/libraries/fof/hal/render', + '/libraries/fof/hal', + '/libraries/fof/form/header', + '/libraries/fof/form/field', + '/libraries/fof/form', + '/libraries/fof/encrypt/aes', + '/libraries/fof/encrypt', + '/libraries/fof/download/adapter', + '/libraries/fof/download', + '/libraries/fof/dispatcher', + '/libraries/fof/database/query', + '/libraries/fof/database/iterator', + '/libraries/fof/database/driver', + '/libraries/fof/database', + '/libraries/fof/controller', + '/libraries/fof/config/domain', + '/libraries/fof/config', + '/libraries/fof/autoloader', + '/libraries/fof', + '/libraries/cms/less/formatter', + '/libraries/cms/less', + '/libraries/cms/html/language/en-GB', + '/libraries/cms/html/language', + '/libraries/cms/html', + '/libraries/cms/class', + '/libraries/cms', + '/layouts/libraries/cms/html/bootstrap', + '/layouts/libraries/cms/html', + '/layouts/libraries/cms', + '/layouts/joomla/tinymce/buttons', + '/layouts/joomla/modal', + '/layouts/joomla/html/formbehavior', + '/components/com_wrapper/views/wrapper/tmpl', + '/components/com_wrapper/views/wrapper', + '/components/com_wrapper/views', + '/components/com_users/views/reset/tmpl', + '/components/com_users/views/reset', + '/components/com_users/views/remind/tmpl', + '/components/com_users/views/remind', + '/components/com_users/views/registration/tmpl', + '/components/com_users/views/registration', + '/components/com_users/views/profile/tmpl', + '/components/com_users/views/profile', + '/components/com_users/views/login/tmpl', + '/components/com_users/views/login', + '/components/com_users/views', + '/components/com_users/models/rules', + '/components/com_users/models/forms', + '/components/com_users/models', + '/components/com_users/layouts/joomla/form', + '/components/com_users/layouts/joomla', + '/components/com_users/layouts', + '/components/com_users/helpers/html', + '/components/com_users/helpers', + '/components/com_users/controllers', + '/components/com_tags/views/tags/tmpl', + '/components/com_tags/views/tags', + '/components/com_tags/views/tag/tmpl', + '/components/com_tags/views/tag', + '/components/com_tags/views', + '/components/com_tags/models', + '/components/com_tags/controllers', + '/components/com_privacy/views/request/tmpl', + '/components/com_privacy/views/request', + '/components/com_privacy/views/remind/tmpl', + '/components/com_privacy/views/remind', + '/components/com_privacy/views/confirm/tmpl', + '/components/com_privacy/views/confirm', + '/components/com_privacy/views', + '/components/com_privacy/models/forms', + '/components/com_privacy/models', + '/components/com_privacy/controllers', + '/components/com_newsfeeds/views/newsfeed/tmpl', + '/components/com_newsfeeds/views/newsfeed', + '/components/com_newsfeeds/views/category/tmpl', + '/components/com_newsfeeds/views/category', + '/components/com_newsfeeds/views/categories/tmpl', + '/components/com_newsfeeds/views/categories', + '/components/com_newsfeeds/views', + '/components/com_newsfeeds/models', + '/components/com_modules/models/forms', + '/components/com_modules/models', + '/components/com_menus/models/forms', + '/components/com_menus/models', + '/components/com_mailto/views/sent/tmpl', + '/components/com_mailto/views/sent', + '/components/com_mailto/views/mailto/tmpl', + '/components/com_mailto/views/mailto', + '/components/com_mailto/views', + '/components/com_mailto/models/forms', + '/components/com_mailto/models', + '/components/com_mailto/helpers', + '/components/com_mailto', + '/components/com_finder/views/search/tmpl', + '/components/com_finder/views/search', + '/components/com_finder/views', + '/components/com_finder/models', + '/components/com_finder/helpers/html', + '/components/com_finder/controllers', + '/components/com_fields/models/forms', + '/components/com_fields/models', + '/components/com_content/views/form/tmpl', + '/components/com_content/views/form', + '/components/com_content/views/featured/tmpl', + '/components/com_content/views/featured', + '/components/com_content/views/category/tmpl', + '/components/com_content/views/category', + '/components/com_content/views/categories/tmpl', + '/components/com_content/views/categories', + '/components/com_content/views/article/tmpl', + '/components/com_content/views/article', + '/components/com_content/views/archive/tmpl', + '/components/com_content/views/archive', + '/components/com_content/views', + '/components/com_content/models/forms', + '/components/com_content/models', + '/components/com_content/controllers', + '/components/com_contact/views/featured/tmpl', + '/components/com_contact/views/featured', + '/components/com_contact/views/contact/tmpl', + '/components/com_contact/views/contact', + '/components/com_contact/views/category/tmpl', + '/components/com_contact/views/category', + '/components/com_contact/views/categories/tmpl', + '/components/com_contact/views/categories', + '/components/com_contact/views', + '/components/com_contact/models/rules', + '/components/com_contact/models/forms', + '/components/com_contact/models', + '/components/com_contact/layouts/joomla/form', + '/components/com_contact/layouts/joomla', + '/components/com_contact/controllers', + '/components/com_config/view/templates/tmpl', + '/components/com_config/view/templates', + '/components/com_config/view/modules/tmpl', + '/components/com_config/view/modules', + '/components/com_config/view/config/tmpl', + '/components/com_config/view/config', + '/components/com_config/view/cms', + '/components/com_config/view', + '/components/com_config/model/form', + '/components/com_config/model', + '/components/com_config/controller/templates', + '/components/com_config/controller/modules', + '/components/com_config/controller/config', + '/components/com_config/controller', + '/components/com_banners/models', + '/components/com_banners/helpers', + '/administrator/templates/system/html', + '/administrator/templates/isis/less/pages', + '/administrator/templates/isis/less/bootstrap', + '/administrator/templates/isis/less/blocks', + '/administrator/templates/isis/less', + '/administrator/templates/isis/language/en-GB', + '/administrator/templates/isis/language', + '/administrator/templates/isis/js', + '/administrator/templates/isis/img', + '/administrator/templates/isis/images/system', + '/administrator/templates/isis/images/admin', + '/administrator/templates/isis/images', + '/administrator/templates/isis/html/mod_version', + '/administrator/templates/isis/html/layouts/joomla/toolbar', + '/administrator/templates/isis/html/layouts/joomla/system', + '/administrator/templates/isis/html/layouts/joomla/pagination', + '/administrator/templates/isis/html/layouts/joomla/form/field', + '/administrator/templates/isis/html/layouts/joomla/form', + '/administrator/templates/isis/html/layouts/joomla', + '/administrator/templates/isis/html/layouts', + '/administrator/templates/isis/html/com_media/medialist', + '/administrator/templates/isis/html/com_media/imageslist', + '/administrator/templates/isis/html/com_media', + '/administrator/templates/isis/html', + '/administrator/templates/isis/css', + '/administrator/templates/isis', + '/administrator/templates/hathor/postinstall', + '/administrator/templates/hathor/less', + '/administrator/templates/hathor/language/en-GB', + '/administrator/templates/hathor/language', + '/administrator/templates/hathor/js', + '/administrator/templates/hathor/images/toolbar', + '/administrator/templates/hathor/images/system', + '/administrator/templates/hathor/images/menu', + '/administrator/templates/hathor/images/header', + '/administrator/templates/hathor/images/admin', + '/administrator/templates/hathor/images', + '/administrator/templates/hathor/html/mod_quickicon', + '/administrator/templates/hathor/html/mod_login', + '/administrator/templates/hathor/html/layouts/plugins/user/profile/fields', + '/administrator/templates/hathor/html/layouts/plugins/user/profile', + '/administrator/templates/hathor/html/layouts/plugins/user', + '/administrator/templates/hathor/html/layouts/plugins', + '/administrator/templates/hathor/html/layouts/joomla/toolbar', + '/administrator/templates/hathor/html/layouts/joomla/sidebars', + '/administrator/templates/hathor/html/layouts/joomla/quickicons', + '/administrator/templates/hathor/html/layouts/joomla/edit', + '/administrator/templates/hathor/html/layouts/joomla', + '/administrator/templates/hathor/html/layouts/com_modules/toolbar', + '/administrator/templates/hathor/html/layouts/com_modules', + '/administrator/templates/hathor/html/layouts/com_messages/toolbar', + '/administrator/templates/hathor/html/layouts/com_messages', + '/administrator/templates/hathor/html/layouts/com_media/toolbar', + '/administrator/templates/hathor/html/layouts/com_media', + '/administrator/templates/hathor/html/layouts', + '/administrator/templates/hathor/html/com_weblinks/weblinks', + '/administrator/templates/hathor/html/com_weblinks/weblink', + '/administrator/templates/hathor/html/com_weblinks', + '/administrator/templates/hathor/html/com_users/users', + '/administrator/templates/hathor/html/com_users/user', + '/administrator/templates/hathor/html/com_users/notes', + '/administrator/templates/hathor/html/com_users/note', + '/administrator/templates/hathor/html/com_users/levels', + '/administrator/templates/hathor/html/com_users/groups', + '/administrator/templates/hathor/html/com_users/debuguser', + '/administrator/templates/hathor/html/com_users/debuggroup', + '/administrator/templates/hathor/html/com_users', + '/administrator/templates/hathor/html/com_templates/templates', + '/administrator/templates/hathor/html/com_templates/template', + '/administrator/templates/hathor/html/com_templates/styles', + '/administrator/templates/hathor/html/com_templates/style', + '/administrator/templates/hathor/html/com_templates', + '/administrator/templates/hathor/html/com_tags/tags', + '/administrator/templates/hathor/html/com_tags/tag', + '/administrator/templates/hathor/html/com_tags', + '/administrator/templates/hathor/html/com_search/searches', + '/administrator/templates/hathor/html/com_search', + '/administrator/templates/hathor/html/com_redirect/links', + '/administrator/templates/hathor/html/com_redirect', + '/administrator/templates/hathor/html/com_postinstall/messages', + '/administrator/templates/hathor/html/com_postinstall', + '/administrator/templates/hathor/html/com_plugins/plugins', + '/administrator/templates/hathor/html/com_plugins/plugin', + '/administrator/templates/hathor/html/com_plugins', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeed', + '/administrator/templates/hathor/html/com_newsfeeds', + '/administrator/templates/hathor/html/com_modules/positions', + '/administrator/templates/hathor/html/com_modules/modules', + '/administrator/templates/hathor/html/com_modules/module', + '/administrator/templates/hathor/html/com_modules', + '/administrator/templates/hathor/html/com_messages/messages', + '/administrator/templates/hathor/html/com_messages/message', + '/administrator/templates/hathor/html/com_messages', + '/administrator/templates/hathor/html/com_menus/menutypes', + '/administrator/templates/hathor/html/com_menus/menus', + '/administrator/templates/hathor/html/com_menus/menu', + '/administrator/templates/hathor/html/com_menus/items', + '/administrator/templates/hathor/html/com_menus/item', + '/administrator/templates/hathor/html/com_menus', + '/administrator/templates/hathor/html/com_languages/overrides', + '/administrator/templates/hathor/html/com_languages/languages', + '/administrator/templates/hathor/html/com_languages/installed', + '/administrator/templates/hathor/html/com_languages', + '/administrator/templates/hathor/html/com_joomlaupdate/default', + '/administrator/templates/hathor/html/com_joomlaupdate', + '/administrator/templates/hathor/html/com_installer/warnings', + '/administrator/templates/hathor/html/com_installer/update', + '/administrator/templates/hathor/html/com_installer/manage', + '/administrator/templates/hathor/html/com_installer/languages', + '/administrator/templates/hathor/html/com_installer/install', + '/administrator/templates/hathor/html/com_installer/discover', + '/administrator/templates/hathor/html/com_installer/default', + '/administrator/templates/hathor/html/com_installer/database', + '/administrator/templates/hathor/html/com_installer', + '/administrator/templates/hathor/html/com_finder/maps', + '/administrator/templates/hathor/html/com_finder/index', + '/administrator/templates/hathor/html/com_finder/filters', + '/administrator/templates/hathor/html/com_finder', + '/administrator/templates/hathor/html/com_fields/groups', + '/administrator/templates/hathor/html/com_fields/group', + '/administrator/templates/hathor/html/com_fields/fields', + '/administrator/templates/hathor/html/com_fields/field', + '/administrator/templates/hathor/html/com_fields', + '/administrator/templates/hathor/html/com_cpanel/cpanel', + '/administrator/templates/hathor/html/com_cpanel', + '/administrator/templates/hathor/html/com_contenthistory/history', + '/administrator/templates/hathor/html/com_contenthistory', + '/administrator/templates/hathor/html/com_content/featured', + '/administrator/templates/hathor/html/com_content/articles', + '/administrator/templates/hathor/html/com_content/article', + '/administrator/templates/hathor/html/com_content', + '/administrator/templates/hathor/html/com_contact/contacts', + '/administrator/templates/hathor/html/com_contact/contact', + '/administrator/templates/hathor/html/com_contact', + '/administrator/templates/hathor/html/com_config/component', + '/administrator/templates/hathor/html/com_config/application', + '/administrator/templates/hathor/html/com_config', + '/administrator/templates/hathor/html/com_checkin/checkin', + '/administrator/templates/hathor/html/com_checkin', + '/administrator/templates/hathor/html/com_categories/category', + '/administrator/templates/hathor/html/com_categories/categories', + '/administrator/templates/hathor/html/com_categories', + '/administrator/templates/hathor/html/com_cache/purge', + '/administrator/templates/hathor/html/com_cache/cache', + '/administrator/templates/hathor/html/com_cache', + '/administrator/templates/hathor/html/com_banners/tracks', + '/administrator/templates/hathor/html/com_banners/download', + '/administrator/templates/hathor/html/com_banners/clients', + '/administrator/templates/hathor/html/com_banners/client', + '/administrator/templates/hathor/html/com_banners/banners', + '/administrator/templates/hathor/html/com_banners/banner', + '/administrator/templates/hathor/html/com_banners', + '/administrator/templates/hathor/html/com_associations/associations', + '/administrator/templates/hathor/html/com_associations', + '/administrator/templates/hathor/html/com_admin/sysinfo', + '/administrator/templates/hathor/html/com_admin/profile', + '/administrator/templates/hathor/html/com_admin/help', + '/administrator/templates/hathor/html/com_admin', + '/administrator/templates/hathor/html', + '/administrator/templates/hathor/css', + '/administrator/templates/hathor', + '/administrator/modules/mod_version/language/en-GB', + '/administrator/modules/mod_version/language', + '/administrator/modules/mod_status/tmpl', + '/administrator/modules/mod_status', + '/administrator/modules/mod_stats_admin/language', + '/administrator/modules/mod_multilangstatus/language/en-GB', + '/administrator/modules/mod_multilangstatus/language', + '/administrator/components/com_users/views/users/tmpl', + '/administrator/components/com_users/views/users', + '/administrator/components/com_users/views/user/tmpl', + '/administrator/components/com_users/views/user', + '/administrator/components/com_users/views/notes/tmpl', + '/administrator/components/com_users/views/notes', + '/administrator/components/com_users/views/note/tmpl', + '/administrator/components/com_users/views/note', + '/administrator/components/com_users/views/mail/tmpl', + '/administrator/components/com_users/views/mail', + '/administrator/components/com_users/views/levels/tmpl', + '/administrator/components/com_users/views/levels', + '/administrator/components/com_users/views/level/tmpl', + '/administrator/components/com_users/views/level', + '/administrator/components/com_users/views/groups/tmpl', + '/administrator/components/com_users/views/groups', + '/administrator/components/com_users/views/group/tmpl', + '/administrator/components/com_users/views/group', + '/administrator/components/com_users/views/debuguser/tmpl', + '/administrator/components/com_users/views/debuguser', + '/administrator/components/com_users/views/debuggroup/tmpl', + '/administrator/components/com_users/views/debuggroup', + '/administrator/components/com_users/views', + '/administrator/components/com_users/tables', + '/administrator/components/com_users/models/forms/fields', + '/administrator/components/com_users/models/forms', + '/administrator/components/com_users/models/fields', + '/administrator/components/com_users/models', + '/administrator/components/com_users/helpers/html', + '/administrator/components/com_users/controllers', + '/administrator/components/com_templates/views/templates/tmpl', + '/administrator/components/com_templates/views/templates', + '/administrator/components/com_templates/views/template/tmpl', + '/administrator/components/com_templates/views/template', + '/administrator/components/com_templates/views/styles/tmpl', + '/administrator/components/com_templates/views/styles', + '/administrator/components/com_templates/views/style/tmpl', + '/administrator/components/com_templates/views/style', + '/administrator/components/com_templates/views', + '/administrator/components/com_templates/tables', + '/administrator/components/com_templates/models/forms', + '/administrator/components/com_templates/models/fields', + '/administrator/components/com_templates/models', + '/administrator/components/com_templates/helpers/html', + '/administrator/components/com_templates/controllers', + '/administrator/components/com_tags/views/tags/tmpl', + '/administrator/components/com_tags/views/tags', + '/administrator/components/com_tags/views/tag/tmpl', + '/administrator/components/com_tags/views/tag', + '/administrator/components/com_tags/views', + '/administrator/components/com_tags/tables', + '/administrator/components/com_tags/models/forms', + '/administrator/components/com_tags/models', + '/administrator/components/com_tags/helpers', + '/administrator/components/com_tags/controllers', + '/administrator/components/com_redirect/views/links/tmpl', + '/administrator/components/com_redirect/views/links', + '/administrator/components/com_redirect/views/link/tmpl', + '/administrator/components/com_redirect/views/link', + '/administrator/components/com_redirect/views', + '/administrator/components/com_redirect/tables', + '/administrator/components/com_redirect/models/forms', + '/administrator/components/com_redirect/models/fields', + '/administrator/components/com_redirect/models', + '/administrator/components/com_redirect/helpers/html', + '/administrator/components/com_redirect/controllers', + '/administrator/components/com_privacy/views/requests/tmpl', + '/administrator/components/com_privacy/views/requests', + '/administrator/components/com_privacy/views/request/tmpl', + '/administrator/components/com_privacy/views/request', + '/administrator/components/com_privacy/views/export', + '/administrator/components/com_privacy/views/dashboard/tmpl', + '/administrator/components/com_privacy/views/dashboard', + '/administrator/components/com_privacy/views/consents/tmpl', + '/administrator/components/com_privacy/views/consents', + '/administrator/components/com_privacy/views/capabilities/tmpl', + '/administrator/components/com_privacy/views/capabilities', + '/administrator/components/com_privacy/views', + '/administrator/components/com_privacy/tables', + '/administrator/components/com_privacy/models/forms', + '/administrator/components/com_privacy/models/fields', + '/administrator/components/com_privacy/models', + '/administrator/components/com_privacy/helpers/removal', + '/administrator/components/com_privacy/helpers/html', + '/administrator/components/com_privacy/helpers/export', + '/administrator/components/com_privacy/helpers', + '/administrator/components/com_privacy/controllers', + '/administrator/components/com_postinstall/views/messages/tmpl', + '/administrator/components/com_postinstall/views/messages', + '/administrator/components/com_postinstall/views', + '/administrator/components/com_postinstall/models', + '/administrator/components/com_postinstall/controllers', + '/administrator/components/com_plugins/views/plugins/tmpl', + '/administrator/components/com_plugins/views/plugins', + '/administrator/components/com_plugins/views/plugin/tmpl', + '/administrator/components/com_plugins/views/plugin', + '/administrator/components/com_plugins/views', + '/administrator/components/com_plugins/models/forms', + '/administrator/components/com_plugins/models/fields', + '/administrator/components/com_plugins/models', + '/administrator/components/com_plugins/controllers', + '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl', + '/administrator/components/com_newsfeeds/views/newsfeeds', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl', + '/administrator/components/com_newsfeeds/views/newsfeed', + '/administrator/components/com_newsfeeds/views', + '/administrator/components/com_newsfeeds/tables', + '/administrator/components/com_newsfeeds/models/forms', + '/administrator/components/com_newsfeeds/models/fields/modal', + '/administrator/components/com_newsfeeds/models/fields', + '/administrator/components/com_newsfeeds/models', + '/administrator/components/com_newsfeeds/helpers/html', + '/administrator/components/com_newsfeeds/controllers', + '/administrator/components/com_modules/views/select/tmpl', + '/administrator/components/com_modules/views/select', + '/administrator/components/com_modules/views/preview/tmpl', + '/administrator/components/com_modules/views/preview', + '/administrator/components/com_modules/views/positions/tmpl', + '/administrator/components/com_modules/views/positions', + '/administrator/components/com_modules/views/modules/tmpl', + '/administrator/components/com_modules/views/modules', + '/administrator/components/com_modules/views/module/tmpl', + '/administrator/components/com_modules/views/module', + '/administrator/components/com_modules/views', + '/administrator/components/com_modules/models/forms', + '/administrator/components/com_modules/models/fields', + '/administrator/components/com_modules/models', + '/administrator/components/com_modules/helpers/html', + '/administrator/components/com_modules/controllers', + '/administrator/components/com_messages/views/messages/tmpl', + '/administrator/components/com_messages/views/messages', + '/administrator/components/com_messages/views/message/tmpl', + '/administrator/components/com_messages/views/message', + '/administrator/components/com_messages/views/config/tmpl', + '/administrator/components/com_messages/views/config', + '/administrator/components/com_messages/views', + '/administrator/components/com_messages/tables', + '/administrator/components/com_messages/models/forms', + '/administrator/components/com_messages/models/fields', + '/administrator/components/com_messages/models', + '/administrator/components/com_messages/helpers/html', + '/administrator/components/com_messages/helpers', + '/administrator/components/com_messages/controllers', + '/administrator/components/com_menus/views/menutypes/tmpl', + '/administrator/components/com_menus/views/menutypes', + '/administrator/components/com_menus/views/menus/tmpl', + '/administrator/components/com_menus/views/menus', + '/administrator/components/com_menus/views/menu/tmpl', + '/administrator/components/com_menus/views/menu', + '/administrator/components/com_menus/views/items/tmpl', + '/administrator/components/com_menus/views/items', + '/administrator/components/com_menus/views/item/tmpl', + '/administrator/components/com_menus/views/item', + '/administrator/components/com_menus/views', + '/administrator/components/com_menus/tables', + '/administrator/components/com_menus/models/forms', + '/administrator/components/com_menus/models/fields/modal', + '/administrator/components/com_menus/models/fields', + '/administrator/components/com_menus/models', + '/administrator/components/com_menus/layouts/joomla/searchtools/default', + '/administrator/components/com_menus/helpers/html', + '/administrator/components/com_menus/controllers', + '/administrator/components/com_media/views/medialist/tmpl', + '/administrator/components/com_media/views/medialist', + '/administrator/components/com_media/views/media/tmpl', + '/administrator/components/com_media/views/media', + '/administrator/components/com_media/views/imageslist/tmpl', + '/administrator/components/com_media/views/imageslist', + '/administrator/components/com_media/views/images/tmpl', + '/administrator/components/com_media/views/images', + '/administrator/components/com_media/views', + '/administrator/components/com_media/models', + '/administrator/components/com_media/controllers', + '/administrator/components/com_login/views/login/tmpl', + '/administrator/components/com_login/views/login', + '/administrator/components/com_login/views', + '/administrator/components/com_login/models', + '/administrator/components/com_languages/views/overrides/tmpl', + '/administrator/components/com_languages/views/overrides', + '/administrator/components/com_languages/views/override/tmpl', + '/administrator/components/com_languages/views/override', + '/administrator/components/com_languages/views/multilangstatus/tmpl', + '/administrator/components/com_languages/views/multilangstatus', + '/administrator/components/com_languages/views/languages/tmpl', + '/administrator/components/com_languages/views/languages', + '/administrator/components/com_languages/views/language/tmpl', + '/administrator/components/com_languages/views/language', + '/administrator/components/com_languages/views/installed/tmpl', + '/administrator/components/com_languages/views/installed', + '/administrator/components/com_languages/views', + '/administrator/components/com_languages/models/forms', + '/administrator/components/com_languages/models/fields', + '/administrator/components/com_languages/models', + '/administrator/components/com_languages/layouts/joomla/searchtools/default', + '/administrator/components/com_languages/layouts/joomla/searchtools', + '/administrator/components/com_languages/layouts/joomla', + '/administrator/components/com_languages/layouts', + '/administrator/components/com_languages/helpers/html', + '/administrator/components/com_languages/helpers', + '/administrator/components/com_languages/controllers', + '/administrator/components/com_joomlaupdate/views/upload/tmpl', + '/administrator/components/com_joomlaupdate/views/upload', + '/administrator/components/com_joomlaupdate/views/update/tmpl', + '/administrator/components/com_joomlaupdate/views/update', + '/administrator/components/com_joomlaupdate/views/default/tmpl', + '/administrator/components/com_joomlaupdate/views/default', + '/administrator/components/com_joomlaupdate/views', + '/administrator/components/com_joomlaupdate/models', + '/administrator/components/com_joomlaupdate/helpers', + '/administrator/components/com_joomlaupdate/controllers', + '/administrator/components/com_installer/views/warnings/tmpl', + '/administrator/components/com_installer/views/warnings', + '/administrator/components/com_installer/views/updatesites/tmpl', + '/administrator/components/com_installer/views/updatesites', + '/administrator/components/com_installer/views/update/tmpl', + '/administrator/components/com_installer/views/update', + '/administrator/components/com_installer/views/manage/tmpl', + '/administrator/components/com_installer/views/manage', + '/administrator/components/com_installer/views/languages/tmpl', + '/administrator/components/com_installer/views/languages', + '/administrator/components/com_installer/views/install/tmpl', + '/administrator/components/com_installer/views/install', + '/administrator/components/com_installer/views/discover/tmpl', + '/administrator/components/com_installer/views/discover', + '/administrator/components/com_installer/views/default/tmpl', + '/administrator/components/com_installer/views/default', + '/administrator/components/com_installer/views/database/tmpl', + '/administrator/components/com_installer/views/database', + '/administrator/components/com_installer/views', + '/administrator/components/com_installer/models/forms', + '/administrator/components/com_installer/models/fields', + '/administrator/components/com_installer/models', + '/administrator/components/com_installer/helpers/html', + '/administrator/components/com_installer/controllers', + '/administrator/components/com_finder/views/statistics/tmpl', + '/administrator/components/com_finder/views/statistics', + '/administrator/components/com_finder/views/maps/tmpl', + '/administrator/components/com_finder/views/maps', + '/administrator/components/com_finder/views/indexer/tmpl', + '/administrator/components/com_finder/views/indexer', + '/administrator/components/com_finder/views/index/tmpl', + '/administrator/components/com_finder/views/index', + '/administrator/components/com_finder/views/filters/tmpl', + '/administrator/components/com_finder/views/filters', + '/administrator/components/com_finder/views/filter/tmpl', + '/administrator/components/com_finder/views/filter', + '/administrator/components/com_finder/views', + '/administrator/components/com_finder/tables', + '/administrator/components/com_finder/models/forms', + '/administrator/components/com_finder/models/fields', + '/administrator/components/com_finder/models', + '/administrator/components/com_finder/helpers/indexer/stemmer', + '/administrator/components/com_finder/helpers/indexer/parser', + '/administrator/components/com_finder/helpers/indexer/driver', + '/administrator/components/com_finder/helpers/html', + '/administrator/components/com_finder/controllers', + '/administrator/components/com_fields/views/groups/tmpl', + '/administrator/components/com_fields/views/groups', + '/administrator/components/com_fields/views/group/tmpl', + '/administrator/components/com_fields/views/group', + '/administrator/components/com_fields/views/fields/tmpl', + '/administrator/components/com_fields/views/fields', + '/administrator/components/com_fields/views/field/tmpl', + '/administrator/components/com_fields/views/field', + '/administrator/components/com_fields/views', + '/administrator/components/com_fields/tables', + '/administrator/components/com_fields/models/forms', + '/administrator/components/com_fields/models/fields', + '/administrator/components/com_fields/models', + '/administrator/components/com_fields/libraries', + '/administrator/components/com_fields/controllers', + '/administrator/components/com_cpanel/views/cpanel/tmpl', + '/administrator/components/com_cpanel/views/cpanel', + '/administrator/components/com_cpanel/views', + '/administrator/components/com_contenthistory/views/preview/tmpl', + '/administrator/components/com_contenthistory/views/preview', + '/administrator/components/com_contenthistory/views/history/tmpl', + '/administrator/components/com_contenthistory/views/history', + '/administrator/components/com_contenthistory/views/compare/tmpl', + '/administrator/components/com_contenthistory/views/compare', + '/administrator/components/com_contenthistory/views', + '/administrator/components/com_contenthistory/models', + '/administrator/components/com_contenthistory/helpers/html', + '/administrator/components/com_contenthistory/controllers', + '/administrator/components/com_content/views/featured/tmpl', + '/administrator/components/com_content/views/featured', + '/administrator/components/com_content/views/articles/tmpl', + '/administrator/components/com_content/views/articles', + '/administrator/components/com_content/views/article/tmpl', + '/administrator/components/com_content/views/article', + '/administrator/components/com_content/views', + '/administrator/components/com_content/tables', + '/administrator/components/com_content/models/forms', + '/administrator/components/com_content/models/fields/modal', + '/administrator/components/com_content/models/fields', + '/administrator/components/com_content/models', + '/administrator/components/com_content/helpers/html', + '/administrator/components/com_content/controllers', + '/administrator/components/com_contact/views/contacts/tmpl', + '/administrator/components/com_contact/views/contacts', + '/administrator/components/com_contact/views/contact/tmpl', + '/administrator/components/com_contact/views/contact', + '/administrator/components/com_contact/views', + '/administrator/components/com_contact/tables', + '/administrator/components/com_contact/models/forms/fields', + '/administrator/components/com_contact/models/forms', + '/administrator/components/com_contact/models/fields/modal', + '/administrator/components/com_contact/models/fields', + '/administrator/components/com_contact/models', + '/administrator/components/com_contact/helpers/html', + '/administrator/components/com_contact/controllers', + '/administrator/components/com_config/view/component/tmpl', + '/administrator/components/com_config/view/component', + '/administrator/components/com_config/view/application/tmpl', + '/administrator/components/com_config/view/application', + '/administrator/components/com_config/view', + '/administrator/components/com_config/models', + '/administrator/components/com_config/model/form', + '/administrator/components/com_config/model/field', + '/administrator/components/com_config/model', + '/administrator/components/com_config/helper', + '/administrator/components/com_config/controllers', + '/administrator/components/com_config/controller/component', + '/administrator/components/com_config/controller/application', + '/administrator/components/com_config/controller', + '/administrator/components/com_checkin/views/checkin/tmpl', + '/administrator/components/com_checkin/views/checkin', + '/administrator/components/com_checkin/views', + '/administrator/components/com_checkin/models/forms', + '/administrator/components/com_checkin/models', + '/administrator/components/com_categories/views/category/tmpl', + '/administrator/components/com_categories/views/category', + '/administrator/components/com_categories/views/categories/tmpl', + '/administrator/components/com_categories/views/categories', + '/administrator/components/com_categories/views', + '/administrator/components/com_categories/tables', + '/administrator/components/com_categories/models/forms', + '/administrator/components/com_categories/models/fields/modal', + '/administrator/components/com_categories/models/fields', + '/administrator/components/com_categories/models', + '/administrator/components/com_categories/helpers/html', + '/administrator/components/com_categories/controllers', + '/administrator/components/com_cache/views/purge/tmpl', + '/administrator/components/com_cache/views/purge', + '/administrator/components/com_cache/views/cache/tmpl', + '/administrator/components/com_cache/views/cache', + '/administrator/components/com_cache/views', + '/administrator/components/com_cache/models/forms', + '/administrator/components/com_cache/models', + '/administrator/components/com_cache/helpers', + '/administrator/components/com_banners/views/tracks/tmpl', + '/administrator/components/com_banners/views/tracks', + '/administrator/components/com_banners/views/download/tmpl', + '/administrator/components/com_banners/views/download', + '/administrator/components/com_banners/views/clients/tmpl', + '/administrator/components/com_banners/views/clients', + '/administrator/components/com_banners/views/client/tmpl', + '/administrator/components/com_banners/views/client', + '/administrator/components/com_banners/views/banners/tmpl', + '/administrator/components/com_banners/views/banners', + '/administrator/components/com_banners/views/banner/tmpl', + '/administrator/components/com_banners/views/banner', + '/administrator/components/com_banners/views', + '/administrator/components/com_banners/tables', + '/administrator/components/com_banners/models/forms', + '/administrator/components/com_banners/models/fields', + '/administrator/components/com_banners/models', + '/administrator/components/com_banners/helpers/html', + '/administrator/components/com_banners/controllers', + '/administrator/components/com_associations/views/associations/tmpl', + '/administrator/components/com_associations/views/associations', + '/administrator/components/com_associations/views/association/tmpl', + '/administrator/components/com_associations/views/association', + '/administrator/components/com_associations/views', + '/administrator/components/com_associations/models/forms', + '/administrator/components/com_associations/models/fields', + '/administrator/components/com_associations/models', + '/administrator/components/com_associations/layouts/joomla/searchtools/default', + '/administrator/components/com_associations/helpers', + '/administrator/components/com_associations/controllers', + '/administrator/components/com_admin/views/sysinfo/tmpl', + '/administrator/components/com_admin/views/sysinfo', + '/administrator/components/com_admin/views/profile/tmpl', + '/administrator/components/com_admin/views/profile', + '/administrator/components/com_admin/views/help/tmpl', + '/administrator/components/com_admin/views/help', + '/administrator/components/com_admin/views', + '/administrator/components/com_admin/sql/updates/sqlazure', + '/administrator/components/com_admin/models/forms', + '/administrator/components/com_admin/models', + '/administrator/components/com_admin/helpers/html', + '/administrator/components/com_admin/helpers', + '/administrator/components/com_admin/controllers', + '/administrator/components/com_actionlogs/views/actionlogs/tmpl', + '/administrator/components/com_actionlogs/views/actionlogs', + '/administrator/components/com_actionlogs/views', + '/administrator/components/com_actionlogs/models/forms', + '/administrator/components/com_actionlogs/models/fields', + '/administrator/components/com_actionlogs/models', + '/administrator/components/com_actionlogs/libraries', + '/administrator/components/com_actionlogs/layouts', + '/administrator/components/com_actionlogs/helpers', + '/administrator/components/com_actionlogs/controllers', + // 4.0 from Beta 1 to Beta 2 + '/libraries/vendor/joomla/controller/src', + '/libraries/vendor/joomla/controller', + '/api/components/com_installer/src/View/Languages', + '/administrator/components/com_finder/src/Indexer/Driver', + // 4.0 from Beta 4 to Beta 5 + '/plugins/content/imagelazyload', + // 4.0 from Beta 5 to Beta 6 + '/media/system/js/core.es6', + '/administrator/modules/mod_multilangstatus/src/Helper', + '/administrator/modules/mod_multilangstatus/src', + // 4.0 from Beta 6 to Beta 7 + '/media/vendor/skipto/css', + // 4.0 from Beta 7 to RC 1 + '/templates/system/js', + '/templates/cassiopeia/scss/tools/mixins', + '/plugins/fields/subfields/tmpl', + '/plugins/fields/subfields/params', + '/plugins/fields/subfields', + '/media/vendor/punycode/js', + '/media/templates/atum/js', + '/media/templates/atum', + '/libraries/vendor/paragonie/random_compat/dist', + '/libraries/vendor/paragonie/random_compat', + '/libraries/vendor/ozdemirburak/iris/src/Traits', + '/libraries/vendor/ozdemirburak/iris/src/Helpers', + '/libraries/vendor/ozdemirburak/iris/src/Exceptions', + '/libraries/vendor/ozdemirburak/iris/src/Color', + '/libraries/vendor/ozdemirburak/iris/src', + '/libraries/vendor/ozdemirburak/iris', + '/libraries/vendor/ozdemirburak', + '/libraries/vendor/bin', + '/components/com_menus/src/Controller', + '/components/com_csp/src/Controller', + '/components/com_csp/src', + '/components/com_csp', + '/administrator/templates/atum/Service/HTML', + '/administrator/templates/atum/Service', + '/administrator/components/com_joomlaupdate/src/Helper', + '/administrator/components/com_csp/tmpl/reports', + '/administrator/components/com_csp/tmpl', + '/administrator/components/com_csp/src/View/Reports', + '/administrator/components/com_csp/src/View', + '/administrator/components/com_csp/src/Table', + '/administrator/components/com_csp/src/Model', + '/administrator/components/com_csp/src/Helper', + '/administrator/components/com_csp/src/Controller', + '/administrator/components/com_csp/src', + '/administrator/components/com_csp/services', + '/administrator/components/com_csp/forms', + '/administrator/components/com_csp', + '/administrator/components/com_admin/tmpl/profile', + '/administrator/components/com_admin/src/View/Profile', + '/administrator/components/com_admin/forms', + // 4.0 from RC 5 to RC 6 + '/templates/cassiopeia/scss/vendor/fontawesome-free', + '/templates/cassiopeia/css/vendor/fontawesome-free', + '/media/templates/cassiopeia/js/mod_menu', + '/media/templates/cassiopeia/js', + '/media/templates/cassiopeia', + // 4.0 from RC 6 to 4.0.0 (stable) + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation', + '/libraries/vendor/willdurand/negotiation/tests', + '/libraries/vendor/jakeasmith/http_build_url/tests', + '/libraries/vendor/doctrine/inflector/docs/en', + '/libraries/vendor/doctrine/inflector/docs', + '/libraries/vendor/algo26-matthias/idna-convert/tests/unit', + '/libraries/vendor/algo26-matthias/idna-convert/tests/integration', + '/libraries/vendor/algo26-matthias/idna-convert/tests', + // From 4.0.3 to 4.0.4 + '/templates/cassiopeia/images/system', + // From 4.0.x to 4.1.0-beta1 + '/templates/system/scss', + '/templates/system/css', + '/templates/cassiopeia/scss/vendor/metismenu', + '/templates/cassiopeia/scss/vendor/joomla-custom-elements', + '/templates/cassiopeia/scss/vendor/choicesjs', + '/templates/cassiopeia/scss/vendor/bootstrap', + '/templates/cassiopeia/scss/vendor', + '/templates/cassiopeia/scss/tools/variables', + '/templates/cassiopeia/scss/tools/functions', + '/templates/cassiopeia/scss/tools', + '/templates/cassiopeia/scss/system/searchtools', + '/templates/cassiopeia/scss/system', + '/templates/cassiopeia/scss/global', + '/templates/cassiopeia/scss/blocks', + '/templates/cassiopeia/scss', + '/templates/cassiopeia/js', + '/templates/cassiopeia/images', + '/templates/cassiopeia/css/vendor/joomla-custom-elements', + '/templates/cassiopeia/css/vendor/choicesjs', + '/templates/cassiopeia/css/vendor', + '/templates/cassiopeia/css/system/searchtools', + '/templates/cassiopeia/css/system', + '/templates/cassiopeia/css/global', + '/templates/cassiopeia/css', + '/administrator/templates/system/scss', + '/administrator/templates/system/images', + '/administrator/templates/system/css', + '/administrator/templates/atum/scss/vendor/minicolors', + '/administrator/templates/atum/scss/vendor/joomla-custom-elements', + '/administrator/templates/atum/scss/vendor/fontawesome-free', + '/administrator/templates/atum/scss/vendor/choicesjs', + '/administrator/templates/atum/scss/vendor/bootstrap', + '/administrator/templates/atum/scss/vendor/awesomplete', + '/administrator/templates/atum/scss/vendor', + '/administrator/templates/atum/scss/system/searchtools', + '/administrator/templates/atum/scss/system', + '/administrator/templates/atum/scss/pages', + '/administrator/templates/atum/scss/blocks', + '/administrator/templates/atum/scss', + '/administrator/templates/atum/images/logos', + '/administrator/templates/atum/images', + '/administrator/templates/atum/css/vendor/minicolors', + '/administrator/templates/atum/css/vendor/joomla-custom-elements', + '/administrator/templates/atum/css/vendor/fontawesome-free', + '/administrator/templates/atum/css/vendor/choicesjs', + '/administrator/templates/atum/css/vendor/awesomplete', + '/administrator/templates/atum/css/vendor', + '/administrator/templates/atum/css/system/searchtools', + '/administrator/templates/atum/css/system', + '/administrator/templates/atum/css', + // From 4.1.0-beta3 to 4.1.0-rc1 + '/api/components/com_media/src/Helper', + // From 4.1.0 to 4.1.1 + '/libraries/vendor/tobscure/json-api/tests/Exception/Handler', + '/libraries/vendor/tobscure/json-api/tests/Exception', + '/libraries/vendor/tobscure/json-api/tests', + '/libraries/vendor/tobscure/json-api/.git/refs/tags', + '/libraries/vendor/tobscure/json-api/.git/refs/remotes/origin', + '/libraries/vendor/tobscure/json-api/.git/refs/remotes', + '/libraries/vendor/tobscure/json-api/.git/refs/heads', + '/libraries/vendor/tobscure/json-api/.git/refs', + '/libraries/vendor/tobscure/json-api/.git/objects/pack', + '/libraries/vendor/tobscure/json-api/.git/objects/info', + '/libraries/vendor/tobscure/json-api/.git/objects', + '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes/origin', + '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes', + '/libraries/vendor/tobscure/json-api/.git/logs/refs/heads', + '/libraries/vendor/tobscure/json-api/.git/logs/refs', + '/libraries/vendor/tobscure/json-api/.git/logs', + '/libraries/vendor/tobscure/json-api/.git/info', + '/libraries/vendor/tobscure/json-api/.git/hooks', + '/libraries/vendor/tobscure/json-api/.git/branches', + '/libraries/vendor/tobscure/json-api/.git', + // From 4.1.3 to 4.1.4 + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar', + '/libraries/vendor/maximebf/debugbar/tests', + '/libraries/vendor/maximebf/debugbar/docs', + '/libraries/vendor/maximebf/debugbar/demo/bridge/twig', + '/libraries/vendor/maximebf/debugbar/demo/bridge/swiftmailer', + '/libraries/vendor/maximebf/debugbar/demo/bridge/slim', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel', + '/libraries/vendor/maximebf/debugbar/demo/bridge/monolog', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src/Demo', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine', + '/libraries/vendor/maximebf/debugbar/demo/bridge/cachecache', + '/libraries/vendor/maximebf/debugbar/demo/bridge', + '/libraries/vendor/maximebf/debugbar/demo', + '/libraries/vendor/maximebf/debugbar/build', + // From 4.1 to 4.2.0-beta1 + '/plugins/twofactorauth/yubikey/tmpl', + '/plugins/twofactorauth/yubikey', + '/plugins/twofactorauth/totp/tmpl', + '/plugins/twofactorauth/totp/postinstall', + '/plugins/twofactorauth/totp', + '/plugins/twofactorauth', + '/libraries/vendor/nyholm/psr7/doc', + // From 4.2.0-beta1 to 4.2.0-beta2 + '/layouts/plugins/user/profile/fields', + '/layouts/plugins/user/profile', + ); + + $status['files_checked'] = $files; + $status['folders_checked'] = $folders; + + foreach ($files as $file) { + if ($fileExists = File::exists(JPATH_ROOT . $file)) { + $status['files_exist'][] = $file; + + if ($dryRun === false) { + if (File::delete(JPATH_ROOT . $file)) { + $status['files_deleted'][] = $file; + } else { + $status['files_errors'][] = Text::sprintf('FILES_JOOMLA_ERROR_FILE_FOLDER', $file); + } + } + } + } + + $this->moveRemainingTemplateFiles(); + + foreach ($folders as $folder) { + if ($folderExists = Folder::exists(JPATH_ROOT . $folder)) { + $status['folders_exist'][] = $folder; + + if ($dryRun === false) { + if (Folder::delete(JPATH_ROOT . $folder)) { + $status['folders_deleted'][] = $folder; + } else { + $status['folders_errors'][] = Text::sprintf('FILES_JOOMLA_ERROR_FILE_FOLDER', $folder); + } + } + } + } + + $this->fixFilenameCasing(); + + /* + * Needed for updates from 3.10 + * If com_search doesn't exist then assume we can delete the search package manifest (included in the update packages) + * We deliberately check for the presence of the files in case people have previously uninstalled their search extension + * but an update has put the files back. In that case it exists even if they don't believe in it! + */ + if ( + !File::exists(JPATH_ROOT . '/administrator/components/com_search/search.php') + && File::exists(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml') + ) { + File::delete(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml'); + } + + if ($suppressOutput === false && count($status['folders_errors'])) { + echo implode('
    ', $status['folders_errors']); + } + + if ($suppressOutput === false && count($status['files_errors'])) { + echo implode('
    ', $status['files_errors']); + } + + return $status; + } + + /** + * Method to create assets for newly installed components + * + * @param Installer $installer The class calling this method + * + * @return boolean + * + * @since 3.2 + */ + public function updateAssets($installer) + { + // List all components added since 4.0 + $newComponents = array( + // Components to be added here + ); + + foreach ($newComponents as $component) { + /** @var \Joomla\CMS\Table\Asset $asset */ + $asset = Table::getInstance('Asset'); + + if ($asset->loadByName($component)) { + continue; + } + + $asset->name = $component; + $asset->parent_id = 1; + $asset->rules = '{}'; + $asset->title = $component; + $asset->setLocation(1, 'last-child'); + + if (!$asset->store()) { + // Install failed, roll back changes + $installer->abort(Text::sprintf('JLIB_INSTALLER_ABORT_COMP_INSTALL_ROLLBACK', $asset->getError(true))); + + return false; + } + } + + return true; + } + + /** + * Converts the site's database tables to support UTF-8 Multibyte. + * + * @param boolean $doDbFixMsg Flag if message to be shown to check db fix + * + * @return void + * + * @since 3.5 + */ + public function convertTablesToUtf8mb4($doDbFixMsg = false) + { + $db = Factory::getDbo(); + + if ($db->getServerType() !== 'mysql') { + return; + } + + // Check if the #__utf8_conversion table exists + $db->setQuery('SHOW TABLES LIKE ' . $db->quote($db->getPrefix() . 'utf8_conversion')); + + try { + $rows = $db->loadRowList(0); + } catch (Exception $e) { + // Render the error message from the Exception object + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + if ($doDbFixMsg) { + // Show an error message telling to check database problems + Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); + } + + return; + } + + // Nothing to do if the table doesn't exist because the CMS has never been updated from a pre-4.0 version + if (count($rows) === 0) { + return; + } + + // Set required conversion status + $converted = 5; + + // Check conversion status in database + $db->setQuery( + 'SELECT ' . $db->quoteName('converted') + . ' FROM ' . $db->quoteName('#__utf8_conversion') + ); + + try { + $convertedDB = $db->loadResult(); + } catch (Exception $e) { + // Render the error message from the Exception object + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + if ($doDbFixMsg) { + // Show an error message telling to check database problems + Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); + } + + return; + } + + // If conversion status from DB is equal to required final status, try to drop the #__utf8_conversion table + if ($convertedDB === $converted) { + $this->dropUtf8ConversionTable(); + + return; + } + + // Perform the required conversions of core tables if not done already in a previous step + if ($convertedDB !== 99) { + $fileName1 = JPATH_ROOT . '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion.sql'; + + if (is_file($fileName1)) { + $fileContents1 = @file_get_contents($fileName1); + $queries1 = $db->splitSql($fileContents1); + + if (!empty($queries1)) { + foreach ($queries1 as $query1) { + try { + $db->setQuery($query1)->execute(); + } catch (Exception $e) { + $converted = $convertedDB; + + // Still render the error message from the Exception object + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + } + } + } + + // If no error before, perform the optional conversions of tables which might or might not exist + if ($converted === 5) { + $fileName2 = JPATH_ROOT . '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion_optional.sql'; + + if (is_file($fileName2)) { + $fileContents2 = @file_get_contents($fileName2); + $queries2 = $db->splitSql($fileContents2); + + if (!empty($queries2)) { + foreach ($queries2 as $query2) { + // Get table name from query + if (preg_match('/^ALTER\s+TABLE\s+([^\s]+)\s+/i', $query2, $matches) === 1) { + $tableName = str_replace('`', '', $matches[1]); + $tableName = str_replace('#__', $db->getPrefix(), $tableName); + + // Check if the table exists and if yes, run the query + try { + $db->setQuery('SHOW TABLES LIKE ' . $db->quote($tableName)); + + $rows = $db->loadRowList(0); + + if (count($rows) > 0) { + $db->setQuery($query2)->execute(); + } + } catch (Exception $e) { + $converted = 99; + + // Still render the error message from the Exception object + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + } + } + } + } + + if ($doDbFixMsg && $converted !== 5) { + // Show an error message telling to check database problems + Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); + } + + // If the conversion was successful try to drop the #__utf8_conversion table + if ($converted === 5 && $this->dropUtf8ConversionTable()) { + // Table successfully dropped + return; + } + + // Set flag in database if the conversion status has changed. + if ($converted !== $convertedDB) { + $db->setQuery('UPDATE ' . $db->quoteName('#__utf8_conversion') + . ' SET ' . $db->quoteName('converted') . ' = ' . $converted . ';')->execute(); + } + } + + /** + * This method clean the Joomla Cache using the method `clean` from the com_cache model + * + * @return void + * + * @since 3.5.1 + */ + private function cleanJoomlaCache() + { + /** @var \Joomla\Component\Cache\Administrator\Model\CacheModel $model */ + $model = Factory::getApplication()->bootComponent('com_cache')->getMVCFactory() + ->createModel('Cache', 'Administrator', ['ignore_request' => true]); + + // Clean frontend cache + $model->clean(); + + // Clean admin cache + $model->setState('client_id', 1); + $model->clean(); + } + + /** + * This method drops the #__utf8_conversion table + * + * @return boolean True on success + * + * @since 4.0.0 + */ + private function dropUtf8ConversionTable() + { + $db = Factory::getDbo(); + + try { + $db->setQuery('DROP TABLE ' . $db->quoteName('#__utf8_conversion') . ';')->execute(); + } catch (Exception $e) { + return false; + } + + return true; + } + + /** + * Called after any type of action + * + * @param string $action Which action is happening (install|uninstall|discover_install|update) + * @param Installer $installer The class calling this method + * + * @return boolean True on success + * + * @since 4.0.0 + */ + public function postflight($action, $installer) + { + if ($action !== 'update') { + return true; + } + + if (empty($this->fromVersion) || version_compare($this->fromVersion, '4.0.0', 'ge')) { + return true; + } + + // Update UCM content types. + $this->updateContentTypes(); + + $db = Factory::getDbo(); + Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/Table/'); + + $tableItem = new \Joomla\Component\Menus\Administrator\Table\MenuTable($db); + + $contactItems = $this->contactItems($tableItem); + $finderItems = $this->finderItems($tableItem); + + $menuItems = array_merge($contactItems, $finderItems); + + foreach ($menuItems as $menuItem) { + // Check an existing record + $keys = [ + 'menutype' => $menuItem['menutype'], + 'type' => $menuItem['type'], + 'title' => $menuItem['title'], + 'parent_id' => $menuItem['parent_id'], + 'client_id' => $menuItem['client_id'], + ]; + + if ($tableItem->load($keys)) { + continue; + } + + $newTableItem = new \Joomla\Component\Menus\Administrator\Table\MenuTable($db); + + // Bind the data. + if (!$newTableItem->bind($menuItem)) { + return false; + } + + $newTableItem->setLocation($menuItem['parent_id'], 'last-child'); + + // Check the data. + if (!$newTableItem->check()) { + return false; + } + + // Store the data. + if (!$newTableItem->store()) { + return false; + } + + // Rebuild the tree path. + if (!$newTableItem->rebuildPath($newTableItem->id)) { + return false; + } + } + + return true; + } + + /** + * Prepare the contact menu items + * + * @return array Menu items + * + * @since 4.0.0 + */ + private function contactItems(Table $tableItem): array + { + // Check for the Contact parent Id Menu Item + $keys = [ + 'menutype' => 'main', + 'type' => 'component', + 'title' => 'com_contact', + 'parent_id' => 1, + 'client_id' => 1, + ]; + + $contactMenuitem = $tableItem->load($keys); + + if (!$contactMenuitem) { + return []; + } + + $parentId = $tableItem->id; + $componentId = ExtensionHelper::getExtensionRecord('com_fields', 'component')->extension_id; + + // Add Contact Fields Menu Items. + $menuItems = [ + [ + 'menutype' => 'main', + 'title' => '-', + 'alias' => microtime(true), + 'note' => '', + 'path' => '', + 'link' => '#', + 'type' => 'separator', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'mod_menu_fields', + 'alias' => 'Contact Custom Fields', + 'note' => '', + 'path' => 'contact/Custom Fields', + 'link' => 'index.php?option=com_fields&context=com_contact.contact', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'mod_menu_fields_group', + 'alias' => 'Contact Custom Fields Group', + 'note' => '', + 'path' => 'contact/Custom Fields Group', + 'link' => 'index.php?option=com_fields&view=groups&context=com_contact.contact', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ] + ]; + + return $menuItems; + } + + /** + * Prepare the finder menu items + * + * @return array Menu items + * + * @since 4.0.0 + */ + private function finderItems(Table $tableItem): array + { + // Check for the Finder parent Id Menu Item + $keys = [ + 'menutype' => 'main', + 'type' => 'component', + 'title' => 'com_finder', + 'parent_id' => 1, + 'client_id' => 1, + ]; + + $finderMenuitem = $tableItem->load($keys); + + if (!$finderMenuitem) { + return []; + } + + $parentId = $tableItem->id; + $componentId = ExtensionHelper::getExtensionRecord('com_finder', 'component')->extension_id; + + // Add Finder Fields Menu Items. + $menuItems = [ + [ + 'menutype' => 'main', + 'title' => '-', + 'alias' => microtime(true), + 'note' => '', + 'path' => '', + 'link' => '#', + 'type' => 'separator', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'com_finder_index', + 'alias' => 'Smart-Search-Index', + 'note' => '', + 'path' => 'Smart Search/Index', + 'link' => 'index.php?option=com_finder&view=index', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'com_finder_maps', + 'alias' => 'Smart-Search-Maps', + 'note' => '', + 'path' => 'Smart Search/Maps', + 'link' => 'index.php?option=com_finder&view=maps', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'com_finder_filters', + 'alias' => 'Smart-Search-Filters', + 'note' => '', + 'path' => 'Smart Search/Filters', + 'link' => 'index.php?option=com_finder&view=filters', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'com_finder_searches', + 'alias' => 'Smart-Search-Searches', + 'note' => '', + 'path' => 'Smart Search/Searches', + 'link' => 'index.php?option=com_finder&view=searches', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ] + ]; + + return $menuItems; + } + + /** + * Updates content type table classes. + * + * @return void + * + * @since 4.0.0 + */ + private function updateContentTypes(): void + { + // Content types to update. + $contentTypes = [ + 'com_content.article', + 'com_contact.contact', + 'com_newsfeeds.newsfeed', + 'com_tags.tag', + 'com_banners.banner', + 'com_banners.client', + 'com_users.note', + 'com_content.category', + 'com_contact.category', + 'com_newsfeeds.category', + 'com_banners.category', + 'com_users.category', + 'com_users.user', + ]; + + // Get table definitions. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('type_alias'), + $db->quoteName('table'), + ] + ) + ->from($db->quoteName('#__content_types')) + ->whereIn($db->quoteName('type_alias'), $contentTypes, ParameterType::STRING); + + $db->setQuery($query); + $contentTypes = $db->loadObjectList(); + + // Prepare the update query. + $query = $db->getQuery(true) + ->update($db->quoteName('#__content_types')) + ->set($db->quoteName('table') . ' = :table') + ->where($db->quoteName('type_alias') . ' = :typeAlias') + ->bind(':table', $table) + ->bind(':typeAlias', $typeAlias); + + $db->setQuery($query); + + foreach ($contentTypes as $contentType) { + list($component, $tableType) = explode('.', $contentType->type_alias); + + // Special case for core table classes. + if ($contentType->type_alias === 'com_users.users' || $tableType === 'category') { + $tablePrefix = 'Joomla\\CMS\Table\\'; + $tableType = ucfirst($tableType); + } else { + $tablePrefix = 'Joomla\\Component\\' . ucfirst(substr($component, 4)) . '\\Administrator\\Table\\'; + $tableType = ucfirst($tableType) . 'Table'; + } + + // Bind type alias. + $typeAlias = $contentType->type_alias; + + $table = json_decode($contentType->table); + + // Update table definitions. + $table->special->type = $tableType; + $table->special->prefix = $tablePrefix; + + // Some content types don't have this property. + if (!empty($table->common->prefix)) { + $table->common->prefix = 'Joomla\\CMS\\Table\\'; + } + + $table = json_encode($table); + + // Execute the query. + $db->execute(); + } + } + + /** + * Renames or removes incorrectly cased files. + * + * @return void + * + * @since 3.9.25 + */ + protected function fixFilenameCasing() + { + $files = array( + // 3.10 changes + '/libraries/src/Filesystem/Support/Stringcontroller.php' => '/libraries/src/Filesystem/Support/StringController.php', + '/libraries/src/Form/Rule/SubFormRule.php' => '/libraries/src/Form/Rule/SubformRule.php', + // 4.0.0 + '/media/vendor/skipto/js/skipTo.js' => '/media/vendor/skipto/js/skipto.js', + ); + + foreach ($files as $old => $expected) { + $oldRealpath = realpath(JPATH_ROOT . $old); + + // On Unix without incorrectly cased file. + if ($oldRealpath === false) { + continue; + } + + $oldBasename = basename($oldRealpath); + $newRealpath = realpath(JPATH_ROOT . $expected); + $newBasename = basename($newRealpath); + $expectedBasename = basename($expected); + + // On Windows or Unix with only the incorrectly cased file. + if ($newBasename !== $expectedBasename) { + // Rename the file. + File::move(JPATH_ROOT . $old, JPATH_ROOT . $old . '.tmp'); + File::move(JPATH_ROOT . $old . '.tmp', JPATH_ROOT . $expected); + + continue; + } + + // There might still be an incorrectly cased file on other OS than Windows. + if ($oldBasename === basename($old)) { + // Check if case-insensitive file system, eg on OSX. + if (fileinode($oldRealpath) === fileinode($newRealpath)) { + // Check deeper because even realpath or glob might not return the actual case. + if (!in_array($expectedBasename, scandir(dirname($newRealpath)))) { + // Rename the file. + File::move(JPATH_ROOT . $old, JPATH_ROOT . $old . '.tmp'); + File::move(JPATH_ROOT . $old . '.tmp', JPATH_ROOT . $expected); + } + } else { + // On Unix with both files: Delete the incorrectly cased file. + File::delete(JPATH_ROOT . $old); + } + } + } + } + + /** + * Move core template (s)css or js or image files which are left after deleting + * obsolete core files to the right place in media folder. + * + * @return void + * + * @since 4.1.0 + */ + protected function moveRemainingTemplateFiles() + { + $folders = [ + '/administrator/templates/atum/css' => '/media/templates/administrator/atum/css', + '/administrator/templates/atum/images' => '/media/templates/administrator/atum/images', + '/administrator/templates/atum/js' => '/media/templates/administrator/atum/js', + '/administrator/templates/atum/scss' => '/media/templates/administrator/atum/scss', + '/templates/cassiopeia/css' => '/media/templates/site/cassiopeia/css', + '/templates/cassiopeia/images' => '/media/templates/site/cassiopeia/images', + '/templates/cassiopeia/js' => '/media/templates/site/cassiopeia/js', + '/templates/cassiopeia/scss' => '/media/templates/site/cassiopeia/scss', + ]; + + foreach ($folders as $oldFolder => $newFolder) { + if (Folder::exists(JPATH_ROOT . $oldFolder)) { + $oldPath = realpath(JPATH_ROOT . $oldFolder); + $newPath = realpath(JPATH_ROOT . $newFolder); + $directory = new \RecursiveDirectoryIterator($oldPath); + $directory->setFlags(RecursiveDirectoryIterator::SKIP_DOTS); + $iterator = new \RecursiveIteratorIterator($directory); + + // Handle all files in this folder and all sub-folders + foreach ($iterator as $oldFile) { + if ($oldFile->isDir()) { + continue; + } + + $newFile = $newPath . substr($oldFile, strlen($oldPath)); + + // Create target folder and parent folders if they don't exist yet + if (is_dir(dirname($newFile)) || @mkdir(dirname($newFile), 0755, true)) { + File::move($oldFile, $newFile); + } + } + } + } + } + + /** + * Ensure the core templates are correctly moved to the new mode. + * + * @return void + * + * @since 4.1.0 + */ + protected function fixTemplateMode(): void + { + $db = Factory::getContainer()->get('DatabaseDriver'); + + array_map( + function ($template) use ($db) { + $clientId = $template === 'atum' ? 1 : 0; + $query = $db->getQuery(true) + ->update($db->quoteName('#__template_styles')) + ->set($db->quoteName('inheritable') . ' = 1') + ->where($db->quoteName('template') . ' = ' . $db->quote($template)) + ->where($db->quoteName('client_id') . ' = ' . $clientId); + + try { + $db->setQuery($query)->execute(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
    '; + + return; + } + }, + ['atum', 'cassiopeia'] + ); + } + + /** + * Add the user Auth Provider Column as it could be present from 3.10 already + * + * @return void + * + * @since 4.1.1 + */ + protected function addUserAuthProviderColumn(): void + { + $db = Factory::getContainer()->get('DatabaseDriver'); + + // Check if the column already exists + $fields = $db->getTableColumns('#__users'); + + // Column exists, skip + if (isset($fields['authProvider'])) { + return; + } + + $query = 'ALTER TABLE ' . $db->quoteName('#__users') + . ' ADD COLUMN ' . $db->quoteName('authProvider') . ' varchar(100) DEFAULT ' . $db->quote('') . ' NOT NULL'; + + // Add column + try { + $db->setQuery($query)->execute(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
    '; + + return; + } + } } diff --git a/administrator/components/com_admin/services/provider.php b/administrator/components/com_admin/services/provider.php index 9973ba6aefd3b..d17a3d0cc5faf 100644 --- a/administrator/components/com_admin/services/provider.php +++ b/administrator/components/com_admin/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Admin')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Admin')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Admin')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Admin')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new AdminComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new AdminComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_admin/src/Controller/DisplayController.php b/administrator/components/com_admin/src/Controller/DisplayController.php index 3857024e52e86..bdee398558447 100644 --- a/administrator/components/com_admin/src/Controller/DisplayController.php +++ b/administrator/components/com_admin/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', $this->default_view); - $format = $this->input->get('format', 'html'); + /** + * View method + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static Supports chaining. + * + * @since 3.9 + */ + public function display($cachable = false, $urlparams = array()) + { + $viewName = $this->input->get('view', $this->default_view); + $format = $this->input->get('format', 'html'); - // Check CSRF token for sysinfo export views - if ($viewName === 'sysinfo' && ($format === 'text' || $format === 'json')) - { - // Check for request forgeries. - $this->checkToken('GET'); - } + // Check CSRF token for sysinfo export views + if ($viewName === 'sysinfo' && ($format === 'text' || $format === 'json')) { + // Check for request forgeries. + $this->checkToken('GET'); + } - return parent::display($cachable, $urlparams); - } + return parent::display($cachable, $urlparams); + } } diff --git a/administrator/components/com_admin/src/Dispatcher/Dispatcher.php b/administrator/components/com_admin/src/Dispatcher/Dispatcher.php index 082a22e9f51f1..8340df60b8d08 100644 --- a/administrator/components/com_admin/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_admin/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ getRegistry()->register('system', new System); - $this->getRegistry()->register('phpsetting', new PhpSetting); - $this->getRegistry()->register('directory', new Directory); - $this->getRegistry()->register('configuration', new Configuration); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('system', new System()); + $this->getRegistry()->register('phpsetting', new PhpSetting()); + $this->getRegistry()->register('directory', new Directory()); + $this->getRegistry()->register('configuration', new Configuration()); + } } diff --git a/administrator/components/com_admin/src/Model/HelpModel.php b/administrator/components/com_admin/src/Model/HelpModel.php index cec67d3093dcb..4cf7d8925ea9a 100644 --- a/administrator/components/com_admin/src/Model/HelpModel.php +++ b/administrator/components/com_admin/src/Model/HelpModel.php @@ -1,4 +1,5 @@ help_search)) - { - $this->help_search = Factory::getApplication()->input->getString('helpsearch'); - } - - return $this->help_search; - } - - /** - * Method to get the page - * - * @return string The page - * - * @since 1.6 - */ - public function &getPage() - { - if (\is_null($this->page)) - { - $this->page = Help::createUrl(Factory::getApplication()->input->get('page', 'Start_Here')); - } - - return $this->page; - } - - /** - * Method to get the lang tag - * - * @return string lang iso tag - * - * @since 1.6 - */ - public function getLangTag() - { - if (\is_null($this->lang_tag)) - { - $this->lang_tag = Factory::getLanguage()->getTag(); - - if (!is_dir(JPATH_BASE . '/help/' . $this->lang_tag)) - { - // Use English as fallback - $this->lang_tag = 'en-GB'; - } - } - - return $this->lang_tag; - } - - /** - * Method to get the table of contents - * - * @return array Table of contents - */ - public function &getToc() - { - if (!\is_null($this->toc)) - { - return $this->toc; - } - - // Get vars - $lang_tag = $this->getLangTag(); - $help_search = $this->getHelpSearch(); - - // New style - Check for a TOC \JSON file - if (file_exists(JPATH_BASE . '/help/' . $lang_tag . '/toc.json')) - { - $data = json_decode(file_get_contents(JPATH_BASE . '/help/' . $lang_tag . '/toc.json')); - - // Loop through the data array - foreach ($data as $key => $value) - { - $this->toc[$key] = Text::_('COM_ADMIN_HELP_' . $value); - } - - // Sort the Table of Contents - asort($this->toc); - - return $this->toc; - } - - // Get Help files - $files = Folder::files(JPATH_BASE . '/help/' . $lang_tag, '\.xml$|\.html$'); - $this->toc = array(); - - foreach ($files as $file) - { - $buffer = file_get_contents(JPATH_BASE . '/help/' . $lang_tag . '/' . $file); - - if (!preg_match('#(.*?)#', $buffer, $m)) - { - continue; - } - - $title = trim($m[1]); - - if (!$title) - { - continue; - } - - // Translate the page title - $title = Text::_($title); - - // Strip the extension - $file = preg_replace('#\.xml$|\.html$#', '', $file); - - if ($help_search && StringHelper::strpos(StringHelper::strtolower(strip_tags($buffer)), StringHelper::strtolower($help_search)) === false) - { - continue; - } - - // Add an item in the Table of Contents - $this->toc[$file] = $title; - } - - // Sort the Table of Contents - asort($this->toc); - - return $this->toc; - } + /** + * The search string + * + * @var string + * @since 1.6 + */ + protected $help_search = null; + + /** + * The page to be viewed + * + * @var string + * @since 1.6 + */ + protected $page = null; + + /** + * The ISO language tag + * + * @var string + * @since 1.6 + */ + protected $lang_tag = null; + + /** + * Table of contents + * + * @var array + * @since 1.6 + */ + protected $toc = null; + + /** + * URL for the latest version check + * + * @var string + * @since 1.6 + */ + protected $latest_version_check = null; + + /** + * Method to get the help search string + * + * @return string Help search string + * + * @since 1.6 + */ + public function &getHelpSearch() + { + if (\is_null($this->help_search)) { + $this->help_search = Factory::getApplication()->input->getString('helpsearch'); + } + + return $this->help_search; + } + + /** + * Method to get the page + * + * @return string The page + * + * @since 1.6 + */ + public function &getPage() + { + if (\is_null($this->page)) { + $this->page = Help::createUrl(Factory::getApplication()->input->get('page', 'Start_Here')); + } + + return $this->page; + } + + /** + * Method to get the lang tag + * + * @return string lang iso tag + * + * @since 1.6 + */ + public function getLangTag() + { + if (\is_null($this->lang_tag)) { + $this->lang_tag = Factory::getLanguage()->getTag(); + + if (!is_dir(JPATH_BASE . '/help/' . $this->lang_tag)) { + // Use English as fallback + $this->lang_tag = 'en-GB'; + } + } + + return $this->lang_tag; + } + + /** + * Method to get the table of contents + * + * @return array Table of contents + */ + public function &getToc() + { + if (!\is_null($this->toc)) { + return $this->toc; + } + + // Get vars + $lang_tag = $this->getLangTag(); + $help_search = $this->getHelpSearch(); + + // New style - Check for a TOC \JSON file + if (file_exists(JPATH_BASE . '/help/' . $lang_tag . '/toc.json')) { + $data = json_decode(file_get_contents(JPATH_BASE . '/help/' . $lang_tag . '/toc.json')); + + // Loop through the data array + foreach ($data as $key => $value) { + $this->toc[$key] = Text::_('COM_ADMIN_HELP_' . $value); + } + + // Sort the Table of Contents + asort($this->toc); + + return $this->toc; + } + + // Get Help files + $files = Folder::files(JPATH_BASE . '/help/' . $lang_tag, '\.xml$|\.html$'); + $this->toc = array(); + + foreach ($files as $file) { + $buffer = file_get_contents(JPATH_BASE . '/help/' . $lang_tag . '/' . $file); + + if (!preg_match('#(.*?)#', $buffer, $m)) { + continue; + } + + $title = trim($m[1]); + + if (!$title) { + continue; + } + + // Translate the page title + $title = Text::_($title); + + // Strip the extension + $file = preg_replace('#\.xml$|\.html$#', '', $file); + + if ($help_search && StringHelper::strpos(StringHelper::strtolower(strip_tags($buffer)), StringHelper::strtolower($help_search)) === false) { + continue; + } + + // Add an item in the Table of Contents + $this->toc[$file] = $title; + } + + // Sort the Table of Contents + asort($this->toc); + + return $this->toc; + } } diff --git a/administrator/components/com_admin/src/Model/SysinfoModel.php b/administrator/components/com_admin/src/Model/SysinfoModel.php index de12194ee3963..d3e1e55cac225 100644 --- a/administrator/components/com_admin/src/Model/SysinfoModel.php +++ b/administrator/components/com_admin/src/Model/SysinfoModel.php @@ -1,4 +1,5 @@ [ - 'CONTEXT_DOCUMENT_ROOT', - 'Cookie', - 'DOCUMENT_ROOT', - 'extension_dir', - 'error_log', - 'Host', - 'HTTP_COOKIE', - 'HTTP_HOST', - 'HTTP_ORIGIN', - 'HTTP_REFERER', - 'HTTP Request', - 'include_path', - 'mysql.default_socket', - 'MYSQL_SOCKET', - 'MYSQL_INCLUDE', - 'MYSQL_LIBS', - 'mysqli.default_socket', - 'MYSQLI_SOCKET', - 'PATH', - 'Path to sendmail', - 'pdo_mysql.default_socket', - 'Referer', - 'REMOTE_ADDR', - 'SCRIPT_FILENAME', - 'sendmail_path', - 'SERVER_ADDR', - 'SERVER_ADMIN', - 'Server Administrator', - 'SERVER_NAME', - 'Server Root', - 'session.name', - 'session.save_path', - 'upload_tmp_dir', - 'User/Group', - 'open_basedir', - ], - 'other' => [ - 'db', - 'dbprefix', - 'fromname', - 'live_site', - 'log_path', - 'mailfrom', - 'memcached_server_host', - 'open_basedir', - 'Origin', - 'proxy_host', - 'proxy_user', - 'proxy_pass', - 'redis_server_host', - 'redis_server_auth', - 'secret', - 'sendmail', - 'session.save_path', - 'session_memcached_server_host', - 'session_redis_server_host', - 'session_redis_server_auth', - 'sitename', - 'smtphost', - 'tmp_path', - 'open_basedir', - ] - ]; - - /** - * System values that can be "safely" shared - * - * @var array - * - * @since 3.5 - */ - protected $safeData; - - /** - * Information about writable state of directories - * - * @var array - * @since 1.6 - */ - protected $directories = []; - - /** - * The current editor. - * - * @var string - * @since 1.6 - */ - protected $editor = null; - - /** - * Remove sections of data marked as private in the privateSettings - * - * @param array $dataArray Array with data that may contain private information - * @param string $dataType Type of data to search for a specific section in the privateSettings array - * - * @return array - * - * @since 3.5 - */ - protected function cleanPrivateData(array $dataArray, string $dataType = 'other'): array - { - $dataType = isset($this->privateSettings[$dataType]) ? $dataType : 'other'; - - $privateSettings = $this->privateSettings[$dataType]; - - if (!$privateSettings) - { - return $dataArray; - } - - foreach ($dataArray as $section => $values) - { - if (\is_array($values)) - { - $dataArray[$section] = $this->cleanPrivateData($values, $dataType); - } - - if (\in_array($section, $privateSettings, true)) - { - $dataArray[$section] = $this->cleanSectionPrivateData($values); - } - } - - return $dataArray; - } - - /** - * Obfuscate section values - * - * @param mixed $sectionValues Section data - * - * @return string|array - * - * @since 3.5 - */ - protected function cleanSectionPrivateData($sectionValues) - { - if (!\is_array($sectionValues)) - { - if (strstr($sectionValues, JPATH_ROOT)) - { - $sectionValues = 'xxxxxx'; - } - - return \strlen($sectionValues) ? 'xxxxxx' : ''; - } - - foreach ($sectionValues as $setting => $value) - { - $sectionValues[$setting] = \strlen($value) ? 'xxxxxx' : ''; - } - - return $sectionValues; - } - - /** - * Method to get the PHP settings - * - * @return array Some PHP settings - * - * @since 1.6 - */ - public function &getPhpSettings(): array - { - if (!empty($this->php_settings)) - { - return $this->php_settings; - } - - $this->php_settings = [ - 'memory_limit' => ini_get('memory_limit'), - 'upload_max_filesize' => ini_get('upload_max_filesize'), - 'post_max_size' => ini_get('post_max_size'), - 'display_errors' => ini_get('display_errors') == '1', - 'short_open_tag' => ini_get('short_open_tag') == '1', - 'file_uploads' => ini_get('file_uploads') == '1', - 'output_buffering' => (int) ini_get('output_buffering') !== 0, - 'open_basedir' => ini_get('open_basedir'), - 'session.save_path' => ini_get('session.save_path'), - 'session.auto_start' => ini_get('session.auto_start'), - 'disable_functions' => ini_get('disable_functions'), - 'xml' => \extension_loaded('xml'), - 'zlib' => \extension_loaded('zlib'), - 'zip' => \function_exists('zip_open') && \function_exists('zip_read'), - 'mbstring' => \extension_loaded('mbstring'), - 'fileinfo' => \extension_loaded('fileinfo'), - 'gd' => \extension_loaded('gd'), - 'iconv' => \function_exists('iconv'), - 'intl' => \function_exists('transliterator_transliterate'), - 'max_input_vars' => ini_get('max_input_vars'), - ]; - - return $this->php_settings; - } - - /** - * Method to get the config - * - * @return array config values - * - * @since 1.6 - */ - public function &getConfig(): array - { - if (!empty($this->config)) - { - return $this->config; - } - - $registry = new Registry(new \JConfig); - $this->config = $registry->toArray(); - $hidden = [ - 'host', 'user', 'password', 'ftp_user', 'ftp_pass', - 'smtpuser', 'smtppass', 'redis_server_auth', 'session_redis_server_auth', - 'proxy_user', 'proxy_pass', 'secret' - ]; - - foreach ($hidden as $key) - { - $this->config[$key] = 'xxxxxx'; - } - - return $this->config; - } - - /** - * Method to get the system information - * - * @return array System information values - * - * @since 1.6 - */ - public function &getInfo(): array - { - if (!empty($this->info)) - { - return $this->info; - } - - $db = $this->getDatabase(); - - $this->info = [ - 'php' => php_uname(), - 'dbserver' => $db->getServerType(), - 'dbversion' => $db->getVersion(), - 'dbcollation' => $db->getCollation(), - 'dbconnectioncollation' => $db->getConnectionCollation(), - 'dbconnectionencryption' => $db->getConnectionEncryption(), - 'dbconnencryptsupported' => $db->isConnectionEncryptionSupported(), - 'phpversion' => PHP_VERSION, - 'server' => $_SERVER['SERVER_SOFTWARE'] ?? getenv('SERVER_SOFTWARE'), - 'sapi_name' => PHP_SAPI, - 'version' => (new Version)->getLongVersion(), - 'useragent' => $_SERVER['HTTP_USER_AGENT'] ?? '', - ]; - - return $this->info; - } - - /** - * Check if the phpinfo function is enabled - * - * @return boolean True if enabled - * - * @since 3.4.1 - */ - public function phpinfoEnabled(): bool - { - return !\in_array('phpinfo', explode(',', ini_get('disable_functions'))); - } - - /** - * Method to get filter data from the model - * - * @param string $dataType Type of data to get safely - * @param bool $public If true no sensitive information will be removed - * - * @return array - * - * @since 3.5 - */ - public function getSafeData(string $dataType, bool $public = true): array - { - if (isset($this->safeData[$dataType])) - { - return $this->safeData[$dataType]; - } - - $methodName = 'get' . ucfirst($dataType); - - if (!method_exists($this, $methodName)) - { - return []; - } - - $data = $this->$methodName($public); - - $this->safeData[$dataType] = $this->cleanPrivateData($data, $dataType); - - return $this->safeData[$dataType]; - } - - /** - * Method to get the PHP info - * - * @return string PHP info - * - * @since 1.6 - */ - public function &getPHPInfo(): string - { - if (!$this->phpinfoEnabled()) - { - $this->php_info = Text::_('COM_ADMIN_PHPINFO_DISABLED'); - - return $this->php_info; - } - - if (!\is_null($this->php_info)) - { - return $this->php_info; - } - - ob_start(); - date_default_timezone_set('UTC'); - phpinfo(INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES); - $phpInfo = ob_get_contents(); - ob_end_clean(); - preg_match_all('#]*>(.*)#siU', $phpInfo, $output); - $output = preg_replace('#]*>#', '', $output[1][0]); - $output = preg_replace('#(\w),(\w)#', '\1, \2', $output); - $output = preg_replace('#
    #', '', $output); - $output = str_replace('
    ', '', $output); - $output = preg_replace('#
    (.*)#', '$1', $output); - $output = str_replace('
    ', '', $output); - $output = str_replace('', '', $output); - $this->php_info = $output; - - return $this->php_info; - } - - /** - * Get phpinfo() output as array - * - * @return array - * - * @since 3.5 - */ - public function getPhpInfoArray(): array - { - // Already cached - if (null !== $this->phpInfoArray) - { - return $this->phpInfoArray; - } - - $phpInfo = $this->getPHPInfo(); - - $this->phpInfoArray = $this->parsePhpInfo($phpInfo); - - return $this->phpInfoArray; - } - - /** - * Method to get a list of installed extensions - * - * @return array installed extensions - * - * @since 3.5 - */ - public function getExtensions(): array - { - $installed = []; - $db = Factory::getContainer()->get('DatabaseDriver'); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__extensions')); - $db->setQuery($query); - - try - { - $extensions = $db->loadObjectList(); - } - catch (\Exception $e) - { - try - { - Log::add(Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()), - 'warning' - ); - } - - return $installed; - } - - if (empty($extensions)) - { - return $installed; - } - - foreach ($extensions as $extension) - { - if (\strlen($extension->name) == 0) - { - continue; - } - - $installed[$extension->name] = [ - 'name' => $extension->name, - 'type' => $extension->type, - 'state' => $extension->enabled ? Text::_('JENABLED') : Text::_('JDISABLED'), - 'author' => 'unknown', - 'version' => 'unknown', - 'creationDate' => 'unknown', - 'authorUrl' => 'unknown', - ]; - - $manifest = new Registry($extension->manifest_cache); - - $extraData = [ - 'author' => $manifest->get('author', ''), - 'version' => $manifest->get('version', ''), - 'creationDate' => $manifest->get('creationDate', ''), - 'authorUrl' => $manifest->get('authorUrl', '') - ]; - - $installed[$extension->name] = array_merge($installed[$extension->name], $extraData); - } - - return $installed; - } - - /** - * Method to get the directory states - * - * @param bool $public If true no information is going to be removed - * - * @return array States of directories - * - * @throws \Exception - * @since 1.6 - */ - public function getDirectory(bool $public = false): array - { - if (!empty($this->directories)) - { - return $this->directories; - } - - $this->directories = []; - - $registry = Factory::getApplication()->getConfig(); - $cparams = ComponentHelper::getParams('com_media'); - - $this->addDirectory('administrator/components', JPATH_ADMINISTRATOR . '/components'); - $this->addDirectory('administrator/components/com_joomlaupdate', JPATH_ADMINISTRATOR . '/components/com_joomlaupdate'); - $this->addDirectory('administrator/language', JPATH_ADMINISTRATOR . '/language'); - - // List all admin languages - $admin_langs = new \DirectoryIterator(JPATH_ADMINISTRATOR . '/language'); - - foreach ($admin_langs as $folder) - { - if ($folder->isDot() || !$folder->isDir()) - { - continue; - } - - $this->addDirectory( - 'administrator/language/' . $folder->getFilename(), - JPATH_ADMINISTRATOR . '/language/' . $folder->getFilename() - ); - } - - // List all manifests folders - $manifests = new \DirectoryIterator(JPATH_ADMINISTRATOR . '/manifests'); - - foreach ($manifests as $folder) - { - if ($folder->isDot() || !$folder->isDir()) - { - continue; - } - - $this->addDirectory( - 'administrator/manifests/' . $folder->getFilename(), - JPATH_ADMINISTRATOR . '/manifests/' . $folder->getFilename() - ); - } - - $this->addDirectory('administrator/modules', JPATH_ADMINISTRATOR . '/modules'); - $this->addDirectory('administrator/templates', JPATH_THEMES); - - $this->addDirectory('components', JPATH_SITE . '/components'); - - $this->addDirectory($cparams->get('image_path'), JPATH_SITE . '/' . $cparams->get('image_path')); - - // List all images folders - $image_folders = new \DirectoryIterator(JPATH_SITE . '/' . $cparams->get('image_path')); - - foreach ($image_folders as $folder) - { - if ($folder->isDot() || !$folder->isDir()) - { - continue; - } - - $this->addDirectory( - 'images/' . $folder->getFilename(), - JPATH_SITE . '/' . $cparams->get('image_path') . '/' . $folder->getFilename() - ); - } - - $this->addDirectory('language', JPATH_SITE . '/language'); - - // List all site languages - $site_langs = new \DirectoryIterator(JPATH_SITE . '/language'); - - foreach ($site_langs as $folder) - { - if ($folder->isDot() || !$folder->isDir()) - { - continue; - } - - $this->addDirectory('language/' . $folder->getFilename(), JPATH_SITE . '/language/' . $folder->getFilename()); - } - - $this->addDirectory('libraries', JPATH_LIBRARIES); - - $this->addDirectory('media', JPATH_SITE . '/media'); - $this->addDirectory('modules', JPATH_SITE . '/modules'); - $this->addDirectory('plugins', JPATH_PLUGINS); - - $plugin_groups = new \DirectoryIterator(JPATH_SITE . '/plugins'); - - foreach ($plugin_groups as $folder) - { - if ($folder->isDot() || !$folder->isDir()) - { - continue; - } - - $this->addDirectory('plugins/' . $folder->getFilename(), JPATH_PLUGINS . '/' . $folder->getFilename()); - } - - $this->addDirectory('templates', JPATH_SITE . '/templates'); - $this->addDirectory('configuration.php', JPATH_CONFIGURATION . '/configuration.php'); - - // Is there a cache path in configuration.php? - if ($cache_path = trim($registry->get('cache_path', ''))) - { - // Frontend and backend use same directory for caching. - $this->addDirectory($cache_path, $cache_path, 'COM_ADMIN_CACHE_DIRECTORY'); - } - else - { - $this->addDirectory('administrator/cache', JPATH_CACHE, 'COM_ADMIN_CACHE_DIRECTORY'); - } - - $this->addDirectory('media/cache', JPATH_ROOT . '/media/cache', 'COM_ADMIN_MEDIA_CACHE_DIRECTORY'); - - if ($public) - { - $this->addDirectory( - 'log', - $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), - 'COM_ADMIN_LOG_DIRECTORY' - ); - $this->addDirectory( - 'tmp', - $registry->get('tmp_path', JPATH_ROOT . '/tmp'), - 'COM_ADMIN_TEMP_DIRECTORY' - ); - } - else - { - $this->addDirectory( - $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), - $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), - 'COM_ADMIN_LOG_DIRECTORY' - ); - $this->addDirectory( - $registry->get('tmp_path', JPATH_ROOT . '/tmp'), - $registry->get('tmp_path', JPATH_ROOT . '/tmp'), - 'COM_ADMIN_TEMP_DIRECTORY' - ); - } - - return $this->directories; - } - - /** - * Method to add a directory - * - * @param string $name Directory Name - * @param string $path Directory path - * @param string $message Message - * - * @return void - * - * @since 1.6 - */ - private function addDirectory(string $name, string $path, string $message = ''): void - { - $this->directories[$name] = ['writable' => is_writable($path), 'message' => $message,]; - } - - /** - * Method to get the editor - * - * @return string The default editor - * - * @note Has to be removed (it is present in the config...) - * @since 1.6 - */ - public function &getEditor(): string - { - if (!is_null($this->editor)) - { - return $this->editor; - } - - $this->editor = Factory::getApplication()->get('editor'); - - return $this->editor; - } - - /** - * Parse phpinfo output into an array - * Source https://gist.github.com/sbmzhcn/6255314 - * - * @param string $html Output of phpinfo() - * - * @return array - * - * @since 3.5 - */ - protected function parsePhpInfo(string $html): array - { - $html = strip_tags($html, '

    '); - $html = preg_replace('/]*>([^<]+)<\/th>/', '\1', $html); - $html = preg_replace('/]*>([^<]+)<\/td>/', '\1', $html); - $t = preg_split('/(]*>[^<]+<\/h2>)/', $html, -1, PREG_SPLIT_DELIM_CAPTURE); - $r = []; - $count = \count($t); - $p1 = '([^<]+)<\/info>'; - $p2 = '/' . $p1 . '\s*' . $p1 . '\s*' . $p1 . '/'; - $p3 = '/' . $p1 . '\s*' . $p1 . '/'; - - for ($i = 1; $i < $count; $i++) - { - if (preg_match('/]*>([^<]+)<\/h2>/', $t[$i], $matches)) - { - $name = trim($matches[1]); - $vals = explode("\n", $t[$i + 1]); - - foreach ($vals AS $val) - { - // 3cols - if (preg_match($p2, $val, $matches)) - { - $r[$name][trim($matches[1])] = [trim($matches[2]), trim($matches[3]),]; - } - // 2cols - elseif (preg_match($p3, $val, $matches)) - { - $r[$name][trim($matches[1])] = trim($matches[2]); - } - } - } - } - - return $r; - } + /** + * Some PHP settings + * + * @var array + * @since 1.6 + */ + protected $php_settings = []; + + /** + * Config values + * + * @var array + * @since 1.6 + */ + protected $config = []; + + /** + * Some system values + * + * @var array + * @since 1.6 + */ + protected $info = []; + + /** + * PHP info + * + * @var string + * @since 1.6 + */ + protected $php_info = null; + + /** + * Array containing the phpinfo() data. + * + * @var array + * + * @since 3.5 + */ + protected $phpInfoArray; + + /** + * Private/critical data that we don't want to share + * + * @var array + * + * @since 3.5 + */ + protected $privateSettings = [ + 'phpInfoArray' => [ + 'CONTEXT_DOCUMENT_ROOT', + 'Cookie', + 'DOCUMENT_ROOT', + 'extension_dir', + 'error_log', + 'Host', + 'HTTP_COOKIE', + 'HTTP_HOST', + 'HTTP_ORIGIN', + 'HTTP_REFERER', + 'HTTP Request', + 'include_path', + 'mysql.default_socket', + 'MYSQL_SOCKET', + 'MYSQL_INCLUDE', + 'MYSQL_LIBS', + 'mysqli.default_socket', + 'MYSQLI_SOCKET', + 'PATH', + 'Path to sendmail', + 'pdo_mysql.default_socket', + 'Referer', + 'REMOTE_ADDR', + 'SCRIPT_FILENAME', + 'sendmail_path', + 'SERVER_ADDR', + 'SERVER_ADMIN', + 'Server Administrator', + 'SERVER_NAME', + 'Server Root', + 'session.name', + 'session.save_path', + 'upload_tmp_dir', + 'User/Group', + 'open_basedir', + ], + 'other' => [ + 'db', + 'dbprefix', + 'fromname', + 'live_site', + 'log_path', + 'mailfrom', + 'memcached_server_host', + 'open_basedir', + 'Origin', + 'proxy_host', + 'proxy_user', + 'proxy_pass', + 'redis_server_host', + 'redis_server_auth', + 'secret', + 'sendmail', + 'session.save_path', + 'session_memcached_server_host', + 'session_redis_server_host', + 'session_redis_server_auth', + 'sitename', + 'smtphost', + 'tmp_path', + 'open_basedir', + ] + ]; + + /** + * System values that can be "safely" shared + * + * @var array + * + * @since 3.5 + */ + protected $safeData; + + /** + * Information about writable state of directories + * + * @var array + * @since 1.6 + */ + protected $directories = []; + + /** + * The current editor. + * + * @var string + * @since 1.6 + */ + protected $editor = null; + + /** + * Remove sections of data marked as private in the privateSettings + * + * @param array $dataArray Array with data that may contain private information + * @param string $dataType Type of data to search for a specific section in the privateSettings array + * + * @return array + * + * @since 3.5 + */ + protected function cleanPrivateData(array $dataArray, string $dataType = 'other'): array + { + $dataType = isset($this->privateSettings[$dataType]) ? $dataType : 'other'; + + $privateSettings = $this->privateSettings[$dataType]; + + if (!$privateSettings) { + return $dataArray; + } + + foreach ($dataArray as $section => $values) { + if (\is_array($values)) { + $dataArray[$section] = $this->cleanPrivateData($values, $dataType); + } + + if (\in_array($section, $privateSettings, true)) { + $dataArray[$section] = $this->cleanSectionPrivateData($values); + } + } + + return $dataArray; + } + + /** + * Obfuscate section values + * + * @param mixed $sectionValues Section data + * + * @return string|array + * + * @since 3.5 + */ + protected function cleanSectionPrivateData($sectionValues) + { + if (!\is_array($sectionValues)) { + if (strstr($sectionValues, JPATH_ROOT)) { + $sectionValues = 'xxxxxx'; + } + + return \strlen($sectionValues) ? 'xxxxxx' : ''; + } + + foreach ($sectionValues as $setting => $value) { + $sectionValues[$setting] = \strlen($value) ? 'xxxxxx' : ''; + } + + return $sectionValues; + } + + /** + * Method to get the PHP settings + * + * @return array Some PHP settings + * + * @since 1.6 + */ + public function &getPhpSettings(): array + { + if (!empty($this->php_settings)) { + return $this->php_settings; + } + + $this->php_settings = [ + 'memory_limit' => ini_get('memory_limit'), + 'upload_max_filesize' => ini_get('upload_max_filesize'), + 'post_max_size' => ini_get('post_max_size'), + 'display_errors' => ini_get('display_errors') == '1', + 'short_open_tag' => ini_get('short_open_tag') == '1', + 'file_uploads' => ini_get('file_uploads') == '1', + 'output_buffering' => (int) ini_get('output_buffering') !== 0, + 'open_basedir' => ini_get('open_basedir'), + 'session.save_path' => ini_get('session.save_path'), + 'session.auto_start' => ini_get('session.auto_start'), + 'disable_functions' => ini_get('disable_functions'), + 'xml' => \extension_loaded('xml'), + 'zlib' => \extension_loaded('zlib'), + 'zip' => \function_exists('zip_open') && \function_exists('zip_read'), + 'mbstring' => \extension_loaded('mbstring'), + 'fileinfo' => \extension_loaded('fileinfo'), + 'gd' => \extension_loaded('gd'), + 'iconv' => \function_exists('iconv'), + 'intl' => \function_exists('transliterator_transliterate'), + 'max_input_vars' => ini_get('max_input_vars'), + ]; + + return $this->php_settings; + } + + /** + * Method to get the config + * + * @return array config values + * + * @since 1.6 + */ + public function &getConfig(): array + { + if (!empty($this->config)) { + return $this->config; + } + + $registry = new Registry(new \JConfig()); + $this->config = $registry->toArray(); + $hidden = [ + 'host', 'user', 'password', 'ftp_user', 'ftp_pass', + 'smtpuser', 'smtppass', 'redis_server_auth', 'session_redis_server_auth', + 'proxy_user', 'proxy_pass', 'secret' + ]; + + foreach ($hidden as $key) { + $this->config[$key] = 'xxxxxx'; + } + + return $this->config; + } + + /** + * Method to get the system information + * + * @return array System information values + * + * @since 1.6 + */ + public function &getInfo(): array + { + if (!empty($this->info)) { + return $this->info; + } + + $db = $this->getDatabase(); + + $this->info = [ + 'php' => php_uname(), + 'dbserver' => $db->getServerType(), + 'dbversion' => $db->getVersion(), + 'dbcollation' => $db->getCollation(), + 'dbconnectioncollation' => $db->getConnectionCollation(), + 'dbconnectionencryption' => $db->getConnectionEncryption(), + 'dbconnencryptsupported' => $db->isConnectionEncryptionSupported(), + 'phpversion' => PHP_VERSION, + 'server' => $_SERVER['SERVER_SOFTWARE'] ?? getenv('SERVER_SOFTWARE'), + 'sapi_name' => PHP_SAPI, + 'version' => (new Version())->getLongVersion(), + 'useragent' => $_SERVER['HTTP_USER_AGENT'] ?? '', + ]; + + return $this->info; + } + + /** + * Check if the phpinfo function is enabled + * + * @return boolean True if enabled + * + * @since 3.4.1 + */ + public function phpinfoEnabled(): bool + { + return !\in_array('phpinfo', explode(',', ini_get('disable_functions'))); + } + + /** + * Method to get filter data from the model + * + * @param string $dataType Type of data to get safely + * @param bool $public If true no sensitive information will be removed + * + * @return array + * + * @since 3.5 + */ + public function getSafeData(string $dataType, bool $public = true): array + { + if (isset($this->safeData[$dataType])) { + return $this->safeData[$dataType]; + } + + $methodName = 'get' . ucfirst($dataType); + + if (!method_exists($this, $methodName)) { + return []; + } + + $data = $this->$methodName($public); + + $this->safeData[$dataType] = $this->cleanPrivateData($data, $dataType); + + return $this->safeData[$dataType]; + } + + /** + * Method to get the PHP info + * + * @return string PHP info + * + * @since 1.6 + */ + public function &getPHPInfo(): string + { + if (!$this->phpinfoEnabled()) { + $this->php_info = Text::_('COM_ADMIN_PHPINFO_DISABLED'); + + return $this->php_info; + } + + if (!\is_null($this->php_info)) { + return $this->php_info; + } + + ob_start(); + date_default_timezone_set('UTC'); + phpinfo(INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES); + $phpInfo = ob_get_contents(); + ob_end_clean(); + preg_match_all('#]*>(.*)#siU', $phpInfo, $output); + $output = preg_replace('#]*>#', '', $output[1][0]); + $output = preg_replace('#(\w),(\w)#', '\1, \2', $output); + $output = preg_replace('#
    #', '', $output); + $output = str_replace('
    ', '', $output); + $output = preg_replace('#
    (.*)#', '$1', $output); + $output = str_replace('
    ', '', $output); + $output = str_replace('', '', $output); + $this->php_info = $output; + + return $this->php_info; + } + + /** + * Get phpinfo() output as array + * + * @return array + * + * @since 3.5 + */ + public function getPhpInfoArray(): array + { + // Already cached + if (null !== $this->phpInfoArray) { + return $this->phpInfoArray; + } + + $phpInfo = $this->getPHPInfo(); + + $this->phpInfoArray = $this->parsePhpInfo($phpInfo); + + return $this->phpInfoArray; + } + + /** + * Method to get a list of installed extensions + * + * @return array installed extensions + * + * @since 3.5 + */ + public function getExtensions(): array + { + $installed = []; + $db = Factory::getContainer()->get('DatabaseDriver'); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__extensions')); + $db->setQuery($query); + + try { + $extensions = $db->loadObjectList(); + } catch (\Exception $e) { + try { + Log::add(Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage( + Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()), + 'warning' + ); + } + + return $installed; + } + + if (empty($extensions)) { + return $installed; + } + + foreach ($extensions as $extension) { + if (\strlen($extension->name) == 0) { + continue; + } + + $installed[$extension->name] = [ + 'name' => $extension->name, + 'type' => $extension->type, + 'state' => $extension->enabled ? Text::_('JENABLED') : Text::_('JDISABLED'), + 'author' => 'unknown', + 'version' => 'unknown', + 'creationDate' => 'unknown', + 'authorUrl' => 'unknown', + ]; + + $manifest = new Registry($extension->manifest_cache); + + $extraData = [ + 'author' => $manifest->get('author', ''), + 'version' => $manifest->get('version', ''), + 'creationDate' => $manifest->get('creationDate', ''), + 'authorUrl' => $manifest->get('authorUrl', '') + ]; + + $installed[$extension->name] = array_merge($installed[$extension->name], $extraData); + } + + return $installed; + } + + /** + * Method to get the directory states + * + * @param bool $public If true no information is going to be removed + * + * @return array States of directories + * + * @throws \Exception + * @since 1.6 + */ + public function getDirectory(bool $public = false): array + { + if (!empty($this->directories)) { + return $this->directories; + } + + $this->directories = []; + + $registry = Factory::getApplication()->getConfig(); + $cparams = ComponentHelper::getParams('com_media'); + + $this->addDirectory('administrator/components', JPATH_ADMINISTRATOR . '/components'); + $this->addDirectory('administrator/components/com_joomlaupdate', JPATH_ADMINISTRATOR . '/components/com_joomlaupdate'); + $this->addDirectory('administrator/language', JPATH_ADMINISTRATOR . '/language'); + + // List all admin languages + $admin_langs = new \DirectoryIterator(JPATH_ADMINISTRATOR . '/language'); + + foreach ($admin_langs as $folder) { + if ($folder->isDot() || !$folder->isDir()) { + continue; + } + + $this->addDirectory( + 'administrator/language/' . $folder->getFilename(), + JPATH_ADMINISTRATOR . '/language/' . $folder->getFilename() + ); + } + + // List all manifests folders + $manifests = new \DirectoryIterator(JPATH_ADMINISTRATOR . '/manifests'); + + foreach ($manifests as $folder) { + if ($folder->isDot() || !$folder->isDir()) { + continue; + } + + $this->addDirectory( + 'administrator/manifests/' . $folder->getFilename(), + JPATH_ADMINISTRATOR . '/manifests/' . $folder->getFilename() + ); + } + + $this->addDirectory('administrator/modules', JPATH_ADMINISTRATOR . '/modules'); + $this->addDirectory('administrator/templates', JPATH_THEMES); + + $this->addDirectory('components', JPATH_SITE . '/components'); + + $this->addDirectory($cparams->get('image_path'), JPATH_SITE . '/' . $cparams->get('image_path')); + + // List all images folders + $image_folders = new \DirectoryIterator(JPATH_SITE . '/' . $cparams->get('image_path')); + + foreach ($image_folders as $folder) { + if ($folder->isDot() || !$folder->isDir()) { + continue; + } + + $this->addDirectory( + 'images/' . $folder->getFilename(), + JPATH_SITE . '/' . $cparams->get('image_path') . '/' . $folder->getFilename() + ); + } + + $this->addDirectory('language', JPATH_SITE . '/language'); + + // List all site languages + $site_langs = new \DirectoryIterator(JPATH_SITE . '/language'); + + foreach ($site_langs as $folder) { + if ($folder->isDot() || !$folder->isDir()) { + continue; + } + + $this->addDirectory('language/' . $folder->getFilename(), JPATH_SITE . '/language/' . $folder->getFilename()); + } + + $this->addDirectory('libraries', JPATH_LIBRARIES); + + $this->addDirectory('media', JPATH_SITE . '/media'); + $this->addDirectory('modules', JPATH_SITE . '/modules'); + $this->addDirectory('plugins', JPATH_PLUGINS); + + $plugin_groups = new \DirectoryIterator(JPATH_SITE . '/plugins'); + + foreach ($plugin_groups as $folder) { + if ($folder->isDot() || !$folder->isDir()) { + continue; + } + + $this->addDirectory('plugins/' . $folder->getFilename(), JPATH_PLUGINS . '/' . $folder->getFilename()); + } + + $this->addDirectory('templates', JPATH_SITE . '/templates'); + $this->addDirectory('configuration.php', JPATH_CONFIGURATION . '/configuration.php'); + + // Is there a cache path in configuration.php? + if ($cache_path = trim($registry->get('cache_path', ''))) { + // Frontend and backend use same directory for caching. + $this->addDirectory($cache_path, $cache_path, 'COM_ADMIN_CACHE_DIRECTORY'); + } else { + $this->addDirectory('administrator/cache', JPATH_CACHE, 'COM_ADMIN_CACHE_DIRECTORY'); + } + + $this->addDirectory('media/cache', JPATH_ROOT . '/media/cache', 'COM_ADMIN_MEDIA_CACHE_DIRECTORY'); + + if ($public) { + $this->addDirectory( + 'log', + $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), + 'COM_ADMIN_LOG_DIRECTORY' + ); + $this->addDirectory( + 'tmp', + $registry->get('tmp_path', JPATH_ROOT . '/tmp'), + 'COM_ADMIN_TEMP_DIRECTORY' + ); + } else { + $this->addDirectory( + $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), + $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), + 'COM_ADMIN_LOG_DIRECTORY' + ); + $this->addDirectory( + $registry->get('tmp_path', JPATH_ROOT . '/tmp'), + $registry->get('tmp_path', JPATH_ROOT . '/tmp'), + 'COM_ADMIN_TEMP_DIRECTORY' + ); + } + + return $this->directories; + } + + /** + * Method to add a directory + * + * @param string $name Directory Name + * @param string $path Directory path + * @param string $message Message + * + * @return void + * + * @since 1.6 + */ + private function addDirectory(string $name, string $path, string $message = ''): void + { + $this->directories[$name] = ['writable' => is_writable($path), 'message' => $message,]; + } + + /** + * Method to get the editor + * + * @return string The default editor + * + * @note Has to be removed (it is present in the config...) + * @since 1.6 + */ + public function &getEditor(): string + { + if (!is_null($this->editor)) { + return $this->editor; + } + + $this->editor = Factory::getApplication()->get('editor'); + + return $this->editor; + } + + /** + * Parse phpinfo output into an array + * Source https://gist.github.com/sbmzhcn/6255314 + * + * @param string $html Output of phpinfo() + * + * @return array + * + * @since 3.5 + */ + protected function parsePhpInfo(string $html): array + { + $html = strip_tags($html, '

    '); + $html = preg_replace('/]*>([^<]+)<\/th>/', '\1', $html); + $html = preg_replace('/]*>([^<]+)<\/td>/', '\1', $html); + $t = preg_split('/(]*>[^<]+<\/h2>)/', $html, -1, PREG_SPLIT_DELIM_CAPTURE); + $r = []; + $count = \count($t); + $p1 = '([^<]+)<\/info>'; + $p2 = '/' . $p1 . '\s*' . $p1 . '\s*' . $p1 . '/'; + $p3 = '/' . $p1 . '\s*' . $p1 . '/'; + + for ($i = 1; $i < $count; $i++) { + if (preg_match('/]*>([^<]+)<\/h2>/', $t[$i], $matches)) { + $name = trim($matches[1]); + $vals = explode("\n", $t[$i + 1]); + + foreach ($vals as $val) { + // 3cols + if (preg_match($p2, $val, $matches)) { + $r[$name][trim($matches[1])] = [trim($matches[2]), trim($matches[3]),]; + } + // 2cols + elseif (preg_match($p3, $val, $matches)) { + $r[$name][trim($matches[1])] = trim($matches[2]); + } + } + } + } + + return $r; + } } diff --git a/administrator/components/com_admin/src/Service/HTML/Directory.php b/administrator/components/com_admin/src/Service/HTML/Directory.php index f5de97a6ca568..0a7870763857d 100644 --- a/administrator/components/com_admin/src/Service/HTML/Directory.php +++ b/administrator/components/com_admin/src/Service/HTML/Directory.php @@ -1,4 +1,5 @@ ' . Text::_('COM_ADMIN_WRITABLE') . ''; - } - - return '' . Text::_('COM_ADMIN_UNWRITABLE') . ''; - } - - /** - * Method to generate a message for a directory - * - * @param string $dir the directory - * @param boolean $message the message - * @param boolean $visible is the $dir visible? - * - * @return string html code - */ - public function message($dir, $message, $visible = true) - { - $output = $visible ? $dir : ''; - - if (empty($message)) - { - return $output; - } - - return $output . ' ' . Text::_($message) . ''; - } + /** + * Method to generate a (un)writable message for directory + * + * @param boolean $writable is the directory writable? + * + * @return string html code + */ + public function writable($writable) + { + if ($writable) { + return '' . Text::_('COM_ADMIN_WRITABLE') . ''; + } + + return '' . Text::_('COM_ADMIN_UNWRITABLE') . ''; + } + + /** + * Method to generate a message for a directory + * + * @param string $dir the directory + * @param boolean $message the message + * @param boolean $visible is the $dir visible? + * + * @return string html code + */ + public function message($dir, $message, $visible = true) + { + $output = $visible ? $dir : ''; + + if (empty($message)) { + return $output; + } + + return $output . ' ' . Text::_($message) . ''; + } } diff --git a/administrator/components/com_admin/src/Service/HTML/PhpSetting.php b/administrator/components/com_admin/src/Service/HTML/PhpSetting.php index 8d4a1cd602dcb..3811e667dd15c 100644 --- a/administrator/components/com_admin/src/Service/HTML/PhpSetting.php +++ b/administrator/components/com_admin/src/Service/HTML/PhpSetting.php @@ -1,4 +1,5 @@ getModel(); - $this->helpSearch = $model->getHelpSearch(); - $this->page = $model->getPage(); - $this->toc = $model->getToc(); - $this->languageTag = $model->getLangTag(); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var HelpModel $model */ + $model = $this->getModel(); + $this->helpSearch = $model->getHelpSearch(); + $this->page = $model->getPage(); + $this->toc = $model->getToc(); + $this->languageTag = $model->getLangTag(); - $this->addToolbar(); + $this->addToolbar(); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Setup the Toolbar - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - ToolbarHelper::title(Text::_('COM_ADMIN_HELP'), 'support help_header'); - } + /** + * Setup the Toolbar + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + ToolbarHelper::title(Text::_('COM_ADMIN_HELP'), 'support help_header'); + } } diff --git a/administrator/components/com_admin/src/View/Sysinfo/HtmlView.php b/administrator/components/com_admin/src/View/Sysinfo/HtmlView.php index 4e7dca1227313..ef35a9b7459b9 100644 --- a/administrator/components/com_admin/src/View/Sysinfo/HtmlView.php +++ b/administrator/components/com_admin/src/View/Sysinfo/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser()->authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + public function display($tpl = null): void + { + // Access check. + if (!$this->getCurrentUser()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - /** @var SysinfoModel $model */ - $model = $this->getModel(); - $this->phpSettings = $model->getPhpSettings(); - $this->config = $model->getConfig(); - $this->info = $model->getInfo(); - $this->phpInfo = $model->getPHPInfo(); - $this->directory = $model->getDirectory(); + /** @var SysinfoModel $model */ + $model = $this->getModel(); + $this->phpSettings = $model->getPhpSettings(); + $this->config = $model->getConfig(); + $this->info = $model->getInfo(); + $this->phpInfo = $model->getPHPInfo(); + $this->directory = $model->getDirectory(); - $this->addToolbar(); + $this->addToolbar(); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Setup the Toolbar - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - ToolbarHelper::title(Text::_('COM_ADMIN_SYSTEM_INFORMATION'), 'info-circle systeminfo'); - ToolbarHelper::link( - Route::_('index.php?option=com_admin&view=sysinfo&format=text&' . Session::getFormToken() . '=1'), - 'COM_ADMIN_DOWNLOAD_SYSTEM_INFORMATION_TEXT', - 'download' - ); - ToolbarHelper::link( - Route::_('index.php?option=com_admin&view=sysinfo&format=json&' . Session::getFormToken() . '=1'), - 'COM_ADMIN_DOWNLOAD_SYSTEM_INFORMATION_JSON', - 'download' - ); - ToolbarHelper::help('Site_System_Information'); - } + /** + * Setup the Toolbar + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + ToolbarHelper::title(Text::_('COM_ADMIN_SYSTEM_INFORMATION'), 'info-circle systeminfo'); + ToolbarHelper::link( + Route::_('index.php?option=com_admin&view=sysinfo&format=text&' . Session::getFormToken() . '=1'), + 'COM_ADMIN_DOWNLOAD_SYSTEM_INFORMATION_TEXT', + 'download' + ); + ToolbarHelper::link( + Route::_('index.php?option=com_admin&view=sysinfo&format=json&' . Session::getFormToken() . '=1'), + 'COM_ADMIN_DOWNLOAD_SYSTEM_INFORMATION_JSON', + 'download' + ); + ToolbarHelper::help('Site_System_Information'); + } } diff --git a/administrator/components/com_admin/src/View/Sysinfo/JsonView.php b/administrator/components/com_admin/src/View/Sysinfo/JsonView.php index 116a0875c9bcb..923a525f71026 100644 --- a/administrator/components/com_admin/src/View/Sysinfo/JsonView.php +++ b/administrator/components/com_admin/src/View/Sysinfo/JsonView.php @@ -1,4 +1,5 @@ authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.5 + * + * @throws Exception + */ + public function display($tpl = null): void + { + // Access check. + if (!Factory::getUser()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - header('MIME-Version: 1.0'); - header('Content-Disposition: attachment; filename="systeminfo-' . date('c') . '.json"'); - header('Content-Transfer-Encoding: binary'); + header('MIME-Version: 1.0'); + header('Content-Disposition: attachment; filename="systeminfo-' . date('c') . '.json"'); + header('Content-Transfer-Encoding: binary'); - $data = $this->getLayoutData(); + $data = $this->getLayoutData(); - echo json_encode($data, JSON_PRETTY_PRINT); + echo json_encode($data, JSON_PRETTY_PRINT); - Factory::getApplication()->close(); - } + Factory::getApplication()->close(); + } - /** - * Get the data for the view - * - * @return array - * - * @since 3.5 - */ - protected function getLayoutData(): array - { - /** @var SysinfoModel $model */ - $model = $this->getModel(); + /** + * Get the data for the view + * + * @return array + * + * @since 3.5 + */ + protected function getLayoutData(): array + { + /** @var SysinfoModel $model */ + $model = $this->getModel(); - return [ - 'info' => $model->getSafeData('info'), - 'phpSettings' => $model->getSafeData('phpSettings'), - 'config' => $model->getSafeData('config'), - 'directories' => $model->getSafeData('directory', true), - 'phpInfo' => $model->getSafeData('phpInfoArray'), - 'extensions' => $model->getSafeData('extensions') - ]; - } + return [ + 'info' => $model->getSafeData('info'), + 'phpSettings' => $model->getSafeData('phpSettings'), + 'config' => $model->getSafeData('config'), + 'directories' => $model->getSafeData('directory', true), + 'phpInfo' => $model->getSafeData('phpInfoArray'), + 'extensions' => $model->getSafeData('extensions') + ]; + } } diff --git a/administrator/components/com_admin/src/View/Sysinfo/TextView.php b/administrator/components/com_admin/src/View/Sysinfo/TextView.php index 6da1078e7c417..275eb71e4f420 100644 --- a/administrator/components/com_admin/src/View/Sysinfo/TextView.php +++ b/administrator/components/com_admin/src/View/Sysinfo/TextView.php @@ -1,4 +1,5 @@ authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - header('Content-Type: text/plain; charset=utf-8'); - header('Content-Description: File Transfer'); - header('Content-Disposition: attachment; filename="systeminfo-' . date('c') . '.txt"'); - header('Cache-Control: must-revalidate'); - - $data = $this->getLayoutData(); - - $lines = []; - - foreach ($data as $sectionName => $section) - { - $customRenderingMethod = 'render' . ucfirst($sectionName); - - if (method_exists($this, $customRenderingMethod)) - { - $lines[] = $this->$customRenderingMethod($section['title'], $section['data']); - } - else - { - $lines[] = $this->renderSection($section['title'], $section['data']); - } - } - - echo str_replace(JPATH_ROOT, 'xxxxxx', implode("\n\n", $lines)); - - Factory::getApplication()->close(); - } - - /** - * Get the data for the view - * - * @return array - * - * @since 3.5 - */ - protected function getLayoutData(): array - { - /** @var SysinfoModel $model */ - $model = $this->getModel(); - - return [ - 'info' => [ - 'title' => Text::_('COM_ADMIN_SYSTEM_INFORMATION', true), - 'data' => $model->getSafeData('info') - ], - 'phpSettings' => [ - 'title' => Text::_('COM_ADMIN_PHP_SETTINGS', true), - 'data' => $model->getSafeData('phpSettings') - ], - 'config' => [ - 'title' => Text::_('COM_ADMIN_CONFIGURATION_FILE', true), - 'data' => $model->getSafeData('config') - ], - 'directories' => [ - 'title' => Text::_('COM_ADMIN_DIRECTORY_PERMISSIONS', true), - 'data' => $model->getSafeData('directory', true) - ], - 'phpInfo' => [ - 'title' => Text::_('COM_ADMIN_PHP_INFORMATION', true), - 'data' => $model->getSafeData('phpInfoArray') - ], - 'extensions' => [ - 'title' => Text::_('COM_ADMIN_EXTENSIONS', true), - 'data' => $model->getSafeData('extensions') - ] - ]; - } - - /** - * Render a section - * - * @param string $sectionName Name of the section to render - * @param array $sectionData Data of the section to render - * @param integer $level Depth level for indentation - * - * @return string - * - * @since 3.5 - */ - protected function renderSection(string $sectionName, array $sectionData, int $level = 0): string - { - $lines = []; - - $margin = ($level > 0) ? str_repeat("\t", $level) : null; - - $lines[] = $margin . '============='; - $lines[] = $margin . $sectionName; - $lines[] = $margin . '============='; - $level++; - - foreach ($sectionData as $name => $value) - { - if (\is_array($value)) - { - if ($name == 'Directive') - { - continue; - } - - $lines[] = ''; - $lines[] = $this->renderSection($name, $value, $level); - } - else - { - if (\is_bool($value)) - { - $value = $value ? 'true' : 'false'; - } - - if (\is_int($name) && ($name == 0 || $name == 1)) - { - $name = ($name == 0 ? 'Local Value' : 'Master Value'); - } - - $lines[] = $margin . $name . ': ' . $value; - } - } - - return implode("\n", $lines); - } - - /** - * Specific rendering for directories - * - * @param string $sectionName Name of the section - * @param array $sectionData Directories information - * @param integer $level Starting level - * - * @return string - * - * @since 3.5 - */ - protected function renderDirectories(string $sectionName, array $sectionData, int $level = -1): string - { - foreach ($sectionData as $directory => $data) - { - $sectionData[$directory] = $data['writable'] ? ' writable' : ' NOT writable'; - } - - return $this->renderSection($sectionName, $sectionData, $level); - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + * + * @since 3.5 + * + * @throws Exception + */ + public function display($tpl = null): void + { + // Access check. + if (!Factory::getUser()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + header('Content-Type: text/plain; charset=utf-8'); + header('Content-Description: File Transfer'); + header('Content-Disposition: attachment; filename="systeminfo-' . date('c') . '.txt"'); + header('Cache-Control: must-revalidate'); + + $data = $this->getLayoutData(); + + $lines = []; + + foreach ($data as $sectionName => $section) { + $customRenderingMethod = 'render' . ucfirst($sectionName); + + if (method_exists($this, $customRenderingMethod)) { + $lines[] = $this->$customRenderingMethod($section['title'], $section['data']); + } else { + $lines[] = $this->renderSection($section['title'], $section['data']); + } + } + + echo str_replace(JPATH_ROOT, 'xxxxxx', implode("\n\n", $lines)); + + Factory::getApplication()->close(); + } + + /** + * Get the data for the view + * + * @return array + * + * @since 3.5 + */ + protected function getLayoutData(): array + { + /** @var SysinfoModel $model */ + $model = $this->getModel(); + + return [ + 'info' => [ + 'title' => Text::_('COM_ADMIN_SYSTEM_INFORMATION', true), + 'data' => $model->getSafeData('info') + ], + 'phpSettings' => [ + 'title' => Text::_('COM_ADMIN_PHP_SETTINGS', true), + 'data' => $model->getSafeData('phpSettings') + ], + 'config' => [ + 'title' => Text::_('COM_ADMIN_CONFIGURATION_FILE', true), + 'data' => $model->getSafeData('config') + ], + 'directories' => [ + 'title' => Text::_('COM_ADMIN_DIRECTORY_PERMISSIONS', true), + 'data' => $model->getSafeData('directory', true) + ], + 'phpInfo' => [ + 'title' => Text::_('COM_ADMIN_PHP_INFORMATION', true), + 'data' => $model->getSafeData('phpInfoArray') + ], + 'extensions' => [ + 'title' => Text::_('COM_ADMIN_EXTENSIONS', true), + 'data' => $model->getSafeData('extensions') + ] + ]; + } + + /** + * Render a section + * + * @param string $sectionName Name of the section to render + * @param array $sectionData Data of the section to render + * @param integer $level Depth level for indentation + * + * @return string + * + * @since 3.5 + */ + protected function renderSection(string $sectionName, array $sectionData, int $level = 0): string + { + $lines = []; + + $margin = ($level > 0) ? str_repeat("\t", $level) : null; + + $lines[] = $margin . '============='; + $lines[] = $margin . $sectionName; + $lines[] = $margin . '============='; + $level++; + + foreach ($sectionData as $name => $value) { + if (\is_array($value)) { + if ($name == 'Directive') { + continue; + } + + $lines[] = ''; + $lines[] = $this->renderSection($name, $value, $level); + } else { + if (\is_bool($value)) { + $value = $value ? 'true' : 'false'; + } + + if (\is_int($name) && ($name == 0 || $name == 1)) { + $name = ($name == 0 ? 'Local Value' : 'Master Value'); + } + + $lines[] = $margin . $name . ': ' . $value; + } + } + + return implode("\n", $lines); + } + + /** + * Specific rendering for directories + * + * @param string $sectionName Name of the section + * @param array $sectionData Directories information + * @param integer $level Starting level + * + * @return string + * + * @since 3.5 + */ + protected function renderDirectories(string $sectionName, array $sectionData, int $level = -1): string + { + foreach ($sectionData as $directory => $data) { + $sectionData[$directory] = $data['writable'] ? ' writable' : ' NOT writable'; + } + + return $this->renderSection($sectionName, $sectionData, $level); + } } diff --git a/administrator/components/com_admin/tmpl/help/default.php b/administrator/components/com_admin/tmpl/help/default.php index 1cdac79740241..1cfe5e816f6b3 100644 --- a/administrator/components/com_admin/tmpl/help/default.php +++ b/administrator/components/com_admin/tmpl/help/default.php @@ -1,4 +1,5 @@
    -
    - -
    - -
    -
    - +
    + +
    + +
    +
    +
    diff --git a/administrator/components/com_admin/tmpl/help/langforum.php b/administrator/components/com_admin/tmpl/help/langforum.php index 3e4b0782978ba..979c726533351 100644 --- a/administrator/components/com_admin/tmpl/help/langforum.php +++ b/administrator/components/com_admin/tmpl/help/langforum.php @@ -1,4 +1,5 @@
    - 'site', 'recall' => true, 'breakpoint' => 768]); ?> + 'site', 'recall' => true, 'breakpoint' => 768]); ?> - - loadTemplate('system'); ?> - + + loadTemplate('system'); ?> + - - loadTemplate('phpsettings'); ?> - + + loadTemplate('phpsettings'); ?> + - - loadTemplate('config'); ?> - + + loadTemplate('config'); ?> + - - loadTemplate('directory'); ?> - + + loadTemplate('directory'); ?> + - - loadTemplate('phpinfo'); ?> - + + loadTemplate('phpinfo'); ?> + - +
    diff --git a/administrator/components/com_admin/tmpl/sysinfo/default_config.php b/administrator/components/com_admin/tmpl/sysinfo/default_config.php index 49e3f14b38fec..9cc7f0d5dcba4 100644 --- a/administrator/components/com_admin/tmpl/sysinfo/default_config.php +++ b/administrator/components/com_admin/tmpl/sysinfo/default_config.php @@ -1,4 +1,5 @@
    - - - - - - - - - - config as $key => $value) : ?> - - - - - - -
    - -
    - - - -
    - - - -
    + + + + + + + + + + config as $key => $value) : ?> + + + + + + +
    + +
    + + + +
    + + + +
    diff --git a/administrator/components/com_admin/tmpl/sysinfo/default_directory.php b/administrator/components/com_admin/tmpl/sysinfo/default_directory.php index a18fd59e19916..135c16eabc6e3 100644 --- a/administrator/components/com_admin/tmpl/sysinfo/default_directory.php +++ b/administrator/components/com_admin/tmpl/sysinfo/default_directory.php @@ -1,4 +1,5 @@
    - - - - - - - - - - directory as $dir => $info) : ?> - - - - - - -
    - -
    - - - -
    - - - -
    + + + + + + + + + + directory as $dir => $info) : ?> + + + + + + +
    + +
    + + + +
    + + + +
    diff --git a/administrator/components/com_admin/tmpl/sysinfo/default_phpinfo.php b/administrator/components/com_admin/tmpl/sysinfo/default_phpinfo.php index b4c8a68faca16..18e0fb1f60155 100644 --- a/administrator/components/com_admin/tmpl/sysinfo/default_phpinfo.php +++ b/administrator/components/com_admin/tmpl/sysinfo/default_phpinfo.php @@ -1,4 +1,5 @@
    - phpInfo; ?> + phpInfo; ?>
    diff --git a/administrator/components/com_admin/tmpl/sysinfo/default_phpsettings.php b/administrator/components/com_admin/tmpl/sysinfo/default_phpsettings.php index 3ea85c31b4024..078555d28d371 100644 --- a/administrator/components/com_admin/tmpl/sysinfo/default_phpsettings.php +++ b/administrator/components/com_admin/tmpl/sysinfo/default_phpsettings.php @@ -1,4 +1,5 @@
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - phpSettings['upload_max_filesize']); ?> -
    - - - phpSettings['post_max_size']); ?> -
    - - - phpSettings['memory_limit']); ?> -
    - - - phpSettings['open_basedir']); ?> -
    - - - phpSettings['display_errors']); ?> -
    - - - phpSettings['short_open_tag']); ?> -
    - - - phpSettings['file_uploads']); ?> -
    - - - phpSettings['output_buffering']); ?> -
    - - - phpSettings['session.save_path']); ?> -
    - - - phpSettings['session.auto_start']; ?> -
    - - - phpSettings['xml']); ?> -
    - - - phpSettings['zlib']); ?> -
    - - - phpSettings['zip']); ?> -
    - - - phpSettings['disable_functions']); ?> -
    - - - phpSettings['fileinfo']); ?> -
    - - - phpSettings['mbstring']); ?> -
    - - - phpSettings['gd']); ?> -
    - - - phpSettings['iconv']); ?> -
    - - - phpSettings['intl']); ?> -
    - - - phpSettings['max_input_vars']; ?> -
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + +
    + + + phpSettings['upload_max_filesize']); ?> +
    + + + phpSettings['post_max_size']); ?> +
    + + + phpSettings['memory_limit']); ?> +
    + + + phpSettings['open_basedir']); ?> +
    + + + phpSettings['display_errors']); ?> +
    + + + phpSettings['short_open_tag']); ?> +
    + + + phpSettings['file_uploads']); ?> +
    + + + phpSettings['output_buffering']); ?> +
    + + + phpSettings['session.save_path']); ?> +
    + + + phpSettings['session.auto_start']; ?> +
    + + + phpSettings['xml']); ?> +
    + + + phpSettings['zlib']); ?> +
    + + + phpSettings['zip']); ?> +
    + + + phpSettings['disable_functions']); ?> +
    + + + phpSettings['fileinfo']); ?> +
    + + + phpSettings['mbstring']); ?> +
    + + + phpSettings['gd']); ?> +
    + + + phpSettings['iconv']); ?> +
    + + + phpSettings['intl']); ?> +
    + + + phpSettings['max_input_vars']; ?> +
    diff --git a/administrator/components/com_admin/tmpl/sysinfo/default_system.php b/administrator/components/com_admin/tmpl/sysinfo/default_system.php index 97ddce3adf5b6..0891b5c2ef82b 100644 --- a/administrator/components/com_admin/tmpl/sysinfo/default_system.php +++ b/administrator/components/com_admin/tmpl/sysinfo/default_system.php @@ -1,4 +1,5 @@
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - info['php']; ?> -
    - - - info['dbserver']; ?> -
    - - - info['dbversion']; ?> -
    - - - info['dbcollation']; ?> -
    - - - info['dbconnectioncollation']; ?> -
    - - - info['dbconnectionencryption'] ?: Text::_('JNONE'); ?> -
    - - - info['dbconnencryptsupported'] ? Text::_('JYES') : Text::_('JNO'); ?> -
    - - - info['phpversion']; ?> -
    - - - info['server']); ?> -
    - - - info['sapi_name']; ?> -
    - - - info['version']; ?> -
    - - - info['useragent'], ENT_COMPAT, 'UTF-8'); ?> -
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + +
    + + + info['php']; ?> +
    + + + info['dbserver']; ?> +
    + + + info['dbversion']; ?> +
    + + + info['dbcollation']; ?> +
    + + + info['dbconnectioncollation']; ?> +
    + + + info['dbconnectionencryption'] ?: Text::_('JNONE'); ?> +
    + + + info['dbconnencryptsupported'] ? Text::_('JYES') : Text::_('JNO'); ?> +
    + + + info['phpversion']; ?> +
    + + + info['server']); ?> +
    + + + info['sapi_name']; ?> +
    + + + info['version']; ?> +
    + + + info['useragent'], ENT_COMPAT, 'UTF-8'); ?> +
    diff --git a/administrator/components/com_ajax/ajax.php b/administrator/components/com_ajax/ajax.php index 40e35cd18f9d4..37e61ad93a326 100644 --- a/administrator/components/com_ajax/ajax.php +++ b/administrator/components/com_ajax/ajax.php @@ -1,4 +1,5 @@ filterForm) && !empty($data['view']->filterForm)) -{ - // Checks if a selector (e.g. client_id) exists. - if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) - { - $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; - - // Checks if a selector should be shown in the current layout. - if (isset($data['view']->layout)) - { - $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; - } - - // Unset the selector field from active filters group. - unset($data['view']->activeFilters[$selectorFieldName]); - } - - // Checks if the filters button should exist. - $filters = $data['view']->filterForm->getGroup('filter'); - $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; - - // Checks if it should show the be hidden. - $hideActiveFilters = empty($data['view']->activeFilters); - - // Check if the no results message should appear. - if (isset($data['view']->total) && (int) $data['view']->total === 0) - { - $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); - if (!empty($noResults)) - { - $noResultsText = Text::_($noResults); - } - } +if (isset($data['view']->filterForm) && !empty($data['view']->filterForm)) { + // Checks if a selector (e.g. client_id) exists. + if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) { + $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; + + // Checks if a selector should be shown in the current layout. + if (isset($data['view']->layout)) { + $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; + } + + // Unset the selector field from active filters group. + unset($data['view']->activeFilters[$selectorFieldName]); + } + + // Checks if the filters button should exist. + $filters = $data['view']->filterForm->getGroup('filter'); + $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; + + // Checks if it should show the be hidden. + $hideActiveFilters = empty($data['view']->activeFilters); + + // Check if the no results message should appear. + if (isset($data['view']->total) && (int) $data['view']->total === 0) { + $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); + if (!empty($noResults)) { + $noResultsText = Text::_($noResults); + } + } } // Set some basic options. $customOptions = array( - 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, - 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, - 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), - 'searchFieldSelector' => '#filter_search', - 'selectorFieldName' => $selectorFieldName, - 'showSelector' => $showSelector, - 'orderFieldSelector' => '#list_fullordering', - 'showNoResults' => !empty($noResultsText), - 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', - 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', + 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, + 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, + 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), + 'searchFieldSelector' => '#filter_search', + 'selectorFieldName' => $selectorFieldName, + 'showSelector' => $showSelector, + 'orderFieldSelector' => '#list_fullordering', + 'showNoResults' => !empty($noResultsText), + 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', + 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', ); // Merge custom options in the options array. @@ -85,44 +81,44 @@ HTMLHelper::_('searchtools.form', $data['options']['formSelector'], $data['options']); ?> - sublayout('noitems', $data); ?> + sublayout('noitems', $data); ?> diff --git a/administrator/components/com_associations/services/provider.php b/administrator/components/com_associations/services/provider.php index 4ad47fec2648b..071fec0053bf2 100644 --- a/administrator/components/com_associations/services/provider.php +++ b/administrator/components/com_associations/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Associations')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Associations')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Associations')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Associations')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_associations/src/Controller/AssociationController.php b/administrator/components/com_associations/src/Controller/AssociationController.php index e3e7f12860480..5d730564df14b 100644 --- a/administrator/components/com_associations/src/Controller/AssociationController.php +++ b/administrator/components/com_associations/src/Controller/AssociationController.php @@ -1,4 +1,5 @@ input->get('itemtype', '', 'string'), 2); - - $id = $this->input->get('id', 0, 'int'); - - // Check if reference item can be edited. - if (!AssociationsHelper::allowEdit($extensionName, $typeName, $id)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); - $this->setRedirect(Route::_('index.php?option=com_associations&view=associations', false)); - - return false; - } - - return parent::display(); - } - - /** - * Method for canceling the edit action - * - * @param string $key The name of the primary key of the URL variable. - * - * @return void - * - * @since 3.7.0 - */ - public function cancel($key = null) - { - $this->checkToken(); - - list($extensionName, $typeName) = explode('.', $this->input->get('itemtype', '', 'string'), 2); - - // Only check in, if component item type allows to check out. - if (AssociationsHelper::typeSupportsCheckout($extensionName, $typeName)) - { - $ids = array(); - $targetId = $this->input->get('target-id', '', 'string'); - - if ($targetId !== '') - { - $ids = array_unique(explode(',', $targetId)); - } - - $ids[] = $this->input->get('id', 0, 'int'); - - foreach ($ids as $key => $id) - { - AssociationsHelper::getItem($extensionName, $typeName, $id)->checkIn(); - } - } - - $this->setRedirect(Route::_('index.php?option=com_associations&view=associations', false)); - } + /** + * Method to edit an existing record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key + * (sometimes required to avoid router collisions). + * + * @return FormController|boolean True if access level check and checkout passes, false otherwise. + * + * @since 3.7.0 + */ + public function edit($key = null, $urlVar = null) + { + list($extensionName, $typeName) = explode('.', $this->input->get('itemtype', '', 'string'), 2); + + $id = $this->input->get('id', 0, 'int'); + + // Check if reference item can be edited. + if (!AssociationsHelper::allowEdit($extensionName, $typeName, $id)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_associations&view=associations', false)); + + return false; + } + + return parent::display(); + } + + /** + * Method for canceling the edit action + * + * @param string $key The name of the primary key of the URL variable. + * + * @return void + * + * @since 3.7.0 + */ + public function cancel($key = null) + { + $this->checkToken(); + + list($extensionName, $typeName) = explode('.', $this->input->get('itemtype', '', 'string'), 2); + + // Only check in, if component item type allows to check out. + if (AssociationsHelper::typeSupportsCheckout($extensionName, $typeName)) { + $ids = array(); + $targetId = $this->input->get('target-id', '', 'string'); + + if ($targetId !== '') { + $ids = array_unique(explode(',', $targetId)); + } + + $ids[] = $this->input->get('id', 0, 'int'); + + foreach ($ids as $key => $id) { + AssociationsHelper::getItem($extensionName, $typeName, $id)->checkIn(); + } + } + + $this->setRedirect(Route::_('index.php?option=com_associations&view=associations', false)); + } } diff --git a/administrator/components/com_associations/src/Controller/AssociationsController.php b/administrator/components/com_associations/src/Controller/AssociationsController.php index 22bf8c7f8bae3..25338986441bb 100644 --- a/administrator/components/com_associations/src/Controller/AssociationsController.php +++ b/administrator/components/com_associations/src/Controller/AssociationsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to purge the associations table. - * - * @return void - * - * @since 3.7.0 - */ - public function purge() - { - $this->checkToken(); - - $this->getModel('associations')->purge(); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); - } - - /** - * Method to delete the orphans from the associations table. - * - * @return void - * - * @since 3.7.0 - */ - public function clean() - { - $this->checkToken(); - - $this->getModel('associations')->clean(); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); - } - - /** - * Method to check in an item from the association item overview. - * - * @return void - * - * @since 3.7.1 - */ - public function checkin() - { - // Set the redirect so we can just stop processing when we find a condition we can't process - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); - - // Figure out if the item supports checking and check it in - list($extensionName, $typeName) = explode('.', $this->input->get('itemtype')); - - $extension = AssociationsHelper::getSupportedExtension($extensionName); - $types = $extension->get('types'); - - if (!\array_key_exists($typeName, $types)) - { - return; - } - - if (AssociationsHelper::typeSupportsCheckout($extensionName, $typeName) === false) - { - // How on earth we came to that point, eject internet - return; - } - - $cid = (array) $this->input->get('cid', array(), 'int'); - - if (empty($cid)) - { - // Seems we don't have an id to work with. - return; - } - - // We know the first element is the one we need because we don't allow multi selection of rows - $id = $cid[0]; - - if ($id === 0) - { - // Seems we don't have an id to work with. - return; - } - - if (AssociationsHelper::canCheckinItem($extensionName, $typeName, $id) === true) - { - $item = AssociationsHelper::getItem($extensionName, $typeName, $id); - - $item->checkIn($id); - - return; - } - - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list), - Text::_('COM_ASSOCIATIONS_YOU_ARE_NOT_ALLOWED_TO_CHECKIN_THIS_ITEM') - ); - } + /** + * The URL view list variable. + * + * @var string + * + * @since 3.7.0 + */ + protected $view_list = 'associations'; + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel|boolean + * + * @since 3.7.0 + */ + public function getModel($name = 'Associations', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to purge the associations table. + * + * @return void + * + * @since 3.7.0 + */ + public function purge() + { + $this->checkToken(); + + $this->getModel('associations')->purge(); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); + } + + /** + * Method to delete the orphans from the associations table. + * + * @return void + * + * @since 3.7.0 + */ + public function clean() + { + $this->checkToken(); + + $this->getModel('associations')->clean(); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); + } + + /** + * Method to check in an item from the association item overview. + * + * @return void + * + * @since 3.7.1 + */ + public function checkin() + { + // Set the redirect so we can just stop processing when we find a condition we can't process + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); + + // Figure out if the item supports checking and check it in + list($extensionName, $typeName) = explode('.', $this->input->get('itemtype')); + + $extension = AssociationsHelper::getSupportedExtension($extensionName); + $types = $extension->get('types'); + + if (!\array_key_exists($typeName, $types)) { + return; + } + + if (AssociationsHelper::typeSupportsCheckout($extensionName, $typeName) === false) { + // How on earth we came to that point, eject internet + return; + } + + $cid = (array) $this->input->get('cid', array(), 'int'); + + if (empty($cid)) { + // Seems we don't have an id to work with. + return; + } + + // We know the first element is the one we need because we don't allow multi selection of rows + $id = $cid[0]; + + if ($id === 0) { + // Seems we don't have an id to work with. + return; + } + + if (AssociationsHelper::canCheckinItem($extensionName, $typeName, $id) === true) { + $item = AssociationsHelper::getItem($extensionName, $typeName, $id); + + $item->checkIn($id); + + return; + } + + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list), + Text::_('COM_ASSOCIATIONS_YOU_ARE_NOT_ALLOWED_TO_CHECKIN_THIS_ITEM') + ); + } } diff --git a/administrator/components/com_associations/src/Controller/DisplayController.php b/administrator/components/com_associations/src/Controller/DisplayController.php index e2a48320e1d91..d7e6dce8321c6 100644 --- a/administrator/components/com_associations/src/Controller/DisplayController.php +++ b/administrator/components/com_associations/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('itemtype', '', 'string'); - - if ($itemType !== '') - { - list($extensionName, $typeName) = explode('.', $itemType); - - if (!AssociationsHelper::hasSupport($extensionName)) - { - throw new \Exception( - Text::sprintf('COM_ASSOCIATIONS_COMPONENT_NOT_SUPPORTED', $this->app->getLanguage()->_($extensionName)), - 404 - ); - } - - if (!$this->app->getIdentity()->authorise('core.manage', $extensionName)) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } - } + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + * + * @throws \Exception|NotAllowed + */ + protected function checkAccess() + { + parent::checkAccess(); + + // Check if user has permission to access the component item type. + $itemType = $this->input->get('itemtype', '', 'string'); + + if ($itemType !== '') { + list($extensionName, $typeName) = explode('.', $itemType); + + if (!AssociationsHelper::hasSupport($extensionName)) { + throw new \Exception( + Text::sprintf('COM_ASSOCIATIONS_COMPONENT_NOT_SUPPORTED', $this->app->getLanguage()->_($extensionName)), + 404 + ); + } + + if (!$this->app->getIdentity()->authorise('core.manage', $extensionName)) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } + } } diff --git a/administrator/components/com_associations/src/Field/ItemlanguageField.php b/administrator/components/com_associations/src/Field/ItemlanguageField.php index 0d63fe5951d4d..9045fcdeda301 100644 --- a/administrator/components/com_associations/src/Field/ItemlanguageField.php +++ b/administrator/components/com_associations/src/Field/ItemlanguageField.php @@ -1,4 +1,5 @@ input; - - list($extensionName, $typeName) = explode('.', $input->get('itemtype', '', 'string'), 2); - - // Get the extension specific helper method - $helper = AssociationsHelper::getExtensionHelper($extensionName); - - $languageField = $helper->getTypeFieldName($typeName, 'language'); - $referenceId = $input->get('id', 0, 'int'); - $reference = ArrayHelper::fromObject(AssociationsHelper::getItem($extensionName, $typeName, $referenceId)); - $referenceLang = $reference[$languageField]; - - // Get item associations given ID and item type - $associations = AssociationsHelper::getAssociationList($extensionName, $typeName, $referenceId); - - // Check if user can create items in this component item type. - $canCreate = AssociationsHelper::allowAdd($extensionName, $typeName); - - // Gets existing languages. - $existingLanguages = LanguageHelper::getContentLanguages(array(0, 1), false); - - $options = array(); - - // Each option has the format "|", example: "en-GB|1" - foreach ($existingLanguages as $langCode => $language) - { - // If language code is equal to reference language we don't need it. - if ($language->lang_code == $referenceLang) - { - continue; - } - - $options[$langCode] = new \stdClass; - $options[$langCode]->text = $language->title; - - // If association exists in this language. - if (isset($associations[$language->lang_code])) - { - $itemId = (int) $associations[$language->lang_code]['id']; - $options[$langCode]->value = $language->lang_code . ':' . $itemId . ':edit'; - - // Check if user does have permission to edit the associated item. - $canEdit = AssociationsHelper::allowEdit($extensionName, $typeName, $itemId); - - // Check if item can be checked out - $canCheckout = AssociationsHelper::canCheckinItem($extensionName, $typeName, $itemId); - - // Disable language if user is not allowed to edit the item associated to it. - $options[$langCode]->disable = !($canEdit && $canCheckout); - } - else - { - // New item, id = 0 and disabled if user is not allowed to create new items. - $options[$langCode]->value = $language->lang_code . ':0:add'; - - // Disable language if user is not allowed to create items. - $options[$langCode]->disable = !$canCreate; - } - } - - return array_merge(parent::getOptions(), $options); - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'Itemlanguage'; + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.7.0 + */ + protected function getOptions() + { + $input = Factory::getApplication()->input; + + list($extensionName, $typeName) = explode('.', $input->get('itemtype', '', 'string'), 2); + + // Get the extension specific helper method + $helper = AssociationsHelper::getExtensionHelper($extensionName); + + $languageField = $helper->getTypeFieldName($typeName, 'language'); + $referenceId = $input->get('id', 0, 'int'); + $reference = ArrayHelper::fromObject(AssociationsHelper::getItem($extensionName, $typeName, $referenceId)); + $referenceLang = $reference[$languageField]; + + // Get item associations given ID and item type + $associations = AssociationsHelper::getAssociationList($extensionName, $typeName, $referenceId); + + // Check if user can create items in this component item type. + $canCreate = AssociationsHelper::allowAdd($extensionName, $typeName); + + // Gets existing languages. + $existingLanguages = LanguageHelper::getContentLanguages(array(0, 1), false); + + $options = array(); + + // Each option has the format "|", example: "en-GB|1" + foreach ($existingLanguages as $langCode => $language) { + // If language code is equal to reference language we don't need it. + if ($language->lang_code == $referenceLang) { + continue; + } + + $options[$langCode] = new \stdClass(); + $options[$langCode]->text = $language->title; + + // If association exists in this language. + if (isset($associations[$language->lang_code])) { + $itemId = (int) $associations[$language->lang_code]['id']; + $options[$langCode]->value = $language->lang_code . ':' . $itemId . ':edit'; + + // Check if user does have permission to edit the associated item. + $canEdit = AssociationsHelper::allowEdit($extensionName, $typeName, $itemId); + + // Check if item can be checked out + $canCheckout = AssociationsHelper::canCheckinItem($extensionName, $typeName, $itemId); + + // Disable language if user is not allowed to edit the item associated to it. + $options[$langCode]->disable = !($canEdit && $canCheckout); + } else { + // New item, id = 0 and disabled if user is not allowed to create new items. + $options[$langCode]->value = $language->lang_code . ':0:add'; + + // Disable language if user is not allowed to create items. + $options[$langCode]->disable = !$canCreate; + } + } + + return array_merge(parent::getOptions(), $options); + } } diff --git a/administrator/components/com_associations/src/Field/ItemtypeField.php b/administrator/components/com_associations/src/Field/ItemtypeField.php index c31c081ead930..faf7b967a18aa 100644 --- a/administrator/components/com_associations/src/Field/ItemtypeField.php +++ b/administrator/components/com_associations/src/Field/ItemtypeField.php @@ -1,4 +1,5 @@ get('associationssupport') === true) - { - foreach ($extension->get('types') as $type) - { - $context = $extension->get('component') . '.' . $type->get('name'); - $options[$extension->get('title')][] = HTMLHelper::_('select.option', $context, $type->get('title')); - } - } - } + foreach ($extensions as $extension) { + if ($extension->get('associationssupport') === true) { + foreach ($extension->get('types') as $type) { + $context = $extension->get('component') . '.' . $type->get('name'); + $options[$extension->get('title')][] = HTMLHelper::_('select.option', $context, $type->get('title')); + } + } + } - // Sort by alpha order. - uksort($options, 'strnatcmp'); + // Sort by alpha order. + uksort($options, 'strnatcmp'); - // Add options to parent array. - return array_merge(parent::getGroups(), $options); - } + // Add options to parent array. + return array_merge(parent::getGroups(), $options); + } } diff --git a/administrator/components/com_associations/src/Field/Modal/AssociationField.php b/administrator/components/com_associations/src/Field/Modal/AssociationField.php index f05d89046b30d..481b2f94292da 100644 --- a/administrator/components/com_associations/src/Field/Modal/AssociationField.php +++ b/administrator/components/com_associations/src/Field/Modal/AssociationField.php @@ -1,4 +1,5 @@ value ?: ''; - - $doc = Factory::getApplication()->getDocument(); - $wa = $doc->getWebAssetManager(); - - $doc->addScriptOptions('admin_associations_modal', ['itemId' => $value]); - $wa->useScript('com_associations.admin-associations-modal'); - - // Setup variables for display. - $html = array(); - - $linkAssociations = 'index.php?option=com_associations&view=associations&layout=modal&tmpl=component' - . '&forcedItemType=' . Factory::getApplication()->input->get('itemtype', '', 'string') . '&function=jSelectAssociation_' . $this->id; - - $linkAssociations .= "&forcedLanguage=' + document.getElementById('target-association').getAttribute('data-language') + '"; - - $urlSelect = $linkAssociations . '&' . Session::getFormToken() . '=1'; - - // Select custom association button - $html[] = '' - . ' ' - . '' - . ''; - - // Clear association button - $html[] = '' - . ' ' . Text::_('JCLEAR') - . ''; - - $html[] = ''; - - // Select custom association modal - $html[] = HTMLHelper::_( - 'bootstrap.renderModal', - 'associationSelect' . $this->id . 'Modal', - array( - 'title' => Text::_('COM_ASSOCIATIONS_SELECT_TARGET'), - 'backdrop' => 'static', - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - - return implode("\n", $html); - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'Modal_Association'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 3.7.0 + */ + protected function getInput() + { + // @todo USE Layouts here!!! + // The active item id field. + $value = (int) $this->value ?: ''; + + $doc = Factory::getApplication()->getDocument(); + $wa = $doc->getWebAssetManager(); + + $doc->addScriptOptions('admin_associations_modal', ['itemId' => $value]); + $wa->useScript('com_associations.admin-associations-modal'); + + // Setup variables for display. + $html = array(); + + $linkAssociations = 'index.php?option=com_associations&view=associations&layout=modal&tmpl=component' + . '&forcedItemType=' . Factory::getApplication()->input->get('itemtype', '', 'string') . '&function=jSelectAssociation_' . $this->id; + + $linkAssociations .= "&forcedLanguage=' + document.getElementById('target-association').getAttribute('data-language') + '"; + + $urlSelect = $linkAssociations . '&' . Session::getFormToken() . '=1'; + + // Select custom association button + $html[] = '' + . ' ' + . '' + . ''; + + // Clear association button + $html[] = '' + . ' ' . Text::_('JCLEAR') + . ''; + + $html[] = ''; + + // Select custom association modal + $html[] = HTMLHelper::_( + 'bootstrap.renderModal', + 'associationSelect' . $this->id . 'Modal', + array( + 'title' => Text::_('COM_ASSOCIATIONS_SELECT_TARGET'), + 'backdrop' => 'static', + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + + return implode("\n", $html); + } } diff --git a/administrator/components/com_associations/src/Helper/AssociationsHelper.php b/administrator/components/com_associations/src/Helper/AssociationsHelper.php index 5bf5b1df72b5d..f17f9ab647aef 100644 --- a/administrator/components/com_associations/src/Helper/AssociationsHelper.php +++ b/administrator/components/com_associations/src/Helper/AssociationsHelper.php @@ -1,4 +1,5 @@ getAssociationList($typeName, $itemId); - - } - - /** - * Get the the instance of the extension helper class - * - * @param string $extensionName The extension name with com_ - * - * @return \Joomla\CMS\Association\AssociationExtensionHelper|null - * - * @since 3.7.0 - */ - public static function getExtensionHelper($extensionName) - { - if (!self::hasSupport($extensionName)) - { - return null; - } - - $support = self::$extensionsSupport[$extensionName]; - - return $support->get('helper'); - } - - /** - * Get item information - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * @param int $itemId The id of item for which we need the associated items - * - * @return \Joomla\CMS\Table\Table|null - * - * @since 3.7.0 - */ - public static function getItem($extensionName, $typeName, $itemId) - { - if (!self::hasSupport($extensionName)) - { - return null; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - return $helper->getItem($typeName, $itemId); - } - - /** - * Check if extension supports associations - * - * @param string $extensionName The extension name with com_ - * - * @return boolean - * - * @since 3.7.0 - */ - public static function hasSupport($extensionName) - { - if (\is_null(self::$extensionsSupport)) - { - self::getSupportedExtensions(); - } - - return \in_array($extensionName, self::$supportedExtensionsList); - } - - /** - * Loads the helper for the given class. - * - * @param string $extensionName The extension name with com_ - * - * @return AssociationExtensionInterface|null - * - * @since 4.0.0 - */ - private static function loadHelper($extensionName) - { - $component = Factory::getApplication()->bootComponent($extensionName); - - if ($component instanceof AssociationServiceInterface) - { - return $component->getAssociationsExtension(); - } - - // Check if associations helper exists - if (!file_exists(JPATH_ADMINISTRATOR . '/components/' . $extensionName . '/helpers/associations.php')) - { - return null; - } - - require_once JPATH_ADMINISTRATOR . '/components/' . $extensionName . '/helpers/associations.php'; - - $componentAssociationsHelperClassName = self::getExtensionHelperClassName($extensionName); - - if (!class_exists($componentAssociationsHelperClassName, false)) - { - return null; - } - - // Create an instance of the helper class - return new $componentAssociationsHelperClassName; - } - - /** - * Get the extension specific helper class name - * - * @param string $extensionName The extension name with com_ - * - * @return string - * - * @since 3.7.0 - */ - private static function getExtensionHelperClassName($extensionName) - { - $realName = self::getExtensionRealName($extensionName); - - return ucfirst($realName) . 'AssociationsHelper'; - } - - /** - * Get the real extension name. This means without com_ - * - * @param string $extensionName The extension name with com_ - * - * @return string - * - * @since 3.7.0 - */ - private static function getExtensionRealName($extensionName) - { - return strpos($extensionName, 'com_') === false ? $extensionName : substr($extensionName, 4); - } - - /** - * Get the associated language edit links Html. - * - * @param string $extensionName Extension Name - * @param string $typeName ItemType - * @param integer $itemId Item id. - * @param string $itemLanguage Item language code. - * @param boolean $addLink True for adding edit links. False for just text. - * @param boolean $assocLanguages True for showing non associated content languages. False only languages with associations. - * - * @return string The language HTML - * - * @since 3.7.0 - */ - public static function getAssociationHtmlList($extensionName, $typeName, $itemId, $itemLanguage, $addLink = true, $assocLanguages = true) - { - // Get the associations list for this item. - $items = self::getAssociationList($extensionName, $typeName, $itemId); - - $titleFieldName = self::getTypeFieldName($extensionName, $typeName, 'title'); - - // Get all content languages. - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - $content_languages = array_column($languages, 'lang_code'); - - // Display warning if Content Language is trashed or deleted - foreach ($items as $item) - { - if (!\in_array($item['language'], $content_languages)) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item['language']), 'warning'); - } - } - - $canEditReference = self::allowEdit($extensionName, $typeName, $itemId); - $canCreate = self::allowAdd($extensionName, $typeName); - - // Create associated items list. - foreach ($languages as $langCode => $language) - { - // Don't do for the reference language. - if ($langCode == $itemLanguage) - { - continue; - } - - // Don't show languages with associations, if we don't want to show them. - if ($assocLanguages && isset($items[$langCode])) - { - unset($items[$langCode]); - continue; - } - - // Don't show languages without associations, if we don't want to show them. - if (!$assocLanguages && !isset($items[$langCode])) - { - continue; - } - - // Get html parameters. - if (isset($items[$langCode])) - { - $title = $items[$langCode][$titleFieldName]; - $additional = ''; - - if (isset($items[$langCode]['catid'])) - { - $db = Factory::getDbo(); - - // Get the category name - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $items[$langCode]['catid'], ParameterType::INTEGER); - - $db->setQuery($query); - $categoryTitle = $db->loadResult(); - - $additional = '' . Text::sprintf('JCATEGORY_SPRINTF', $categoryTitle) . '
    '; - } - elseif (isset($items[$langCode]['menutype'])) - { - $db = Factory::getDbo(); - - // Get the menutype name - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__menu_types')) - ->where($db->quoteName('menutype') . ' = :menutype') - ->bind(':menutype', $items[$langCode]['menutype']); - - $db->setQuery($query); - $menutypeTitle = $db->loadResult(); - - $additional = '' . Text::sprintf('COM_MENUS_MENU_SPRINTF', $menutypeTitle) . '
    '; - } - - $labelClass = 'bg-secondary'; - $target = $langCode . ':' . $items[$langCode]['id'] . ':edit'; - $allow = $canEditReference - && self::allowEdit($extensionName, $typeName, $items[$langCode]['id']) - && self::canCheckinItem($extensionName, $typeName, $items[$langCode]['id']); - - $additional .= $addLink && $allow ? Text::_('COM_ASSOCIATIONS_EDIT_ASSOCIATION') : ''; - } - else - { - $items[$langCode] = array(); - - $title = Text::_('COM_ASSOCIATIONS_NO_ASSOCIATION'); - $additional = $addLink ? Text::_('COM_ASSOCIATIONS_ADD_NEW_ASSOCIATION') : ''; - $labelClass = 'bg-warning text-dark'; - $target = $langCode . ':0:add'; - $allow = $canCreate; - } - - // Generate item Html. - $options = array( - 'option' => 'com_associations', - 'view' => 'association', - 'layout' => 'edit', - 'itemtype' => $extensionName . '.' . $typeName, - 'task' => 'association.edit', - 'id' => $itemId, - 'target' => $target, - ); - - $url = Route::_('index.php?' . http_build_query($options)); - $url = $allow && $addLink ? $url : ''; - $text = $language->lang_code; - - $tooltip = '' . htmlspecialchars($language->title, ENT_QUOTES, 'UTF-8') . '
    ' - . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '

    ' . $additional; - $classes = 'badge ' . $labelClass; - - $items[$langCode]['link'] = '' . $text . '' - . '
    ' . $tooltip . '
    '; - } - - return LayoutHelper::render('joomla.content.associations', $items); - } - - /** - * Get all extensions with associations support. - * - * @return array The extensions. - * - * @since 3.7.0 - */ - public static function getSupportedExtensions() - { - if (!\is_null(self::$extensionsSupport)) - { - return self::$extensionsSupport; - } - - self::$extensionsSupport = array(); - - $extensions = self::getEnabledExtensions(); - - foreach ($extensions as $extension) - { - $support = self::getSupportedExtension($extension->element); - - if ($support->get('associationssupport') === true) - { - self::$supportedExtensionsList[] = $extension->element; - } - - self::$extensionsSupport[$extension->element] = $support; - } - - return self::$extensionsSupport; - } - - /** - * Get item context based on the item key. - * - * @param string $extensionName The extension identifier. - * - * @return \Joomla\Registry\Registry The item properties. - * - * @since 3.7.0 - */ - public static function getSupportedExtension($extensionName) - { - $result = new Registry; - - $result->def('component', $extensionName); - $result->def('associationssupport', false); - $result->def('helper', null); - - $helper = self::loadHelper($extensionName); - - if (!$helper) - { - return $result; - } - - $result->set('helper', $helper); - - if ($helper->hasAssociationsSupport() === false) - { - return $result; - } - - $result->set('associationssupport', true); - - // Get the translated titles. - $languagePath = JPATH_ADMINISTRATOR . '/components/' . $extensionName; - $lang = Factory::getLanguage(); - - $lang->load($extensionName . '.sys', JPATH_ADMINISTRATOR); - $lang->load($extensionName . '.sys', $languagePath); - $lang->load($extensionName, JPATH_ADMINISTRATOR); - $lang->load($extensionName, $languagePath); - - $result->def('title', Text::_(strtoupper($extensionName))); - - // Get the supported types - $types = $helper->getItemTypes(); - $rTypes = array(); - - foreach ($types as $typeName) - { - $details = $helper->getType($typeName); - $context = 'component'; - $title = $helper->getTypeTitle($typeName); - $languageKey = $typeName; - - $typeNameExploded = explode('.', $typeName); - - if (array_pop($typeNameExploded) === 'category') - { - $languageKey = strtoupper($extensionName) . '_CATEGORIES'; - $context = 'category'; - } - - if ($lang->hasKey(strtoupper($extensionName . '_' . $title . 'S'))) - { - $languageKey = strtoupper($extensionName . '_' . $title . 'S'); - } - - $title = $lang->hasKey($languageKey) ? Text::_($languageKey) : Text::_('COM_ASSOCIATIONS_ITEMS'); - - $rType = new Registry; - - $rType->def('name', $typeName); - $rType->def('details', $details); - $rType->def('title', $title); - $rType->def('context', $context); - - $rTypes[$typeName] = $rType; - } - - $result->def('types', $rTypes); - - return $result; - } - - /** - * Get all installed and enabled extensions - * - * @return mixed - * - * @since 3.7.0 - */ - private static function getEnabledExtensions() - { - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('component')) - ->where($db->quoteName('enabled') . ' = 1'); - - $db->setQuery($query); - - return $db->loadObjectList(); - } - - /** - * Get all the content languages. - * - * @return array Array of objects all content languages by language code. - * - * @since 3.7.0 - */ - public static function getContentLanguages() - { - return LanguageHelper::getContentLanguages(array(0, 1)); - } - - /** - * Get the associated items for an item - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * @param int $itemId The id of item for which we need the associated items - * - * @return boolean - * - * @since 3.7.0 - */ - public static function allowEdit($extensionName, $typeName, $itemId) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - if (method_exists($helper, 'allowEdit')) - { - return $helper->allowEdit($typeName, $itemId); - } - - return Factory::getUser()->authorise('core.edit', $extensionName); - } - - /** - * Check if user is allowed to create items. - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * - * @return boolean True on allowed. - * - * @since 3.7.0 - */ - public static function allowAdd($extensionName, $typeName) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - if (method_exists($helper, 'allowAdd')) - { - return $helper->allowAdd($typeName); - } - - return Factory::getUser()->authorise('core.create', $extensionName); - } - - /** - * Check if an item is checked out - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * @param int $itemId The id of item for which we need the associated items - * - * @return boolean True if item is checked out. - * - * @since 3.7.0 - */ - public static function isCheckoutItem($extensionName, $typeName, $itemId) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - if (!self::typeSupportsCheckout($extensionName, $typeName)) - { - return false; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - if (method_exists($helper, 'isCheckoutItem')) - { - return $helper->isCheckoutItem($typeName, $itemId); - } - - $item = self::getItem($extensionName, $typeName, $itemId); - - $checkedOutFieldName = $helper->getTypeFieldName($typeName, 'checked_out'); - - return $item->{$checkedOutFieldName} != 0; - } - - /** - * Check if user can checkin an item. - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * @param int $itemId The id of item for which we need the associated items - * - * @return boolean True on allowed. - * - * @since 3.7.0 - */ - public static function canCheckinItem($extensionName, $typeName, $itemId) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - if (!self::typeSupportsCheckout($extensionName, $typeName)) - { - return true; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - if (method_exists($helper, 'canCheckinItem')) - { - return $helper->canCheckinItem($typeName, $itemId); - } - - $item = self::getItem($extensionName, $typeName, $itemId); - - $checkedOutFieldName = $helper->getTypeFieldName($typeName, 'checked_out'); - - $userId = Factory::getUser()->id; - - return ($item->{$checkedOutFieldName} == $userId || $item->{$checkedOutFieldName} == 0); - } - - /** - * Check if the type supports checkout - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * - * @return boolean True on allowed. - * - * @since 3.7.0 - */ - public static function typeSupportsCheckout($extensionName, $typeName) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - $support = $helper->getTypeSupport($typeName); - - return !empty($support['checkout']); - } - - /** - * Get a table field name for a type - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * @param string $fieldName The item type - * - * @return boolean True on allowed. - * - * @since 3.7.0 - */ - public static function getTypeFieldName($extensionName, $typeName, $fieldName) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - return $helper->getTypeFieldName($typeName, $fieldName); - } - - /** - * Gets the language filter system plugin extension id. - * - * @return integer The language filter system plugin extension id. - * - * @since 3.7.2 - */ - public static function getLanguagefilterPluginId() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('languagefilter')); - $db->setQuery($query); - - try - { - $result = (int) $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - return $result; - } + /** + * Array of Registry objects of extensions + * + * @var array + * @since 3.7.0 + */ + public static $extensionsSupport = null; + + /** + * List of extensions name with support + * + * @var array + * @since 3.7.0 + */ + public static $supportedExtensionsList = array(); + + /** + * Get the associated items for an item + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public static function getAssociationList($extensionName, $typeName, $itemId) + { + if (!self::hasSupport($extensionName)) { + return array(); + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + return $helper->getAssociationList($typeName, $itemId); + } + + /** + * Get the the instance of the extension helper class + * + * @param string $extensionName The extension name with com_ + * + * @return \Joomla\CMS\Association\AssociationExtensionHelper|null + * + * @since 3.7.0 + */ + public static function getExtensionHelper($extensionName) + { + if (!self::hasSupport($extensionName)) { + return null; + } + + $support = self::$extensionsSupport[$extensionName]; + + return $support->get('helper'); + } + + /** + * Get item information + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return \Joomla\CMS\Table\Table|null + * + * @since 3.7.0 + */ + public static function getItem($extensionName, $typeName, $itemId) + { + if (!self::hasSupport($extensionName)) { + return null; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + return $helper->getItem($typeName, $itemId); + } + + /** + * Check if extension supports associations + * + * @param string $extensionName The extension name with com_ + * + * @return boolean + * + * @since 3.7.0 + */ + public static function hasSupport($extensionName) + { + if (\is_null(self::$extensionsSupport)) { + self::getSupportedExtensions(); + } + + return \in_array($extensionName, self::$supportedExtensionsList); + } + + /** + * Loads the helper for the given class. + * + * @param string $extensionName The extension name with com_ + * + * @return AssociationExtensionInterface|null + * + * @since 4.0.0 + */ + private static function loadHelper($extensionName) + { + $component = Factory::getApplication()->bootComponent($extensionName); + + if ($component instanceof AssociationServiceInterface) { + return $component->getAssociationsExtension(); + } + + // Check if associations helper exists + if (!file_exists(JPATH_ADMINISTRATOR . '/components/' . $extensionName . '/helpers/associations.php')) { + return null; + } + + require_once JPATH_ADMINISTRATOR . '/components/' . $extensionName . '/helpers/associations.php'; + + $componentAssociationsHelperClassName = self::getExtensionHelperClassName($extensionName); + + if (!class_exists($componentAssociationsHelperClassName, false)) { + return null; + } + + // Create an instance of the helper class + return new $componentAssociationsHelperClassName(); + } + + /** + * Get the extension specific helper class name + * + * @param string $extensionName The extension name with com_ + * + * @return string + * + * @since 3.7.0 + */ + private static function getExtensionHelperClassName($extensionName) + { + $realName = self::getExtensionRealName($extensionName); + + return ucfirst($realName) . 'AssociationsHelper'; + } + + /** + * Get the real extension name. This means without com_ + * + * @param string $extensionName The extension name with com_ + * + * @return string + * + * @since 3.7.0 + */ + private static function getExtensionRealName($extensionName) + { + return strpos($extensionName, 'com_') === false ? $extensionName : substr($extensionName, 4); + } + + /** + * Get the associated language edit links Html. + * + * @param string $extensionName Extension Name + * @param string $typeName ItemType + * @param integer $itemId Item id. + * @param string $itemLanguage Item language code. + * @param boolean $addLink True for adding edit links. False for just text. + * @param boolean $assocLanguages True for showing non associated content languages. False only languages with associations. + * + * @return string The language HTML + * + * @since 3.7.0 + */ + public static function getAssociationHtmlList($extensionName, $typeName, $itemId, $itemLanguage, $addLink = true, $assocLanguages = true) + { + // Get the associations list for this item. + $items = self::getAssociationList($extensionName, $typeName, $itemId); + + $titleFieldName = self::getTypeFieldName($extensionName, $typeName, 'title'); + + // Get all content languages. + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + $content_languages = array_column($languages, 'lang_code'); + + // Display warning if Content Language is trashed or deleted + foreach ($items as $item) { + if (!\in_array($item['language'], $content_languages)) { + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item['language']), 'warning'); + } + } + + $canEditReference = self::allowEdit($extensionName, $typeName, $itemId); + $canCreate = self::allowAdd($extensionName, $typeName); + + // Create associated items list. + foreach ($languages as $langCode => $language) { + // Don't do for the reference language. + if ($langCode == $itemLanguage) { + continue; + } + + // Don't show languages with associations, if we don't want to show them. + if ($assocLanguages && isset($items[$langCode])) { + unset($items[$langCode]); + continue; + } + + // Don't show languages without associations, if we don't want to show them. + if (!$assocLanguages && !isset($items[$langCode])) { + continue; + } + + // Get html parameters. + if (isset($items[$langCode])) { + $title = $items[$langCode][$titleFieldName]; + $additional = ''; + + if (isset($items[$langCode]['catid'])) { + $db = Factory::getDbo(); + + // Get the category name + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $items[$langCode]['catid'], ParameterType::INTEGER); + + $db->setQuery($query); + $categoryTitle = $db->loadResult(); + + $additional = '' . Text::sprintf('JCATEGORY_SPRINTF', $categoryTitle) . '
    '; + } elseif (isset($items[$langCode]['menutype'])) { + $db = Factory::getDbo(); + + // Get the menutype name + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__menu_types')) + ->where($db->quoteName('menutype') . ' = :menutype') + ->bind(':menutype', $items[$langCode]['menutype']); + + $db->setQuery($query); + $menutypeTitle = $db->loadResult(); + + $additional = '' . Text::sprintf('COM_MENUS_MENU_SPRINTF', $menutypeTitle) . '
    '; + } + + $labelClass = 'bg-secondary'; + $target = $langCode . ':' . $items[$langCode]['id'] . ':edit'; + $allow = $canEditReference + && self::allowEdit($extensionName, $typeName, $items[$langCode]['id']) + && self::canCheckinItem($extensionName, $typeName, $items[$langCode]['id']); + + $additional .= $addLink && $allow ? Text::_('COM_ASSOCIATIONS_EDIT_ASSOCIATION') : ''; + } else { + $items[$langCode] = array(); + + $title = Text::_('COM_ASSOCIATIONS_NO_ASSOCIATION'); + $additional = $addLink ? Text::_('COM_ASSOCIATIONS_ADD_NEW_ASSOCIATION') : ''; + $labelClass = 'bg-warning text-dark'; + $target = $langCode . ':0:add'; + $allow = $canCreate; + } + + // Generate item Html. + $options = array( + 'option' => 'com_associations', + 'view' => 'association', + 'layout' => 'edit', + 'itemtype' => $extensionName . '.' . $typeName, + 'task' => 'association.edit', + 'id' => $itemId, + 'target' => $target, + ); + + $url = Route::_('index.php?' . http_build_query($options)); + $url = $allow && $addLink ? $url : ''; + $text = $language->lang_code; + + $tooltip = '' . htmlspecialchars($language->title, ENT_QUOTES, 'UTF-8') . '
    ' + . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '

    ' . $additional; + $classes = 'badge ' . $labelClass; + + $items[$langCode]['link'] = '' . $text . '' + . '
    ' . $tooltip . '
    '; + } + + return LayoutHelper::render('joomla.content.associations', $items); + } + + /** + * Get all extensions with associations support. + * + * @return array The extensions. + * + * @since 3.7.0 + */ + public static function getSupportedExtensions() + { + if (!\is_null(self::$extensionsSupport)) { + return self::$extensionsSupport; + } + + self::$extensionsSupport = array(); + + $extensions = self::getEnabledExtensions(); + + foreach ($extensions as $extension) { + $support = self::getSupportedExtension($extension->element); + + if ($support->get('associationssupport') === true) { + self::$supportedExtensionsList[] = $extension->element; + } + + self::$extensionsSupport[$extension->element] = $support; + } + + return self::$extensionsSupport; + } + + /** + * Get item context based on the item key. + * + * @param string $extensionName The extension identifier. + * + * @return \Joomla\Registry\Registry The item properties. + * + * @since 3.7.0 + */ + public static function getSupportedExtension($extensionName) + { + $result = new Registry(); + + $result->def('component', $extensionName); + $result->def('associationssupport', false); + $result->def('helper', null); + + $helper = self::loadHelper($extensionName); + + if (!$helper) { + return $result; + } + + $result->set('helper', $helper); + + if ($helper->hasAssociationsSupport() === false) { + return $result; + } + + $result->set('associationssupport', true); + + // Get the translated titles. + $languagePath = JPATH_ADMINISTRATOR . '/components/' . $extensionName; + $lang = Factory::getLanguage(); + + $lang->load($extensionName . '.sys', JPATH_ADMINISTRATOR); + $lang->load($extensionName . '.sys', $languagePath); + $lang->load($extensionName, JPATH_ADMINISTRATOR); + $lang->load($extensionName, $languagePath); + + $result->def('title', Text::_(strtoupper($extensionName))); + + // Get the supported types + $types = $helper->getItemTypes(); + $rTypes = array(); + + foreach ($types as $typeName) { + $details = $helper->getType($typeName); + $context = 'component'; + $title = $helper->getTypeTitle($typeName); + $languageKey = $typeName; + + $typeNameExploded = explode('.', $typeName); + + if (array_pop($typeNameExploded) === 'category') { + $languageKey = strtoupper($extensionName) . '_CATEGORIES'; + $context = 'category'; + } + + if ($lang->hasKey(strtoupper($extensionName . '_' . $title . 'S'))) { + $languageKey = strtoupper($extensionName . '_' . $title . 'S'); + } + + $title = $lang->hasKey($languageKey) ? Text::_($languageKey) : Text::_('COM_ASSOCIATIONS_ITEMS'); + + $rType = new Registry(); + + $rType->def('name', $typeName); + $rType->def('details', $details); + $rType->def('title', $title); + $rType->def('context', $context); + + $rTypes[$typeName] = $rType; + } + + $result->def('types', $rTypes); + + return $result; + } + + /** + * Get all installed and enabled extensions + * + * @return mixed + * + * @since 3.7.0 + */ + private static function getEnabledExtensions() + { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')) + ->where($db->quoteName('enabled') . ' = 1'); + + $db->setQuery($query); + + return $db->loadObjectList(); + } + + /** + * Get all the content languages. + * + * @return array Array of objects all content languages by language code. + * + * @since 3.7.0 + */ + public static function getContentLanguages() + { + return LanguageHelper::getContentLanguages(array(0, 1)); + } + + /** + * Get the associated items for an item + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return boolean + * + * @since 3.7.0 + */ + public static function allowEdit($extensionName, $typeName, $itemId) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + if (method_exists($helper, 'allowEdit')) { + return $helper->allowEdit($typeName, $itemId); + } + + return Factory::getUser()->authorise('core.edit', $extensionName); + } + + /** + * Check if user is allowed to create items. + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * + * @return boolean True on allowed. + * + * @since 3.7.0 + */ + public static function allowAdd($extensionName, $typeName) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + if (method_exists($helper, 'allowAdd')) { + return $helper->allowAdd($typeName); + } + + return Factory::getUser()->authorise('core.create', $extensionName); + } + + /** + * Check if an item is checked out + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return boolean True if item is checked out. + * + * @since 3.7.0 + */ + public static function isCheckoutItem($extensionName, $typeName, $itemId) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + if (!self::typeSupportsCheckout($extensionName, $typeName)) { + return false; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + if (method_exists($helper, 'isCheckoutItem')) { + return $helper->isCheckoutItem($typeName, $itemId); + } + + $item = self::getItem($extensionName, $typeName, $itemId); + + $checkedOutFieldName = $helper->getTypeFieldName($typeName, 'checked_out'); + + return $item->{$checkedOutFieldName} != 0; + } + + /** + * Check if user can checkin an item. + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return boolean True on allowed. + * + * @since 3.7.0 + */ + public static function canCheckinItem($extensionName, $typeName, $itemId) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + if (!self::typeSupportsCheckout($extensionName, $typeName)) { + return true; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + if (method_exists($helper, 'canCheckinItem')) { + return $helper->canCheckinItem($typeName, $itemId); + } + + $item = self::getItem($extensionName, $typeName, $itemId); + + $checkedOutFieldName = $helper->getTypeFieldName($typeName, 'checked_out'); + + $userId = Factory::getUser()->id; + + return ($item->{$checkedOutFieldName} == $userId || $item->{$checkedOutFieldName} == 0); + } + + /** + * Check if the type supports checkout + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * + * @return boolean True on allowed. + * + * @since 3.7.0 + */ + public static function typeSupportsCheckout($extensionName, $typeName) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + $support = $helper->getTypeSupport($typeName); + + return !empty($support['checkout']); + } + + /** + * Get a table field name for a type + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param string $fieldName The item type + * + * @return boolean True on allowed. + * + * @since 3.7.0 + */ + public static function getTypeFieldName($extensionName, $typeName, $fieldName) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + return $helper->getTypeFieldName($typeName, $fieldName); + } + + /** + * Gets the language filter system plugin extension id. + * + * @return integer The language filter system plugin extension id. + * + * @since 3.7.2 + */ + public static function getLanguagefilterPluginId() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('languagefilter')); + $db->setQuery($query); + + try { + $result = (int) $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + return $result; + } } diff --git a/administrator/components/com_associations/src/Model/AssociationModel.php b/administrator/components/com_associations/src/Model/AssociationModel.php index 54796fdedad5a..82182bc2e9cd9 100644 --- a/administrator/components/com_associations/src/Model/AssociationModel.php +++ b/administrator/components/com_associations/src/Model/AssociationModel.php @@ -1,4 +1,5 @@ loadForm('com_associations.association', 'association', array('control' => 'jform', 'load_data' => $loadData)); + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure + * + * @since 3.7.0 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_associations.association', 'association', array('control' => 'jform', 'load_data' => $loadData)); - return !empty($form) ? $form : false; - } + return !empty($form) ? $form : false; + } } diff --git a/administrator/components/com_associations/src/Model/AssociationsModel.php b/administrator/components/com_associations/src/Model/AssociationsModel.php index d63dc5bcf1586..6c8e08d586b59 100644 --- a/administrator/components/com_associations/src/Model/AssociationsModel.php +++ b/administrator/components/com_associations/src/Model/AssociationsModel.php @@ -1,4 +1,5 @@ input->get('forcedLanguage', '', 'cmd'); - $forcedItemType = $app->input->get('forcedItemType', '', 'string'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - // Adjust the context to support forced component item types. - if ($forcedItemType) - { - $this->context .= '.' . $forcedItemType; - } - - $this->setState('itemtype', $this->getUserStateFromRequest($this->context . '.itemtype', 'itemtype', '', 'string')); - $this->setState('language', $this->getUserStateFromRequest($this->context . '.language', 'language', '', 'string')); - - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); - $this->setState('filter.category_id', $this->getUserStateFromRequest($this->context . '.filter.category_id', 'filter_category_id', '', 'cmd')); - $this->setState('filter.menutype', $this->getUserStateFromRequest($this->context . '.filter.menutype', 'filter_menutype', '', 'string')); - $this->setState('filter.access', $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access', '', 'string')); - $this->setState('filter.level', $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level', '', 'cmd')); - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language. - if (!empty($forcedLanguage)) - { - $this->setState('language', $forcedLanguage); - } - - // Force a component item type. - if (!empty($forcedItemType)) - { - $this->setState('itemtype', $forcedItemType); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 3.7.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('itemtype'); - $id .= ':' . $this->getState('language'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.category_id'); - $id .= ':' . $this->getState('filter.menutype'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.level'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery|boolean - * - * @since 3.7.0 - */ - protected function getListQuery() - { - $type = null; - - list($extensionName, $typeName) = explode('.', $this->state->get('itemtype'), 2); - - $extension = AssociationsHelper::getSupportedExtension($extensionName); - $types = $extension->get('types'); - - if (\array_key_exists($typeName, $types)) - { - $type = $types[$typeName]; - } - - if (\is_null($type)) - { - return false; - } - - // Create a new query object. - $user = Factory::getUser(); - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $details = $type->get('details'); - - if (!\array_key_exists('support', $details)) - { - return false; - } - - $support = $details['support']; - - if (!\array_key_exists('fields', $details)) - { - return false; - } - - $fields = $details['fields']; - - // Main query. - $query->select($db->quoteName($fields['id'], 'id')) - ->select($db->quoteName($fields['title'], 'title')) - ->select($db->quoteName($fields['alias'], 'alias')); - - if (!\array_key_exists('tables', $details)) - { - return false; - } - - $tables = $details['tables']; - - foreach ($tables as $key => $table) - { - $query->from($db->quoteName($table, $key)); - } - - if (!\array_key_exists('joins', $details)) - { - return false; - } - - $joins = $details['joins']; - - foreach ($joins as $join) - { - $query->join($join['type'], $db->quoteName($join['condition'])); - } - - // Join over the language. - $query->select($db->quoteName($fields['language'], 'language')) - ->select($db->quoteName('l.title', 'language_title')) - ->select($db->quoteName('l.image', 'language_image')) - ->join( - 'LEFT', - $db->quoteName('#__languages', 'l'), - $db->quoteName('l.lang_code') . ' = ' . $db->quoteName($fields['language']) - ); - $extensionNameItem = $extensionName . '.item'; - - // Join over the associations. - $query->select('COUNT(' . $db->quoteName('asso2.id') . ') > 1 AS ' . $db->quoteName('association')) - ->join( - 'LEFT', - $db->quoteName('#__associations', 'asso'), - $db->quoteName('asso.id') . ' = ' . $db->quoteName($fields['id']) - . ' AND ' . $db->quoteName('asso.context') . ' = :context' - ) - ->join( - 'LEFT', - $db->quoteName('#__associations', 'asso2'), - $db->quoteName('asso2.key') . ' = ' . $db->quoteName('asso.key') - ) - ->bind(':context', $extensionNameItem); - - // Prepare the group by clause. - $groupby = array( - $fields['id'], - $fields['title'], - $fields['alias'], - $fields['language'], - 'l.title', - 'l.image', - ); - - // Select author for ACL checks. - if (!empty($fields['created_user_id'])) - { - $query->select($db->quoteName($fields['created_user_id'], 'created_user_id')); - - $groupby[] = $fields['created_user_id']; - } - - // Select checked out data for check in checkins. - if (!empty($fields['checked_out']) && !empty($fields['checked_out_time'])) - { - $query->select($db->quoteName($fields['checked_out'], 'checked_out')) - ->select($db->quoteName($fields['checked_out_time'], 'checked_out_time')); - - // Join over the users. - $query->select($db->quoteName('u.name', 'editor')) - ->join( - 'LEFT', - $db->quoteName('#__users', 'u'), - $db->quoteName('u.id') . ' = ' . $db->quoteName($fields['checked_out']) - ); - - $groupby[] = 'u.name'; - $groupby[] = $fields['checked_out']; - $groupby[] = $fields['checked_out_time']; - } - - // If component item type supports ordering, select the ordering also. - if (!empty($fields['ordering'])) - { - $query->select($db->quoteName($fields['ordering'], 'ordering')); - - $groupby[] = $fields['ordering']; - } - - // If component item type supports state, select the item state also. - if (!empty($fields['state'])) - { - $query->select($db->quoteName($fields['state'], 'state')); - - $groupby[] = $fields['state']; - } - - // If component item type supports level, select the level also. - if (!empty($fields['level'])) - { - $query->select($db->quoteName($fields['level'], 'level')); - - $groupby[] = $fields['level']; - } - - // If component item type supports categories, select the category also. - if (!empty($fields['catid'])) - { - $query->select($db->quoteName($fields['catid'], 'catid')); - - // Join over the categories. - $query->select($db->quoteName('c.title', 'category_title')) - ->join( - 'LEFT', - $db->quoteName('#__categories', 'c'), - $db->quoteName('c.id') . ' = ' . $db->quoteName($fields['catid']) - ); - - $groupby[] = 'c.title'; - $groupby[] = $fields['catid']; - } - - // If component item type supports menu type, select the menu type also. - if (!empty($fields['menutype'])) - { - $query->select($db->quoteName($fields['menutype'], 'menutype')); - - // Join over the menu types. - $query->select($db->quoteName('mt.title', 'menutype_title')) - ->select($db->quoteName('mt.id', 'menutypeid')) - ->join( - 'LEFT', - $db->quoteName('#__menu_types', 'mt'), - $db->quoteName('mt.menutype') . ' = ' . $db->quoteName($fields['menutype']) - ); - - $groupby[] = 'mt.title'; - $groupby[] = 'mt.id'; - $groupby[] = $fields['menutype']; - } - - // If component item type supports access level, select the access level also. - if (\array_key_exists('acl', $support) && $support['acl'] == true && !empty($fields['access'])) - { - $query->select($db->quoteName($fields['access'], 'access')); - - // Join over the access levels. - $query->select($db->quoteName('ag.title', 'access_level')) - ->join( - 'LEFT', - $db->quoteName('#__viewlevels', 'ag'), - $db->quoteName('ag.id') . ' = ' . $db->quoteName($fields['access']) - ); - - $groupby[] = 'ag.title'; - $groupby[] = $fields['access']; - - // Implement View Level Access. - if (!$user->authorise('core.admin', $extensionName)) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName($fields['access']), $groups); - } - } - - // If component item type is menus we need to remove the root item and the administrator menu. - if ($extensionName === 'com_menus') - { - $query->where($db->quoteName($fields['id']) . ' > 1') - ->where($db->quoteName('a.client_id') . ' = 0'); - } - - // If component item type is category we need to remove all other component categories. - if ($typeName === 'category') - { - $query->where($db->quoteName('a.extension') . ' = :extensionname') - ->bind(':extensionname', $extensionName); - } - elseif ($typeNameExploded = explode('.', $typeName)) - { - if (\count($typeNameExploded) > 1 && array_pop($typeNameExploded) === 'category') - { - $section = implode('.', $typeNameExploded); - $extensionNameSection = $extensionName . '.' . $section; - $query->where($db->quoteName('a.extension') . ' = :extensionsection') - ->bind(':extensionsection', $extensionNameSection); - } - } - - // Filter on the language. - if ($language = $this->getState('language')) - { - $query->where($db->quoteName($fields['language']) . ' = :language') - ->bind(':language', $language); - } - - // Filter by item state. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName($fields['state']) . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif ($state === '') - { - $query->whereIn($db->quoteName($fields['state']), [0, 1]); - } - - // Filter on the category. - $baselevel = 1; - - if ($categoryId = $this->getState('filter.category_id')) - { - $categoryTable = Table::getInstance('Category', 'JTable'); - $categoryTable->load($categoryId); - $baselevel = (int) $categoryTable->level; - - $lft = (int) $categoryTable->lft; - $rgt = (int) $categoryTable->rgt; - $query->where($db->quoteName('c.lft') . ' >= :lft') - ->where($db->quoteName('c.rgt') . ' <= :rgt') - ->bind(':lft', $lft, ParameterType::INTEGER) - ->bind(':rgt', $rgt, ParameterType::INTEGER); - } - - // Filter on the level. - if ($level = $this->getState('filter.level')) - { - $queryLevel = ((int) $level + (int) $baselevel - 1); - $query->where($db->quoteName('a.level') . ' <= :alevel') - ->bind(':alevel', $queryLevel, ParameterType::INTEGER); - } - - // Filter by menu type. - if ($menutype = $this->getState('filter.menutype')) - { - $query->where($db->quoteName($fields['menutype']) . ' = :menutype2') - ->bind(':menutype2', $menutype); - } - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - $access = (int) $access; - $query->where($db->quoteName($fields['access']) . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Filter by search in name. - if ($search = $this->getState('filter.search')) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName($fields['id']) . ' = :searchid') - ->bind(':searchid', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName($fields['title']) . ' LIKE :title' - . ' OR ' . $db->quoteName($fields['alias']) . ' LIKE :alias)' - ) - ->bind(':title', $search) - ->bind(':alias', $search); - } - } - - // Add the group by clause - $query->group($db->quoteName($groupby)); - - // Add the list ordering clause - $listOrdering = $this->state->get('list.ordering', 'id'); - $orderDirn = $this->state->get('list.direction', 'ASC'); - - $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); - - return $query; - } - - /** - * Delete associations from #__associations table. - * - * @param string $context The associations context. Empty for all. - * @param string $key The associations key. Empty for all. - * - * @return boolean True on success. - * - * @since 3.7.0 - */ - public function purge($context = '', $key = '') - { - $app = Factory::getApplication(); - $db = $this->getDatabase(); - $query = $db->getQuery(true)->delete($db->quoteName('#__associations')); - - // Filter by associations context. - if ($context) - { - $query->where($db->quoteName('context') . ' = :context') - ->bind(':context', $context); - } - - // Filter by key. - if ($key) - { - $query->where($db->quoteName('key') . ' = :key') - ->bind(':key', $key); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - $app->enqueueMessage(Text::_('COM_ASSOCIATIONS_PURGE_FAILED'), 'error'); - - return false; - } - - $app->enqueueMessage( - Text::_((int) $db->getAffectedRows() > 0 ? 'COM_ASSOCIATIONS_PURGE_SUCCESS' : 'COM_ASSOCIATIONS_PURGE_NONE'), - 'message' - ); - - return true; - } - - /** - * Delete orphans from the #__associations table. - * - * @param string $context The associations context. Empty for all. - * @param string $key The associations key. Empty for all. - * - * @return boolean True on success - * - * @since 3.7.0 - */ - public function clean($context = '', $key = '') - { - $app = Factory::getApplication(); - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('key') . ', COUNT(*)') - ->from($db->quoteName('#__associations')) - ->group($db->quoteName('key')) - ->having('COUNT(*) = 1'); - - // Filter by associations context. - if ($context) - { - $query->where($db->quoteName('context') . ' = :context') - ->bind(':context', $context); - } - - // Filter by key. - if ($key) - { - $query->where($db->quoteName('key') . ' = :key') - ->bind(':key', $key); - } - - $db->setQuery($query); - - $assocKeys = $db->loadObjectList(); - - $count = 0; - - // We have orphans. Let's delete them. - foreach ($assocKeys as $value) - { - $query->clear() - ->delete($db->quoteName('#__associations')) - ->where($db->quoteName('key') . ' = :valuekey') - ->bind(':valuekey', $value->key); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - $app->enqueueMessage(Text::_('COM_ASSOCIATIONS_DELETE_ORPHANS_FAILED'), 'error'); - - return false; - } - - $count += (int) $db->getAffectedRows(); - } - - $app->enqueueMessage( - Text::_($count > 0 ? 'COM_ASSOCIATIONS_DELETE_ORPHANS_SUCCESS' : 'COM_ASSOCIATIONS_DELETE_ORPHANS_NONE'), - 'message' - ); - - return true; - } + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.7 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', + 'title', + 'ordering', + 'itemtype', + 'language', + 'association', + 'menutype', + 'menutype_title', + 'level', + 'state', + 'category_id', + 'category_title', + 'access', + 'access_level', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.7.0 + */ + protected function populateState($ordering = 'ordering', $direction = 'asc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + $forcedItemType = $app->input->get('forcedItemType', '', 'string'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + // Adjust the context to support forced component item types. + if ($forcedItemType) { + $this->context .= '.' . $forcedItemType; + } + + $this->setState('itemtype', $this->getUserStateFromRequest($this->context . '.itemtype', 'itemtype', '', 'string')); + $this->setState('language', $this->getUserStateFromRequest($this->context . '.language', 'language', '', 'string')); + + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); + $this->setState('filter.category_id', $this->getUserStateFromRequest($this->context . '.filter.category_id', 'filter_category_id', '', 'cmd')); + $this->setState('filter.menutype', $this->getUserStateFromRequest($this->context . '.filter.menutype', 'filter_menutype', '', 'string')); + $this->setState('filter.access', $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access', '', 'string')); + $this->setState('filter.level', $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level', '', 'cmd')); + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language. + if (!empty($forcedLanguage)) { + $this->setState('language', $forcedLanguage); + } + + // Force a component item type. + if (!empty($forcedItemType)) { + $this->setState('itemtype', $forcedItemType); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 3.7.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('itemtype'); + $id .= ':' . $this->getState('language'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.category_id'); + $id .= ':' . $this->getState('filter.menutype'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.level'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery|boolean + * + * @since 3.7.0 + */ + protected function getListQuery() + { + $type = null; + + list($extensionName, $typeName) = explode('.', $this->state->get('itemtype'), 2); + + $extension = AssociationsHelper::getSupportedExtension($extensionName); + $types = $extension->get('types'); + + if (\array_key_exists($typeName, $types)) { + $type = $types[$typeName]; + } + + if (\is_null($type)) { + return false; + } + + // Create a new query object. + $user = Factory::getUser(); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $details = $type->get('details'); + + if (!\array_key_exists('support', $details)) { + return false; + } + + $support = $details['support']; + + if (!\array_key_exists('fields', $details)) { + return false; + } + + $fields = $details['fields']; + + // Main query. + $query->select($db->quoteName($fields['id'], 'id')) + ->select($db->quoteName($fields['title'], 'title')) + ->select($db->quoteName($fields['alias'], 'alias')); + + if (!\array_key_exists('tables', $details)) { + return false; + } + + $tables = $details['tables']; + + foreach ($tables as $key => $table) { + $query->from($db->quoteName($table, $key)); + } + + if (!\array_key_exists('joins', $details)) { + return false; + } + + $joins = $details['joins']; + + foreach ($joins as $join) { + $query->join($join['type'], $db->quoteName($join['condition'])); + } + + // Join over the language. + $query->select($db->quoteName($fields['language'], 'language')) + ->select($db->quoteName('l.title', 'language_title')) + ->select($db->quoteName('l.image', 'language_image')) + ->join( + 'LEFT', + $db->quoteName('#__languages', 'l'), + $db->quoteName('l.lang_code') . ' = ' . $db->quoteName($fields['language']) + ); + $extensionNameItem = $extensionName . '.item'; + + // Join over the associations. + $query->select('COUNT(' . $db->quoteName('asso2.id') . ') > 1 AS ' . $db->quoteName('association')) + ->join( + 'LEFT', + $db->quoteName('#__associations', 'asso'), + $db->quoteName('asso.id') . ' = ' . $db->quoteName($fields['id']) + . ' AND ' . $db->quoteName('asso.context') . ' = :context' + ) + ->join( + 'LEFT', + $db->quoteName('#__associations', 'asso2'), + $db->quoteName('asso2.key') . ' = ' . $db->quoteName('asso.key') + ) + ->bind(':context', $extensionNameItem); + + // Prepare the group by clause. + $groupby = array( + $fields['id'], + $fields['title'], + $fields['alias'], + $fields['language'], + 'l.title', + 'l.image', + ); + + // Select author for ACL checks. + if (!empty($fields['created_user_id'])) { + $query->select($db->quoteName($fields['created_user_id'], 'created_user_id')); + + $groupby[] = $fields['created_user_id']; + } + + // Select checked out data for check in checkins. + if (!empty($fields['checked_out']) && !empty($fields['checked_out_time'])) { + $query->select($db->quoteName($fields['checked_out'], 'checked_out')) + ->select($db->quoteName($fields['checked_out_time'], 'checked_out_time')); + + // Join over the users. + $query->select($db->quoteName('u.name', 'editor')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'u'), + $db->quoteName('u.id') . ' = ' . $db->quoteName($fields['checked_out']) + ); + + $groupby[] = 'u.name'; + $groupby[] = $fields['checked_out']; + $groupby[] = $fields['checked_out_time']; + } + + // If component item type supports ordering, select the ordering also. + if (!empty($fields['ordering'])) { + $query->select($db->quoteName($fields['ordering'], 'ordering')); + + $groupby[] = $fields['ordering']; + } + + // If component item type supports state, select the item state also. + if (!empty($fields['state'])) { + $query->select($db->quoteName($fields['state'], 'state')); + + $groupby[] = $fields['state']; + } + + // If component item type supports level, select the level also. + if (!empty($fields['level'])) { + $query->select($db->quoteName($fields['level'], 'level')); + + $groupby[] = $fields['level']; + } + + // If component item type supports categories, select the category also. + if (!empty($fields['catid'])) { + $query->select($db->quoteName($fields['catid'], 'catid')); + + // Join over the categories. + $query->select($db->quoteName('c.title', 'category_title')) + ->join( + 'LEFT', + $db->quoteName('#__categories', 'c'), + $db->quoteName('c.id') . ' = ' . $db->quoteName($fields['catid']) + ); + + $groupby[] = 'c.title'; + $groupby[] = $fields['catid']; + } + + // If component item type supports menu type, select the menu type also. + if (!empty($fields['menutype'])) { + $query->select($db->quoteName($fields['menutype'], 'menutype')); + + // Join over the menu types. + $query->select($db->quoteName('mt.title', 'menutype_title')) + ->select($db->quoteName('mt.id', 'menutypeid')) + ->join( + 'LEFT', + $db->quoteName('#__menu_types', 'mt'), + $db->quoteName('mt.menutype') . ' = ' . $db->quoteName($fields['menutype']) + ); + + $groupby[] = 'mt.title'; + $groupby[] = 'mt.id'; + $groupby[] = $fields['menutype']; + } + + // If component item type supports access level, select the access level also. + if (\array_key_exists('acl', $support) && $support['acl'] == true && !empty($fields['access'])) { + $query->select($db->quoteName($fields['access'], 'access')); + + // Join over the access levels. + $query->select($db->quoteName('ag.title', 'access_level')) + ->join( + 'LEFT', + $db->quoteName('#__viewlevels', 'ag'), + $db->quoteName('ag.id') . ' = ' . $db->quoteName($fields['access']) + ); + + $groupby[] = 'ag.title'; + $groupby[] = $fields['access']; + + // Implement View Level Access. + if (!$user->authorise('core.admin', $extensionName)) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName($fields['access']), $groups); + } + } + + // If component item type is menus we need to remove the root item and the administrator menu. + if ($extensionName === 'com_menus') { + $query->where($db->quoteName($fields['id']) . ' > 1') + ->where($db->quoteName('a.client_id') . ' = 0'); + } + + // If component item type is category we need to remove all other component categories. + if ($typeName === 'category') { + $query->where($db->quoteName('a.extension') . ' = :extensionname') + ->bind(':extensionname', $extensionName); + } elseif ($typeNameExploded = explode('.', $typeName)) { + if (\count($typeNameExploded) > 1 && array_pop($typeNameExploded) === 'category') { + $section = implode('.', $typeNameExploded); + $extensionNameSection = $extensionName . '.' . $section; + $query->where($db->quoteName('a.extension') . ' = :extensionsection') + ->bind(':extensionsection', $extensionNameSection); + } + } + + // Filter on the language. + if ($language = $this->getState('language')) { + $query->where($db->quoteName($fields['language']) . ' = :language') + ->bind(':language', $language); + } + + // Filter by item state. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName($fields['state']) . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif ($state === '') { + $query->whereIn($db->quoteName($fields['state']), [0, 1]); + } + + // Filter on the category. + $baselevel = 1; + + if ($categoryId = $this->getState('filter.category_id')) { + $categoryTable = Table::getInstance('Category', 'JTable'); + $categoryTable->load($categoryId); + $baselevel = (int) $categoryTable->level; + + $lft = (int) $categoryTable->lft; + $rgt = (int) $categoryTable->rgt; + $query->where($db->quoteName('c.lft') . ' >= :lft') + ->where($db->quoteName('c.rgt') . ' <= :rgt') + ->bind(':lft', $lft, ParameterType::INTEGER) + ->bind(':rgt', $rgt, ParameterType::INTEGER); + } + + // Filter on the level. + if ($level = $this->getState('filter.level')) { + $queryLevel = ((int) $level + (int) $baselevel - 1); + $query->where($db->quoteName('a.level') . ' <= :alevel') + ->bind(':alevel', $queryLevel, ParameterType::INTEGER); + } + + // Filter by menu type. + if ($menutype = $this->getState('filter.menutype')) { + $query->where($db->quoteName($fields['menutype']) . ' = :menutype2') + ->bind(':menutype2', $menutype); + } + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + $access = (int) $access; + $query->where($db->quoteName($fields['access']) . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Filter by search in name. + if ($search = $this->getState('filter.search')) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName($fields['id']) . ' = :searchid') + ->bind(':searchid', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName($fields['title']) . ' LIKE :title' + . ' OR ' . $db->quoteName($fields['alias']) . ' LIKE :alias)') + ->bind(':title', $search) + ->bind(':alias', $search); + } + } + + // Add the group by clause + $query->group($db->quoteName($groupby)); + + // Add the list ordering clause + $listOrdering = $this->state->get('list.ordering', 'id'); + $orderDirn = $this->state->get('list.direction', 'ASC'); + + $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); + + return $query; + } + + /** + * Delete associations from #__associations table. + * + * @param string $context The associations context. Empty for all. + * @param string $key The associations key. Empty for all. + * + * @return boolean True on success. + * + * @since 3.7.0 + */ + public function purge($context = '', $key = '') + { + $app = Factory::getApplication(); + $db = $this->getDatabase(); + $query = $db->getQuery(true)->delete($db->quoteName('#__associations')); + + // Filter by associations context. + if ($context) { + $query->where($db->quoteName('context') . ' = :context') + ->bind(':context', $context); + } + + // Filter by key. + if ($key) { + $query->where($db->quoteName('key') . ' = :key') + ->bind(':key', $key); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + $app->enqueueMessage(Text::_('COM_ASSOCIATIONS_PURGE_FAILED'), 'error'); + + return false; + } + + $app->enqueueMessage( + Text::_((int) $db->getAffectedRows() > 0 ? 'COM_ASSOCIATIONS_PURGE_SUCCESS' : 'COM_ASSOCIATIONS_PURGE_NONE'), + 'message' + ); + + return true; + } + + /** + * Delete orphans from the #__associations table. + * + * @param string $context The associations context. Empty for all. + * @param string $key The associations key. Empty for all. + * + * @return boolean True on success + * + * @since 3.7.0 + */ + public function clean($context = '', $key = '') + { + $app = Factory::getApplication(); + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('key') . ', COUNT(*)') + ->from($db->quoteName('#__associations')) + ->group($db->quoteName('key')) + ->having('COUNT(*) = 1'); + + // Filter by associations context. + if ($context) { + $query->where($db->quoteName('context') . ' = :context') + ->bind(':context', $context); + } + + // Filter by key. + if ($key) { + $query->where($db->quoteName('key') . ' = :key') + ->bind(':key', $key); + } + + $db->setQuery($query); + + $assocKeys = $db->loadObjectList(); + + $count = 0; + + // We have orphans. Let's delete them. + foreach ($assocKeys as $value) { + $query->clear() + ->delete($db->quoteName('#__associations')) + ->where($db->quoteName('key') . ' = :valuekey') + ->bind(':valuekey', $value->key); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + $app->enqueueMessage(Text::_('COM_ASSOCIATIONS_DELETE_ORPHANS_FAILED'), 'error'); + + return false; + } + + $count += (int) $db->getAffectedRows(); + } + + $app->enqueueMessage( + Text::_($count > 0 ? 'COM_ASSOCIATIONS_DELETE_ORPHANS_SUCCESS' : 'COM_ASSOCIATIONS_DELETE_ORPHANS_NONE'), + 'message' + ); + + return true; + } } diff --git a/administrator/components/com_associations/src/View/Association/HtmlView.php b/administrator/components/com_associations/src/View/Association/HtmlView.php index c54f8b80dcfeb..5f270b6793c84 100644 --- a/administrator/components/com_associations/src/View/Association/HtmlView.php +++ b/administrator/components/com_associations/src/View/Association/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - - // Check for errors. - if (\count($errors = $model->getErrors())) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->app = Factory::getApplication(); - $this->form = $model->getForm(); - /** @var Input $input */ - $input = $this->app->input; - $this->referenceId = $input->get('id', 0, 'int'); - - [$extensionName, $typeName] = explode('.', $input->get('itemtype', '', 'string'), 2); - - /** @var Registry $extension */ - $extension = AssociationsHelper::getSupportedExtension($extensionName); - $types = $extension->get('types'); - - if (\array_key_exists($typeName, $types)) - { - $this->type = $types[$typeName]; - $this->typeSupports = []; - $details = $this->type->get('details'); - $this->save2copy = false; - - if (\array_key_exists('support', $details)) - { - $support = $details['support']; - $this->typeSupports = $support; - } - - if (!empty($this->typeSupports['save2copy'])) - { - $this->save2copy = true; - } - } - - $this->extensionName = $extensionName; - $this->typeName = $typeName; - $this->itemType = $extensionName . '.' . $typeName; - - $languageField = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'language'); - $referenceId = $input->get('id', 0, 'int'); - $reference = ArrayHelper::fromObject(AssociationsHelper::getItem($extensionName, $typeName, $referenceId)); - - $this->referenceLanguage = $reference[$languageField]; - $this->referenceTitle = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'title'); - $this->referenceTitleValue = $reference[$this->referenceTitle]; - - // Check for special case category - $typeNameExploded = explode('.', $typeName); - - if (array_pop($typeNameExploded) === 'category') - { - $this->typeName = 'category'; - - if ($typeNameExploded) - { - $extensionName .= '.' . implode('.', $typeNameExploded); - } - - $options = [ - 'option' => 'com_categories', - 'view' => 'category', - 'extension' => $extensionName, - 'tmpl' => 'component', - ]; - } - else - { - $options = [ - 'option' => $extensionName, - 'view' => $typeName, - 'extension' => $extensionName, - 'tmpl' => 'component', - ]; - } - - // Reference and target edit links. - $this->editUri = 'index.php?' . http_build_query($options); - - // Get target language. - $this->targetId = '0'; - $this->targetLanguage = ''; - $this->defaultTargetSrc = ''; - $this->targetAction = ''; - $this->targetTitle = ''; - - if ($target = $input->get('target', '', 'string')) - { - $matches = preg_split("#[\:]+#", $target); - $this->targetAction = $matches[2]; - $this->targetId = $matches[1]; - $this->targetLanguage = $matches[0]; - $this->targetTitle = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'title'); - $task = $typeName . '.' . $this->targetAction; - - /** - * Let's put the target src into a variable to use in the javascript code - * to avoid race conditions when the reference iframe loads. - */ - $this->document->addScriptOptions('targetSrc', Route::_($this->editUri . '&task=' . $task . '&id=' . (int) $this->targetId)); - $this->form->setValue('itemlanguage', '', $this->targetLanguage . ':' . $this->targetId . ':' . $this->targetAction); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.7.0 - * - * @throws \Exception - */ - protected function addToolbar(): void - { - // Hide main menu. - $this->app->input->set('hidemainmenu', 1); - - $helper = AssociationsHelper::getExtensionHelper($this->extensionName); - $title = $helper->getTypeTitle($this->typeName); - - $languageKey = strtoupper($this->extensionName . '_' . $title . 'S'); - - if ($this->typeName === 'category') - { - $languageKey = strtoupper($this->extensionName) . '_CATEGORIES'; - } - - ToolbarHelper::title( - Text::sprintf( - 'COM_ASSOCIATIONS_TITLE_EDIT', - Text::_($this->extensionName), - Text::_($languageKey) - ), - 'language assoc' - ); - - $bar = Toolbar::getInstance(); - - $bar->appendButton( - 'Custom', '', 'reference' - ); - - $bar->appendButton( - 'Custom', '', 'target' - ); - - if ($this->typeName === 'category' || $this->extensionName === 'com_menus' || $this->save2copy === true) - { - ToolbarHelper::custom('copy', 'copy.png', '', 'COM_ASSOCIATIONS_COPY_REFERENCE', false); - } - - ToolbarHelper::cancel('association.cancel', 'JTOOLBAR_CLOSE'); - ToolbarHelper::help('Multilingual_Associations:_Edit'); - } + /** + * An array of items + * + * @var array + * + * @since 3.7.0 + */ + protected $items = []; + + /** + * The pagination object + * + * @var Pagination + * + * @since 3.7.0 + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + * + * @since 3.7.0 + */ + protected $state; + + /** + * Selected item type properties. + * + * @var Registry + * + * @since 3.7.0 + */ + protected $itemType; + + /** + * The application + * + * @var AdministratorApplication + * @since 3.7.0 + */ + protected $app; + + /** + * The ID of the reference language + * + * @var integer + * @since 3.7.0 + */ + protected $referenceId = 0; + + /** + * The type name + * + * @var string + * @since 3.7.0 + */ + protected $typeName = ''; + + /** + * The reference language + * + * @var string + * @since 3.7.0 + */ + protected $referenceLanguage = ''; + + /** + * The title of the reference language + * + * @var string + * @since 3.7.0 + */ + protected $referenceTitle = ''; + + /** + * The value of the reference title + * + * @var string + * @since 3.7.0 + */ + protected $referenceTitleValue = ''; + + /** + * The URL to the edit screen + * + * @var string + * @since 3.7.0 + */ + protected $editUri = ''; + + /** + * The ID of the target field + * + * @var string + * @since 3.7.0 + */ + protected $targetId = ''; + + /** + * The target language + * + * @var string + * @since 3.7.0 + */ + protected $targetLanguage = ''; + + /** + * The source of the target field + * + * @var string + * @since 3.7.0 + */ + protected $defaultTargetSrc = ''; + + /** + * The action to perform for the target field + * + * @var string + * @since 3.7.0 + */ + protected $targetAction = ''; + + /** + * The title of the target field + * + * @var string + * @since 3.7.0 + */ + protected $targetTitle = ''; + + /** + * The edit form + * + * @var Form + * @since 3.7.0 + */ + protected $form; + + /** + * Set if the option is set to save as copy + * + * @var boolean + * @since 3.7.0 + */ + private $save2copy = false; + + /** + * The type of language + * + * @var Registry + * @since 3.7.0 + */ + private $type; + + /** + * The supported types + * + * @var array + * @since 3.7.0 + */ + private $typeSupports = []; + + /** + * The extension name + * + * @var string + * @since 3.7.0 + */ + private $extensionName = ''; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.7.0 + * + * @throws \Exception + */ + public function display($tpl = null): void + { + /** @var AssociationModel $model */ + $model = $this->getModel(); + + // Check for errors. + if (\count($errors = $model->getErrors())) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->app = Factory::getApplication(); + $this->form = $model->getForm(); + /** @var Input $input */ + $input = $this->app->input; + $this->referenceId = $input->get('id', 0, 'int'); + + [$extensionName, $typeName] = explode('.', $input->get('itemtype', '', 'string'), 2); + + /** @var Registry $extension */ + $extension = AssociationsHelper::getSupportedExtension($extensionName); + $types = $extension->get('types'); + + if (\array_key_exists($typeName, $types)) { + $this->type = $types[$typeName]; + $this->typeSupports = []; + $details = $this->type->get('details'); + $this->save2copy = false; + + if (\array_key_exists('support', $details)) { + $support = $details['support']; + $this->typeSupports = $support; + } + + if (!empty($this->typeSupports['save2copy'])) { + $this->save2copy = true; + } + } + + $this->extensionName = $extensionName; + $this->typeName = $typeName; + $this->itemType = $extensionName . '.' . $typeName; + + $languageField = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'language'); + $referenceId = $input->get('id', 0, 'int'); + $reference = ArrayHelper::fromObject(AssociationsHelper::getItem($extensionName, $typeName, $referenceId)); + + $this->referenceLanguage = $reference[$languageField]; + $this->referenceTitle = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'title'); + $this->referenceTitleValue = $reference[$this->referenceTitle]; + + // Check for special case category + $typeNameExploded = explode('.', $typeName); + + if (array_pop($typeNameExploded) === 'category') { + $this->typeName = 'category'; + + if ($typeNameExploded) { + $extensionName .= '.' . implode('.', $typeNameExploded); + } + + $options = [ + 'option' => 'com_categories', + 'view' => 'category', + 'extension' => $extensionName, + 'tmpl' => 'component', + ]; + } else { + $options = [ + 'option' => $extensionName, + 'view' => $typeName, + 'extension' => $extensionName, + 'tmpl' => 'component', + ]; + } + + // Reference and target edit links. + $this->editUri = 'index.php?' . http_build_query($options); + + // Get target language. + $this->targetId = '0'; + $this->targetLanguage = ''; + $this->defaultTargetSrc = ''; + $this->targetAction = ''; + $this->targetTitle = ''; + + if ($target = $input->get('target', '', 'string')) { + $matches = preg_split("#[\:]+#", $target); + $this->targetAction = $matches[2]; + $this->targetId = $matches[1]; + $this->targetLanguage = $matches[0]; + $this->targetTitle = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'title'); + $task = $typeName . '.' . $this->targetAction; + + /** + * Let's put the target src into a variable to use in the javascript code + * to avoid race conditions when the reference iframe loads. + */ + $this->document->addScriptOptions('targetSrc', Route::_($this->editUri . '&task=' . $task . '&id=' . (int) $this->targetId)); + $this->form->setValue('itemlanguage', '', $this->targetLanguage . ':' . $this->targetId . ':' . $this->targetAction); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.7.0 + * + * @throws \Exception + */ + protected function addToolbar(): void + { + // Hide main menu. + $this->app->input->set('hidemainmenu', 1); + + $helper = AssociationsHelper::getExtensionHelper($this->extensionName); + $title = $helper->getTypeTitle($this->typeName); + + $languageKey = strtoupper($this->extensionName . '_' . $title . 'S'); + + if ($this->typeName === 'category') { + $languageKey = strtoupper($this->extensionName) . '_CATEGORIES'; + } + + ToolbarHelper::title( + Text::sprintf( + 'COM_ASSOCIATIONS_TITLE_EDIT', + Text::_($this->extensionName), + Text::_($languageKey) + ), + 'language assoc' + ); + + $bar = Toolbar::getInstance(); + + $bar->appendButton( + 'Custom', + '', + 'reference' + ); + + $bar->appendButton( + 'Custom', + '', + 'target' + ); + + if ($this->typeName === 'category' || $this->extensionName === 'com_menus' || $this->save2copy === true) { + ToolbarHelper::custom('copy', 'copy.png', '', 'COM_ASSOCIATIONS_COPY_REFERENCE', false); + } + + ToolbarHelper::cancel('association.cancel', 'JTOOLBAR_CLOSE'); + ToolbarHelper::help('Multilingual_Associations:_Edit'); + } } diff --git a/administrator/components/com_associations/src/View/Associations/HtmlView.php b/administrator/components/com_associations/src/View/Associations/HtmlView.php index 12dc60323e892..9dfa2d3e27072 100644 --- a/administrator/components/com_associations/src/View/Associations/HtmlView.php +++ b/administrator/components/com_associations/src/View/Associations/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!Associations::isEnabled()) - { - $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . AssociationsHelper::getLanguagefilterPluginId()); - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_ASSOCIATIONS_ERROR_NO_ASSOC', $link), 'warning'); - } - elseif ($this->state->get('itemtype') != '' && $this->state->get('language') != '') - { - $type = null; - - list($extensionName, $typeName) = explode('.', $this->state->get('itemtype'), 2); - - $extension = AssociationsHelper::getSupportedExtension($extensionName); - - $types = $extension->get('types'); - - if (\array_key_exists($typeName, $types)) - { - $type = $types[$typeName]; - } - - $this->itemType = $type; - - if (\is_null($type)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_ASSOCIATIONS_ERROR_NO_TYPE'), 'warning'); - } - else - { - $this->extensionName = $extensionName; - $this->typeName = $typeName; - $this->typeSupports = array(); - $this->typeFields = array(); - - $details = $type->get('details'); - - if (\array_key_exists('support', $details)) - { - $support = $details['support']; - $this->typeSupports = $support; - } - - if (\array_key_exists('fields', $details)) - { - $fields = $details['fields']; - $this->typeFields = $fields; - } - - // Dynamic filter form. - // This selectors doesn't have to activate the filter bar. - unset($this->activeFilters['itemtype']); - unset($this->activeFilters['language']); - - // Remove filters options depending on selected type. - if (empty($support['state'])) - { - unset($this->activeFilters['state']); - $this->filterForm->removeField('state', 'filter'); - } - - if (empty($support['category'])) - { - unset($this->activeFilters['category_id']); - $this->filterForm->removeField('category_id', 'filter'); - } - - if ($extensionName !== 'com_menus') - { - unset($this->activeFilters['menutype']); - $this->filterForm->removeField('menutype', 'filter'); - } - - if (empty($support['level'])) - { - unset($this->activeFilters['level']); - $this->filterForm->removeField('level', 'filter'); - } - - if (empty($support['acl'])) - { - unset($this->activeFilters['access']); - $this->filterForm->removeField('access', 'filter'); - } - - // Add extension attribute to category filter. - if (empty($support['catid'])) - { - $this->filterForm->setFieldAttribute('category_id', 'extension', $extensionName, 'filter'); - - if ($this->getLayout() == 'modal') - { - // We need to change the category filter to only show categories tagged to All or to the forced language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); - } - } - } - - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - - $linkParameters = array( - 'layout' => 'edit', - 'itemtype' => $extensionName . '.' . $typeName, - 'task' => 'association.edit', - ); - - $this->editUri = 'index.php?option=com_associations&view=association&' . http_build_query($linkParameters); - } - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new \Exception(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.7.0 - */ - protected function addToolbar() - { - $user = $this->getCurrentUser(); - - if (isset($this->typeName) && isset($this->extensionName)) - { - $helper = AssociationsHelper::getExtensionHelper($this->extensionName); - $title = $helper->getTypeTitle($this->typeName); - - $languageKey = strtoupper($this->extensionName . '_' . $title . 'S'); - - if ($this->typeName === 'category') - { - $languageKey = strtoupper($this->extensionName) . '_CATEGORIES'; - } - - ToolbarHelper::title( - Text::sprintf( - 'COM_ASSOCIATIONS_TITLE_LIST', Text::_($this->extensionName), Text::_($languageKey) - ), 'language assoc' - ); - } - else - { - ToolbarHelper::title(Text::_('COM_ASSOCIATIONS_TITLE_LIST_SELECT'), 'language assoc'); - } - - if ($user->authorise('core.admin', 'com_associations') || $user->authorise('core.options', 'com_associations')) - { - if (!isset($this->typeName)) - { - ToolbarHelper::custom('associations.purge', 'purge', '', 'COM_ASSOCIATIONS_PURGE', false, false); - ToolbarHelper::custom('associations.clean', 'refresh', '', 'COM_ASSOCIATIONS_DELETE_ORPHANS', false, false); - } - - ToolbarHelper::preferences('com_associations'); - } - - ToolbarHelper::help('Multilingual_Associations'); - } + /** + * An array of items + * + * @var array + * + * @since 3.7.0 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.7.0 + */ + protected $pagination; + + /** + * The model state + * + * @var object + * + * @since 3.7.0 + */ + protected $state; + + /** + * Selected item type properties. + * + * @var \Joomla\Registry\Registry + * + * @since 3.7.0 + */ + public $itemType = null; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.7.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!Associations::isEnabled()) { + $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . AssociationsHelper::getLanguagefilterPluginId()); + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_ASSOCIATIONS_ERROR_NO_ASSOC', $link), 'warning'); + } elseif ($this->state->get('itemtype') != '' && $this->state->get('language') != '') { + $type = null; + + list($extensionName, $typeName) = explode('.', $this->state->get('itemtype'), 2); + + $extension = AssociationsHelper::getSupportedExtension($extensionName); + + $types = $extension->get('types'); + + if (\array_key_exists($typeName, $types)) { + $type = $types[$typeName]; + } + + $this->itemType = $type; + + if (\is_null($type)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_ASSOCIATIONS_ERROR_NO_TYPE'), 'warning'); + } else { + $this->extensionName = $extensionName; + $this->typeName = $typeName; + $this->typeSupports = array(); + $this->typeFields = array(); + + $details = $type->get('details'); + + if (\array_key_exists('support', $details)) { + $support = $details['support']; + $this->typeSupports = $support; + } + + if (\array_key_exists('fields', $details)) { + $fields = $details['fields']; + $this->typeFields = $fields; + } + + // Dynamic filter form. + // This selectors doesn't have to activate the filter bar. + unset($this->activeFilters['itemtype']); + unset($this->activeFilters['language']); + + // Remove filters options depending on selected type. + if (empty($support['state'])) { + unset($this->activeFilters['state']); + $this->filterForm->removeField('state', 'filter'); + } + + if (empty($support['category'])) { + unset($this->activeFilters['category_id']); + $this->filterForm->removeField('category_id', 'filter'); + } + + if ($extensionName !== 'com_menus') { + unset($this->activeFilters['menutype']); + $this->filterForm->removeField('menutype', 'filter'); + } + + if (empty($support['level'])) { + unset($this->activeFilters['level']); + $this->filterForm->removeField('level', 'filter'); + } + + if (empty($support['acl'])) { + unset($this->activeFilters['access']); + $this->filterForm->removeField('access', 'filter'); + } + + // Add extension attribute to category filter. + if (empty($support['catid'])) { + $this->filterForm->setFieldAttribute('category_id', 'extension', $extensionName, 'filter'); + + if ($this->getLayout() == 'modal') { + // We need to change the category filter to only show categories tagged to All or to the forced language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); + } + } + } + + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + + $linkParameters = array( + 'layout' => 'edit', + 'itemtype' => $extensionName . '.' . $typeName, + 'task' => 'association.edit', + ); + + $this->editUri = 'index.php?option=com_associations&view=association&' . http_build_query($linkParameters); + } + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new \Exception(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.7.0 + */ + protected function addToolbar() + { + $user = $this->getCurrentUser(); + + if (isset($this->typeName) && isset($this->extensionName)) { + $helper = AssociationsHelper::getExtensionHelper($this->extensionName); + $title = $helper->getTypeTitle($this->typeName); + + $languageKey = strtoupper($this->extensionName . '_' . $title . 'S'); + + if ($this->typeName === 'category') { + $languageKey = strtoupper($this->extensionName) . '_CATEGORIES'; + } + + ToolbarHelper::title( + Text::sprintf( + 'COM_ASSOCIATIONS_TITLE_LIST', + Text::_($this->extensionName), + Text::_($languageKey) + ), + 'language assoc' + ); + } else { + ToolbarHelper::title(Text::_('COM_ASSOCIATIONS_TITLE_LIST_SELECT'), 'language assoc'); + } + + if ($user->authorise('core.admin', 'com_associations') || $user->authorise('core.options', 'com_associations')) { + if (!isset($this->typeName)) { + ToolbarHelper::custom('associations.purge', 'purge', '', 'COM_ASSOCIATIONS_PURGE', false, false); + ToolbarHelper::custom('associations.clean', 'refresh', '', 'COM_ASSOCIATIONS_DELETE_ORPHANS', false, false); + } + + ToolbarHelper::preferences('com_associations'); + } + + ToolbarHelper::help('Multilingual_Associations'); + } } diff --git a/administrator/components/com_associations/tmpl/association/edit.php b/administrator/components/com_associations/tmpl/association/edit.php index 422799a43312a..0f7b5bc1b4a1a 100644 --- a/administrator/components/com_associations/tmpl/association/edit.php +++ b/administrator/components/com_associations/tmpl/association/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->usePreset('com_associations.sidebyside') - ->useScript('webcomponent.core-loader'); + ->useScript('form.validate') + ->usePreset('com_associations.sidebyside') + ->useScript('webcomponent.core-loader'); $options = [ - 'layout' => $this->app->input->get('layout', '', 'string'), - 'itemtype' => $this->itemType, - 'id' => $this->referenceId, + 'layout' => $this->app->input->get('layout', '', 'string'), + 'itemtype' => $this->itemType, + 'id' => $this->referenceId, ]; ?>
    -
    -
    -
    -

    - -
    -
    -
    -
    -
    -
    -

    -
    -
    -
    - form->getLabel('itemlanguage'); ?> -
    - form->getInput('itemlanguage'); ?> -
    -
    - form->getInput('modalassociation'); ?> -
    -
    - -
    -
    -
    +
    +
    +
    +

    + +
    +
    +
    +
    +
    +
    +

    +
    +
    +
    + form->getLabel('itemlanguage'); ?> +
    + form->getInput('itemlanguage'); ?> +
    +
    + form->getInput('modalassociation'); ?> +
    +
    + +
    +
    +
    - - - + + +
    diff --git a/administrator/components/com_associations/tmpl/associations/default.php b/administrator/components/com_associations/tmpl/associations/default.php index fd7879f38d399..ec539fed43377 100644 --- a/administrator/components/com_associations/tmpl/associations/default.php +++ b/administrator/components/com_associations/tmpl/associations/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_associations.admin-associations-default') - ->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('table.columns') + ->useScript('multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); $canManageCheckin = Factory::getUser()->authorise('core.manage', 'com_checkin'); $iconStates = array( - -2 => 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', + -2 => 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', ); Text::script('COM_ASSOCIATIONS_PURGE_CONFIRM_PROMPT', true); ?>
    -
    -
    -
    - $this)); ?> - state->get('itemtype') == '' || $this->state->get('language') == '') : ?> -
    - - -
    - items)) : ?> -
    - - -
    - - - - - - typeSupports['state'])) : ?> - - - - - - - typeFields['menutype'])) : ?> - - - typeFields['access'])) : ?> - - - - - - - items as $i => $item) : - $canCheckin = true; - $canEdit = AssociationsHelper::allowEdit($this->extensionName, $this->typeName, $item->id); - $canCheckin = $canManageCheckin || AssociationsHelper::canCheckinItem($this->extensionName, $this->typeName, $item->id); - $isCheckout = AssociationsHelper::isCheckoutItem($this->extensionName, $this->typeName, $item->id); - ?> - - typeSupports['state'])) : ?> - - - - - - - typeFields['menutype'])) : ?> - - - typeFields['access'])) : ?> - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - - - -
    - - -
    - level)) : ?> - $item->level)); ?> - - - editor, $item->checked_out_time, 'associations.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - - typeFields['alias'])) : ?> -
    - escape($item->alias)); ?> -
    - - typeFields['catid'])) : ?> -
    - escape($item->category_title); ?> -
    - -
    -
    - - - extensionName, $this->typeName, (int) $item->id, $item->language, !$isCheckout, false); ?> - - extensionName, $this->typeName, (int) $item->id, $item->language, !$isCheckout, true); ?> - - escape($item->menutype_title); ?> - - escape($item->access_level); ?> - - id; ?> -
    +
    +
    +
    + $this)); ?> + state->get('itemtype') == '' || $this->state->get('language') == '') : ?> +
    + + +
    + items)) : ?> +
    + + +
    + + + + + + typeSupports['state'])) : ?> + + + + + + + typeFields['menutype'])) : ?> + + + typeFields['access'])) : ?> + + + + + + + items as $i => $item) : + $canCheckin = true; + $canEdit = AssociationsHelper::allowEdit($this->extensionName, $this->typeName, $item->id); + $canCheckin = $canManageCheckin || AssociationsHelper::canCheckinItem($this->extensionName, $this->typeName, $item->id); + $isCheckout = AssociationsHelper::isCheckoutItem($this->extensionName, $this->typeName, $item->id); + ?> + + typeSupports['state'])) : ?> + + + + + + + typeFields['menutype'])) : ?> + + + typeFields['access'])) : ?> + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + + + +
    + + +
    + level)) : ?> + $item->level)); ?> + + + editor, $item->checked_out_time, 'associations.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + + typeFields['alias'])) : ?> +
    + escape($item->alias)); ?> +
    + + typeFields['catid'])) : ?> +
    + escape($item->category_title); ?> +
    + +
    +
    + + + extensionName, $this->typeName, (int) $item->id, $item->language, !$isCheckout, false); ?> + + extensionName, $this->typeName, (int) $item->id, $item->language, !$isCheckout, true); ?> + + escape($item->menutype_title); ?> + + escape($item->access_level); ?> + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - -
    -
    -
    + + + +
    +
    +
    diff --git a/administrator/components/com_associations/tmpl/associations/modal.php b/administrator/components/com_associations/tmpl/associations/modal.php index b46c16594488a..398e271296b1c 100644 --- a/administrator/components/com_associations/tmpl/associations/modal.php +++ b/administrator/components/com_associations/tmpl/associations/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if ($app->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('multiselect') - ->useScript('com_associations.admin-associations-modal'); + ->useScript('com_associations.admin-associations-modal'); $function = $app->input->getCmd('function', 'jSelectAssociation'); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -35,136 +35,136 @@ $canManageCheckin = Factory::getUser()->authorise('core.manage', 'com_checkin'); $iconStates = array( - -2 => 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', + -2 => 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', ); $this->document->addScriptOptions('associations-modal', ['func' => $function]); ?>
    -
    - $this)); ?> - items)) : ?> -
    - - -
    - - - - - - typeSupports['state'])) : ?> - - - - - - typeFields['menutype'])) : ?> - - - typeSupports['acl'])) : ?> - - - - - - - items as $i => $item) : - $canEdit = AssociationsHelper::allowEdit($this->extensionName, $this->typeName, $item->id); - $canCheckin = $canManageCheckin || AssociationsHelper::canCheckinItem($this->extensionName, $this->typeName, $item->id); - $isCheckout = AssociationsHelper::isCheckoutItem($this->extensionName, $this->typeName, $item->id); - ?> - - typeSupports['state'])) : ?> - - - - - - typeFields['menutype'])) : ?> - - - typeSupports['acl'])) : ?> - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - -
    - - - - - level)) : ?> - $item->level)); ?> - - - - escape($item->title); ?> - - editor, $item->checked_out_time, 'associations.'); ?> - - escape($item->title); ?> - - - escape($item->title); ?> - - typeFields['alias'])) : ?> - - escape($item->alias)); ?> - - - typeFields['catid'])) : ?> -
    - escape($item->category_title); ?> -
    - -
    - - - association) : ?> - extensionName, $this->typeName, (int) $item->id, $item->language, false, false); ?> - - - escape($item->menutype_title); ?> - - escape($item->access_level); ?> - - id; ?> -
    + + $this)); ?> + items)) : ?> +
    + + +
    + + + + + + typeSupports['state'])) : ?> + + + + + + typeFields['menutype'])) : ?> + + + typeSupports['acl'])) : ?> + + + + + + + items as $i => $item) : + $canEdit = AssociationsHelper::allowEdit($this->extensionName, $this->typeName, $item->id); + $canCheckin = $canManageCheckin || AssociationsHelper::canCheckinItem($this->extensionName, $this->typeName, $item->id); + $isCheckout = AssociationsHelper::isCheckoutItem($this->extensionName, $this->typeName, $item->id); + ?> + + typeSupports['state'])) : ?> + + + + + + typeFields['menutype'])) : ?> + + + typeSupports['acl'])) : ?> + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + +
    + + + + + level)) : ?> + $item->level)); ?> + + + + escape($item->title); ?> + + editor, $item->checked_out_time, 'associations.'); ?> + + escape($item->title); ?> + + + escape($item->title); ?> + + typeFields['alias'])) : ?> + + escape($item->alias)); ?> + + + typeFields['catid'])) : ?> +
    + escape($item->category_title); ?> +
    + +
    + + + association) : ?> + extensionName, $this->typeName, (int) $item->id, $item->language, false, false); ?> + + + escape($item->menutype_title); ?> + + escape($item->access_level); ?> + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - -
    + + + + +
    diff --git a/administrator/components/com_banners/helpers/banners.php b/administrator/components/com_banners/helpers/banners.php index 97747c9c6c728..50760dce1ad99 100644 --- a/administrator/components/com_banners/helpers/banners.php +++ b/administrator/components/com_banners/helpers/banners.php @@ -1,4 +1,5 @@ registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Banners')); - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Banners')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Banners')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Banners')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Banners')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Banners')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Banners')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Banners')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new BannersComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new BannersComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_banners/src/Controller/BannerController.php b/administrator/components/com_banners/src/Controller/BannerController.php index bd3f642f963c9..6357653f6cb40 100644 --- a/administrator/components/com_banners/src/Controller/BannerController.php +++ b/administrator/components/com_banners/src/Controller/BannerController.php @@ -1,4 +1,5 @@ input->getInt('filter_category_id'); - $categoryId = ArrayHelper::getValue($data, 'catid', $filter, 'int'); - - if ($categoryId) - { - // If the category has been passed in the URL check it. - return $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); - } - - // In the absence of better information, revert to the component permissions. - return parent::allowAdd($data); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $categoryId = 0; - - if ($recordId) - { - $categoryId = (int) $this->getModel()->getItem($recordId)->catid; - } - - if ($categoryId) - { - // The category has been set. Check the category permissions. - return $this->app->getIdentity()->authorise('core.edit', $this->option . '.category.' . $categoryId); - } - - // Since there is no asset tracking, revert to the component permissions. - return parent::allowEdit($data, $key); - } - - /** - * Method to run batch operations. - * - * @param string $model The model - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('Banner', '', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_banners&view=banners' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } + use VersionableControllerTrait; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_BANNERS_BANNER'; + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $filter = $this->input->getInt('filter_category_id'); + $categoryId = ArrayHelper::getValue($data, 'catid', $filter, 'int'); + + if ($categoryId) { + // If the category has been passed in the URL check it. + return $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); + } + + // In the absence of better information, revert to the component permissions. + return parent::allowAdd($data); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $categoryId = 0; + + if ($recordId) { + $categoryId = (int) $this->getModel()->getItem($recordId)->catid; + } + + if ($categoryId) { + // The category has been set. Check the category permissions. + return $this->app->getIdentity()->authorise('core.edit', $this->option . '.category.' . $categoryId); + } + + // Since there is no asset tracking, revert to the component permissions. + return parent::allowEdit($data, $key); + } + + /** + * Method to run batch operations. + * + * @param string $model The model + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('Banner', '', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_banners&view=banners' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } } diff --git a/administrator/components/com_banners/src/Controller/BannersController.php b/administrator/components/com_banners/src/Controller/BannersController.php index 8dcc0df9c05ef..2ad4b8ad84c61 100644 --- a/administrator/components/com_banners/src/Controller/BannersController.php +++ b/administrator/components/com_banners/src/Controller/BannersController.php @@ -1,4 +1,5 @@ registerTask('sticky_unpublish', 'sticky_publish'); - } + $this->registerTask('sticky_unpublish', 'sticky_publish'); + } - /** - * Method to get a model object, loading it if required. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 1.6 - */ - public function getModel($name = 'Banner', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Banner', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } - /** - * Stick items - * - * @return void - * - * @since 1.6 - */ - public function sticky_publish() - { - // Check for request forgeries. - $this->checkToken(); + /** + * Stick items + * + * @return void + * + * @since 1.6 + */ + public function sticky_publish() + { + // Check for request forgeries. + $this->checkToken(); - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('sticky_publish' => 1, 'sticky_unpublish' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('sticky_publish' => 1, 'sticky_unpublish' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); - // Remove zero values resulting from input filter - $ids = array_filter($ids); + // Remove zero values resulting from input filter + $ids = array_filter($ids); - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('COM_BANNERS_NO_BANNERS_SELECTED'), 'warning'); - } - else - { - // Get the model. - /** @var \Joomla\Component\Banners\Administrator\Model\BannerModel $model */ - $model = $this->getModel(); + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('COM_BANNERS_NO_BANNERS_SELECTED'), 'warning'); + } else { + // Get the model. + /** @var \Joomla\Component\Banners\Administrator\Model\BannerModel $model */ + $model = $this->getModel(); - // Change the state of the records. - if (!$model->stick($ids, $value)) - { - $this->app->enqueueMessage($model->getError(), 'warning'); - } - else - { - if ($value == 1) - { - $ntext = 'COM_BANNERS_N_BANNERS_STUCK'; - } - else - { - $ntext = 'COM_BANNERS_N_BANNERS_UNSTUCK'; - } + // Change the state of the records. + if (!$model->stick($ids, $value)) { + $this->app->enqueueMessage($model->getError(), 'warning'); + } else { + if ($value == 1) { + $ntext = 'COM_BANNERS_N_BANNERS_STUCK'; + } else { + $ntext = 'COM_BANNERS_N_BANNERS_UNSTUCK'; + } - $this->setMessage(Text::plural($ntext, \count($ids))); - } - } + $this->setMessage(Text::plural($ntext, \count($ids))); + } + } - $this->setRedirect('index.php?option=com_banners&view=banners'); - } + $this->setRedirect('index.php?option=com_banners&view=banners'); + } } diff --git a/administrator/components/com_banners/src/Controller/ClientController.php b/administrator/components/com_banners/src/Controller/ClientController.php index 9b28ec21f5827..df2a0c59739d1 100644 --- a/administrator/components/com_banners/src/Controller/ClientController.php +++ b/administrator/components/com_banners/src/Controller/ClientController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Client', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_banners/src/Controller/DisplayController.php b/administrator/components/com_banners/src/Controller/DisplayController.php index 41468a8b71fe4..0ded6eede6b56 100644 --- a/administrator/components/com_banners/src/Controller/DisplayController.php +++ b/administrator/components/com_banners/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'banners'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + $view = $this->input->get('view', 'banners'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($view == 'banner' && $layout == 'edit' && !$this->checkEditId('com_banners.edit.banner', $id)) - { - // 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', $id), 'error'); - } + // Check for edit form. + if ($view == 'banner' && $layout == 'edit' && !$this->checkEditId('com_banners.edit.banner', $id)) { + // 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', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_banners&view=banners', false)); + $this->setRedirect(Route::_('index.php?option=com_banners&view=banners', false)); - return false; - } - elseif ($view == 'client' && $layout == 'edit' && !$this->checkEditId('com_banners.edit.client', $id)) - { - // 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', $id), 'error'); - } + return false; + } elseif ($view == 'client' && $layout == 'edit' && !$this->checkEditId('com_banners.edit.client', $id)) { + // 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', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_banners&view=clients', false)); + $this->setRedirect(Route::_('index.php?option=com_banners&view=clients', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/administrator/components/com_banners/src/Controller/TracksController.php b/administrator/components/com_banners/src/Controller/TracksController.php index fbca3db5c12b0..448aa0b296569 100644 --- a/administrator/components/com_banners/src/Controller/TracksController.php +++ b/administrator/components/com_banners/src/Controller/TracksController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to remove a record. - * - * @return void - * - * @since 1.6 - */ - public function delete() - { - // Check for request forgeries. - $this->checkToken(); - - // Get the model. - /** @var \Joomla\Component\Banners\Administrator\Model\TracksModel $model */ - $model = $this->getModel(); - - // Load the filter state. - $model->setState('filter.type', $this->app->getUserState($this->context . '.filter.type')); - $model->setState('filter.begin', $this->app->getUserState($this->context . '.filter.begin')); - $model->setState('filter.end', $this->app->getUserState($this->context . '.filter.end')); - $model->setState('filter.category_id', $this->app->getUserState($this->context . '.filter.category_id')); - $model->setState('filter.client_id', $this->app->getUserState($this->context . '.filter.client_id')); - $model->setState('list.limit', 0); - $model->setState('list.start', 0); - - $count = $model->getTotal(); - - // Remove the items. - if (!$model->delete()) - { - $this->app->enqueueMessage($model->getError(), 'warning'); - } - elseif ($count > 0) - { - $this->setMessage(Text::plural('COM_BANNERS_TRACKS_N_ITEMS_DELETED', $count)); - } - else - { - $this->setMessage(Text::_('COM_BANNERS_TRACKS_NO_ITEMS_DELETED')); - } - - $this->setRedirect('index.php?option=com_banners&view=tracks'); - } - - /** - * Display method for the raw track data. - * - * @param boolean $cachable If true, the view output will be cached - * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. - * - * @return static This object to support chaining. - * - * @since 1.5 - * @todo This should be done as a view, not here! - */ - public function display($cachable = false, $urlparams = array()) - { - // Get the document object. - $vName = 'tracks'; - - // Get and render the view. - if ($view = $this->getView($vName, 'raw')) - { - // Check for request forgeries. - $this->checkToken('GET'); - - // Get the model for the view. - /** @var \Joomla\Component\Banners\Administrator\Model\TracksModel $model */ - $model = $this->getModel($vName); - - // Load the filter state. - $app = $this->app; - - $model->setState('filter.type', $app->getUserState($this->context . '.filter.type')); - $model->setState('filter.begin', $app->getUserState($this->context . '.filter.begin')); - $model->setState('filter.end', $app->getUserState($this->context . '.filter.end')); - $model->setState('filter.category_id', $app->getUserState($this->context . '.filter.category_id')); - $model->setState('filter.client_id', $app->getUserState($this->context . '.filter.client_id')); - $model->setState('list.limit', 0); - $model->setState('list.start', 0); - - $form = $this->input->get('jform', array(), 'array'); - - $model->setState('basename', $form['basename']); - $model->setState('compressed', $form['compressed']); - - // Create one year cookies. - $cookieLifeTime = time() + 365 * 86400; - $cookieDomain = $app->get('cookie_domain', ''); - $cookiePath = $app->get('cookie_path', '/'); - $isHttpsForced = $app->isHttpsForced(); - - $this->input->cookie->set( - ApplicationHelper::getHash($this->context . '.basename'), - $form['basename'], - $cookieLifeTime, - $cookiePath, - $cookieDomain, - $isHttpsForced, - true - ); - - $this->input->cookie->set( - ApplicationHelper::getHash($this->context . '.compressed'), - $form['compressed'], - $cookieLifeTime, - $cookiePath, - $cookieDomain, - $isHttpsForced, - true - ); - - // Push the model into the view (as default). - $view->setModel($model, true); - - // Push document object into the view. - $view->document = $this->app->getDocument(); - - $view->display(); - } - - return $this; - } + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $context = 'com_banners.tracks'; + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Tracks', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to remove a record. + * + * @return void + * + * @since 1.6 + */ + public function delete() + { + // Check for request forgeries. + $this->checkToken(); + + // Get the model. + /** @var \Joomla\Component\Banners\Administrator\Model\TracksModel $model */ + $model = $this->getModel(); + + // Load the filter state. + $model->setState('filter.type', $this->app->getUserState($this->context . '.filter.type')); + $model->setState('filter.begin', $this->app->getUserState($this->context . '.filter.begin')); + $model->setState('filter.end', $this->app->getUserState($this->context . '.filter.end')); + $model->setState('filter.category_id', $this->app->getUserState($this->context . '.filter.category_id')); + $model->setState('filter.client_id', $this->app->getUserState($this->context . '.filter.client_id')); + $model->setState('list.limit', 0); + $model->setState('list.start', 0); + + $count = $model->getTotal(); + + // Remove the items. + if (!$model->delete()) { + $this->app->enqueueMessage($model->getError(), 'warning'); + } elseif ($count > 0) { + $this->setMessage(Text::plural('COM_BANNERS_TRACKS_N_ITEMS_DELETED', $count)); + } else { + $this->setMessage(Text::_('COM_BANNERS_TRACKS_NO_ITEMS_DELETED')); + } + + $this->setRedirect('index.php?option=com_banners&view=tracks'); + } + + /** + * Display method for the raw track data. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 1.5 + * @todo This should be done as a view, not here! + */ + public function display($cachable = false, $urlparams = array()) + { + // Get the document object. + $vName = 'tracks'; + + // Get and render the view. + if ($view = $this->getView($vName, 'raw')) { + // Check for request forgeries. + $this->checkToken('GET'); + + // Get the model for the view. + /** @var \Joomla\Component\Banners\Administrator\Model\TracksModel $model */ + $model = $this->getModel($vName); + + // Load the filter state. + $app = $this->app; + + $model->setState('filter.type', $app->getUserState($this->context . '.filter.type')); + $model->setState('filter.begin', $app->getUserState($this->context . '.filter.begin')); + $model->setState('filter.end', $app->getUserState($this->context . '.filter.end')); + $model->setState('filter.category_id', $app->getUserState($this->context . '.filter.category_id')); + $model->setState('filter.client_id', $app->getUserState($this->context . '.filter.client_id')); + $model->setState('list.limit', 0); + $model->setState('list.start', 0); + + $form = $this->input->get('jform', array(), 'array'); + + $model->setState('basename', $form['basename']); + $model->setState('compressed', $form['compressed']); + + // Create one year cookies. + $cookieLifeTime = time() + 365 * 86400; + $cookieDomain = $app->get('cookie_domain', ''); + $cookiePath = $app->get('cookie_path', '/'); + $isHttpsForced = $app->isHttpsForced(); + + $this->input->cookie->set( + ApplicationHelper::getHash($this->context . '.basename'), + $form['basename'], + $cookieLifeTime, + $cookiePath, + $cookieDomain, + $isHttpsForced, + true + ); + + $this->input->cookie->set( + ApplicationHelper::getHash($this->context . '.compressed'), + $form['compressed'], + $cookieLifeTime, + $cookiePath, + $cookieDomain, + $isHttpsForced, + true + ); + + // Push the model into the view (as default). + $view->setModel($model, true); + + // Push document object into the view. + $view->document = $this->app->getDocument(); + + $view->display(); + } + + return $this; + } } diff --git a/administrator/components/com_banners/src/Extension/BannersComponent.php b/administrator/components/com_banners/src/Extension/BannersComponent.php index 2a52518e079e1..4b2803711c589 100644 --- a/administrator/components/com_banners/src/Extension/BannersComponent.php +++ b/administrator/components/com_banners/src/Extension/BannersComponent.php @@ -1,4 +1,5 @@ setDatabase($container->get(DatabaseInterface::class)); + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $banner = new Banner(); + $banner->setDatabase($container->get(DatabaseInterface::class)); - $this->getRegistry()->register('banner', $banner); - } + $this->getRegistry()->register('banner', $banner); + } - /** - * Returns the table for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getTableNameForSection(string $section = null) - { - return 'banners'; - } + /** + * Returns the table for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getTableNameForSection(string $section = null) + { + return 'banners'; + } } diff --git a/administrator/components/com_banners/src/Field/BannerclientField.php b/administrator/components/com_banners/src/Field/BannerclientField.php index 5c0f33863d2a9..5d1488bd729e5 100644 --- a/administrator/components/com_banners/src/Field/BannerclientField.php +++ b/administrator/components/com_banners/src/Field/BannerclientField.php @@ -1,4 +1,5 @@ id . '\').value=\'0\';"'; + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $onclick = ' onclick="document.getElementById(\'' . $this->id . '\').value=\'0\';"'; - return '
    ' - . '
    '; - } + return '
    ' + . '
    '; + } } diff --git a/administrator/components/com_banners/src/Field/ImpmadeField.php b/administrator/components/com_banners/src/Field/ImpmadeField.php index f385b8531e88a..6c4eeef8d7580 100644 --- a/administrator/components/com_banners/src/Field/ImpmadeField.php +++ b/administrator/components/com_banners/src/Field/ImpmadeField.php @@ -1,4 +1,5 @@ id . '\').value=\'0\';"'; + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $onclick = ' onclick="document.getElementById(\'' . $this->id . '\').value=\'0\';"'; - return '
    ' - . '
    '; - } + return '
    ' + . '
    '; + } } diff --git a/administrator/components/com_banners/src/Field/ImptotalField.php b/administrator/components/com_banners/src/Field/ImptotalField.php index 54550bbaa5749..31f1d4d17f8d6 100644 --- a/administrator/components/com_banners/src/Field/ImptotalField.php +++ b/administrator/components/com_banners/src/Field/ImptotalField.php @@ -1,4 +1,5 @@ id . '_unlimited\').checked=document.getElementById(\'' . $this->id - . '\').value==\'\';"'; - $onclick = ' onclick="if (document.getElementById(\'' . $this->id . '_unlimited\').checked) document.getElementById(\'' . $this->id - . '\').value=\'\';"'; - $value = empty($this->value) ? '' : $this->value; - $checked = empty($this->value) ? ' checked="checked"' : ''; + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $class = ' class="form-control validate-numeric text_area"'; + $onchange = ' onchange="document.getElementById(\'' . $this->id . '_unlimited\').checked=document.getElementById(\'' . $this->id + . '\').value==\'\';"'; + $onclick = ' onclick="if (document.getElementById(\'' . $this->id . '_unlimited\').checked) document.getElementById(\'' . $this->id + . '\').value=\'\';"'; + $value = empty($this->value) ? '' : $this->value; + $checked = empty($this->value) ? ' checked="checked"' : ''; - return '' - . '
    ' - . '
    '; - } + return '' + . '
    ' + . '
    '; + } } diff --git a/administrator/components/com_banners/src/Helper/BannersHelper.php b/administrator/components/com_banners/src/Helper/BannersHelper.php index 824318b5beadd..e061b23946c0a 100644 --- a/administrator/components/com_banners/src/Helper/BannersHelper.php +++ b/administrator/components/com_banners/src/Helper/BannersHelper.php @@ -1,4 +1,5 @@ getIdentity(); - - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__banners')) - ->where( - [ - $db->quoteName('reset') . ' <= :date', - $db->quoteName('reset') . ' IS NOT NULL', - ] - ) - ->bind(':date', $date) - ->extendWhere( - 'AND', - [ - $db->quoteName('checked_out') . ' IS NULL', - $db->quoteName('checked_out') . ' = :userId', - ], - 'OR' - ) - ->bind(':userId', $user->id, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - foreach ($rows as $row) - { - $purchaseType = $row->purchase_type; - - if ($purchaseType < 0 && $row->cid) - { - /** @var \Joomla\Component\Banners\Administrator\Table\ClientTable $client */ - $client = Table::getInstance('ClientTable', '\\Joomla\\Component\\Banners\\Administrator\\Table\\'); - $client->load($row->cid); - $purchaseType = $client->purchase_type; - } - - if ($purchaseType < 0) - { - $params = ComponentHelper::getParams('com_banners'); - $purchaseType = $params->get('purchase_type'); - } - - switch ($purchaseType) - { - case 1: - $reset = null; - break; - case 2: - $date = Factory::getDate('+1 year ' . date('Y-m-d')); - $reset = $date->toSql(); - break; - case 3: - $date = Factory::getDate('+1 month ' . date('Y-m-d')); - $reset = $date->toSql(); - break; - case 4: - $date = Factory::getDate('+7 day ' . date('Y-m-d')); - $reset = $date->toSql(); - break; - case 5: - $date = Factory::getDate('+1 day ' . date('Y-m-d')); - $reset = $date->toSql(); - break; - } - - // Update the row ordering field. - $query = $db->getQuery(true) - ->update($db->quoteName('#__banners')) - ->set( - [ - $db->quoteName('reset') . ' = :reset', - $db->quoteName('impmade') . ' = 0', - $db->quoteName('clicks') . ' = 0', - ] - ) - ->where($db->quoteName('id') . ' = :id') - ->bind(':reset', $reset, $reset === null ? ParameterType::NULL : ParameterType::STRING) - ->bind(':id', $row->id, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - } - - return true; - } - - /** - * Get client list in text/value format for a select field - * - * @return array - */ - public static function getClientOptions() - { - $options = array(); - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('id', 'value'), - $db->quoteName('name', 'text'), - ] - ) - ->from($db->quoteName('#__banner_clients', 'a')) - ->where($db->quoteName('a.state') . ' = 1') - ->order($db->quoteName('a.name')); - - // Get the options. - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - array_unshift($options, HTMLHelper::_('select.option', '0', Text::_('COM_BANNERS_NO_CLIENT'))); - - return $options; - } + /** + * Update / reset the banners + * + * @return boolean + * + * @since 1.6 + */ + public static function updateReset() + { + $db = Factory::getDbo(); + $date = Factory::getDate(); + $app = Factory::getApplication(); + $user = $app->getIdentity(); + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__banners')) + ->where( + [ + $db->quoteName('reset') . ' <= :date', + $db->quoteName('reset') . ' IS NOT NULL', + ] + ) + ->bind(':date', $date) + ->extendWhere( + 'AND', + [ + $db->quoteName('checked_out') . ' IS NULL', + $db->quoteName('checked_out') . ' = :userId', + ], + 'OR' + ) + ->bind(':userId', $user->id, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + foreach ($rows as $row) { + $purchaseType = $row->purchase_type; + + if ($purchaseType < 0 && $row->cid) { + /** @var \Joomla\Component\Banners\Administrator\Table\ClientTable $client */ + $client = Table::getInstance('ClientTable', '\\Joomla\\Component\\Banners\\Administrator\\Table\\'); + $client->load($row->cid); + $purchaseType = $client->purchase_type; + } + + if ($purchaseType < 0) { + $params = ComponentHelper::getParams('com_banners'); + $purchaseType = $params->get('purchase_type'); + } + + switch ($purchaseType) { + case 1: + $reset = null; + break; + case 2: + $date = Factory::getDate('+1 year ' . date('Y-m-d')); + $reset = $date->toSql(); + break; + case 3: + $date = Factory::getDate('+1 month ' . date('Y-m-d')); + $reset = $date->toSql(); + break; + case 4: + $date = Factory::getDate('+7 day ' . date('Y-m-d')); + $reset = $date->toSql(); + break; + case 5: + $date = Factory::getDate('+1 day ' . date('Y-m-d')); + $reset = $date->toSql(); + break; + } + + // Update the row ordering field. + $query = $db->getQuery(true) + ->update($db->quoteName('#__banners')) + ->set( + [ + $db->quoteName('reset') . ' = :reset', + $db->quoteName('impmade') . ' = 0', + $db->quoteName('clicks') . ' = 0', + ] + ) + ->where($db->quoteName('id') . ' = :id') + ->bind(':reset', $reset, $reset === null ? ParameterType::NULL : ParameterType::STRING) + ->bind(':id', $row->id, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + } + + return true; + } + + /** + * Get client list in text/value format for a select field + * + * @return array + */ + public static function getClientOptions() + { + $options = array(); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('id', 'value'), + $db->quoteName('name', 'text'), + ] + ) + ->from($db->quoteName('#__banner_clients', 'a')) + ->where($db->quoteName('a.state') . ' = 1') + ->order($db->quoteName('a.name')); + + // Get the options. + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + array_unshift($options, HTMLHelper::_('select.option', '0', Text::_('COM_BANNERS_NO_CLIENT'))); + + return $options; + } } diff --git a/administrator/components/com_banners/src/Model/BannerModel.php b/administrator/components/com_banners/src/Model/BannerModel.php index 92e34088576cf..7c0827cf1b7d4 100644 --- a/administrator/components/com_banners/src/Model/BannerModel.php +++ b/administrator/components/com_banners/src/Model/BannerModel.php @@ -1,4 +1,5 @@ 'batchClient', - 'language_id' => 'batchLanguage' - ); - - /** - * Batch client changes for a group of banners. - * - * @param string $value The new value matching a client. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - protected function batchClient($value, $pks, $contexts) - { - // Set the variables - $user = Factory::getUser(); - - /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $table */ - $table = $this->getTable(); - - foreach ($pks as $pk) - { - if (!$user->authorise('core.edit', $contexts[$pk])) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); - - return false; - } - - $table->reset(); - $table->load($pk); - $table->cid = (int) $value; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->state != -2) - { - return false; - } - - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.delete', 'com_banners.category.' . (int) $record->catid); - } - - return parent::canDelete($record); - } - - /** - * A method to preprocess generating a new title in order to allow tables with alternative names - * for alias and title to use the batch move and copy methods - * - * @param integer $categoryId The target category id - * @param Table $table The JTable within which move or copy is taking place - * - * @return void - * - * @since 3.8.12 - */ - public function generateTitle($categoryId, $table) - { - // Alter the title & alias - $data = $this->generateNewTitle($categoryId, $table->alias, $table->name); - $table->name = $data['0']; - $table->alias = $data['1']; - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - // Check against the category. - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.edit.state', 'com_banners.category.' . (int) $record->catid); - } - - // Default to component settings if category not known. - return parent::canEditState($record); - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. [optional] - * @param boolean $loadData True if the form is to load its own data (default case), false if not. [optional] - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_banners.banner', 'banner', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('publish_up', 'disabled', 'true'); - $form->setFieldAttribute('publish_down', 'disabled', 'true'); - $form->setFieldAttribute('state', 'disabled', 'true'); - $form->setFieldAttribute('sticky', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('publish_up', 'filter', 'unset'); - $form->setFieldAttribute('publish_down', 'filter', 'unset'); - $form->setFieldAttribute('state', 'filter', 'unset'); - $form->setFieldAttribute('sticky', 'filter', 'unset'); - } - - // Don't allow to change the created_by user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_by', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_banners.edit.banner.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Prime some default values. - if ($this->getState('banner.id') == 0) - { - $filters = (array) $app->getUserState('com_banners.banners.filter'); - $filterCatId = $filters['category_id'] ?? null; - - $data->set('catid', $app->input->getInt('catid', $filterCatId)); - } - } - - $this->preprocessData('com_banners.banner', $data); - - return $data; - } - - /** - * Method to stick records. - * - * @param array $pks The ids of the items to publish. - * @param integer $value The value of the published state - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function stick(&$pks, $value = 1) - { - /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $table */ - $table = $this->getTable(); - $pks = (array) $pks; - - // Access checks. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if (!$this->canEditState($table)) - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - } - } - } - - // Attempt to change the state of the records. - if (!$table->stick($pks, $value, Factory::getUser()->id)) - { - $this->setError($table->getError()); - - return false; - } - - return true; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param Table $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - $db = $this->getDatabase(); - - return [ - $db->quoteName('catid') . ' = ' . (int) $table->catid, - $db->quoteName('state') . ' >= 0', - ]; - } - - /** - * Prepare and sanitise the table prior to saving. - * - * @param Table $table A Table object. - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - $date = Factory::getDate(); - $user = Factory::getUser(); - - if (empty($table->id)) - { - // Set the values - $table->created = $date->toSql(); - $table->created_by = $user->id; - - // Set ordering to the last item if not set - if (empty($table->ordering)) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('MAX(' . $db->quoteName('ordering') . ')') - ->from($db->quoteName('#__banners')); - - $db->setQuery($query); - $max = $db->loadResult(); - - $table->ordering = $max + 1; - } - } - else - { - // Set the values - $table->modified = $date->toSql(); - $table->modified_by = $user->id; - } - - // Increment the content version number. - $table->version++; - } - - /** - * Allows preprocessing of the Form object. - * - * @param Form $form The form object - * @param array $data The data to be merged into the form object - * @param string $group The plugin group to be executed - * - * @return void - * - * @since 3.6.1 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - if ($this->canCreateCategory()) - { - $form->setFieldAttribute('catid', 'allowAdd', 'true'); - - // Add a prefix for categories created on the fly. - $form->setFieldAttribute('catid', 'customPrefix', '#new#'); - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $input = Factory::getApplication()->input; - - // Create new category, if needed. - $createCategory = true; - - // If category ID is provided, check if it's valid. - if (is_numeric($data['catid']) && $data['catid']) - { - $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_banners'); - } - - // Save New Category - if ($createCategory && $this->canCreateCategory()) - { - $category = [ - // Remove #new# prefix, if exists. - 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], - 'parent_id' => 1, - 'extension' => 'com_banners', - 'language' => $data['language'], - 'published' => 1, - ]; - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ - $categoryModel = Factory::getApplication()->bootComponent('com_categories') - ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); - - // Create new category. - if (!$categoryModel->save($category)) - { - $this->setError($categoryModel->getError()); - - return false; - } - - // Get the new category ID. - $data['catid'] = $categoryModel->getState('category.id'); - } - - // Alter the name for save as copy - if ($input->get('task') == 'save2copy') - { - /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $origTable */ - $origTable = clone $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['name'] == $origTable->name) - { - list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); - $data['name'] = $name; - $data['alias'] = $alias; - } - else - { - if ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - } - - $data['state'] = 0; - } - - return parent::save($data); - } - - /** - * Is the user allowed to create an on the fly category? - * - * @return boolean - * - * @since 3.6.1 - */ - private function canCreateCategory() - { - return Factory::getUser()->authorise('core.create', 'com_banners'); - } + use VersionableModelTrait; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_BANNERS_BANNER'; + + /** + * The type alias for this content type. + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_banners.banner'; + + /** + * Batch copy/move command. If set to false, the batch copy/move command is not supported + * + * @var string + */ + protected $batch_copymove = 'category_id'; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'client_id' => 'batchClient', + 'language_id' => 'batchLanguage' + ); + + /** + * Batch client changes for a group of banners. + * + * @param string $value The new value matching a client. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + protected function batchClient($value, $pks, $contexts) + { + // Set the variables + $user = Factory::getUser(); + + /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $table */ + $table = $this->getTable(); + + foreach ($pks as $pk) { + if (!$user->authorise('core.edit', $contexts[$pk])) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); + + return false; + } + + $table->reset(); + $table->load($pk); + $table->cid = (int) $value; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->state != -2) { + return false; + } + + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.delete', 'com_banners.category.' . (int) $record->catid); + } + + return parent::canDelete($record); + } + + /** + * A method to preprocess generating a new title in order to allow tables with alternative names + * for alias and title to use the batch move and copy methods + * + * @param integer $categoryId The target category id + * @param Table $table The JTable within which move or copy is taking place + * + * @return void + * + * @since 3.8.12 + */ + public function generateTitle($categoryId, $table) + { + // Alter the title & alias + $data = $this->generateNewTitle($categoryId, $table->alias, $table->name); + $table->name = $data['0']; + $table->alias = $data['1']; + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + // Check against the category. + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.edit.state', 'com_banners.category.' . (int) $record->catid); + } + + // Default to component settings if category not known. + return parent::canEditState($record); + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. [optional] + * @param boolean $loadData True if the form is to load its own data (default case), false if not. [optional] + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_banners.banner', 'banner', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('publish_up', 'disabled', 'true'); + $form->setFieldAttribute('publish_down', 'disabled', 'true'); + $form->setFieldAttribute('state', 'disabled', 'true'); + $form->setFieldAttribute('sticky', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('publish_up', 'filter', 'unset'); + $form->setFieldAttribute('publish_down', 'filter', 'unset'); + $form->setFieldAttribute('state', 'filter', 'unset'); + $form->setFieldAttribute('sticky', 'filter', 'unset'); + } + + // Don't allow to change the created_by user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_by', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_banners.edit.banner.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Prime some default values. + if ($this->getState('banner.id') == 0) { + $filters = (array) $app->getUserState('com_banners.banners.filter'); + $filterCatId = $filters['category_id'] ?? null; + + $data->set('catid', $app->input->getInt('catid', $filterCatId)); + } + } + + $this->preprocessData('com_banners.banner', $data); + + return $data; + } + + /** + * Method to stick records. + * + * @param array $pks The ids of the items to publish. + * @param integer $value The value of the published state + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function stick(&$pks, $value = 1) + { + /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $table */ + $table = $this->getTable(); + $pks = (array) $pks; + + // Access checks. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if (!$this->canEditState($table)) { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + } + } + } + + // Attempt to change the state of the records. + if (!$table->stick($pks, $value, Factory::getUser()->id)) { + $this->setError($table->getError()); + + return false; + } + + return true; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param Table $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('catid') . ' = ' . (int) $table->catid, + $db->quoteName('state') . ' >= 0', + ]; + } + + /** + * Prepare and sanitise the table prior to saving. + * + * @param Table $table A Table object. + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + $date = Factory::getDate(); + $user = Factory::getUser(); + + if (empty($table->id)) { + // Set the values + $table->created = $date->toSql(); + $table->created_by = $user->id; + + // Set ordering to the last item if not set + if (empty($table->ordering)) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('MAX(' . $db->quoteName('ordering') . ')') + ->from($db->quoteName('#__banners')); + + $db->setQuery($query); + $max = $db->loadResult(); + + $table->ordering = $max + 1; + } + } else { + // Set the values + $table->modified = $date->toSql(); + $table->modified_by = $user->id; + } + + // Increment the content version number. + $table->version++; + } + + /** + * Allows preprocessing of the Form object. + * + * @param Form $form The form object + * @param array $data The data to be merged into the form object + * @param string $group The plugin group to be executed + * + * @return void + * + * @since 3.6.1 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + if ($this->canCreateCategory()) { + $form->setFieldAttribute('catid', 'allowAdd', 'true'); + + // Add a prefix for categories created on the fly. + $form->setFieldAttribute('catid', 'customPrefix', '#new#'); + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $input = Factory::getApplication()->input; + + // Create new category, if needed. + $createCategory = true; + + // If category ID is provided, check if it's valid. + if (is_numeric($data['catid']) && $data['catid']) { + $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_banners'); + } + + // Save New Category + if ($createCategory && $this->canCreateCategory()) { + $category = [ + // Remove #new# prefix, if exists. + 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], + 'parent_id' => 1, + 'extension' => 'com_banners', + 'language' => $data['language'], + 'published' => 1, + ]; + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ + $categoryModel = Factory::getApplication()->bootComponent('com_categories') + ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); + + // Create new category. + if (!$categoryModel->save($category)) { + $this->setError($categoryModel->getError()); + + return false; + } + + // Get the new category ID. + $data['catid'] = $categoryModel->getState('category.id'); + } + + // Alter the name for save as copy + if ($input->get('task') == 'save2copy') { + /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $origTable */ + $origTable = clone $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['name'] == $origTable->name) { + list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); + $data['name'] = $name; + $data['alias'] = $alias; + } else { + if ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + } + + $data['state'] = 0; + } + + return parent::save($data); + } + + /** + * Is the user allowed to create an on the fly category? + * + * @return boolean + * + * @since 3.6.1 + */ + private function canCreateCategory() + { + return Factory::getUser()->authorise('core.create', 'com_banners'); + } } diff --git a/administrator/components/com_banners/src/Model/BannersModel.php b/administrator/components/com_banners/src/Model/BannersModel.php index 03a7307861c01..2e6859a8f154c 100644 --- a/administrator/components/com_banners/src/Model/BannersModel.php +++ b/administrator/components/com_banners/src/Model/BannersModel.php @@ -1,4 +1,5 @@ cache['categoryorders'])) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - 'MAX(' . $db->quoteName('ordering') . ') AS ' . $db->quoteName('max'), - $db->quoteName('catid'), - ] - ) - ->from($db->quoteName('#__banners')) - ->group($db->quoteName('catid')); - $db->setQuery($query); - $this->cache['categoryorders'] = $db->loadAssocList('catid', 0); - } + /** + * Method to get the maximum ordering value for each category. + * + * @return array + * + * @since 1.6 + */ + public function &getCategoryOrders() + { + if (!isset($this->cache['categoryorders'])) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + 'MAX(' . $db->quoteName('ordering') . ') AS ' . $db->quoteName('max'), + $db->quoteName('catid'), + ] + ) + ->from($db->quoteName('#__banners')) + ->group($db->quoteName('catid')); + $db->setQuery($query); + $this->cache['categoryorders'] = $db->loadAssocList('catid', 0); + } - return $this->cache['categoryorders']; - } + return $this->cache['categoryorders']; + } - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.name'), - $db->quoteName('a.alias'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.catid'), - $db->quoteName('a.clicks'), - $db->quoteName('a.metakey'), - $db->quoteName('a.sticky'), - $db->quoteName('a.impmade'), - $db->quoteName('a.imptotal'), - $db->quoteName('a.state'), - $db->quoteName('a.ordering'), - $db->quoteName('a.purchase_type'), - $db->quoteName('a.language'), - $db->quoteName('a.publish_up'), - $db->quoteName('a.publish_down'), - ] - ) - ) - ->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - $db->quoteName('uc.name', 'editor'), - $db->quoteName('c.title', 'category_title'), - $db->quoteName('cl.name', 'client_name'), - $db->quoteName('cl.purchase_type', 'client_purchase_type'), - ] - ) - ->from($db->quoteName('#__banners', 'a')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) - ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')); + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.name'), + $db->quoteName('a.alias'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.catid'), + $db->quoteName('a.clicks'), + $db->quoteName('a.metakey'), + $db->quoteName('a.sticky'), + $db->quoteName('a.impmade'), + $db->quoteName('a.imptotal'), + $db->quoteName('a.state'), + $db->quoteName('a.ordering'), + $db->quoteName('a.purchase_type'), + $db->quoteName('a.language'), + $db->quoteName('a.publish_up'), + $db->quoteName('a.publish_down'), + ] + ) + ) + ->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + $db->quoteName('uc.name', 'editor'), + $db->quoteName('c.title', 'category_title'), + $db->quoteName('cl.name', 'client_name'), + $db->quoteName('cl.purchase_type', 'client_purchase_type'), + ] + ) + ->from($db->quoteName('#__banners', 'a')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) + ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')); - // Filter by published state - $published = (string) $this->getState('filter.published'); + // Filter by published state + $published = (string) $this->getState('filter.published'); - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.state') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where($db->quoteName('a.state') . ' IN (0, 1)'); - } + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.state') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where($db->quoteName('a.state') . ' IN (0, 1)'); + } - // Filter by category. - $categoryId = $this->getState('filter.category_id'); + // Filter by category. + $categoryId = $this->getState('filter.category_id'); - if (is_numeric($categoryId)) - { - $categoryId = (int) $categoryId; - $query->where($db->quoteName('a.catid') . ' = :categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } + if (is_numeric($categoryId)) { + $categoryId = (int) $categoryId; + $query->where($db->quoteName('a.catid') . ' = :categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } - // Filter by client. - $clientId = $this->getState('filter.client_id'); + // Filter by client. + $clientId = $this->getState('filter.client_id'); - if (is_numeric($clientId)) - { - $clientId = (int) $clientId; - $query->where($db->quoteName('a.cid') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } + if (is_numeric($clientId)) { + $clientId = (int) $clientId; + $query->where($db->quoteName('a.cid') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } - // Filter by search in title - if ($search = $this->getState('filter.search')) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName('a.name') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - } + // Filter by search in title + if ($search = $this->getState('filter.search')) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName('a.name') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + } - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } - // Filter on the level. - if ($level = (int) $this->getState('filter.level')) - { - $query->where($db->quoteName('c.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } + // Filter on the level. + if ($level = (int) $this->getState('filter.level')) { + $query->where($db->quoteName('c.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 'a.name'); - $orderDirn = $this->state->get('list.direction', 'ASC'); + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 'a.name'); + $orderDirn = $this->state->get('list.direction', 'ASC'); - if ($orderCol === 'a.ordering' || $orderCol === 'category_title') - { - $ordering = [ - $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), - $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), - ]; - } - else - { - if ($orderCol === 'client_name') - { - $orderCol = 'cl.name'; - } + if ($orderCol === 'a.ordering' || $orderCol === 'category_title') { + $ordering = [ + $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), + $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), + ]; + } else { + if ($orderCol === 'client_name') { + $orderCol = 'cl.name'; + } - $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); - } + $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); + } - $query->order($ordering); + $query->order($ordering); - return $query; - } + return $query; + } - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.category_id'); - $id .= ':' . $this->getState('filter.client_id'); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . $this->getState('filter.level'); + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.category_id'); + $id .= ':' . $this->getState('filter.client_id'); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . $this->getState('filter.level'); - return parent::getStoreId($id); - } + return parent::getStoreId($id); + } - /** - * Returns a reference to the a Table object, always creating it. - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 1.6 - */ - public function getTable($type = 'Banner', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } + /** + * Returns a reference to the a Table object, always creating it. + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 1.6 + */ + public function getTable($type = 'Banner', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.name', $direction = 'asc') - { - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_banners')); + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.name', $direction = 'asc') + { + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_banners')); - // List state information. - parent::populateState($ordering, $direction); - } + // List state information. + parent::populateState($ordering, $direction); + } } diff --git a/administrator/components/com_banners/src/Model/ClientModel.php b/administrator/components/com_banners/src/Model/ClientModel.php index bbe474d565744..93d0789da444f 100644 --- a/administrator/components/com_banners/src/Model/ClientModel.php +++ b/administrator/components/com_banners/src/Model/ClientModel.php @@ -1,4 +1,5 @@ id) || $record->state != -2) - { - return false; - } - - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.delete', 'com_banners.category.' . (int) $record->catid); - } - - return parent::canDelete($record); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. - * Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - - if (!empty($record->catid)) - { - return $user->authorise('core.edit.state', 'com_banners.category.' . (int) $record->catid); - } - - return $user->authorise('core.edit.state', 'com_banners'); - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_banners.client', 'client', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_banners.edit.client.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_banners.client', $data); - - return $data; - } - - /** - * Prepare and sanitise the table prior to saving. - * - * @param Table $table A Table object. - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); - } + use VersionableModelTrait; + + /** + * The type alias for this content type. + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_banners.client'; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->state != -2) { + return false; + } + + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.delete', 'com_banners.category.' . (int) $record->catid); + } + + return parent::canDelete($record); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. + * Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + + if (!empty($record->catid)) { + return $user->authorise('core.edit.state', 'com_banners.category.' . (int) $record->catid); + } + + return $user->authorise('core.edit.state', 'com_banners'); + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_banners.client', 'client', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_banners.edit.client.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_banners.client', $data); + + return $data; + } + + /** + * Prepare and sanitise the table prior to saving. + * + * @param Table $table A Table object. + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); + } } diff --git a/administrator/components/com_banners/src/Model/ClientsModel.php b/administrator/components/com_banners/src/Model/ClientsModel.php index fea2e7b870bf4..d47153b921fee 100644 --- a/administrator/components/com_banners/src/Model/ClientsModel.php +++ b/administrator/components/com_banners/src/Model/ClientsModel.php @@ -1,4 +1,5 @@ setState('params', ComponentHelper::getParams('com_banners')); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.purchase_type'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $defaultPurchase = (int) ComponentHelper::getParams('com_banners')->get('purchase_type', 3); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.name'), - $db->quoteName('a.contact'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.state'), - $db->quoteName('a.metakey'), - $db->quoteName('a.purchase_type'), - ] - ) - ) - ->select( - [ - 'COUNT(' . $db->quoteName('b.id') . ') AS ' . $db->quoteName('nbanners'), - $db->quoteName('uc.name', 'editor'), - ] - ); - - $query->from($db->quoteName('#__banner_clients', 'a')); - - // Join over the banners for counting - $query->join('LEFT', $db->quoteName('#__banners', 'b'), $db->quoteName('a.id') . ' = ' . $db->quoteName('b.cid')); - - // Join over the users for the checked out user. - $query->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Filter by published state - $published = (string) $this->getState('filter.state'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.state') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where($db->quoteName('a.state') . ' IN (0, 1)'); - } - - $query->group( - [ - $db->quoteName('a.id'), - $db->quoteName('a.name'), - $db->quoteName('a.contact'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.state'), - $db->quoteName('a.metakey'), - $db->quoteName('a.purchase_type'), - $db->quoteName('uc.name'), - ] - ); - - // Filter by search in title - if ($search = trim($this->getState('filter.search', ''))) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', $search) . '%'; - $query->where($db->quoteName('a.name') . ' LIKE :search') - ->bind(':search', $search); - } - } - - // Filter by purchase type - if ($purchaseType = (int) $this->getState('filter.purchase_type')) - { - if ($defaultPurchase === $purchaseType) - { - $query->where('(' . $db->quoteName('a.purchase_type') . ' = :type OR ' . $db->quoteName('a.purchase_type') . ' = -1)'); - } - else - { - $query->where($db->quoteName('a.purchase_type') . ' = :type'); - } - - $query->bind(':type', $purchaseType, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $query->order( - $db->quoteName($db->escape($this->getState('list.ordering', 'a.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) - ); - - return $query; - } - - /** - * Overrides the getItems method to attach additional metrics to the list. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.6 - */ - public function getItems() - { - // Get a storage key. - $store = $this->getStoreId('getItems'); - - // Try to load the data from internal storage. - if (!empty($this->cache[$store])) - { - return $this->cache[$store]; - } - - // Load the list items. - $items = parent::getItems(); - - // If empty or an error, just return. - if (empty($items)) - { - return array(); - } - - // Getting the following metric by joins is WAY TOO SLOW. - // Faster to do three queries for very large banner trees. - - // Get the clients in the list. - $db = $this->getDatabase(); - $clientIds = array_column($items, 'id'); - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('cid'), - 'COUNT(' . $db->quoteName('cid') . ') AS ' . $db->quoteName('count_published'), - ] - ) - ->from($db->quoteName('#__banners')) - ->where($db->quoteName('state') . ' = :state') - ->whereIn($db->quoteName('cid'), $clientIds) - ->group($db->quoteName('cid')) - ->bind(':state', $state, ParameterType::INTEGER); - - $db->setQuery($query); - - // Get the published banners count. - try - { - $state = 1; - $countPublished = $db->loadAssocList('cid', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get the unpublished banners count. - try - { - $state = 0; - $countUnpublished = $db->loadAssocList('cid', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get the trashed banners count. - try - { - $state = -2; - $countTrashed = $db->loadAssocList('cid', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get the archived banners count. - try - { - $state = 2; - $countArchived = $db->loadAssocList('cid', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Inject the values back into the array. - foreach ($items as $item) - { - $item->count_published = isset($countPublished[$item->id]) ? $countPublished[$item->id] : 0; - $item->count_unpublished = isset($countUnpublished[$item->id]) ? $countUnpublished[$item->id] : 0; - $item->count_trashed = isset($countTrashed[$item->id]) ? $countTrashed[$item->id] : 0; - $item->count_archived = isset($countArchived[$item->id]) ? $countArchived[$item->id] : 0; - } - - // Add the items to the internal cache. - $this->cache[$store] = $items; - - return $this->cache[$store]; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'contact', 'a.contact', + 'state', 'a.state', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'purchase_type', 'a.purchase_type' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.name', $direction = 'asc') + { + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_banners')); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.purchase_type'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $defaultPurchase = (int) ComponentHelper::getParams('com_banners')->get('purchase_type', 3); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.name'), + $db->quoteName('a.contact'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.state'), + $db->quoteName('a.metakey'), + $db->quoteName('a.purchase_type'), + ] + ) + ) + ->select( + [ + 'COUNT(' . $db->quoteName('b.id') . ') AS ' . $db->quoteName('nbanners'), + $db->quoteName('uc.name', 'editor'), + ] + ); + + $query->from($db->quoteName('#__banner_clients', 'a')); + + // Join over the banners for counting + $query->join('LEFT', $db->quoteName('#__banners', 'b'), $db->quoteName('a.id') . ' = ' . $db->quoteName('b.cid')); + + // Join over the users for the checked out user. + $query->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Filter by published state + $published = (string) $this->getState('filter.state'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.state') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where($db->quoteName('a.state') . ' IN (0, 1)'); + } + + $query->group( + [ + $db->quoteName('a.id'), + $db->quoteName('a.name'), + $db->quoteName('a.contact'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.state'), + $db->quoteName('a.metakey'), + $db->quoteName('a.purchase_type'), + $db->quoteName('uc.name'), + ] + ); + + // Filter by search in title + if ($search = trim($this->getState('filter.search', ''))) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', $search) . '%'; + $query->where($db->quoteName('a.name') . ' LIKE :search') + ->bind(':search', $search); + } + } + + // Filter by purchase type + if ($purchaseType = (int) $this->getState('filter.purchase_type')) { + if ($defaultPurchase === $purchaseType) { + $query->where('(' . $db->quoteName('a.purchase_type') . ' = :type OR ' . $db->quoteName('a.purchase_type') . ' = -1)'); + } else { + $query->where($db->quoteName('a.purchase_type') . ' = :type'); + } + + $query->bind(':type', $purchaseType, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $query->order( + $db->quoteName($db->escape($this->getState('list.ordering', 'a.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) + ); + + return $query; + } + + /** + * Overrides the getItems method to attach additional metrics to the list. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.6 + */ + public function getItems() + { + // Get a storage key. + $store = $this->getStoreId('getItems'); + + // Try to load the data from internal storage. + if (!empty($this->cache[$store])) { + return $this->cache[$store]; + } + + // Load the list items. + $items = parent::getItems(); + + // If empty or an error, just return. + if (empty($items)) { + return array(); + } + + // Getting the following metric by joins is WAY TOO SLOW. + // Faster to do three queries for very large banner trees. + + // Get the clients in the list. + $db = $this->getDatabase(); + $clientIds = array_column($items, 'id'); + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('cid'), + 'COUNT(' . $db->quoteName('cid') . ') AS ' . $db->quoteName('count_published'), + ] + ) + ->from($db->quoteName('#__banners')) + ->where($db->quoteName('state') . ' = :state') + ->whereIn($db->quoteName('cid'), $clientIds) + ->group($db->quoteName('cid')) + ->bind(':state', $state, ParameterType::INTEGER); + + $db->setQuery($query); + + // Get the published banners count. + try { + $state = 1; + $countPublished = $db->loadAssocList('cid', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get the unpublished banners count. + try { + $state = 0; + $countUnpublished = $db->loadAssocList('cid', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get the trashed banners count. + try { + $state = -2; + $countTrashed = $db->loadAssocList('cid', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get the archived banners count. + try { + $state = 2; + $countArchived = $db->loadAssocList('cid', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Inject the values back into the array. + foreach ($items as $item) { + $item->count_published = isset($countPublished[$item->id]) ? $countPublished[$item->id] : 0; + $item->count_unpublished = isset($countUnpublished[$item->id]) ? $countUnpublished[$item->id] : 0; + $item->count_trashed = isset($countTrashed[$item->id]) ? $countTrashed[$item->id] : 0; + $item->count_archived = isset($countArchived[$item->id]) ? $countArchived[$item->id] : 0; + } + + // Add the items to the internal cache. + $this->cache[$store] = $items; + + return $this->cache[$store]; + } } diff --git a/administrator/components/com_banners/src/Model/DownloadModel.php b/administrator/components/com_banners/src/Model/DownloadModel.php index 5cd2d4ff61a11..10a0af993a243 100644 --- a/administrator/components/com_banners/src/Model/DownloadModel.php +++ b/administrator/components/com_banners/src/Model/DownloadModel.php @@ -1,4 +1,5 @@ input; + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $input = Factory::getApplication()->input; - $this->setState('basename', $input->cookie->getString(ApplicationHelper::getHash($this->_context . '.basename'), '__SITE__')); - $this->setState('compressed', $input->cookie->getInt(ApplicationHelper::getHash($this->_context . '.compressed'), 1)); - } + $this->setState('basename', $input->cookie->getString(ApplicationHelper::getHash($this->_context . '.basename'), '__SITE__')); + $this->setState('compressed', $input->cookie->getInt(ApplicationHelper::getHash($this->_context . '.compressed'), 1)); + } - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_banners.download', 'download', array('control' => 'jform', 'load_data' => $loadData)); + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_banners.download', 'download', array('control' => 'jform', 'load_data' => $loadData)); - if (empty($form)) - { - return false; - } + if (empty($form)) { + return false; + } - return $form; - } + return $form; + } - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $data = (object) array( - 'basename' => $this->getState('basename'), - 'compressed' => $this->getState('compressed'), - ); + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $data = (object) array( + 'basename' => $this->getState('basename'), + 'compressed' => $this->getState('compressed'), + ); - $this->preprocessData('com_banners.download', $data); + $this->preprocessData('com_banners.download', $data); - return $data; - } + return $data; + } } diff --git a/administrator/components/com_banners/src/Model/TracksModel.php b/administrator/components/com_banners/src/Model/TracksModel.php index b9e56e73e6c42..7302fb9d6b42d 100644 --- a/administrator/components/com_banners/src/Model/TracksModel.php +++ b/administrator/components/com_banners/src/Model/TracksModel.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Banners\Administrator\Model; \defined('_JEXEC') or die; @@ -27,524 +29,464 @@ */ class TracksModel extends ListModel { - /** - * The base name - * - * @var string - * @since 1.6 - */ - protected $basename; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * - * @since 1.6 - */ - public function __construct($config = array()) - { - if (empty($config['filter_fields'])) - { - $config['filter_fields'] = array( - 'b.name', 'banner_name', - 'cl.name', 'client_name', 'client_id', - 'c.title', 'category_title', 'category_id', - 'track_type', 'a.track_type', 'type', - 'count', 'a.count', - 'track_date', 'a.track_date', 'end', 'begin', - 'level', 'c.level', - ); - } - - parent::__construct($config); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'b.name', $direction = 'asc') - { - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_banners')); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - [ - $db->quoteName('a.track_date'), - $db->quoteName('a.track_type'), - $db->quoteName('a.count'), - $db->quoteName('b.name', 'banner_name'), - $db->quoteName('cl.name', 'client_name'), - $db->quoteName('c.title', 'category_title'), - ] - ); - - // From tracks table. - $query->from($db->quoteName('#__banner_tracks', 'a')); - - // Join with the banners. - $query->join('LEFT', $db->quoteName('#__banners', 'b'), $db->quoteName('b.id') . ' = ' . $db->quoteName('a.banner_id')); - - // Join with the client. - $query->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('b.cid')); - - // Join with the category. - $query->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('b.catid')); - - // Filter by type. - - if ($type = (int) $this->getState('filter.type')) - { - $query->where($db->quoteName('a.track_type') . ' = :type') - ->bind(':type', $type, ParameterType::INTEGER); - } - - // Filter by client. - $clientId = $this->getState('filter.client_id'); - - if (is_numeric($clientId)) - { - $clientId = (int) $clientId; - $query->where($db->quoteName('b.cid') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - // Filter by category. - $categoryId = $this->getState('filter.category_id'); - - if (is_numeric($categoryId)) - { - $categoryId = (int) $categoryId; - $query->where($db->quoteName('b.catid') . ' = :categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - - // Filter by begin date. - if ($begin = $this->getState('filter.begin')) - { - $query->where($db->quoteName('a.track_date') . ' >= :begin') - ->bind(':begin', $begin); - } - - // Filter by end date. - if ($end = $this->getState('filter.end')) - { - $query->where($db->quoteName('a.track_date') . ' <= :end') - ->bind(':end', $end); - } - - // Filter on the level. - if ($level = (int) $this->getState('filter.level')) - { - $query->where($db->quoteName('c.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter by search in banner name or client name. - if ($search = $this->getState('filter.search')) - { - $search = '%' . StringHelper::strtolower($search) . '%'; - $query->where('(LOWER(' . $db->quoteName('b.name') . ') LIKE :search1 OR LOWER(' . $db->quoteName('cl.name') . ') LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - - // Add the list ordering clause. - $query->order( - $db->quoteName($db->escape($this->getState('list.ordering', 'b.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) - ); - - return $query; - } - - /** - * Method to delete rows. - * - * @return boolean Returns true on success, false on failure. - */ - public function delete() - { - $user = Factory::getUser(); - $categoryId = (int) $this->getState('category_id'); - - // Access checks. - if ($categoryId) - { - $allow = $user->authorise('core.delete', 'com_banners.category.' . $categoryId); - } - else - { - $allow = $user->authorise('core.delete', 'com_banners'); - } - - if ($allow) - { - // Delete tracks from this banner - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__banner_tracks')); - - // Filter by type - if ($type = (int) $this->getState('filter.type')) - { - $query->where($db->quoteName('track_type') . ' = :type') - ->bind(':type', $type, ParameterType::INTEGER); - } - - // Filter by begin date - if ($begin = $this->getState('filter.begin')) - { - $query->where($db->quoteName('track_date') . ' >= :begin') - ->bind(':begin', $begin); - } - - // Filter by end date - if ($end = $this->getState('filter.end')) - { - $query->where($db->quoteName('track_date') . ' <= :end') - ->bind(':end', $end); - } - - $subQuery = $db->getQuery(true); - $subQuery->select($db->quoteName('id')) - ->from($db->quoteName('#__banners')); - - // Filter by client - if ($clientId = (int) $this->getState('filter.client_id')) - { - $subQuery->where($db->quoteName('cid') . ' = :clientId'); - $query->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - // Filter by category - if ($categoryId) - { - $subQuery->where($db->quoteName('catid') . ' = :categoryId'); - $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - - $query->where($db->quoteName('banner_id') . ' IN (' . $subQuery . ')'); - - $db->setQuery($query); - $this->setError((string) $query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - else - { - Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); - } - - return true; - } - - /** - * Get file name - * - * @return string The file name - * - * @since 1.6 - */ - public function getBaseName() - { - if (!isset($this->basename)) - { - $basename = str_replace('__SITE__', Factory::getApplication()->get('sitename'), $this->getState('basename')); - $categoryId = $this->getState('filter.category_id'); - - if (is_numeric($categoryId)) - { - if ($categoryId > 0) - { - $basename = str_replace('__CATID__', $categoryId, $basename); - } - else - { - $basename = str_replace('__CATID__', '', $basename); - } - - $categoryName = $this->getCategoryName(); - $basename = str_replace('__CATNAME__', $categoryName, $basename); - } - else - { - $basename = str_replace(array('__CATID__', '__CATNAME__'), '', $basename); - } - - $clientId = $this->getState('filter.client_id'); - - if (is_numeric($clientId)) - { - if ($clientId > 0) - { - $basename = str_replace('__CLIENTID__', $clientId, $basename); - } - else - { - $basename = str_replace('__CLIENTID__', '', $basename); - } - - $clientName = $this->getClientName(); - $basename = str_replace('__CLIENTNAME__', $clientName, $basename); - } - else - { - $basename = str_replace(array('__CLIENTID__', '__CLIENTNAME__'), '', $basename); - } - - $type = $this->getState('filter.type'); - - if ($type > 0) - { - $basename = str_replace('__TYPE__', $type, $basename); - $typeName = Text::_('COM_BANNERS_TYPE' . $type); - $basename = str_replace('__TYPENAME__', $typeName, $basename); - } - else - { - $basename = str_replace(array('__TYPE__', '__TYPENAME__'), '', $basename); - } - - $begin = $this->getState('filter.begin'); - - if (!empty($begin)) - { - $basename = str_replace('__BEGIN__', $begin, $basename); - } - else - { - $basename = str_replace('__BEGIN__', '', $basename); - } - - $end = $this->getState('filter.end'); - - if (!empty($end)) - { - $basename = str_replace('__END__', $end, $basename); - } - else - { - $basename = str_replace('__END__', '', $basename); - } - - $this->basename = $basename; - } - - return $this->basename; - } - - /** - * Get the category name. - * - * @return string The category name - * - * @since 1.6 - */ - protected function getCategoryName() - { - $categoryId = (int) $this->getState('filter.category_id'); - - if ($categoryId) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('id') . ' = :categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $name = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $name; - } - - return Text::_('COM_BANNERS_NOCATEGORYNAME'); - } - - /** - * Get the client name - * - * @return string The client name. - * - * @since 1.6 - */ - protected function getClientName() - { - $clientId = (int) $this->getState('filter.client_id'); - - if ($clientId) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__banner_clients')) - ->where($db->quoteName('id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $name = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $name; - } - - return Text::_('COM_BANNERS_NOCLIENTNAME'); - } - - /** - * Get the file type. - * - * @return string The file type - * - * @since 1.6 - */ - public function getFileType() - { - return $this->getState('compressed') ? 'zip' : 'csv'; - } - - /** - * Get the mime type. - * - * @return string The mime type. - * - * @since 1.6 - */ - public function getMimeType() - { - return $this->getState('compressed') ? 'application/zip' : 'text/csv'; - } - - /** - * Get the content - * - * @return string The content. - * - * @since 1.6 - */ - public function getContent() - { - if (!isset($this->content)) - { - $this->content = '"' . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_NAME')) . '","' - . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_CLIENT')) . '","' - . str_replace('"', '""', Text::_('JCATEGORY')) . '","' - . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_TYPE')) . '","' - . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_COUNT')) . '","' - . str_replace('"', '""', Text::_('JDATE')) . '"' . "\n"; - - foreach ($this->getItems() as $item) - { - $this->content .= '"' . str_replace('"', '""', $item->banner_name) . '","' - . str_replace('"', '""', $item->client_name) . '","' - . str_replace('"', '""', $item->category_title) . '","' - . str_replace('"', '""', ($item->track_type == 1 ? Text::_('COM_BANNERS_IMPRESSION') : Text::_('COM_BANNERS_CLICK'))) . '","' - . str_replace('"', '""', $item->count) . '","' - . str_replace('"', '""', $item->track_date) . '"' . "\n"; - } - - if ($this->getState('compressed')) - { - $app = Factory::getApplication(); - - $files = array( - 'track' => array( - 'name' => $this->getBaseName() . '.csv', - 'data' => $this->content, - 'time' => time() - ) - ); - $ziproot = $app->get('tmp_path') . '/' . uniqid('banners_tracks_') . '.zip'; - - // Run the packager - $delete = Folder::files($app->get('tmp_path') . '/', uniqid('banners_tracks_'), false, true); - - if (!empty($delete)) - { - if (!File::delete($delete)) - { - // File::delete throws an error - $this->setError(Text::_('COM_BANNERS_ERR_ZIP_DELETE_FAILURE')); - - return false; - } - } - - $archive = new Archive; - - if (!$packager = $archive->getAdapter('zip')) - { - $this->setError(Text::_('COM_BANNERS_ERR_ZIP_ADAPTER_FAILURE')); - - return false; - } - elseif (!$packager->create($ziproot, $files)) - { - $this->setError(Text::_('COM_BANNERS_ERR_ZIP_CREATE_FAILURE')); - - return false; - } - - $this->content = file_get_contents($ziproot); - - // Remove tmp zip file, it's no longer needed. - File::delete($ziproot); - } - } - - return $this->content; - } + /** + * The base name + * + * @var string + * @since 1.6 + */ + protected $basename; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'b.name', 'banner_name', + 'cl.name', 'client_name', 'client_id', + 'c.title', 'category_title', 'category_id', + 'track_type', 'a.track_type', 'type', + 'count', 'a.count', + 'track_date', 'a.track_date', 'end', 'begin', + 'level', 'c.level', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'b.name', $direction = 'asc') + { + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_banners')); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + [ + $db->quoteName('a.track_date'), + $db->quoteName('a.track_type'), + $db->quoteName('a.count'), + $db->quoteName('b.name', 'banner_name'), + $db->quoteName('cl.name', 'client_name'), + $db->quoteName('c.title', 'category_title'), + ] + ); + + // From tracks table. + $query->from($db->quoteName('#__banner_tracks', 'a')); + + // Join with the banners. + $query->join('LEFT', $db->quoteName('#__banners', 'b'), $db->quoteName('b.id') . ' = ' . $db->quoteName('a.banner_id')); + + // Join with the client. + $query->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('b.cid')); + + // Join with the category. + $query->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('b.catid')); + + // Filter by type. + + if ($type = (int) $this->getState('filter.type')) { + $query->where($db->quoteName('a.track_type') . ' = :type') + ->bind(':type', $type, ParameterType::INTEGER); + } + + // Filter by client. + $clientId = $this->getState('filter.client_id'); + + if (is_numeric($clientId)) { + $clientId = (int) $clientId; + $query->where($db->quoteName('b.cid') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + // Filter by category. + $categoryId = $this->getState('filter.category_id'); + + if (is_numeric($categoryId)) { + $categoryId = (int) $categoryId; + $query->where($db->quoteName('b.catid') . ' = :categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + + // Filter by begin date. + if ($begin = $this->getState('filter.begin')) { + $query->where($db->quoteName('a.track_date') . ' >= :begin') + ->bind(':begin', $begin); + } + + // Filter by end date. + if ($end = $this->getState('filter.end')) { + $query->where($db->quoteName('a.track_date') . ' <= :end') + ->bind(':end', $end); + } + + // Filter on the level. + if ($level = (int) $this->getState('filter.level')) { + $query->where($db->quoteName('c.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter by search in banner name or client name. + if ($search = $this->getState('filter.search')) { + $search = '%' . StringHelper::strtolower($search) . '%'; + $query->where('(LOWER(' . $db->quoteName('b.name') . ') LIKE :search1 OR LOWER(' . $db->quoteName('cl.name') . ') LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + + // Add the list ordering clause. + $query->order( + $db->quoteName($db->escape($this->getState('list.ordering', 'b.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) + ); + + return $query; + } + + /** + * Method to delete rows. + * + * @return boolean Returns true on success, false on failure. + */ + public function delete() + { + $user = Factory::getUser(); + $categoryId = (int) $this->getState('category_id'); + + // Access checks. + if ($categoryId) { + $allow = $user->authorise('core.delete', 'com_banners.category.' . $categoryId); + } else { + $allow = $user->authorise('core.delete', 'com_banners'); + } + + if ($allow) { + // Delete tracks from this banner + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__banner_tracks')); + + // Filter by type + if ($type = (int) $this->getState('filter.type')) { + $query->where($db->quoteName('track_type') . ' = :type') + ->bind(':type', $type, ParameterType::INTEGER); + } + + // Filter by begin date + if ($begin = $this->getState('filter.begin')) { + $query->where($db->quoteName('track_date') . ' >= :begin') + ->bind(':begin', $begin); + } + + // Filter by end date + if ($end = $this->getState('filter.end')) { + $query->where($db->quoteName('track_date') . ' <= :end') + ->bind(':end', $end); + } + + $subQuery = $db->getQuery(true); + $subQuery->select($db->quoteName('id')) + ->from($db->quoteName('#__banners')); + + // Filter by client + if ($clientId = (int) $this->getState('filter.client_id')) { + $subQuery->where($db->quoteName('cid') . ' = :clientId'); + $query->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + // Filter by category + if ($categoryId) { + $subQuery->where($db->quoteName('catid') . ' = :categoryId'); + $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + + $query->where($db->quoteName('banner_id') . ' IN (' . $subQuery . ')'); + + $db->setQuery($query); + $this->setError((string) $query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } else { + Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); + } + + return true; + } + + /** + * Get file name + * + * @return string The file name + * + * @since 1.6 + */ + public function getBaseName() + { + if (!isset($this->basename)) { + $basename = str_replace('__SITE__', Factory::getApplication()->get('sitename'), $this->getState('basename')); + $categoryId = $this->getState('filter.category_id'); + + if (is_numeric($categoryId)) { + if ($categoryId > 0) { + $basename = str_replace('__CATID__', $categoryId, $basename); + } else { + $basename = str_replace('__CATID__', '', $basename); + } + + $categoryName = $this->getCategoryName(); + $basename = str_replace('__CATNAME__', $categoryName, $basename); + } else { + $basename = str_replace(array('__CATID__', '__CATNAME__'), '', $basename); + } + + $clientId = $this->getState('filter.client_id'); + + if (is_numeric($clientId)) { + if ($clientId > 0) { + $basename = str_replace('__CLIENTID__', $clientId, $basename); + } else { + $basename = str_replace('__CLIENTID__', '', $basename); + } + + $clientName = $this->getClientName(); + $basename = str_replace('__CLIENTNAME__', $clientName, $basename); + } else { + $basename = str_replace(array('__CLIENTID__', '__CLIENTNAME__'), '', $basename); + } + + $type = $this->getState('filter.type'); + + if ($type > 0) { + $basename = str_replace('__TYPE__', $type, $basename); + $typeName = Text::_('COM_BANNERS_TYPE' . $type); + $basename = str_replace('__TYPENAME__', $typeName, $basename); + } else { + $basename = str_replace(array('__TYPE__', '__TYPENAME__'), '', $basename); + } + + $begin = $this->getState('filter.begin'); + + if (!empty($begin)) { + $basename = str_replace('__BEGIN__', $begin, $basename); + } else { + $basename = str_replace('__BEGIN__', '', $basename); + } + + $end = $this->getState('filter.end'); + + if (!empty($end)) { + $basename = str_replace('__END__', $end, $basename); + } else { + $basename = str_replace('__END__', '', $basename); + } + + $this->basename = $basename; + } + + return $this->basename; + } + + /** + * Get the category name. + * + * @return string The category name + * + * @since 1.6 + */ + protected function getCategoryName() + { + $categoryId = (int) $this->getState('filter.category_id'); + + if ($categoryId) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('id') . ' = :categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $name = $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $name; + } + + return Text::_('COM_BANNERS_NOCATEGORYNAME'); + } + + /** + * Get the client name + * + * @return string The client name. + * + * @since 1.6 + */ + protected function getClientName() + { + $clientId = (int) $this->getState('filter.client_id'); + + if ($clientId) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__banner_clients')) + ->where($db->quoteName('id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $name = $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $name; + } + + return Text::_('COM_BANNERS_NOCLIENTNAME'); + } + + /** + * Get the file type. + * + * @return string The file type + * + * @since 1.6 + */ + public function getFileType() + { + return $this->getState('compressed') ? 'zip' : 'csv'; + } + + /** + * Get the mime type. + * + * @return string The mime type. + * + * @since 1.6 + */ + public function getMimeType() + { + return $this->getState('compressed') ? 'application/zip' : 'text/csv'; + } + + /** + * Get the content + * + * @return string The content. + * + * @since 1.6 + */ + public function getContent() + { + if (!isset($this->content)) { + $this->content = '"' . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_NAME')) . '","' + . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_CLIENT')) . '","' + . str_replace('"', '""', Text::_('JCATEGORY')) . '","' + . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_TYPE')) . '","' + . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_COUNT')) . '","' + . str_replace('"', '""', Text::_('JDATE')) . '"' . "\n"; + + foreach ($this->getItems() as $item) { + $this->content .= '"' . str_replace('"', '""', $item->banner_name) . '","' + . str_replace('"', '""', $item->client_name) . '","' + . str_replace('"', '""', $item->category_title) . '","' + . str_replace('"', '""', ($item->track_type == 1 ? Text::_('COM_BANNERS_IMPRESSION') : Text::_('COM_BANNERS_CLICK'))) . '","' + . str_replace('"', '""', $item->count) . '","' + . str_replace('"', '""', $item->track_date) . '"' . "\n"; + } + + if ($this->getState('compressed')) { + $app = Factory::getApplication(); + + $files = array( + 'track' => array( + 'name' => $this->getBaseName() . '.csv', + 'data' => $this->content, + 'time' => time() + ) + ); + $ziproot = $app->get('tmp_path') . '/' . uniqid('banners_tracks_') . '.zip'; + + // Run the packager + $delete = Folder::files($app->get('tmp_path') . '/', uniqid('banners_tracks_'), false, true); + + if (!empty($delete)) { + if (!File::delete($delete)) { + // File::delete throws an error + $this->setError(Text::_('COM_BANNERS_ERR_ZIP_DELETE_FAILURE')); + + return false; + } + } + + $archive = new Archive(); + + if (!$packager = $archive->getAdapter('zip')) { + $this->setError(Text::_('COM_BANNERS_ERR_ZIP_ADAPTER_FAILURE')); + + return false; + } elseif (!$packager->create($ziproot, $files)) { + $this->setError(Text::_('COM_BANNERS_ERR_ZIP_CREATE_FAILURE')); + + return false; + } + + $this->content = file_get_contents($ziproot); + + // Remove tmp zip file, it's no longer needed. + File::delete($ziproot); + } + } + + return $this->content; + } } diff --git a/administrator/components/com_banners/src/Service/Html/Banner.php b/administrator/components/com_banners/src/Service/Html/Banner.php index 3573e949be327..d7137956547d0 100644 --- a/administrator/components/com_banners/src/Service/Html/Banner.php +++ b/administrator/components/com_banners/src/Service/Html/Banner.php @@ -1,4 +1,5 @@ ', - Text::_('COM_BANNERS_BATCH_CLIENT_LABEL'), - '', - '' - ) - ); - } + /** + * Display a batch widget for the client selector. + * + * @return string The necessary HTML for the widget. + * + * @since 2.5 + */ + public function clients() + { + // Create the batch selector to change the client on a selection list. + return implode( + "\n", + array( + '', + '' + ) + ); + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 1.6 - */ - public function clientlist() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('id', 'value'), - $db->quoteName('name', 'text'), - ] - ) - ->from($db->quoteName('#__banner_clients')) - ->order($db->quoteName('name')); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 1.6 + */ + public function clientlist() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('id', 'value'), + $db->quoteName('name', 'text'), + ] + ) + ->from($db->quoteName('#__banner_clients')) + ->order($db->quoteName('name')); - // Get the options. - $db->setQuery($query); + // Get the options. + $db->setQuery($query); - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } - return $options; - } + return $options; + } - /** - * Returns a pinned state on a grid - * - * @param integer $value The state value. - * @param integer $i The row index - * @param boolean $enabled An optional setting for access control on the action. - * @param string $checkbox An optional prefix for checkboxes. - * - * @return string The Html code - * - * @see HTMLHelperJGrid::state - * @since 2.5.5 - */ - public function pinned($value, $i, $enabled = true, $checkbox = 'cb') - { - $states = array( - 1 => array( - 'sticky_unpublish', - 'COM_BANNERS_BANNERS_PINNED', - 'COM_BANNERS_BANNERS_HTML_PIN_BANNER', - 'COM_BANNERS_BANNERS_PINNED', - true, - 'publish', - 'publish' - ), - 0 => array( - 'sticky_publish', - 'COM_BANNERS_BANNERS_UNPINNED', - 'COM_BANNERS_BANNERS_HTML_UNPIN_BANNER', - 'COM_BANNERS_BANNERS_UNPINNED', - true, - 'unpublish', - 'unpublish' - ), - ); + /** + * Returns a pinned state on a grid + * + * @param integer $value The state value. + * @param integer $i The row index + * @param boolean $enabled An optional setting for access control on the action. + * @param string $checkbox An optional prefix for checkboxes. + * + * @return string The Html code + * + * @see HTMLHelperJGrid::state + * @since 2.5.5 + */ + public function pinned($value, $i, $enabled = true, $checkbox = 'cb') + { + $states = array( + 1 => array( + 'sticky_unpublish', + 'COM_BANNERS_BANNERS_PINNED', + 'COM_BANNERS_BANNERS_HTML_PIN_BANNER', + 'COM_BANNERS_BANNERS_PINNED', + true, + 'publish', + 'publish' + ), + 0 => array( + 'sticky_publish', + 'COM_BANNERS_BANNERS_UNPINNED', + 'COM_BANNERS_BANNERS_HTML_UNPIN_BANNER', + 'COM_BANNERS_BANNERS_UNPINNED', + true, + 'unpublish', + 'unpublish' + ), + ); - return HTMLHelper::_('jgrid.state', $states, $value, $i, 'banners.', $enabled, true, $checkbox); - } + return HTMLHelper::_('jgrid.state', $states, $value, $i, 'banners.', $enabled, true, $checkbox); + } } diff --git a/administrator/components/com_banners/src/Table/BannerTable.php b/administrator/components/com_banners/src/Table/BannerTable.php index dd23a61538042..94054c1fff2f7 100644 --- a/administrator/components/com_banners/src/Table/BannerTable.php +++ b/administrator/components/com_banners/src/Table/BannerTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_banners.banner'; - - parent::__construct('#__banners', 'id', $db); - - $this->created = Factory::getDate()->toSql(); - $this->setColumnAlias('published', 'state'); - } - - /** - * Increase click count - * - * @return void - */ - public function clicks() - { - $id = (int) $this->id; - $query = $this->_db->getQuery(true) - ->update($this->_db->quoteName('#__banners')) - ->set($this->_db->quoteName('clicks') . ' = ' . $this->_db->quoteName('clicks') . ' + 1') - ->where($this->_db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $this->_db->setQuery($query); - $this->_db->execute(); - } - - /** - * Overloaded check function - * - * @return boolean - * - * @see Table::check - * @since 1.5 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Set name - $this->name = htmlspecialchars_decode($this->name, ENT_QUOTES); - - // Set alias - if (trim($this->alias) == '') - { - $this->alias = $this->name; - } - - $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); - - if (trim(str_replace('-', '', $this->alias)) == '') - { - $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); - } - - // Check for a valid category. - if (!$this->catid = (int) $this->catid) - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); - - return false; - } - - // Set created date if not set. - if (!(int) $this->created) - { - $this->created = Factory::getDate()->toSql(); - } - - // Set publish_up, publish_down to null if not set - if (!$this->publish_up) - { - $this->publish_up = null; - } - - if (!$this->publish_down) - { - $this->publish_down = null; - } - - // Check the publish down date is not earlier than publish up. - if (!\is_null($this->publish_down) && !\is_null($this->publish_up) && $this->publish_down < $this->publish_up) - { - $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); - - return false; - } - - // Set ordering - if ($this->state < 0) - { - // Set ordering to 0 if state is archived or trashed - $this->ordering = 0; - } - elseif (empty($this->ordering)) - { - // Set ordering to last if ordering was 0 - $this->ordering = self::getNextOrder($this->_db->quoteName('catid') . ' = ' . ((int) $this->catid) . ' AND ' . $this->_db->quoteName('state') . ' >= 0'); - } - - // Set modified to created if not set - if (!$this->modified) - { - $this->modified = $this->created; - } - - // Set modified_by to created_by if not set - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - - return true; - } - - /** - * Overloaded bind function - * - * @param mixed $array An associative array or object to bind to the \JTable instance. - * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean True on success - * - * @since 1.5 - */ - public function bind($array, $ignore = array()) - { - if (isset($array['params']) && \is_array($array['params'])) - { - $registry = new Registry($array['params']); - - if ((int) $registry->get('width', 0) < 0) - { - $this->setError(Text::sprintf('JLIB_DATABASE_ERROR_NEGATIVE_NOT_PERMITTED', Text::_('COM_BANNERS_FIELD_WIDTH_LABEL'))); - - return false; - } - - if ((int) $registry->get('height', 0) < 0) - { - $this->setError(Text::sprintf('JLIB_DATABASE_ERROR_NEGATIVE_NOT_PERMITTED', Text::_('COM_BANNERS_FIELD_HEIGHT_LABEL'))); - - return false; - } - - // Converts the width and height to an absolute numeric value: - $width = abs((int) $registry->get('width', 0)); - $height = abs((int) $registry->get('height', 0)); - - // Sets the width and height to an empty string if = 0 - $registry->set('width', $width ?: ''); - $registry->set('height', $height ?: ''); - - $array['params'] = (string) $registry; - } - - if (isset($array['imptotal'])) - { - $array['imptotal'] = abs((int) $array['imptotal']); - } - - return parent::bind($array, $ignore); - } - - /** - * Method to store a row - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success, false on failure. - */ - public function store($updateNulls = true) - { - $db = $this->getDbo(); - - if (empty($this->id)) - { - $purchaseType = $this->purchase_type; - - if ($purchaseType < 0 && $this->cid) - { - $client = new ClientTable($db); - $client->load($this->cid); - $purchaseType = $client->purchase_type; - } - - if ($purchaseType < 0) - { - $purchaseType = ComponentHelper::getParams('com_banners')->get('purchase_type'); - } - - switch ($purchaseType) - { - case 1: - $this->reset = null; - break; - case 2: - $date = Factory::getDate('+1 year ' . date('Y-m-d')); - $this->reset = $date->toSql(); - break; - case 3: - $date = Factory::getDate('+1 month ' . date('Y-m-d')); - $this->reset = $date->toSql(); - break; - case 4: - $date = Factory::getDate('+7 day ' . date('Y-m-d')); - $this->reset = $date->toSql(); - break; - case 5: - $date = Factory::getDate('+1 day ' . date('Y-m-d')); - $this->reset = $date->toSql(); - break; - } - - // Store the row - parent::store($updateNulls); - } - else - { - // Get the old row - /** @var BannerTable $oldrow */ - $oldrow = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $db)); - - if (!$oldrow->load($this->id) && $oldrow->getError()) - { - $this->setError($oldrow->getError()); - } - - // Verify that the alias is unique - /** @var BannerTable $table */ - $table = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $db)); - - if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) - { - $this->setError(Text::_('COM_BANNERS_ERROR_UNIQUE_ALIAS')); - - return false; - } - - // Store the new row - parent::store($updateNulls); - - // Need to reorder ? - if ($oldrow->state >= 0 && ($this->state < 0 || $oldrow->catid != $this->catid)) - { - // Reorder the oldrow - $this->reorder($this->_db->quoteName('catid') . ' = ' . ((int) $oldrow->catid) . ' AND ' . $this->_db->quoteName('state') . ' >= 0'); - } - } - - return \count($this->getErrors()) == 0; - } - - /** - * Method to set the sticky state for a row or list of rows in the database - * table. The method respects checked out rows by other users and will attempt - * to checkin rows that it can after adjustments are made. - * - * @param mixed $pks An optional array of primary key values to update. If not set the instance property value is used. - * @param integer $state The sticky state. eg. [0 = unsticked, 1 = sticked] - * @param integer $userId The user id of the user performing the operation. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function stick($pks = null, $state = 1, $userId = 0) - { - $k = $this->_tbl_key; - - // Sanitize input. - $pks = ArrayHelper::toInteger($pks); - $userId = (int) $userId; - $state = (int) $state; - - // If there are no primary keys set check to see if the instance key is set. - if (empty($pks)) - { - if ($this->$k) - { - $pks = array($this->$k); - } - // Nothing to set publishing state on, return false. - else - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED')); - - return false; - } - } - - // Get an instance of the table - /** @var BannerTable $table */ - $table = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $this->_db)); - - // For all keys - foreach ($pks as $pk) - { - // Load the banner - if (!$table->load($pk)) - { - $this->setError($table->getError()); - } - - // Verify checkout - if (\is_null($table->checked_out) || $table->checked_out == $userId) - { - // Change the state - $table->sticky = $state; - $table->checked_out = null; - $table->checked_out_time = null; - - // Check the row - $table->check(); - - // Store the row - if (!$table->store()) - { - $this->setError($table->getError()); - } - } - } - - return \count($this->getErrors()) == 0; - } - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Constructor + * + * @param DatabaseDriver $db Database connector object + * + * @since 1.5 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_banners.banner'; + + parent::__construct('#__banners', 'id', $db); + + $this->created = Factory::getDate()->toSql(); + $this->setColumnAlias('published', 'state'); + } + + /** + * Increase click count + * + * @return void + */ + public function clicks() + { + $id = (int) $this->id; + $query = $this->_db->getQuery(true) + ->update($this->_db->quoteName('#__banners')) + ->set($this->_db->quoteName('clicks') . ' = ' . $this->_db->quoteName('clicks') . ' + 1') + ->where($this->_db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $this->_db->setQuery($query); + $this->_db->execute(); + } + + /** + * Overloaded check function + * + * @return boolean + * + * @see Table::check + * @since 1.5 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Set name + $this->name = htmlspecialchars_decode($this->name, ENT_QUOTES); + + // Set alias + if (trim($this->alias) == '') { + $this->alias = $this->name; + } + + $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); + + if (trim(str_replace('-', '', $this->alias)) == '') { + $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); + } + + // Check for a valid category. + if (!$this->catid = (int) $this->catid) { + $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); + + return false; + } + + // Set created date if not set. + if (!(int) $this->created) { + $this->created = Factory::getDate()->toSql(); + } + + // Set publish_up, publish_down to null if not set + if (!$this->publish_up) { + $this->publish_up = null; + } + + if (!$this->publish_down) { + $this->publish_down = null; + } + + // Check the publish down date is not earlier than publish up. + if (!\is_null($this->publish_down) && !\is_null($this->publish_up) && $this->publish_down < $this->publish_up) { + $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); + + return false; + } + + // Set ordering + if ($this->state < 0) { + // Set ordering to 0 if state is archived or trashed + $this->ordering = 0; + } elseif (empty($this->ordering)) { + // Set ordering to last if ordering was 0 + $this->ordering = self::getNextOrder($this->_db->quoteName('catid') . ' = ' . ((int) $this->catid) . ' AND ' . $this->_db->quoteName('state') . ' >= 0'); + } + + // Set modified to created if not set + if (!$this->modified) { + $this->modified = $this->created; + } + + // Set modified_by to created_by if not set + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + + return true; + } + + /** + * Overloaded bind function + * + * @param mixed $array An associative array or object to bind to the \JTable instance. + * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success + * + * @since 1.5 + */ + public function bind($array, $ignore = array()) + { + if (isset($array['params']) && \is_array($array['params'])) { + $registry = new Registry($array['params']); + + if ((int) $registry->get('width', 0) < 0) { + $this->setError(Text::sprintf('JLIB_DATABASE_ERROR_NEGATIVE_NOT_PERMITTED', Text::_('COM_BANNERS_FIELD_WIDTH_LABEL'))); + + return false; + } + + if ((int) $registry->get('height', 0) < 0) { + $this->setError(Text::sprintf('JLIB_DATABASE_ERROR_NEGATIVE_NOT_PERMITTED', Text::_('COM_BANNERS_FIELD_HEIGHT_LABEL'))); + + return false; + } + + // Converts the width and height to an absolute numeric value: + $width = abs((int) $registry->get('width', 0)); + $height = abs((int) $registry->get('height', 0)); + + // Sets the width and height to an empty string if = 0 + $registry->set('width', $width ?: ''); + $registry->set('height', $height ?: ''); + + $array['params'] = (string) $registry; + } + + if (isset($array['imptotal'])) { + $array['imptotal'] = abs((int) $array['imptotal']); + } + + return parent::bind($array, $ignore); + } + + /** + * Method to store a row + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success, false on failure. + */ + public function store($updateNulls = true) + { + $db = $this->getDbo(); + + if (empty($this->id)) { + $purchaseType = $this->purchase_type; + + if ($purchaseType < 0 && $this->cid) { + $client = new ClientTable($db); + $client->load($this->cid); + $purchaseType = $client->purchase_type; + } + + if ($purchaseType < 0) { + $purchaseType = ComponentHelper::getParams('com_banners')->get('purchase_type'); + } + + switch ($purchaseType) { + case 1: + $this->reset = null; + break; + case 2: + $date = Factory::getDate('+1 year ' . date('Y-m-d')); + $this->reset = $date->toSql(); + break; + case 3: + $date = Factory::getDate('+1 month ' . date('Y-m-d')); + $this->reset = $date->toSql(); + break; + case 4: + $date = Factory::getDate('+7 day ' . date('Y-m-d')); + $this->reset = $date->toSql(); + break; + case 5: + $date = Factory::getDate('+1 day ' . date('Y-m-d')); + $this->reset = $date->toSql(); + break; + } + + // Store the row + parent::store($updateNulls); + } else { + // Get the old row + /** @var BannerTable $oldrow */ + $oldrow = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $db)); + + if (!$oldrow->load($this->id) && $oldrow->getError()) { + $this->setError($oldrow->getError()); + } + + // Verify that the alias is unique + /** @var BannerTable $table */ + $table = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $db)); + + if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) { + $this->setError(Text::_('COM_BANNERS_ERROR_UNIQUE_ALIAS')); + + return false; + } + + // Store the new row + parent::store($updateNulls); + + // Need to reorder ? + if ($oldrow->state >= 0 && ($this->state < 0 || $oldrow->catid != $this->catid)) { + // Reorder the oldrow + $this->reorder($this->_db->quoteName('catid') . ' = ' . ((int) $oldrow->catid) . ' AND ' . $this->_db->quoteName('state') . ' >= 0'); + } + } + + return \count($this->getErrors()) == 0; + } + + /** + * Method to set the sticky state for a row or list of rows in the database + * table. The method respects checked out rows by other users and will attempt + * to checkin rows that it can after adjustments are made. + * + * @param mixed $pks An optional array of primary key values to update. If not set the instance property value is used. + * @param integer $state The sticky state. eg. [0 = unsticked, 1 = sticked] + * @param integer $userId The user id of the user performing the operation. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function stick($pks = null, $state = 1, $userId = 0) + { + $k = $this->_tbl_key; + + // Sanitize input. + $pks = ArrayHelper::toInteger($pks); + $userId = (int) $userId; + $state = (int) $state; + + // If there are no primary keys set check to see if the instance key is set. + if (empty($pks)) { + if ($this->$k) { + $pks = array($this->$k); + } + // Nothing to set publishing state on, return false. + else { + $this->setError(Text::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED')); + + return false; + } + } + + // Get an instance of the table + /** @var BannerTable $table */ + $table = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $this->_db)); + + // For all keys + foreach ($pks as $pk) { + // Load the banner + if (!$table->load($pk)) { + $this->setError($table->getError()); + } + + // Verify checkout + if (\is_null($table->checked_out) || $table->checked_out == $userId) { + // Change the state + $table->sticky = $state; + $table->checked_out = null; + $table->checked_out_time = null; + + // Check the row + $table->check(); + + // Store the row + if (!$table->store()) { + $this->setError($table->getError()); + } + } + } + + return \count($this->getErrors()) == 0; + } + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } } diff --git a/administrator/components/com_banners/src/Table/ClientTable.php b/administrator/components/com_banners/src/Table/ClientTable.php index 977dc5f656857..df34e48a0e6e3 100644 --- a/administrator/components/com_banners/src/Table/ClientTable.php +++ b/administrator/components/com_banners/src/Table/ClientTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_banners.client'; - - $this->setColumnAlias('published', 'state'); - - parent::__construct('#__banner_clients', 'id', $db); - } - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } - - /** - * Overloaded check function - * - * @return boolean True if the object is ok - * - * @see Table::check() - * @since 4.0.0 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Check for valid name - if (trim($this->name) === '') - { - $this->setError(Text::_('COM_BANNERS_WARNING_PROVIDE_VALID_NAME')); - - return false; - } - - // Check for valid contact - if (trim($this->contact) === '') - { - $this->setError(Text::_('COM_BANNERS_PROVIDE_VALID_CONTACT')); - - return false; - } - - return true; - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Constructor + * + * @param DatabaseDriver $db Database connector object + * + * @since 1.5 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_banners.client'; + + $this->setColumnAlias('published', 'state'); + + parent::__construct('#__banner_clients', 'id', $db); + } + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } + + /** + * Overloaded check function + * + * @return boolean True if the object is ok + * + * @see Table::check() + * @since 4.0.0 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Check for valid name + if (trim($this->name) === '') { + $this->setError(Text::_('COM_BANNERS_WARNING_PROVIDE_VALID_NAME')); + + return false; + } + + // Check for valid contact + if (trim($this->contact) === '') { + $this->setError(Text::_('COM_BANNERS_PROVIDE_VALID_CONTACT')); + + return false; + } + + return true; + } } diff --git a/administrator/components/com_banners/src/View/Banner/HtmlView.php b/administrator/components/com_banners/src/View/Banner/HtmlView.php index a73f0d9f2415c..c9743c518720f 100644 --- a/administrator/components/com_banners/src/View/Banner/HtmlView.php +++ b/administrator/components/com_banners/src/View/Banner/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->form = $model->getForm(); - $this->item = $model->getItem(); - $this->state = $model->getState(); - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * @throws Exception - */ - protected function addToolbar(): void - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $userId = $user->id; - $isNew = ($this->item->id == 0); - $checkedOut = !(\is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Since we don't track these assets at the item level, use the category id. - $canDo = ContentHelper::getActions('com_banners', 'category', $this->item->catid); - - ToolbarHelper::title($isNew ? Text::_('COM_BANNERS_MANAGER_BANNER_NEW') : Text::_('COM_BANNERS_MANAGER_BANNER_EDIT'), 'bookmark banners'); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if (!$checkedOut && ($canDo->get('core.edit') || \count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0)) - { - ToolbarHelper::apply('banner.apply'); - $toolbarButtons[] = ['save', 'banner.save']; - - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'banner.save2new']; - } - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'banner.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('banner.cancel'); - } - else - { - ToolbarHelper::cancel('banner.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) - { - ToolbarHelper::versions('com_banners.banner', $this->item->id); - } - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Banners:_Edit'); - } + /** + * The Form object + * + * @var Form + * @since 1.5 + */ + protected $form; + + /** + * The active item + * + * @var object + * @since 1.5 + */ + protected $item; + + /** + * The model state + * + * @var object + * @since 1.5 + */ + protected $state; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.5 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var BannerModel $model */ + $model = $this->getModel(); + $this->form = $model->getForm(); + $this->item = $model->getItem(); + $this->state = $model->getState(); + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * @throws Exception + */ + protected function addToolbar(): void + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = ($this->item->id == 0); + $checkedOut = !(\is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Since we don't track these assets at the item level, use the category id. + $canDo = ContentHelper::getActions('com_banners', 'category', $this->item->catid); + + ToolbarHelper::title($isNew ? Text::_('COM_BANNERS_MANAGER_BANNER_NEW') : Text::_('COM_BANNERS_MANAGER_BANNER_EDIT'), 'bookmark banners'); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if (!$checkedOut && ($canDo->get('core.edit') || \count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0)) { + ToolbarHelper::apply('banner.apply'); + $toolbarButtons[] = ['save', 'banner.save']; + + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'banner.save2new']; + } + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'banner.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('banner.cancel'); + } else { + ToolbarHelper::cancel('banner.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) { + ToolbarHelper::versions('com_banners.banner', $this->item->id); + } + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Banners:_Edit'); + } } diff --git a/administrator/components/com_banners/src/View/Banners/HtmlView.php b/administrator/components/com_banners/src/View/Banners/HtmlView.php index 5019ffd9e592d..4a22e4684c0a4 100644 --- a/administrator/components/com_banners/src/View/Banners/HtmlView.php +++ b/administrator/components/com_banners/src/View/Banners/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->categories = $model->getCategoryOrders(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->state = $model->getState(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_banners', 'category', $this->state->get('filter.category_id')); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_BANNERS'), 'bookmark banners'); - - if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0) - { - $toolbar->addNew('banner.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - if ($this->state->get('filter.published') != 2) - { - $childBar->publish('banners.publish')->listCheck(true); - - $childBar->unpublish('banners.unpublish')->listCheck(true); - } - - if ($this->state->get('filter.published') != -1) - { - if ($this->state->get('filter.published') != 2) - { - $childBar->archive('banners.archive')->listCheck(true); - } - elseif ($this->state->get('filter.published') == 2) - { - $childBar->publish('publish')->task('banners.publish')->listCheck(true); - } - } - - $childBar->checkin('banners.checkin')->listCheck(true); - - if ($this->state->get('filter.published') != -2) - { - $childBar->trash('banners.trash')->listCheck(true); - } - } - - if ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('banners.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - // Add a batch button - if ($user->authorise('core.create', 'com_banners') - && $user->authorise('core.edit', 'com_banners') - && $user->authorise('core.edit.state', 'com_banners')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if ($user->authorise('core.admin', 'com_banners') || $user->authorise('core.options', 'com_banners')) - { - $toolbar->preferences('com_banners'); - } - - $toolbar->help('Banners'); - } + /** + * The search tools form + * + * @var Form + * @since 1.6 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 1.6 + */ + public $activeFilters = []; + + /** + * Category data + * + * @var array + * @since 1.6 + */ + protected $categories = []; + + /** + * An array of items + * + * @var array + * @since 1.6 + */ + protected $items = []; + + /** + * The pagination object + * + * @var Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state + * + * @var Registry + * @since 1.6 + */ + protected $state; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 1.6 + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var BannersModel $model */ + $model = $this->getModel(); + $this->categories = $model->getCategoryOrders(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->state = $model->getState(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_banners', 'category', $this->state->get('filter.category_id')); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_BANNERS'), 'bookmark banners'); + + if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0) { + $toolbar->addNew('banner.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + if ($this->state->get('filter.published') != 2) { + $childBar->publish('banners.publish')->listCheck(true); + + $childBar->unpublish('banners.unpublish')->listCheck(true); + } + + if ($this->state->get('filter.published') != -1) { + if ($this->state->get('filter.published') != 2) { + $childBar->archive('banners.archive')->listCheck(true); + } elseif ($this->state->get('filter.published') == 2) { + $childBar->publish('publish')->task('banners.publish')->listCheck(true); + } + } + + $childBar->checkin('banners.checkin')->listCheck(true); + + if ($this->state->get('filter.published') != -2) { + $childBar->trash('banners.trash')->listCheck(true); + } + } + + if ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('banners.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + // Add a batch button + if ( + $user->authorise('core.create', 'com_banners') + && $user->authorise('core.edit', 'com_banners') + && $user->authorise('core.edit.state', 'com_banners') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if ($user->authorise('core.admin', 'com_banners') || $user->authorise('core.options', 'com_banners')) { + $toolbar->preferences('com_banners'); + } + + $toolbar->help('Banners'); + } } diff --git a/administrator/components/com_banners/src/View/Client/HtmlView.php b/administrator/components/com_banners/src/View/Client/HtmlView.php index b7b820267800c..6ca293f8804da 100644 --- a/administrator/components/com_banners/src/View/Client/HtmlView.php +++ b/administrator/components/com_banners/src/View/Client/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->form = $model->getForm(); - $this->item = $model->getItem(); - $this->state = $model->getState(); - $this->canDo = ContentHelper::getActions('com_banners'); - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * - * @throws Exception - */ - protected function addToolbar(): void - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $isNew = ($this->item->id == 0); - $checkedOut = !(\is_null($this->item->checked_out) || $this->item->checked_out == $user->id); - $canDo = $this->canDo; - - ToolbarHelper::title( - $isNew ? Text::_('COM_BANNERS_MANAGER_CLIENT_NEW') : Text::_('COM_BANNERS_MANAGER_CLIENT_EDIT'), - 'bookmark banners-clients' - ); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if (!$checkedOut && ($canDo->get('core.edit') || $canDo->get('core.create'))) - { - ToolbarHelper::apply('client.apply'); - $toolbarButtons[] = ['save', 'client.save']; - } - - if (!$checkedOut && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'client.save2new']; - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'client.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('client.cancel'); - } - else - { - ToolbarHelper::cancel('client.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) - { - ToolbarHelper::versions('com_banners.client', $this->item->id); - } - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Banners:_New_or_Edit_Client'); - } + /** + * The Form object + * + * @var Form + * @since 1.5 + */ + protected $form; + + /** + * The active item + * + * @var CMSObject + * @since 1.5 + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + * @since 1.5 + */ + protected $state; + + /** + * Object containing permissions for the item + * + * @var CMSObject + * @since 1.5 + */ + protected $canDo; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.5 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var ClientModel $model */ + $model = $this->getModel(); + $this->form = $model->getForm(); + $this->item = $model->getItem(); + $this->state = $model->getState(); + $this->canDo = ContentHelper::getActions('com_banners'); + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + protected function addToolbar(): void + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $isNew = ($this->item->id == 0); + $checkedOut = !(\is_null($this->item->checked_out) || $this->item->checked_out == $user->id); + $canDo = $this->canDo; + + ToolbarHelper::title( + $isNew ? Text::_('COM_BANNERS_MANAGER_CLIENT_NEW') : Text::_('COM_BANNERS_MANAGER_CLIENT_EDIT'), + 'bookmark banners-clients' + ); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if (!$checkedOut && ($canDo->get('core.edit') || $canDo->get('core.create'))) { + ToolbarHelper::apply('client.apply'); + $toolbarButtons[] = ['save', 'client.save']; + } + + if (!$checkedOut && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'client.save2new']; + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'client.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('client.cancel'); + } else { + ToolbarHelper::cancel('client.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) { + ToolbarHelper::versions('com_banners.client', $this->item->id); + } + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Banners:_New_or_Edit_Client'); + } } diff --git a/administrator/components/com_banners/src/View/Clients/HtmlView.php b/administrator/components/com_banners/src/View/Clients/HtmlView.php index b18edbf38b771..c232f31d33379 100644 --- a/administrator/components/com_banners/src/View/Clients/HtmlView.php +++ b/administrator/components/com_banners/src/View/Clients/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->state = $model->getState(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_banners'); - - ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_CLIENTS'), 'bookmark banners-clients'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('client.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('clients.publish')->listCheck(true); - $childBar->unpublish('clients.unpublish')->listCheck(true); - $childBar->archive('clients.archive')->listCheck(true); - - if ($canDo->get('core.admin')) - { - $childBar->checkin('clients.checkin')->listCheck(true); - } - - if (!$this->state->get('filter.state') == -2) - { - $childBar->trash('clients.trash')->listCheck(true); - } - } - - if (!$this->isEmptyState && $this->state->get('filter.state') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('clients.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_banners'); - } - - $toolbar->help('Banners:_Clients'); - } + /** + * The search tools form + * + * @var Form + * @since 1.6 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 1.6 + */ + public $activeFilters = []; + + /** + * An array of items + * + * @var array + * @since 1.6 + */ + protected $items = []; + + /** + * The pagination object + * + * @var Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var ClientsModel $model */ + $model = $this->getModel(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->state = $model->getState(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_banners'); + + ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_CLIENTS'), 'bookmark banners-clients'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('client.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('clients.publish')->listCheck(true); + $childBar->unpublish('clients.unpublish')->listCheck(true); + $childBar->archive('clients.archive')->listCheck(true); + + if ($canDo->get('core.admin')) { + $childBar->checkin('clients.checkin')->listCheck(true); + } + + if (!$this->state->get('filter.state') == -2) { + $childBar->trash('clients.trash')->listCheck(true); + } + } + + if (!$this->isEmptyState && $this->state->get('filter.state') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('clients.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_banners'); + } + + $toolbar->help('Banners:_Clients'); + } } diff --git a/administrator/components/com_banners/src/View/Download/HtmlView.php b/administrator/components/com_banners/src/View/Download/HtmlView.php index 6307ee8d06992..0f2176815791f 100644 --- a/administrator/components/com_banners/src/View/Download/HtmlView.php +++ b/administrator/components/com_banners/src/View/Download/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->form = $model->getForm(); - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - parent::display($tpl); - } + /** + * The Form object + * + * @var Form + * @since 1.6 + */ + protected $form; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var DownloadModel $model */ + $model = $this->getModel(); + $this->form = $model->getForm(); + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + parent::display($tpl); + } } diff --git a/administrator/components/com_banners/src/View/Tracks/HtmlView.php b/administrator/components/com_banners/src/View/Tracks/HtmlView.php index 5931f49a3dfc5..0d03bd76e8a36 100644 --- a/administrator/components/com_banners/src/View/Tracks/HtmlView.php +++ b/administrator/components/com_banners/src/View/Tracks/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->state = $model->getState(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_banners', 'category', $this->state->get('filter.category_id')); - - ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_TRACKS'), 'bookmark banners-tracks'); - - $bar = Toolbar::getInstance('toolbar'); - - if (!$this->isEmptyState) - { - $bar->popupButton() - ->url(Route::_('index.php?option=com_banners&view=download&tmpl=component')) - ->text('JTOOLBAR_EXPORT') - ->selector('downloadModal') - ->icon('icon-download') - ->footer('' - . '' - ); - } - - if (!$this->isEmptyState && $canDo->get('core.delete')) - { - $bar->appendButton('Confirm', 'COM_BANNERS_DELETE_MSG', 'delete', 'COM_BANNERS_TRACKS_DELETE', 'tracks.delete', false); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_banners'); - } - - ToolbarHelper::help('Banners:_Tracks'); - } + /** + * The search tools form + * + * @var Form + * @since 1.6 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 1.6 + */ + public $activeFilters = []; + + /** + * An array of items + * + * @var array + * @since 1.6 + */ + protected $items = []; + + /** + * The pagination object + * + * @var Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var TracksModel $model */ + $model = $this->getModel(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->state = $model->getState(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_banners', 'category', $this->state->get('filter.category_id')); + + ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_TRACKS'), 'bookmark banners-tracks'); + + $bar = Toolbar::getInstance('toolbar'); + + if (!$this->isEmptyState) { + $bar->popupButton() + ->url(Route::_('index.php?option=com_banners&view=download&tmpl=component')) + ->text('JTOOLBAR_EXPORT') + ->selector('downloadModal') + ->icon('icon-download') + ->footer('' + . ''); + } + + if (!$this->isEmptyState && $canDo->get('core.delete')) { + $bar->appendButton('Confirm', 'COM_BANNERS_DELETE_MSG', 'delete', 'COM_BANNERS_TRACKS_DELETE', 'tracks.delete', false); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_banners'); + } + + ToolbarHelper::help('Banners:_Tracks'); + } } diff --git a/administrator/components/com_banners/src/View/Tracks/RawView.php b/administrator/components/com_banners/src/View/Tracks/RawView.php index 17470ab692f79..03ee8e4b7e1a4 100644 --- a/administrator/components/com_banners/src/View/Tracks/RawView.php +++ b/administrator/components/com_banners/src/View/Tracks/RawView.php @@ -1,4 +1,5 @@ getModel(); - $basename = $model->getBaseName(); - $fileType = $model->getFileType(); - $mimeType = $model->getMimeType(); - $content = $model->getContent(); + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var TracksModel $model */ + $model = $this->getModel(); + $basename = $model->getBaseName(); + $fileType = $model->getFileType(); + $mimeType = $model->getMimeType(); + $content = $model->getContent(); - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - $this->document->setMimeEncoding($mimeType); + $this->document->setMimeEncoding($mimeType); - /** @var CMSApplication $app */ - $app = Factory::getApplication(); - $app->setHeader( - 'Content-disposition', - 'attachment; filename="' . $basename . '.' . $fileType . '"; creation-date="' . Factory::getDate()->toRFC822() . '"', - true - ); - echo $content; - } + /** @var CMSApplication $app */ + $app = Factory::getApplication(); + $app->setHeader( + 'Content-disposition', + 'attachment; filename="' . $basename . '.' . $fileType . '"; creation-date="' . Factory::getDate()->toRFC822() . '"', + true + ); + echo $content; + } } diff --git a/administrator/components/com_banners/tmpl/banner/edit.php b/administrator/components/com_banners/tmpl/banner/edit.php index 3320b3121114b..d6fe9c69f9a88 100644 --- a/administrator/components/com_banners/tmpl/banner/edit.php +++ b/administrator/components/com_banners/tmpl/banner/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_banners.admin-banner-edit'); + ->useScript('form.validate') + ->useScript('com_banners.admin-banner-edit'); ?> diff --git a/administrator/components/com_banners/tmpl/banners/default.php b/administrator/components/com_banners/tmpl/banners/default.php index 22ec2c5d1310e..1f8a3d6fdd804 100644 --- a/administrator/components/com_banners/tmpl/banners/default.php +++ b/administrator/components/com_banners/tmpl/banners/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $userId = $user->get('id'); @@ -30,173 +31,173 @@ $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'a.ordering'; -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_banners&task=banners.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_banners&task=banners.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
    -
    -
    -
    - $this]); - ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : - $ordering = ($listOrder == 'ordering'); - $item->cat_link = Route::_('index.php?option=com_categories&extension=com_banners&task=edit&type=other&cid[]=' . $item->catid); - $canCreate = $user->authorise('core.create', 'com_banners.category.' . $item->catid); - $canEdit = $user->authorise('core.edit', 'com_banners.category.' . $item->catid); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_banners.category.' . $item->catid) && $canCheckin; - ?> - - - + + + + + + + + + + + + + +
    - , - , - -
    - - - - - - - - - - - - - - - - - - - -
    - id, false, 'cid', 'cb', $item->name); ?> - - +
    +
    + $this]); + ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : + $ordering = ($listOrder == 'ordering'); + $item->cat_link = Route::_('index.php?option=com_categories&extension=com_banners&task=edit&type=other&cid[]=' . $item->catid); + $canCreate = $user->authorise('core.create', 'com_banners.category.' . $item->catid); + $canEdit = $user->authorise('core.edit', 'com_banners.category.' . $item->catid); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_banners.category.' . $item->catid) && $canCheckin; + ?> + + + - - - - - - - - - - - - - -
    + , + , + +
    + + + + + + + + + + + + + + + + + + + +
    + id, false, 'cid', 'cb', $item->name); ?> + + - - - - - - - - state, $i, 'banners.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> - -
    - checked_out) : ?> - editor, $item->checked_out_time, 'banners.', $canCheckin); ?> - - - - escape($item->name); ?> - - escape($item->name); ?> - -
    - escape($item->alias)); ?> -
    -
    - escape($item->category_title); ?> -
    -
    -
    - sticky, $i, $canChange); ?> - - client_name; ?> - - impmade, $item->imptotal ?: Text::_('COM_BANNERS_UNLIMITED')); ?> - - clicks; ?> - - impmade ? 100 * $item->clicks / $item->impmade : 0); ?> - - - - id; ?> -
    + if (!$canChange) { + $iconClass = ' inactive'; + } elseif (!$saveOrder) { + $iconClass = ' inactive" title="' . Text::_('JORDERINGDISABLED'); + } + ?> + + + + + + +
    + state, $i, 'banners.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + +
    + checked_out) : ?> + editor, $item->checked_out_time, 'banners.', $canCheckin); ?> + + + + escape($item->name); ?> + + escape($item->name); ?> + +
    + escape($item->alias)); ?> +
    +
    + escape($item->category_title); ?> +
    +
    +
    + sticky, $i, $canChange); ?> + + client_name; ?> + + impmade, $item->imptotal ?: Text::_('COM_BANNERS_UNLIMITED')); ?> + + clicks; ?> - + impmade ? 100 * $item->clicks / $item->impmade : 0); ?> + + + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_banners') - && $user->authorise('core.edit', 'com_banners') - && $user->authorise('core.edit.state', 'com_banners')) : ?> - Text::_('COM_BANNERS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer') - ], - $this->loadTemplate('batch_body') - ); ?> - - + + authorise('core.create', 'com_banners') + && $user->authorise('core.edit', 'com_banners') + && $user->authorise('core.edit.state', 'com_banners') +) : ?> + Text::_('COM_BANNERS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer') + ], + $this->loadTemplate('batch_body') + ); ?> + + - - - -
    -
    -
    + + + + + +
    diff --git a/administrator/components/com_banners/tmpl/banners/default_batch_body.php b/administrator/components/com_banners/tmpl/banners/default_batch_body.php index f53c0395e2d66..fbc990db6f439 100644 --- a/administrator/components/com_banners/tmpl/banners/default_batch_body.php +++ b/administrator/components/com_banners/tmpl/banners/default_batch_body.php @@ -1,4 +1,5 @@ -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    - = 0) : ?> -
    -
    - 'com_banners']); ?> -
    -
    - -
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + = 0) : ?> +
    +
    + 'com_banners']); ?> +
    +
    + +
    diff --git a/administrator/components/com_banners/tmpl/banners/default_batch_footer.php b/administrator/components/com_banners/tmpl/banners/default_batch_footer.php index 723541c31226c..d1282ec1246fe 100644 --- a/administrator/components/com_banners/tmpl/banners/default_batch_footer.php +++ b/administrator/components/com_banners/tmpl/banners/default_batch_footer.php @@ -1,4 +1,5 @@ diff --git a/administrator/components/com_banners/tmpl/banners/emptystate.php b/administrator/components/com_banners/tmpl/banners/emptystate.php index ec83587c839c2..b4b0db976c3a1 100644 --- a/administrator/components/com_banners/tmpl/banners/emptystate.php +++ b/administrator/components/com_banners/tmpl/banners/emptystate.php @@ -1,4 +1,5 @@ 'COM_BANNERS', - 'formURL' => 'index.php?option=com_banners&view=banners', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners', - 'icon' => 'icon-bookmark banners', + 'textPrefix' => 'COM_BANNERS', + 'formURL' => 'index.php?option=com_banners&view=banners', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners', + 'icon' => 'icon-bookmark banners', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_banners') || count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_banners&task=banner.add'; +if ($user->authorise('core.create', 'com_banners') || count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_banners&task=banner.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_banners/tmpl/client/edit.php b/administrator/components/com_banners/tmpl/client/edit.php index 9b8466a530183..bd0b60e087fb0 100644 --- a/administrator/components/com_banners/tmpl/client/edit.php +++ b/administrator/components/com_banners/tmpl/client/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
    - + -
    - 'general', 'recall' => true, 'breakpoint' => 768]); ?> +
    + 'general', 'recall' => true, 'breakpoint' => 768]); ?> - item->id) ? Text::_('COM_BANNERS_NEW_CLIENT') : Text::_('COM_BANNERS_EDIT_CLIENT')); ?> -
    -
    - form->renderField('contact'); - echo $this->form->renderField('email'); - echo $this->form->renderField('purchase_type'); - echo $this->form->renderField('track_impressions'); - echo $this->form->renderField('track_clicks'); - echo $this->form->renderFieldset('extra'); - ?> -
    -
    - -
    -
    - + item->id) ? Text::_('COM_BANNERS_NEW_CLIENT') : Text::_('COM_BANNERS_EDIT_CLIENT')); ?> +
    +
    + form->renderField('contact'); + echo $this->form->renderField('email'); + echo $this->form->renderField('purchase_type'); + echo $this->form->renderField('track_impressions'); + echo $this->form->renderField('track_clicks'); + echo $this->form->renderFieldset('extra'); + ?> +
    +
    + +
    +
    + - -
    -
    -
    - -
    - form->renderFieldset('metadata'); ?> -
    -
    -
    -
    - + +
    +
    +
    + +
    + form->renderFieldset('metadata'); ?> +
    +
    +
    +
    + - -
    + +
    - - + +
    diff --git a/administrator/components/com_banners/tmpl/clients/default.php b/administrator/components/com_banners/tmpl/clients/default.php index 31da558bd75b3..8db12d77ee7f3 100644 --- a/administrator/components/com_banners/tmpl/clients/default.php +++ b/administrator/components/com_banners/tmpl/clients/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $purchaseTypes = [ - '1' => 'UNLIMITED', - '2' => 'YEARLY', - '3' => 'MONTHLY', - '4' => 'WEEKLY', - '5' => 'DAILY', + '1' => 'UNLIMITED', + '2' => 'YEARLY', + '3' => 'MONTHLY', + '4' => 'WEEKLY', + '5' => 'DAILY', ]; $user = Factory::getUser(); $userId = $user->get('id'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); -$params = $this->state->params ?? new CMSObject; +$params = $this->state->params ?? new CMSObject(); ?>
    -
    -
    -
    - $this]); - ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - - items as $i => $item) : - $canCreate = $user->authorise('core.create', 'com_banners'); - $canEdit = $user->authorise('core.edit', 'com_banners'); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_banners') && $canCheckin; - ?> - - - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - - - - - - - - - - - -
    - id, false, 'cid', 'cb', $item->name); ?> - - state, $i, 'clients.', $canChange); ?> - -
    - checked_out) : ?> - editor, $item->checked_out_time, 'clients.', $canCheckin); ?> - - - - escape($item->name); ?> - - escape($item->name); ?> - -
    -
    - contact; ?> - - - count_published; ?> - - - - - count_unpublished; ?> - - - - - count_archived; ?> - - - - - count_trashed; ?> - - - - purchase_type < 0) : ?> - get('purchase_type')])); ?> - - purchase_type]); ?> - - - id; ?> -
    +
    +
    +
    + $this]); + ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + + + items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_banners'); + $canEdit = $user->authorise('core.edit', 'com_banners'); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_banners') && $canCheckin; + ?> + + + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + id, false, 'cid', 'cb', $item->name); ?> + + state, $i, 'clients.', $canChange); ?> + +
    + checked_out) : ?> + editor, $item->checked_out_time, 'clients.', $canCheckin); ?> + + + + escape($item->name); ?> + + escape($item->name); ?> + +
    +
    + contact; ?> + + + count_published; ?> + + + + + count_unpublished; ?> + + + + + count_archived; ?> + + + + + count_trashed; ?> + + + + purchase_type < 0) : ?> + get('purchase_type')])); ?> + + purchase_type]); ?> + + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
    -
    -
    + + + +
    +
    +
    diff --git a/administrator/components/com_banners/tmpl/clients/emptystate.php b/administrator/components/com_banners/tmpl/clients/emptystate.php index 1feedff923aac..c7e79091d9a11 100644 --- a/administrator/components/com_banners/tmpl/clients/emptystate.php +++ b/administrator/components/com_banners/tmpl/clients/emptystate.php @@ -1,4 +1,5 @@ 'COM_BANNERS_CLIENT', - 'formURL' => 'index.php?option=com_banners&view=clients', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners:_Clients', - 'icon' => 'icon-bookmark banners', + 'textPrefix' => 'COM_BANNERS_CLIENT', + 'formURL' => 'index.php?option=com_banners&view=clients', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners:_Clients', + 'icon' => 'icon-bookmark banners', ]; -if (count(Factory::getApplication()->getIdentity()->getAuthorisedCategories('com_banners', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_banners&task=client.add'; +if (count(Factory::getApplication()->getIdentity()->getAuthorisedCategories('com_banners', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_banners&task=client.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_banners/tmpl/download/default.php b/administrator/components/com_banners/tmpl/download/default.php index dc433005a22b7..dacd8035fa53b 100644 --- a/administrator/components/com_banners/tmpl/download/default.php +++ b/administrator/components/com_banners/tmpl/download/default.php @@ -1,4 +1,5 @@
    -
    - - form->getFieldset() as $field) : ?> - form->renderField($field->fieldname); ?> - - - -
    +
    + + form->getFieldset() as $field) : ?> + form->renderField($field->fieldname); ?> + + + +
    diff --git a/administrator/components/com_banners/tmpl/tracks/default.php b/administrator/components/com_banners/tmpl/tracks/default.php index 0e0113ec16c77..04ff5e1f2a0be 100644 --- a/administrator/components/com_banners/tmpl/tracks/default.php +++ b/administrator/components/com_banners/tmpl/tracks/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
    -
    -
    -
    - $this]); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - items as $i => $item) : ?> - - - - - - - - - -
    - , - , - -
    - - - - - - - - - -
    - banner_name; ?> -
    - escape($item->category_title); ?> -
    -
    - client_name; ?> - - track_type == 1 ? Text::_('COM_BANNERS_IMPRESSION') : Text::_('COM_BANNERS_CLICK'); ?> - - count; ?> - - track_date, Text::_('DATE_FORMAT_LC5')); ?> -
    +
    +
    +
    + $this]); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + + +
    + , + , + +
    + + + + + + + + + +
    + banner_name; ?> +
    + escape($item->category_title); ?> +
    +
    + client_name; ?> + + track_type == 1 ? Text::_('COM_BANNERS_IMPRESSION') : Text::_('COM_BANNERS_CLICK'); ?> + + count; ?> + + track_date, Text::_('DATE_FORMAT_LC5')); ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
    -
    -
    + + + + +
    +
    +
    diff --git a/administrator/components/com_banners/tmpl/tracks/emptystate.php b/administrator/components/com_banners/tmpl/tracks/emptystate.php index 4ad37d7cafcab..0fe1cd2f152d7 100644 --- a/administrator/components/com_banners/tmpl/tracks/emptystate.php +++ b/administrator/components/com_banners/tmpl/tracks/emptystate.php @@ -1,4 +1,5 @@ 'COM_BANNERS_TRACKS', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners:_Tracks', - 'icon' => 'icon-bookmark banners', + 'textPrefix' => 'COM_BANNERS_TRACKS', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners:_Tracks', + 'icon' => 'icon-bookmark banners', ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_cache/services/provider.php b/administrator/components/com_cache/services/provider.php index 09e3a1e2c42e0..01ca10a33e748 100644 --- a/administrator/components/com_cache/services/provider.php +++ b/administrator/components/com_cache/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Cache')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Cache')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Cache')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Cache')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_cache/src/Controller/DisplayController.php b/administrator/components/com_cache/src/Controller/DisplayController.php index ccbdcc9698348..e2e6603e9d44e 100644 --- a/administrator/components/com_cache/src/Controller/DisplayController.php +++ b/administrator/components/com_cache/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ getModel('Cache'); - - $data = $model->getData(); - - $size = 0; - - if (!empty($data)) - { - foreach ($data as $d) - { - $size += $d->size; - } - } - - // Number bytes are returned in format xxx.xx MB - $bytes = HTMLHelper::_('number.bytes', $size, 'MB', 1); - - if (!empty($bytes)) - { - $result['amount'] = $bytes; - $result['sronly'] = Text::sprintf('COM_CACHE_QUICKICON_SRONLY', $bytes); - } - else - { - $result['amount'] = 0; - $result['sronly'] = Text::sprintf('COM_CACHE_QUICKICON_SRONLY_NOCACHE'); - } - - echo new JsonResponse($result); - } - - /** - * Method to delete a list of cache groups. - * - * @return void - */ - public function delete() - { - // Check for request forgeries - $this->checkToken(); - - $cid = (array) $this->input->post->get('cid', array(), 'string'); - - if (empty($cid)) - { - $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'warning'); - } - else - { - $result = $this->getModel('cache')->cleanlist($cid); - - if ($result !== array()) - { - $this->app->enqueueMessage(Text::sprintf('COM_CACHE_EXPIRED_ITEMS_DELETE_ERROR', implode(', ', $result)), 'error'); - } - else - { - $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_HAVE_BEEN_DELETED'), 'message'); - } - } - - $this->setRedirect('index.php?option=com_cache'); - } - - /** - * Method to delete all cache groups. - * - * @return void - * - * @since 3.6.0 - */ - public function deleteAll() - { - // Check for request forgeries - $this->checkToken(); - - /** @var \Joomla\Component\Cache\Administrator\Model\CacheModel $model */ - $model = $this->getModel('cache'); - $allCleared = true; - - $mCache = $model->getCache(); - - foreach ($mCache->getAll() as $cache) - { - if ($mCache->clean($cache->group) === false) - { - $this->app->enqueueMessage( - Text::sprintf( - 'COM_CACHE_EXPIRED_ITEMS_DELETE_ERROR', Text::_('JADMINISTRATOR') . ' > ' . $cache->group - ), 'error' - ); - $allCleared = false; - } - } - - if ($allCleared) - { - $this->app->enqueueMessage(Text::_('COM_CACHE_MSG_ALL_CACHE_GROUPS_CLEARED'), 'message'); - } - else - { - $this->app->enqueueMessage(Text::_('COM_CACHE_MSG_SOME_CACHE_GROUPS_CLEARED'), 'warning'); - } - - $this->app->triggerEvent('onAfterPurge', array()); - $this->setRedirect('index.php?option=com_cache&view=cache'); - } - - /** - * Purge the cache. - * - * @return void - */ - public function purge() - { - // Check for request forgeries - $this->checkToken(); - - if (!$this->getModel('cache')->purge()) - { - $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_PURGING_ERROR'), 'error'); - } - else - { - $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_HAVE_BEEN_PURGED'), 'message'); - } - - $this->setRedirect('index.php?option=com_cache&view=cache'); - } + /** + * The default view for the display method. + * + * @var string + * @since 4.0.0 + */ + protected $default_view = 'cache'; + + /** + * Method to get The Cache Size + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Cache'); + + $data = $model->getData(); + + $size = 0; + + if (!empty($data)) { + foreach ($data as $d) { + $size += $d->size; + } + } + + // Number bytes are returned in format xxx.xx MB + $bytes = HTMLHelper::_('number.bytes', $size, 'MB', 1); + + if (!empty($bytes)) { + $result['amount'] = $bytes; + $result['sronly'] = Text::sprintf('COM_CACHE_QUICKICON_SRONLY', $bytes); + } else { + $result['amount'] = 0; + $result['sronly'] = Text::sprintf('COM_CACHE_QUICKICON_SRONLY_NOCACHE'); + } + + echo new JsonResponse($result); + } + + /** + * Method to delete a list of cache groups. + * + * @return void + */ + public function delete() + { + // Check for request forgeries + $this->checkToken(); + + $cid = (array) $this->input->post->get('cid', array(), 'string'); + + if (empty($cid)) { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'warning'); + } else { + $result = $this->getModel('cache')->cleanlist($cid); + + if ($result !== array()) { + $this->app->enqueueMessage(Text::sprintf('COM_CACHE_EXPIRED_ITEMS_DELETE_ERROR', implode(', ', $result)), 'error'); + } else { + $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_HAVE_BEEN_DELETED'), 'message'); + } + } + + $this->setRedirect('index.php?option=com_cache'); + } + + /** + * Method to delete all cache groups. + * + * @return void + * + * @since 3.6.0 + */ + public function deleteAll() + { + // Check for request forgeries + $this->checkToken(); + + /** @var \Joomla\Component\Cache\Administrator\Model\CacheModel $model */ + $model = $this->getModel('cache'); + $allCleared = true; + + $mCache = $model->getCache(); + + foreach ($mCache->getAll() as $cache) { + if ($mCache->clean($cache->group) === false) { + $this->app->enqueueMessage( + Text::sprintf( + 'COM_CACHE_EXPIRED_ITEMS_DELETE_ERROR', + Text::_('JADMINISTRATOR') . ' > ' . $cache->group + ), + 'error' + ); + $allCleared = false; + } + } + + if ($allCleared) { + $this->app->enqueueMessage(Text::_('COM_CACHE_MSG_ALL_CACHE_GROUPS_CLEARED'), 'message'); + } else { + $this->app->enqueueMessage(Text::_('COM_CACHE_MSG_SOME_CACHE_GROUPS_CLEARED'), 'warning'); + } + + $this->app->triggerEvent('onAfterPurge', array()); + $this->setRedirect('index.php?option=com_cache&view=cache'); + } + + /** + * Purge the cache. + * + * @return void + */ + public function purge() + { + // Check for request forgeries + $this->checkToken(); + + if (!$this->getModel('cache')->purge()) { + $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_PURGING_ERROR'), 'error'); + } else { + $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_HAVE_BEEN_PURGED'), 'message'); + } + + $this->setRedirect('index.php?option=com_cache&view=cache'); + } } diff --git a/administrator/components/com_cache/src/Model/CacheModel.php b/administrator/components/com_cache/src/Model/CacheModel.php index cdbcebf587dec..783271317335b 100644 --- a/administrator/components/com_cache/src/Model/CacheModel.php +++ b/administrator/components/com_cache/src/Model/CacheModel.php @@ -1,4 +1,5 @@ setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 3.5 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Method to get cache data - * - * @return array - */ - public function getData() - { - if (empty($this->_data)) - { - try - { - $cache = $this->getCache(); - $data = $cache->getAll(); - - if ($data && \count($data) > 0) - { - // Process filter by search term. - if ($search = $this->getState('filter.search')) - { - foreach ($data as $key => $cacheItem) - { - if (stripos($cacheItem->group, $search) === false) - { - unset($data[$key]); - } - } - } - - // Process ordering. - $listOrder = $this->getState('list.ordering', 'group'); - $listDirn = $this->getState('list.direction', 'ASC'); - - $this->_data = ArrayHelper::sortObjects($data, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); - - // Process pagination. - $limit = (int) $this->getState('list.limit', 25); - - if ($limit !== 0) - { - $start = (int) $this->getState('list.start', 0); - - return \array_slice($this->_data, $start, $limit); - } - } - else - { - $this->_data = array(); - } - } - catch (CacheConnectingException $exception) - { - $this->setError(Text::_('COM_CACHE_ERROR_CACHE_CONNECTION_FAILED')); - $this->_data = array(); - } - catch (UnsupportedCacheException $exception) - { - $this->setError(Text::_('COM_CACHE_ERROR_CACHE_DRIVER_UNSUPPORTED')); - $this->_data = array(); - } - } - - return $this->_data; - } - - /** - * Method to get cache instance. - * - * @return CacheController - */ - public function getCache() - { - $app = Factory::getApplication(); - - $options = array( - 'defaultgroup' => '', - 'storage' => $app->get('cache_handler', ''), - 'caching' => true, - 'cachebase' => $app->get('cache_path', JPATH_CACHE) - ); - - return Cache::getInstance('', $options); - } - - /** - * Get the number of current Cache Groups. - * - * @return integer - */ - public function getTotal() - { - if (empty($this->_total)) - { - $this->_total = count($this->getData()); - } - - return $this->_total; - } - - /** - * Method to get a pagination object for the cache. - * - * @return Pagination - */ - public function getPagination() - { - if (empty($this->_pagination)) - { - $this->_pagination = new Pagination($this->getTotal(), $this->getState('list.start'), $this->getState('list.limit')); - } - - return $this->_pagination; - } - - /** - * Clean out a cache group as named by param. - * If no param is passed clean all cache groups. - * - * @param string $group Cache group name. - * - * @return boolean True on success, false otherwise - */ - public function clean($group = '') - { - try - { - $this->getCache()->clean($group); - } - catch (CacheConnectingException $exception) - { - return false; - } - catch (UnsupportedCacheException $exception) - { - return false; - } - - Factory::getApplication()->triggerEvent('onAfterPurge', array($group)); - - return true; - } - - /** - * Purge an array of cache groups. - * - * @param array $array Array of cache group names. - * - * @return array Array with errors, if they exist. - */ - public function cleanlist($array) - { - $errors = array(); - - foreach ($array as $group) - { - if (!$this->clean($group)) - { - $errors[] = $group; - } - } - - return $errors; - } - - /** - * Purge all cache items. - * - * @return boolean True if successful; false otherwise. - */ - public function purge() - { - try - { - Factory::getCache('')->gc(); - } - catch (CacheConnectingException $exception) - { - return false; - } - catch (UnsupportedCacheException $exception) - { - return false; - } - - Factory::getApplication()->triggerEvent('onAfterPurge', array()); - - return true; - } + /** + * An Array of CacheItems indexed by cache group ID + * + * @var array + */ + protected $_data = array(); + + /** + * Group total + * + * @var integer + */ + protected $_total = null; + + /** + * Pagination object + * + * @var object + */ + protected $_pagination = null; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 3.5 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'group', + 'count', + 'size', + 'client_id', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering Field for ordering. + * @param string $direction Direction of ordering. + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'group', $direction = 'asc') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 3.5 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Method to get cache data + * + * @return array + */ + public function getData() + { + if (empty($this->_data)) { + try { + $cache = $this->getCache(); + $data = $cache->getAll(); + + if ($data && \count($data) > 0) { + // Process filter by search term. + if ($search = $this->getState('filter.search')) { + foreach ($data as $key => $cacheItem) { + if (stripos($cacheItem->group, $search) === false) { + unset($data[$key]); + } + } + } + + // Process ordering. + $listOrder = $this->getState('list.ordering', 'group'); + $listDirn = $this->getState('list.direction', 'ASC'); + + $this->_data = ArrayHelper::sortObjects($data, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); + + // Process pagination. + $limit = (int) $this->getState('list.limit', 25); + + if ($limit !== 0) { + $start = (int) $this->getState('list.start', 0); + + return \array_slice($this->_data, $start, $limit); + } + } else { + $this->_data = array(); + } + } catch (CacheConnectingException $exception) { + $this->setError(Text::_('COM_CACHE_ERROR_CACHE_CONNECTION_FAILED')); + $this->_data = array(); + } catch (UnsupportedCacheException $exception) { + $this->setError(Text::_('COM_CACHE_ERROR_CACHE_DRIVER_UNSUPPORTED')); + $this->_data = array(); + } + } + + return $this->_data; + } + + /** + * Method to get cache instance. + * + * @return CacheController + */ + public function getCache() + { + $app = Factory::getApplication(); + + $options = array( + 'defaultgroup' => '', + 'storage' => $app->get('cache_handler', ''), + 'caching' => true, + 'cachebase' => $app->get('cache_path', JPATH_CACHE) + ); + + return Cache::getInstance('', $options); + } + + /** + * Get the number of current Cache Groups. + * + * @return integer + */ + public function getTotal() + { + if (empty($this->_total)) { + $this->_total = count($this->getData()); + } + + return $this->_total; + } + + /** + * Method to get a pagination object for the cache. + * + * @return Pagination + */ + public function getPagination() + { + if (empty($this->_pagination)) { + $this->_pagination = new Pagination($this->getTotal(), $this->getState('list.start'), $this->getState('list.limit')); + } + + return $this->_pagination; + } + + /** + * Clean out a cache group as named by param. + * If no param is passed clean all cache groups. + * + * @param string $group Cache group name. + * + * @return boolean True on success, false otherwise + */ + public function clean($group = '') + { + try { + $this->getCache()->clean($group); + } catch (CacheConnectingException $exception) { + return false; + } catch (UnsupportedCacheException $exception) { + return false; + } + + Factory::getApplication()->triggerEvent('onAfterPurge', array($group)); + + return true; + } + + /** + * Purge an array of cache groups. + * + * @param array $array Array of cache group names. + * + * @return array Array with errors, if they exist. + */ + public function cleanlist($array) + { + $errors = array(); + + foreach ($array as $group) { + if (!$this->clean($group)) { + $errors[] = $group; + } + } + + return $errors; + } + + /** + * Purge all cache items. + * + * @return boolean True if successful; false otherwise. + */ + public function purge() + { + try { + Factory::getCache('')->gc(); + } catch (CacheConnectingException $exception) { + return false; + } catch (UnsupportedCacheException $exception) { + return false; + } + + Factory::getApplication()->triggerEvent('onAfterPurge', array()); + + return true; + } } diff --git a/administrator/components/com_categories/helpers/categories.php b/administrator/components/com_categories/helpers/categories.php index b3caa9bf86cd5..18f783389c226 100644 --- a/administrator/components/com_categories/helpers/categories.php +++ b/administrator/components/com_categories/helpers/categories.php @@ -1,4 +1,5 @@ value as array - if ($multiple && is_array($value)) - { - if (!count($value)) - { - $value[] = ''; - } - - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } -} -else -{ - // Create a regular list. - if (count($options) === 0) - { - // All Categories have been deleted, so we need a new category (This will create on save if selected). - $options[0] = new \stdClass; - $options[0]->value = 'Uncategorised'; - $options[0]->text = 'Uncategorised'; - $options[0]->level = '1'; - $options[0]->published = '1'; - $options[0]->lft = '1'; - } - - $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); +if ($readonly) { + $html[] = HTMLHelper::_('select.genericlist', $options, '', trim($attr), 'value', 'text', $value, $id); + + // E.g. form field type tag sends $this->value as array + if ($multiple && is_array($value)) { + if (!count($value)) { + $value[] = ''; + } + + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } +} else { + // Create a regular list. + if (count($options) === 0) { + // All Categories have been deleted, so we need a new category (This will create on save if selected). + $options[0] = new \stdClass(); + $options[0]->value = 'Uncategorised'; + $options[0]->text = 'Uncategorised'; + $options[0]->level = '1'; + $options[0]->published = '1'; + $options[0]->lft = '1'; + } + + $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); } -if ($refreshPage === true) -{ - $attr2 .= ' data-refresh-catid="' . $refreshCatId . '" data-refresh-section="' . $refreshSection . '"'; - $attr2 .= ' onchange="Joomla.categoryHasChanged(this)"'; +if ($refreshPage === true) { + $attr2 .= ' data-refresh-catid="' . $refreshCatId . '" data-refresh-section="' . $refreshSection . '"'; + $attr2 .= ' onchange="Joomla.categoryHasChanged(this)"'; - Factory::getDocument()->getWebAssetManager() - ->registerAndUseScript('field.category-change', 'layouts/joomla/form/field/category-change.min.js', [], ['defer' => true], ['core']) - ->useScript('webcomponent.core-loader'); + Factory::getDocument()->getWebAssetManager() + ->registerAndUseScript('field.category-change', 'layouts/joomla/form/field/category-change.min.js', [], ['defer' => true], ['core']) + ->useScript('webcomponent.core-loader'); - // Pass the element id to the javascript - Factory::getDocument()->addScriptOptions('category-change', $id); -} -else -{ - $attr2 .= $onchange ? ' onchange="' . $onchange . '"' : ''; + // Pass the element id to the javascript + Factory::getDocument()->addScriptOptions('category-change', $id); +} else { + $attr2 .= $onchange ? ' onchange="' . $onchange . '"' : ''; } Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getDocument()->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select'); ?> > diff --git a/administrator/components/com_categories/services/provider.php b/administrator/components/com_categories/services/provider.php index 2ef22e136fe12..043c4850d6425 100644 --- a/administrator/components/com_categories/services/provider.php +++ b/administrator/components/com_categories/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Categories')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Categories')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Categories')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Categories')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new CategoriesComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new CategoriesComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_categories/src/Controller/AjaxController.php b/administrator/components/com_categories/src/Controller/AjaxController.php index f0bdbe8e9b767..48417ffcf96b8 100644 --- a/administrator/components/com_categories/src/Controller/AjaxController.php +++ b/administrator/components/com_categories/src/Controller/AjaxController.php @@ -1,4 +1,5 @@ input->get('extension'); - - $assocId = $this->input->getInt('assocId', 0); - - if ($assocId == 0) - { - echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); - - return; - } - - $excludeLang = $this->input->get('excludeLang', '', 'STRING'); - - $associations = Associations::getAssociations($extension, '#__categories', 'com_categories.item', (int) $assocId, 'id', 'alias', ''); - - unset($associations[$excludeLang]); - - // Add the title to each of the associated records - Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_categories/tables'); - $categoryTable = Table::getInstance('Category', 'JTable'); - - foreach ($associations as $lang => $association) - { - $categoryTable->load($association->id); - $associations[$lang]->title = $categoryTable->title; - } - - $countContentLanguages = \count(LanguageHelper::getContentLanguages(array(0, 1), false)); - - if (\count($associations) == 0) - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); - } - elseif ($countContentLanguages > \count($associations) + 2) - { - $tags = implode(', ', array_keys($associations)); - $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); - } - else - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); - } - - echo new JsonResponse($associations, $message); - } - } + /** + * Method to fetch associations of a category + * + * The method assumes that the following http parameters are passed in an Ajax Get request: + * token: the form token + * assocId: the id of the category whose associations are to be returned + * excludeLang: the association for this language is to be excluded + * + * @return void + * + * @since 3.9.0 + */ + public function fetchAssociations() + { + if (!Session::checkToken('get')) { + echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true); + } else { + $extension = $this->input->get('extension'); + + $assocId = $this->input->getInt('assocId', 0); + + if ($assocId == 0) { + echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); + + return; + } + + $excludeLang = $this->input->get('excludeLang', '', 'STRING'); + + $associations = Associations::getAssociations($extension, '#__categories', 'com_categories.item', (int) $assocId, 'id', 'alias', ''); + + unset($associations[$excludeLang]); + + // Add the title to each of the associated records + Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_categories/tables'); + $categoryTable = Table::getInstance('Category', 'JTable'); + + foreach ($associations as $lang => $association) { + $categoryTable->load($association->id); + $associations[$lang]->title = $categoryTable->title; + } + + $countContentLanguages = \count(LanguageHelper::getContentLanguages(array(0, 1), false)); + + if (\count($associations) == 0) { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); + } elseif ($countContentLanguages > \count($associations) + 2) { + $tags = implode(', ', array_keys($associations)); + $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); + } else { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); + } + + echo new JsonResponse($associations, $message); + } + } } diff --git a/administrator/components/com_categories/src/Controller/CategoriesController.php b/administrator/components/com_categories/src/Controller/CategoriesController.php index ad46433211073..a6ff1d987d484 100644 --- a/administrator/components/com_categories/src/Controller/CategoriesController.php +++ b/administrator/components/com_categories/src/Controller/CategoriesController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Outputs the JSON-encoded amount of published content categories - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('Categories'); - $model->setState('filter.published', 1); - $model->setState('filter.extension', 'com_content'); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_CATEGORIES_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_CATEGORIES_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } - - /** - * Rebuild the nested set tree. - * - * @return boolean False on failure or error, true on success. - * - * @since 1.6 - */ - public function rebuild() - { - $this->checkToken(); - - $extension = $this->input->get('extension'); - $this->setRedirect(Route::_('index.php?option=com_categories&view=categories&extension=' . $extension, false)); - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $model */ - $model = $this->getModel(); - - if ($model->rebuild()) - { - // Rebuild succeeded. - $this->setMessage(Text::_('COM_CATEGORIES_REBUILD_SUCCESS')); - - return true; - } - - // Rebuild failed. - $this->setMessage(Text::_('COM_CATEGORIES_REBUILD_FAILURE')); - - return false; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $extension = $this->input->getCmd('extension', null); - - return '&extension=' . $extension; - } + /** + * Proxy for getModel + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Category', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Outputs the JSON-encoded amount of published content categories + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Categories'); + $model->setState('filter.published', 1); + $model->setState('filter.extension', 'com_content'); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_CATEGORIES_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_CATEGORIES_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } + + /** + * Rebuild the nested set tree. + * + * @return boolean False on failure or error, true on success. + * + * @since 1.6 + */ + public function rebuild() + { + $this->checkToken(); + + $extension = $this->input->get('extension'); + $this->setRedirect(Route::_('index.php?option=com_categories&view=categories&extension=' . $extension, false)); + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $model */ + $model = $this->getModel(); + + if ($model->rebuild()) { + // Rebuild succeeded. + $this->setMessage(Text::_('COM_CATEGORIES_REBUILD_SUCCESS')); + + return true; + } + + // Rebuild failed. + $this->setMessage(Text::_('COM_CATEGORIES_REBUILD_FAILURE')); + + return false; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $extension = $this->input->getCmd('extension', null); + + return '&extension=' . $extension; + } } diff --git a/administrator/components/com_categories/src/Controller/CategoryController.php b/administrator/components/com_categories/src/Controller/CategoryController.php index 0a7e746733da5..23efe499d0804 100644 --- a/administrator/components/com_categories/src/Controller/CategoryController.php +++ b/administrator/components/com_categories/src/Controller/CategoryController.php @@ -1,4 +1,5 @@ extension)) - { - $this->extension = $this->input->get('extension', 'com_content'); - } - } - - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowAdd($data = array()) - { - $user = $this->app->getIdentity(); - - return ($user->authorise('core.create', $this->extension) || \count($user->getAuthorisedCategories($this->extension, 'core.create'))); - } - - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'parent_id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $user = $this->app->getIdentity(); - - // Check "edit" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->extension . '.category.' . $recordId)) - { - return true; - } - - // Check "edit own" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->extension . '.category.' . $recordId)) - { - // Need to do a lookup from the model to get the owner - $record = $this->getModel()->getItem($recordId); - - if (empty($record)) - { - return false; - } - - $ownerId = $record->created_user_id; - - // If the owner matches 'me' then do the test. - if ($ownerId == $user->id) - { - return true; - } - } - - return false; - } - - /** - * Override parent save method to store form data with right key as expected by edit category page - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 3.10.3 - */ - public function save($key = null, $urlVar = null) - { - $result = parent::save($key, $urlVar); - - $oldKey = $this->option . '.edit.category.data'; - $newKey = $this->option . '.edit.category.' . substr($this->extension, 4) . '.data'; - $this->app->setUserState($newKey, $this->app->getUserState($oldKey)); - - return $result; - } - - /** - * Override cancel method to clear form data for a failed edit action - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 3.10.3 - */ - public function cancel($key = null) - { - $result = parent::cancel($key); - - $newKey = $this->option . '.edit.category.' . substr($this->extension, 4) . '.data'; - $this->app->setUserState($newKey, null); - - return $result; - } - - /** - * Method to run batch operations. - * - * @param object|null $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 1.6 - */ - public function batch($model = null) - { - $this->checkToken(); - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $model */ - $model = $this->getModel('Category'); - - // Preset the redirect - $this->setRedirect('index.php?option=com_categories&view=categories&extension=' . $this->extension); - - return parent::batch($model); - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer|null $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 1.6 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - - // In case extension is not passed in the URL, get it directly from category instead of default to com_content - if (!$this->input->exists('extension') && $recordId > 0) - { - $table = $this->getModel('Category')->getTable(); - - if ($table->load($recordId)) - { - $this->extension = $table->extension; - } - } - - $append .= '&extension=' . $this->extension; - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 1.6 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&extension=' . $this->extension; - - return $append; - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param \Joomla\CMS\MVC\Model\BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 3.1 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - $item = $model->getItem(); - - if (isset($item->params) && \is_array($item->params)) - { - $registry = new Registry($item->params); - $item->params = (string) $registry; - } - - if (isset($item->metadata) && \is_array($item->metadata)) - { - $registry = new Registry($item->metadata); - $item->metadata = (string) $registry; - } - } + use VersionableControllerTrait; + + /** + * The extension for which the categories apply. + * + * @var string + * @since 1.6 + */ + protected $extension; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param Input|null $input Input + * + * @since 1.6 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, CMSApplication $app = null, Input $input = null) + { + parent::__construct($config, $factory, $app, $input); + + if (empty($this->extension)) { + $this->extension = $this->input->get('extension', 'com_content'); + } + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $user = $this->app->getIdentity(); + + return ($user->authorise('core.create', $this->extension) || \count($user->getAuthorisedCategories($this->extension, 'core.create'))); + } + + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'parent_id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $user = $this->app->getIdentity(); + + // Check "edit" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->extension . '.category.' . $recordId)) { + return true; + } + + // Check "edit own" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->extension . '.category.' . $recordId)) { + // Need to do a lookup from the model to get the owner + $record = $this->getModel()->getItem($recordId); + + if (empty($record)) { + return false; + } + + $ownerId = $record->created_user_id; + + // If the owner matches 'me' then do the test. + if ($ownerId == $user->id) { + return true; + } + } + + return false; + } + + /** + * Override parent save method to store form data with right key as expected by edit category page + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 3.10.3 + */ + public function save($key = null, $urlVar = null) + { + $result = parent::save($key, $urlVar); + + $oldKey = $this->option . '.edit.category.data'; + $newKey = $this->option . '.edit.category.' . substr($this->extension, 4) . '.data'; + $this->app->setUserState($newKey, $this->app->getUserState($oldKey)); + + return $result; + } + + /** + * Override cancel method to clear form data for a failed edit action + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 3.10.3 + */ + public function cancel($key = null) + { + $result = parent::cancel($key); + + $newKey = $this->option . '.edit.category.' . substr($this->extension, 4) . '.data'; + $this->app->setUserState($newKey, null); + + return $result; + } + + /** + * Method to run batch operations. + * + * @param object|null $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 1.6 + */ + public function batch($model = null) + { + $this->checkToken(); + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $model */ + $model = $this->getModel('Category'); + + // Preset the redirect + $this->setRedirect('index.php?option=com_categories&view=categories&extension=' . $this->extension); + + return parent::batch($model); + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer|null $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 1.6 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + + // In case extension is not passed in the URL, get it directly from category instead of default to com_content + if (!$this->input->exists('extension') && $recordId > 0) { + $table = $this->getModel('Category')->getTable(); + + if ($table->load($recordId)) { + $this->extension = $table->extension; + } + } + + $append .= '&extension=' . $this->extension; + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 1.6 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&extension=' . $this->extension; + + return $append; + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param \Joomla\CMS\MVC\Model\BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 3.1 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + $item = $model->getItem(); + + if (isset($item->params) && \is_array($item->params)) { + $registry = new Registry($item->params); + $item->params = (string) $registry; + } + + if (isset($item->metadata) && \is_array($item->metadata)) { + $registry = new Registry($item->metadata); + $item->metadata = (string) $registry; + } + } } diff --git a/administrator/components/com_categories/src/Controller/DisplayController.php b/administrator/components/com_categories/src/Controller/DisplayController.php index 5f9ab4ae35db5..5c3748761582c 100644 --- a/administrator/components/com_categories/src/Controller/DisplayController.php +++ b/administrator/components/com_categories/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ extension)) - { - $this->extension = $this->input->get('extension', 'com_content'); - } - } - - /** - * Method to display a view. - * - * @param boolean $cachable If true, the view output will be cached - * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. - * - * @return static|boolean This object to support chaining. - * - * @since 1.5 - */ - public function display($cachable = false, $urlparams = array()) - { - // Get the document object. - $document = $this->app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'categories'); - $vFormat = $document->getType(); - $lName = $this->input->get('layout', 'default', 'string'); - $id = $this->input->getInt('id'); - - // Check for edit form. - if ($vName == 'category' && $lName == 'edit' && !$this->checkEditId('com_categories.edit.category', $id)) - { - // 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', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_categories&view=categories&extension=' . $this->extension, false)); - - return false; - } - - // Get and render the view. - if ($view = $this->getView($vName, $vFormat)) - { - // Get the model for the view. - $model = $this->getModel($vName, 'Administrator', array('name' => $vName . '.' . substr($this->extension, 4))); - - // Push the model into the view (as default). - $view->setModel($model, true); - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - $view->display(); - } - - return $this; - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'categories'; + + /** + * The extension for which the categories apply. + * + * @var string + * @since 1.6 + */ + protected $extension; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param Input|null $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // Guess the Text message prefix. Defaults to the option. + if (empty($this->extension)) { + $this->extension = $this->input->get('extension', 'com_content'); + } + } + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'categories'); + $vFormat = $document->getType(); + $lName = $this->input->get('layout', 'default', 'string'); + $id = $this->input->getInt('id'); + + // Check for edit form. + if ($vName == 'category' && $lName == 'edit' && !$this->checkEditId('com_categories.edit.category', $id)) { + // 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', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_categories&view=categories&extension=' . $this->extension, false)); + + return false; + } + + // Get and render the view. + if ($view = $this->getView($vName, $vFormat)) { + // Get the model for the view. + $model = $this->getModel($vName, 'Administrator', array('name' => $vName . '.' . substr($this->extension, 4))); + + // Push the model into the view (as default). + $view->setModel($model, true); + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + $view->display(); + } + + return $this; + } } diff --git a/administrator/components/com_categories/src/Dispatcher/Dispatcher.php b/administrator/components/com_categories/src/Dispatcher/Dispatcher.php index d32c1c0ece757..d7f77ecbabe4f 100644 --- a/administrator/components/com_categories/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_categories/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ getApplication()->input->getCmd('extension'); + /** + * Categories have to check for extension permission + * + * @return void + */ + protected function checkAccess() + { + $extension = $this->getApplication()->input->getCmd('extension'); - $parts = explode('.', $extension); + $parts = explode('.', $extension); - // Check the user has permission to access this component if in the backend - if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.manage', $parts[0])) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + // Check the user has permission to access this component if in the backend + if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.manage', $parts[0])) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_categories/src/Extension/CategoriesComponent.php b/administrator/components/com_categories/src/Extension/CategoriesComponent.php index 8b2eb72f2b9dd..6483f2a2ec3ab 100644 --- a/administrator/components/com_categories/src/Extension/CategoriesComponent.php +++ b/administrator/components/com_categories/src/Extension/CategoriesComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('categoriesadministrator', new AdministratorService); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('categoriesadministrator', new AdministratorService()); + } } diff --git a/administrator/components/com_categories/src/Field/CategoryeditField.php b/administrator/components/com_categories/src/Field/CategoryeditField.php index 4a8ccec8e8d22..db9423c577ae5 100644 --- a/administrator/components/com_categories/src/Field/CategoryeditField.php +++ b/administrator/components/com_categories/src/Field/CategoryeditField.php @@ -1,4 +1,5 @@ tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string|null $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see FormField::setup() - * @since 3.2 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $return = parent::setup($element, $value, $group); - - if ($return) - { - $this->allowAdd = isset($this->element['allowAdd']) ? (boolean) $this->element['allowAdd'] : false; - $this->customPrefix = (string) $this->element['customPrefix']; - } - - return $return; - } - - /** - * Method to get certain otherwise inaccessible properties from the form field object. - * - * @param string $name The property name for which to get the value. - * - * @return mixed The property value or null. - * - * @since 3.6 - */ - public function __get($name) - { - switch ($name) - { - case 'allowAdd': - return (bool) $this->$name; - case 'customPrefix': - return $this->$name; - } - - return parent::__get($name); - } - - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 3.6 - */ - public function __set($name, $value) - { - $value = (string) $value; - - switch ($name) - { - case 'allowAdd': - $value = (string) $value; - $this->$name = ($value === 'true' || $value === $name || $value === '1'); - break; - case 'customPrefix': - $this->$name = (string) $value; - break; - default: - parent::__set($name, $value); - } - } - - /** - * Method to get a list of categories that respects access controls and can be used for - * either category assignment or parent category assignment in edit screens. - * Use the parent element to indicate that the field will be used for assigning parent categories. - * - * @return array The field option objects. - * - * @since 1.6 - */ - protected function getOptions() - { - $options = array(); - $published = $this->element['published'] ? explode(',', (string) $this->element['published']) : array(0, 1); - $name = (string) $this->element['name']; - - // Let's get the id for the current item, either category or content item. - $jinput = Factory::getApplication()->input; - - // Load the category options for a given extension. - - // For categories the old category is the category id or 0 for new category. - if ($this->element['parent'] || $jinput->get('option') == 'com_categories') - { - $oldCat = $jinput->get('id', 0); - $oldParent = $this->form->getValue($name, 0); - $extension = $this->element['extension'] ? (string) $this->element['extension'] : (string) $jinput->get('extension', 'com_content'); - } - else - // For items the old category is the category they are in when opened or 0 if new. - { - $oldCat = $this->form->getValue($name, 0); - $extension = $this->element['extension'] ? (string) $this->element['extension'] : (string) $jinput->get('option', 'com_content'); - } - - // Account for case that a submitted form has a multi-value category id field (e.g. a filtering form), just use the first category - $oldCat = \is_array($oldCat) - ? (int) reset($oldCat) - : (int) $oldCat; - - $db = $this->getDatabase(); - $user = Factory::getUser(); - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('a.id', 'value'), - $db->quoteName('a.title', 'text'), - $db->quoteName('a.level'), - $db->quoteName('a.published'), - $db->quoteName('a.lft'), - $db->quoteName('a.language'), - ] - ) - ->from($db->quoteName('#__categories', 'a')); - - // Filter by the extension type - if ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') - { - $query->where('(' . $db->quoteName('a.extension') . ' = :extension OR ' . $db->quoteName('a.parent_id') . ' = 0)') - ->bind(':extension', $extension); - } - else - { - $query->where($db->quoteName('a.extension') . ' = :extension') - ->bind(':extension', $extension); - } - - // Filter language - if (!empty($this->element['language'])) - { - if (strpos($this->element['language'], ',') !== false) - { - $language = explode(',', $this->element['language']); - } - else - { - $language = $this->element['language']; - } - - $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); - } - - // Filter on the published state - $state = ArrayHelper::toInteger($published); - $query->whereIn($db->quoteName('a.published'), $state); - - // Filter categories on User Access Level - // Filter by access level on categories. - if (!$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - } - - $query->order($db->quoteName('a.lft') . ' ASC'); - - // If parent isn't explicitly stated but we are in com_categories assume we want parents - if ($oldCat != 0 && ($this->element['parent'] == true || $jinput->get('option') == 'com_categories')) - { - // Prevent parenting to children of this item. - // To rearrange parents and children move the children up, not the parents down. - $query->join( - 'LEFT', - $db->quoteName('#__categories', 'p'), - $db->quoteName('p.id') . ' = :oldcat' - ) - ->bind(':oldcat', $oldCat, ParameterType::INTEGER) - ->where('NOT(' . $db->quoteName('a.lft') . ' >= ' . $db->quoteName('p.lft') - . ' AND ' . $db->quoteName('a.rgt') . ' <= ' . $db->quoteName('p.rgt') . ')' - ); - } - - // Get the options. - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Pad the option text with spaces using depth level as a multiplier. - for ($i = 0, $n = \count($options); $i < $n; $i++) - { - // Translate ROOT - if ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') - { - if ($options[$i]->level == 0) - { - $options[$i]->text = Text::_('JGLOBAL_ROOT_PARENT'); - } - } - - if ($options[$i]->published == 1) - { - $options[$i]->text = str_repeat('- ', !$options[$i]->level ? 0 : $options[$i]->level - 1) . $options[$i]->text; - } - else - { - $options[$i]->text = str_repeat('- ', !$options[$i]->level ? 0 : $options[$i]->level - 1) . '[' . $options[$i]->text . ']'; - } - - // Displays language code if not set to All - if ($options[$i]->language !== '*') - { - $options[$i]->text = $options[$i]->text . ' (' . $options[$i]->language . ')'; - } - } - - // For new items we want a list of categories you are allowed to create in. - if ($oldCat == 0) - { - foreach ($options as $i => $option) - { - /* - * To take save or create in a category you need to have create rights for that category unless the item is already in that category. - * Unset the option if the user isn't authorised for it. In this field assets are always categories. - */ - if ($option->level != 0 && !$user->authorise('core.create', $extension . '.category.' . $option->value)) - { - unset($options[$i]); - } - } - } - // If you have an existing category id things are more complex. - else - { - /* - * If you are only allowed to edit in this category but not edit.state, you should not get any - * option to change the category parent for a category or the category for a content item, - * but you should be able to save in that category. - */ - foreach ($options as $i => $option) - { - $assetKey = $extension . '.category.' . $oldCat; - - if ($option->level != 0 && !isset($oldParent) && $option->value != $oldCat && !$user->authorise('core.edit.state', $assetKey)) - { - unset($options[$i]); - continue; - } - - if ($option->level != 0 && isset($oldParent) && $option->value != $oldParent && !$user->authorise('core.edit.state', $assetKey)) - { - unset($options[$i]); - continue; - } - - /* - * However, if you can edit.state you can also move this to another category for which you have - * create permission and you should also still be able to save in the current category. - */ - $assetKey = $extension . '.category.' . $option->value; - - if ($option->level != 0 && !isset($oldParent) && $option->value != $oldCat && !$user->authorise('core.create', $assetKey)) - { - unset($options[$i]); - continue; - } - - if ($option->level != 0 && isset($oldParent) && $option->value != $oldParent && !$user->authorise('core.create', $assetKey)) - { - unset($options[$i]); - } - } - } - - if ($oldCat != 0 && ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') - && !isset($options[0]) - && isset($this->element['show_root'])) - { - $rowQuery = $db->getQuery(true) - ->select( - [ - $db->quoteName('a.id', 'value'), - $db->quoteName('a.title', 'text'), - $db->quoteName('a.level'), - $db->quoteName('a.parent_id'), - ] - ) - ->from($db->quoteName('#__categories', 'a')) - ->where($db->quoteName('a.id') . ' = :aid') - ->bind(':aid', $oldCat, ParameterType::INTEGER); - $db->setQuery($rowQuery); - $row = $db->loadObject(); - - if ($row->parent_id == '1') - { - $parent = new \stdClass; - $parent->text = Text::_('JGLOBAL_ROOT_PARENT'); - array_unshift($options, $parent); - } - - array_unshift($options, HTMLHelper::_('select.option', '0', Text::_('JGLOBAL_ROOT'))); - } - - // Merge any additional options in the XML definition. - return array_merge(parent::getOptions(), $options); - } - - /** - * Method to get the field input markup for a generic list. - * Use the multiple attribute to enable multiselect. - * - * @return string The field input markup. - * - * @since 3.6 - */ - protected function getInput() - { - $data = $this->getLayoutData(); - - $data['options'] = $this->getOptions(); - $data['allowCustom'] = $this->allowAdd; - $data['customPrefix'] = $this->customPrefix; - $data['refreshPage'] = (boolean) $this->element['refresh-enabled']; - $data['refreshCatId'] = (string) $this->element['refresh-cat-id']; - $data['refreshSection'] = (string) $this->element['refresh-section']; - - $renderer = $this->getRenderer($this->layout); - $renderer->setComponent('com_categories'); - $renderer->setClient(1); - - return $renderer->render($data); - } + /** + * To allow creation of new categories. + * + * @var integer + * @since 3.6 + */ + protected $allowAdd; + + /** + * Optional prefix for new categories. + * + * @var string + * @since 3.9.11 + */ + protected $customPrefix; + + /** + * A flexible category list that respects access controls + * + * @var string + * @since 1.6 + */ + public $type = 'CategoryEdit'; + + /** + * Name of the layout being used to render the field + * + * @var string + * @since 4.0.0 + */ + protected $layout = 'joomla.form.field.categoryedit'; + + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string|null $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see FormField::setup() + * @since 3.2 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $return = parent::setup($element, $value, $group); + + if ($return) { + $this->allowAdd = isset($this->element['allowAdd']) ? (bool) $this->element['allowAdd'] : false; + $this->customPrefix = (string) $this->element['customPrefix']; + } + + return $return; + } + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 3.6 + */ + public function __get($name) + { + switch ($name) { + case 'allowAdd': + return (bool) $this->$name; + case 'customPrefix': + return $this->$name; + } + + return parent::__get($name); + } + + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 3.6 + */ + public function __set($name, $value) + { + $value = (string) $value; + + switch ($name) { + case 'allowAdd': + $value = (string) $value; + $this->$name = ($value === 'true' || $value === $name || $value === '1'); + break; + case 'customPrefix': + $this->$name = (string) $value; + break; + default: + parent::__set($name, $value); + } + } + + /** + * Method to get a list of categories that respects access controls and can be used for + * either category assignment or parent category assignment in edit screens. + * Use the parent element to indicate that the field will be used for assigning parent categories. + * + * @return array The field option objects. + * + * @since 1.6 + */ + protected function getOptions() + { + $options = array(); + $published = $this->element['published'] ? explode(',', (string) $this->element['published']) : array(0, 1); + $name = (string) $this->element['name']; + + // Let's get the id for the current item, either category or content item. + $jinput = Factory::getApplication()->input; + + // Load the category options for a given extension. + + // For categories the old category is the category id or 0 for new category. + if ($this->element['parent'] || $jinput->get('option') == 'com_categories') { + $oldCat = $jinput->get('id', 0); + $oldParent = $this->form->getValue($name, 0); + $extension = $this->element['extension'] ? (string) $this->element['extension'] : (string) $jinput->get('extension', 'com_content'); + } else // For items the old category is the category they are in when opened or 0 if new. + { + $oldCat = $this->form->getValue($name, 0); + $extension = $this->element['extension'] ? (string) $this->element['extension'] : (string) $jinput->get('option', 'com_content'); + } + + // Account for case that a submitted form has a multi-value category id field (e.g. a filtering form), just use the first category + $oldCat = \is_array($oldCat) + ? (int) reset($oldCat) + : (int) $oldCat; + + $db = $this->getDatabase(); + $user = Factory::getUser(); + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('a.id', 'value'), + $db->quoteName('a.title', 'text'), + $db->quoteName('a.level'), + $db->quoteName('a.published'), + $db->quoteName('a.lft'), + $db->quoteName('a.language'), + ] + ) + ->from($db->quoteName('#__categories', 'a')); + + // Filter by the extension type + if ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') { + $query->where('(' . $db->quoteName('a.extension') . ' = :extension OR ' . $db->quoteName('a.parent_id') . ' = 0)') + ->bind(':extension', $extension); + } else { + $query->where($db->quoteName('a.extension') . ' = :extension') + ->bind(':extension', $extension); + } + + // Filter language + if (!empty($this->element['language'])) { + if (strpos($this->element['language'], ',') !== false) { + $language = explode(',', $this->element['language']); + } else { + $language = $this->element['language']; + } + + $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); + } + + // Filter on the published state + $state = ArrayHelper::toInteger($published); + $query->whereIn($db->quoteName('a.published'), $state); + + // Filter categories on User Access Level + // Filter by access level on categories. + if (!$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + } + + $query->order($db->quoteName('a.lft') . ' ASC'); + + // If parent isn't explicitly stated but we are in com_categories assume we want parents + if ($oldCat != 0 && ($this->element['parent'] == true || $jinput->get('option') == 'com_categories')) { + // Prevent parenting to children of this item. + // To rearrange parents and children move the children up, not the parents down. + $query->join( + 'LEFT', + $db->quoteName('#__categories', 'p'), + $db->quoteName('p.id') . ' = :oldcat' + ) + ->bind(':oldcat', $oldCat, ParameterType::INTEGER) + ->where('NOT(' . $db->quoteName('a.lft') . ' >= ' . $db->quoteName('p.lft') + . ' AND ' . $db->quoteName('a.rgt') . ' <= ' . $db->quoteName('p.rgt') . ')'); + } + + // Get the options. + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Pad the option text with spaces using depth level as a multiplier. + for ($i = 0, $n = \count($options); $i < $n; $i++) { + // Translate ROOT + if ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') { + if ($options[$i]->level == 0) { + $options[$i]->text = Text::_('JGLOBAL_ROOT_PARENT'); + } + } + + if ($options[$i]->published == 1) { + $options[$i]->text = str_repeat('- ', !$options[$i]->level ? 0 : $options[$i]->level - 1) . $options[$i]->text; + } else { + $options[$i]->text = str_repeat('- ', !$options[$i]->level ? 0 : $options[$i]->level - 1) . '[' . $options[$i]->text . ']'; + } + + // Displays language code if not set to All + if ($options[$i]->language !== '*') { + $options[$i]->text = $options[$i]->text . ' (' . $options[$i]->language . ')'; + } + } + + // For new items we want a list of categories you are allowed to create in. + if ($oldCat == 0) { + foreach ($options as $i => $option) { + /* + * To take save or create in a category you need to have create rights for that category unless the item is already in that category. + * Unset the option if the user isn't authorised for it. In this field assets are always categories. + */ + if ($option->level != 0 && !$user->authorise('core.create', $extension . '.category.' . $option->value)) { + unset($options[$i]); + } + } + } + // If you have an existing category id things are more complex. + else { + /* + * If you are only allowed to edit in this category but not edit.state, you should not get any + * option to change the category parent for a category or the category for a content item, + * but you should be able to save in that category. + */ + foreach ($options as $i => $option) { + $assetKey = $extension . '.category.' . $oldCat; + + if ($option->level != 0 && !isset($oldParent) && $option->value != $oldCat && !$user->authorise('core.edit.state', $assetKey)) { + unset($options[$i]); + continue; + } + + if ($option->level != 0 && isset($oldParent) && $option->value != $oldParent && !$user->authorise('core.edit.state', $assetKey)) { + unset($options[$i]); + continue; + } + + /* + * However, if you can edit.state you can also move this to another category for which you have + * create permission and you should also still be able to save in the current category. + */ + $assetKey = $extension . '.category.' . $option->value; + + if ($option->level != 0 && !isset($oldParent) && $option->value != $oldCat && !$user->authorise('core.create', $assetKey)) { + unset($options[$i]); + continue; + } + + if ($option->level != 0 && isset($oldParent) && $option->value != $oldParent && !$user->authorise('core.create', $assetKey)) { + unset($options[$i]); + } + } + } + + if ( + $oldCat != 0 && ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') + && !isset($options[0]) + && isset($this->element['show_root']) + ) { + $rowQuery = $db->getQuery(true) + ->select( + [ + $db->quoteName('a.id', 'value'), + $db->quoteName('a.title', 'text'), + $db->quoteName('a.level'), + $db->quoteName('a.parent_id'), + ] + ) + ->from($db->quoteName('#__categories', 'a')) + ->where($db->quoteName('a.id') . ' = :aid') + ->bind(':aid', $oldCat, ParameterType::INTEGER); + $db->setQuery($rowQuery); + $row = $db->loadObject(); + + if ($row->parent_id == '1') { + $parent = new \stdClass(); + $parent->text = Text::_('JGLOBAL_ROOT_PARENT'); + array_unshift($options, $parent); + } + + array_unshift($options, HTMLHelper::_('select.option', '0', Text::_('JGLOBAL_ROOT'))); + } + + // Merge any additional options in the XML definition. + return array_merge(parent::getOptions(), $options); + } + + /** + * Method to get the field input markup for a generic list. + * Use the multiple attribute to enable multiselect. + * + * @return string The field input markup. + * + * @since 3.6 + */ + protected function getInput() + { + $data = $this->getLayoutData(); + + $data['options'] = $this->getOptions(); + $data['allowCustom'] = $this->allowAdd; + $data['customPrefix'] = $this->customPrefix; + $data['refreshPage'] = (bool) $this->element['refresh-enabled']; + $data['refreshCatId'] = (string) $this->element['refresh-cat-id']; + $data['refreshSection'] = (string) $this->element['refresh-section']; + + $renderer = $this->getRenderer($this->layout); + $renderer->setComponent('com_categories'); + $renderer->setClient(1); + + return $renderer->render($data); + } } diff --git a/administrator/components/com_categories/src/Field/ComponentsCategoryField.php b/administrator/components/com_categories/src/Field/ComponentsCategoryField.php index 8fd9838dd588a..13ca43fcd89f5 100644 --- a/administrator/components/com_categories/src/Field/ComponentsCategoryField.php +++ b/administrator/components/com_categories/src/Field/ComponentsCategoryField.php @@ -1,4 +1,5 @@ getDatabase(); - $options = array(); - - $query = $db->getQuery(true); - $query->select('DISTINCT ' . $db->quoteName('extension')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('extension') . ' != ' . $db->quote('system')); - - $db->setQuery($query); - $categoryTypes = $db->loadColumn(); - - foreach ($categoryTypes as $categoryType) - { - $option = new \stdClass; - $option->value = $categoryType; - - // Extract the component name and optional section name - $parts = explode('.', $categoryType); - $component = $parts[0]; - $section = (\count($parts) > 1) ? $parts[1] : null; - - // Load component language files - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE) - || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); - - // If the component section string exists, let's use it - if ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) - { - $option->text = Text::_($component_section_key); - } - else - // Else use the component title - { - $option->text = Text::_(strtoupper($component)); - } - - $options[] = $option; - } - - // Sort by name - $options = ArrayHelper::sortObjects($options, 'text', 1, true, true); - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'ComponentsCategory'; + + /** + * Method to get a list of options for a list input. + * + * @return array An array of JHtml options. + * + * @since 3.7.0 + */ + protected function getOptions() + { + // Initialise variable. + $db = $this->getDatabase(); + $options = array(); + + $query = $db->getQuery(true); + $query->select('DISTINCT ' . $db->quoteName('extension')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('extension') . ' != ' . $db->quote('system')); + + $db->setQuery($query); + $categoryTypes = $db->loadColumn(); + + foreach ($categoryTypes as $categoryType) { + $option = new \stdClass(); + $option->value = $categoryType; + + // Extract the component name and optional section name + $parts = explode('.', $categoryType); + $component = $parts[0]; + $section = (\count($parts) > 1) ? $parts[1] : null; + + // Load component language files + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE) + || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); + + // If the component section string exists, let's use it + if ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) { + $option->text = Text::_($component_section_key); + } else // Else use the component title + { + $option->text = Text::_(strtoupper($component)); + } + + $options[] = $option; + } + + // Sort by name + $options = ArrayHelper::sortObjects($options, 'text', 1, true, true); + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); + + return $options; + } } diff --git a/administrator/components/com_categories/src/Field/Modal/CategoryField.php b/administrator/components/com_categories/src/Field/Modal/CategoryField.php index 70c1f5ff18237..8cda7b61db591 100644 --- a/administrator/components/com_categories/src/Field/Modal/CategoryField.php +++ b/administrator/components/com_categories/src/Field/Modal/CategoryField.php @@ -1,4 +1,5 @@ element['extension']) - { - $extension = (string) $this->element['extension']; - } - else - { - $extension = (string) Factory::getApplication()->input->get('extension', 'com_content'); - } - - $allowNew = ((string) $this->element['new'] == 'true'); - $allowEdit = ((string) $this->element['edit'] == 'true'); - $allowClear = ((string) $this->element['clear'] != 'false'); - $allowSelect = ((string) $this->element['select'] != 'false'); - $allowPropagate = ((string) $this->element['propagate'] == 'true'); - - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - - // Load language. - Factory::getLanguage()->load('com_categories', JPATH_ADMINISTRATOR); - - // The active category id field. - $value = (int) $this->value ?: ''; - - // Create the modal id. - $modalId = 'Category_' . $this->id; - - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - - // Add the modal field script to the document head. - $wa->useScript('field.modal-fields'); - - // Script to proxy the select modal function to the modal-fields.js file. - if ($allowSelect) - { - static $scriptSelect = null; - - if (is_null($scriptSelect)) - { - $scriptSelect = array(); - } - - if (!isset($scriptSelect[$this->id])) - { - $wa->addInlineScript(" + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'Modal_Category'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + if ($this->element['extension']) { + $extension = (string) $this->element['extension']; + } else { + $extension = (string) Factory::getApplication()->input->get('extension', 'com_content'); + } + + $allowNew = ((string) $this->element['new'] == 'true'); + $allowEdit = ((string) $this->element['edit'] == 'true'); + $allowClear = ((string) $this->element['clear'] != 'false'); + $allowSelect = ((string) $this->element['select'] != 'false'); + $allowPropagate = ((string) $this->element['propagate'] == 'true'); + + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + + // Load language. + Factory::getLanguage()->load('com_categories', JPATH_ADMINISTRATOR); + + // The active category id field. + $value = (int) $this->value ?: ''; + + // Create the modal id. + $modalId = 'Category_' . $this->id; + + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + + // Add the modal field script to the document head. + $wa->useScript('field.modal-fields'); + + // Script to proxy the select modal function to the modal-fields.js file. + if ($allowSelect) { + static $scriptSelect = null; + + if (is_null($scriptSelect)) { + $scriptSelect = array(); + } + + if (!isset($scriptSelect[$this->id])) { + $wa->addInlineScript( + " window.jSelectCategory_" . $this->id . " = function (id, title, object) { window.processModalSelect('Category', '" . $this->id . "', id, title, '', object); }", - [], - ['type' => 'module'] - ); - - Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); - - $scriptSelect[$this->id] = true; - } - } - - // Setup variables for display. - $linkCategories = 'index.php?option=com_categories&view=categories&layout=modal&tmpl=component&' . Session::getFormToken() . '=1' - . '&extension=' . $extension; - $linkCategory = 'index.php?option=com_categories&view=category&layout=modal&tmpl=component&' . Session::getFormToken() . '=1' - . '&extension=' . $extension; - $modalTitle = Text::_('COM_CATEGORIES_SELECT_A_CATEGORY'); - - if (isset($this->element['language'])) - { - $linkCategories .= '&forcedLanguage=' . $this->element['language']; - $linkCategory .= '&forcedLanguage=' . $this->element['language']; - $modalTitle .= ' — ' . $this->element['label']; - } - - $urlSelect = $linkCategories . '&function=jSelectCategory_' . $this->id; - $urlEdit = $linkCategory . '&task=category.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; - $urlNew = $linkCategory . '&task=category.add'; - - if ($value) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('id') . ' = :value') - ->bind(':value', $value, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $title = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - $title = empty($title) ? Text::_('COM_CATEGORIES_SELECT_A_CATEGORY') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); - - // The current category display field. - $html = ''; - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - $html .= ''; - - // Select category button. - if ($allowSelect) - { - $html .= '' - . ' ' . Text::_('JSELECT') - . ''; - } - - // New category button. - if ($allowNew) - { - $html .= '' - . ' ' . Text::_('JACTION_CREATE') - . ''; - } - - // Edit category button. - if ($allowEdit) - { - $html .= '' - . ' ' . Text::_('JACTION_EDIT') - . ''; - } - - // Clear category button. - if ($allowClear) - { - $html .= '' - . ' ' . Text::_('JCLEAR') - . ''; - } - - // Propagate category button - if ($allowPropagate && \count($languages) > 2) - { - // Strip off language tag at the end - $tagLength = (int) \strlen($this->element['language']); - $callbackFunctionStem = substr("jSelectCategory_" . $this->id, 0, -$tagLength); - - $html .= '' - . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') - . ''; - } - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - // Select category modal. - if ($allowSelect) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalSelect' . $modalId, - array( - 'title' => $modalTitle, - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - } - - // New category modal. - if ($allowNew) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalNew' . $modalId, - array( - 'title' => Text::_('COM_CATEGORIES_NEW_CATEGORY'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlNew, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Edit category modal. - if ($allowEdit) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalEdit' . $modalId, - array( - 'title' => Text::_('COM_CATEGORIES_EDIT_CATEGORY'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlEdit, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Note: class='required' for client side validation - $class = $this->required ? ' class="required modal-value"' : ''; - - $html .= ''; - - return $html; - } - - /** - * Method to get the field label markup. - * - * @return string The field label markup. - * - * @since 3.7.0 - */ - protected function getLabel() - { - return str_replace($this->id, $this->id . '_name', parent::getLabel()); - } + [], + ['type' => 'module'] + ); + + Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); + + $scriptSelect[$this->id] = true; + } + } + + // Setup variables for display. + $linkCategories = 'index.php?option=com_categories&view=categories&layout=modal&tmpl=component&' . Session::getFormToken() . '=1' + . '&extension=' . $extension; + $linkCategory = 'index.php?option=com_categories&view=category&layout=modal&tmpl=component&' . Session::getFormToken() . '=1' + . '&extension=' . $extension; + $modalTitle = Text::_('COM_CATEGORIES_SELECT_A_CATEGORY'); + + if (isset($this->element['language'])) { + $linkCategories .= '&forcedLanguage=' . $this->element['language']; + $linkCategory .= '&forcedLanguage=' . $this->element['language']; + $modalTitle .= ' — ' . $this->element['label']; + } + + $urlSelect = $linkCategories . '&function=jSelectCategory_' . $this->id; + $urlEdit = $linkCategory . '&task=category.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; + $urlNew = $linkCategory . '&task=category.add'; + + if ($value) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('id') . ' = :value') + ->bind(':value', $value, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $title = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + $title = empty($title) ? Text::_('COM_CATEGORIES_SELECT_A_CATEGORY') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + + // The current category display field. + $html = ''; + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + $html .= ''; + + // Select category button. + if ($allowSelect) { + $html .= '' + . ' ' . Text::_('JSELECT') + . ''; + } + + // New category button. + if ($allowNew) { + $html .= '' + . ' ' . Text::_('JACTION_CREATE') + . ''; + } + + // Edit category button. + if ($allowEdit) { + $html .= '' + . ' ' . Text::_('JACTION_EDIT') + . ''; + } + + // Clear category button. + if ($allowClear) { + $html .= '' + . ' ' . Text::_('JCLEAR') + . ''; + } + + // Propagate category button + if ($allowPropagate && \count($languages) > 2) { + // Strip off language tag at the end + $tagLength = (int) \strlen($this->element['language']); + $callbackFunctionStem = substr("jSelectCategory_" . $this->id, 0, -$tagLength); + + $html .= '' + . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') + . ''; + } + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + // Select category modal. + if ($allowSelect) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalSelect' . $modalId, + array( + 'title' => $modalTitle, + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + } + + // New category modal. + if ($allowNew) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalNew' . $modalId, + array( + 'title' => Text::_('COM_CATEGORIES_NEW_CATEGORY'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlNew, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Edit category modal. + if ($allowEdit) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalEdit' . $modalId, + array( + 'title' => Text::_('COM_CATEGORIES_EDIT_CATEGORY'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlEdit, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Note: class='required' for client side validation + $class = $this->required ? ' class="required modal-value"' : ''; + + $html .= ''; + + return $html; + } + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + * + * @since 3.7.0 + */ + protected function getLabel() + { + return str_replace($this->id, $this->id . '_name', parent::getLabel()); + } } diff --git a/administrator/components/com_categories/src/Helper/CategoriesHelper.php b/administrator/components/com_categories/src/Helper/CategoriesHelper.php index dfaacc58ae81c..9f4701054448b 100644 --- a/administrator/components/com_categories/src/Helper/CategoriesHelper.php +++ b/administrator/components/com_categories/src/Helper/CategoriesHelper.php @@ -1,4 +1,5 @@ getAuthorisedViewLevels(); - - foreach ($langAssociations as $langAssociation) - { - // Include only published categories with user access - $arrId = explode(':', $langAssociation->id); - $assocId = (int) $arrId[0]; - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select($db->quoteName('published')) - ->from($db->quoteName('#__categories')) - ->whereIn($db->quoteName('access'), $groups) - ->where($db->quoteName('id') . ' = :associd') - ->bind(':associd', $assocId, ParameterType::INTEGER); - - $result = (int) $db->setQuery($query)->loadResult(); - - if ($result === 1) - { - $associations[$langAssociation->language] = $langAssociation->id; - } - } - - return $associations; - } - - /** - * Check if Category ID exists otherwise assign to ROOT category. - * - * @param mixed $catid Name or ID of category. - * @param string $extension Extension that triggers this function - * - * @return integer $catid Category ID. - */ - public static function validateCategoryId($catid, $extension) - { - $categoryTable = Table::getInstance('CategoryTable', '\\Joomla\\Component\\Categories\\Administrator\\Table\\'); - - $data = array(); - $data['id'] = $catid; - $data['extension'] = $extension; - - if (!$categoryTable->load($data)) - { - $catid = 0; - } - - return (int) $catid; - } - - /** - * Create new Category from within item view. - * - * @param array $data Array of data for new category. - * - * @return integer - */ - public static function createCategory($data) - { - $categoryModel = Factory::getApplication()->bootComponent('com_categories') - ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); - $categoryModel->save($data); - - $catid = $categoryModel->getState('category.id'); - - return $catid; - } + /** + * Gets a list of associations for a given item. + * + * @param integer $pk Content item key. + * @param string $extension Optional extension name. + * + * @return array of associations. + */ + public static function getAssociations($pk, $extension = 'com_content') + { + $langAssociations = Associations::getAssociations($extension, '#__categories', 'com_categories.item', $pk, 'id', 'alias', ''); + $associations = array(); + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + foreach ($langAssociations as $langAssociation) { + // Include only published categories with user access + $arrId = explode(':', $langAssociation->id); + $assocId = (int) $arrId[0]; + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select($db->quoteName('published')) + ->from($db->quoteName('#__categories')) + ->whereIn($db->quoteName('access'), $groups) + ->where($db->quoteName('id') . ' = :associd') + ->bind(':associd', $assocId, ParameterType::INTEGER); + + $result = (int) $db->setQuery($query)->loadResult(); + + if ($result === 1) { + $associations[$langAssociation->language] = $langAssociation->id; + } + } + + return $associations; + } + + /** + * Check if Category ID exists otherwise assign to ROOT category. + * + * @param mixed $catid Name or ID of category. + * @param string $extension Extension that triggers this function + * + * @return integer $catid Category ID. + */ + public static function validateCategoryId($catid, $extension) + { + $categoryTable = Table::getInstance('CategoryTable', '\\Joomla\\Component\\Categories\\Administrator\\Table\\'); + + $data = array(); + $data['id'] = $catid; + $data['extension'] = $extension; + + if (!$categoryTable->load($data)) { + $catid = 0; + } + + return (int) $catid; + } + + /** + * Create new Category from within item view. + * + * @param array $data Array of data for new category. + * + * @return integer + */ + public static function createCategory($data) + { + $categoryModel = Factory::getApplication()->bootComponent('com_categories') + ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); + $categoryModel->save($data); + + $catid = $categoryModel->getState('category.id'); + + return $catid; + } } diff --git a/administrator/components/com_categories/src/Helper/CategoryAssociationHelper.php b/administrator/components/com_categories/src/Helper/CategoryAssociationHelper.php index 17ada8cf21648..f6c00756b9de8 100644 --- a/administrator/components/com_categories/src/Helper/CategoryAssociationHelper.php +++ b/administrator/components/com_categories/src/Helper/CategoryAssociationHelper.php @@ -1,4 +1,5 @@ $item) - { - if (class_exists($helperClassname) && \is_callable(array($helperClassname, 'getCategoryRoute'))) - { - $return[$tag] = $helperClassname::getCategoryRoute($item, $tag, $layout); - } - else - { - $viewLayout = $layout ? '&layout=' . $layout : ''; - - $return[$tag] = 'index.php?option=' . $extension . '&view=category&id=' . $item . $viewLayout; - } - } - } - - return $return; - } + /** + * Flag if associations are present for categories + * + * @var boolean + * @since 3.0 + */ + public static $category_association = true; + + /** + * Method to get the associations for a given category + * + * @param integer $id Id of the item + * @param string $extension Name of the component + * @param string|null $layout Category layout + * + * @return array Array of associations for the component categories + * + * @since 3.0 + */ + public static function getCategoryAssociations($id = 0, $extension = 'com_content', $layout = null) + { + $return = array(); + + if ($id) { + $helperClassname = ucfirst(substr($extension, 4)) . 'HelperRoute'; + + $associations = CategoriesHelper::getAssociations($id, $extension); + + foreach ($associations as $tag => $item) { + if (class_exists($helperClassname) && \is_callable(array($helperClassname, 'getCategoryRoute'))) { + $return[$tag] = $helperClassname::getCategoryRoute($item, $tag, $layout); + } else { + $viewLayout = $layout ? '&layout=' . $layout : ''; + + $return[$tag] = 'index.php?option=' . $extension . '&view=category&id=' . $item . $viewLayout; + } + } + } + + return $return; + } } diff --git a/administrator/components/com_categories/src/Model/CategoriesModel.php b/administrator/components/com_categories/src/Model/CategoriesModel.php index 5637b90d2446a..b7d8b77144117 100644 --- a/administrator/components/com_categories/src/Model/CategoriesModel.php +++ b/administrator/components/com_categories/src/Model/CategoriesModel.php @@ -1,4 +1,5 @@ input->get('forcedLanguage', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', 'com_content', 'cmd'); - - $this->setState('filter.extension', $extension); - $parts = explode('.', $extension); - - // Extract the component name - $this->setState('filter.component', $parts[0]); - - // Extract the optional section name - $this->setState('filter.section', (\count($parts) > 1) ? $parts[1] : null); - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language. - if (!empty($forcedLanguage)) - { - $this->setState('filter.language', $forcedLanguage); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.extension'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . $this->getState('filter.level'); - $id .= ':' . serialize($this->getState('filter.tag')); - - return parent::getStoreId($id); - } - - /** - * Method to get a database query to list categories. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.id, a.title, a.alias, a.note, a.published, a.access' . - ', a.checked_out, a.checked_out_time, a.created_user_id' . - ', a.path, a.parent_id, a.level, a.lft, a.rgt' . - ', a.language' - ) - ); - $query->from($db->quoteName('#__categories', 'a')); - - // Join over the language - $query->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - ] - ) - ->join( - 'LEFT', - $db->quoteName('#__languages', 'l'), - $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language') - ); - - // Join over the users for the checked out user. - $query->select($db->quoteName('uc.name', 'editor')) - ->join( - 'LEFT', - $db->quoteName('#__users', 'uc'), - $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out') - ); - - // Join over the asset groups. - $query->select($db->quoteName('ag.title', 'access_level')) - ->join( - 'LEFT', - $db->quoteName('#__viewlevels', 'ag'), - $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access') - ); - - // Join over the users for the author. - $query->select($db->quoteName('ua.name', 'author_name')) - ->join( - 'LEFT', - $db->quoteName('#__users', 'ua'), - $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_user_id') - ); - - // Join over the associations. - $assoc = $this->getAssoc(); - - if ($assoc) - { - $query->select('COUNT(asso2.id)>1 as association') - ->join( - 'LEFT', - $db->quoteName('#__associations', 'asso'), - $db->quoteName('asso.id') . ' = ' . $db->quoteName('a.id') - . ' AND ' . $db->quoteName('asso.context') . ' = ' . $db->quote('com_categories.item') - ) - ->join( - 'LEFT', - $db->quoteName('#__associations', 'asso2'), - $db->quoteName('asso2.key') . ' = ' . $db->quoteName('asso.key') - ) - ->group('a.id, l.title, uc.name, ag.title, ua.name'); - } - - // Filter by extension - if ($extension = $this->getState('filter.extension')) - { - $query->where($db->quoteName('a.extension') . ' = :extension') - ->bind(':extension', $extension); - } - - // Filter on the level. - if ($level = (int) $this->getState('filter.level')) - { - $query->where($db->quoteName('a.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter by access level. - if ($access = (int) $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - } - - // Filter by published state - $published = (string) $this->getState('filter.published'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->whereIn($db->quoteName('a.published'), [0, 1]); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.title') . ' LIKE :title', - $db->quoteName('a.alias') . ' LIKE :alias', - $db->quoteName('a.note') . ' LIKE :note', - ], - 'OR' - ) - ->bind(':title', $search) - ->bind(':alias', $search) - ->bind(':note', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - - // Filter by a single or group of tags. - $tag = $this->getState('filter.tag'); - $typeAlias = $extension . '.category'; - - // Run simplified query when filtering by one tag. - if (\is_array($tag) && \count($tag) === 1) - { - $tag = $tag[0]; - } - - if ($tag && \is_array($tag)) - { - $tag = ArrayHelper::toInteger($tag); - - $subQuery = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('content_item_id')) - ->from($db->quoteName('#__contentitem_tag_map')) - ->where( - [ - $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', - $db->quoteName('type_alias') . ' = :typeAlias', - ] - ); - - $query->join( - 'INNER', - '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ) - ->bind(':typeAlias', $typeAlias); - } - elseif ($tag = (int) $tag) - { - $query->join( - 'INNER', - $db->quoteName('#__contentitem_tag_map', 'tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ) - ->where( - [ - $db->quoteName('tagmap.tag_id') . ' = :tag', - $db->quoteName('tagmap.type_alias') . ' = :typeAlias', - ] - ) - ->bind(':tag', $tag, ParameterType::INTEGER) - ->bind(':typeAlias', $typeAlias); - } - - // Add the list ordering clause - $listOrdering = $this->getState('list.ordering', 'a.lft'); - $listDirn = $db->escape($this->getState('list.direction', 'ASC')); - - if ($listOrdering == 'a.access') - { - $query->order('a.access ' . $listDirn . ', a.lft ' . $listDirn); - } - else - { - $query->order($db->escape($listOrdering) . ' ' . $listDirn); - } - - // Group by on Categories for \JOIN with component tables to count items - $query->group('a.id, + /** + * Does an association exist? Caches the result of getAssoc(). + * + * @var boolean|null + * @since 4.0.5 + */ + private $hasAssociation; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface|null $factory The factory. + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'alias', 'a.alias', + 'published', 'a.published', + 'access', 'a.access', 'access_level', + 'language', 'a.language', 'language_title', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'created_time', 'a.created_time', + 'created_user_id', 'a.created_user_id', + 'lft', 'a.lft', + 'rgt', 'a.rgt', + 'level', 'a.level', + 'path', 'a.path', + 'tag', + ); + } + + if (Associations::isEnabled()) { + $config['filter_fields'][] = 'association'; + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', 'com_content', 'cmd'); + + $this->setState('filter.extension', $extension); + $parts = explode('.', $extension); + + // Extract the component name + $this->setState('filter.component', $parts[0]); + + // Extract the optional section name + $this->setState('filter.section', (\count($parts) > 1) ? $parts[1] : null); + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language. + if (!empty($forcedLanguage)) { + $this->setState('filter.language', $forcedLanguage); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.extension'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . $this->getState('filter.level'); + $id .= ':' . serialize($this->getState('filter.tag')); + + return parent::getStoreId($id); + } + + /** + * Method to get a database query to list categories. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.title, a.alias, a.note, a.published, a.access' . + ', a.checked_out, a.checked_out_time, a.created_user_id' . + ', a.path, a.parent_id, a.level, a.lft, a.rgt' . + ', a.language' + ) + ); + $query->from($db->quoteName('#__categories', 'a')); + + // Join over the language + $query->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + ] + ) + ->join( + 'LEFT', + $db->quoteName('#__languages', 'l'), + $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language') + ); + + // Join over the users for the checked out user. + $query->select($db->quoteName('uc.name', 'editor')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'uc'), + $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out') + ); + + // Join over the asset groups. + $query->select($db->quoteName('ag.title', 'access_level')) + ->join( + 'LEFT', + $db->quoteName('#__viewlevels', 'ag'), + $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access') + ); + + // Join over the users for the author. + $query->select($db->quoteName('ua.name', 'author_name')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'ua'), + $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_user_id') + ); + + // Join over the associations. + $assoc = $this->getAssoc(); + + if ($assoc) { + $query->select('COUNT(asso2.id)>1 as association') + ->join( + 'LEFT', + $db->quoteName('#__associations', 'asso'), + $db->quoteName('asso.id') . ' = ' . $db->quoteName('a.id') + . ' AND ' . $db->quoteName('asso.context') . ' = ' . $db->quote('com_categories.item') + ) + ->join( + 'LEFT', + $db->quoteName('#__associations', 'asso2'), + $db->quoteName('asso2.key') . ' = ' . $db->quoteName('asso.key') + ) + ->group('a.id, l.title, uc.name, ag.title, ua.name'); + } + + // Filter by extension + if ($extension = $this->getState('filter.extension')) { + $query->where($db->quoteName('a.extension') . ' = :extension') + ->bind(':extension', $extension); + } + + // Filter on the level. + if ($level = (int) $this->getState('filter.level')) { + $query->where($db->quoteName('a.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter by access level. + if ($access = (int) $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + } + + // Filter by published state + $published = (string) $this->getState('filter.published'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->whereIn($db->quoteName('a.published'), [0, 1]); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.title') . ' LIKE :title', + $db->quoteName('a.alias') . ' LIKE :alias', + $db->quoteName('a.note') . ' LIKE :note', + ], + 'OR' + ) + ->bind(':title', $search) + ->bind(':alias', $search) + ->bind(':note', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + + // Filter by a single or group of tags. + $tag = $this->getState('filter.tag'); + $typeAlias = $extension . '.category'; + + // Run simplified query when filtering by one tag. + if (\is_array($tag) && \count($tag) === 1) { + $tag = $tag[0]; + } + + if ($tag && \is_array($tag)) { + $tag = ArrayHelper::toInteger($tag); + + $subQuery = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('content_item_id')) + ->from($db->quoteName('#__contentitem_tag_map')) + ->where( + [ + $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', + $db->quoteName('type_alias') . ' = :typeAlias', + ] + ); + + $query->join( + 'INNER', + '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ) + ->bind(':typeAlias', $typeAlias); + } elseif ($tag = (int) $tag) { + $query->join( + 'INNER', + $db->quoteName('#__contentitem_tag_map', 'tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ) + ->where( + [ + $db->quoteName('tagmap.tag_id') . ' = :tag', + $db->quoteName('tagmap.type_alias') . ' = :typeAlias', + ] + ) + ->bind(':tag', $tag, ParameterType::INTEGER) + ->bind(':typeAlias', $typeAlias); + } + + // Add the list ordering clause + $listOrdering = $this->getState('list.ordering', 'a.lft'); + $listDirn = $db->escape($this->getState('list.direction', 'ASC')); + + if ($listOrdering == 'a.access') { + $query->order('a.access ' . $listDirn . ', a.lft ' . $listDirn); + } else { + $query->order($db->escape($listOrdering) . ' ' . $listDirn); + } + + // Group by on Categories for \JOIN with component tables to count items + $query->group('a.id, a.title, a.alias, a.note, @@ -395,125 +371,118 @@ protected function getListQuery() l.image, uc.name, ag.title, - ua.name' - ); + ua.name'); - return $query; - } + return $query; + } - /** - * Method to determine if an association exists - * - * @return boolean True if the association exists - * - * @since 3.0 - */ - public function getAssoc() - { - if (!\is_null($this->hasAssociation)) - { - return $this->hasAssociation; - } + /** + * Method to determine if an association exists + * + * @return boolean True if the association exists + * + * @since 3.0 + */ + public function getAssoc() + { + if (!\is_null($this->hasAssociation)) { + return $this->hasAssociation; + } - $extension = $this->getState('filter.extension'); + $extension = $this->getState('filter.extension'); - $this->hasAssociation = Associations::isEnabled(); - $extension = explode('.', $extension); - $component = array_shift($extension); - $cname = str_replace('com_', '', $component); + $this->hasAssociation = Associations::isEnabled(); + $extension = explode('.', $extension); + $component = array_shift($extension); + $cname = str_replace('com_', '', $component); - if (!$this->hasAssociation || !$component || !$cname) - { - $this->hasAssociation = false; + if (!$this->hasAssociation || !$component || !$cname) { + $this->hasAssociation = false; - return $this->hasAssociation; - } + return $this->hasAssociation; + } - $componentObject = $this->bootComponent($component); + $componentObject = $this->bootComponent($component); - if ($componentObject instanceof AssociationServiceInterface && $componentObject instanceof CategoryServiceInterface) - { - $this->hasAssociation = true; + if ($componentObject instanceof AssociationServiceInterface && $componentObject instanceof CategoryServiceInterface) { + $this->hasAssociation = true; - return $this->hasAssociation; - } + return $this->hasAssociation; + } - $hname = $cname . 'HelperAssociation'; - \JLoader::register($hname, JPATH_SITE . '/components/' . $component . '/helpers/association.php'); + $hname = $cname . 'HelperAssociation'; + \JLoader::register($hname, JPATH_SITE . '/components/' . $component . '/helpers/association.php'); /* @codingStandardsIgnoreStart */ $this->hasAssociation = class_exists($hname) && !empty($hname::$category_association); /* @codingStandardsIgnoreEnd */ - return $this->hasAssociation; - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.0.1 - */ - public function getItems() - { - $items = parent::getItems(); - - if ($items != false) - { - $extension = $this->getState('filter.extension'); - - $this->countItems($items, $extension); - } - - return $items; - } - - /** - * Method to load the countItems method from the extensions - * - * @param \stdClass[] $items The category items - * @param string $extension The category extension - * - * @return void - * - * @since 3.5 - */ - public function countItems(&$items, $extension) - { - $parts = explode('.', $extension, 2); - $section = ''; - - if (\count($parts) > 1) - { - $section = $parts[1]; - } - - $component = Factory::getApplication()->bootComponent($parts[0]); - - if ($component instanceof CategoryServiceInterface) - { - $component->countItems($items, $section); - } - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - // Get the extension from the filter - $extension = $this->getState('filter.extension'); - - $query->where($this->getDatabase()->quoteName('extension') . ' = :extension') - ->bind(':extension', $extension); - - return $query; - } + return $this->hasAssociation; + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.0.1 + */ + public function getItems() + { + $items = parent::getItems(); + + if ($items != false) { + $extension = $this->getState('filter.extension'); + + $this->countItems($items, $extension); + } + + return $items; + } + + /** + * Method to load the countItems method from the extensions + * + * @param \stdClass[] $items The category items + * @param string $extension The category extension + * + * @return void + * + * @since 3.5 + */ + public function countItems(&$items, $extension) + { + $parts = explode('.', $extension, 2); + $section = ''; + + if (\count($parts) > 1) { + $section = $parts[1]; + } + + $component = Factory::getApplication()->bootComponent($parts[0]); + + if ($component instanceof CategoryServiceInterface) { + $component->countItems($items, $section); + } + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + // Get the extension from the filter + $extension = $this->getState('filter.extension'); + + $query->where($this->getDatabase()->quoteName('extension') . ' = :extension') + ->bind(':extension', $extension); + + return $query; + } } diff --git a/administrator/components/com_categories/src/Model/CategoryModel.php b/administrator/components/com_categories/src/Model/CategoryModel.php index 3cd6e49238f94..766d8f1868820 100644 --- a/administrator/components/com_categories/src/Model/CategoryModel.php +++ b/administrator/components/com_categories/src/Model/CategoryModel.php @@ -1,4 +1,5 @@ input->get('extension', 'com_content'); - $this->typeAlias = $extension . '.category'; - - // Add a new batch command - $this->batch_commands['flip_ordering'] = 'batchFlipordering'; - - parent::__construct($config, $factory); - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - return Factory::getUser()->authorise('core.delete', $record->extension . '.category.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - - // Check for existing category. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $record->extension . '.category.' . (int) $record->id); - } - - // New category, so check against the parent. - if (!empty($record->parent_id)) - { - return $user->authorise('core.edit.state', $record->extension . '.category.' . (int) $record->parent_id); - } - - // Default to component settings if neither category nor parent known. - return $user->authorise('core.edit.state', $record->extension); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\Table\Table A Table object - * - * @since 1.6 - */ - public function getTable($type = 'Category', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * Auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - $parentId = $app->input->getInt('parent_id'); - $this->setState('category.parent_id', $parentId); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState($this->getName() . '.id', $pk); - - $extension = $app->input->get('extension', 'com_content'); - $this->setState('category.extension', $extension); - $parts = explode('.', $extension); - - // Extract the component name - $this->setState('category.component', $parts[0]); - - // Extract the optional section name - $this->setState('category.section', (\count($parts) > 1) ? $parts[1] : null); - - // Load the parameters. - $params = ComponentHelper::getParams('com_categories'); - $this->setState('params', $params); - } - - /** - * Method to get a category. - * - * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. - * - * @return mixed Category data object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - if ($result = parent::getItem($pk)) - { - // Prime required properties. - if (empty($result->id)) - { - $result->parent_id = $this->getState('category.parent_id'); - $result->extension = $this->getState('category.extension'); - } - - // Convert the metadata field to an array. - $registry = new Registry($result->metadata); - $result->metadata = $registry->toArray(); - - if (!empty($result->id)) - { - $result->tags = new TagsHelper; - $result->tags->getTagIds($result->id, $result->extension . '.category'); - } - } - - $assoc = $this->getAssoc(); - - if ($assoc) - { - if ($result->id != null) - { - $result->associations = ArrayHelper::toInteger(CategoriesHelper::getAssociations($result->id, $result->extension)); - } - else - { - $result->associations = array(); - } - } - - return $result; - } - - /** - * Method to get the row form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A JForm object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - $extension = $this->getState('category.extension'); - $jinput = Factory::getApplication()->input; - - // A workaround to get the extension into the model for save requests. - if (empty($extension) && isset($data['extension'])) - { - $extension = $data['extension']; - $parts = explode('.', $extension); - - $this->setState('category.extension', $extension); - $this->setState('category.component', $parts[0]); - $this->setState('category.section', @$parts[1]); - } - - // Get the form. - $form = $this->loadForm('com_categories.category' . $extension, 'category', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on Edit State access controls. - if (empty($data['extension'])) - { - $data['extension'] = $extension; - } - - $categoryId = $jinput->get('id'); - $parts = explode('.', $extension); - $assetKey = $categoryId ? $extension . '.category.' . $categoryId : $parts[0]; - - if (!Factory::getUser()->authorise('core.edit.state', $assetKey)) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - // Don't allow to change the created_user_id user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_user_id', 'filter', 'unset'); - } - - return $form; - } - - /** - * A protected method to get the where clause for the reorder - * This ensures that the row will be moved relative to a row with the same extension - * - * @param Category $table Current table instance - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - $db = $this->getDatabase(); - - return [ - $db->quoteName('extension') . ' = ' . $db->quote($table->extension), - ]; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_categories.edit.' . $this->getName() . '.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Pre-select some filters (Status, Language, Access) in edit form if those have been selected in Category Manager - if (!$data->id) - { - // Check for which extension the Category Manager is used and get selected fields - $extension = substr($app->getUserState('com_categories.categories.filter.extension', ''), 4); - $filters = (array) $app->getUserState('com_categories.categories.' . $extension . '.filter'); - - $data->set( - 'published', - $app->input->getInt( - 'published', - ((isset($filters['published']) && $filters['published'] !== '') ? $filters['published'] : null) - ) - ); - $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); - $data->set( - 'access', - $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) - ); - } - } - - $this->preprocessData('com_categories.category', $data); - - return $data; - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see JFormRule - * @see JFilterInput - * @since 3.9.23 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', $data['extension'])) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import. - * - * @return mixed - * - * @since 1.6 - * - * @throws \Exception if there is an error in the form event. - * - * @see \Joomla\CMS\Form\FormField - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $lang = Factory::getLanguage(); - $component = $this->getState('category.component'); - $section = $this->getState('category.section'); - $extension = Factory::getApplication()->input->get('extension', null); - - // Get the component form if it exists - $name = 'category' . ($section ? ('.' . $section) : ''); - - // Looking first in the component forms folder - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/forms/$name.xml"); - - // Looking in the component models/forms folder (J! 3) - if (!file_exists($path)) - { - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/models/forms/$name.xml"); - } - - // Old way: looking in the component folder - if (!file_exists($path)) - { - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/$name.xml"); - } - - if (file_exists($path)) - { - $lang->load($component, JPATH_BASE); - $lang->load($component, JPATH_BASE . '/components/' . $component); - - if (!$form->loadFile($path, false)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - - $componentInterface = Factory::getApplication()->bootComponent($component); - - if ($componentInterface instanceof CategoryServiceInterface) - { - $componentInterface->prepareForm($form, $data); - } - else - { - // Try to find the component helper. - $eName = str_replace('com_', '', $component); - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/helpers/category.php"); - - if (file_exists($path)) - { - $cName = ucfirst($eName) . ucfirst($section) . 'HelperCategory'; - - \JLoader::register($cName, $path); - - if (class_exists($cName) && \is_callable(array($cName, 'onPrepareForm'))) - { - $lang->load($component, JPATH_BASE, null, false, false) - || $lang->load($component, JPATH_BASE . '/components/' . $component, null, false, false) - || $lang->load($component, JPATH_BASE, $lang->getDefault(), false, false) - || $lang->load($component, JPATH_BASE . '/components/' . $component, $lang->getDefault(), false, false); - \call_user_func_array(array($cName, 'onPrepareForm'), array(&$form)); - - // Check for an error. - if ($form instanceof \Exception) - { - $this->setError($form->getMessage()); - - return false; - } - } - } - } - - // Set the access control rules field component value. - $form->setFieldAttribute('rules', 'component', $component); - $form->setFieldAttribute('rules', 'section', $name); - - // Association category items - if ($this->getAssoc()) - { - $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); - - if (\count($languages) > 1) - { - $addform = new \SimpleXMLElement('
    '); - $fields = $addform->addChild('fields'); - $fields->addAttribute('name', 'associations'); - $fieldset = $fields->addChild('fieldset'); - $fieldset->addAttribute('name', 'item_associations'); - - foreach ($languages as $language) - { - $field = $fieldset->addChild('field'); - $field->addAttribute('name', $language->lang_code); - $field->addAttribute('type', 'modal_category'); - $field->addAttribute('language', $language->lang_code); - $field->addAttribute('label', $language->title); - $field->addAttribute('translate_label', 'false'); - $field->addAttribute('extension', $extension); - $field->addAttribute('select', 'true'); - $field->addAttribute('new', 'true'); - $field->addAttribute('edit', 'true'); - $field->addAttribute('clear', 'true'); - $field->addAttribute('propagate', 'true'); - } - - $form->load($addform, false); - } - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $table = $this->getTable(); - $input = Factory::getApplication()->input; - $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState($this->getName() . '.id'); - $isNew = true; - $context = $this->option . '.' . $this->name; - - if (!empty($data['tags']) && $data['tags'][0] != '') - { - $table->newTags = $data['tags']; - } - - // Include the plugins for the save events. - PluginHelper::importPlugin($this->events_map['save']); - - // Load the row if saving an existing category. - if ($pk > 0) - { - $table->load($pk); - $isNew = false; - } - - // Set the new parent id if parent id not matched OR while New/Save as Copy . - if ($table->parent_id != $data['parent_id'] || $data['id'] == 0) - { - $table->setLocation($data['parent_id'], 'last-child'); - } - - // Alter the title for save as copy - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['title'] == $origTable->title) - { - [$title, $alias] = $this->generateNewTitle($data['parent_id'], $data['alias'], $data['title']); - $data['title'] = $title; - $data['alias'] = $alias; - } - else - { - if ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - } - - $data['published'] = 0; - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Bind the rules. - if (isset($data['rules'])) - { - $rules = new Rules($data['rules']); - $table->setRules($rules); - } - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew, $data)); - - if (\in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - $assoc = $this->getAssoc(); - - if ($assoc) - { - // Adding self to the association - $associations = $data['associations'] ?? array(); - - // Unset any invalid associations - $associations = ArrayHelper::toInteger($associations); - - foreach ($associations as $tag => $id) - { - if (!$id) - { - unset($associations[$tag]); - } - } - - // Detecting all item menus - $allLanguage = $table->language == '*'; - - if ($allLanguage && !empty($associations)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CATEGORIES_ERROR_ALL_LANGUAGE_ASSOCIATED'), 'notice'); - } - - // Get associationskey for edited item - $db = $this->getDatabase(); - $id = (int) $table->id; - $query = $db->getQuery(true) - ->select($db->quoteName('key')) - ->from($db->quoteName('#__associations')) - ->where($db->quoteName('context') . ' = :associationscontext') - ->where($db->quoteName('id') . ' = :id') - ->bind(':associationscontext', $this->associationsContext) - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $oldKey = $db->loadResult(); - - if ($associations || $oldKey !== null) - { - $where = []; - - // Deleting old associations for the associated items - $query = $db->getQuery(true) - ->delete($db->quoteName('#__associations')) - ->where($db->quoteName('context') . ' = :associationscontext') - ->bind(':associationscontext', $this->associationsContext); - - if ($associations) - { - $where[] = $db->quoteName('id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')'; - } - - if ($oldKey !== null) - { - $where[] = $db->quoteName('key') . ' = :oldKey'; - $query->bind(':oldKey', $oldKey); - } - - $query->extendWhere('AND', $where, 'OR'); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Adding self to the association - if (!$allLanguage) - { - $associations[$table->language] = (int) $table->id; - } - - if (\count($associations) > 1) - { - // Adding new association for these items - $key = md5(json_encode($associations)); - $query->clear() - ->insert($db->quoteName('#__associations')) - ->columns( - [ - $db->quoteName('id'), - $db->quoteName('context'), - $db->quoteName('key'), - ] - ); - - foreach ($associations as $id) - { - $id = (int) $id; - - $query->values( - implode( - ',', - $query->bindArray( - [$id, $this->associationsContext, $key], - [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] - ) - ) - ); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew, $data)); - - // Rebuild the path for the category: - if (!$table->rebuildPath($table->id)) - { - $this->setError($table->getError()); - - return false; - } - - // Rebuild the paths of the category's children: - if (!$table->rebuild($table->id, $table->lft, $table->level, $table->path)) - { - $this->setError($table->getError()); - - return false; - } - - $this->setState($this->getName() . '.id', $table->id); - - if (Factory::getApplication()->input->get('task') == 'editAssociations') - { - return $this->redirectToAssociations($data); - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the published state of one or more records. - * - * @param array $pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function publish(&$pks, $value = 1) - { - if (parent::publish($pks, $value)) - { - $extension = Factory::getApplication()->input->get('extension'); - - // Include the content plugins for the change of category state event. - PluginHelper::importPlugin('content'); - - // Trigger the onCategoryChangeState event. - Factory::getApplication()->triggerEvent('onCategoryChangeState', array($extension, $pks, $value)); - - return true; - } - } - - /** - * Method rebuild the entire nested set tree. - * - * @return boolean False on failure or error, true otherwise. - * - * @since 1.6 - */ - public function rebuild() - { - // Get an instance of the table object. - $table = $this->getTable(); - - if (!$table->rebuild()) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to save the reordered nested set tree. - * First we save the new order values in the lft values of the changed ids. - * Then we invoke the table rebuild to implement the new ordering. - * - * @param array $idArray An array of primary key ids. - * @param integer $lftArray The lft value - * - * @return boolean False on failure or error, True otherwise - * - * @since 1.6 - */ - public function saveorder($idArray = null, $lftArray = null) - { - // Get an instance of the table object. - $table = $this->getTable(); - - if (!$table->saveorder($idArray, $lftArray)) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Batch flip category ordering. - * - * @param integer $value The new category. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return mixed An array of new IDs on success, boolean false on failure. - * - * @since 3.6.3 - */ - protected function batchFlipordering($value, $pks, $contexts) - { - $successful = array(); - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - /** - * For each category get the max ordering value - * Re-order with max - ordering - */ - foreach ($pks as $id) - { - $query->select('MAX(' . $db->quoteName('ordering') . ')') - ->from($db->quoteName('#__content')) - ->where($db->quoteName('catid') . ' = :catid') - ->bind(':catid', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - $max = (int) $db->loadResult(); - $max++; - - $query->clear(); - - $query->update($db->quoteName('#__content')) - ->set($db->quoteName('ordering') . ' = :max - ' . $db->quoteName('ordering')) - ->where($db->quoteName('catid') . ' = :catid') - ->bind(':max', $max, ParameterType::INTEGER) - ->bind(':catid', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - if ($db->execute()) - { - $successful[] = $id; - } - } - - return empty($successful) ? false : $successful; - } - - /** - * Batch copy categories to a new category. - * - * @param integer $value The new category. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return mixed An array of new IDs on success, boolean false on failure. - * - * @since 1.6 - */ - protected function batchCopy($value, $pks, $contexts) - { - $type = new UCMType; - $this->type = $type->getTypeByAlias($this->typeAlias); - - // $value comes as {parent_id}.{extension} - $parts = explode('.', $value); - $parentId = (int) ArrayHelper::getValue($parts, 0, 1); - - $db = $this->getDatabase(); - $extension = Factory::getApplication()->input->get('extension', '', 'word'); - $newIds = array(); - - // Check that the parent exists - if ($parentId) - { - if (!$this->table->load($parentId)) - { - if ($error = $this->table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Non-fatal error - $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); - $parentId = 0; - } - } - - // Check that user has create permission for parent category - if ($parentId == $this->table->getRootId()) - { - $canCreate = $this->user->authorise('core.create', $extension); - } - else - { - $canCreate = $this->user->authorise('core.create', $extension . '.category.' . $parentId); - } - - if (!$canCreate) - { - // Error since user cannot create in parent category - $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); - - return false; - } - } - - // If the parent is 0, set it to the ID of the root item in the tree - if (empty($parentId)) - { - if (!$parentId = $this->table->getRootId()) - { - $this->setError($this->table->getError()); - - return false; - } - // Make sure we can create in root - elseif (!$this->user->authorise('core.create', $extension)) - { - $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); - - return false; - } - } - - // We need to log the parent ID - $parents = array(); - - // Calculate the emergency stop count as a precaution against a runaway loop bug - $query = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName('id') . ')') - ->from($db->quoteName('#__categories')); - $db->setQuery($query); - - try - { - $count = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Parent exists so let's proceed - while (!empty($pks) && $count > 0) - { - // Pop the first id off the stack - $pk = array_shift($pks); - - $this->table->reset(); - - // Check that the row actually exists - if (!$this->table->load($pk)) - { - if ($error = $this->table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Not fatal error - $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); - continue; - } - } - - // Copy is a bit tricky, because we also need to copy the children - $lft = (int) $this->table->lft; - $rgt = (int) $this->table->rgt; - $query->clear() - ->select($db->quoteName('id')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('lft') . ' > :lft') - ->where($db->quoteName('rgt') . ' < :rgt') - ->bind(':lft', $lft, ParameterType::INTEGER) - ->bind(':rgt', $rgt, ParameterType::INTEGER); - $db->setQuery($query); - $childIds = $db->loadColumn(); - - // Add child ID's to the array only if they aren't already there. - foreach ($childIds as $childId) - { - if (!\in_array($childId, $pks)) - { - $pks[] = $childId; - } - } - - // Make a copy of the old ID, Parent ID and Asset ID - $oldId = $this->table->id; - $oldParentId = $this->table->parent_id; - $oldAssetId = $this->table->asset_id; - - // Reset the id because we are making a copy. - $this->table->id = 0; - - // If we a copying children, the Old ID will turn up in the parents list - // otherwise it's a new top level item - $this->table->parent_id = $parents[$oldParentId] ?? $parentId; - - // Set the new location in the tree for the node. - $this->table->setLocation($this->table->parent_id, 'last-child'); - - // @TODO: Deal with ordering? - // $this->table->ordering = 1; - $this->table->level = null; - $this->table->asset_id = null; - $this->table->lft = null; - $this->table->rgt = null; - - // Alter the title & alias - [$title, $alias] = $this->generateNewTitle($this->table->parent_id, $this->table->alias, $this->table->title); - $this->table->title = $title; - $this->table->alias = $alias; - - // Unpublish because we are making a copy - $this->table->published = 0; - - // Store the row. - if (!$this->table->store()) - { - $this->setError($this->table->getError()); - - return false; - } - - // Get the new item ID - $newId = $this->table->get('id'); - - // Add the new ID to the array - $newIds[$pk] = $newId; - - // Copy rules - $query->clear() - ->update($db->quoteName('#__assets', 't')) - ->join('INNER', - $db->quoteName('#__assets', 's'), - $db->quoteName('s.id') . ' = :oldid' - ) - ->bind(':oldid', $oldAssetId, ParameterType::INTEGER) - ->set($db->quoteName('t.rules') . ' = ' . $db->quoteName('s.rules')) - ->where($db->quoteName('t.id') . ' = :assetid') - ->bind(':assetid', $this->table->asset_id, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - // Now we log the old 'parent' to the new 'parent' - $parents[$oldId] = $this->table->id; - $count--; - } - - // Rebuild the hierarchy. - if (!$this->table->rebuild()) - { - $this->setError($this->table->getError()); - - return false; - } - - // Rebuild the tree path. - if (!$this->table->rebuildPath($this->table->id)) - { - $this->setError($this->table->getError()); - - return false; - } - - return $newIds; - } - - /** - * Batch move categories to a new category. - * - * @param integer $value The new category ID. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True on success. - * - * @since 1.6 - */ - protected function batchMove($value, $pks, $contexts) - { - $parentId = (int) $value; - $type = new UCMType; - $this->type = $type->getTypeByAlias($this->typeAlias); - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $extension = Factory::getApplication()->input->get('extension', '', 'word'); - - // Check that the parent exists. - if ($parentId) - { - if (!$this->table->load($parentId)) - { - if ($error = $this->table->getError()) - { - // Fatal error. - $this->setError($error); - - return false; - } - else - { - // Non-fatal error. - $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); - $parentId = 0; - } - } - - // Check that user has create permission for parent category. - if ($parentId == $this->table->getRootId()) - { - $canCreate = $this->user->authorise('core.create', $extension); - } - else - { - $canCreate = $this->user->authorise('core.create', $extension . '.category.' . $parentId); - } - - if (!$canCreate) - { - // Error since user cannot create in parent category - $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); - - return false; - } - - // Check that user has edit permission for every category being moved - // Note that the entire batch operation fails if any category lacks edit permission - foreach ($pks as $pk) - { - if (!$this->user->authorise('core.edit', $extension . '.category.' . $pk)) - { - // Error since user cannot edit this category - $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_EDIT')); - - return false; - } - } - } - - // We are going to store all the children and just move the category - $children = array(); - - // Parent exists so let's proceed - foreach ($pks as $pk) - { - // Check that the row actually exists - if (!$this->table->load($pk)) - { - if ($error = $this->table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Not fatal error - $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); - continue; - } - } - - // Set the new location in the tree for the node. - $this->table->setLocation($parentId, 'last-child'); - - // Check if we are moving to a different parent - if ($parentId != $this->table->parent_id) - { - $lft = (int) $this->table->lft; - $rgt = (int) $this->table->rgt; - - // Add the child node ids to the children array. - $query->clear() - ->select($db->quoteName('id')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('lft') . ' BETWEEN :lft AND :rgt') - ->bind(':lft', $lft, ParameterType::INTEGER) - ->bind(':rgt', $rgt, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $children = array_merge($children, (array) $db->loadColumn()); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - // Store the row. - if (!$this->table->store()) - { - $this->setError($this->table->getError()); - - return false; - } - - // Rebuild the tree path. - if (!$this->table->rebuildPath()) - { - $this->setError($this->table->getError()); - - return false; - } - } - - // Process the child rows - if (!empty($children)) - { - // Remove any duplicates and sanitize ids. - $children = array_unique($children); - $children = ArrayHelper::toInteger($children); - } - - return true; - } - - /** - * Custom clean the cache of com_content and content modules - * - * @param string $group Cache group name. - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - $extension = Factory::getApplication()->input->get('extension'); - - switch ($extension) - { - case 'com_content': - parent::cleanCache('com_content'); - parent::cleanCache('mod_articles_archive'); - parent::cleanCache('mod_articles_categories'); - parent::cleanCache('mod_articles_category'); - parent::cleanCache('mod_articles_latest'); - parent::cleanCache('mod_articles_news'); - parent::cleanCache('mod_articles_popular'); - break; - default: - parent::cleanCache($extension); - break; - } - } - - /** - * Method to change the title & alias. - * - * @param integer $parentId The id of the parent. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 1.7 - */ - protected function generateNewTitle($parentId, $alias, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) - { - $title = StringHelper::increment($title); - $alias = StringHelper::increment($alias, 'dash'); - } - - return array($title, $alias); - } - - /** - * Method to determine if a category association is available. - * - * @return boolean True if a category association is available; false otherwise. - */ - public function getAssoc() - { - if (!\is_null($this->hasAssociation)) - { - return $this->hasAssociation; - } - - $extension = $this->getState('category.extension', ''); - - $this->hasAssociation = Associations::isEnabled(); - $extension = explode('.', $extension); - $component = array_shift($extension); - $cname = str_replace('com_', '', $component); - - if (!$this->hasAssociation || !$component || !$cname) - { - $this->hasAssociation = false; - - return $this->hasAssociation; - } - - $componentObject = $this->bootComponent($component); - - if ($componentObject instanceof AssociationServiceInterface && $componentObject instanceof CategoryServiceInterface) - { - $this->hasAssociation = true; - - return $this->hasAssociation; - } - - $hname = $cname . 'HelperAssociation'; - \JLoader::register($hname, JPATH_SITE . '/components/' . $component . '/helpers/association.php'); - - $this->hasAssociation = class_exists($hname) && !empty($hname::$category_association); - - return $this->hasAssociation; - } + use VersionableModelTrait; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_CATEGORIES'; + + /** + * The type alias for this content type. Used for content version history. + * + * @var string + * @since 3.2 + */ + public $typeAlias = null; + + /** + * The context used for the associations table + * + * @var string + * @since 3.4.4 + */ + protected $associationsContext = 'com_categories.item'; + + /** + * Does an association exist? Caches the result of getAssoc(). + * + * @var boolean|null + * @since 3.10.4 + */ + private $hasAssociation; + + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface|null $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $extension = Factory::getApplication()->input->get('extension', 'com_content'); + $this->typeAlias = $extension . '.category'; + + // Add a new batch command + $this->batch_commands['flip_ordering'] = 'batchFlipordering'; + + parent::__construct($config, $factory); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + return Factory::getUser()->authorise('core.delete', $record->extension . '.category.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + + // Check for existing category. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $record->extension . '.category.' . (int) $record->id); + } + + // New category, so check against the parent. + if (!empty($record->parent_id)) { + return $user->authorise('core.edit.state', $record->extension . '.category.' . (int) $record->parent_id); + } + + // Default to component settings if neither category nor parent known. + return $user->authorise('core.edit.state', $record->extension); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\Table\Table A Table object + * + * @since 1.6 + */ + public function getTable($type = 'Category', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + $parentId = $app->input->getInt('parent_id'); + $this->setState('category.parent_id', $parentId); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState($this->getName() . '.id', $pk); + + $extension = $app->input->get('extension', 'com_content'); + $this->setState('category.extension', $extension); + $parts = explode('.', $extension); + + // Extract the component name + $this->setState('category.component', $parts[0]); + + // Extract the optional section name + $this->setState('category.section', (\count($parts) > 1) ? $parts[1] : null); + + // Load the parameters. + $params = ComponentHelper::getParams('com_categories'); + $this->setState('params', $params); + } + + /** + * Method to get a category. + * + * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. + * + * @return mixed Category data object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + if ($result = parent::getItem($pk)) { + // Prime required properties. + if (empty($result->id)) { + $result->parent_id = $this->getState('category.parent_id'); + $result->extension = $this->getState('category.extension'); + } + + // Convert the metadata field to an array. + $registry = new Registry($result->metadata); + $result->metadata = $registry->toArray(); + + if (!empty($result->id)) { + $result->tags = new TagsHelper(); + $result->tags->getTagIds($result->id, $result->extension . '.category'); + } + } + + $assoc = $this->getAssoc(); + + if ($assoc) { + if ($result->id != null) { + $result->associations = ArrayHelper::toInteger(CategoriesHelper::getAssociations($result->id, $result->extension)); + } else { + $result->associations = array(); + } + } + + return $result; + } + + /** + * Method to get the row form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A JForm object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + $extension = $this->getState('category.extension'); + $jinput = Factory::getApplication()->input; + + // A workaround to get the extension into the model for save requests. + if (empty($extension) && isset($data['extension'])) { + $extension = $data['extension']; + $parts = explode('.', $extension); + + $this->setState('category.extension', $extension); + $this->setState('category.component', $parts[0]); + $this->setState('category.section', @$parts[1]); + } + + // Get the form. + $form = $this->loadForm('com_categories.category' . $extension, 'category', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on Edit State access controls. + if (empty($data['extension'])) { + $data['extension'] = $extension; + } + + $categoryId = $jinput->get('id'); + $parts = explode('.', $extension); + $assetKey = $categoryId ? $extension . '.category.' . $categoryId : $parts[0]; + + if (!Factory::getUser()->authorise('core.edit.state', $assetKey)) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + // Don't allow to change the created_user_id user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_user_id', 'filter', 'unset'); + } + + return $form; + } + + /** + * A protected method to get the where clause for the reorder + * This ensures that the row will be moved relative to a row with the same extension + * + * @param Category $table Current table instance + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('extension') . ' = ' . $db->quote($table->extension), + ]; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_categories.edit.' . $this->getName() . '.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Pre-select some filters (Status, Language, Access) in edit form if those have been selected in Category Manager + if (!$data->id) { + // Check for which extension the Category Manager is used and get selected fields + $extension = substr($app->getUserState('com_categories.categories.filter.extension', ''), 4); + $filters = (array) $app->getUserState('com_categories.categories.' . $extension . '.filter'); + + $data->set( + 'published', + $app->input->getInt( + 'published', + ((isset($filters['published']) && $filters['published'] !== '') ? $filters['published'] : null) + ) + ); + $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); + $data->set( + 'access', + $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) + ); + } + } + + $this->preprocessData('com_categories.category', $data); + + return $data; + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see JFormRule + * @see JFilterInput + * @since 3.9.23 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', $data['extension'])) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import. + * + * @return mixed + * + * @since 1.6 + * + * @throws \Exception if there is an error in the form event. + * + * @see \Joomla\CMS\Form\FormField + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $lang = Factory::getLanguage(); + $component = $this->getState('category.component'); + $section = $this->getState('category.section'); + $extension = Factory::getApplication()->input->get('extension', null); + + // Get the component form if it exists + $name = 'category' . ($section ? ('.' . $section) : ''); + + // Looking first in the component forms folder + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/forms/$name.xml"); + + // Looking in the component models/forms folder (J! 3) + if (!file_exists($path)) { + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/models/forms/$name.xml"); + } + + // Old way: looking in the component folder + if (!file_exists($path)) { + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/$name.xml"); + } + + if (file_exists($path)) { + $lang->load($component, JPATH_BASE); + $lang->load($component, JPATH_BASE . '/components/' . $component); + + if (!$form->loadFile($path, false)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + + $componentInterface = Factory::getApplication()->bootComponent($component); + + if ($componentInterface instanceof CategoryServiceInterface) { + $componentInterface->prepareForm($form, $data); + } else { + // Try to find the component helper. + $eName = str_replace('com_', '', $component); + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/helpers/category.php"); + + if (file_exists($path)) { + $cName = ucfirst($eName) . ucfirst($section) . 'HelperCategory'; + + \JLoader::register($cName, $path); + + if (class_exists($cName) && \is_callable(array($cName, 'onPrepareForm'))) { + $lang->load($component, JPATH_BASE, null, false, false) + || $lang->load($component, JPATH_BASE . '/components/' . $component, null, false, false) + || $lang->load($component, JPATH_BASE, $lang->getDefault(), false, false) + || $lang->load($component, JPATH_BASE . '/components/' . $component, $lang->getDefault(), false, false); + \call_user_func_array(array($cName, 'onPrepareForm'), array(&$form)); + + // Check for an error. + if ($form instanceof \Exception) { + $this->setError($form->getMessage()); + + return false; + } + } + } + } + + // Set the access control rules field component value. + $form->setFieldAttribute('rules', 'component', $component); + $form->setFieldAttribute('rules', 'section', $name); + + // Association category items + if ($this->getAssoc()) { + $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); + + if (\count($languages) > 1) { + $addform = new \SimpleXMLElement(''); + $fields = $addform->addChild('fields'); + $fields->addAttribute('name', 'associations'); + $fieldset = $fields->addChild('fieldset'); + $fieldset->addAttribute('name', 'item_associations'); + + foreach ($languages as $language) { + $field = $fieldset->addChild('field'); + $field->addAttribute('name', $language->lang_code); + $field->addAttribute('type', 'modal_category'); + $field->addAttribute('language', $language->lang_code); + $field->addAttribute('label', $language->title); + $field->addAttribute('translate_label', 'false'); + $field->addAttribute('extension', $extension); + $field->addAttribute('select', 'true'); + $field->addAttribute('new', 'true'); + $field->addAttribute('edit', 'true'); + $field->addAttribute('clear', 'true'); + $field->addAttribute('propagate', 'true'); + } + + $form->load($addform, false); + } + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $table = $this->getTable(); + $input = Factory::getApplication()->input; + $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState($this->getName() . '.id'); + $isNew = true; + $context = $this->option . '.' . $this->name; + + if (!empty($data['tags']) && $data['tags'][0] != '') { + $table->newTags = $data['tags']; + } + + // Include the plugins for the save events. + PluginHelper::importPlugin($this->events_map['save']); + + // Load the row if saving an existing category. + if ($pk > 0) { + $table->load($pk); + $isNew = false; + } + + // Set the new parent id if parent id not matched OR while New/Save as Copy . + if ($table->parent_id != $data['parent_id'] || $data['id'] == 0) { + $table->setLocation($data['parent_id'], 'last-child'); + } + + // Alter the title for save as copy + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['title'] == $origTable->title) { + [$title, $alias] = $this->generateNewTitle($data['parent_id'], $data['alias'], $data['title']); + $data['title'] = $title; + $data['alias'] = $alias; + } else { + if ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + } + + $data['published'] = 0; + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Bind the rules. + if (isset($data['rules'])) { + $rules = new Rules($data['rules']); + $table->setRules($rules); + } + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew, $data)); + + if (\in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + $assoc = $this->getAssoc(); + + if ($assoc) { + // Adding self to the association + $associations = $data['associations'] ?? array(); + + // Unset any invalid associations + $associations = ArrayHelper::toInteger($associations); + + foreach ($associations as $tag => $id) { + if (!$id) { + unset($associations[$tag]); + } + } + + // Detecting all item menus + $allLanguage = $table->language == '*'; + + if ($allLanguage && !empty($associations)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CATEGORIES_ERROR_ALL_LANGUAGE_ASSOCIATED'), 'notice'); + } + + // Get associationskey for edited item + $db = $this->getDatabase(); + $id = (int) $table->id; + $query = $db->getQuery(true) + ->select($db->quoteName('key')) + ->from($db->quoteName('#__associations')) + ->where($db->quoteName('context') . ' = :associationscontext') + ->where($db->quoteName('id') . ' = :id') + ->bind(':associationscontext', $this->associationsContext) + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $oldKey = $db->loadResult(); + + if ($associations || $oldKey !== null) { + $where = []; + + // Deleting old associations for the associated items + $query = $db->getQuery(true) + ->delete($db->quoteName('#__associations')) + ->where($db->quoteName('context') . ' = :associationscontext') + ->bind(':associationscontext', $this->associationsContext); + + if ($associations) { + $where[] = $db->quoteName('id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')'; + } + + if ($oldKey !== null) { + $where[] = $db->quoteName('key') . ' = :oldKey'; + $query->bind(':oldKey', $oldKey); + } + + $query->extendWhere('AND', $where, 'OR'); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Adding self to the association + if (!$allLanguage) { + $associations[$table->language] = (int) $table->id; + } + + if (\count($associations) > 1) { + // Adding new association for these items + $key = md5(json_encode($associations)); + $query->clear() + ->insert($db->quoteName('#__associations')) + ->columns( + [ + $db->quoteName('id'), + $db->quoteName('context'), + $db->quoteName('key'), + ] + ); + + foreach ($associations as $id) { + $id = (int) $id; + + $query->values( + implode( + ',', + $query->bindArray( + [$id, $this->associationsContext, $key], + [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] + ) + ) + ); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew, $data)); + + // Rebuild the path for the category: + if (!$table->rebuildPath($table->id)) { + $this->setError($table->getError()); + + return false; + } + + // Rebuild the paths of the category's children: + if (!$table->rebuild($table->id, $table->lft, $table->level, $table->path)) { + $this->setError($table->getError()); + + return false; + } + + $this->setState($this->getName() . '.id', $table->id); + + if (Factory::getApplication()->input->get('task') == 'editAssociations') { + return $this->redirectToAssociations($data); + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the published state of one or more records. + * + * @param array $pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function publish(&$pks, $value = 1) + { + if (parent::publish($pks, $value)) { + $extension = Factory::getApplication()->input->get('extension'); + + // Include the content plugins for the change of category state event. + PluginHelper::importPlugin('content'); + + // Trigger the onCategoryChangeState event. + Factory::getApplication()->triggerEvent('onCategoryChangeState', array($extension, $pks, $value)); + + return true; + } + } + + /** + * Method rebuild the entire nested set tree. + * + * @return boolean False on failure or error, true otherwise. + * + * @since 1.6 + */ + public function rebuild() + { + // Get an instance of the table object. + $table = $this->getTable(); + + if (!$table->rebuild()) { + $this->setError($table->getError()); + + return false; + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to save the reordered nested set tree. + * First we save the new order values in the lft values of the changed ids. + * Then we invoke the table rebuild to implement the new ordering. + * + * @param array $idArray An array of primary key ids. + * @param integer $lftArray The lft value + * + * @return boolean False on failure or error, True otherwise + * + * @since 1.6 + */ + public function saveorder($idArray = null, $lftArray = null) + { + // Get an instance of the table object. + $table = $this->getTable(); + + if (!$table->saveorder($idArray, $lftArray)) { + $this->setError($table->getError()); + + return false; + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Batch flip category ordering. + * + * @param integer $value The new category. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return mixed An array of new IDs on success, boolean false on failure. + * + * @since 3.6.3 + */ + protected function batchFlipordering($value, $pks, $contexts) + { + $successful = array(); + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + /** + * For each category get the max ordering value + * Re-order with max - ordering + */ + foreach ($pks as $id) { + $query->select('MAX(' . $db->quoteName('ordering') . ')') + ->from($db->quoteName('#__content')) + ->where($db->quoteName('catid') . ' = :catid') + ->bind(':catid', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + $max = (int) $db->loadResult(); + $max++; + + $query->clear(); + + $query->update($db->quoteName('#__content')) + ->set($db->quoteName('ordering') . ' = :max - ' . $db->quoteName('ordering')) + ->where($db->quoteName('catid') . ' = :catid') + ->bind(':max', $max, ParameterType::INTEGER) + ->bind(':catid', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + if ($db->execute()) { + $successful[] = $id; + } + } + + return empty($successful) ? false : $successful; + } + + /** + * Batch copy categories to a new category. + * + * @param integer $value The new category. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return mixed An array of new IDs on success, boolean false on failure. + * + * @since 1.6 + */ + protected function batchCopy($value, $pks, $contexts) + { + $type = new UCMType(); + $this->type = $type->getTypeByAlias($this->typeAlias); + + // $value comes as {parent_id}.{extension} + $parts = explode('.', $value); + $parentId = (int) ArrayHelper::getValue($parts, 0, 1); + + $db = $this->getDatabase(); + $extension = Factory::getApplication()->input->get('extension', '', 'word'); + $newIds = array(); + + // Check that the parent exists + if ($parentId) { + if (!$this->table->load($parentId)) { + if ($error = $this->table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Non-fatal error + $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); + $parentId = 0; + } + } + + // Check that user has create permission for parent category + if ($parentId == $this->table->getRootId()) { + $canCreate = $this->user->authorise('core.create', $extension); + } else { + $canCreate = $this->user->authorise('core.create', $extension . '.category.' . $parentId); + } + + if (!$canCreate) { + // Error since user cannot create in parent category + $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); + + return false; + } + } + + // If the parent is 0, set it to the ID of the root item in the tree + if (empty($parentId)) { + if (!$parentId = $this->table->getRootId()) { + $this->setError($this->table->getError()); + + return false; + } + // Make sure we can create in root + elseif (!$this->user->authorise('core.create', $extension)) { + $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); + + return false; + } + } + + // We need to log the parent ID + $parents = array(); + + // Calculate the emergency stop count as a precaution against a runaway loop bug + $query = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName('id') . ')') + ->from($db->quoteName('#__categories')); + $db->setQuery($query); + + try { + $count = $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Parent exists so let's proceed + while (!empty($pks) && $count > 0) { + // Pop the first id off the stack + $pk = array_shift($pks); + + $this->table->reset(); + + // Check that the row actually exists + if (!$this->table->load($pk)) { + if ($error = $this->table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Not fatal error + $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); + continue; + } + } + + // Copy is a bit tricky, because we also need to copy the children + $lft = (int) $this->table->lft; + $rgt = (int) $this->table->rgt; + $query->clear() + ->select($db->quoteName('id')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('lft') . ' > :lft') + ->where($db->quoteName('rgt') . ' < :rgt') + ->bind(':lft', $lft, ParameterType::INTEGER) + ->bind(':rgt', $rgt, ParameterType::INTEGER); + $db->setQuery($query); + $childIds = $db->loadColumn(); + + // Add child ID's to the array only if they aren't already there. + foreach ($childIds as $childId) { + if (!\in_array($childId, $pks)) { + $pks[] = $childId; + } + } + + // Make a copy of the old ID, Parent ID and Asset ID + $oldId = $this->table->id; + $oldParentId = $this->table->parent_id; + $oldAssetId = $this->table->asset_id; + + // Reset the id because we are making a copy. + $this->table->id = 0; + + // If we a copying children, the Old ID will turn up in the parents list + // otherwise it's a new top level item + $this->table->parent_id = $parents[$oldParentId] ?? $parentId; + + // Set the new location in the tree for the node. + $this->table->setLocation($this->table->parent_id, 'last-child'); + + // @TODO: Deal with ordering? + // $this->table->ordering = 1; + $this->table->level = null; + $this->table->asset_id = null; + $this->table->lft = null; + $this->table->rgt = null; + + // Alter the title & alias + [$title, $alias] = $this->generateNewTitle($this->table->parent_id, $this->table->alias, $this->table->title); + $this->table->title = $title; + $this->table->alias = $alias; + + // Unpublish because we are making a copy + $this->table->published = 0; + + // Store the row. + if (!$this->table->store()) { + $this->setError($this->table->getError()); + + return false; + } + + // Get the new item ID + $newId = $this->table->get('id'); + + // Add the new ID to the array + $newIds[$pk] = $newId; + + // Copy rules + $query->clear() + ->update($db->quoteName('#__assets', 't')) + ->join( + 'INNER', + $db->quoteName('#__assets', 's'), + $db->quoteName('s.id') . ' = :oldid' + ) + ->bind(':oldid', $oldAssetId, ParameterType::INTEGER) + ->set($db->quoteName('t.rules') . ' = ' . $db->quoteName('s.rules')) + ->where($db->quoteName('t.id') . ' = :assetid') + ->bind(':assetid', $this->table->asset_id, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + // Now we log the old 'parent' to the new 'parent' + $parents[$oldId] = $this->table->id; + $count--; + } + + // Rebuild the hierarchy. + if (!$this->table->rebuild()) { + $this->setError($this->table->getError()); + + return false; + } + + // Rebuild the tree path. + if (!$this->table->rebuildPath($this->table->id)) { + $this->setError($this->table->getError()); + + return false; + } + + return $newIds; + } + + /** + * Batch move categories to a new category. + * + * @param integer $value The new category ID. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True on success. + * + * @since 1.6 + */ + protected function batchMove($value, $pks, $contexts) + { + $parentId = (int) $value; + $type = new UCMType(); + $this->type = $type->getTypeByAlias($this->typeAlias); + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $extension = Factory::getApplication()->input->get('extension', '', 'word'); + + // Check that the parent exists. + if ($parentId) { + if (!$this->table->load($parentId)) { + if ($error = $this->table->getError()) { + // Fatal error. + $this->setError($error); + + return false; + } else { + // Non-fatal error. + $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); + $parentId = 0; + } + } + + // Check that user has create permission for parent category. + if ($parentId == $this->table->getRootId()) { + $canCreate = $this->user->authorise('core.create', $extension); + } else { + $canCreate = $this->user->authorise('core.create', $extension . '.category.' . $parentId); + } + + if (!$canCreate) { + // Error since user cannot create in parent category + $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); + + return false; + } + + // Check that user has edit permission for every category being moved + // Note that the entire batch operation fails if any category lacks edit permission + foreach ($pks as $pk) { + if (!$this->user->authorise('core.edit', $extension . '.category.' . $pk)) { + // Error since user cannot edit this category + $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_EDIT')); + + return false; + } + } + } + + // We are going to store all the children and just move the category + $children = array(); + + // Parent exists so let's proceed + foreach ($pks as $pk) { + // Check that the row actually exists + if (!$this->table->load($pk)) { + if ($error = $this->table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Not fatal error + $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); + continue; + } + } + + // Set the new location in the tree for the node. + $this->table->setLocation($parentId, 'last-child'); + + // Check if we are moving to a different parent + if ($parentId != $this->table->parent_id) { + $lft = (int) $this->table->lft; + $rgt = (int) $this->table->rgt; + + // Add the child node ids to the children array. + $query->clear() + ->select($db->quoteName('id')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('lft') . ' BETWEEN :lft AND :rgt') + ->bind(':lft', $lft, ParameterType::INTEGER) + ->bind(':rgt', $rgt, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $children = array_merge($children, (array) $db->loadColumn()); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + // Store the row. + if (!$this->table->store()) { + $this->setError($this->table->getError()); + + return false; + } + + // Rebuild the tree path. + if (!$this->table->rebuildPath()) { + $this->setError($this->table->getError()); + + return false; + } + } + + // Process the child rows + if (!empty($children)) { + // Remove any duplicates and sanitize ids. + $children = array_unique($children); + $children = ArrayHelper::toInteger($children); + } + + return true; + } + + /** + * Custom clean the cache of com_content and content modules + * + * @param string $group Cache group name. + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + $extension = Factory::getApplication()->input->get('extension'); + + switch ($extension) { + case 'com_content': + parent::cleanCache('com_content'); + parent::cleanCache('mod_articles_archive'); + parent::cleanCache('mod_articles_categories'); + parent::cleanCache('mod_articles_category'); + parent::cleanCache('mod_articles_latest'); + parent::cleanCache('mod_articles_news'); + parent::cleanCache('mod_articles_popular'); + break; + default: + parent::cleanCache($extension); + break; + } + } + + /** + * Method to change the title & alias. + * + * @param integer $parentId The id of the parent. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 1.7 + */ + protected function generateNewTitle($parentId, $alias, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) { + $title = StringHelper::increment($title); + $alias = StringHelper::increment($alias, 'dash'); + } + + return array($title, $alias); + } + + /** + * Method to determine if a category association is available. + * + * @return boolean True if a category association is available; false otherwise. + */ + public function getAssoc() + { + if (!\is_null($this->hasAssociation)) { + return $this->hasAssociation; + } + + $extension = $this->getState('category.extension', ''); + + $this->hasAssociation = Associations::isEnabled(); + $extension = explode('.', $extension); + $component = array_shift($extension); + $cname = str_replace('com_', '', $component); + + if (!$this->hasAssociation || !$component || !$cname) { + $this->hasAssociation = false; + + return $this->hasAssociation; + } + + $componentObject = $this->bootComponent($component); + + if ($componentObject instanceof AssociationServiceInterface && $componentObject instanceof CategoryServiceInterface) { + $this->hasAssociation = true; + + return $this->hasAssociation; + } + + $hname = $cname . 'HelperAssociation'; + \JLoader::register($hname, JPATH_SITE . '/components/' . $component . '/helpers/association.php'); + + $this->hasAssociation = class_exists($hname) && !empty($hname::$category_association); + + return $this->hasAssociation; + } } diff --git a/administrator/components/com_categories/src/Service/HTML/AdministratorService.php b/administrator/components/com_categories/src/Service/HTML/AdministratorService.php index 0a48842e8485e..28f69784139e1 100644 --- a/administrator/components/com_categories/src/Service/HTML/AdministratorService.php +++ b/administrator/components/com_categories/src/Service/HTML/AdministratorService.php @@ -1,4 +1,5 @@ getQuery(true) - ->select( - [ - $db->quoteName('c.id'), - $db->quoteName('c.title'), - $db->quoteName('l.sef', 'lang_sef'), - $db->quoteName('l.lang_code'), - $db->quoteName('l.image'), - $db->quoteName('l.title', 'language_title'), - ] - ) - ->from($db->quoteName('#__categories', 'c')) - ->whereIn($db->quoteName('c.id'), array_values($associations)) - ->where($db->quoteName('c.id') . ' != :catid') - ->bind(':catid', $catid, ParameterType::INTEGER) - ->join( - 'LEFT', - $db->quoteName('#__languages', 'l'), - $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code') - ); - $db->setQuery($query); + // Get the associated categories + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('c.id'), + $db->quoteName('c.title'), + $db->quoteName('l.sef', 'lang_sef'), + $db->quoteName('l.lang_code'), + $db->quoteName('l.image'), + $db->quoteName('l.title', 'language_title'), + ] + ) + ->from($db->quoteName('#__categories', 'c')) + ->whereIn($db->quoteName('c.id'), array_values($associations)) + ->where($db->quoteName('c.id') . ' != :catid') + ->bind(':catid', $catid, ParameterType::INTEGER) + ->join( + 'LEFT', + $db->quoteName('#__languages', 'l'), + $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code') + ); + $db->setQuery($query); - try - { - $items = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500, $e); - } + try { + $items = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500, $e); + } - if ($items) - { - $languages = LanguageHelper::getContentLanguages(array(0, 1)); - $content_languages = array_column($languages, 'lang_code'); + if ($items) { + $languages = LanguageHelper::getContentLanguages(array(0, 1)); + $content_languages = array_column($languages, 'lang_code'); - foreach ($items as &$item) - { - if (in_array($item->lang_code, $content_languages)) - { - $text = $item->lang_code; - $url = Route::_('index.php?option=com_categories&task=category.edit&id=' . (int) $item->id . '&extension=' . $extension); - $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
    ' - . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8'); - $classes = 'badge bg-secondary'; + foreach ($items as &$item) { + if (in_array($item->lang_code, $content_languages)) { + $text = $item->lang_code; + $url = Route::_('index.php?option=com_categories&task=category.edit&id=' . (int) $item->id . '&extension=' . $extension); + $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
    ' + . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8'); + $classes = 'badge bg-secondary'; - $item->link = '' . $text . '' - . ''; - } - else - { - // Display warning if Content Language is trashed or deleted - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); - } - } - } + $item->link = '' . $text . '' + . ''; + } else { + // Display warning if Content Language is trashed or deleted + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); + } + } + } - $html = LayoutHelper::render('joomla.content.associations', $items); - } + $html = LayoutHelper::render('joomla.content.associations', $items); + } - return $html; - } + return $html; + } } diff --git a/administrator/components/com_categories/src/Table/CategoryTable.php b/administrator/components/com_categories/src/Table/CategoryTable.php index dc63e189a81da..fe34ba097d55a 100644 --- a/administrator/components/com_categories/src/Table/CategoryTable.php +++ b/administrator/components/com_categories/src/Table/CategoryTable.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->assoc = $this->get('Assoc'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Written this way because we only want to call IsEmptyState if no items, to prevent always calling it when not needed. - if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Preprocess the list of items to find ordering divisions. - foreach ($this->items as &$item) - { - $this->ordering[$item->parent_id][] = $item->id; - } - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - else - { - // In article associations modal we need to remove language filter if forcing a language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. - $languageXml = new \SimpleXMLElement(''); - $this->filterForm->setField($languageXml, 'filter', true); - - // Also, unset the active language filter so the search tools is not open by default with this filter. - unset($this->activeFilters['language']); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @throws \Exception - * @since 1.6 - */ - protected function addToolbar() - { - $categoryId = $this->state->get('filter.category_id'); - $component = $this->state->get('filter.component'); - $section = $this->state->get('filter.section'); - $canDo = ContentHelper::getActions($component, 'category', $categoryId); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - // Avoid nonsense situation. - if ($component == 'com_categories') - { - return; - } - - // Need to load the menu language file as mod_menu hasn't been loaded yet. - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE) - || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); - - // If a component categories title string is present, let's use it. - if ($lang->hasKey($component_title_key = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_TITLE')) - { - $title = Text::_($component_title_key); - } - elseif ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) - // Else if the component section string exists, let's use it. - { - $title = Text::sprintf('COM_CATEGORIES_CATEGORIES_TITLE', $this->escape(Text::_($component_section_key))); - } - else - // Else use the base title - { - $title = Text::_('COM_CATEGORIES_CATEGORIES_BASE_TITLE'); - } - - // Load specific css component - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = $this->document->getWebAssetManager(); - $wa->getRegistry()->addExtensionRegistryFile($component); - - if ($wa->assetExists('style', $component . '.admin-categories')) - { - $wa->useStyle($component . '.admin-categories'); - } - else - { - $wa->registerAndUseStyle($component . '.admin-categories', $component . '/administrator/categories.css'); - } - - // Prepare the toolbar. - ToolbarHelper::title($title, 'folder categories ' . substr($component, 4) . ($section ? "-$section" : '') . '-categories'); - - if ($canDo->get('core.create') || count($user->getAuthorisedCategories($component, 'core.create')) > 0) - { - $toolbar->addNew('category.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('categories.publish')->listCheck(true); - - $childBar->unpublish('categories.unpublish')->listCheck(true); - - $childBar->archive('categories.archive')->listCheck(true); - } - - if ($user->authorise('core.admin')) - { - $childBar->checkin('categories.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) - { - $childBar->trash('categories.trash')->listCheck(true); - } - - // Add a batch button - if ($canDo->get('core.create') - && $canDo->get('core.edit') - && $canDo->get('core.edit.state')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && $canDo->get('core.admin')) - { - $toolbar->standardButton('refresh') - ->text('JTOOLBAR_REBUILD') - ->task('categories.rebuild'); - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete', $component)) - { - $toolbar->delete('categories.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences($component); - } - - // Get the component form if it exists for the help key/url - $name = 'category' . ($section ? ('.' . $section) : ''); - - // Looking first in the component forms folder - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/forms/$name.xml"); - - // Looking in the component models/forms folder (J! 3) - if (!file_exists($path)) - { - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/models/forms/$name.xml"); - } - - $ref_key = ''; - $url = ''; - - // Look first in form for help key and url - if (file_exists($path)) - { - if (!$xml = simplexml_load_file($path)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - $ref_key = (string) $xml->listhelp['key']; - $url = (string) $xml->listhelp['url']; - } - - if (!$ref_key) - { - // Compute the ref_key if it does exist in the component - $languageKey = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_HELP_KEY'; - - if ($lang->hasKey($languageKey)) - { - $ref_key = $languageKey; - } - else - { - $languageKey = 'JHELP_COMPONENTS_' . strtoupper(substr($component, 4) . ($section ? "_$section" : '')) . '_CATEGORIES'; - - if ($lang->hasKey($languageKey)) - { - $ref_key = $languageKey; - } - } - } - - /* - * Get help for the categories view for the component by - * -remotely searching in a URL defined in the category form - * -remotely searching in a language defined dedicated URL: *component*_HELP_URL - * -locally searching in a component help file if helpURL param exists in the component and is set to '' - * -remotely searching in a component URL if helpURL param exists in the component and is NOT set to '' - */ - if (!$url) - { - if ($lang->hasKey($lang_help_url = strtoupper($component) . '_HELP_URL')) - { - $debug = $lang->setDebug(false); - $url = Text::_($lang_help_url); - $lang->setDebug($debug); - } - } - - ToolbarHelper::help($ref_key, ComponentHelper::getParams($component)->exists('helpURL'), $url); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var object + */ + protected $state; + + /** + * Flag if an association exists + * + * @var boolean + */ + protected $assoc; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string|null $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @throws GenericDataException + * + * @return void + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->assoc = $this->get('Assoc'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Written this way because we only want to call IsEmptyState if no items, to prevent always calling it when not needed. + if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Preprocess the list of items to find ordering divisions. + foreach ($this->items as &$item) { + $this->ordering[$item->parent_id][] = $item->id; + } + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // In article associations modal we need to remove language filter if forcing a language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. + $languageXml = new \SimpleXMLElement(''); + $this->filterForm->setField($languageXml, 'filter', true); + + // Also, unset the active language filter so the search tools is not open by default with this filter. + unset($this->activeFilters['language']); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @throws \Exception + * @since 1.6 + */ + protected function addToolbar() + { + $categoryId = $this->state->get('filter.category_id'); + $component = $this->state->get('filter.component'); + $section = $this->state->get('filter.section'); + $canDo = ContentHelper::getActions($component, 'category', $categoryId); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + // Avoid nonsense situation. + if ($component == 'com_categories') { + return; + } + + // Need to load the menu language file as mod_menu hasn't been loaded yet. + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE) + || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); + + // If a component categories title string is present, let's use it. + if ($lang->hasKey($component_title_key = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_TITLE')) { + $title = Text::_($component_title_key); + } elseif ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) { + // Else if the component section string exists, let's use it. + $title = Text::sprintf('COM_CATEGORIES_CATEGORIES_TITLE', $this->escape(Text::_($component_section_key))); + } else // Else use the base title + { + $title = Text::_('COM_CATEGORIES_CATEGORIES_BASE_TITLE'); + } + + // Load specific css component + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = $this->document->getWebAssetManager(); + $wa->getRegistry()->addExtensionRegistryFile($component); + + if ($wa->assetExists('style', $component . '.admin-categories')) { + $wa->useStyle($component . '.admin-categories'); + } else { + $wa->registerAndUseStyle($component . '.admin-categories', $component . '/administrator/categories.css'); + } + + // Prepare the toolbar. + ToolbarHelper::title($title, 'folder categories ' . substr($component, 4) . ($section ? "-$section" : '') . '-categories'); + + if ($canDo->get('core.create') || count($user->getAuthorisedCategories($component, 'core.create')) > 0) { + $toolbar->addNew('category.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('categories.publish')->listCheck(true); + + $childBar->unpublish('categories.unpublish')->listCheck(true); + + $childBar->archive('categories.archive')->listCheck(true); + } + + if ($user->authorise('core.admin')) { + $childBar->checkin('categories.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) { + $childBar->trash('categories.trash')->listCheck(true); + } + + // Add a batch button + if ( + $canDo->get('core.create') + && $canDo->get('core.edit') + && $canDo->get('core.edit.state') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && $canDo->get('core.admin')) { + $toolbar->standardButton('refresh') + ->text('JTOOLBAR_REBUILD') + ->task('categories.rebuild'); + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete', $component)) { + $toolbar->delete('categories.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences($component); + } + + // Get the component form if it exists for the help key/url + $name = 'category' . ($section ? ('.' . $section) : ''); + + // Looking first in the component forms folder + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/forms/$name.xml"); + + // Looking in the component models/forms folder (J! 3) + if (!file_exists($path)) { + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/models/forms/$name.xml"); + } + + $ref_key = ''; + $url = ''; + + // Look first in form for help key and url + if (file_exists($path)) { + if (!$xml = simplexml_load_file($path)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + $ref_key = (string) $xml->listhelp['key']; + $url = (string) $xml->listhelp['url']; + } + + if (!$ref_key) { + // Compute the ref_key if it does exist in the component + $languageKey = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_HELP_KEY'; + + if ($lang->hasKey($languageKey)) { + $ref_key = $languageKey; + } else { + $languageKey = 'JHELP_COMPONENTS_' . strtoupper(substr($component, 4) . ($section ? "_$section" : '')) . '_CATEGORIES'; + + if ($lang->hasKey($languageKey)) { + $ref_key = $languageKey; + } + } + } + + /* + * Get help for the categories view for the component by + * -remotely searching in a URL defined in the category form + * -remotely searching in a language defined dedicated URL: *component*_HELP_URL + * -locally searching in a component help file if helpURL param exists in the component and is set to '' + * -remotely searching in a component URL if helpURL param exists in the component and is NOT set to '' + */ + if (!$url) { + if ($lang->hasKey($lang_help_url = strtoupper($component) . '_HELP_URL')) { + $debug = $lang->setDebug(false); + $url = Text::_($lang_help_url); + $lang->setDebug($debug); + } + } + + ToolbarHelper::help($ref_key, ComponentHelper::getParams($component)->exists('helpURL'), $url); + } } diff --git a/administrator/components/com_categories/src/View/Category/HtmlView.php b/administrator/components/com_categories/src/View/Category/HtmlView.php index d0a31e2b49143..c90afc026ed64 100644 --- a/administrator/components/com_categories/src/View/Category/HtmlView.php +++ b/administrator/components/com_categories/src/View/Category/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - $section = $this->state->get('category.section') ? $this->state->get('category.section') . '.' : ''; - $this->canDo = ContentHelper::getActions($this->state->get('category.component'), $section . 'category', $this->item->id); - $this->assoc = $this->get('Assoc'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check if we have a content type for this alias - if (!empty(TagsHelper::getTypes('objectList', array($this->state->get('category.extension') . '.category'), true))) - { - $this->checkTags = true; - } - - Factory::getApplication()->input->set('hidemainmenu', true); - - // If we are forcing a language in modal (used for associations). - if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) - { - // Set the language field to the forcedLanguage and disable changing it. - $this->form->setValue('language', null, $forcedLanguage); - $this->form->setFieldAttribute('language', 'readonly', 'true'); - - // Only allow to select categories with All language or with the forced language. - $this->form->setFieldAttribute('parent_id', 'language', '*,' . $forcedLanguage); - - // Only allow to select tags with All language or with the forced language. - $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $extension = Factory::getApplication()->input->get('extension'); - $user = $this->getCurrentUser(); - $userId = $user->id; - - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Avoid nonsense situation. - if ($extension == 'com_categories') - { - return; - } - - // The extension can be in the form com_foo.section - $parts = explode('.', $extension); - $component = $parts[0]; - $section = (count($parts) > 1) ? $parts[1] : null; - $componentParams = ComponentHelper::getParams($component); - - // Need to load the menu language file as mod_menu hasn't been loaded yet. - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE) - || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); - - // Get the results for each action. - $canDo = $this->canDo; - - // If a component categories title string is present, let's use it. - if ($lang->hasKey($component_title_key = $component . ($section ? "_$section" : '') . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE')) - { - $title = Text::_($component_title_key); - } - // Else if the component section string exists, let's use it. - elseif ($lang->hasKey($component_section_key = $component . ($section ? "_$section" : ''))) - { - $title = Text::sprintf('COM_CATEGORIES_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') - . '_TITLE', $this->escape(Text::_($component_section_key)) - ); - } - // Else use the base title - else - { - $title = Text::_('COM_CATEGORIES_CATEGORY_BASE_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE'); - } - - // Load specific css component - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = $this->document->getWebAssetManager(); - $wa->getRegistry()->addExtensionRegistryFile($component); - - if ($wa->assetExists('style', $component . '.admin-categories')) - { - $wa->useStyle($component . '.admin-categories'); - } - else - { - $wa->registerAndUseStyle($component . '.admin-categories', $component . '/administrator/categories.css'); - } - - // Prepare the toolbar. - ToolbarHelper::title( - $title, - 'folder category-' . ($isNew ? 'add' : 'edit') - . ' ' . substr($component, 4) . ($section ? "-$section" : '') . '-category-' . ($isNew ? 'add' : 'edit') - ); - - // For new records, check the create permission. - if ($isNew && (count($user->getAuthorisedCategories($component, 'core.create')) > 0)) - { - ToolbarHelper::apply('category.apply'); - ToolbarHelper::saveGroup( - [ - ['save', 'category.save'], - ['save2new', 'category.save2new'] - ], - 'btn-success' - ); - - ToolbarHelper::cancel('category.cancel'); - } - - // If not checked out, can save the item. - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_user_id == $userId); - - $toolbarButtons = []; - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - ToolbarHelper::apply('category.apply'); - - $toolbarButtons[] = ['save', 'category.save']; - - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'category.save2new']; - } - } - - // If an existing item, can save to a copy. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'category.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('category.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $componentParams->get('save_history', 0) && $itemEditable) - { - $typeAlias = $extension . '.category'; - ToolbarHelper::versions($typeAlias, $this->item->id); - } - - if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) - { - ToolbarHelper::custom('category.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); - } - } - - ToolbarHelper::divider(); - - // Look first in form for help key - $ref_key = (string) $this->form->getXml()->help['key']; - - // Try with a language string - if (!$ref_key) - { - // Compute the ref_key if it does exist in the component - $languageKey = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') . '_HELP_KEY'; - - if ($lang->hasKey($languageKey)) - { - $ref_key = $languageKey; - } - else - { - $languageKey = 'JHELP_COMPONENTS_' - . strtoupper(substr($component, 4) . ($section ? "_$section" : '')) - . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT'); - - if ($lang->hasKey($languageKey)) - { - $ref_key = $languageKey; - } - } - } - - /* - * Get help for the category/section view for the component by - * -remotely searching in a URL defined in the category form - * -remotely searching in a language defined dedicated URL: *component*_HELP_URL - * -locally searching in a component help file if helpURL param exists in the component and is set to '' - * -remotely searching in a component URL if helpURL param exists in the component and is NOT set to '' - */ - $url = (string) $this->form->getXml()->help['url']; - - if (!$url) - { - if ($lang->hasKey($lang_help_url = strtoupper($component) . '_HELP_URL')) - { - $debug = $lang->setDebug(false); - $url = Text::_($lang_help_url); - $lang->setDebug($debug); - } - } - - ToolbarHelper::help($ref_key, $componentParams->exists('helpURL'), $url, $component); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * Flag if an association exists + * + * @var boolean + */ + protected $assoc; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + */ + protected $canDo; + + /** + * Is there a content type associated with this category alias + * + * @var boolean + * @since 4.0.0 + */ + protected $checkTags = false; + + /** + * Display the view. + * + * @param string|null $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + $section = $this->state->get('category.section') ? $this->state->get('category.section') . '.' : ''; + $this->canDo = ContentHelper::getActions($this->state->get('category.component'), $section . 'category', $this->item->id); + $this->assoc = $this->get('Assoc'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check if we have a content type for this alias + if (!empty(TagsHelper::getTypes('objectList', array($this->state->get('category.extension') . '.category'), true))) { + $this->checkTags = true; + } + + Factory::getApplication()->input->set('hidemainmenu', true); + + // If we are forcing a language in modal (used for associations). + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) { + // Set the language field to the forcedLanguage and disable changing it. + $this->form->setValue('language', null, $forcedLanguage); + $this->form->setFieldAttribute('language', 'readonly', 'true'); + + // Only allow to select categories with All language or with the forced language. + $this->form->setFieldAttribute('parent_id', 'language', '*,' . $forcedLanguage); + + // Only allow to select tags with All language or with the forced language. + $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $extension = Factory::getApplication()->input->get('extension'); + $user = $this->getCurrentUser(); + $userId = $user->id; + + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Avoid nonsense situation. + if ($extension == 'com_categories') { + return; + } + + // The extension can be in the form com_foo.section + $parts = explode('.', $extension); + $component = $parts[0]; + $section = (count($parts) > 1) ? $parts[1] : null; + $componentParams = ComponentHelper::getParams($component); + + // Need to load the menu language file as mod_menu hasn't been loaded yet. + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE) + || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); + + // Get the results for each action. + $canDo = $this->canDo; + + // If a component categories title string is present, let's use it. + if ($lang->hasKey($component_title_key = $component . ($section ? "_$section" : '') . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE')) { + $title = Text::_($component_title_key); + } + // Else if the component section string exists, let's use it. + elseif ($lang->hasKey($component_section_key = $component . ($section ? "_$section" : ''))) { + $title = Text::sprintf('COM_CATEGORIES_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') + . '_TITLE', $this->escape(Text::_($component_section_key))); + } + // Else use the base title + else { + $title = Text::_('COM_CATEGORIES_CATEGORY_BASE_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE'); + } + + // Load specific css component + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = $this->document->getWebAssetManager(); + $wa->getRegistry()->addExtensionRegistryFile($component); + + if ($wa->assetExists('style', $component . '.admin-categories')) { + $wa->useStyle($component . '.admin-categories'); + } else { + $wa->registerAndUseStyle($component . '.admin-categories', $component . '/administrator/categories.css'); + } + + // Prepare the toolbar. + ToolbarHelper::title( + $title, + 'folder category-' . ($isNew ? 'add' : 'edit') + . ' ' . substr($component, 4) . ($section ? "-$section" : '') . '-category-' . ($isNew ? 'add' : 'edit') + ); + + // For new records, check the create permission. + if ($isNew && (count($user->getAuthorisedCategories($component, 'core.create')) > 0)) { + ToolbarHelper::apply('category.apply'); + ToolbarHelper::saveGroup( + [ + ['save', 'category.save'], + ['save2new', 'category.save2new'] + ], + 'btn-success' + ); + + ToolbarHelper::cancel('category.cancel'); + } + + // If not checked out, can save the item. + else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_user_id == $userId); + + $toolbarButtons = []; + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + ToolbarHelper::apply('category.apply'); + + $toolbarButtons[] = ['save', 'category.save']; + + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'category.save2new']; + } + } + + // If an existing item, can save to a copy. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'category.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('category.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $componentParams->get('save_history', 0) && $itemEditable) { + $typeAlias = $extension . '.category'; + ToolbarHelper::versions($typeAlias, $this->item->id); + } + + if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) { + ToolbarHelper::custom('category.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); + } + } + + ToolbarHelper::divider(); + + // Look first in form for help key + $ref_key = (string) $this->form->getXml()->help['key']; + + // Try with a language string + if (!$ref_key) { + // Compute the ref_key if it does exist in the component + $languageKey = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') . '_HELP_KEY'; + + if ($lang->hasKey($languageKey)) { + $ref_key = $languageKey; + } else { + $languageKey = 'JHELP_COMPONENTS_' + . strtoupper(substr($component, 4) . ($section ? "_$section" : '')) + . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT'); + + if ($lang->hasKey($languageKey)) { + $ref_key = $languageKey; + } + } + } + + /* + * Get help for the category/section view for the component by + * -remotely searching in a URL defined in the category form + * -remotely searching in a language defined dedicated URL: *component*_HELP_URL + * -locally searching in a component help file if helpURL param exists in the component and is set to '' + * -remotely searching in a component URL if helpURL param exists in the component and is NOT set to '' + */ + $url = (string) $this->form->getXml()->help['url']; + + if (!$url) { + if ($lang->hasKey($lang_help_url = strtoupper($component) . '_HELP_URL')) { + $debug = $lang->setDebug(false); + $url = Text::_($lang_help_url); + $lang->setDebug($debug); + } + } + + ToolbarHelper::help($ref_key, $componentParams->exists('helpURL'), $url, $component); + } } diff --git a/administrator/components/com_categories/tmpl/categories/default.php b/administrator/components/com_categories/tmpl/categories/default.php index e4ad8677615e6..9a05036baf514 100644 --- a/administrator/components/com_categories/tmpl/categories/default.php +++ b/administrator/components/com_categories/tmpl/categories/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $userId = $user->get('id'); @@ -33,281 +34,273 @@ $component = $parts[0]; $section = null; -if (count($parts) > 1) -{ - $section = $parts[1]; +if (count($parts) > 1) { + $section = $parts[1]; - $inflector = Inflector::getInstance(); + $inflector = Inflector::getInstance(); - if (!$inflector->isPlural($section)) - { - $section = $inflector->toPlural($section); - } + if (!$inflector->isPlural($section)) { + $section = $inflector->toPlural($section); + } } -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_categories&task=categories.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_categories&task=categories.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?> -
    -
    -
    - $this)); - ?> - items)) : ?> -
    - - -
    - - - - - - - - - - items[0]) && property_exists($this->items[0], 'count_published')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_archived')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> - - - - assoc) : ?> - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="false"> - items as $i => $item) : ?> - authorise('core.edit', $extension . '.category.' . $item->id); - $canCheckin = $user->authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canEditOwn = $user->authorise('core.edit.own', $extension . '.category.' . $item->id) && $item->created_user_id == $userId; - $canChange = $user->authorise('core.edit.state', $extension . '.category.' . $item->id) && $canCheckin; +
    +
    +
    + $this)); + ?> + items)) : ?> +
    + + +
    + +
    - , - , - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    + + + + + + + + items[0]) && property_exists($this->items[0], 'count_published')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_archived')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> + + + + assoc) : ?> + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="false"> + items as $i => $item) : ?> + authorise('core.edit', $extension . '.category.' . $item->id); + $canCheckin = $user->authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', $extension . '.category.' . $item->id) && $item->created_user_id == $userId; + $canChange = $user->authorise('core.edit.state', $extension . '.category.' . $item->id) && $canCheckin; - // Get the parents of item for sorting - if ($item->level > 1) - { - $parentsStr = ''; - $_currentParentId = $item->parent_id; - $parentsStr = ' ' . $_currentParentId; - for ($i2 = 0; $i2 < $item->level; $i2++) - { - foreach ($this->ordering as $k => $v) - { - $v = implode('-', $v); - $v = '-' . $v . '-'; - if (strpos($v, '-' . $_currentParentId . '-') !== false) - { - $parentsStr .= ' ' . $k; - $_currentParentId = $k; - break; - } - } - } - } - else - { - $parentsStr = ''; - } - ?> - - - - - - items[0]) && property_exists($this->items[0], 'count_published')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_archived')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> - - + // Get the parents of item for sorting + if ($item->level > 1) { + $parentsStr = ''; + $_currentParentId = $item->parent_id; + $parentsStr = ' ' . $_currentParentId; + for ($i2 = 0; $i2 < $item->level; $i2++) { + foreach ($this->ordering as $k => $v) { + $v = implode('-', $v); + $v = '-' . $v . '-'; + if (strpos($v, '-' . $_currentParentId . '-') !== false) { + $parentsStr .= ' ' . $k; + $_currentParentId = $k; + break; + } + } + } + } else { + $parentsStr = ''; + } + ?> + + + + + + items[0]) && property_exists($this->items[0], 'count_published')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_archived')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> + + - - assoc) : ?> - - - - - - - - - -
    + , + , + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    - id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - published, $i, 'categories.', $canChange); ?> - - $item->level)); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'categories.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
    - - - note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - - -
    -
    - - count_published; ?> - - - - - count_unpublished; ?> - - - - - count_archived; ?> - - - - - count_trashed; ?> - - -
    + id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + published, $i, 'categories.', $canChange); ?> + + $item->level)); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'categories.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
    + + + note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + + +
    +
    + + count_published; ?> + + + + + count_unpublished; ?> + + + + + count_archived; ?> + + + + + count_trashed; ?> + + + - escape($item->access_level); ?> - - association) : ?> - id, $extension); ?> - - - - - id; ?> -
    + + escape($item->access_level); ?> + + assoc) : ?> + + association) : ?> + id, $extension); ?> + + + + + + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', $extension) - && $user->authorise('core.edit', $extension) - && $user->authorise('core.edit.state', $extension)) : ?> - Text::_('COM_CATEGORIES_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - + + authorise('core.create', $extension) + && $user->authorise('core.edit', $extension) + && $user->authorise('core.edit.state', $extension) +) : ?> + Text::_('COM_CATEGORIES_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + - - - - -
    -
    -
    + + + + + + + diff --git a/administrator/components/com_categories/tmpl/categories/default_batch_body.php b/administrator/components/com_categories/tmpl/categories/default_batch_body.php index 700addd6f6756..aa432d63f01c5 100644 --- a/administrator/components/com_categories/tmpl/categories/default_batch_body.php +++ b/administrator/components/com_categories/tmpl/categories/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\HTML\HTMLHelper; @@ -19,46 +21,46 @@ ?>
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    - = 0) : ?> -
    -
    - $extension, 'addRoot' => true]); ?> -
    -
    - -
    -
    - -
    -
    -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    - +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + = 0) : ?> +
    +
    + $extension, 'addRoot' => true]); ?> +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    diff --git a/administrator/components/com_categories/tmpl/categories/default_batch_footer.php b/administrator/components/com_categories/tmpl/categories/default_batch_footer.php index 5f37efc761a37..3f29dd80ef2ed 100644 --- a/administrator/components/com_categories/tmpl/categories/default_batch_footer.php +++ b/administrator/components/com_categories/tmpl/categories/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/administrator/components/com_categories/tmpl/categories/emptystate.php b/administrator/components/com_categories/tmpl/categories/emptystate.php index 03b15bdbece0b..52d0e38594eae 100644 --- a/administrator/components/com_categories/tmpl/categories/emptystate.php +++ b/administrator/components/com_categories/tmpl/categories/emptystate.php @@ -1,4 +1,5 @@ load($component, JPATH_ADMINISTRATOR . '/components/' . $component); // If a component categories title string is present, let's use it. -if ($lang->hasKey($component_title_key = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_TITLE')) -{ - $title = Text::_($component_title_key); +if ($lang->hasKey($component_title_key = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_TITLE')) { + $title = Text::_($component_title_key); } // Else if the component section string exists, let's use it -elseif ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) +elseif ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) { + $title = Text::sprintf('COM_CATEGORIES_CATEGORIES_TITLE', $this->escape(Text::_($component_section_key))); +} else // Else use the base title { - $title = Text::sprintf('COM_CATEGORIES_CATEGORIES_TITLE', $this->escape(Text::_($component_section_key))); -} -else // Else use the base title -{ - $title = Text::_('COM_CATEGORIES_CATEGORIES_BASE_TITLE'); + $title = Text::_('COM_CATEGORIES_CATEGORIES_BASE_TITLE'); } $displayData = [ - 'textPrefix' => 'COM_CATEGORIES', - 'formURL' => 'index.php?option=com_categories&extension=' . $extension, - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Category', - 'title' => $title, - 'icon' => 'icon-folder categories content-categories', + 'textPrefix' => 'COM_CATEGORIES', + 'formURL' => 'index.php?option=com_categories&extension=' . $extension, + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Category', + 'title' => $title, + 'icon' => 'icon-folder categories content-categories', ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', $extension)) -{ - $displayData['createURL'] = 'index.php?option=com_categories&extension=' . $extension . '&task=category.add'; +if (Factory::getApplication()->getIdentity()->authorise('core.create', $extension)) { + $displayData['createURL'] = 'index.php?option=com_categories&extension=' . $extension . '&task=category.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_categories/tmpl/categories/modal.php b/administrator/components/com_categories/tmpl/categories/modal.php index 2500c0b44a004..819957f58ddc7 100644 --- a/administrator/components/com_categories/tmpl/categories/modal.php +++ b/administrator/components/com_categories/tmpl/categories/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if ($app->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } HTMLHelper::_('behavior.core'); @@ -34,114 +34,106 @@ ?>
    -
    + - $this)); ?> + $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', - ); - ?> - items as $i => $item) : ?> - language && Multilanguage::isEnabled()) - { - $tag = strlen($item->language); - if ($tag == 5) - { - $lang = substr($item->language, 0, 2); - } - elseif ($tag == 6) - { - $lang = substr($item->language, 0, 3); - } - else - { - $lang = ''; - } - } - elseif (!Multilanguage::isEnabled()) - { - $lang = ''; - } - ?> - - - - - - - - - -
    - , - , - -
    - - - - - - - - - -
    - - - - - $item->level)); ?> - - escape($item->title); ?> -
    - note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - -
    -
    - escape($item->access_level); ?> - - - - id; ?> -
    + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', + ); + ?> + items as $i => $item) : ?> + language && Multilanguage::isEnabled()) { + $tag = strlen($item->language); + if ($tag == 5) { + $lang = substr($item->language, 0, 2); + } elseif ($tag == 6) { + $lang = substr($item->language, 0, 3); + } else { + $lang = ''; + } + } elseif (!Multilanguage::isEnabled()) { + $lang = ''; + } + ?> + + + + + + + + + +
    + , + , + +
    + + + + + + + + + +
    + + + + + $item->level)); ?> + + escape($item->title); ?> +
    + note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
    +
    + escape($item->access_level); ?> + + + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - - + + + + + -
    +
    diff --git a/administrator/components/com_categories/tmpl/category/edit.php b/administrator/components/com_categories/tmpl/category/edit.php index 448b4a18d5832..b20338fdbb7e4 100644 --- a/administrator/components/com_categories/tmpl/category/edit.php +++ b/administrator/components/com_categories/tmpl/category/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $input = $app->input; @@ -34,14 +35,12 @@ $c = Factory::getApplication()->bootComponent($this->state->get('category.extension')); -if ($c instanceof WorkflowServiceInterface) -{ - $wcontext = $c->getCategoryWorkflowContext($this->state->get('category.section')); +if ($c instanceof WorkflowServiceInterface) { + $wcontext = $c->getCategoryWorkflowContext($this->state->get('category.section')); - if (!$c->isWorkflowActive($wcontext)) - { - $this->ignore_fieldsets[] = 'workflow'; - } + if (!$c->isWorkflowActive($wcontext)) { + $this->ignore_fieldsets[] = 'workflow'; + } } $this->useCoreUI = true; @@ -54,78 +53,77 @@
    - - -
    - 'general', 'recall' => true, 'breakpoint' => 768]); ?> - -
    -
    - form->getLabel('description'); ?> - form->getInput('description'); ?> -
    -
    - -
    -
    - - - - - - -
    -
    -
    - -
    - -
    -
    -
    -
    -
    - -
    - -
    -
    -
    -
    - - - - - -
    - -
    - -
    -
    - - - - - - canDo->get('core.admin')) : ?> - - -
    - -
    - form->getInput('rules'); ?> -
    -
    - - - - - - form->getInput('extension'); ?> - - - - -
    + + +
    + 'general', 'recall' => true, 'breakpoint' => 768]); ?> + +
    +
    + form->getLabel('description'); ?> + form->getInput('description'); ?> +
    +
    + +
    +
    + + + + + + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + + + + + +
    + +
    + +
    +
    + + + + + + canDo->get('core.admin')) : ?> + +
    + +
    + form->getInput('rules'); ?> +
    +
    + + + + + + form->getInput('extension'); ?> + + + + +
    diff --git a/administrator/components/com_categories/tmpl/category/modal.php b/administrator/components/com_categories/tmpl/category/modal.php index 2adebfb9bcee7..87526bec61712 100644 --- a/administrator/components/com_categories/tmpl/category/modal.php +++ b/administrator/components/com_categories/tmpl/category/modal.php @@ -1,4 +1,5 @@
    - setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
    diff --git a/administrator/components/com_checkin/services/provider.php b/administrator/components/com_checkin/services/provider.php index e4ad52d3d4754..66b8f0fec1811 100644 --- a/administrator/components/com_checkin/services/provider.php +++ b/administrator/components/com_checkin/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Checkin')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Checkin')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Checkin')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Checkin')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_checkin/src/Controller/DisplayController.php b/administrator/components/com_checkin/src/Controller/DisplayController.php index 34c5512726681..f655edf793a5e 100644 --- a/administrator/components/com_checkin/src/Controller/DisplayController.php +++ b/administrator/components/com_checkin/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'string'); - - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST'), 'warning'); - } - else - { - // Get the model. - /** @var \Joomla\Component\Checkin\Administrator\Model\CheckinModel $model */ - $model = $this->getModel('Checkin'); - - // Checked in the items. - $this->setMessage(Text::plural('COM_CHECKIN_N_ITEMS_CHECKED_IN', $model->checkin($ids))); - } - - $this->setRedirect('index.php?option=com_checkin'); - } - - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_checkin')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Checkin'); - - $amount = (int) count($model->getItems()); - - echo new JsonResponse($amount); - } - - /** - * Method to get the number of locked icons - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getQuickiconContent() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_checkin')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Checkin'); - - $amount = (int) count($model->getItems()); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_CHECKIN_N_QUICKICON_SRONLY', $amount); - - echo new JsonResponse($result); - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'checkin'; + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static A \JControllerLegacy object to support chaining. + */ + public function display($cachable = false, $urlparams = array()) + { + return parent::display(); + } + + /** + * Check in a list of items. + * + * @return void + */ + public function checkin() + { + // Check for request forgeries + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'string'); + + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST'), 'warning'); + } else { + // Get the model. + /** @var \Joomla\Component\Checkin\Administrator\Model\CheckinModel $model */ + $model = $this->getModel('Checkin'); + + // Checked in the items. + $this->setMessage(Text::plural('COM_CHECKIN_N_ITEMS_CHECKED_IN', $model->checkin($ids))); + } + + $this->setRedirect('index.php?option=com_checkin'); + } + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_checkin')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Checkin'); + + $amount = (int) count($model->getItems()); + + echo new JsonResponse($amount); + } + + /** + * Method to get the number of locked icons + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getQuickiconContent() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_checkin')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Checkin'); + + $amount = (int) count($model->getItems()); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_CHECKIN_N_QUICKICON_SRONLY', $amount); + + echo new JsonResponse($result); + } } diff --git a/administrator/components/com_checkin/src/Model/CheckinModel.php b/administrator/components/com_checkin/src/Model/CheckinModel.php index efd485d2a8911..bd775c29f4b07 100644 --- a/administrator/components/com_checkin/src/Model/CheckinModel.php +++ b/administrator/components/com_checkin/src/Model/CheckinModel.php @@ -1,4 +1,5 @@ getDatabase(); - - if (!is_array($ids)) - { - return 0; - } - - // This int will hold the checked item count. - $results = 0; - - $app = Factory::getApplication(); - - foreach ($ids as $tn) - { - // Make sure we get the right tables based on prefix. - if (stripos($tn, $app->get('dbprefix')) !== 0) - { - continue; - } - - $fields = $db->getTableColumns($tn, false); - - if (!(isset($fields['checked_out']) && isset($fields['checked_out_time']))) - { - continue; - } - - $query = $db->getQuery(true) - ->update($db->quoteName($tn)) - ->set($db->quoteName('checked_out') . ' = DEFAULT'); - - if ($fields['checked_out_time']->Null === 'YES') - { - $query->set($db->quoteName('checked_out_time') . ' = NULL'); - } - else - { - $nullDate = $db->getNullDate(); - - $query->set($db->quoteName('checked_out_time') . ' = :checkouttime') - ->bind(':checkouttime', $nullDate); - } - - if ($fields['checked_out']->Null === 'YES') - { - $query->where($db->quoteName('checked_out') . ' IS NOT NULL'); - } - else - { - $query->where($db->quoteName('checked_out') . ' > 0'); - } - - $db->setQuery($query); - - if ($db->execute()) - { - $results = $results + $db->getAffectedRows(); - $app->triggerEvent('onAfterCheckin', array($tn)); - } - } - - return $results; - } - - /** - * Get total of tables - * - * @return integer Total to check-in tables - * - * @since 1.6 - */ - public function getTotal() - { - if (!isset($this->total)) - { - $this->getItems(); - } - - return $this->total; - } - - /** - * Get tables - * - * @return array Checked in table names as keys and checked in item count as values. - * - * @since 1.6 - */ - public function getItems() - { - if (!isset($this->items)) - { - $db = $this->getDatabase(); - $tables = $db->getTableList(); - $prefix = Factory::getApplication()->get('dbprefix'); - - // This array will hold table name as key and checked in item count as value. - $results = array(); - - foreach ($tables as $tn) - { - // Make sure we get the right tables based on prefix. - if (stripos($tn, $prefix) !== 0) - { - continue; - } - - if ($this->getState('filter.search') && stripos($tn, $this->getState('filter.search')) === false) - { - continue; - } - - $fields = $db->getTableColumns($tn, false); - - if (!(isset($fields['checked_out']) && isset($fields['checked_out_time']))) - { - continue; - } - - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName($tn)); - - if ($fields['checked_out']->Null === 'YES') - { - $query->where($db->quoteName('checked_out') . ' IS NOT NULL'); - } - else - { - $query->where($db->quoteName('checked_out') . ' > 0'); - } - - $db->setQuery($query); - $count = $db->loadResult(); - - if ($count) - { - $results[$tn] = $count; - } - } - - $this->total = count($results); - - // Order items by table - if ($this->getState('list.ordering') == 'table') - { - if (strtolower($this->getState('list.direction')) == 'asc') - { - ksort($results); - } - else - { - krsort($results); - } - } - // Order items by number of items - else - { - if (strtolower($this->getState('list.direction')) == 'asc') - { - asort($results); - } - else - { - arsort($results); - } - } - - // Pagination - $limit = (int) $this->getState('list.limit'); - - if ($limit !== 0) - { - $this->items = array_slice($results, $this->getState('list.start'), $limit); - } - else - { - $this->items = $results; - } - } - - return $this->items; - } + /** + * Count of the total items checked out + * + * @var integer + */ + protected $total; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'table', + 'count', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note: Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'table', $direction = 'asc') + { + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Checks in requested tables + * + * @param array $ids An array of table names. Optional. + * + * @return mixed The database results or 0 + * + * @since 1.6 + */ + public function checkin($ids = array()) + { + $db = $this->getDatabase(); + + if (!is_array($ids)) { + return 0; + } + + // This int will hold the checked item count. + $results = 0; + + $app = Factory::getApplication(); + + foreach ($ids as $tn) { + // Make sure we get the right tables based on prefix. + if (stripos($tn, $app->get('dbprefix')) !== 0) { + continue; + } + + $fields = $db->getTableColumns($tn, false); + + if (!(isset($fields['checked_out']) && isset($fields['checked_out_time']))) { + continue; + } + + $query = $db->getQuery(true) + ->update($db->quoteName($tn)) + ->set($db->quoteName('checked_out') . ' = DEFAULT'); + + if ($fields['checked_out_time']->Null === 'YES') { + $query->set($db->quoteName('checked_out_time') . ' = NULL'); + } else { + $nullDate = $db->getNullDate(); + + $query->set($db->quoteName('checked_out_time') . ' = :checkouttime') + ->bind(':checkouttime', $nullDate); + } + + if ($fields['checked_out']->Null === 'YES') { + $query->where($db->quoteName('checked_out') . ' IS NOT NULL'); + } else { + $query->where($db->quoteName('checked_out') . ' > 0'); + } + + $db->setQuery($query); + + if ($db->execute()) { + $results = $results + $db->getAffectedRows(); + $app->triggerEvent('onAfterCheckin', array($tn)); + } + } + + return $results; + } + + /** + * Get total of tables + * + * @return integer Total to check-in tables + * + * @since 1.6 + */ + public function getTotal() + { + if (!isset($this->total)) { + $this->getItems(); + } + + return $this->total; + } + + /** + * Get tables + * + * @return array Checked in table names as keys and checked in item count as values. + * + * @since 1.6 + */ + public function getItems() + { + if (!isset($this->items)) { + $db = $this->getDatabase(); + $tables = $db->getTableList(); + $prefix = Factory::getApplication()->get('dbprefix'); + + // This array will hold table name as key and checked in item count as value. + $results = array(); + + foreach ($tables as $tn) { + // Make sure we get the right tables based on prefix. + if (stripos($tn, $prefix) !== 0) { + continue; + } + + if ($this->getState('filter.search') && stripos($tn, $this->getState('filter.search')) === false) { + continue; + } + + $fields = $db->getTableColumns($tn, false); + + if (!(isset($fields['checked_out']) && isset($fields['checked_out_time']))) { + continue; + } + + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName($tn)); + + if ($fields['checked_out']->Null === 'YES') { + $query->where($db->quoteName('checked_out') . ' IS NOT NULL'); + } else { + $query->where($db->quoteName('checked_out') . ' > 0'); + } + + $db->setQuery($query); + $count = $db->loadResult(); + + if ($count) { + $results[$tn] = $count; + } + } + + $this->total = count($results); + + // Order items by table + if ($this->getState('list.ordering') == 'table') { + if (strtolower($this->getState('list.direction')) == 'asc') { + ksort($results); + } else { + krsort($results); + } + } + // Order items by number of items + else { + if (strtolower($this->getState('list.direction')) == 'asc') { + asort($results); + } else { + arsort($results); + } + } + + // Pagination + $limit = (int) $this->getState('list.limit'); + + if ($limit !== 0) { + $this->items = array_slice($results, $this->getState('list.start'), $limit); + } else { + $this->items = $results; + } + } + + return $this->items; + } } diff --git a/administrator/components/com_checkin/src/View/Checkin/HtmlView.php b/administrator/components/com_checkin/src/View/Checkin/HtmlView.php index 7bb8218f1611d..7f970e62c4d7f 100644 --- a/administrator/components/com_checkin/src/View/Checkin/HtmlView.php +++ b/administrator/components/com_checkin/src/View/Checkin/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->total = $this->get('Total'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items)) - { - $this->isEmptyState = true; - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_CHECKIN_GLOBAL_CHECK_IN'), 'check-square'); - - if (!$this->isEmptyState) - { - ToolbarHelper::custom('checkin', 'checkin', '', 'JTOOLBAR_CHECKIN', true); - } - - if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_checkin')) - { - ToolbarHelper::divider(); - ToolbarHelper::preferences('com_checkin'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Maintenance:_Global_Check-in'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->total = $this->get('Total'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items)) { + $this->isEmptyState = true; + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_CHECKIN_GLOBAL_CHECK_IN'), 'check-square'); + + if (!$this->isEmptyState) { + ToolbarHelper::custom('checkin', 'checkin', '', 'JTOOLBAR_CHECKIN', true); + } + + if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_checkin')) { + ToolbarHelper::divider(); + ToolbarHelper::preferences('com_checkin'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Maintenance:_Global_Check-in'); + } } diff --git a/administrator/components/com_checkin/tmpl/checkin/default.php b/administrator/components/com_checkin/tmpl/checkin/default.php index 930cec729630d..1718632ffaa34 100644 --- a/administrator/components/com_checkin/tmpl/checkin/default.php +++ b/administrator/components/com_checkin/tmpl/checkin/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
    -
    -
    -
    - $this)); ?> - total > 0) : ?> - - - - - - - - - - - - items as $table => $count) : ?> - - - - - - - - -
    - , - , - -
    - - - - - -
    - - - - - -
    +
    +
    +
    + $this)); ?> + total > 0) : ?> + + + + + + + + + + + + items as $table => $count) : ?> + + + + + + + + +
    + , + , + +
    + + + + + +
    + + + + + +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
    -
    -
    + + + + +
    +
    +
    diff --git a/administrator/components/com_checkin/tmpl/checkin/emptystate.php b/administrator/components/com_checkin/tmpl/checkin/emptystate.php index 9a85834893264..640e87e608e7b 100644 --- a/administrator/components/com_checkin/tmpl/checkin/emptystate.php +++ b/administrator/components/com_checkin/tmpl/checkin/emptystate.php @@ -1,4 +1,5 @@ 'COM_CHECKIN', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Maintenance:_Global_Check-in', - 'icon' => 'icon-check-square', - 'title' => Text::_('COM_CHECKIN_GLOBAL_CHECK_IN'), + 'textPrefix' => 'COM_CHECKIN', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Maintenance:_Global_Check-in', + 'icon' => 'icon-check-square', + 'title' => Text::_('COM_CHECKIN_GLOBAL_CHECK_IN'), ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_config/services/provider.php b/administrator/components/com_config/services/provider.php index d5a8df8a3d035..f0b39f879ae5f 100644 --- a/administrator/components/com_config/services/provider.php +++ b/administrator/components/com_config/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Config')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Config')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Config')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Config')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Config')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Config')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new ConfigComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new ConfigComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_config/src/Controller/ApplicationController.php b/administrator/components/com_config/src/Controller/ApplicationController.php index 9fc6687c48cab..3c78d9511266e 100644 --- a/administrator/components/com_config/src/Controller/ApplicationController.php +++ b/administrator/components/com_config/src/Controller/ApplicationController.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - } - - /** - * Cancel operation. - * - * @return void - * - * @since 3.0.0 - */ - public function cancel() - { - $this->setRedirect(Route::_('index.php?option=com_cpanel')); - } - - /** - * Saves the form - * - * @return void|boolean Void on success. Boolean false on fail. - * - * @since 4.0.0 - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - // Check if the user is authorized to do this. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - - return false; - } - - $this->app->setUserState('com_config.config.global.data', null); - - /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ - $model = $this->getModel('Application', 'Administrator'); - - $data = $this->input->post->get('jform', array(), 'array'); - - // Complete data array if needed - $oldData = $model->getData(); - $data = array_replace($oldData, $data); - - // Get request type - $saveFormat = $this->app->getDocument()->getType(); - - // Handle service requests - if ($saveFormat == 'json') - { - $form = $model->getForm(); - $return = $model->validate($form, $data); - - if ($return === false) - { - $this->app->setHeader('Status', 422, true); - - return false; - } - - return $model->save($return); - } - - // Must load after serving service-requests - $form = $model->getForm(); - - // Validate the posted data. - $return = $model->validate($form, $data); - - // Check for validation errors. - if ($return === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the posted data in the session. - $this->app->setUserState('com_config.config.global.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_config', false)); - - return false; - } - - // Validate database connection data. - $data = $return; - $return = $model->validateDbConnection($data); - - // Check for validation errors. - if ($return === false) - { - /* - * The validateDbConnection method enqueued all messages for us. - */ - - // Save the posted data in the session. - $this->app->setUserState('com_config.config.global.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_config', false)); - - return false; - } - - // Save the validated data in the session. - $this->app->setUserState('com_config.config.global.data', $return); - - // Attempt to save the configuration. - $data = $return; - $return = $model->save($data); - - // Check the return value. - if ($return === false) - { - /* - * The save method enqueued all messages for us, so we just need to redirect back. - */ - - // Save failed, go back to the screen and display a notice. - $this->setRedirect(Route::_('index.php?option=com_config', false)); - - return false; - } - - // Set the success message. - $this->app->enqueueMessage(Text::_('COM_CONFIG_SAVE_SUCCESS'), 'message'); - - // Set the redirect based on the task. - switch ($this->input->getCmd('task')) - { - case 'apply': - $this->setRedirect(Route::_('index.php?option=com_config', false)); - break; - - case 'save': - default: - $this->setRedirect(Route::_('index.php', false)); - break; - } - } - - /** - * Method to remove root in global configuration. - * - * @return boolean - * - * @since 3.2 - */ - public function removeroot() - { - // Check for request forgeries. - if (!Session::checkToken('get')) - { - $this->setRedirect('index.php', Text::_('JINVALID_TOKEN'), 'error'); - - return false; - } - - // Check if the user is authorized to do this. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - - return false; - } - - // Initialise model. - - /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ - $model = $this->getModel('Application', 'Administrator'); - - // Attempt to save the configuration and remove root. - try - { - $model->removeroot(); - } - catch (\RuntimeException $e) - { - // Save failed, go back to the screen and display a notice. - $this->setRedirect('index.php', Text::_('JERROR_SAVE_FAILED', $e->getMessage()), 'error'); - - return false; - } - - // Set the redirect based on the task. - $this->setRedirect(Route::_('index.php'), Text::_('COM_CONFIG_SAVE_SUCCESS')); - - return true; - } - - /** - * Method to send the test mail. - * - * @return void - * - * @since 3.5 - */ - public function sendtestmail() - { - // Send json mime type. - $this->app->mimeType = 'application/json'; - $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); - $this->app->sendHeaders(); - - // Check if user token is valid. - if (!Session::checkToken()) - { - $this->app->enqueueMessage(Text::_('JINVALID_TOKEN'), 'error'); - echo new JsonResponse; - $this->app->close(); - } - - // Check if the user is authorized to do this. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - echo new JsonResponse; - $this->app->close(); - } - - /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ - $model = $this->getModel('Application', 'Administrator'); - - echo new JsonResponse($model->sendTestMail()); - - $this->app->close(); - } - - /** - * Method to GET permission value and give it to the model for storing in the database. - * - * @return void - * - * @since 3.5 - */ - public function store() - { - // Send json mime type. - $this->app->mimeType = 'application/json'; - $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); - $this->app->sendHeaders(); - - // Check if user token is valid. - if (!Session::checkToken('get')) - { - $this->app->enqueueMessage(Text::_('JINVALID_TOKEN'), 'error'); - echo new JsonResponse; - $this->app->close(); - } - - /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ - $model = $this->getModel('Application', 'Administrator'); - echo new JsonResponse($model->storePermissions()); - $this->app->close(); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // Map the apply task to the save method. + $this->registerTask('apply', 'save'); + } + + /** + * Cancel operation. + * + * @return void + * + * @since 3.0.0 + */ + public function cancel() + { + $this->setRedirect(Route::_('index.php?option=com_cpanel')); + } + + /** + * Saves the form + * + * @return void|boolean Void on success. Boolean false on fail. + * + * @since 4.0.0 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + // Check if the user is authorized to do this. + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + + return false; + } + + $this->app->setUserState('com_config.config.global.data', null); + + /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ + $model = $this->getModel('Application', 'Administrator'); + + $data = $this->input->post->get('jform', array(), 'array'); + + // Complete data array if needed + $oldData = $model->getData(); + $data = array_replace($oldData, $data); + + // Get request type + $saveFormat = $this->app->getDocument()->getType(); + + // Handle service requests + if ($saveFormat == 'json') { + $form = $model->getForm(); + $return = $model->validate($form, $data); + + if ($return === false) { + $this->app->setHeader('Status', 422, true); + + return false; + } + + return $model->save($return); + } + + // Must load after serving service-requests + $form = $model->getForm(); + + // Validate the posted data. + $return = $model->validate($form, $data); + + // Check for validation errors. + if ($return === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the posted data in the session. + $this->app->setUserState('com_config.config.global.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_config', false)); + + return false; + } + + // Validate database connection data. + $data = $return; + $return = $model->validateDbConnection($data); + + // Check for validation errors. + if ($return === false) { + /* + * The validateDbConnection method enqueued all messages for us. + */ + + // Save the posted data in the session. + $this->app->setUserState('com_config.config.global.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_config', false)); + + return false; + } + + // Save the validated data in the session. + $this->app->setUserState('com_config.config.global.data', $return); + + // Attempt to save the configuration. + $data = $return; + $return = $model->save($data); + + // Check the return value. + if ($return === false) { + /* + * The save method enqueued all messages for us, so we just need to redirect back. + */ + + // Save failed, go back to the screen and display a notice. + $this->setRedirect(Route::_('index.php?option=com_config', false)); + + return false; + } + + // Set the success message. + $this->app->enqueueMessage(Text::_('COM_CONFIG_SAVE_SUCCESS'), 'message'); + + // Set the redirect based on the task. + switch ($this->input->getCmd('task')) { + case 'apply': + $this->setRedirect(Route::_('index.php?option=com_config', false)); + break; + + case 'save': + default: + $this->setRedirect(Route::_('index.php', false)); + break; + } + } + + /** + * Method to remove root in global configuration. + * + * @return boolean + * + * @since 3.2 + */ + public function removeroot() + { + // Check for request forgeries. + if (!Session::checkToken('get')) { + $this->setRedirect('index.php', Text::_('JINVALID_TOKEN'), 'error'); + + return false; + } + + // Check if the user is authorized to do this. + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + + return false; + } + + // Initialise model. + + /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ + $model = $this->getModel('Application', 'Administrator'); + + // Attempt to save the configuration and remove root. + try { + $model->removeroot(); + } catch (\RuntimeException $e) { + // Save failed, go back to the screen and display a notice. + $this->setRedirect('index.php', Text::_('JERROR_SAVE_FAILED', $e->getMessage()), 'error'); + + return false; + } + + // Set the redirect based on the task. + $this->setRedirect(Route::_('index.php'), Text::_('COM_CONFIG_SAVE_SUCCESS')); + + return true; + } + + /** + * Method to send the test mail. + * + * @return void + * + * @since 3.5 + */ + public function sendtestmail() + { + // Send json mime type. + $this->app->mimeType = 'application/json'; + $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); + $this->app->sendHeaders(); + + // Check if user token is valid. + if (!Session::checkToken()) { + $this->app->enqueueMessage(Text::_('JINVALID_TOKEN'), 'error'); + echo new JsonResponse(); + $this->app->close(); + } + + // Check if the user is authorized to do this. + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + echo new JsonResponse(); + $this->app->close(); + } + + /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ + $model = $this->getModel('Application', 'Administrator'); + + echo new JsonResponse($model->sendTestMail()); + + $this->app->close(); + } + + /** + * Method to GET permission value and give it to the model for storing in the database. + * + * @return void + * + * @since 3.5 + */ + public function store() + { + // Send json mime type. + $this->app->mimeType = 'application/json'; + $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); + $this->app->sendHeaders(); + + // Check if user token is valid. + if (!Session::checkToken('get')) { + $this->app->enqueueMessage(Text::_('JINVALID_TOKEN'), 'error'); + echo new JsonResponse(); + $this->app->close(); + } + + /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ + $model = $this->getModel('Application', 'Administrator'); + echo new JsonResponse($model->storePermissions()); + $this->app->close(); + } } diff --git a/administrator/components/com_config/src/Controller/ComponentController.php b/administrator/components/com_config/src/Controller/ComponentController.php index 247aecede26fb..9206ecb9e509e 100644 --- a/administrator/components/com_config/src/Controller/ComponentController.php +++ b/administrator/components/com_config/src/Controller/ComponentController.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - } - - /** - * Method to save component configuration. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean - * - * @since 3.2 - */ - public function save($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - $data = $this->input->get('jform', [], 'ARRAY'); - $id = $this->input->get('id', null, 'INT'); - $option = $this->input->get('component'); - $user = $this->app->getIdentity(); - $context = "$this->option.edit.$this->context.$option"; - - /** @var \Joomla\Component\Config\Administrator\Model\ComponentModel $model */ - $model = $this->getModel('Component', 'Administrator'); - $model->setState('component.option', $option); - $form = $model->getForm(); - - // Make sure com_joomlaupdate and com_privacy can only be accessed by SuperUser - if (\in_array(strtolower($option), ['com_joomlaupdate', 'com_privacy'], true) && !$user->authorise('core.admin')) - { - $this->setRedirect(Route::_('index.php', false), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - } - - // Check if the user is authorised to do this. - if (!$user->authorise('core.admin', $option) && !$user->authorise('core.options', $option)) - { - $this->setRedirect(Route::_('index.php', false), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - } - - // Remove the permissions rules data if user isn't allowed to edit them. - if (!$user->authorise('core.admin', $option) && isset($data['params']) && isset($data['params']['rules'])) - { - unset($data['params']['rules']); - } - - $returnUri = $this->input->post->get('return', null, 'base64'); - - $redirect = ''; - - if (!empty($returnUri)) - { - $redirect = '&return=' . urlencode($returnUri); - } - - // Validate the posted data. - $return = $model->validate($form, $data); - - // Check for validation errors. - if ($return === false) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), - $model->getError(), - 'error' - ); - - return false; - } - - // Attempt to save the configuration. - $data = [ - 'params' => $return, - 'id' => $id, - 'option' => $option, - ]; - - try - { - $model->save($data); - } - catch (\RuntimeException $e) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $data); - - // Save failed, go back to the screen and display a notice. - $this->setRedirect( - Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), - Text::_('JERROR_SAVE_FAILED', $e->getMessage()), - 'error' - ); - - return false; - } - - // Clear session data. - $this->app->setUserState($context . '.data', null); - - // Set the redirect based on the task. - switch ($this->input->get('task')) - { - case 'apply': - $this->setRedirect( - Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), - Text::_('COM_CONFIG_SAVE_SUCCESS'), - 'message' - ); - - break; - - case 'save': - $this->setMessage(Text::_('COM_CONFIG_SAVE_SUCCESS'), 'message'); - - // No break - - default: - $redirect = 'index.php?option=' . $option; - - if (!empty($returnUri)) - { - $redirect = base64_decode($returnUri); - } - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect)) - { - $redirect = Uri::base(); - } - - $this->setRedirect(Route::_($redirect, false)); - } - - return true; - } - - /** - * Method to cancel global configuration component. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean - * - * @since 3.2 - */ - public function cancel($key = null) - { - $component = $this->input->get('component'); - - // Clear session data. - $this->app->setUserState("$this->option.edit.$this->context.$component.data", null); - - // Calculate redirect URL - $returnUri = $this->input->post->get('return', null, 'base64'); - - $redirect = 'index.php?option=' . $component; - - if (!empty($returnUri)) - { - $redirect = base64_decode($returnUri); - } - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect)) - { - $redirect = Uri::base(); - } - - $this->setRedirect(Route::_($redirect, false)); - - return true; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // Map the apply task to the save method. + $this->registerTask('apply', 'save'); + } + + /** + * Method to save component configuration. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean + * + * @since 3.2 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + $data = $this->input->get('jform', [], 'ARRAY'); + $id = $this->input->get('id', null, 'INT'); + $option = $this->input->get('component'); + $user = $this->app->getIdentity(); + $context = "$this->option.edit.$this->context.$option"; + + /** @var \Joomla\Component\Config\Administrator\Model\ComponentModel $model */ + $model = $this->getModel('Component', 'Administrator'); + $model->setState('component.option', $option); + $form = $model->getForm(); + + // Make sure com_joomlaupdate and com_privacy can only be accessed by SuperUser + if (\in_array(strtolower($option), ['com_joomlaupdate', 'com_privacy'], true) && !$user->authorise('core.admin')) { + $this->setRedirect(Route::_('index.php', false), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + } + + // Check if the user is authorised to do this. + if (!$user->authorise('core.admin', $option) && !$user->authorise('core.options', $option)) { + $this->setRedirect(Route::_('index.php', false), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + } + + // Remove the permissions rules data if user isn't allowed to edit them. + if (!$user->authorise('core.admin', $option) && isset($data['params']) && isset($data['params']['rules'])) { + unset($data['params']['rules']); + } + + $returnUri = $this->input->post->get('return', null, 'base64'); + + $redirect = ''; + + if (!empty($returnUri)) { + $redirect = '&return=' . urlencode($returnUri); + } + + // Validate the posted data. + $return = $model->validate($form, $data); + + // Check for validation errors. + if ($return === false) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), + $model->getError(), + 'error' + ); + + return false; + } + + // Attempt to save the configuration. + $data = [ + 'params' => $return, + 'id' => $id, + 'option' => $option, + ]; + + try { + $model->save($data); + } catch (\RuntimeException $e) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $data); + + // Save failed, go back to the screen and display a notice. + $this->setRedirect( + Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), + Text::_('JERROR_SAVE_FAILED', $e->getMessage()), + 'error' + ); + + return false; + } + + // Clear session data. + $this->app->setUserState($context . '.data', null); + + // Set the redirect based on the task. + switch ($this->input->get('task')) { + case 'apply': + $this->setRedirect( + Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), + Text::_('COM_CONFIG_SAVE_SUCCESS'), + 'message' + ); + + break; + + case 'save': + $this->setMessage(Text::_('COM_CONFIG_SAVE_SUCCESS'), 'message'); + + // No break + + default: + $redirect = 'index.php?option=' . $option; + + if (!empty($returnUri)) { + $redirect = base64_decode($returnUri); + } + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect)) { + $redirect = Uri::base(); + } + + $this->setRedirect(Route::_($redirect, false)); + } + + return true; + } + + /** + * Method to cancel global configuration component. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean + * + * @since 3.2 + */ + public function cancel($key = null) + { + $component = $this->input->get('component'); + + // Clear session data. + $this->app->setUserState("$this->option.edit.$this->context.$component.data", null); + + // Calculate redirect URL + $returnUri = $this->input->post->get('return', null, 'base64'); + + $redirect = 'index.php?option=' . $component; + + if (!empty($returnUri)) { + $redirect = base64_decode($returnUri); + } + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect)) { + $redirect = Uri::base(); + } + + $this->setRedirect(Route::_($redirect, false)); + + return true; + } } diff --git a/administrator/components/com_config/src/Controller/DisplayController.php b/administrator/components/com_config/src/Controller/DisplayController.php index 2ed6a6f030c14..8e7a8eee0013b 100644 --- a/administrator/components/com_config/src/Controller/DisplayController.php +++ b/administrator/components/com_config/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('component', ''); - - // Make sure com_joomlaupdate and com_privacy can only be accessed by SuperUser - if (in_array(strtolower($component), array('com_joomlaupdate', 'com_privacy')) - && !$this->app->getIdentity()->authorise('core.admin')) - { - $this->setRedirect(Route::_('index.php'), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - } - - return parent::display($cachable, $urlparams); - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'application'; + + + /** + * Typical view method for MVC based architecture + * + * This function is provide as a default implementation, in most cases + * you will need to override it in your own controllers. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe url parameters and their variable types, for valid values see {@link InputFilter::clean()}. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 3.0 + * @throws \Exception + */ + public function display($cachable = false, $urlparams = array()) + { + $component = $this->input->get('component', ''); + + // Make sure com_joomlaupdate and com_privacy can only be accessed by SuperUser + if ( + in_array(strtolower($component), array('com_joomlaupdate', 'com_privacy')) + && !$this->app->getIdentity()->authorise('core.admin') + ) { + $this->setRedirect(Route::_('index.php'), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + } + + return parent::display($cachable, $urlparams); + } } diff --git a/administrator/components/com_config/src/Controller/RequestController.php b/administrator/components/com_config/src/Controller/RequestController.php index f3121774503e9..bdfd7423e9f2d 100644 --- a/administrator/components/com_config/src/Controller/RequestController.php +++ b/administrator/components/com_config/src/Controller/RequestController.php @@ -1,4 +1,5 @@ input->getWord('option', 'com_config'); - - if ($this->app->isClient('administrator')) - { - $viewName = $this->input->getWord('view', 'application'); - } - else - { - $viewName = $this->input->getWord('view', 'config'); - } - - // Register the layout paths for the view - $paths = new \SplPriorityQueue; - - if ($this->app->isClient('administrator')) - { - $paths->insert(JPATH_ADMINISTRATOR . '/components/' . $componentFolder . '/view/' . $viewName . '/tmpl', 1); - } - else - { - $paths->insert(JPATH_BASE . '/components/' . $componentFolder . '/view/' . $viewName . '/tmpl', 1); - } - - $model = new \Joomla\Component\Config\Administrator\Model\ApplicationModel; - $component = $model->getState()->get('component.option'); - - // Access check. - if (!$this->app->getIdentity()->authorise('core.admin', $component) - && !$this->app->getIdentity()->authorise('core.options', $component)) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - - return false; - } - - try - { - $data = $model->getData(); - $user = $this->app->getIdentity(); - } - catch (\Exception $e) - { - $this->app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - $this->userIsSuperAdmin = $user->authorise('core.admin'); - - // Required data - $requiredData = array( - 'sitename' => null, - 'offline' => null, - 'access' => null, - 'list_limit' => null, - 'MetaDesc' => null, - 'MetaRights' => null, - 'sef' => null, - 'sitename_pagetitles' => null, - 'debug' => null, - 'debug_lang' => null, - 'error_reporting' => null, - 'mailfrom' => null, - 'fromname' => null - ); - - $data = array_intersect_key($data, $requiredData); - - return json_encode($data); - } + /** + * Execute the controller. + * + * @return mixed A rendered view or false + * + * @since 3.2 + */ + public function getJson() + { + $componentFolder = $this->input->getWord('option', 'com_config'); + + if ($this->app->isClient('administrator')) { + $viewName = $this->input->getWord('view', 'application'); + } else { + $viewName = $this->input->getWord('view', 'config'); + } + + // Register the layout paths for the view + $paths = new \SplPriorityQueue(); + + if ($this->app->isClient('administrator')) { + $paths->insert(JPATH_ADMINISTRATOR . '/components/' . $componentFolder . '/view/' . $viewName . '/tmpl', 1); + } else { + $paths->insert(JPATH_BASE . '/components/' . $componentFolder . '/view/' . $viewName . '/tmpl', 1); + } + + $model = new \Joomla\Component\Config\Administrator\Model\ApplicationModel(); + $component = $model->getState()->get('component.option'); + + // Access check. + if ( + !$this->app->getIdentity()->authorise('core.admin', $component) + && !$this->app->getIdentity()->authorise('core.options', $component) + ) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + + return false; + } + + try { + $data = $model->getData(); + $user = $this->app->getIdentity(); + } catch (\Exception $e) { + $this->app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + $this->userIsSuperAdmin = $user->authorise('core.admin'); + + // Required data + $requiredData = array( + 'sitename' => null, + 'offline' => null, + 'access' => null, + 'list_limit' => null, + 'MetaDesc' => null, + 'MetaRights' => null, + 'sef' => null, + 'sitename_pagetitles' => null, + 'debug' => null, + 'debug_lang' => null, + 'error_reporting' => null, + 'mailfrom' => null, + 'fromname' => null + ); + + $data = array_intersect_key($data, $requiredData); + + return json_encode($data); + } } diff --git a/administrator/components/com_config/src/Extension/ConfigComponent.php b/administrator/components/com_config/src/Extension/ConfigComponent.php index f275a9d63321c..e0a56e8c59d21 100644 --- a/administrator/components/com_config/src/Extension/ConfigComponent.php +++ b/administrator/components/com_config/src/Extension/ConfigComponent.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select('name AS text, element AS value') - ->from('#__extensions') - ->where('enabled >= 1') - ->where('type =' . $db->quote('component')); + /** + * Method to get a list of options for a list input. + * + * @return array An array of JHtml options. + * + * @since 3.7.0 + */ + protected function getOptions() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('name AS text, element AS value') + ->from('#__extensions') + ->where('enabled >= 1') + ->where('type =' . $db->quote('component')); - $items = $db->setQuery($query)->loadObjectList(); + $items = $db->setQuery($query)->loadObjectList(); - if ($items) - { - $lang = Factory::getLanguage(); + if ($items) { + $lang = Factory::getLanguage(); - foreach ($items as &$item) - { - // Load language - $extension = $item->value; + foreach ($items as &$item) { + // Load language + $extension = $item->value; - if (File::exists(JPATH_ADMINISTRATOR . '/components/' . $extension . '/config.xml')) - { - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) - || $lang->load("$extension.sys", $source); + if (File::exists(JPATH_ADMINISTRATOR . '/components/' . $extension . '/config.xml')) { + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) + || $lang->load("$extension.sys", $source); - // Translate component name - $item->text = Text::_($item->text); - } - else - { - $item = null; - } - } + // Translate component name + $item->text = Text::_($item->text); + } else { + $item = null; + } + } - // Sort by component name - $items = ArrayHelper::sortObjects(array_filter($items), 'text', 1, true, true); - } + // Sort by component name + $items = ArrayHelper::sortObjects(array_filter($items), 'text', 1, true, true); + } - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $items); + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $items); - return $options; - } + return $options; + } } diff --git a/administrator/components/com_config/src/Field/FiltersField.php b/administrator/components/com_config/src/Field/FiltersField.php index 052c548face8b..0ea4cfe57ada5 100644 --- a/administrator/components/com_config/src/Field/FiltersField.php +++ b/administrator/components/com_config/src/Field/FiltersField.php @@ -1,4 +1,5 @@ getWebAssetManager()->useScript('com_config.filters'); - - // Get the available user groups. - $groups = $this->getUserGroups(); - - // Build the form control. - $html = array(); - - // Open the table. - $html[] = ''; - - // The table heading. - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - - // The table body. - $html[] = ' '; - - foreach ($groups as $group) - { - if (!isset($this->value[$group->value])) - { - $this->value[$group->value] = array('filter_type' => 'BL', 'filter_tags' => '', 'filter_attributes' => ''); - } - - $group_filter = $this->value[$group->value]; - - $group_filter['filter_tags'] = !empty($group_filter['filter_tags']) ? $group_filter['filter_tags'] : ''; - $group_filter['filter_attributes'] = !empty($group_filter['filter_attributes']) ? $group_filter['filter_attributes'] : ''; - - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - } - - $html[] = ' '; - - // Close the table. - $html[] = '
    '; - $html[] = ' ' . Text::_('JGLOBAL_FILTER_GROUPS_LABEL') . ''; - $html[] = ' '; - $html[] = ' ' . Text::_('JGLOBAL_FILTER_TYPE_LABEL') . ''; - $html[] = ' '; - $html[] = ' ' . Text::_('JGLOBAL_FILTER_TAGS_LABEL') . ''; - $html[] = ' '; - $html[] = ' ' . Text::_('JGLOBAL_FILTER_ATTRIBUTES_LABEL') . ''; - $html[] = '
    '; - $html[] = ' ' . LayoutHelper::render('joomla.html.treeprefix', array('level' => $group->level + 1)) . $group->text; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = '
    '; - - return implode("\n", $html); - } - - /** - * A helper to get the list of user groups. - * - * @return array - * - * @since 1.6 - */ - protected function getUserGroups() - { - // Get a database object. - $db = $this->getDatabase(); - - // Get the user groups from the database. - $query = $db->getQuery(true); - $query->select('a.id AS value, a.title AS text, COUNT(DISTINCT b.id) AS level, a.parent_id as parent'); - $query->from('#__usergroups AS a'); - $query->join('LEFT', '#__usergroups AS b on a.lft > b.lft AND a.rgt < b.rgt'); - $query->group('a.id, a.title, a.lft'); - $query->order('a.lft ASC'); - $db->setQuery($query); - $options = $db->loadObjectList(); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + public $type = 'Filters'; + + /** + * Method to get the field input markup. + * + * @todo: Add access check. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + // Add translation string for notification + Text::script('COM_CONFIG_TEXT_FILTERS_NOTE'); + + // Add Javascript + Factory::getDocument()->getWebAssetManager()->useScript('com_config.filters'); + + // Get the available user groups. + $groups = $this->getUserGroups(); + + // Build the form control. + $html = array(); + + // Open the table. + $html[] = ''; + + // The table heading. + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + + // The table body. + $html[] = ' '; + + foreach ($groups as $group) { + if (!isset($this->value[$group->value])) { + $this->value[$group->value] = array('filter_type' => 'BL', 'filter_tags' => '', 'filter_attributes' => ''); + } + + $group_filter = $this->value[$group->value]; + + $group_filter['filter_tags'] = !empty($group_filter['filter_tags']) ? $group_filter['filter_tags'] : ''; + $group_filter['filter_attributes'] = !empty($group_filter['filter_attributes']) ? $group_filter['filter_attributes'] : ''; + + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + } + + $html[] = ' '; + + // Close the table. + $html[] = '
    '; + $html[] = ' ' . Text::_('JGLOBAL_FILTER_GROUPS_LABEL') . ''; + $html[] = ' '; + $html[] = ' ' . Text::_('JGLOBAL_FILTER_TYPE_LABEL') . ''; + $html[] = ' '; + $html[] = ' ' . Text::_('JGLOBAL_FILTER_TAGS_LABEL') . ''; + $html[] = ' '; + $html[] = ' ' . Text::_('JGLOBAL_FILTER_ATTRIBUTES_LABEL') . ''; + $html[] = '
    '; + $html[] = ' ' . LayoutHelper::render('joomla.html.treeprefix', array('level' => $group->level + 1)) . $group->text; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = '
    '; + + return implode("\n", $html); + } + + /** + * A helper to get the list of user groups. + * + * @return array + * + * @since 1.6 + */ + protected function getUserGroups() + { + // Get a database object. + $db = $this->getDatabase(); + + // Get the user groups from the database. + $query = $db->getQuery(true); + $query->select('a.id AS value, a.title AS text, COUNT(DISTINCT b.id) AS level, a.parent_id as parent'); + $query->from('#__usergroups AS a'); + $query->join('LEFT', '#__usergroups AS b on a.lft > b.lft AND a.rgt < b.rgt'); + $query->group('a.id, a.title, a.lft'); + $query->order('a.lft ASC'); + $db->setQuery($query); + $options = $db->loadObjectList(); + + return $options; + } } diff --git a/administrator/components/com_config/src/Helper/ConfigHelper.php b/administrator/components/com_config/src/Helper/ConfigHelper.php index ee5ed8b3d84f5..15aea075f9d7a 100644 --- a/administrator/components/com_config/src/Helper/ConfigHelper.php +++ b/administrator/components/com_config/src/Helper/ConfigHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('element') - ->from('#__extensions') - ->where('type = ' . $db->quote('component')) - ->where('enabled = 1'); - $db->setQuery($query); - $result = $db->loadColumn(); - - return $result; - } - - /** - * Returns true if the component has configuration options. - * - * @param string $component Component name - * - * @return boolean - * - * @since 3.0 - */ - public static function hasComponentConfig($component) - { - return is_file(JPATH_ADMINISTRATOR . '/components/' . $component . '/config.xml'); - } - - /** - * Returns an array of all components with configuration options. - * Optionally return only those components for which the current user has 'core.manage' rights. - * - * @param boolean $authCheck True to restrict to components where current user has 'core.manage' rights. - * - * @return array - * - * @since 3.0 - */ - public static function getComponentsWithConfig($authCheck = true) - { - $result = array(); - $components = self::getAllComponents(); - $user = Factory::getUser(); - - // Remove com_config from the array as that may have weird side effects - $components = array_diff($components, array('com_config')); - - foreach ($components as $component) - { - if (self::hasComponentConfig($component) && (!$authCheck || $user->authorise('core.manage', $component))) - { - self::loadLanguageForComponent($component); - $result[$component] = ApplicationHelper::stringURLSafe(Text::_($component)) . '_' . $component; - } - } - - asort($result); - - return array_keys($result); - } - - /** - * Load the sys language for the given component. - * - * @param array $components Array of component names. - * - * @return void - * - * @since 3.0 - */ - public static function loadLanguageForComponents($components) - { - foreach ($components as $component) - { - self::loadLanguageForComponent($component); - } - } - - /** - * Load the sys language for the given component. - * - * @param string $component component name. - * - * @return void - * - * @since 3.5 - */ - public static function loadLanguageForComponent($component) - { - if (empty($component)) - { - return; - } - - $lang = Factory::getLanguage(); - - // Load the core file then - // Load extension-local file. - $lang->load($component . '.sys', JPATH_BASE) - || $lang->load($component . '.sys', JPATH_ADMINISTRATOR . '/components/' . $component); - } + /** + * Get an array of all enabled components. + * + * @return array + * + * @since 3.0 + */ + public static function getAllComponents() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('element') + ->from('#__extensions') + ->where('type = ' . $db->quote('component')) + ->where('enabled = 1'); + $db->setQuery($query); + $result = $db->loadColumn(); + + return $result; + } + + /** + * Returns true if the component has configuration options. + * + * @param string $component Component name + * + * @return boolean + * + * @since 3.0 + */ + public static function hasComponentConfig($component) + { + return is_file(JPATH_ADMINISTRATOR . '/components/' . $component . '/config.xml'); + } + + /** + * Returns an array of all components with configuration options. + * Optionally return only those components for which the current user has 'core.manage' rights. + * + * @param boolean $authCheck True to restrict to components where current user has 'core.manage' rights. + * + * @return array + * + * @since 3.0 + */ + public static function getComponentsWithConfig($authCheck = true) + { + $result = array(); + $components = self::getAllComponents(); + $user = Factory::getUser(); + + // Remove com_config from the array as that may have weird side effects + $components = array_diff($components, array('com_config')); + + foreach ($components as $component) { + if (self::hasComponentConfig($component) && (!$authCheck || $user->authorise('core.manage', $component))) { + self::loadLanguageForComponent($component); + $result[$component] = ApplicationHelper::stringURLSafe(Text::_($component)) . '_' . $component; + } + } + + asort($result); + + return array_keys($result); + } + + /** + * Load the sys language for the given component. + * + * @param array $components Array of component names. + * + * @return void + * + * @since 3.0 + */ + public static function loadLanguageForComponents($components) + { + foreach ($components as $component) { + self::loadLanguageForComponent($component); + } + } + + /** + * Load the sys language for the given component. + * + * @param string $component component name. + * + * @return void + * + * @since 3.5 + */ + public static function loadLanguageForComponent($component) + { + if (empty($component)) { + return; + } + + $lang = Factory::getLanguage(); + + // Load the core file then + // Load extension-local file. + $lang->load($component . '.sys', JPATH_BASE) + || $lang->load($component . '.sys', JPATH_ADMINISTRATOR . '/components/' . $component); + } } diff --git a/administrator/components/com_config/src/Model/ApplicationModel.php b/administrator/components/com_config/src/Model/ApplicationModel.php index 77d70c8669785..1b193f4190423 100644 --- a/administrator/components/com_config/src/Model/ApplicationModel.php +++ b/administrator/components/com_config/src/Model/ApplicationModel.php @@ -1,4 +1,5 @@ loadForm('com_config.application', 'application', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the configuration data. - * - * This method will load the global configuration data straight from - * JConfig. If configuration data has been saved in the session, that - * data will be merged into the original data, overwriting it. - * - * @return array An array containing all global config data. - * - * @since 1.6 - */ - public function getData() - { - // Get the config data. - $config = new \JConfig; - $data = ArrayHelper::fromObject($config); - - // Get the correct driver at runtime - $data['dbtype'] = $this->getDatabase()->getName(); - - // Prime the asset_id for the rules. - $data['asset_id'] = 1; - - // Get the text filter data - $params = ComponentHelper::getParams('com_config'); - $data['filters'] = ArrayHelper::fromObject($params->get('filters')); - - // If no filter data found, get from com_content (update of 1.6/1.7 site) - if (empty($data['filters'])) - { - $contentParams = ComponentHelper::getParams('com_content'); - $data['filters'] = ArrayHelper::fromObject($contentParams->get('filters')); - } - - // Check for data in the session. - $temp = Factory::getApplication()->getUserState('com_config.config.global.data'); - - // Merge in the session data. - if (!empty($temp)) - { - // $temp can sometimes be an object, and we need it to be an array - if (is_object($temp)) - { - $temp = ArrayHelper::fromObject($temp); - } - - $data = array_merge($temp, $data); - } - - // Correct error_reporting value, since we removed "development", the "maximum" should be set instead - // @TODO: This can be removed in 5.0 - if (!empty($data['error_reporting']) && $data['error_reporting'] === 'development') - { - $data['error_reporting'] = 'maximum'; - } - - return $data; - } - - /** - * Method to validate the db connection properties. - * - * @param array $data An array containing all global config data. - * - * @return array|boolean Array with the validated global config data or boolean false on a validation failure. - * - * @since 4.0.0 - */ - public function validateDbConnection($data) - { - // Validate database connection encryption options - if ((int) $data['dbencryption'] === 0) - { - // Reset unused options - if (!empty($data['dbsslkey'])) - { - $data['dbsslkey'] = ''; - } - - if (!empty($data['dbsslcert'])) - { - $data['dbsslcert'] = ''; - } - - if ((bool) $data['dbsslverifyservercert'] === true) - { - $data['dbsslverifyservercert'] = false; - } - - if (!empty($data['dbsslca'])) - { - $data['dbsslca'] = ''; - } - - if (!empty($data['dbsslcipher'])) - { - $data['dbsslcipher'] = ''; - } - } - else - { - // Check localhost - if (strtolower($data['host']) === 'localhost') - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_LOCALHOST'), 'error'); - - return false; - } - - // Check CA file and folder depending on database type if server certificate verification - if ((bool) $data['dbsslverifyservercert'] === true) - { - if (empty($data['dbsslca'])) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CA_LABEL') - ), - 'error' - ); - - return false; - } - - if (!File::exists(Path::clean($data['dbsslca']))) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CA_LABEL') - ), - 'error' - ); - - return false; - } - } - else - { - // Reset unused option - if (!empty($data['dbsslca'])) - { - $data['dbsslca'] = ''; - } - } - - // Check key and certificate if two-way encryption - if ((int) $data['dbencryption'] === 2) - { - if (empty($data['dbsslkey'])) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_KEY_LABEL') - ), - 'error' - ); - - return false; - } - - if (!File::exists(Path::clean($data['dbsslkey']))) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_KEY_LABEL') - ), - 'error' - ); - - return false; - } - - if (empty($data['dbsslcert'])) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CERT_LABEL') - ), - 'error' - ); - - return false; - } - - if (!File::exists(Path::clean($data['dbsslcert']))) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CERT_LABEL') - ), - 'error' - ); - - return false; - } - } - else - { - // Reset unused options - if (!empty($data['dbsslkey'])) - { - $data['dbsslkey'] = ''; - } - - if (!empty($data['dbsslcert'])) - { - $data['dbsslcert'] = ''; - } - } - } - - return $data; - } - - /** - * Method to save the configuration data. - * - * @param array $data An array containing all global config data. - * - * @return boolean True on success, false on failure. - * - * @since 1.6 - */ - public function save($data) - { - $app = Factory::getApplication(); - - // Try to load the values from the configuration file - foreach ($this->protectedConfigurationFields as $fieldKey) - { - if (!isset($data[$fieldKey])) - { - $data[$fieldKey] = $app->get($fieldKey, ''); - } - } - - // Check that we aren't setting wrong database configuration - $options = array( - 'driver' => $data['dbtype'], - 'host' => $data['host'], - 'user' => $data['user'], - 'password' => $data['password'], - 'database' => $data['db'], - 'prefix' => $data['dbprefix'], - ); - - if ((int) $data['dbencryption'] !== 0) - { - $options['ssl'] = [ - 'enable' => true, - 'verify_server_cert' => (bool) $data['dbsslverifyservercert'], - ]; - - foreach (['cipher', 'ca', 'key', 'cert'] as $value) - { - $confVal = trim($data['dbssl' . $value]); - - if ($confVal !== '') - { - $options['ssl'][$value] = $confVal; - } - } - } - - try - { - $revisedDbo = DatabaseDriver::getInstance($options); - $revisedDbo->getVersion(); - } - catch (\Exception $e) - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_DATABASE_NOT_AVAILABLE', $e->getCode(), $e->getMessage()), 'error'); - - return false; - } - - if ((int) $data['dbencryption'] !== 0 && empty($revisedDbo->getConnectionEncryption())) - { - if ($revisedDbo->isConnectionEncryptionSupported()) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_CONN_NOT_ENCRYPT'), 'error'); - } - else - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_SRV_NOT_SUPPORTS'), 'error'); - } - - return false; - } - - // Check if we can set the Force SSL option - if ((int) $data['force_ssl'] !== 0 && (int) $data['force_ssl'] !== (int) $app->get('force_ssl', '0')) - { - try - { - // Make an HTTPS request to check if the site is available in HTTPS. - $host = Uri::getInstance()->getHost(); - $options = new Registry; - $options->set('userAgent', 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0'); - - // Do not check for valid server certificate here, leave this to the user, moreover disable using a proxy if any is configured. - $options->set('transport.curl', - array( - CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_SSL_VERIFYHOST => false, - CURLOPT_PROXY => null, - CURLOPT_PROXYUSERPWD => null, - ) - ); - $response = HttpFactory::getHttp($options)->get('https://' . $host . Uri::root(true) . '/', array('Host' => $host), 10); - - // If available in HTTPS check also the status code. - if (!in_array($response->code, array(200, 503, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 401), true)) - { - throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_SSL_NOT_AVAILABLE_HTTP_CODE')); - } - } - catch (\RuntimeException $e) - { - $data['force_ssl'] = 0; - - // Also update the user state - $app->setUserState('com_config.config.global.data.force_ssl', 0); - - // Inform the user - $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_SSL_NOT_AVAILABLE', $e->getMessage()), 'warning'); - } - } - - // Save the rules - if (isset($data['rules'])) - { - $rules = new Rules($data['rules']); - - // Check that we aren't removing our Super User permission - // Need to get groups from database, since they might have changed - $myGroups = Access::getGroupsByUser(Factory::getUser()->get('id')); - $myRules = $rules->getData(); - $hasSuperAdmin = $myRules['core.admin']->allow($myGroups); - - if (!$hasSuperAdmin) - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_REMOVING_SUPER_ADMIN'), 'error'); - - return false; - } - - $asset = Table::getInstance('asset'); - - if ($asset->loadByName('root.1')) - { - $asset->rules = (string) $rules; - - if (!$asset->check() || !$asset->store()) - { - $app->enqueueMessage($asset->getError(), 'error'); - - return false; - } - } - else - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_ROOT_ASSET_NOT_FOUND'), 'error'); - - return false; - } - - unset($data['rules']); - } - - // Save the text filters - if (isset($data['filters'])) - { - $registry = new Registry(array('filters' => $data['filters'])); - - $extension = Table::getInstance('extension'); - - // Get extension_id - $extensionId = $extension->find(array('name' => 'com_config')); - - if ($extension->load((int) $extensionId)) - { - $extension->params = (string) $registry; - - if (!$extension->check() || !$extension->store()) - { - $app->enqueueMessage($extension->getError(), 'error'); - - return false; - } - } - else - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIG_EXTENSION_NOT_FOUND'), 'error'); - - return false; - } - - unset($data['filters']); - } - - // Get the previous configuration. - $prev = new \JConfig; - $prev = ArrayHelper::fromObject($prev); - - // Merge the new data in. We do this to preserve values that were not in the form. - $data = array_merge($prev, $data); - - /* - * Perform miscellaneous options based on configuration settings/changes. - */ - - // Escape the offline message if present. - if (isset($data['offline_message'])) - { - $data['offline_message'] = OutputFilter::ampReplace($data['offline_message']); - } - - // Purge the database session table if we are changing to the database handler. - if ($prev['session_handler'] != 'database' && $data['session_handler'] == 'database') - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__session')) - ->where($db->quoteName('time') . ' < ' . (time() - 1)); - $db->setQuery($query); - $db->execute(); - } - - // Purge the database session table if we are disabling session metadata - if ($prev['session_metadata'] == 1 && $data['session_metadata'] == 0) - { - try - { - // If we are are using the session handler, purge the extra columns, otherwise truncate the whole session table - if ($data['session_handler'] === 'database') - { - $revisedDbo->setQuery( - $revisedDbo->getQuery(true) - ->update('#__session') - ->set( - [ - $revisedDbo->quoteName('client_id') . ' = 0', - $revisedDbo->quoteName('guest') . ' = NULL', - $revisedDbo->quoteName('userid') . ' = NULL', - $revisedDbo->quoteName('username') . ' = NULL', - ] - ) - )->execute(); - } - else - { - $revisedDbo->truncateTable('#__session'); - } - } - catch (\RuntimeException $e) - { - /* - * The database API logs errors on failures so we don't need to add any error handling mechanisms here. - * Also, this data won't be added or checked anymore once the configuration is saved, so it'll purge itself - * through normal garbage collection anyway or if not using the database handler someone can purge the - * table on their own. Either way, carry on Soldier! - */ - } - } - - // Ensure custom session file path exists or try to create it if changed - if (!empty($data['session_filesystem_path'])) - { - $currentPath = $prev['session_filesystem_path'] ?? null; - - if ($currentPath) - { - $currentPath = Path::clean($currentPath); - } - - $data['session_filesystem_path'] = Path::clean($data['session_filesystem_path']); - - if ($currentPath !== $data['session_filesystem_path']) - { - if (!Folder::exists($data['session_filesystem_path']) && !Folder::create($data['session_filesystem_path'])) - { - try - { - Log::add( - Text::sprintf( - 'COM_CONFIG_ERROR_CUSTOM_SESSION_FILESYSTEM_PATH_NOTWRITABLE_USING_DEFAULT', - $data['session_filesystem_path'] - ), - Log::WARNING, - 'jerror' - ); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_CUSTOM_SESSION_FILESYSTEM_PATH_NOTWRITABLE_USING_DEFAULT', - $data['session_filesystem_path'] - ), - 'warning' - ); - } - - $data['session_filesystem_path'] = $currentPath; - } - } - } - - // Set the shared session configuration - if (isset($data['shared_session'])) - { - $currentShared = $prev['shared_session'] ?? '0'; - - // Has the user enabled shared sessions? - if ($data['shared_session'] == 1 && $currentShared == 0) - { - // Generate a random shared session name - $data['session_name'] = UserHelper::genRandomPassword(16); - } - - // Has the user disabled shared sessions? - if ($data['shared_session'] == 0 && $currentShared == 1) - { - // Remove the session name value - unset($data['session_name']); - } - } - - // Set the shared session configuration - if (isset($data['shared_session'])) - { - $currentShared = $prev['shared_session'] ?? '0'; - - // Has the user enabled shared sessions? - if ($data['shared_session'] == 1 && $currentShared == 0) - { - // Generate a random shared session name - $data['session_name'] = UserHelper::genRandomPassword(16); - } - - // Has the user disabled shared sessions? - if ($data['shared_session'] == 0 && $currentShared == 1) - { - // Remove the session name value - unset($data['session_name']); - } - } - - if (empty($data['cache_handler'])) - { - $data['caching'] = 0; - } - - /* - * Look for a custom cache_path - * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default - */ - if (!empty($data['cache_path'])) - { - $path = $data['cache_path']; - } - elseif (!empty($prev['cache_path'])) - { - $path = $prev['cache_path']; - } - else - { - $path = JPATH_CACHE; - } - - // Give a warning if the cache-folder can not be opened - if ($data['caching'] > 0 && $data['cache_handler'] == 'file' && @opendir($path) == false) - { - $error = true; - - // If a custom path is in use, try using the system default instead of disabling cache - if ($path !== JPATH_CACHE && @opendir(JPATH_CACHE) != false) - { - try - { - Log::add( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_CACHE_PATH_NOTWRITABLE_USING_DEFAULT', $path, JPATH_CACHE), - Log::WARNING, - 'jerror' - ); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_CACHE_PATH_NOTWRITABLE_USING_DEFAULT', $path, JPATH_CACHE), - 'warning' - ); - } - - $path = JPATH_CACHE; - $error = false; - - $data['cache_path'] = ''; - } - - if ($error) - { - try - { - Log::add(Text::sprintf('COM_CONFIG_ERROR_CACHE_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_CACHE_PATH_NOTWRITABLE', $path), 'warning'); - } - - $data['caching'] = 0; - } - } - - // Did the user remove their custom cache path? Don't save the variable to the config - if (empty($data['cache_path'])) - { - unset($data['cache_path']); - } - - // Clean the cache if disabled but previously enabled or changing cache handlers; these operations use the `$prev` data already in memory - if ((!$data['caching'] && $prev['caching']) || $data['cache_handler'] !== $prev['cache_handler']) - { - try - { - Factory::getCache()->clean(); - } - catch (CacheConnectingException $exception) - { - try - { - Log::add(Text::_('COM_CONFIG_ERROR_CACHE_CONNECTION_FAILED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CACHE_CONNECTION_FAILED'), 'warning'); - } - } - catch (UnsupportedCacheException $exception) - { - try - { - Log::add(Text::_('COM_CONFIG_ERROR_CACHE_DRIVER_UNSUPPORTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CACHE_DRIVER_UNSUPPORTED'), 'warning'); - } - } - } - - /* - * Look for a custom tmp_path - * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default - */ - $defaultTmpPath = JPATH_ROOT . '/tmp'; - - if (!empty($data['tmp_path'])) - { - $path = $data['tmp_path']; - } - elseif (!empty($prev['tmp_path'])) - { - $path = $prev['tmp_path']; - } - else - { - $path = $defaultTmpPath; - } - - $path = Path::clean($path); - - // Give a warning if the tmp-folder is not valid or not writable - if (!is_dir($path) || !is_writable($path)) - { - $error = true; - - // If a custom path is in use, try using the system default tmp path - if ($path !== $defaultTmpPath && is_dir($defaultTmpPath) && is_writable($defaultTmpPath)) - { - try - { - Log::add( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_TEMP_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultTmpPath), - Log::WARNING, - 'jerror' - ); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_TEMP_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultTmpPath), - 'warning' - ); - } - - $error = false; - - $data['tmp_path'] = $defaultTmpPath; - } - - if ($error) - { - try - { - Log::add(Text::sprintf('COM_CONFIG_ERROR_TMP_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_TMP_PATH_NOTWRITABLE', $path), 'warning'); - } - } - } - - /* - * Look for a custom log_path - * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default - */ - $defaultLogPath = JPATH_ADMINISTRATOR . '/logs'; - - if (!empty($data['log_path'])) - { - $path = $data['log_path']; - } - elseif (!empty($prev['log_path'])) - { - $path = $prev['log_path']; - } - else - { - $path = $defaultLogPath; - } - - $path = Path::clean($path); - - // Give a warning if the log-folder is not valid or not writable - if (!is_dir($path) || !is_writable($path)) - { - $error = true; - - // If a custom path is in use, try using the system default log path - if ($path !== $defaultLogPath && is_dir($defaultLogPath) && is_writable($defaultLogPath)) - { - try - { - Log::add( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_LOG_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultLogPath), - Log::WARNING, - 'jerror' - ); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_LOG_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultLogPath), - 'warning' - ); - } - - $error = false; - $data['log_path'] = $defaultLogPath; - } - - if ($error) - { - try - { - Log::add(Text::sprintf('COM_CONFIG_ERROR_LOG_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_LOG_PATH_NOTWRITABLE', $path), 'warning'); - } - } - } - - // Create the new configuration object. - $config = new Registry($data); - - // Overwrite webservices cors settings - $app->set('cors', $data['cors']); - $app->set('cors_allow_origin', $data['cors_allow_origin']); - $app->set('cors_allow_headers', $data['cors_allow_headers']); - $app->set('cors_allow_methods', $data['cors_allow_methods']); - - // Clear cache of com_config component. - $this->cleanCache('_system'); - - $result = $app->triggerEvent('onApplicationBeforeSave', array($config)); - - // Store the data. - if (in_array(false, $result, true)) - { - throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_UNKNOWN_BEFORE_SAVING')); - } - - // Write the configuration file. - $result = $this->writeConfigFile($config); - - // Trigger the after save event. - $app->triggerEvent('onApplicationAfterSave', array($config)); - - return $result; - } - - /** - * Method to unset the root_user value from configuration data. - * - * This method will load the global configuration data straight from - * JConfig and remove the root_user value for security, then save the configuration. - * - * @return boolean True on success, false on failure. - * - * @since 1.6 - */ - public function removeroot() - { - $app = Factory::getApplication(); - - // Get the previous configuration. - $prev = new \JConfig; - $prev = ArrayHelper::fromObject($prev); - - // Create the new configuration object, and unset the root_user property - unset($prev['root_user']); - $config = new Registry($prev); - - $result = $app->triggerEvent('onApplicationBeforeSave', array($config)); - - // Store the data. - if (in_array(false, $result, true)) - { - throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_UNKNOWN_BEFORE_SAVING')); - } - - // Write the configuration file. - $result = $this->writeConfigFile($config); - - // Trigger the after save event. - $app->triggerEvent('onApplicationAfterSave', array($config)); - - return $result; - } - - /** - * Method to write the configuration to a file. - * - * @param Registry $config A Registry object containing all global config data. - * - * @return boolean True on success, false on failure. - * - * @since 2.5.4 - * @throws \RuntimeException - */ - private function writeConfigFile(Registry $config) - { - // Set the configuration file path. - $file = JPATH_CONFIGURATION . '/configuration.php'; - - $app = Factory::getApplication(); - - // Attempt to make the file writeable. - if (Path::isOwner($file) && !Path::setPermissions($file, '0644')) - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTWRITABLE'), 'notice'); - } - - // Attempt to write the configuration file as a PHP class named JConfig. - $configuration = $config->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); - - if (!File::write($file, $configuration)) - { - throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_WRITE_FAILED')); - } - - // Attempt to make the file unwriteable. - if (Path::isOwner($file) && !Path::setPermissions($file, '0444')) - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTUNWRITABLE'), 'notice'); - } - - return true; - } - - /** - * Method to store the permission values in the asset table. - * - * This method will get an array with permission key value pairs and transform it - * into json and update the asset table in the database. - * - * @param string $permission Need an array with Permissions (component, rule, value and title) - * - * @return array|bool A list of result data or false on failure. - * - * @since 3.5 - */ - public function storePermissions($permission = null) - { - $app = Factory::getApplication(); - $user = Factory::getUser(); - - if (is_null($permission)) - { - // Get data from input. - $permission = array( - 'component' => $app->input->Json->get('comp'), - 'action' => $app->input->Json->get('action'), - 'rule' => $app->input->Json->get('rule'), - 'value' => $app->input->Json->get('value'), - 'title' => $app->input->Json->get('title', '', 'RAW') - ); - } - - // We are creating a new item so we don't have an item id so don't allow. - if (substr($permission['component'], -6) === '.false') - { - $app->enqueueMessage(Text::_('JLIB_RULES_SAVE_BEFORE_CHANGE_PERMISSIONS'), 'error'); - - return false; - } - - // Check if the user is authorized to do this. - if (!$user->authorise('core.admin', $permission['component'])) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - - return false; - } - - $permission['component'] = empty($permission['component']) ? 'root.1' : $permission['component']; - - // Current view is global config? - $isGlobalConfig = $permission['component'] === 'root.1'; - - // Check if changed group has Super User permissions. - $isSuperUserGroupBefore = Access::checkGroup($permission['rule'], 'core.admin'); - - // Check if current user belongs to changed group. - $currentUserBelongsToGroup = in_array((int) $permission['rule'], $user->groups) ? true : false; - - // Get current user groups tree. - $currentUserGroupsTree = Access::getGroupsByUser($user->id, true); - - // Check if current user belongs to changed group. - $currentUserSuperUser = $user->authorise('core.admin'); - - // If user is not Super User cannot change the permissions of a group it belongs to. - if (!$currentUserSuperUser && $currentUserBelongsToGroup) - { - $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_OWN_GROUPS'), 'error'); - - return false; - } - - // If user is not Super User cannot change the permissions of a group it belongs to. - if (!$currentUserSuperUser && in_array((int) $permission['rule'], $currentUserGroupsTree)) - { - $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_OWN_PARENT_GROUPS'), 'error'); - - return false; - } - - // If user is not Super User cannot change the permissions of a Super User Group. - if (!$currentUserSuperUser && $isSuperUserGroupBefore && !$currentUserBelongsToGroup) - { - $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_SUPER_USER'), 'error'); - - return false; - } - - // If user is not Super User cannot change the Super User permissions in any group it belongs to. - if ($isSuperUserGroupBefore && $currentUserBelongsToGroup && $permission['action'] === 'core.admin') - { - $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_DEMOTE_SELF'), 'error'); - - return false; - } - - try - { - /** @var Asset $asset */ - $asset = Table::getInstance('asset'); - $result = $asset->loadByName($permission['component']); - - if ($result === false) - { - $data = array($permission['action'] => array($permission['rule'] => $permission['value'])); - - $rules = new Rules($data); - $asset->rules = (string) $rules; - $asset->name = (string) $permission['component']; - $asset->title = (string) $permission['title']; - - // Get the parent asset id so we have a correct tree. - /** @var Asset $parentAsset */ - $parentAsset = Table::getInstance('Asset'); - - if (strpos($asset->name, '.') !== false) - { - $assetParts = explode('.', $asset->name); - $parentAsset->loadByName($assetParts[0]); - $parentAssetId = $parentAsset->id; - } - else - { - $parentAssetId = $parentAsset->getRootId(); - } - - /** - * @todo: incorrect ACL stored - * When changing a permission of an item that doesn't have a row in the asset table the row a new row is created. - * This works fine for item <-> component <-> global config scenario and component <-> global config scenario. - * But doesn't work properly for item <-> section(s) <-> component <-> global config scenario, - * because a wrong parent asset id (the component) is stored. - * Happens when there is no row in the asset table (ex: deleted or not created on update). - */ - - $asset->setLocation($parentAssetId, 'last-child'); - } - else - { - // Decode the rule settings. - $temp = json_decode($asset->rules, true); - - // Check if a new value is to be set. - if (isset($permission['value'])) - { - // Check if we already have an action entry. - if (!isset($temp[$permission['action']])) - { - $temp[$permission['action']] = array(); - } - - // Check if we already have a rule entry. - if (!isset($temp[$permission['action']][$permission['rule']])) - { - $temp[$permission['action']][$permission['rule']] = array(); - } - - // Set the new permission. - $temp[$permission['action']][$permission['rule']] = (int) $permission['value']; - - // Check if we have an inherited setting. - if ($permission['value'] === '') - { - unset($temp[$permission['action']][$permission['rule']]); - } - - // Check if we have any rules. - if (!$temp[$permission['action']]) - { - unset($temp[$permission['action']]); - } - } - else - { - // There is no value so remove the action as it's not needed. - unset($temp[$permission['action']]); - } - - $asset->rules = json_encode($temp, JSON_FORCE_OBJECT); - } - - if (!$asset->check() || !$asset->store()) - { - $app->enqueueMessage(Text::_('JLIB_UNKNOWN'), 'error'); - - return false; - } - } - catch (\Exception $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // All checks done. - $result = array( - 'text' => '', - 'class' => '', - 'result' => true, - ); - - // Show the current effective calculated permission considering current group, path and cascade. - - try - { - // The database instance - $db = $this->getDatabase(); - - // Get the asset id by the name of the component. - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__assets')) - ->where($db->quoteName('name') . ' = :component') - ->bind(':component', $permission['component']); - - $db->setQuery($query); - - $assetId = (int) $db->loadResult(); - - // Fetch the parent asset id. - $parentAssetId = null; - - /** - * @todo: incorrect info - * When creating a new item (not saving) it uses the calculated permissions from the component (item <-> component <-> global config). - * But if we have a section too (item <-> section(s) <-> component <-> global config) this is not correct. - * Also, currently it uses the component permission, but should use the calculated permissions for a child of the component/section. - */ - - // If not in global config we need the parent_id asset to calculate permissions. - if (!$isGlobalConfig) - { - // In this case we need to get the component rules too. - $query->clear() - ->select($db->quoteName('parent_id')) - ->from($db->quoteName('#__assets')) - ->where($db->quoteName('id') . ' = :assetid') - ->bind(':assetid', $assetId, ParameterType::INTEGER); - - $db->setQuery($query); - - $parentAssetId = (int) $db->loadResult(); - } - - // Get the group parent id of the current group. - $rule = (int) $permission['rule']; - $query->clear() - ->select($db->quoteName('parent_id')) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('id') . ' = :rule') - ->bind(':rule', $rule, ParameterType::INTEGER); - - $db->setQuery($query); - - $parentGroupId = (int) $db->loadResult(); - - // Count the number of child groups of the current group. - $query->clear() - ->select('COUNT(' . $db->quoteName('id') . ')') - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('parent_id') . ' = :rule') - ->bind(':rule', $rule, ParameterType::INTEGER); - - $db->setQuery($query); - - $totalChildGroups = (int) $db->loadResult(); - } - catch (\Exception $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // Clear access statistics. - Access::clearStatics(); - - // After current group permission is changed we need to check again if the group has Super User permissions. - $isSuperUserGroupAfter = Access::checkGroup($permission['rule'], 'core.admin'); - - // Get the rule for just this asset (non-recursive) and get the actual setting for the action for this group. - $assetRule = Access::getAssetRules($assetId, false, false)->allow($permission['action'], $permission['rule']); - - // Get the group, group parent id, and group global config recursive calculated permission for the chosen action. - $inheritedGroupRule = Access::checkGroup($permission['rule'], $permission['action'], $assetId); - - if (!empty($parentAssetId)) - { - $inheritedGroupParentAssetRule = Access::checkGroup($permission['rule'], $permission['action'], $parentAssetId); - } - else - { - $inheritedGroupParentAssetRule = null; - } - - $inheritedParentGroupRule = !empty($parentGroupId) ? Access::checkGroup($parentGroupId, $permission['action'], $assetId) : null; - - // Current group is a Super User group, so calculated setting is "Allowed (Super User)". - if ($isSuperUserGroupAfter) - { - $result['class'] = 'badge bg-success'; - $result['text'] = '' . Text::_('JLIB_RULES_ALLOWED_ADMIN'); - } - // Not super user. - else - { - // First get the real recursive calculated setting and add (Inherited) to it. - - // If recursive calculated setting is "Denied" or null. Calculated permission is "Not Allowed (Inherited)". - if ($inheritedGroupRule === null || $inheritedGroupRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_INHERITED'); - } - // If recursive calculated setting is "Allowed". Calculated permission is "Allowed (Inherited)". - else - { - $result['class'] = 'badge bg-success'; - $result['text'] = Text::_('JLIB_RULES_ALLOWED_INHERITED'); - } - - // Second part: Overwrite the calculated permissions labels if there is an explicit permission in the current group. - - /** - * @todo: incorrect info - * If a component has a permission that doesn't exists in global config (ex: frontend editing in com_modules) by default - * we get "Not Allowed (Inherited)" when we should get "Not Allowed (Default)". - */ - - // If there is an explicit permission "Not Allowed". Calculated permission is "Not Allowed". - if ($assetRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED'); - } - // If there is an explicit permission is "Allowed". Calculated permission is "Allowed". - elseif ($assetRule === true) - { - $result['class'] = 'badge bg-success'; - $result['text'] = Text::_('JLIB_RULES_ALLOWED'); - } - - // Third part: Overwrite the calculated permissions labels for special cases. - - // Global configuration with "Not Set" permission. Calculated permission is "Not Allowed (Default)". - if (empty($parentGroupId) && $isGlobalConfig === true && $assetRule === null) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_DEFAULT'); - } - - /** - * Component/Item with explicit "Denied" permission at parent Asset (Category, Component or Global config) configuration. - * Or some parent group has an explicit "Denied". - * Calculated permission is "Not Allowed (Locked)". - */ - elseif ($inheritedGroupParentAssetRule === false || $inheritedParentGroupRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = '' . Text::_('JLIB_RULES_NOT_ALLOWED_LOCKED'); - } - } - - // If removed or added super user from group, we need to refresh the page to recalculate all settings. - if ($isSuperUserGroupBefore != $isSuperUserGroupAfter) - { - $app->enqueueMessage(Text::_('JLIB_RULES_NOTICE_RECALCULATE_GROUP_PERMISSIONS'), 'notice'); - } - - // If this group has child groups, we need to refresh the page to recalculate the child settings. - if ($totalChildGroups > 0) - { - $app->enqueueMessage(Text::_('JLIB_RULES_NOTICE_RECALCULATE_GROUP_CHILDS_PERMISSIONS'), 'notice'); - } - - return $result; - } - - /** - * Method to send a test mail which is called via an AJAX request - * - * @return boolean - * - * @since 3.5 - */ - public function sendTestMail() - { - // Set the new values to test with the current settings - $app = Factory::getApplication(); - $user = Factory::getUser(); - $input = $app->input->json; - $smtppass = $input->get('smtppass', null, 'RAW'); - - $app->set('smtpauth', $input->get('smtpauth')); - $app->set('smtpuser', $input->get('smtpuser', '', 'STRING')); - $app->set('smtphost', $input->get('smtphost')); - $app->set('smtpsecure', $input->get('smtpsecure')); - $app->set('smtpport', $input->get('smtpport')); - $app->set('mailfrom', $input->get('mailfrom', '', 'STRING')); - $app->set('fromname', $input->get('fromname', '', 'STRING')); - $app->set('mailer', $input->get('mailer')); - $app->set('mailonline', $input->get('mailonline')); - - // Use smtppass only if it was submitted - if ($smtppass !== null) - { - $app->set('smtppass', $smtppass); - } - - $mail = Factory::getMailer(); - - // Prepare email and try to send it - $mailer = new MailTemplate('com_config.test_mail', $user->getParam('language', $app->get('language')), $mail); - $mailer->addTemplateData( - array( - 'sitename' => $app->get('sitename'), - 'method' => Text::_('COM_CONFIG_SENDMAIL_METHOD_' . strtoupper($mail->Mailer)) - ) - ); - $mailer->addRecipient($app->get('mailfrom'), $app->get('fromname')); - - try - { - $mailSent = $mailer->send(); - } - catch (MailDisabledException | phpMailerException $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - if ($mailSent === true) - { - $methodName = Text::_('COM_CONFIG_SENDMAIL_METHOD_' . strtoupper($mail->Mailer)); - - // If JMail send the mail using PHP Mail as fallback. - if ($mail->Mailer !== $app->get('mailer')) - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_SENDMAIL_SUCCESS_FALLBACK', $app->get('mailfrom'), $methodName), 'warning'); - } - else - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_SENDMAIL_SUCCESS', $app->get('mailfrom'), $methodName), 'message'); - } - - return true; - } - - $app->enqueueMessage(Text::_('COM_CONFIG_SENDMAIL_ERROR'), 'error'); - - return false; - } + /** + * Array of protected password fields from the configuration.php + * + * @var array + * @since 3.9.23 + */ + private $protectedConfigurationFields = array('password', 'secret', 'smtppass', 'redis_server_auth', 'session_redis_server_auth'); + + /** + * Method to get a form object. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return mixed A JForm object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_config.application', 'application', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the configuration data. + * + * This method will load the global configuration data straight from + * JConfig. If configuration data has been saved in the session, that + * data will be merged into the original data, overwriting it. + * + * @return array An array containing all global config data. + * + * @since 1.6 + */ + public function getData() + { + // Get the config data. + $config = new \JConfig(); + $data = ArrayHelper::fromObject($config); + + // Get the correct driver at runtime + $data['dbtype'] = $this->getDatabase()->getName(); + + // Prime the asset_id for the rules. + $data['asset_id'] = 1; + + // Get the text filter data + $params = ComponentHelper::getParams('com_config'); + $data['filters'] = ArrayHelper::fromObject($params->get('filters')); + + // If no filter data found, get from com_content (update of 1.6/1.7 site) + if (empty($data['filters'])) { + $contentParams = ComponentHelper::getParams('com_content'); + $data['filters'] = ArrayHelper::fromObject($contentParams->get('filters')); + } + + // Check for data in the session. + $temp = Factory::getApplication()->getUserState('com_config.config.global.data'); + + // Merge in the session data. + if (!empty($temp)) { + // $temp can sometimes be an object, and we need it to be an array + if (is_object($temp)) { + $temp = ArrayHelper::fromObject($temp); + } + + $data = array_merge($temp, $data); + } + + // Correct error_reporting value, since we removed "development", the "maximum" should be set instead + // @TODO: This can be removed in 5.0 + if (!empty($data['error_reporting']) && $data['error_reporting'] === 'development') { + $data['error_reporting'] = 'maximum'; + } + + return $data; + } + + /** + * Method to validate the db connection properties. + * + * @param array $data An array containing all global config data. + * + * @return array|boolean Array with the validated global config data or boolean false on a validation failure. + * + * @since 4.0.0 + */ + public function validateDbConnection($data) + { + // Validate database connection encryption options + if ((int) $data['dbencryption'] === 0) { + // Reset unused options + if (!empty($data['dbsslkey'])) { + $data['dbsslkey'] = ''; + } + + if (!empty($data['dbsslcert'])) { + $data['dbsslcert'] = ''; + } + + if ((bool) $data['dbsslverifyservercert'] === true) { + $data['dbsslverifyservercert'] = false; + } + + if (!empty($data['dbsslca'])) { + $data['dbsslca'] = ''; + } + + if (!empty($data['dbsslcipher'])) { + $data['dbsslcipher'] = ''; + } + } else { + // Check localhost + if (strtolower($data['host']) === 'localhost') { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_LOCALHOST'), 'error'); + + return false; + } + + // Check CA file and folder depending on database type if server certificate verification + if ((bool) $data['dbsslverifyservercert'] === true) { + if (empty($data['dbsslca'])) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CA_LABEL') + ), + 'error' + ); + + return false; + } + + if (!File::exists(Path::clean($data['dbsslca']))) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CA_LABEL') + ), + 'error' + ); + + return false; + } + } else { + // Reset unused option + if (!empty($data['dbsslca'])) { + $data['dbsslca'] = ''; + } + } + + // Check key and certificate if two-way encryption + if ((int) $data['dbencryption'] === 2) { + if (empty($data['dbsslkey'])) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_KEY_LABEL') + ), + 'error' + ); + + return false; + } + + if (!File::exists(Path::clean($data['dbsslkey']))) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_KEY_LABEL') + ), + 'error' + ); + + return false; + } + + if (empty($data['dbsslcert'])) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CERT_LABEL') + ), + 'error' + ); + + return false; + } + + if (!File::exists(Path::clean($data['dbsslcert']))) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CERT_LABEL') + ), + 'error' + ); + + return false; + } + } else { + // Reset unused options + if (!empty($data['dbsslkey'])) { + $data['dbsslkey'] = ''; + } + + if (!empty($data['dbsslcert'])) { + $data['dbsslcert'] = ''; + } + } + } + + return $data; + } + + /** + * Method to save the configuration data. + * + * @param array $data An array containing all global config data. + * + * @return boolean True on success, false on failure. + * + * @since 1.6 + */ + public function save($data) + { + $app = Factory::getApplication(); + + // Try to load the values from the configuration file + foreach ($this->protectedConfigurationFields as $fieldKey) { + if (!isset($data[$fieldKey])) { + $data[$fieldKey] = $app->get($fieldKey, ''); + } + } + + // Check that we aren't setting wrong database configuration + $options = array( + 'driver' => $data['dbtype'], + 'host' => $data['host'], + 'user' => $data['user'], + 'password' => $data['password'], + 'database' => $data['db'], + 'prefix' => $data['dbprefix'], + ); + + if ((int) $data['dbencryption'] !== 0) { + $options['ssl'] = [ + 'enable' => true, + 'verify_server_cert' => (bool) $data['dbsslverifyservercert'], + ]; + + foreach (['cipher', 'ca', 'key', 'cert'] as $value) { + $confVal = trim($data['dbssl' . $value]); + + if ($confVal !== '') { + $options['ssl'][$value] = $confVal; + } + } + } + + try { + $revisedDbo = DatabaseDriver::getInstance($options); + $revisedDbo->getVersion(); + } catch (\Exception $e) { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_DATABASE_NOT_AVAILABLE', $e->getCode(), $e->getMessage()), 'error'); + + return false; + } + + if ((int) $data['dbencryption'] !== 0 && empty($revisedDbo->getConnectionEncryption())) { + if ($revisedDbo->isConnectionEncryptionSupported()) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_CONN_NOT_ENCRYPT'), 'error'); + } else { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_SRV_NOT_SUPPORTS'), 'error'); + } + + return false; + } + + // Check if we can set the Force SSL option + if ((int) $data['force_ssl'] !== 0 && (int) $data['force_ssl'] !== (int) $app->get('force_ssl', '0')) { + try { + // Make an HTTPS request to check if the site is available in HTTPS. + $host = Uri::getInstance()->getHost(); + $options = new Registry(); + $options->set('userAgent', 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0'); + + // Do not check for valid server certificate here, leave this to the user, moreover disable using a proxy if any is configured. + $options->set( + 'transport.curl', + array( + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_PROXY => null, + CURLOPT_PROXYUSERPWD => null, + ) + ); + $response = HttpFactory::getHttp($options)->get('https://' . $host . Uri::root(true) . '/', array('Host' => $host), 10); + + // If available in HTTPS check also the status code. + if (!in_array($response->code, array(200, 503, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 401), true)) { + throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_SSL_NOT_AVAILABLE_HTTP_CODE')); + } + } catch (\RuntimeException $e) { + $data['force_ssl'] = 0; + + // Also update the user state + $app->setUserState('com_config.config.global.data.force_ssl', 0); + + // Inform the user + $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_SSL_NOT_AVAILABLE', $e->getMessage()), 'warning'); + } + } + + // Save the rules + if (isset($data['rules'])) { + $rules = new Rules($data['rules']); + + // Check that we aren't removing our Super User permission + // Need to get groups from database, since they might have changed + $myGroups = Access::getGroupsByUser(Factory::getUser()->get('id')); + $myRules = $rules->getData(); + $hasSuperAdmin = $myRules['core.admin']->allow($myGroups); + + if (!$hasSuperAdmin) { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_REMOVING_SUPER_ADMIN'), 'error'); + + return false; + } + + $asset = Table::getInstance('asset'); + + if ($asset->loadByName('root.1')) { + $asset->rules = (string) $rules; + + if (!$asset->check() || !$asset->store()) { + $app->enqueueMessage($asset->getError(), 'error'); + + return false; + } + } else { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_ROOT_ASSET_NOT_FOUND'), 'error'); + + return false; + } + + unset($data['rules']); + } + + // Save the text filters + if (isset($data['filters'])) { + $registry = new Registry(array('filters' => $data['filters'])); + + $extension = Table::getInstance('extension'); + + // Get extension_id + $extensionId = $extension->find(array('name' => 'com_config')); + + if ($extension->load((int) $extensionId)) { + $extension->params = (string) $registry; + + if (!$extension->check() || !$extension->store()) { + $app->enqueueMessage($extension->getError(), 'error'); + + return false; + } + } else { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIG_EXTENSION_NOT_FOUND'), 'error'); + + return false; + } + + unset($data['filters']); + } + + // Get the previous configuration. + $prev = new \JConfig(); + $prev = ArrayHelper::fromObject($prev); + + // Merge the new data in. We do this to preserve values that were not in the form. + $data = array_merge($prev, $data); + + /* + * Perform miscellaneous options based on configuration settings/changes. + */ + + // Escape the offline message if present. + if (isset($data['offline_message'])) { + $data['offline_message'] = OutputFilter::ampReplace($data['offline_message']); + } + + // Purge the database session table if we are changing to the database handler. + if ($prev['session_handler'] != 'database' && $data['session_handler'] == 'database') { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__session')) + ->where($db->quoteName('time') . ' < ' . (time() - 1)); + $db->setQuery($query); + $db->execute(); + } + + // Purge the database session table if we are disabling session metadata + if ($prev['session_metadata'] == 1 && $data['session_metadata'] == 0) { + try { + // If we are are using the session handler, purge the extra columns, otherwise truncate the whole session table + if ($data['session_handler'] === 'database') { + $revisedDbo->setQuery( + $revisedDbo->getQuery(true) + ->update('#__session') + ->set( + [ + $revisedDbo->quoteName('client_id') . ' = 0', + $revisedDbo->quoteName('guest') . ' = NULL', + $revisedDbo->quoteName('userid') . ' = NULL', + $revisedDbo->quoteName('username') . ' = NULL', + ] + ) + )->execute(); + } else { + $revisedDbo->truncateTable('#__session'); + } + } catch (\RuntimeException $e) { + /* + * The database API logs errors on failures so we don't need to add any error handling mechanisms here. + * Also, this data won't be added or checked anymore once the configuration is saved, so it'll purge itself + * through normal garbage collection anyway or if not using the database handler someone can purge the + * table on their own. Either way, carry on Soldier! + */ + } + } + + // Ensure custom session file path exists or try to create it if changed + if (!empty($data['session_filesystem_path'])) { + $currentPath = $prev['session_filesystem_path'] ?? null; + + if ($currentPath) { + $currentPath = Path::clean($currentPath); + } + + $data['session_filesystem_path'] = Path::clean($data['session_filesystem_path']); + + if ($currentPath !== $data['session_filesystem_path']) { + if (!Folder::exists($data['session_filesystem_path']) && !Folder::create($data['session_filesystem_path'])) { + try { + Log::add( + Text::sprintf( + 'COM_CONFIG_ERROR_CUSTOM_SESSION_FILESYSTEM_PATH_NOTWRITABLE_USING_DEFAULT', + $data['session_filesystem_path'] + ), + Log::WARNING, + 'jerror' + ); + } catch (\RuntimeException $logException) { + $app->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_CUSTOM_SESSION_FILESYSTEM_PATH_NOTWRITABLE_USING_DEFAULT', + $data['session_filesystem_path'] + ), + 'warning' + ); + } + + $data['session_filesystem_path'] = $currentPath; + } + } + } + + // Set the shared session configuration + if (isset($data['shared_session'])) { + $currentShared = $prev['shared_session'] ?? '0'; + + // Has the user enabled shared sessions? + if ($data['shared_session'] == 1 && $currentShared == 0) { + // Generate a random shared session name + $data['session_name'] = UserHelper::genRandomPassword(16); + } + + // Has the user disabled shared sessions? + if ($data['shared_session'] == 0 && $currentShared == 1) { + // Remove the session name value + unset($data['session_name']); + } + } + + // Set the shared session configuration + if (isset($data['shared_session'])) { + $currentShared = $prev['shared_session'] ?? '0'; + + // Has the user enabled shared sessions? + if ($data['shared_session'] == 1 && $currentShared == 0) { + // Generate a random shared session name + $data['session_name'] = UserHelper::genRandomPassword(16); + } + + // Has the user disabled shared sessions? + if ($data['shared_session'] == 0 && $currentShared == 1) { + // Remove the session name value + unset($data['session_name']); + } + } + + if (empty($data['cache_handler'])) { + $data['caching'] = 0; + } + + /* + * Look for a custom cache_path + * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default + */ + if (!empty($data['cache_path'])) { + $path = $data['cache_path']; + } elseif (!empty($prev['cache_path'])) { + $path = $prev['cache_path']; + } else { + $path = JPATH_CACHE; + } + + // Give a warning if the cache-folder can not be opened + if ($data['caching'] > 0 && $data['cache_handler'] == 'file' && @opendir($path) == false) { + $error = true; + + // If a custom path is in use, try using the system default instead of disabling cache + if ($path !== JPATH_CACHE && @opendir(JPATH_CACHE) != false) { + try { + Log::add( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_CACHE_PATH_NOTWRITABLE_USING_DEFAULT', $path, JPATH_CACHE), + Log::WARNING, + 'jerror' + ); + } catch (\RuntimeException $logException) { + $app->enqueueMessage( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_CACHE_PATH_NOTWRITABLE_USING_DEFAULT', $path, JPATH_CACHE), + 'warning' + ); + } + + $path = JPATH_CACHE; + $error = false; + + $data['cache_path'] = ''; + } + + if ($error) { + try { + Log::add(Text::sprintf('COM_CONFIG_ERROR_CACHE_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_CACHE_PATH_NOTWRITABLE', $path), 'warning'); + } + + $data['caching'] = 0; + } + } + + // Did the user remove their custom cache path? Don't save the variable to the config + if (empty($data['cache_path'])) { + unset($data['cache_path']); + } + + // Clean the cache if disabled but previously enabled or changing cache handlers; these operations use the `$prev` data already in memory + if ((!$data['caching'] && $prev['caching']) || $data['cache_handler'] !== $prev['cache_handler']) { + try { + Factory::getCache()->clean(); + } catch (CacheConnectingException $exception) { + try { + Log::add(Text::_('COM_CONFIG_ERROR_CACHE_CONNECTION_FAILED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $logException) { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CACHE_CONNECTION_FAILED'), 'warning'); + } + } catch (UnsupportedCacheException $exception) { + try { + Log::add(Text::_('COM_CONFIG_ERROR_CACHE_DRIVER_UNSUPPORTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $logException) { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CACHE_DRIVER_UNSUPPORTED'), 'warning'); + } + } + } + + /* + * Look for a custom tmp_path + * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default + */ + $defaultTmpPath = JPATH_ROOT . '/tmp'; + + if (!empty($data['tmp_path'])) { + $path = $data['tmp_path']; + } elseif (!empty($prev['tmp_path'])) { + $path = $prev['tmp_path']; + } else { + $path = $defaultTmpPath; + } + + $path = Path::clean($path); + + // Give a warning if the tmp-folder is not valid or not writable + if (!is_dir($path) || !is_writable($path)) { + $error = true; + + // If a custom path is in use, try using the system default tmp path + if ($path !== $defaultTmpPath && is_dir($defaultTmpPath) && is_writable($defaultTmpPath)) { + try { + Log::add( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_TEMP_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultTmpPath), + Log::WARNING, + 'jerror' + ); + } catch (\RuntimeException $logException) { + $app->enqueueMessage( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_TEMP_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultTmpPath), + 'warning' + ); + } + + $error = false; + + $data['tmp_path'] = $defaultTmpPath; + } + + if ($error) { + try { + Log::add(Text::sprintf('COM_CONFIG_ERROR_TMP_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_TMP_PATH_NOTWRITABLE', $path), 'warning'); + } + } + } + + /* + * Look for a custom log_path + * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default + */ + $defaultLogPath = JPATH_ADMINISTRATOR . '/logs'; + + if (!empty($data['log_path'])) { + $path = $data['log_path']; + } elseif (!empty($prev['log_path'])) { + $path = $prev['log_path']; + } else { + $path = $defaultLogPath; + } + + $path = Path::clean($path); + + // Give a warning if the log-folder is not valid or not writable + if (!is_dir($path) || !is_writable($path)) { + $error = true; + + // If a custom path is in use, try using the system default log path + if ($path !== $defaultLogPath && is_dir($defaultLogPath) && is_writable($defaultLogPath)) { + try { + Log::add( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_LOG_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultLogPath), + Log::WARNING, + 'jerror' + ); + } catch (\RuntimeException $logException) { + $app->enqueueMessage( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_LOG_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultLogPath), + 'warning' + ); + } + + $error = false; + $data['log_path'] = $defaultLogPath; + } + + if ($error) { + try { + Log::add(Text::sprintf('COM_CONFIG_ERROR_LOG_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_LOG_PATH_NOTWRITABLE', $path), 'warning'); + } + } + } + + // Create the new configuration object. + $config = new Registry($data); + + // Overwrite webservices cors settings + $app->set('cors', $data['cors']); + $app->set('cors_allow_origin', $data['cors_allow_origin']); + $app->set('cors_allow_headers', $data['cors_allow_headers']); + $app->set('cors_allow_methods', $data['cors_allow_methods']); + + // Clear cache of com_config component. + $this->cleanCache('_system'); + + $result = $app->triggerEvent('onApplicationBeforeSave', array($config)); + + // Store the data. + if (in_array(false, $result, true)) { + throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_UNKNOWN_BEFORE_SAVING')); + } + + // Write the configuration file. + $result = $this->writeConfigFile($config); + + // Trigger the after save event. + $app->triggerEvent('onApplicationAfterSave', array($config)); + + return $result; + } + + /** + * Method to unset the root_user value from configuration data. + * + * This method will load the global configuration data straight from + * JConfig and remove the root_user value for security, then save the configuration. + * + * @return boolean True on success, false on failure. + * + * @since 1.6 + */ + public function removeroot() + { + $app = Factory::getApplication(); + + // Get the previous configuration. + $prev = new \JConfig(); + $prev = ArrayHelper::fromObject($prev); + + // Create the new configuration object, and unset the root_user property + unset($prev['root_user']); + $config = new Registry($prev); + + $result = $app->triggerEvent('onApplicationBeforeSave', array($config)); + + // Store the data. + if (in_array(false, $result, true)) { + throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_UNKNOWN_BEFORE_SAVING')); + } + + // Write the configuration file. + $result = $this->writeConfigFile($config); + + // Trigger the after save event. + $app->triggerEvent('onApplicationAfterSave', array($config)); + + return $result; + } + + /** + * Method to write the configuration to a file. + * + * @param Registry $config A Registry object containing all global config data. + * + * @return boolean True on success, false on failure. + * + * @since 2.5.4 + * @throws \RuntimeException + */ + private function writeConfigFile(Registry $config) + { + // Set the configuration file path. + $file = JPATH_CONFIGURATION . '/configuration.php'; + + $app = Factory::getApplication(); + + // Attempt to make the file writeable. + if (Path::isOwner($file) && !Path::setPermissions($file, '0644')) { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTWRITABLE'), 'notice'); + } + + // Attempt to write the configuration file as a PHP class named JConfig. + $configuration = $config->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); + + if (!File::write($file, $configuration)) { + throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_WRITE_FAILED')); + } + + // Attempt to make the file unwriteable. + if (Path::isOwner($file) && !Path::setPermissions($file, '0444')) { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTUNWRITABLE'), 'notice'); + } + + return true; + } + + /** + * Method to store the permission values in the asset table. + * + * This method will get an array with permission key value pairs and transform it + * into json and update the asset table in the database. + * + * @param string $permission Need an array with Permissions (component, rule, value and title) + * + * @return array|bool A list of result data or false on failure. + * + * @since 3.5 + */ + public function storePermissions($permission = null) + { + $app = Factory::getApplication(); + $user = Factory::getUser(); + + if (is_null($permission)) { + // Get data from input. + $permission = array( + 'component' => $app->input->Json->get('comp'), + 'action' => $app->input->Json->get('action'), + 'rule' => $app->input->Json->get('rule'), + 'value' => $app->input->Json->get('value'), + 'title' => $app->input->Json->get('title', '', 'RAW') + ); + } + + // We are creating a new item so we don't have an item id so don't allow. + if (substr($permission['component'], -6) === '.false') { + $app->enqueueMessage(Text::_('JLIB_RULES_SAVE_BEFORE_CHANGE_PERMISSIONS'), 'error'); + + return false; + } + + // Check if the user is authorized to do this. + if (!$user->authorise('core.admin', $permission['component'])) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + + return false; + } + + $permission['component'] = empty($permission['component']) ? 'root.1' : $permission['component']; + + // Current view is global config? + $isGlobalConfig = $permission['component'] === 'root.1'; + + // Check if changed group has Super User permissions. + $isSuperUserGroupBefore = Access::checkGroup($permission['rule'], 'core.admin'); + + // Check if current user belongs to changed group. + $currentUserBelongsToGroup = in_array((int) $permission['rule'], $user->groups) ? true : false; + + // Get current user groups tree. + $currentUserGroupsTree = Access::getGroupsByUser($user->id, true); + + // Check if current user belongs to changed group. + $currentUserSuperUser = $user->authorise('core.admin'); + + // If user is not Super User cannot change the permissions of a group it belongs to. + if (!$currentUserSuperUser && $currentUserBelongsToGroup) { + $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_OWN_GROUPS'), 'error'); + + return false; + } + + // If user is not Super User cannot change the permissions of a group it belongs to. + if (!$currentUserSuperUser && in_array((int) $permission['rule'], $currentUserGroupsTree)) { + $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_OWN_PARENT_GROUPS'), 'error'); + + return false; + } + + // If user is not Super User cannot change the permissions of a Super User Group. + if (!$currentUserSuperUser && $isSuperUserGroupBefore && !$currentUserBelongsToGroup) { + $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_SUPER_USER'), 'error'); + + return false; + } + + // If user is not Super User cannot change the Super User permissions in any group it belongs to. + if ($isSuperUserGroupBefore && $currentUserBelongsToGroup && $permission['action'] === 'core.admin') { + $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_DEMOTE_SELF'), 'error'); + + return false; + } + + try { + /** @var Asset $asset */ + $asset = Table::getInstance('asset'); + $result = $asset->loadByName($permission['component']); + + if ($result === false) { + $data = array($permission['action'] => array($permission['rule'] => $permission['value'])); + + $rules = new Rules($data); + $asset->rules = (string) $rules; + $asset->name = (string) $permission['component']; + $asset->title = (string) $permission['title']; + + // Get the parent asset id so we have a correct tree. + /** @var Asset $parentAsset */ + $parentAsset = Table::getInstance('Asset'); + + if (strpos($asset->name, '.') !== false) { + $assetParts = explode('.', $asset->name); + $parentAsset->loadByName($assetParts[0]); + $parentAssetId = $parentAsset->id; + } else { + $parentAssetId = $parentAsset->getRootId(); + } + + /** + * @todo: incorrect ACL stored + * When changing a permission of an item that doesn't have a row in the asset table the row a new row is created. + * This works fine for item <-> component <-> global config scenario and component <-> global config scenario. + * But doesn't work properly for item <-> section(s) <-> component <-> global config scenario, + * because a wrong parent asset id (the component) is stored. + * Happens when there is no row in the asset table (ex: deleted or not created on update). + */ + + $asset->setLocation($parentAssetId, 'last-child'); + } else { + // Decode the rule settings. + $temp = json_decode($asset->rules, true); + + // Check if a new value is to be set. + if (isset($permission['value'])) { + // Check if we already have an action entry. + if (!isset($temp[$permission['action']])) { + $temp[$permission['action']] = array(); + } + + // Check if we already have a rule entry. + if (!isset($temp[$permission['action']][$permission['rule']])) { + $temp[$permission['action']][$permission['rule']] = array(); + } + + // Set the new permission. + $temp[$permission['action']][$permission['rule']] = (int) $permission['value']; + + // Check if we have an inherited setting. + if ($permission['value'] === '') { + unset($temp[$permission['action']][$permission['rule']]); + } + + // Check if we have any rules. + if (!$temp[$permission['action']]) { + unset($temp[$permission['action']]); + } + } else { + // There is no value so remove the action as it's not needed. + unset($temp[$permission['action']]); + } + + $asset->rules = json_encode($temp, JSON_FORCE_OBJECT); + } + + if (!$asset->check() || !$asset->store()) { + $app->enqueueMessage(Text::_('JLIB_UNKNOWN'), 'error'); + + return false; + } + } catch (\Exception $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // All checks done. + $result = array( + 'text' => '', + 'class' => '', + 'result' => true, + ); + + // Show the current effective calculated permission considering current group, path and cascade. + + try { + // The database instance + $db = $this->getDatabase(); + + // Get the asset id by the name of the component. + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('name') . ' = :component') + ->bind(':component', $permission['component']); + + $db->setQuery($query); + + $assetId = (int) $db->loadResult(); + + // Fetch the parent asset id. + $parentAssetId = null; + + /** + * @todo: incorrect info + * When creating a new item (not saving) it uses the calculated permissions from the component (item <-> component <-> global config). + * But if we have a section too (item <-> section(s) <-> component <-> global config) this is not correct. + * Also, currently it uses the component permission, but should use the calculated permissions for a child of the component/section. + */ + + // If not in global config we need the parent_id asset to calculate permissions. + if (!$isGlobalConfig) { + // In this case we need to get the component rules too. + $query->clear() + ->select($db->quoteName('parent_id')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('id') . ' = :assetid') + ->bind(':assetid', $assetId, ParameterType::INTEGER); + + $db->setQuery($query); + + $parentAssetId = (int) $db->loadResult(); + } + + // Get the group parent id of the current group. + $rule = (int) $permission['rule']; + $query->clear() + ->select($db->quoteName('parent_id')) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('id') . ' = :rule') + ->bind(':rule', $rule, ParameterType::INTEGER); + + $db->setQuery($query); + + $parentGroupId = (int) $db->loadResult(); + + // Count the number of child groups of the current group. + $query->clear() + ->select('COUNT(' . $db->quoteName('id') . ')') + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('parent_id') . ' = :rule') + ->bind(':rule', $rule, ParameterType::INTEGER); + + $db->setQuery($query); + + $totalChildGroups = (int) $db->loadResult(); + } catch (\Exception $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // Clear access statistics. + Access::clearStatics(); + + // After current group permission is changed we need to check again if the group has Super User permissions. + $isSuperUserGroupAfter = Access::checkGroup($permission['rule'], 'core.admin'); + + // Get the rule for just this asset (non-recursive) and get the actual setting for the action for this group. + $assetRule = Access::getAssetRules($assetId, false, false)->allow($permission['action'], $permission['rule']); + + // Get the group, group parent id, and group global config recursive calculated permission for the chosen action. + $inheritedGroupRule = Access::checkGroup($permission['rule'], $permission['action'], $assetId); + + if (!empty($parentAssetId)) { + $inheritedGroupParentAssetRule = Access::checkGroup($permission['rule'], $permission['action'], $parentAssetId); + } else { + $inheritedGroupParentAssetRule = null; + } + + $inheritedParentGroupRule = !empty($parentGroupId) ? Access::checkGroup($parentGroupId, $permission['action'], $assetId) : null; + + // Current group is a Super User group, so calculated setting is "Allowed (Super User)". + if ($isSuperUserGroupAfter) { + $result['class'] = 'badge bg-success'; + $result['text'] = '' . Text::_('JLIB_RULES_ALLOWED_ADMIN'); + } + // Not super user. + else { + // First get the real recursive calculated setting and add (Inherited) to it. + + // If recursive calculated setting is "Denied" or null. Calculated permission is "Not Allowed (Inherited)". + if ($inheritedGroupRule === null || $inheritedGroupRule === false) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_INHERITED'); + } + // If recursive calculated setting is "Allowed". Calculated permission is "Allowed (Inherited)". + else { + $result['class'] = 'badge bg-success'; + $result['text'] = Text::_('JLIB_RULES_ALLOWED_INHERITED'); + } + + // Second part: Overwrite the calculated permissions labels if there is an explicit permission in the current group. + + /** + * @todo: incorrect info + * If a component has a permission that doesn't exists in global config (ex: frontend editing in com_modules) by default + * we get "Not Allowed (Inherited)" when we should get "Not Allowed (Default)". + */ + + // If there is an explicit permission "Not Allowed". Calculated permission is "Not Allowed". + if ($assetRule === false) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED'); + } + // If there is an explicit permission is "Allowed". Calculated permission is "Allowed". + elseif ($assetRule === true) { + $result['class'] = 'badge bg-success'; + $result['text'] = Text::_('JLIB_RULES_ALLOWED'); + } + + // Third part: Overwrite the calculated permissions labels for special cases. + + // Global configuration with "Not Set" permission. Calculated permission is "Not Allowed (Default)". + if (empty($parentGroupId) && $isGlobalConfig === true && $assetRule === null) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_DEFAULT'); + } + + /** + * Component/Item with explicit "Denied" permission at parent Asset (Category, Component or Global config) configuration. + * Or some parent group has an explicit "Denied". + * Calculated permission is "Not Allowed (Locked)". + */ + elseif ($inheritedGroupParentAssetRule === false || $inheritedParentGroupRule === false) { + $result['class'] = 'badge bg-danger'; + $result['text'] = '' . Text::_('JLIB_RULES_NOT_ALLOWED_LOCKED'); + } + } + + // If removed or added super user from group, we need to refresh the page to recalculate all settings. + if ($isSuperUserGroupBefore != $isSuperUserGroupAfter) { + $app->enqueueMessage(Text::_('JLIB_RULES_NOTICE_RECALCULATE_GROUP_PERMISSIONS'), 'notice'); + } + + // If this group has child groups, we need to refresh the page to recalculate the child settings. + if ($totalChildGroups > 0) { + $app->enqueueMessage(Text::_('JLIB_RULES_NOTICE_RECALCULATE_GROUP_CHILDS_PERMISSIONS'), 'notice'); + } + + return $result; + } + + /** + * Method to send a test mail which is called via an AJAX request + * + * @return boolean + * + * @since 3.5 + */ + public function sendTestMail() + { + // Set the new values to test with the current settings + $app = Factory::getApplication(); + $user = Factory::getUser(); + $input = $app->input->json; + $smtppass = $input->get('smtppass', null, 'RAW'); + + $app->set('smtpauth', $input->get('smtpauth')); + $app->set('smtpuser', $input->get('smtpuser', '', 'STRING')); + $app->set('smtphost', $input->get('smtphost')); + $app->set('smtpsecure', $input->get('smtpsecure')); + $app->set('smtpport', $input->get('smtpport')); + $app->set('mailfrom', $input->get('mailfrom', '', 'STRING')); + $app->set('fromname', $input->get('fromname', '', 'STRING')); + $app->set('mailer', $input->get('mailer')); + $app->set('mailonline', $input->get('mailonline')); + + // Use smtppass only if it was submitted + if ($smtppass !== null) { + $app->set('smtppass', $smtppass); + } + + $mail = Factory::getMailer(); + + // Prepare email and try to send it + $mailer = new MailTemplate('com_config.test_mail', $user->getParam('language', $app->get('language')), $mail); + $mailer->addTemplateData( + array( + 'sitename' => $app->get('sitename'), + 'method' => Text::_('COM_CONFIG_SENDMAIL_METHOD_' . strtoupper($mail->Mailer)) + ) + ); + $mailer->addRecipient($app->get('mailfrom'), $app->get('fromname')); + + try { + $mailSent = $mailer->send(); + } catch (MailDisabledException | phpMailerException $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + if ($mailSent === true) { + $methodName = Text::_('COM_CONFIG_SENDMAIL_METHOD_' . strtoupper($mail->Mailer)); + + // If JMail send the mail using PHP Mail as fallback. + if ($mail->Mailer !== $app->get('mailer')) { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_SENDMAIL_SUCCESS_FALLBACK', $app->get('mailfrom'), $methodName), 'warning'); + } else { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_SENDMAIL_SUCCESS', $app->get('mailfrom'), $methodName), 'message'); + } + + return true; + } + + $app->enqueueMessage(Text::_('COM_CONFIG_SENDMAIL_ERROR'), 'error'); + + return false; + } } diff --git a/administrator/components/com_config/src/Model/ComponentModel.php b/administrator/components/com_config/src/Model/ComponentModel.php index 34e97a703fb44..541c2e770c3e2 100644 --- a/administrator/components/com_config/src/Model/ComponentModel.php +++ b/administrator/components/com_config/src/Model/ComponentModel.php @@ -1,4 +1,5 @@ input; - - // Set the component (option) we are dealing with. - $component = $input->get('component'); - - $this->state->set('component.option', $component); - - // Set an alternative path for the configuration file. - if ($path = $input->getString('path')) - { - $path = Path::clean(JPATH_SITE . '/' . $path); - Path::check($path); - $this->state->set('component.path', $path); - } - } - - /** - * Method to get a form object. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return mixed A JForm object on success, false on failure - * - * @since 3.2 - */ - public function getForm($data = array(), $loadData = true) - { - $state = $this->getState(); - $option = $state->get('component.option'); - - if ($path = $state->get('component.path')) - { - // Add the search path for the admin component config.xml file. - Form::addFormPath($path); - } - else - { - // Add the search path for the admin component config.xml file. - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/' . $option); - } - - // Get the form. - $form = $this->loadForm( - 'com_config.component', - 'config', - array('control' => 'jform', 'load_data' => $loadData), - false, - '/config' - ); - - if (empty($form)) - { - return false; - } - - $lang = Factory::getLanguage(); - $lang->load($option, JPATH_BASE) - || $lang->load($option, JPATH_BASE . "/components/$option"); - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - $option = $this->getState()->get('component.option'); - - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_config.edit.component.' . $option . '.data', []); - - if (empty($data)) - { - return $this->getComponent()->getParams()->toArray(); - } - - return $data; - } - - /** - * Get the component information. - * - * @return object - * - * @since 3.2 - */ - public function getComponent() - { - $state = $this->getState(); - $option = $state->get('component.option'); - - // Load common and local language files. - $lang = Factory::getLanguage(); - $lang->load($option, JPATH_BASE) - || $lang->load($option, JPATH_BASE . "/components/$option"); - - $result = ComponentHelper::getComponent($option); - - return $result; - } - - /** - * Method to save the configuration data. - * - * @param array $data An array containing all global config data. - * - * @return boolean True on success, false on failure. - * - * @since 3.2 - * @throws \RuntimeException - */ - public function save($data) - { - $table = Table::getInstance('extension'); - $context = $this->option . '.' . $this->name; - PluginHelper::importPlugin('extension'); - - // Check super user group. - if (isset($data['params']) && !Factory::getUser()->authorise('core.admin')) - { - $form = $this->getForm(array(), false); - - foreach ($form->getFieldsets() as $fieldset) - { - foreach ($form->getFieldset($fieldset->name) as $field) - { - if ($field->type === 'UserGroupList' && isset($data['params'][$field->fieldname]) - && (int) $field->getAttribute('checksuperusergroup', 0) === 1 - && Access::checkGroup($data['params'][$field->fieldname], 'core.admin')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED')); - } - } - } - } - - // Save the rules. - if (isset($data['params']) && isset($data['params']['rules'])) - { - if (!Factory::getUser()->authorise('core.admin', $data['option'])) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED')); - } - - $rules = new Rules($data['params']['rules']); - $asset = Table::getInstance('asset'); - - if (!$asset->loadByName($data['option'])) - { - $root = Table::getInstance('asset'); - $root->loadByName('root.1'); - $asset->name = $data['option']; - $asset->title = $data['option']; - $asset->setLocation($root->id, 'last-child'); - } - - $asset->rules = (string) $rules; - - if (!$asset->check() || !$asset->store()) - { - throw new \RuntimeException($asset->getError()); - } - - // We don't need this anymore - unset($data['option']); - unset($data['params']['rules']); - } - - // Load the previous Data - if (!$table->load($data['id'])) - { - throw new \RuntimeException($table->getError()); - } - - unset($data['id']); - - // Bind the data. - if (!$table->bind($data)) - { - throw new \RuntimeException($table->getError()); - } - - // Check the data. - if (!$table->check()) - { - throw new \RuntimeException($table->getError()); - } - - $result = Factory::getApplication()->triggerEvent('onExtensionBeforeSave', array($context, $table, false)); - - // Store the data. - if (in_array(false, $result, true) || !$table->store()) - { - throw new \RuntimeException($table->getError()); - } - - Factory::getApplication()->triggerEvent('onExtensionAfterSave', array($context, $table, false)); - - // Clean the component cache. - $this->cleanCache('_system'); - - return true; - } + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.2 + */ + protected function populateState() + { + $input = Factory::getApplication()->input; + + // Set the component (option) we are dealing with. + $component = $input->get('component'); + + $this->state->set('component.option', $component); + + // Set an alternative path for the configuration file. + if ($path = $input->getString('path')) { + $path = Path::clean(JPATH_SITE . '/' . $path); + Path::check($path); + $this->state->set('component.path', $path); + } + } + + /** + * Method to get a form object. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return mixed A JForm object on success, false on failure + * + * @since 3.2 + */ + public function getForm($data = array(), $loadData = true) + { + $state = $this->getState(); + $option = $state->get('component.option'); + + if ($path = $state->get('component.path')) { + // Add the search path for the admin component config.xml file. + Form::addFormPath($path); + } else { + // Add the search path for the admin component config.xml file. + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/' . $option); + } + + // Get the form. + $form = $this->loadForm( + 'com_config.component', + 'config', + array('control' => 'jform', 'load_data' => $loadData), + false, + '/config' + ); + + if (empty($form)) { + return false; + } + + $lang = Factory::getLanguage(); + $lang->load($option, JPATH_BASE) + || $lang->load($option, JPATH_BASE . "/components/$option"); + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + $option = $this->getState()->get('component.option'); + + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_config.edit.component.' . $option . '.data', []); + + if (empty($data)) { + return $this->getComponent()->getParams()->toArray(); + } + + return $data; + } + + /** + * Get the component information. + * + * @return object + * + * @since 3.2 + */ + public function getComponent() + { + $state = $this->getState(); + $option = $state->get('component.option'); + + // Load common and local language files. + $lang = Factory::getLanguage(); + $lang->load($option, JPATH_BASE) + || $lang->load($option, JPATH_BASE . "/components/$option"); + + $result = ComponentHelper::getComponent($option); + + return $result; + } + + /** + * Method to save the configuration data. + * + * @param array $data An array containing all global config data. + * + * @return boolean True on success, false on failure. + * + * @since 3.2 + * @throws \RuntimeException + */ + public function save($data) + { + $table = Table::getInstance('extension'); + $context = $this->option . '.' . $this->name; + PluginHelper::importPlugin('extension'); + + // Check super user group. + if (isset($data['params']) && !Factory::getUser()->authorise('core.admin')) { + $form = $this->getForm(array(), false); + + foreach ($form->getFieldsets() as $fieldset) { + foreach ($form->getFieldset($fieldset->name) as $field) { + if ( + $field->type === 'UserGroupList' && isset($data['params'][$field->fieldname]) + && (int) $field->getAttribute('checksuperusergroup', 0) === 1 + && Access::checkGroup($data['params'][$field->fieldname], 'core.admin') + ) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED')); + } + } + } + } + + // Save the rules. + if (isset($data['params']) && isset($data['params']['rules'])) { + if (!Factory::getUser()->authorise('core.admin', $data['option'])) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED')); + } + + $rules = new Rules($data['params']['rules']); + $asset = Table::getInstance('asset'); + + if (!$asset->loadByName($data['option'])) { + $root = Table::getInstance('asset'); + $root->loadByName('root.1'); + $asset->name = $data['option']; + $asset->title = $data['option']; + $asset->setLocation($root->id, 'last-child'); + } + + $asset->rules = (string) $rules; + + if (!$asset->check() || !$asset->store()) { + throw new \RuntimeException($asset->getError()); + } + + // We don't need this anymore + unset($data['option']); + unset($data['params']['rules']); + } + + // Load the previous Data + if (!$table->load($data['id'])) { + throw new \RuntimeException($table->getError()); + } + + unset($data['id']); + + // Bind the data. + if (!$table->bind($data)) { + throw new \RuntimeException($table->getError()); + } + + // Check the data. + if (!$table->check()) { + throw new \RuntimeException($table->getError()); + } + + $result = Factory::getApplication()->triggerEvent('onExtensionBeforeSave', array($context, $table, false)); + + // Store the data. + if (in_array(false, $result, true) || !$table->store()) { + throw new \RuntimeException($table->getError()); + } + + Factory::getApplication()->triggerEvent('onExtensionAfterSave', array($context, $table, false)); + + // Clean the component cache. + $this->cleanCache('_system'); + + return true; + } } diff --git a/administrator/components/com_config/src/View/Application/HtmlView.php b/administrator/components/com_config/src/View/Application/HtmlView.php index 4811211852143..dbf77767dfc7d 100644 --- a/administrator/components/com_config/src/View/Application/HtmlView.php +++ b/administrator/components/com_config/src/View/Application/HtmlView.php @@ -1,4 +1,5 @@ get('form'); - $data = $this->get('data'); - $user = $this->getCurrentUser(); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return; - } - - // Bind data - if ($form && $data) - { - $form->bind($data); - } - - // Get the params for com_users. - $usersParams = ComponentHelper::getParams('com_users'); - - // Get the params for com_media. - $mediaParams = ComponentHelper::getParams('com_media'); - - $this->form = &$form; - $this->data = &$data; - $this->usersParams = &$usersParams; - $this->mediaParams = &$mediaParams; - $this->components = ConfigHelper::getComponentsWithConfig(); - ConfigHelper::loadLanguageForComponents($this->components); - - $this->userIsSuperAdmin = $user->authorise('core.admin'); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.2 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_CONFIG_GLOBAL_CONFIGURATION'), 'cog config'); - ToolbarHelper::apply('application.apply'); - ToolbarHelper::divider(); - ToolbarHelper::save('application.save'); - ToolbarHelper::divider(); - ToolbarHelper::cancel('application.cancel', 'JTOOLBAR_CLOSE'); - ToolbarHelper::divider(); - ToolbarHelper::inlinehelp(); - ToolbarHelper::help('Site_Global_Configuration'); - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * @since 3.2 + */ + public $state; + + /** + * The form object + * + * @var \Joomla\CMS\Form\Form + * @since 3.2 + */ + public $form; + + /** + * The data to be displayed in the form + * + * @var array + * @since 3.2 + */ + public $data; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see \JViewLegacy::loadTemplate() + * @since 3.0 + */ + public function display($tpl = null) + { + try { + // Load Form and Data + $form = $this->get('form'); + $data = $this->get('data'); + $user = $this->getCurrentUser(); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return; + } + + // Bind data + if ($form && $data) { + $form->bind($data); + } + + // Get the params for com_users. + $usersParams = ComponentHelper::getParams('com_users'); + + // Get the params for com_media. + $mediaParams = ComponentHelper::getParams('com_media'); + + $this->form = &$form; + $this->data = &$data; + $this->usersParams = &$usersParams; + $this->mediaParams = &$mediaParams; + $this->components = ConfigHelper::getComponentsWithConfig(); + ConfigHelper::loadLanguageForComponents($this->components); + + $this->userIsSuperAdmin = $user->authorise('core.admin'); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.2 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_CONFIG_GLOBAL_CONFIGURATION'), 'cog config'); + ToolbarHelper::apply('application.apply'); + ToolbarHelper::divider(); + ToolbarHelper::save('application.save'); + ToolbarHelper::divider(); + ToolbarHelper::cancel('application.cancel', 'JTOOLBAR_CLOSE'); + ToolbarHelper::divider(); + ToolbarHelper::inlinehelp(); + ToolbarHelper::help('Site_Global_Configuration'); + } } diff --git a/administrator/components/com_config/src/View/Component/HtmlView.php b/administrator/components/com_config/src/View/Component/HtmlView.php index b29e47b5031ad..bb5126ddcc9d9 100644 --- a/administrator/components/com_config/src/View/Component/HtmlView.php +++ b/administrator/components/com_config/src/View/Component/HtmlView.php @@ -1,4 +1,5 @@ get('component'); - - if (!$component->enabled) - { - return; - } - - $form = $this->get('form'); - $user = $this->getCurrentUser(); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return; - } - - $this->fieldsets = $form ? $form->getFieldsets() : null; - $this->formControl = $form ? $form->getFormControl() : null; - - // Don't show permissions fieldset if not authorised. - if (!$user->authorise('core.admin', $component->option) && isset($this->fieldsets['permissions'])) - { - unset($this->fieldsets['permissions']); - } - - $this->form = &$form; - $this->component = &$component; - - $this->components = ConfigHelper::getComponentsWithConfig(); - - $this->userIsSuperAdmin = $user->authorise('core.admin'); - $this->currentComponent = Factory::getApplication()->input->get('component'); - $this->return = Factory::getApplication()->input->get('return', '', 'base64'); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.2 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_($this->component->option . '_configuration'), 'cog config'); - ToolbarHelper::apply('component.apply'); - ToolbarHelper::divider(); - ToolbarHelper::save('component.save'); - ToolbarHelper::divider(); - ToolbarHelper::cancel('component.cancel', 'JTOOLBAR_CLOSE'); - ToolbarHelper::divider(); - - $inlinehelp = (string) $this->form->getXml()->config->inlinehelp['button'] == 'show' ?: false; - $targetClass = (string) $this->form->getXml()->config->inlinehelp['targetclass'] ?: 'hide-aware-inline-help'; - - if ($inlinehelp) - { - ToolbarHelper::inlinehelp($targetClass); - } - - $helpUrl = $this->form->getData()->get('helpURL'); - $helpKey = (string) $this->form->getXml()->config->help['key']; - - // Try with legacy language key - if (!$helpKey) - { - $language = Factory::getApplication()->getLanguage(); - $languageKey = 'JHELP_COMPONENTS_' . strtoupper($this->currentComponent) . '_OPTIONS'; - - if ($language->hasKey($languageKey)) - { - $helpKey = $languageKey; - } - } - - ToolbarHelper::help($helpKey, (boolean) $helpUrl, null, $this->currentComponent); - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * @since 3.2 + */ + public $state; + + /** + * The form object + * + * @var \Joomla\CMS\Form\Form + * @since 3.2 + */ + public $form; + + /** + * An object with the information for the component + * + * @var \Joomla\CMS\Component\ComponentRecord + * @since 3.2 + */ + public $component; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see \JViewLegacy::loadTemplate() + * @since 3.2 + */ + public function display($tpl = null) + { + try { + $component = $this->get('component'); + + if (!$component->enabled) { + return; + } + + $form = $this->get('form'); + $user = $this->getCurrentUser(); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return; + } + + $this->fieldsets = $form ? $form->getFieldsets() : null; + $this->formControl = $form ? $form->getFormControl() : null; + + // Don't show permissions fieldset if not authorised. + if (!$user->authorise('core.admin', $component->option) && isset($this->fieldsets['permissions'])) { + unset($this->fieldsets['permissions']); + } + + $this->form = &$form; + $this->component = &$component; + + $this->components = ConfigHelper::getComponentsWithConfig(); + + $this->userIsSuperAdmin = $user->authorise('core.admin'); + $this->currentComponent = Factory::getApplication()->input->get('component'); + $this->return = Factory::getApplication()->input->get('return', '', 'base64'); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.2 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_($this->component->option . '_configuration'), 'cog config'); + ToolbarHelper::apply('component.apply'); + ToolbarHelper::divider(); + ToolbarHelper::save('component.save'); + ToolbarHelper::divider(); + ToolbarHelper::cancel('component.cancel', 'JTOOLBAR_CLOSE'); + ToolbarHelper::divider(); + + $inlinehelp = (string) $this->form->getXml()->config->inlinehelp['button'] == 'show' ?: false; + $targetClass = (string) $this->form->getXml()->config->inlinehelp['targetclass'] ?: 'hide-aware-inline-help'; + + if ($inlinehelp) { + ToolbarHelper::inlinehelp($targetClass); + } + + $helpUrl = $this->form->getData()->get('helpURL'); + $helpKey = (string) $this->form->getXml()->config->help['key']; + + // Try with legacy language key + if (!$helpKey) { + $language = Factory::getApplication()->getLanguage(); + $languageKey = 'JHELP_COMPONENTS_' . strtoupper($this->currentComponent) . '_OPTIONS'; + + if ($language->hasKey($languageKey)) { + $helpKey = $languageKey; + } + } + + ToolbarHelper::help($helpKey, (bool) $helpUrl, null, $this->currentComponent); + } } diff --git a/administrator/components/com_config/tmpl/application/default.php b/administrator/components/com_config/tmpl/application/default.php index 9dba06f4e808e..9cc769e0d13e1 100644 --- a/administrator/components/com_config/tmpl/application/default.php +++ b/administrator/components/com_config/tmpl/application/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); // Load JS message titles Text::script('ERROR'); @@ -26,56 +27,56 @@ ?>
    -
    - -
    - 'page-site', 'recall' => true, 'breakpoint' => 768]); ?> - - loadTemplate('site'); ?> - loadTemplate('metadata'); ?> - loadTemplate('seo'); ?> - loadTemplate('cookie'); ?> - +
    + +
    + 'page-site', 'recall' => true, 'breakpoint' => 768]); ?> + + loadTemplate('site'); ?> + loadTemplate('metadata'); ?> + loadTemplate('seo'); ?> + loadTemplate('cookie'); ?> + - - loadTemplate('debug'); ?> - loadTemplate('cache'); ?> - loadTemplate('session'); ?> - + + loadTemplate('debug'); ?> + loadTemplate('cache'); ?> + loadTemplate('session'); ?> + - - loadTemplate('server'); ?> - loadTemplate('locale'); ?> - loadTemplate('webservices'); ?> - loadTemplate('proxy'); ?> - loadTemplate('database'); ?> - loadTemplate('mail'); ?> - + + loadTemplate('server'); ?> + loadTemplate('locale'); ?> + loadTemplate('webservices'); ?> + loadTemplate('proxy'); ?> + loadTemplate('database'); ?> + loadTemplate('mail'); ?> + - - loadTemplate('logging'); ?> - loadTemplate('logging_custom'); ?> - + + loadTemplate('logging'); ?> + loadTemplate('logging_custom'); ?> + - - loadTemplate('filters'); ?> - + + loadTemplate('filters'); ?> + - - loadTemplate('permissions'); ?> - - + + loadTemplate('permissions'); ?> + + - - -
    -
    + + +
    +
    diff --git a/administrator/components/com_config/tmpl/application/default_cache.php b/administrator/components/com_config/tmpl/application/default_cache.php index 79d8bb004f498..621f93e66383d 100644 --- a/administrator/components/com_config/tmpl/application/default_cache.php +++ b/administrator/components/com_config/tmpl/application/default_cache.php @@ -1,4 +1,5 @@ document->getWebAssetManager() - ->useScript('webcomponent.field-send-test-mail'); + ->useScript('webcomponent.field-send-test-mail'); // Load JavaScript message titles Text::script('ERROR'); @@ -42,9 +43,9 @@ ?> - + - + diff --git a/administrator/components/com_config/tmpl/application/default_metadata.php b/administrator/components/com_config/tmpl/application/default_metadata.php index 912499a3b6a36..75f9eeb1fea93 100644 --- a/administrator/components/com_config/tmpl/application/default_metadata.php +++ b/administrator/components/com_config/tmpl/application/default_metadata.php @@ -1,4 +1,5 @@ diff --git a/administrator/components/com_config/tmpl/application/default_permissions.php b/administrator/components/com_config/tmpl/application/default_permissions.php index 83cd3c43bfe0b..929e62b1a3cfd 100644 --- a/administrator/components/com_config/tmpl/application/default_permissions.php +++ b/administrator/components/com_config/tmpl/application/default_permissions.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('form.validate') - ->useScript('keepalive'); + ->useScript('keepalive'); -if ($this->fieldsets) -{ - HTMLHelper::_('bootstrap.framework'); +if ($this->fieldsets) { + HTMLHelper::_('bootstrap.framework'); } $xml = $this->form->getXml(); ?>
    -
    - - - - -
    - fieldsets) : ?> - - - true, 'breakpoint' => 768]); ?> - - fieldsets as $name => $fieldSet) : ?> - xpath('//fieldset[@name="' . $name . '"]/fieldset'); - $hasParent = $xml->xpath('//fieldset/fieldset[@name="' . $name . '"]'); - $isGrandchild = $xml->xpath('//fieldset/fieldset/fieldset[@name="' . $name . '"]'); - ?> - - - showon)) : ?> - useScript('showon'); ?> - showon, $this->formControl)) . '\''; ?> - - - label) ? 'COM_CONFIG_' . $name . '_FIELDSET_LABEL' : $fieldSet->label; ?> - - -
    - label); ?> -
    - - - - 1) : ?> -
    -
    - - - - - - - - - - - - -
    - label); ?> -
    - - - - - description)) : ?> -
    - - description); ?> -
    - - - - form->renderFieldset($name, $name === 'permissions' ? ['hiddenLabel' => true, 'class' => 'revert-controls'] : []); ?> - - - -
    -
    - - - - - - 1) : ?> -
    - - - - - - - - -
    - - -
    - -
    - - - - - - - +
    + + + + +
    + fieldsets) : ?> + + + true, 'breakpoint' => 768]); ?> + + fieldsets as $name => $fieldSet) : ?> + xpath('//fieldset[@name="' . $name . '"]/fieldset'); + $hasParent = $xml->xpath('//fieldset/fieldset[@name="' . $name . '"]'); + $isGrandchild = $xml->xpath('//fieldset/fieldset/fieldset[@name="' . $name . '"]'); + ?> + + + showon)) : ?> + useScript('showon'); ?> + showon, $this->formControl)) . '\''; ?> + + + label) ? 'COM_CONFIG_' . $name . '_FIELDSET_LABEL' : $fieldSet->label; ?> + + +
    + label); ?> +
    + + + 1) : ?> +
    +
    + + + + + + + + + + + +
    + label); ?> +
    + + + + + description)) : ?> +
    + + description); ?> +
    + + + + form->renderFieldset($name, $name === 'permissions' ? ['hiddenLabel' => true, 'class' => 'revert-controls'] : []); ?> + + + +
    +
    + + + + + 1) : ?> +
    + + + + + + + + +
    + + +
    + +
    + + + + + + +
    diff --git a/administrator/components/com_config/tmpl/component/default_navigation.php b/administrator/components/com_config/tmpl/component/default_navigation.php index 7f8f9a68b12cb..2eb654443da86 100644 --- a/administrator/components/com_config/tmpl/component/default_navigation.php +++ b/administrator/components/com_config/tmpl/component/default_navigation.php @@ -1,4 +1,5 @@ diff --git a/administrator/components/com_contact/helpers/contact.php b/administrator/components/com_contact/helpers/contact.php index 262294130943f..7648463beab9e 100644 --- a/administrator/components/com_contact/helpers/contact.php +++ b/administrator/components/com_contact/helpers/contact.php @@ -1,4 +1,5 @@ set(AssociationExtensionInterface::class, new AssociationsHelper); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->set(AssociationExtensionInterface::class, new AssociationsHelper()); - $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Contact')); - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Contact')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Contact')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Contact')); + $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Contact')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Contact')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Contact')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Contact')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new ContactComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new ContactComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); - $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); + $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_contact/src/Controller/AjaxController.php b/administrator/components/com_contact/src/Controller/AjaxController.php index 1aff104743200..cfc3f6bbe06fc 100644 --- a/administrator/components/com_contact/src/Controller/AjaxController.php +++ b/administrator/components/com_contact/src/Controller/AjaxController.php @@ -1,4 +1,5 @@ input->getInt('assocId', 0); + /** + * Method to fetch associations of a contact + * + * The method assumes that the following http parameters are passed in an Ajax Get request: + * token: the form token + * assocId: the id of the contact whose associations are to be returned + * excludeLang: the association for this language is to be excluded + * + * @return void + * + * @since 3.9.0 + */ + public function fetchAssociations() + { + if (!Session::checkToken('get')) { + echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true); + } else { + $assocId = $this->input->getInt('assocId', 0); - if ($assocId == 0) - { - echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); + if ($assocId == 0) { + echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); - return; - } + return; + } - $excludeLang = $this->input->get('excludeLang', '', 'STRING'); + $excludeLang = $this->input->get('excludeLang', '', 'STRING'); - $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', (int) $assocId); + $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', (int) $assocId); - unset($associations[$excludeLang]); + unset($associations[$excludeLang]); - // Add the title to each of the associated records - $contactTable = $this->factory->createTable('Contact', 'Administrator'); + // Add the title to each of the associated records + $contactTable = $this->factory->createTable('Contact', 'Administrator'); - foreach ($associations as $lang => $association) - { - $contactTable->load($association->id); - $associations[$lang]->title = $contactTable->name; - } + foreach ($associations as $lang => $association) { + $contactTable->load($association->id); + $associations[$lang]->title = $contactTable->name; + } - $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); + $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); - if (count($associations) == 0) - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); - } - elseif ($countContentLanguages > count($associations) + 2) - { - $tags = implode(', ', array_keys($associations)); - $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); - } - else - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); - } + if (count($associations) == 0) { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); + } elseif ($countContentLanguages > count($associations) + 2) { + $tags = implode(', ', array_keys($associations)); + $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); + } else { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); + } - echo new JsonResponse($associations, $message); - } - } + echo new JsonResponse($associations, $message); + } + } } diff --git a/administrator/components/com_contact/src/Controller/ContactController.php b/administrator/components/com_contact/src/Controller/ContactController.php index 0817e4245da84..c697bd23386bb 100644 --- a/administrator/components/com_contact/src/Controller/ContactController.php +++ b/administrator/components/com_contact/src/Controller/ContactController.php @@ -1,4 +1,5 @@ input->getInt('filter_category_id'), 'int'); - - if ($categoryId) - { - // If the category has been passed in the URL check it. - return $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); - } - - // In the absence of better information, revert to the component permissions. - return parent::allowAdd($data); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - - // Since there is no asset tracking, fallback to the component permissions. - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Get the item. - $item = $this->getModel()->getItem($recordId); - - // Since there is no item, return false. - if (empty($item)) - { - return false; - } - - $user = $this->app->getIdentity(); - - // Check if can edit own core.edit.own. - $canEditOwn = $user->authorise('core.edit.own', $this->option . '.category.' . (int) $item->catid) && $item->created_by == $user->id; - - // Check the category core.edit permissions. - return $canEditOwn || $user->authorise('core.edit', $this->option . '.category.' . (int) $item->catid); - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - /** @var \Joomla\Component\Contact\Administrator\Model\ContactModel $model */ - $model = $this->getModel('Contact', 'Administrator', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_contact&view=contacts' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } - - /** - * Function that allows child controller access to model data - * after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 4.1.0 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = []) - { - if ($this->getTask() === 'save2menu') - { - $editState = []; - - $id = $model->getState('contact.id'); - - $link = 'index.php?option=com_contact&view=contact'; - $type = 'component'; - - $editState['id'] = $id; - $editState['link'] = $link; - $editState['title'] = $model->getItem($id)->name; - $editState['type'] = $type; - $editState['request']['id'] = $id; - - $this->app->setUserState( - 'com_menus.edit.item', - [ - 'data' => $editState, - 'type' => $type, - 'link' => $link, - ] - ); - - $this->setRedirect(Route::_('index.php?option=com_menus&view=item&client_id=0&menutype=mainmenu&layout=edit', false)); - } - } + use VersionableControllerTrait; + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('filter_category_id'), 'int'); + + if ($categoryId) { + // If the category has been passed in the URL check it. + return $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); + } + + // In the absence of better information, revert to the component permissions. + return parent::allowAdd($data); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + + // Since there is no asset tracking, fallback to the component permissions. + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Get the item. + $item = $this->getModel()->getItem($recordId); + + // Since there is no item, return false. + if (empty($item)) { + return false; + } + + $user = $this->app->getIdentity(); + + // Check if can edit own core.edit.own. + $canEditOwn = $user->authorise('core.edit.own', $this->option . '.category.' . (int) $item->catid) && $item->created_by == $user->id; + + // Check the category core.edit permissions. + return $canEditOwn || $user->authorise('core.edit', $this->option . '.category.' . (int) $item->catid); + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + /** @var \Joomla\Component\Contact\Administrator\Model\ContactModel $model */ + $model = $this->getModel('Contact', 'Administrator', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_contact&view=contacts' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } + + /** + * Function that allows child controller access to model data + * after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 4.1.0 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = []) + { + if ($this->getTask() === 'save2menu') { + $editState = []; + + $id = $model->getState('contact.id'); + + $link = 'index.php?option=com_contact&view=contact'; + $type = 'component'; + + $editState['id'] = $id; + $editState['link'] = $link; + $editState['title'] = $model->getItem($id)->name; + $editState['type'] = $type; + $editState['request']['id'] = $id; + + $this->app->setUserState( + 'com_menus.edit.item', + [ + 'data' => $editState, + 'type' => $type, + 'link' => $link, + ] + ); + + $this->setRedirect(Route::_('index.php?option=com_menus&view=item&client_id=0&menutype=mainmenu&layout=edit', false)); + } + } } diff --git a/administrator/components/com_contact/src/Controller/ContactsController.php b/administrator/components/com_contact/src/Controller/ContactsController.php index 953b454c28863..cb3d1a5ac03ec 100644 --- a/administrator/components/com_contact/src/Controller/ContactsController.php +++ b/administrator/components/com_contact/src/Controller/ContactsController.php @@ -1,4 +1,5 @@ registerTask('unfeatured', 'featured'); - } - - /** - * Method to toggle the featured setting of a list of contacts. - * - * @return void - * - * @since 1.6 - */ - public function featured() - { - // Check for request forgeries - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('featured' => 1, 'unfeatured' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - - // Get the model. - /** @var \Joomla\Component\Contact\Administrator\Model\ContactModel $model */ - $model = $this->getModel(); - - // Access checks. - foreach ($ids as $i => $id) - { - // Remove zero value resulting from input filter - if ($id === 0) - { - unset($ids[$i]); - - continue; - } - - $item = $model->getItem($id); - - if (!$this->app->getIdentity()->authorise('core.edit.state', 'com_contact.category.' . (int) $item->catid)) - { - // Prune items that you can't change. - unset($ids[$i]); - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice'); - } - } - - if (empty($ids)) - { - $message = null; - - $this->app->enqueueMessage(Text::_('COM_CONTACT_NO_ITEM_SELECTED'), 'warning'); - } - else - { - // Publish the items. - if (!$model->featured($ids, $value)) - { - $this->app->enqueueMessage($model->getError(), 'warning'); - } - - if ($value == 1) - { - $message = Text::plural('COM_CONTACT_N_ITEMS_FEATURED', count($ids)); - } - else - { - $message = Text::plural('COM_CONTACT_N_ITEMS_UNFEATURED', count($ids)); - } - } - - $this->setRedirect('index.php?option=com_contact&view=contacts', $message); - } - - /** - * Proxy for getModel. - * - * @param string $name The name of the model. - * @param string $prefix The prefix for the PHP class name. - * @param array $config Array of configuration parameters. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel - * - * @since 1.6 - */ - public function getModel($name = 'Contact', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('unfeatured', 'featured'); + } + + /** + * Method to toggle the featured setting of a list of contacts. + * + * @return void + * + * @since 1.6 + */ + public function featured() + { + // Check for request forgeries + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('featured' => 1, 'unfeatured' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + + // Get the model. + /** @var \Joomla\Component\Contact\Administrator\Model\ContactModel $model */ + $model = $this->getModel(); + + // Access checks. + foreach ($ids as $i => $id) { + // Remove zero value resulting from input filter + if ($id === 0) { + unset($ids[$i]); + + continue; + } + + $item = $model->getItem($id); + + if (!$this->app->getIdentity()->authorise('core.edit.state', 'com_contact.category.' . (int) $item->catid)) { + // Prune items that you can't change. + unset($ids[$i]); + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice'); + } + } + + if (empty($ids)) { + $message = null; + + $this->app->enqueueMessage(Text::_('COM_CONTACT_NO_ITEM_SELECTED'), 'warning'); + } else { + // Publish the items. + if (!$model->featured($ids, $value)) { + $this->app->enqueueMessage($model->getError(), 'warning'); + } + + if ($value == 1) { + $message = Text::plural('COM_CONTACT_N_ITEMS_FEATURED', count($ids)); + } else { + $message = Text::plural('COM_CONTACT_N_ITEMS_UNFEATURED', count($ids)); + } + } + + $this->setRedirect('index.php?option=com_contact&view=contacts', $message); + } + + /** + * Proxy for getModel. + * + * @param string $name The name of the model. + * @param string $prefix The prefix for the PHP class name. + * @param array $config Array of configuration parameters. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since 1.6 + */ + public function getModel($name = 'Contact', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_contact/src/Controller/DisplayController.php b/administrator/components/com_contact/src/Controller/DisplayController.php index 81f436b15d2a6..92cc8a7a5f4cf 100644 --- a/administrator/components/com_contact/src/Controller/DisplayController.php +++ b/administrator/components/com_contact/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', $this->default_view); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static |boolean This object to support chaining. False on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', $this->default_view); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($view == 'contact' && $layout == 'edit' && !$this->checkEditId('com_contact.edit.contact', $id)) - { - // 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', $id), 'error'); - } + // Check for edit form. + if ($view == 'contact' && $layout == 'edit' && !$this->checkEditId('com_contact.edit.contact', $id)) { + // 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', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_contact&view=contacts', false)); + $this->setRedirect(Route::_('index.php?option=com_contact&view=contacts', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/administrator/components/com_contact/src/Extension/ContactComponent.php b/administrator/components/com_contact/src/Extension/ContactComponent.php index cfddee3de41a3..41448c0378f82 100644 --- a/administrator/components/com_contact/src/Extension/ContactComponent.php +++ b/administrator/components/com_contact/src/Extension/ContactComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('contactadministrator', new AdministratorService); - $this->getRegistry()->register('contacticon', new Icon($container->get(UserFactoryInterface::class))); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('contactadministrator', new AdministratorService()); + $this->getRegistry()->register('contacticon', new Icon($container->get(UserFactoryInterface::class))); + } - /** - * Returns a valid section for the given section. If it is not valid then null - * is returned. - * - * @param string $section The section to get the mapping for - * @param object $item The item - * - * @return string|null The new section - * - * @since 4.0.0 - */ - public function validateSection($section, $item = null) - { - if (Factory::getApplication()->isClient('site') && $section == 'contact' && $item instanceof Form) - { - // The contact form needs to be the mail section - $section = 'mail'; - } + /** + * Returns a valid section for the given section. If it is not valid then null + * is returned. + * + * @param string $section The section to get the mapping for + * @param object $item The item + * + * @return string|null The new section + * + * @since 4.0.0 + */ + public function validateSection($section, $item = null) + { + if (Factory::getApplication()->isClient('site') && $section == 'contact' && $item instanceof Form) { + // The contact form needs to be the mail section + $section = 'mail'; + } - if (Factory::getApplication()->isClient('site') && ($section === 'category' || $section === 'form')) - { - // The contact form needs to be the mail section - $section = 'contact'; - } + if (Factory::getApplication()->isClient('site') && ($section === 'category' || $section === 'form')) { + // The contact form needs to be the mail section + $section = 'contact'; + } - if ($section !== 'mail' && $section !== 'contact') - { - // We don't know other sections - return null; - } + if ($section !== 'mail' && $section !== 'contact') { + // We don't know other sections + return null; + } - return $section; - } + return $section; + } - /** - * Returns valid contexts - * - * @return array - * - * @since 4.0.0 - */ - public function getContexts(): array - { - Factory::getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); + /** + * Returns valid contexts + * + * @return array + * + * @since 4.0.0 + */ + public function getContexts(): array + { + Factory::getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); - $contexts = array( - 'com_contact.contact' => Text::_('COM_CONTACT_FIELDS_CONTEXT_CONTACT'), - 'com_contact.mail' => Text::_('COM_CONTACT_FIELDS_CONTEXT_MAIL'), - 'com_contact.categories' => Text::_('JCATEGORY') - ); + $contexts = array( + 'com_contact.contact' => Text::_('COM_CONTACT_FIELDS_CONTEXT_CONTACT'), + 'com_contact.mail' => Text::_('COM_CONTACT_FIELDS_CONTEXT_MAIL'), + 'com_contact.categories' => Text::_('JCATEGORY') + ); - return $contexts; - } + return $contexts; + } - /** - * Returns the table for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getTableNameForSection(string $section = null) - { - return ($section === 'category' ? 'categories' : 'contact_details'); - } + /** + * Returns the table for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getTableNameForSection(string $section = null) + { + return ($section === 'category' ? 'categories' : 'contact_details'); + } - /** - * Returns the state column for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getStateColumnForSection(string $section = null) - { - return 'published'; - } + /** + * Returns the state column for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getStateColumnForSection(string $section = null) + { + return 'published'; + } } diff --git a/administrator/components/com_contact/src/Field/Modal/ContactField.php b/administrator/components/com_contact/src/Field/Modal/ContactField.php index a67d64497d0df..14e9d0199584d 100644 --- a/administrator/components/com_contact/src/Field/Modal/ContactField.php +++ b/administrator/components/com_contact/src/Field/Modal/ContactField.php @@ -1,4 +1,5 @@ element['new'] == 'true'); - $allowEdit = ((string) $this->element['edit'] == 'true'); - $allowClear = ((string) $this->element['clear'] != 'false'); - $allowSelect = ((string) $this->element['select'] != 'false'); - $allowPropagate = ((string) $this->element['propagate'] == 'true'); - - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - - // Load language - Factory::getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); - - // The active contact id field. - $value = (int) $this->value ?: ''; - - // Create the modal id. - $modalId = 'Contact_' . $this->id; - - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - - // Add the modal field script to the document head. - $wa->useScript('field.modal-fields'); - - // Script to proxy the select modal function to the modal-fields.js file. - if ($allowSelect) - { - static $scriptSelect = null; - - if (is_null($scriptSelect)) - { - $scriptSelect = array(); - } - - if (!isset($scriptSelect[$this->id])) - { - $wa->addInlineScript(" + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'Modal_Contact'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $allowNew = ((string) $this->element['new'] == 'true'); + $allowEdit = ((string) $this->element['edit'] == 'true'); + $allowClear = ((string) $this->element['clear'] != 'false'); + $allowSelect = ((string) $this->element['select'] != 'false'); + $allowPropagate = ((string) $this->element['propagate'] == 'true'); + + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + + // Load language + Factory::getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); + + // The active contact id field. + $value = (int) $this->value ?: ''; + + // Create the modal id. + $modalId = 'Contact_' . $this->id; + + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + + // Add the modal field script to the document head. + $wa->useScript('field.modal-fields'); + + // Script to proxy the select modal function to the modal-fields.js file. + if ($allowSelect) { + static $scriptSelect = null; + + if (is_null($scriptSelect)) { + $scriptSelect = array(); + } + + if (!isset($scriptSelect[$this->id])) { + $wa->addInlineScript( + " window.jSelectContact_" . $this->id . " = function (id, title, object) { window.processModalSelect('Contact', '" . $this->id . "', id, title, '', object); }", - [], - ['type' => 'module'] - ); - - Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); - - $scriptSelect[$this->id] = true; - } - } - - // Setup variables for display. - $linkContacts = 'index.php?option=com_contact&view=contacts&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - $linkContact = 'index.php?option=com_contact&view=contact&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - $modalTitle = Text::_('COM_CONTACT_SELECT_A_CONTACT'); - - if (isset($this->element['language'])) - { - $linkContacts .= '&forcedLanguage=' . $this->element['language']; - $linkContact .= '&forcedLanguage=' . $this->element['language']; - $modalTitle .= ' — ' . $this->element['label']; - } - - $urlSelect = $linkContacts . '&function=jSelectContact_' . $this->id; - $urlEdit = $linkContact . '&task=contact.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; - $urlNew = $linkContact . '&task=contact.add'; - - if ($value) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__contact_details')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $value, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $title = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - $title = empty($title) ? Text::_('COM_CONTACT_SELECT_A_CONTACT') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); - - // The current contact display field. - $html = ''; - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - $html .= ''; - - // Select contact button - if ($allowSelect) - { - $html .= '' - . ' ' . Text::_('JSELECT') - . ''; - } - - // New contact button - if ($allowNew) - { - $html .= '' - . ' ' . Text::_('JACTION_CREATE') - . ''; - } - - // Edit contact button - if ($allowEdit) - { - $html .= '' - . ' ' . Text::_('JACTION_EDIT') - . ''; - } - - // Clear contact button - if ($allowClear) - { - $html .= '' - . ' ' . Text::_('JCLEAR') - . ''; - } - - // Propagate contact button - if ($allowPropagate && count($languages) > 2) - { - // Strip off language tag at the end - $tagLength = (int) strlen($this->element['language']); - $callbackFunctionStem = substr("jSelectContact_" . $this->id, 0, -$tagLength); - - $html .= '' - . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') - . ''; - } - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - // Select contact modal - if ($allowSelect) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalSelect' . $modalId, - array( - 'title' => $modalTitle, - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - } - - // New contact modal - if ($allowNew) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalNew' . $modalId, - array( - 'title' => Text::_('COM_CONTACT_NEW_CONTACT'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlNew, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Edit contact modal. - if ($allowEdit) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalEdit' . $modalId, - array( - 'title' => Text::_('COM_CONTACT_EDIT_CONTACT'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlEdit, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Note: class='required' for client side validation. - $class = $this->required ? ' class="required modal-value"' : ''; - - $html .= ''; - - return $html; - } - - /** - * Method to get the field label markup. - * - * @return string The field label markup. - * - * @since 3.4 - */ - protected function getLabel() - { - return str_replace($this->id, $this->id . '_name', parent::getLabel()); - } + [], + ['type' => 'module'] + ); + + Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); + + $scriptSelect[$this->id] = true; + } + } + + // Setup variables for display. + $linkContacts = 'index.php?option=com_contact&view=contacts&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + $linkContact = 'index.php?option=com_contact&view=contact&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + $modalTitle = Text::_('COM_CONTACT_SELECT_A_CONTACT'); + + if (isset($this->element['language'])) { + $linkContacts .= '&forcedLanguage=' . $this->element['language']; + $linkContact .= '&forcedLanguage=' . $this->element['language']; + $modalTitle .= ' — ' . $this->element['label']; + } + + $urlSelect = $linkContacts . '&function=jSelectContact_' . $this->id; + $urlEdit = $linkContact . '&task=contact.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; + $urlNew = $linkContact . '&task=contact.add'; + + if ($value) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__contact_details')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $value, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $title = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + $title = empty($title) ? Text::_('COM_CONTACT_SELECT_A_CONTACT') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + + // The current contact display field. + $html = ''; + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + $html .= ''; + + // Select contact button + if ($allowSelect) { + $html .= '' + . ' ' . Text::_('JSELECT') + . ''; + } + + // New contact button + if ($allowNew) { + $html .= '' + . ' ' . Text::_('JACTION_CREATE') + . ''; + } + + // Edit contact button + if ($allowEdit) { + $html .= '' + . ' ' . Text::_('JACTION_EDIT') + . ''; + } + + // Clear contact button + if ($allowClear) { + $html .= '' + . ' ' . Text::_('JCLEAR') + . ''; + } + + // Propagate contact button + if ($allowPropagate && count($languages) > 2) { + // Strip off language tag at the end + $tagLength = (int) strlen($this->element['language']); + $callbackFunctionStem = substr("jSelectContact_" . $this->id, 0, -$tagLength); + + $html .= '' + . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') + . ''; + } + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + // Select contact modal + if ($allowSelect) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalSelect' . $modalId, + array( + 'title' => $modalTitle, + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + } + + // New contact modal + if ($allowNew) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalNew' . $modalId, + array( + 'title' => Text::_('COM_CONTACT_NEW_CONTACT'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlNew, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Edit contact modal. + if ($allowEdit) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalEdit' . $modalId, + array( + 'title' => Text::_('COM_CONTACT_EDIT_CONTACT'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlEdit, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Note: class='required' for client side validation. + $class = $this->required ? ' class="required modal-value"' : ''; + + $html .= ''; + + return $html; + } + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + * + * @since 3.4 + */ + protected function getLabel() + { + return str_replace($this->id, $this->id . '_name', parent::getLabel()); + } } diff --git a/administrator/components/com_contact/src/Helper/AssociationsHelper.php b/administrator/components/com_contact/src/Helper/AssociationsHelper.php index 87551616a9836..1ba6b01987d9a 100644 --- a/administrator/components/com_contact/src/Helper/AssociationsHelper.php +++ b/administrator/components/com_contact/src/Helper/AssociationsHelper.php @@ -1,4 +1,5 @@ getType($typeName); - - $context = $this->extension . '.item'; - $catidField = 'catid'; - - if ($typeName === 'category') - { - $context = 'com_categories.item'; - $catidField = ''; - } - - // Get the associations. - $associations = Associations::getAssociations( - $this->extension, - $type['tables']['a'], - $context, - $id, - 'id', - 'alias', - $catidField - ); - - return $associations; - } - - /** - * Get item information - * - * @param string $typeName The item type - * @param int $id The id of item for which we need the associated items - * - * @return Table|null - * - * @since 3.7.0 - */ - public function getItem($typeName, $id) - { - if (empty($id)) - { - return null; - } - - $table = null; - - switch ($typeName) - { - case 'contact': - $table = Table::getInstance('ContactTable', 'Joomla\\Component\\Contact\\Administrator\\Table\\'); - break; - - case 'category': - $table = Table::getInstance('Category'); - break; - } - - if (empty($table)) - { - return null; - } - - $table->load($id); - - return $table; - } - - /** - * Get information about the type - * - * @param string $typeName The item type - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getType($typeName = '') - { - $fields = $this->getFieldsTemplate(); - $tables = array(); - $joins = array(); - $support = $this->getSupportTemplate(); - $title = ''; - - if (in_array($typeName, $this->itemTypes)) - { - switch ($typeName) - { - case 'contact': - $fields['title'] = 'a.name'; - $fields['state'] = 'a.published'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['category'] = true; - $support['save2copy'] = true; - - $tables = array( - 'a' => '#__contact_details' - ); - - $title = 'contact'; - break; - - case 'category': - $fields['created_user_id'] = 'a.created_user_id'; - $fields['ordering'] = 'a.lft'; - $fields['level'] = 'a.level'; - $fields['catid'] = ''; - $fields['state'] = 'a.published'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['level'] = true; - - $tables = array( - 'a' => '#__categories' - ); - - $title = 'category'; - break; - } - } - - return array( - 'fields' => $fields, - 'support' => $support, - 'tables' => $tables, - 'joins' => $joins, - 'title' => $title - ); - } + /** + * The extension name + * + * @var array $extension + * + * @since 3.7.0 + */ + protected $extension = 'com_contact'; + + /** + * Array of item types + * + * @var array $itemTypes + * + * @since 3.7.0 + */ + protected $itemTypes = array('contact', 'category'); + + /** + * Has the extension association support + * + * @var boolean $associationsSupport + * + * @since 3.7.0 + */ + protected $associationsSupport = true; + + /** + * Method to get the associations for a given item. + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 4.0.0 + */ + public function getAssociationsForItem($id = 0, $view = null) + { + return AssociationHelper::getAssociations($id, $view); + } + + /** + * Get the associated items for an item + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public function getAssociations($typeName, $id) + { + $type = $this->getType($typeName); + + $context = $this->extension . '.item'; + $catidField = 'catid'; + + if ($typeName === 'category') { + $context = 'com_categories.item'; + $catidField = ''; + } + + // Get the associations. + $associations = Associations::getAssociations( + $this->extension, + $type['tables']['a'], + $context, + $id, + 'id', + 'alias', + $catidField + ); + + return $associations; + } + + /** + * Get item information + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return Table|null + * + * @since 3.7.0 + */ + public function getItem($typeName, $id) + { + if (empty($id)) { + return null; + } + + $table = null; + + switch ($typeName) { + case 'contact': + $table = Table::getInstance('ContactTable', 'Joomla\\Component\\Contact\\Administrator\\Table\\'); + break; + + case 'category': + $table = Table::getInstance('Category'); + break; + } + + if (empty($table)) { + return null; + } + + $table->load($id); + + return $table; + } + + /** + * Get information about the type + * + * @param string $typeName The item type + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getType($typeName = '') + { + $fields = $this->getFieldsTemplate(); + $tables = array(); + $joins = array(); + $support = $this->getSupportTemplate(); + $title = ''; + + if (in_array($typeName, $this->itemTypes)) { + switch ($typeName) { + case 'contact': + $fields['title'] = 'a.name'; + $fields['state'] = 'a.published'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['category'] = true; + $support['save2copy'] = true; + + $tables = array( + 'a' => '#__contact_details' + ); + + $title = 'contact'; + break; + + case 'category': + $fields['created_user_id'] = 'a.created_user_id'; + $fields['ordering'] = 'a.lft'; + $fields['level'] = 'a.level'; + $fields['catid'] = ''; + $fields['state'] = 'a.published'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['level'] = true; + + $tables = array( + 'a' => '#__categories' + ); + + $title = 'category'; + break; + } + } + + return array( + 'fields' => $fields, + 'support' => $support, + 'tables' => $tables, + 'joins' => $joins, + 'title' => $title + ); + } } diff --git a/administrator/components/com_contact/src/Helper/ContactHelper.php b/administrator/components/com_contact/src/Helper/ContactHelper.php index 8928555cc685d..76c7f9240fd9e 100644 --- a/administrator/components/com_contact/src/Helper/ContactHelper.php +++ b/administrator/components/com_contact/src/Helper/ContactHelper.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage', - 'tag' => 'batchTag', - 'user_id' => 'batchUser', - ); - - /** - * Name of the form - * - * @var string - * @since 4.0.0 - */ - protected $formName = 'contact'; - - /** - * Batch change a linked user. - * - * @param integer $value The new value matching a User ID. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - protected function batchUser($value, $pks, $contexts) - { - foreach ($pks as $pk) - { - if ($this->user->authorise('core.edit', $contexts[$pk])) - { - $this->table->reset(); - $this->table->load($pk); - $this->table->user_id = (int) $value; - - if (!$this->table->store()) - { - $this->setError($this->table->getError()); - - return false; - } - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - return Factory::getUser()->authorise('core.delete', 'com_contact.category.' . (int) $record->catid); - } - - /** - * Method to test whether a record can have its state edited. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - // Check against the category. - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.edit.state', 'com_contact.category.' . (int) $record->catid); - } - - // Default to component settings if category not known. - return parent::canEditState($record); - } - - /** - * Method to get the row form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - Form::addFieldPath(JPATH_ADMINISTRATOR . '/components/com_users/models/fields'); - - // Get the form. - $form = $this->loadForm('com_contact.' . $this->formName, $this->formName, array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('featured', 'disabled', 'true'); - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('featured', 'filter', 'unset'); - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - // Don't allow to change the created_by user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_by', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - if ($item = parent::getItem($pk)) - { - // Convert the metadata field to an array. - $registry = new Registry($item->metadata); - $item->metadata = $registry->toArray(); - } - - // Load associated contact items - $assoc = Associations::isEnabled(); - - if ($assoc) - { - $item->associations = array(); - - if ($item->id != null) - { - $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $item->id); - - foreach ($associations as $tag => $association) - { - $item->associations[$tag] = $association->id; - } - } - } - - // Load item tags - if (!empty($item->id)) - { - $item->tags = new TagsHelper; - $item->tags->getTagIds($item->id, 'com_contact.contact'); - } - - return $item; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $app = Factory::getApplication(); - - // Check the session for previously entered form data. - $data = $app->getUserState('com_contact.edit.contact.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Prime some default values. - if ($this->getState('contact.id') == 0) - { - $data->set('catid', $app->input->get('catid', $app->getUserState('com_contact.contacts.filter.category_id'), 'int')); - } - } - - $this->preprocessData('com_contact.contact', $data); - - return $data; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 3.0 - */ - public function save($data) - { - $input = Factory::getApplication()->input; - - // Create new category, if needed. - $createCategory = true; - - // If category ID is provided, check if it's valid. - if (is_numeric($data['catid']) && $data['catid']) - { - $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_contact'); - } - - // Save New Category - if ($createCategory && $this->canCreateCategory()) - { - $category = [ - // Remove #new# prefix, if exists. - 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], - 'parent_id' => 1, - 'extension' => 'com_contact', - 'language' => $data['language'], - 'published' => 1, - ]; - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ - $categoryModel = Factory::getApplication()->bootComponent('com_categories') - ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); - - // Create new category. - if (!$categoryModel->save($category)) - { - $this->setError($categoryModel->getError()); - - return false; - } - - // Get the Category ID. - $data['catid'] = $categoryModel->getState('category.id'); - } - - // Alter the name for save as copy - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['name'] == $origTable->name) - { - list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); - $data['name'] = $name; - $data['alias'] = $alias; - } - else - { - if ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - } - - $data['published'] = 0; - } - - $links = array('linka', 'linkb', 'linkc', 'linkd', 'linke'); - - foreach ($links as $link) - { - if (!empty($data['params'][$link])) - { - $data['params'][$link] = PunycodeHelper::urlToPunycode($data['params'][$link]); - } - } - - return parent::save($data); - } - - /** - * Prepare and sanitise the table prior to saving. - * - * @param \Joomla\CMS\Table\Table $table The Table object - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - $date = Factory::getDate()->toSql(); - - $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); - - $table->generateAlias(); - - if (empty($table->id)) - { - // Set the values - $table->created = $date; - - // Set ordering to the last item if not set - if (empty($table->ordering)) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('MAX(ordering)') - ->from($db->quoteName('#__contact_details')); - $db->setQuery($query); - $max = $db->loadResult(); - - $table->ordering = $max + 1; - } - } - else - { - // Set the values - $table->modified = $date; - $table->modified_by = Factory::getUser()->id; - } - - // Increment the content version number. - $table->version++; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param \Joomla\CMS\Table\Table $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - return [ - $this->getDatabase()->quoteName('catid') . ' = ' . (int) $table->catid, - ]; - } - - /** - * Preprocess the form. - * - * @param Form $form Form object. - * @param object $data Data object. - * @param string $group Group name. - * - * @return void - * - * @since 3.0.3 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - if ($this->canCreateCategory()) - { - $form->setFieldAttribute('catid', 'allowAdd', 'true'); - - // Add a prefix for categories created on the fly. - $form->setFieldAttribute('catid', 'customPrefix', '#new#'); - } - - // Association contact items - if (Associations::isEnabled()) - { - $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); - - if (count($languages) > 1) - { - $addform = new \SimpleXMLElement('
    '); - $fields = $addform->addChild('fields'); - $fields->addAttribute('name', 'associations'); - $fieldset = $fields->addChild('fieldset'); - $fieldset->addAttribute('name', 'item_associations'); - - foreach ($languages as $language) - { - $field = $fieldset->addChild('field'); - $field->addAttribute('name', $language->lang_code); - $field->addAttribute('type', 'modal_contact'); - $field->addAttribute('language', $language->lang_code); - $field->addAttribute('label', $language->title); - $field->addAttribute('translate_label', 'false'); - $field->addAttribute('select', 'true'); - $field->addAttribute('new', 'true'); - $field->addAttribute('edit', 'true'); - $field->addAttribute('clear', 'true'); - $field->addAttribute('propagate', 'true'); - } - - $form->load($addform, false); - } - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to toggle the featured setting of contacts. - * - * @param array $pks The ids of the items to toggle. - * @param integer $value The value to toggle to. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function featured($pks, $value = 0) - { - // Sanitize the ids. - $pks = ArrayHelper::toInteger((array) $pks); - - if (empty($pks)) - { - $this->setError(Text::_('COM_CONTACT_NO_ITEM_SELECTED')); - - return false; - } - - $table = $this->getTable(); - - try - { - $db = $this->getDatabase(); - - $query = $db->getQuery(true); - $query->update($db->quoteName('#__contact_details')); - $query->set($db->quoteName('featured') . ' = :featured'); - $query->whereIn($db->quoteName('id'), $pks); - $query->bind(':featured', $value, ParameterType::INTEGER); - - $db->setQuery($query); - - $db->execute(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $table->reorder(); - - // Clean component's cache - $this->cleanCache(); - - return true; - } - - /** - * Is the user allowed to create an on the fly category? - * - * @return boolean - * - * @since 3.6.1 - */ - private function canCreateCategory() - { - return Factory::getUser()->authorise('core.create', 'com_contact'); - } + use VersionableModelTrait; + + /** + * The type alias for this content type. + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_contact.contact'; + + /** + * The context used for the associations table + * + * @var string + * @since 3.4.4 + */ + protected $associationsContext = 'com_contact.item'; + + /** + * Batch copy/move command. If set to false, the batch copy/move command is not supported + * + * @var string + */ + protected $batch_copymove = 'category_id'; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage', + 'tag' => 'batchTag', + 'user_id' => 'batchUser', + ); + + /** + * Name of the form + * + * @var string + * @since 4.0.0 + */ + protected $formName = 'contact'; + + /** + * Batch change a linked user. + * + * @param integer $value The new value matching a User ID. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + protected function batchUser($value, $pks, $contexts) + { + foreach ($pks as $pk) { + if ($this->user->authorise('core.edit', $contexts[$pk])) { + $this->table->reset(); + $this->table->load($pk); + $this->table->user_id = (int) $value; + + if (!$this->table->store()) { + $this->setError($this->table->getError()); + + return false; + } + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + return Factory::getUser()->authorise('core.delete', 'com_contact.category.' . (int) $record->catid); + } + + /** + * Method to test whether a record can have its state edited. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + // Check against the category. + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.edit.state', 'com_contact.category.' . (int) $record->catid); + } + + // Default to component settings if category not known. + return parent::canEditState($record); + } + + /** + * Method to get the row form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + Form::addFieldPath(JPATH_ADMINISTRATOR . '/components/com_users/models/fields'); + + // Get the form. + $form = $this->loadForm('com_contact.' . $this->formName, $this->formName, array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('featured', 'disabled', 'true'); + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('featured', 'filter', 'unset'); + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + // Don't allow to change the created_by user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_by', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + if ($item = parent::getItem($pk)) { + // Convert the metadata field to an array. + $registry = new Registry($item->metadata); + $item->metadata = $registry->toArray(); + } + + // Load associated contact items + $assoc = Associations::isEnabled(); + + if ($assoc) { + $item->associations = array(); + + if ($item->id != null) { + $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $item->id); + + foreach ($associations as $tag => $association) { + $item->associations[$tag] = $association->id; + } + } + } + + // Load item tags + if (!empty($item->id)) { + $item->tags = new TagsHelper(); + $item->tags->getTagIds($item->id, 'com_contact.contact'); + } + + return $item; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $app = Factory::getApplication(); + + // Check the session for previously entered form data. + $data = $app->getUserState('com_contact.edit.contact.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Prime some default values. + if ($this->getState('contact.id') == 0) { + $data->set('catid', $app->input->get('catid', $app->getUserState('com_contact.contacts.filter.category_id'), 'int')); + } + } + + $this->preprocessData('com_contact.contact', $data); + + return $data; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 3.0 + */ + public function save($data) + { + $input = Factory::getApplication()->input; + + // Create new category, if needed. + $createCategory = true; + + // If category ID is provided, check if it's valid. + if (is_numeric($data['catid']) && $data['catid']) { + $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_contact'); + } + + // Save New Category + if ($createCategory && $this->canCreateCategory()) { + $category = [ + // Remove #new# prefix, if exists. + 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], + 'parent_id' => 1, + 'extension' => 'com_contact', + 'language' => $data['language'], + 'published' => 1, + ]; + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ + $categoryModel = Factory::getApplication()->bootComponent('com_categories') + ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); + + // Create new category. + if (!$categoryModel->save($category)) { + $this->setError($categoryModel->getError()); + + return false; + } + + // Get the Category ID. + $data['catid'] = $categoryModel->getState('category.id'); + } + + // Alter the name for save as copy + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['name'] == $origTable->name) { + list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); + $data['name'] = $name; + $data['alias'] = $alias; + } else { + if ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + } + + $data['published'] = 0; + } + + $links = array('linka', 'linkb', 'linkc', 'linkd', 'linke'); + + foreach ($links as $link) { + if (!empty($data['params'][$link])) { + $data['params'][$link] = PunycodeHelper::urlToPunycode($data['params'][$link]); + } + } + + return parent::save($data); + } + + /** + * Prepare and sanitise the table prior to saving. + * + * @param \Joomla\CMS\Table\Table $table The Table object + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + $date = Factory::getDate()->toSql(); + + $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); + + $table->generateAlias(); + + if (empty($table->id)) { + // Set the values + $table->created = $date; + + // Set ordering to the last item if not set + if (empty($table->ordering)) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('MAX(ordering)') + ->from($db->quoteName('#__contact_details')); + $db->setQuery($query); + $max = $db->loadResult(); + + $table->ordering = $max + 1; + } + } else { + // Set the values + $table->modified = $date; + $table->modified_by = Factory::getUser()->id; + } + + // Increment the content version number. + $table->version++; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param \Joomla\CMS\Table\Table $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + return [ + $this->getDatabase()->quoteName('catid') . ' = ' . (int) $table->catid, + ]; + } + + /** + * Preprocess the form. + * + * @param Form $form Form object. + * @param object $data Data object. + * @param string $group Group name. + * + * @return void + * + * @since 3.0.3 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + if ($this->canCreateCategory()) { + $form->setFieldAttribute('catid', 'allowAdd', 'true'); + + // Add a prefix for categories created on the fly. + $form->setFieldAttribute('catid', 'customPrefix', '#new#'); + } + + // Association contact items + if (Associations::isEnabled()) { + $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); + + if (count($languages) > 1) { + $addform = new \SimpleXMLElement(''); + $fields = $addform->addChild('fields'); + $fields->addAttribute('name', 'associations'); + $fieldset = $fields->addChild('fieldset'); + $fieldset->addAttribute('name', 'item_associations'); + + foreach ($languages as $language) { + $field = $fieldset->addChild('field'); + $field->addAttribute('name', $language->lang_code); + $field->addAttribute('type', 'modal_contact'); + $field->addAttribute('language', $language->lang_code); + $field->addAttribute('label', $language->title); + $field->addAttribute('translate_label', 'false'); + $field->addAttribute('select', 'true'); + $field->addAttribute('new', 'true'); + $field->addAttribute('edit', 'true'); + $field->addAttribute('clear', 'true'); + $field->addAttribute('propagate', 'true'); + } + + $form->load($addform, false); + } + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to toggle the featured setting of contacts. + * + * @param array $pks The ids of the items to toggle. + * @param integer $value The value to toggle to. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function featured($pks, $value = 0) + { + // Sanitize the ids. + $pks = ArrayHelper::toInteger((array) $pks); + + if (empty($pks)) { + $this->setError(Text::_('COM_CONTACT_NO_ITEM_SELECTED')); + + return false; + } + + $table = $this->getTable(); + + try { + $db = $this->getDatabase(); + + $query = $db->getQuery(true); + $query->update($db->quoteName('#__contact_details')); + $query->set($db->quoteName('featured') . ' = :featured'); + $query->whereIn($db->quoteName('id'), $pks); + $query->bind(':featured', $value, ParameterType::INTEGER); + + $db->setQuery($query); + + $db->execute(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $table->reorder(); + + // Clean component's cache + $this->cleanCache(); + + return true; + } + + /** + * Is the user allowed to create an on the fly category? + * + * @return boolean + * + * @since 3.6.1 + */ + private function canCreateCategory() + { + return Factory::getUser()->authorise('core.create', 'com_contact'); + } } diff --git a/administrator/components/com_contact/src/Model/ContactsModel.php b/administrator/components/com_contact/src/Model/ContactsModel.php index f4adaf4746963..d0f8071cc7959 100644 --- a/administrator/components/com_contact/src/Model/ContactsModel.php +++ b/administrator/components/com_contact/src/Model/ContactsModel.php @@ -1,4 +1,5 @@ input->get('forcedLanguage', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language. - if (!empty($forcedLanguage)) - { - $this->setState('filter.language', $forcedLanguage); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . serialize($this->getState('filter.category_id')); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . serialize($this->getState('filter.tag')); - $id .= ':' . $this->getState('filter.level'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - // Select the required fields from the table. - $query->select( - $db->quoteName( - explode( - ', ', - $this->getState( - 'list.select', - 'a.id, a.name, a.alias, a.checked_out, a.checked_out_time, a.catid, a.user_id' . - ', a.published, a.access, a.created, a.created_by, a.ordering, a.featured, a.language' . - ', a.publish_up, a.publish_down' - ) - ) - ) - ); - $query->from($db->quoteName('#__contact_details', 'a')); - - // Join over the users for the linked user. - $query->select( - array( - $db->quoteName('ul.name', 'linked_user'), - $db->quoteName('ul.email') - ) - ) - ->join( - 'LEFT', - $db->quoteName('#__users', 'ul') . ' ON ' . $db->quoteName('ul.id') . ' = ' . $db->quoteName('a.user_id') - ); - - // Join over the language - $query->select($db->quoteName('l.title', 'language_title')) - ->select($db->quoteName('l.image', 'language_image')) - ->join( - 'LEFT', - $db->quoteName('#__languages', 'l') . ' ON ' . $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language') - ); - - // Join over the users for the checked out user. - $query->select($db->quoteName('uc.name', 'editor')) - ->join( - 'LEFT', - $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out') - ); - - // Join over the asset groups. - $query->select($db->quoteName('ag.title', 'access_level')) - ->join( - 'LEFT', - $db->quoteName('#__viewlevels', 'ag') . ' ON ' . $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access') - ); - - // Join over the categories. - $query->select($db->quoteName('c.title', 'category_title')) - ->join( - 'LEFT', - $db->quoteName('#__categories', 'c') . ' ON ' . $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid') - ); - - // Join over the associations. - if (Associations::isEnabled()) - { - $subQuery = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') - ->from($db->quoteName('#__associations', 'asso1')) - ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) - ->where( - [ - $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), - $db->quoteName('asso1.context') . ' = ' . $db->quote('com_contact.item'), - ] - ); - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); - } - - // Filter by featured. - $featured = (string) $this->getState('filter.featured'); - - if (in_array($featured, ['0','1'])) - { - $query->where($db->quoteName('a.featured') . ' = ' . (int) $featured); - } - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access'); - $query->bind(':access', $access, ParameterType::INTEGER); - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - $query->whereIn($db->quoteName('a.access'), $user->getAuthorisedViewLevels()); - } - - // Filter by published state - $published = (string) $this->getState('filter.published'); - - if (is_numeric($published)) - { - $query->where($db->quoteName('a.published') . ' = :published'); - $query->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where('(' . $db->quoteName('a.published') . ' = 0 OR ' . $db->quoteName('a.published') . ' = 1)'); - } - - // Filter by search in name. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search = substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . trim($search) . '%'; - $query->where( - '(' . $db->quoteName('a.name') . ' LIKE :name OR ' . $db->quoteName('a.alias') . ' LIKE :alias)' - ); - $query->bind(':name', $search); - $query->bind(':alias', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language'); - $query->bind(':language', $language); - } - - // Filter by a single or group of tags. - $tag = $this->getState('filter.tag'); - - // Run simplified query when filtering by one tag. - if (\is_array($tag) && \count($tag) === 1) - { - $tag = $tag[0]; - } - - if ($tag && \is_array($tag)) - { - $tag = ArrayHelper::toInteger($tag); - - $subQuery = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('content_item_id')) - ->from($db->quoteName('#__contentitem_tag_map')) - ->where( - [ - $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', - $db->quoteName('type_alias') . ' = ' . $db->quote('com_contact.contact'), - ] - ); - - $query->join( - 'INNER', - '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ); - } - elseif ($tag = (int) $tag) - { - $query->join( - 'INNER', - $db->quoteName('#__contentitem_tag_map', 'tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ) - ->where( - [ - $db->quoteName('tagmap.tag_id') . ' = :tag', - $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_contact.contact'), - ] - ) - ->bind(':tag', $tag, ParameterType::INTEGER); - } - - // Filter by categories and by level - $categoryId = $this->getState('filter.category_id', array()); - $level = $this->getState('filter.level'); - - if (!is_array($categoryId)) - { - $categoryId = $categoryId ? array($categoryId) : array(); - } - - // Case: Using both categories filter and by level filter - if (count($categoryId)) - { - $categoryId = ArrayHelper::toInteger($categoryId); - $categoryTable = Table::getInstance('Category', 'JTable'); - $subCatItemsWhere = array(); - - // @todo: Convert to prepared statement - foreach ($categoryId as $filter_catid) - { - $categoryTable->load($filter_catid); - $subCatItemsWhere[] = '(' . - ($level ? 'c.level <= ' . ((int) $level + (int) $categoryTable->level - 1) . ' AND ' : '') . - 'c.lft >= ' . (int) $categoryTable->lft . ' AND ' . - 'c.rgt <= ' . (int) $categoryTable->rgt . ')'; - } - - $query->where('(' . implode(' OR ', $subCatItemsWhere) . ')'); - } - - // Case: Using only the by level filter - elseif ($level) - { - $query->where($db->quoteName('c.level') . ' <= :level'); - $query->bind(':level', $level, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 'a.name'); - $orderDirn = $this->state->get('list.direction', 'asc'); - - if ($orderCol == 'a.ordering' || $orderCol == 'category_title') - { - $orderCol = $db->quoteName('c.title') . ' ' . $orderDirn . ', ' . $db->quoteName('a.ordering'); - } - - $query->order($db->escape($orderCol . ' ' . $orderDirn)); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'alias', 'a.alias', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'catid', 'a.catid', 'category_id', 'category_title', + 'user_id', 'a.user_id', + 'published', 'a.published', + 'access', 'a.access', 'access_level', + 'created', 'a.created', + 'created_by', 'a.created_by', + 'ordering', 'a.ordering', + 'featured', 'a.featured', + 'language', 'a.language', 'language_title', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'ul.name', 'linked_user', + 'tag', + 'level', 'c.level', + ); + + if (Associations::isEnabled()) { + $config['filter_fields'][] = 'association'; + } + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.name', $direction = 'asc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language. + if (!empty($forcedLanguage)) { + $this->setState('filter.language', $forcedLanguage); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . serialize($this->getState('filter.category_id')); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . serialize($this->getState('filter.tag')); + $id .= ':' . $this->getState('filter.level'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + // Select the required fields from the table. + $query->select( + $db->quoteName( + explode( + ', ', + $this->getState( + 'list.select', + 'a.id, a.name, a.alias, a.checked_out, a.checked_out_time, a.catid, a.user_id' . + ', a.published, a.access, a.created, a.created_by, a.ordering, a.featured, a.language' . + ', a.publish_up, a.publish_down' + ) + ) + ) + ); + $query->from($db->quoteName('#__contact_details', 'a')); + + // Join over the users for the linked user. + $query->select( + array( + $db->quoteName('ul.name', 'linked_user'), + $db->quoteName('ul.email') + ) + ) + ->join( + 'LEFT', + $db->quoteName('#__users', 'ul') . ' ON ' . $db->quoteName('ul.id') . ' = ' . $db->quoteName('a.user_id') + ); + + // Join over the language + $query->select($db->quoteName('l.title', 'language_title')) + ->select($db->quoteName('l.image', 'language_image')) + ->join( + 'LEFT', + $db->quoteName('#__languages', 'l') . ' ON ' . $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language') + ); + + // Join over the users for the checked out user. + $query->select($db->quoteName('uc.name', 'editor')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out') + ); + + // Join over the asset groups. + $query->select($db->quoteName('ag.title', 'access_level')) + ->join( + 'LEFT', + $db->quoteName('#__viewlevels', 'ag') . ' ON ' . $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access') + ); + + // Join over the categories. + $query->select($db->quoteName('c.title', 'category_title')) + ->join( + 'LEFT', + $db->quoteName('#__categories', 'c') . ' ON ' . $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid') + ); + + // Join over the associations. + if (Associations::isEnabled()) { + $subQuery = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') + ->from($db->quoteName('#__associations', 'asso1')) + ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) + ->where( + [ + $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), + $db->quoteName('asso1.context') . ' = ' . $db->quote('com_contact.item'), + ] + ); + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); + } + + // Filter by featured. + $featured = (string) $this->getState('filter.featured'); + + if (in_array($featured, ['0','1'])) { + $query->where($db->quoteName('a.featured') . ' = ' . (int) $featured); + } + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access'); + $query->bind(':access', $access, ParameterType::INTEGER); + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + $query->whereIn($db->quoteName('a.access'), $user->getAuthorisedViewLevels()); + } + + // Filter by published state + $published = (string) $this->getState('filter.published'); + + if (is_numeric($published)) { + $query->where($db->quoteName('a.published') . ' = :published'); + $query->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where('(' . $db->quoteName('a.published') . ' = 0 OR ' . $db->quoteName('a.published') . ' = 1)'); + } + + // Filter by search in name. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search = substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $search, ParameterType::INTEGER); + } else { + $search = '%' . trim($search) . '%'; + $query->where( + '(' . $db->quoteName('a.name') . ' LIKE :name OR ' . $db->quoteName('a.alias') . ' LIKE :alias)' + ); + $query->bind(':name', $search); + $query->bind(':alias', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language'); + $query->bind(':language', $language); + } + + // Filter by a single or group of tags. + $tag = $this->getState('filter.tag'); + + // Run simplified query when filtering by one tag. + if (\is_array($tag) && \count($tag) === 1) { + $tag = $tag[0]; + } + + if ($tag && \is_array($tag)) { + $tag = ArrayHelper::toInteger($tag); + + $subQuery = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('content_item_id')) + ->from($db->quoteName('#__contentitem_tag_map')) + ->where( + [ + $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', + $db->quoteName('type_alias') . ' = ' . $db->quote('com_contact.contact'), + ] + ); + + $query->join( + 'INNER', + '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ); + } elseif ($tag = (int) $tag) { + $query->join( + 'INNER', + $db->quoteName('#__contentitem_tag_map', 'tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ) + ->where( + [ + $db->quoteName('tagmap.tag_id') . ' = :tag', + $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_contact.contact'), + ] + ) + ->bind(':tag', $tag, ParameterType::INTEGER); + } + + // Filter by categories and by level + $categoryId = $this->getState('filter.category_id', array()); + $level = $this->getState('filter.level'); + + if (!is_array($categoryId)) { + $categoryId = $categoryId ? array($categoryId) : array(); + } + + // Case: Using both categories filter and by level filter + if (count($categoryId)) { + $categoryId = ArrayHelper::toInteger($categoryId); + $categoryTable = Table::getInstance('Category', 'JTable'); + $subCatItemsWhere = array(); + + // @todo: Convert to prepared statement + foreach ($categoryId as $filter_catid) { + $categoryTable->load($filter_catid); + $subCatItemsWhere[] = '(' . + ($level ? 'c.level <= ' . ((int) $level + (int) $categoryTable->level - 1) . ' AND ' : '') . + 'c.lft >= ' . (int) $categoryTable->lft . ' AND ' . + 'c.rgt <= ' . (int) $categoryTable->rgt . ')'; + } + + $query->where('(' . implode(' OR ', $subCatItemsWhere) . ')'); + } + + // Case: Using only the by level filter + elseif ($level) { + $query->where($db->quoteName('c.level') . ' <= :level'); + $query->bind(':level', $level, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 'a.name'); + $orderDirn = $this->state->get('list.direction', 'asc'); + + if ($orderCol == 'a.ordering' || $orderCol == 'category_title') { + $orderCol = $db->quoteName('c.title') . ' ' . $orderDirn . ', ' . $db->quoteName('a.ordering'); + } + + $query->order($db->escape($orderCol . ' ' . $orderDirn)); + + return $query; + } } diff --git a/administrator/components/com_contact/src/Service/HTML/AdministratorService.php b/administrator/components/com_contact/src/Service/HTML/AdministratorService.php index 7255f69339d97..9173df2377989 100644 --- a/administrator/components/com_contact/src/Service/HTML/AdministratorService.php +++ b/administrator/components/com_contact/src/Service/HTML/AdministratorService.php @@ -1,4 +1,5 @@ $associated) - { - $associations[$tag] = (int) $associated->id; - } - - // Get the associated contact items - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('c.id'), - $db->quoteName('c.name', 'title'), - $db->quoteName('l.sef', 'lang_sef'), - $db->quoteName('lang_code'), - $db->quoteName('cat.title', 'category_title'), - $db->quoteName('l.image'), - $db->quoteName('l.title', 'language_title'), - ] - ) - ->from($db->quoteName('#__contact_details', 'c')) - ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) - ->whereIn($db->quoteName('c.id'), array_values($associations)) - ->where($db->quoteName('c.id') . ' != :id') - ->bind(':id', $contactid, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $items = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500, $e); - } - - if ($items) - { - $languages = LanguageHelper::getContentLanguages(array(0, 1)); - $content_languages = array_column($languages, 'lang_code'); - - foreach ($items as &$item) - { - if (in_array($item->lang_code, $content_languages)) - { - $text = $item->lang_code; - $url = Route::_('index.php?option=com_contact&task=contact.edit&id=' . (int) $item->id); - $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
    ' - . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
    ' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); - $classes = 'badge bg-secondary'; - - $item->link = '' . $text . '' - . ''; - } - else - { - // Display warning if Content Language is trashed or deleted - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); - } - } - } - - $html = LayoutHelper::render('joomla.content.associations', $items); - } - - return $html; - } - - /** - * Show the featured/not-featured icon. - * - * @param integer $value The featured value. - * @param integer $i Id of the item. - * @param boolean $canChange Whether the value can be changed or not. - * - * @return string The anchor tag to toggle featured/unfeatured contacts. - * - * @since 1.6 - */ - public function featured($value, $i, $canChange = true) - { - // Array of image, task, title, action - $states = array( - 0 => array('unfeatured', 'contacts.featured', 'COM_CONTACT_UNFEATURED', 'JGLOBAL_ITEM_FEATURE'), - 1 => array('featured', 'contacts.unfeatured', 'JFEATURED', 'JGLOBAL_ITEM_UNFEATURE'), - ); - $state = ArrayHelper::getValue($states, (int) $value, $states[1]); - $icon = $state[0] === 'featured' ? 'star featured' : 'circle'; - $onclick = 'onclick="return Joomla.listItemTask(\'cb' . $i . '\',\'' . $state[1] . '\')"'; - $tooltipText = Text::_($state[3]); - - if (!$canChange) - { - $onclick = 'disabled'; - $tooltipText = Text::_($state[2]); - } - - $html = '' - . ''; - - return $html; - } + /** + * Get the associated language flags + * + * @param integer $contactid The item id to search associations + * + * @return string The language HTML + * + * @throws \Exception + */ + public function association($contactid) + { + // Defaults + $html = ''; + + // Get the associations + if ($associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $contactid)) { + foreach ($associations as $tag => $associated) { + $associations[$tag] = (int) $associated->id; + } + + // Get the associated contact items + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('c.id'), + $db->quoteName('c.name', 'title'), + $db->quoteName('l.sef', 'lang_sef'), + $db->quoteName('lang_code'), + $db->quoteName('cat.title', 'category_title'), + $db->quoteName('l.image'), + $db->quoteName('l.title', 'language_title'), + ] + ) + ->from($db->quoteName('#__contact_details', 'c')) + ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) + ->whereIn($db->quoteName('c.id'), array_values($associations)) + ->where($db->quoteName('c.id') . ' != :id') + ->bind(':id', $contactid, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $items = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500, $e); + } + + if ($items) { + $languages = LanguageHelper::getContentLanguages(array(0, 1)); + $content_languages = array_column($languages, 'lang_code'); + + foreach ($items as &$item) { + if (in_array($item->lang_code, $content_languages)) { + $text = $item->lang_code; + $url = Route::_('index.php?option=com_contact&task=contact.edit&id=' . (int) $item->id); + $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
    ' + . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
    ' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); + $classes = 'badge bg-secondary'; + + $item->link = '' . $text . '' + . ''; + } else { + // Display warning if Content Language is trashed or deleted + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); + } + } + } + + $html = LayoutHelper::render('joomla.content.associations', $items); + } + + return $html; + } + + /** + * Show the featured/not-featured icon. + * + * @param integer $value The featured value. + * @param integer $i Id of the item. + * @param boolean $canChange Whether the value can be changed or not. + * + * @return string The anchor tag to toggle featured/unfeatured contacts. + * + * @since 1.6 + */ + public function featured($value, $i, $canChange = true) + { + // Array of image, task, title, action + $states = array( + 0 => array('unfeatured', 'contacts.featured', 'COM_CONTACT_UNFEATURED', 'JGLOBAL_ITEM_FEATURE'), + 1 => array('featured', 'contacts.unfeatured', 'JFEATURED', 'JGLOBAL_ITEM_UNFEATURE'), + ); + $state = ArrayHelper::getValue($states, (int) $value, $states[1]); + $icon = $state[0] === 'featured' ? 'star featured' : 'circle'; + $onclick = 'onclick="return Joomla.listItemTask(\'cb' . $i . '\',\'' . $state[1] . '\')"'; + $tooltipText = Text::_($state[3]); + + if (!$canChange) { + $onclick = 'disabled'; + $tooltipText = Text::_($state[2]); + } + + $html = '' + . ''; + + return $html; + } } diff --git a/administrator/components/com_contact/src/Service/HTML/Icon.php b/administrator/components/com_contact/src/Service/HTML/Icon.php index 07bb27759e342..f6d9e639f703c 100644 --- a/administrator/components/com_contact/src/Service/HTML/Icon.php +++ b/administrator/components/com_contact/src/Service/HTML/Icon.php @@ -1,4 +1,5 @@ userFactory = $userFactory; - } - - /** - * Method to generate a link to the create item page for the given category - * - * @param object $category The category information - * @param Registry $params The item parameters - * @param array $attribs Optional attributes for the link - * - * @return string The HTML markup for the create item link - * - * @since 4.0.0 - */ - public function create($category, $params, $attribs = array()) - { - $uri = Uri::getInstance(); - - $url = 'index.php?option=com_contact&task=contact.add&return=' . base64_encode($uri) . '&id=0&catid=' . $category->id; - - $text = ''; - - if ($params->get('show_icons')) - { - $text .= ''; - } - - $text .= Text::_('COM_CONTACT_NEW_CONTACT'); - - // Add the button classes to the attribs array - if (isset($attribs['class'])) - { - $attribs['class'] .= ' btn btn-primary'; - } - else - { - $attribs['class'] = 'btn btn-primary'; - } - - $button = HTMLHelper::_('link', Route::_($url), $text, $attribs); - - return $button; - } - - /** - * Display an edit icon for the contact. - * - * This icon will not display in a popup window, nor if the contact is trashed. - * Edit access checks must be performed in the calling code. - * - * @param object $contact The contact information - * @param Registry $params The item parameters - * @param array $attribs Optional attributes for the link - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML for the contact edit icon. - * - * @since 4.0.0 - */ - public function edit($contact, $params, $attribs = array(), $legacy = false) - { - $user = Factory::getUser(); - $uri = Uri::getInstance(); - - // Ignore if in a popup window. - if ($params && $params->get('popup')) - { - return ''; - } - - // Ignore if the state is negative (trashed). - if ($contact->published < 0) - { - return ''; - } - - // Show checked_out icon if the contact is checked out by a different user - if (property_exists($contact, 'checked_out') - && property_exists($contact, 'checked_out_time') - && !is_null($contact->checked_out) - && $contact->checked_out !== $user->get('id')) - { - $checkoutUser = $this->userFactory->loadUserById($contact->checked_out); - $date = HTMLHelper::_('date', $contact->checked_out_time); - $tooltip = Text::sprintf('COM_CONTACT_CHECKED_OUT_BY', $checkoutUser->name) - . '
    ' . $date; - - $text = LayoutHelper::render('joomla.content.icons.edit_lock', array('contact' => $contact, 'tooltip' => $tooltip, 'legacy' => $legacy)); - - $attribs['aria-describedby'] = 'editcontact-' . (int) $contact->id; - $output = HTMLHelper::_('link', '#', $text, $attribs); - - return $output; - } - - $contactUrl = RouteHelper::getContactRoute($contact->slug, $contact->catid, $contact->language); - $url = $contactUrl . '&task=contact.edit&id=' . $contact->id . '&return=' . base64_encode($uri); - - if ((int) $contact->published === 0) - { - $tooltip = Text::_('COM_CONTACT_EDIT_UNPUBLISHED_CONTACT'); - } - else - { - $tooltip = Text::_('COM_CONTACT_EDIT_PUBLISHED_CONTACT'); - } - - $nowDate = strtotime(Factory::getDate()); - $icon = $contact->published ? 'edit' : 'eye-slash'; - - if (($contact->publish_up !== null && strtotime($contact->publish_up) > $nowDate) - || ($contact->publish_down !== null && strtotime($contact->publish_down) < $nowDate)) - { - $icon = 'eye-slash'; - } - - $aria_described = 'editcontact-' . (int) $contact->id; - - $text = ''; - $text .= Text::_('JGLOBAL_EDIT'); - $text .= ''; - - $attribs['aria-describedby'] = $aria_described; - $output = HTMLHelper::_('link', Route::_($url), $text, $attribs); - - return $output; - } + /** + * The user factory + * + * @var UserFactoryInterface + * + * @since 4.2.0 + */ + private $userFactory; + + /** + * Service constructor + * + * @param UserFactoryInterface $userFactory The userFactory + * + * @since 4.0.0 + */ + public function __construct(UserFactoryInterface $userFactory) + { + $this->userFactory = $userFactory; + } + + /** + * Method to generate a link to the create item page for the given category + * + * @param object $category The category information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * + * @return string The HTML markup for the create item link + * + * @since 4.0.0 + */ + public function create($category, $params, $attribs = array()) + { + $uri = Uri::getInstance(); + + $url = 'index.php?option=com_contact&task=contact.add&return=' . base64_encode($uri) . '&id=0&catid=' . $category->id; + + $text = ''; + + if ($params->get('show_icons')) { + $text .= ''; + } + + $text .= Text::_('COM_CONTACT_NEW_CONTACT'); + + // Add the button classes to the attribs array + if (isset($attribs['class'])) { + $attribs['class'] .= ' btn btn-primary'; + } else { + $attribs['class'] = 'btn btn-primary'; + } + + $button = HTMLHelper::_('link', Route::_($url), $text, $attribs); + + return $button; + } + + /** + * Display an edit icon for the contact. + * + * This icon will not display in a popup window, nor if the contact is trashed. + * Edit access checks must be performed in the calling code. + * + * @param object $contact The contact information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML for the contact edit icon. + * + * @since 4.0.0 + */ + public function edit($contact, $params, $attribs = array(), $legacy = false) + { + $user = Factory::getUser(); + $uri = Uri::getInstance(); + + // Ignore if in a popup window. + if ($params && $params->get('popup')) { + return ''; + } + + // Ignore if the state is negative (trashed). + if ($contact->published < 0) { + return ''; + } + + // Show checked_out icon if the contact is checked out by a different user + if ( + property_exists($contact, 'checked_out') + && property_exists($contact, 'checked_out_time') + && !is_null($contact->checked_out) + && $contact->checked_out !== $user->get('id') + ) { + $checkoutUser = $this->userFactory->loadUserById($contact->checked_out); + $date = HTMLHelper::_('date', $contact->checked_out_time); + $tooltip = Text::sprintf('COM_CONTACT_CHECKED_OUT_BY', $checkoutUser->name) + . '
    ' . $date; + + $text = LayoutHelper::render('joomla.content.icons.edit_lock', array('contact' => $contact, 'tooltip' => $tooltip, 'legacy' => $legacy)); + + $attribs['aria-describedby'] = 'editcontact-' . (int) $contact->id; + $output = HTMLHelper::_('link', '#', $text, $attribs); + + return $output; + } + + $contactUrl = RouteHelper::getContactRoute($contact->slug, $contact->catid, $contact->language); + $url = $contactUrl . '&task=contact.edit&id=' . $contact->id . '&return=' . base64_encode($uri); + + if ((int) $contact->published === 0) { + $tooltip = Text::_('COM_CONTACT_EDIT_UNPUBLISHED_CONTACT'); + } else { + $tooltip = Text::_('COM_CONTACT_EDIT_PUBLISHED_CONTACT'); + } + + $nowDate = strtotime(Factory::getDate()); + $icon = $contact->published ? 'edit' : 'eye-slash'; + + if ( + ($contact->publish_up !== null && strtotime($contact->publish_up) > $nowDate) + || ($contact->publish_down !== null && strtotime($contact->publish_down) < $nowDate) + ) { + $icon = 'eye-slash'; + } + + $aria_described = 'editcontact-' . (int) $contact->id; + + $text = ''; + $text .= Text::_('JGLOBAL_EDIT'); + $text .= ''; + + $attribs['aria-describedby'] = $aria_described; + $output = HTMLHelper::_('link', Route::_($url), $text, $attribs); + + return $output; + } } diff --git a/administrator/components/com_contact/src/Table/ContactTable.php b/administrator/components/com_contact/src/Table/ContactTable.php index 4748f0ed52217..9728c03590caf 100644 --- a/administrator/components/com_contact/src/Table/ContactTable.php +++ b/administrator/components/com_contact/src/Table/ContactTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_contact.contact'; - - parent::__construct('#__contact_details', 'id', $db); - - $this->setColumnAlias('title', 'name'); - } - - /** - * Stores a contact. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success, false on failure. - * - * @since 1.6 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate()->toSql(); - $userId = Factory::getUser()->id; - - // Set created date if not set. - if (!(int) $this->created) - { - $this->created = $date; - } - - if ($this->id) - { - // Existing item - $this->modified_by = $userId; - $this->modified = $date; - } - else - { - // Field created_by field can be set by the user, so we don't touch it if it's set. - if (empty($this->created_by)) - { - $this->created_by = $userId; - } - - if (!(int) $this->modified) - { - $this->modified = $date; - } - - if (empty($this->modified_by)) - { - $this->modified_by = $userId; - } - } - - // Store utf8 email as punycode - if ($this->email_to !== null) - { - $this->email_to = PunycodeHelper::emailToPunycode($this->email_to); - } - - // Convert IDN urls to punycode - if ($this->webpage !== null) - { - $this->webpage = PunycodeHelper::urlToPunycode($this->webpage); - } - - // Verify that the alias is unique - $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)) - { - $this->setError(Text::_('COM_CONTACT_ERROR_UNIQUE_ALIAS')); - - return false; - } - - return parent::store($updateNulls); - } - - /** - * Overloaded check function - * - * @return boolean True on success, false on failure - * - * @see \JTable::check - * @since 1.5 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $this->default_con = (int) $this->default_con; - - if ($this->webpage !== null && InputFilter::checkAttribute(array('href', $this->webpage))) - { - $this->setError(Text::_('COM_CONTACT_WARNING_PROVIDE_VALID_URL')); - - return false; - } - - // Check for valid name - if (trim($this->name) == '') - { - $this->setError(Text::_('COM_CONTACT_WARNING_PROVIDE_VALID_NAME')); - - return false; - } - - // Generate a valid alias - $this->generateAlias(); - - // Check for a valid category. - if (!$this->catid = (int) $this->catid) - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); - - return false; - } - - // Sanity check for user_id - if (!$this->user_id) - { - $this->user_id = 0; - } - - // Check the publish down date is not earlier than publish up. - if ((int) $this->publish_down > 0 && $this->publish_down < $this->publish_up) - { - $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); - - return false; - } - - if (!$this->id) - { - // Hits must be zero on a new item - $this->hits = 0; - } - - // Clean up description -- eliminate quotes and <> brackets - if (!empty($this->metadesc)) - { - // Only process if not empty - $badCharacters = array("\"", '<', '>'); - $this->metadesc = StringHelper::str_ireplace($badCharacters, '', $this->metadesc); - } - else - { - $this->metadesc = ''; - } - - if (empty($this->params)) - { - $this->params = '{}'; - } - - if (empty($this->metadata)) - { - $this->metadata = '{}'; - } - - // Set publish_up, publish_down to null if not set - if (!$this->publish_up) - { - $this->publish_up = null; - } - - if (!$this->publish_down) - { - $this->publish_down = null; - } - - if (!$this->modified) - { - $this->modified = $this->created; - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - - return true; - } - - /** - * Generate a valid alias from title / date. - * Remains public to be able to check for duplicated alias before saving - * - * @return string - */ - public function generateAlias() - { - if (empty($this->alias)) - { - $this->alias = $this->name; - } - - $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); - - if (trim(str_replace('-', '', $this->alias)) == '') - { - $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); - } - - return $this->alias; - } - - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } + use TaggableTableTrait; + + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Ensure the params and metadata in json encoded in the bind method + * + * @var array + * @since 3.3 + */ + protected $_jsonEncode = array('params', 'metadata'); + + /** + * Constructor + * + * @param DatabaseDriver $db Database connector object + * + * @since 1.0 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_contact.contact'; + + parent::__construct('#__contact_details', 'id', $db); + + $this->setColumnAlias('title', 'name'); + } + + /** + * Stores a contact. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success, false on failure. + * + * @since 1.6 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate()->toSql(); + $userId = Factory::getUser()->id; + + // Set created date if not set. + if (!(int) $this->created) { + $this->created = $date; + } + + if ($this->id) { + // Existing item + $this->modified_by = $userId; + $this->modified = $date; + } else { + // Field created_by field can be set by the user, so we don't touch it if it's set. + if (empty($this->created_by)) { + $this->created_by = $userId; + } + + if (!(int) $this->modified) { + $this->modified = $date; + } + + if (empty($this->modified_by)) { + $this->modified_by = $userId; + } + } + + // Store utf8 email as punycode + if ($this->email_to !== null) { + $this->email_to = PunycodeHelper::emailToPunycode($this->email_to); + } + + // Convert IDN urls to punycode + if ($this->webpage !== null) { + $this->webpage = PunycodeHelper::urlToPunycode($this->webpage); + } + + // Verify that the alias is unique + $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)) { + $this->setError(Text::_('COM_CONTACT_ERROR_UNIQUE_ALIAS')); + + return false; + } + + return parent::store($updateNulls); + } + + /** + * Overloaded check function + * + * @return boolean True on success, false on failure + * + * @see \JTable::check + * @since 1.5 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $this->default_con = (int) $this->default_con; + + if ($this->webpage !== null && InputFilter::checkAttribute(array('href', $this->webpage))) { + $this->setError(Text::_('COM_CONTACT_WARNING_PROVIDE_VALID_URL')); + + return false; + } + + // Check for valid name + if (trim($this->name) == '') { + $this->setError(Text::_('COM_CONTACT_WARNING_PROVIDE_VALID_NAME')); + + return false; + } + + // Generate a valid alias + $this->generateAlias(); + + // Check for a valid category. + if (!$this->catid = (int) $this->catid) { + $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); + + return false; + } + + // Sanity check for user_id + if (!$this->user_id) { + $this->user_id = 0; + } + + // Check the publish down date is not earlier than publish up. + if ((int) $this->publish_down > 0 && $this->publish_down < $this->publish_up) { + $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); + + return false; + } + + if (!$this->id) { + // Hits must be zero on a new item + $this->hits = 0; + } + + // Clean up description -- eliminate quotes and <> brackets + if (!empty($this->metadesc)) { + // Only process if not empty + $badCharacters = array("\"", '<', '>'); + $this->metadesc = StringHelper::str_ireplace($badCharacters, '', $this->metadesc); + } else { + $this->metadesc = ''; + } + + if (empty($this->params)) { + $this->params = '{}'; + } + + if (empty($this->metadata)) { + $this->metadata = '{}'; + } + + // Set publish_up, publish_down to null if not set + if (!$this->publish_up) { + $this->publish_up = null; + } + + if (!$this->publish_down) { + $this->publish_down = null; + } + + if (!$this->modified) { + $this->modified = $this->created; + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + + return true; + } + + /** + * Generate a valid alias from title / date. + * Remains public to be able to check for duplicated alias before saving + * + * @return string + */ + public function generateAlias() + { + if (empty($this->alias)) { + $this->alias = $this->name; + } + + $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); + + if (trim(str_replace('-', '', $this->alias)) == '') { + $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); + } + + return $this->alias; + } + + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } } diff --git a/administrator/components/com_contact/src/View/Contact/HtmlView.php b/administrator/components/com_contact/src/View/Contact/HtmlView.php index c3b55bb0fa0a9..89d5c69517026 100644 --- a/administrator/components/com_contact/src/View/Contact/HtmlView.php +++ b/administrator/components/com_contact/src/View/Contact/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // If we are forcing a language in modal (used for associations). - if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) - { - // Set the language field to the forcedLanguage and disable changing it. - $this->form->setValue('language', null, $forcedLanguage); - $this->form->setFieldAttribute('language', 'readonly', 'true'); - - // Only allow to select categories with All language or with the forced language. - $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); - - // Only allow to select tags with All language or with the forced language. - $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $userId = $user->id; - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Since we don't track these assets at the item level, use the category id. - $canDo = ContentHelper::getActions('com_contact', 'category', $this->item->catid); - - $toolbar = Toolbar::getInstance(); - - ToolbarHelper::title($isNew ? Text::_('COM_CONTACT_MANAGER_CONTACT_NEW') : Text::_('COM_CONTACT_MANAGER_CONTACT_EDIT'), 'address-book contact'); - - // Build the actions for new and existing records. - if ($isNew) - { - // For new records, check the create permission. - if (count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) - { - ToolbarHelper::apply('contact.apply'); - - $saveGroup = $toolbar->dropdownButton('save-group'); - - $saveGroup->configure( - function (Toolbar $childBar) use ($user) - { - $childBar->save('contact.save'); - - if ($user->authorise('core.create', 'com_menus.menu')) - { - $childBar->save('contact.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); - } - - $childBar->save2new('contact.save2new'); - } - ); - } - - ToolbarHelper::cancel('contact.cancel'); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - $toolbar->apply('contact.apply'); - } - - $saveGroup = $toolbar->dropdownButton('save-group'); - - $saveGroup->configure( - function (Toolbar $childBar) use ($checkedOut, $itemEditable, $canDo, $user) - { - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - $childBar->save('contact.save'); - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $childBar->save2new('contact.save2new'); - } - } - - // If checked out, we can still save2menu - if ($user->authorise('core.create', 'com_menus.menu')) - { - $childBar->save('contact.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); - } - - // If checked out, we can still save - if ($canDo->get('core.create')) - { - $childBar->save2copy('contact.save2copy'); - } - } - ); - - ToolbarHelper::cancel('contact.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) - { - ToolbarHelper::versions('com_contact.contact', $this->item->id); - } - - if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) - { - ToolbarHelper::custom('contact.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); - } - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Contacts:_New_or_Edit'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + // Initialise variables. + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // If we are forcing a language in modal (used for associations). + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) { + // Set the language field to the forcedLanguage and disable changing it. + $this->form->setValue('language', null, $forcedLanguage); + $this->form->setFieldAttribute('language', 'readonly', 'true'); + + // Only allow to select categories with All language or with the forced language. + $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); + + // Only allow to select tags with All language or with the forced language. + $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Since we don't track these assets at the item level, use the category id. + $canDo = ContentHelper::getActions('com_contact', 'category', $this->item->catid); + + $toolbar = Toolbar::getInstance(); + + ToolbarHelper::title($isNew ? Text::_('COM_CONTACT_MANAGER_CONTACT_NEW') : Text::_('COM_CONTACT_MANAGER_CONTACT_EDIT'), 'address-book contact'); + + // Build the actions for new and existing records. + if ($isNew) { + // For new records, check the create permission. + if (count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) { + ToolbarHelper::apply('contact.apply'); + + $saveGroup = $toolbar->dropdownButton('save-group'); + + $saveGroup->configure( + function (Toolbar $childBar) use ($user) { + $childBar->save('contact.save'); + + if ($user->authorise('core.create', 'com_menus.menu')) { + $childBar->save('contact.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); + } + + $childBar->save2new('contact.save2new'); + } + ); + } + + ToolbarHelper::cancel('contact.cancel'); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + $toolbar->apply('contact.apply'); + } + + $saveGroup = $toolbar->dropdownButton('save-group'); + + $saveGroup->configure( + function (Toolbar $childBar) use ($checkedOut, $itemEditable, $canDo, $user) { + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + $childBar->save('contact.save'); + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $childBar->save2new('contact.save2new'); + } + } + + // If checked out, we can still save2menu + if ($user->authorise('core.create', 'com_menus.menu')) { + $childBar->save('contact.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); + } + + // If checked out, we can still save + if ($canDo->get('core.create')) { + $childBar->save2copy('contact.save2copy'); + } + } + ); + + ToolbarHelper::cancel('contact.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) { + ToolbarHelper::versions('com_contact.contact', $this->item->id); + } + + if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) { + ToolbarHelper::custom('contact.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); + } + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Contacts:_New_or_Edit'); + } } diff --git a/administrator/components/com_contact/src/View/Contacts/HtmlView.php b/administrator/components/com_contact/src/View/Contacts/HtmlView.php index a03a9a060d153..ba389d6d2b625 100644 --- a/administrator/components/com_contact/src/View/Contacts/HtmlView.php +++ b/administrator/components/com_contact/src/View/Contacts/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Preprocess the list of items to find ordering divisions. - // @todo: Complete the ordering stuff with nested sets - foreach ($this->items as &$item) - { - $item->order_up = true; - $item->order_dn = true; - } - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - else - { - // In article associations modal we need to remove language filter if forcing a language. - // We also need to change the category filter to show show categories with All or the forced language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. - $languageXml = new \SimpleXMLElement(''); - $this->filterForm->setField($languageXml, 'filter', true); - - // Also, unset the active language filter so the search tools is not open by default with this filter. - unset($this->activeFilters['language']); - - // One last changes needed is to change the category filter to just show categories with All language or with the forced language. - $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_contact', 'category', $this->state->get('filter.category_id')); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_CONTACT_MANAGER_CONTACTS'), 'address-book contact'); - - if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) - { - $toolbar->addNew('contact.add'); - } - - if (!$this->isEmptyState && $canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('contacts.publish')->listCheck(true); - - $childBar->unpublish('contacts.unpublish')->listCheck(true); - - $childBar->standardButton('featured') - ->text('JFEATURE') - ->task('contacts.featured') - ->listCheck(true); - $childBar->standardButton('unfeatured') - ->text('JUNFEATURE') - ->task('contacts.unfeatured') - ->listCheck(true); - - $childBar->archive('contacts.archive')->listCheck(true); - - if ($user->authorise('core.admin')) - { - $childBar->checkin('contacts.checkin')->listCheck(true); - } - - if ($this->state->get('filter.published') != -2) - { - $childBar->trash('contacts.trash')->listCheck(true); - } - - // Add a batch button - if ($user->authorise('core.create', 'com_contact') - && $user->authorise('core.edit', 'com_contact') - && $user->authorise('core.edit.state', 'com_contact')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('contacts.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($user->authorise('core.admin', 'com_contact') || $user->authorise('core.options', 'com_contact')) - { - $toolbar->preferences('com_contact'); - } - - $toolbar->help('Contacts'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Preprocess the list of items to find ordering divisions. + // @todo: Complete the ordering stuff with nested sets + foreach ($this->items as &$item) { + $item->order_up = true; + $item->order_dn = true; + } + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // In article associations modal we need to remove language filter if forcing a language. + // We also need to change the category filter to show show categories with All or the forced language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. + $languageXml = new \SimpleXMLElement(''); + $this->filterForm->setField($languageXml, 'filter', true); + + // Also, unset the active language filter so the search tools is not open by default with this filter. + unset($this->activeFilters['language']); + + // One last changes needed is to change the category filter to just show categories with All language or with the forced language. + $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_contact', 'category', $this->state->get('filter.category_id')); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_CONTACT_MANAGER_CONTACTS'), 'address-book contact'); + + if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) { + $toolbar->addNew('contact.add'); + } + + if (!$this->isEmptyState && $canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('contacts.publish')->listCheck(true); + + $childBar->unpublish('contacts.unpublish')->listCheck(true); + + $childBar->standardButton('featured') + ->text('JFEATURE') + ->task('contacts.featured') + ->listCheck(true); + $childBar->standardButton('unfeatured') + ->text('JUNFEATURE') + ->task('contacts.unfeatured') + ->listCheck(true); + + $childBar->archive('contacts.archive')->listCheck(true); + + if ($user->authorise('core.admin')) { + $childBar->checkin('contacts.checkin')->listCheck(true); + } + + if ($this->state->get('filter.published') != -2) { + $childBar->trash('contacts.trash')->listCheck(true); + } + + // Add a batch button + if ( + $user->authorise('core.create', 'com_contact') + && $user->authorise('core.edit', 'com_contact') + && $user->authorise('core.edit.state', 'com_contact') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('contacts.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($user->authorise('core.admin', 'com_contact') || $user->authorise('core.options', 'com_contact')) { + $toolbar->preferences('com_contact'); + } + + $toolbar->help('Contacts'); + } } diff --git a/administrator/components/com_contact/tmpl/contact/edit.php b/administrator/components/com_contact/tmpl/contact/edit.php index 61f2b057ff60b..6798cc6e1cc2d 100644 --- a/administrator/components/com_contact/tmpl/contact/edit.php +++ b/administrator/components/com_contact/tmpl/contact/edit.php @@ -20,7 +20,7 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $input = $app->input; @@ -39,96 +39,96 @@ - - -
    - 'details', 'recall' => true, 'breakpoint' => 768)); ?> - - item->id) ? Text::_('COM_CONTACT_NEW_CONTACT') : Text::_('COM_CONTACT_EDIT_CONTACT')); ?> -
    -
    -
    -
    - form->renderField('user_id'); ?> - form->renderField('image'); ?> - form->renderField('con_position'); ?> - form->renderField('email_to'); ?> - form->renderField('address'); ?> - form->renderField('suburb'); ?> - form->renderField('state'); ?> - form->renderField('postcode'); ?> - form->renderField('country'); ?> -
    -
    - form->renderField('telephone'); ?> - form->renderField('mobile'); ?> - form->renderField('fax'); ?> - form->renderField('webpage'); ?> - form->renderField('sortname1'); ?> - form->renderField('sortname2'); ?> - form->renderField('sortname3'); ?> -
    -
    -
    -
    - -
    -
    - - - -
    -
    -
    - form->getField('misc')->title; ?> -
    - form->getLabel('misc'); ?> - form->getInput('misc'); ?> -
    -
    -
    -
    - - - - - -
    -
    -
    - -
    - -
    -
    -
    -
    -
    - -
    - -
    -
    -
    -
    - - - - -
    - -
    - -
    -
    - - - - - - -
    - - - + + +
    + 'details', 'recall' => true, 'breakpoint' => 768)); ?> + + item->id) ? Text::_('COM_CONTACT_NEW_CONTACT') : Text::_('COM_CONTACT_EDIT_CONTACT')); ?> +
    +
    +
    +
    + form->renderField('user_id'); ?> + form->renderField('image'); ?> + form->renderField('con_position'); ?> + form->renderField('email_to'); ?> + form->renderField('address'); ?> + form->renderField('suburb'); ?> + form->renderField('state'); ?> + form->renderField('postcode'); ?> + form->renderField('country'); ?> +
    +
    + form->renderField('telephone'); ?> + form->renderField('mobile'); ?> + form->renderField('fax'); ?> + form->renderField('webpage'); ?> + form->renderField('sortname1'); ?> + form->renderField('sortname2'); ?> + form->renderField('sortname3'); ?> +
    +
    +
    +
    + +
    +
    + + + +
    +
    +
    + form->getField('misc')->title; ?> +
    + form->getLabel('misc'); ?> + form->getInput('misc'); ?> +
    +
    +
    +
    + + + + + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + + + + +
    + +
    + +
    +
    + + + + + + +
    + + + diff --git a/administrator/components/com_contact/tmpl/contact/modal.php b/administrator/components/com_contact/tmpl/contact/modal.php index cf70ffd379321..04740bf4c752d 100644 --- a/administrator/components/com_contact/tmpl/contact/modal.php +++ b/administrator/components/com_contact/tmpl/contact/modal.php @@ -1,4 +1,5 @@
    - setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
    diff --git a/administrator/components/com_contact/tmpl/contacts/default.php b/administrator/components/com_contact/tmpl/contacts/default.php index e4f20893ee730..cae745e06d33d 100644 --- a/administrator/components/com_contact/tmpl/contacts/default.php +++ b/administrator/components/com_contact/tmpl/contacts/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $userId = $user->get('id'); @@ -30,180 +31,180 @@ $saveOrder = $listOrder == 'a.ordering'; $assoc = Associations::isEnabled(); -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_contact&task=contacts.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_contact&task=contacts.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
    -
    -
    -
    - $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items); - foreach ($this->items as $i => $item) : - $canCreate = $user->authorise('core.create', 'com_contact.category.' . $item->catid); - $canEdit = $user->authorise('core.edit', 'com_contact.category.' . $item->catid); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canEditOwn = $user->authorise('core.edit.own', 'com_contact.category.' . $item->catid) && $item->created_by == $userId; - $canChange = $user->authorise('core.edit.state', 'com_contact.category.' . $item->catid) && $canCheckin; +
    +
    +
    + $this)); ?> + items)) : ?> +
    + + +
    + +
    - , - , - -
    - - - - - - - - - - - - - - - - - - - -
    + + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items); + foreach ($this->items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_contact.category.' . $item->catid); + $canEdit = $user->authorise('core.edit', 'com_contact.category.' . $item->catid); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', 'com_contact.category.' . $item->catid) && $item->created_by == $userId; + $canChange = $user->authorise('core.edit.state', 'com_contact.category.' . $item->catid) && $canCheckin; - $item->cat_link = Route::_('index.php?option=com_categories&extension=com_contact&task=edit&type=other&id=' . $item->catid); - ?> - - - - - + $item->cat_link = Route::_('index.php?option=com_categories&extension=com_contact&task=edit&type=other&id=' . $item->catid); + ?> + + + + + - - - - - - - - - - - - - -
    + , + , + +
    + + + + + + + + + + + + + + + + + + + +
    - id, false, 'cid', 'cb', $item->name); ?> - - - - - - - - - - featured, $i, $canChange); ?> - - published, $i, 'contacts.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> -
    + id, false, 'cid', 'cb', $item->name); ?> + + + + + + + + + + featured, $i, $canChange); ?> + + published, $i, 'contacts.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + -
    - checked_out) : ?> - editor, $item->checked_out_time, 'contacts.', $canCheckin); ?> - - - - escape($item->name); ?> - - escape($item->name); ?> - -
    - escape($item->alias)); ?> -
    -
    - escape($item->category_title); ?> -
    -
    -
    - linked_user)) : ?> - linked_user; ?> -
    email; ?>
    - -
    - access_level; ?> - - association) : ?> - id); ?> - - - - - id; ?> -
    + +
    + checked_out) : ?> + editor, $item->checked_out_time, 'contacts.', $canCheckin); ?> + + + + escape($item->name); ?> + + escape($item->name); ?> + +
    + escape($item->alias)); ?> +
    +
    + escape($item->category_title); ?> +
    +
    + + + linked_user)) : ?> + linked_user; ?> +
    email; ?>
    + + + + access_level; ?> + + + + association) : ?> + id); ?> + + + + + + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_contact') - && $user->authorise('core.edit', 'com_contact') - && $user->authorise('core.edit.state', 'com_contact')) : ?> - Text::_('COM_CONTACT_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - - - - -
    -
    -
    + + authorise('core.create', 'com_contact') + && $user->authorise('core.edit', 'com_contact') + && $user->authorise('core.edit.state', 'com_contact') +) : ?> + Text::_('COM_CONTACT_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + + + + + + +
    diff --git a/administrator/components/com_contact/tmpl/contacts/default_batch_body.php b/administrator/components/com_contact/tmpl/contacts/default_batch_body.php index 4d5bcd3610f7e..101087e40c57d 100644 --- a/administrator/components/com_contact/tmpl/contacts/default_batch_body.php +++ b/administrator/components/com_contact/tmpl/contacts/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Multilanguage; @@ -16,37 +18,37 @@ ?>
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    - = 0) : ?> -
    -
    - 'com_contact']); ?> -
    -
    - -
    -
    - -
    -
    -
    -
    - $noUser]); ?> -
    -
    -
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + = 0) : ?> +
    +
    + 'com_contact']); ?> +
    +
    + +
    +
    + +
    +
    +
    +
    + $noUser]); ?> +
    +
    +
    diff --git a/administrator/components/com_contact/tmpl/contacts/default_batch_footer.php b/administrator/components/com_contact/tmpl/contacts/default_batch_footer.php index 3b64d567c3d67..440ed84e4008b 100644 --- a/administrator/components/com_contact/tmpl/contacts/default_batch_footer.php +++ b/administrator/components/com_contact/tmpl/contacts/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/administrator/components/com_contact/tmpl/contacts/emptystate.php b/administrator/components/com_contact/tmpl/contacts/emptystate.php index cf18d79722ea1..5cfb9038c043d 100644 --- a/administrator/components/com_contact/tmpl/contacts/emptystate.php +++ b/administrator/components/com_contact/tmpl/contacts/emptystate.php @@ -1,4 +1,5 @@ 'COM_CONTACT', - 'formURL' => 'index.php?option=com_contact', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Contacts', - 'icon' => 'icon-address-book contact', + 'textPrefix' => 'COM_CONTACT', + 'formURL' => 'index.php?option=com_contact', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Contacts', + 'icon' => 'icon-address-book contact', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_contact') || count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_contact&task=contact.add'; +if ($user->authorise('core.create', 'com_contact') || count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_contact&task=contact.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_contact/tmpl/contacts/modal.php b/administrator/components/com_contact/tmpl/contacts/modal.php index 72b644fe3ed06..6f5a11153c85c 100644 --- a/administrator/components/com_contact/tmpl/contacts/modal.php +++ b/administrator/components/com_contact/tmpl/contacts/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if ($app->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ @@ -36,128 +36,120 @@ $onclick = $this->escape($function); $multilang = Multilanguage::isEnabled(); -if (!empty($editor)) -{ - // This view is used also in com_menus. Load the xtd script only if the editor is set! - $this->document->addScriptOptions('xtd-contacts', array('editor' => $editor)); - $onclick = "jSelectContact"; +if (!empty($editor)) { + // This view is used also in com_menus. Load the xtd script only if the editor is set! + $this->document->addScriptOptions('xtd-contacts', array('editor' => $editor)); + $onclick = "jSelectContact"; } ?>
    -
    + - $this)); ?> + $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', - ); - ?> - items as $i => $item) : ?> - language && $multilang) - { - $tag = strlen($item->language); - if ($tag == 5) - { - $lang = substr($item->language, 0, 2); - } - elseif ($tag == 6) - { - $lang = substr($item->language, 0, 3); - } - else { - $lang = ''; - } - } - elseif (!$multilang) - { - $lang = ''; - } - ?> - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - -
    - - - - - - escape($item->name); ?> - -
    - escape($item->category_title); ?> -
    -
    - linked_user)) : ?> - linked_user; ?> - - - escape($item->access_level); ?> - - - - id; ?> -
    + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', + ); + ?> + items as $i => $item) : ?> + language && $multilang) { + $tag = strlen($item->language); + if ($tag == 5) { + $lang = substr($item->language, 0, 2); + } elseif ($tag == 6) { + $lang = substr($item->language, 0, 3); + } else { + $lang = ''; + } + } elseif (!$multilang) { + $lang = ''; + } + ?> + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + +
    + + + + + + escape($item->name); ?> + +
    + escape($item->category_title); ?> +
    +
    + linked_user)) : ?> + linked_user; ?> + + + escape($item->access_level); ?> + + + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - + + + -
    +
    diff --git a/administrator/components/com_content/helpers/content.php b/administrator/components/com_content/helpers/content.php index ffa37af26c2b6..78dffbd4fe93a 100644 --- a/administrator/components/com_content/helpers/content.php +++ b/administrator/components/com_content/helpers/content.php @@ -1,4 +1,5 @@ set(AssociationExtensionInterface::class, new AssociationsHelper); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->set(AssociationExtensionInterface::class, new AssociationsHelper()); - $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Content')); - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Content')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Content')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Content')); + $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Content')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Content')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Content')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Content')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new ContentComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new ContentComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); - $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); + $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_content/src/Controller/AjaxController.php b/administrator/components/com_content/src/Controller/AjaxController.php index ddbe1add4a159..9f4c4ae0b8bcc 100644 --- a/administrator/components/com_content/src/Controller/AjaxController.php +++ b/administrator/components/com_content/src/Controller/AjaxController.php @@ -1,4 +1,5 @@ input->getInt('assocId', 0); + /** + * Method to fetch associations of an article + * + * The method assumes that the following http parameters are passed in an Ajax Get request: + * token: the form token + * assocId: the id of the article whose associations are to be returned + * excludeLang: the association for this language is to be excluded + * + * @return null + * + * @since 3.9.0 + */ + public function fetchAssociations() + { + if (!Session::checkToken('get')) { + echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true); + } else { + $assocId = $this->input->getInt('assocId', 0); - if ($assocId == 0) - { - echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); + if ($assocId == 0) { + echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); - return; - } + return; + } - $excludeLang = $this->input->get('excludeLang', '', 'STRING'); + $excludeLang = $this->input->get('excludeLang', '', 'STRING'); - $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', (int) $assocId); + $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', (int) $assocId); - unset($associations[$excludeLang]); + unset($associations[$excludeLang]); - // Add the title to each of the associated records - $contentTable = Table::getInstance('Content', 'JTable'); + // Add the title to each of the associated records + $contentTable = Table::getInstance('Content', 'JTable'); - foreach ($associations as $lang => $association) - { - $contentTable->load($association->id); - $associations[$lang]->title = $contentTable->title; - } + foreach ($associations as $lang => $association) { + $contentTable->load($association->id); + $associations[$lang]->title = $contentTable->title; + } - $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); + $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); - if (count($associations) == 0) - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); - } - elseif ($countContentLanguages > count($associations) + 2) - { - $tags = implode(', ', array_keys($associations)); - $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); - } - else - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); - } + if (count($associations) == 0) { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); + } elseif ($countContentLanguages > count($associations) + 2) { + $tags = implode(', ', array_keys($associations)); + $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); + } else { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); + } - echo new JsonResponse($associations, $message); - } - } + echo new JsonResponse($associations, $message); + } + } } diff --git a/administrator/components/com_content/src/Controller/ArticleController.php b/administrator/components/com_content/src/Controller/ArticleController.php index 4147cb5b7de04..ecc0ef6580b71 100644 --- a/administrator/components/com_content/src/Controller/ArticleController.php +++ b/administrator/components/com_content/src/Controller/ArticleController.php @@ -1,4 +1,5 @@ input->get('return') == 'featured') - { - $this->view_list = 'featured'; - $this->view_item = 'article&return=featured'; - } - } - - /** - * Function that allows child controller access to model data - * after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 4.0.0 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - if ($this->getTask() === 'save2menu') - { - $editState = []; - - $id = $model->getState('article.id'); - - $link = 'index.php?option=com_content&view=article'; - $type = 'component'; - - $editState['id'] = $id; - $editState['link'] = $link; - $editState['title'] = $model->getItem($id)->title; - $editState['type'] = $type; - $editState['request']['id'] = $id; - - $this->app->setUserState('com_menus.edit.item', array( - 'data' => $editState, - 'type' => $type, - 'link' => $link) - ); - - $this->setRedirect(Route::_('index.php?option=com_menus&view=item&client_id=0&menutype=mainmenu&layout=edit', false)); - } - } - - /** - * Method override to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowAdd($data = array()) - { - $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('filter_category_id'), 'int'); - - if ($categoryId) - { - // If the category has been passed in the data or URL check it. - return $this->app->getIdentity()->authorise('core.create', 'com_content.category.' . $categoryId); - } - - // In the absence of better information, revert to the component permissions. - return parent::allowAdd(); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $user = $this->app->getIdentity(); - - // Zero record (id:0), return component edit permission by calling parent controller method - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Check edit on the record asset (explicit or inherited) - if ($user->authorise('core.edit', 'com_content.article.' . $recordId)) - { - return true; - } - - // Check edit own on the record asset (explicit or inherited) - if ($user->authorise('core.edit.own', 'com_content.article.' . $recordId)) - { - // Existing record already has an owner, get it - $record = $this->getModel()->getItem($recordId); - - if (empty($record)) - { - return false; - } - - // Grant if current user is owner of the record - return $user->id == $record->created_by; - } - - return false; - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 1.6 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ - $model = $this->getModel('Article', 'Administrator', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_content&view=articles' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } + use VersionableControllerTrait; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // An article edit form can come from the articles or featured view. + // Adjust the redirect view on the value of 'return' in the request. + if ($this->input->get('return') == 'featured') { + $this->view_list = 'featured'; + $this->view_item = 'article&return=featured'; + } + } + + /** + * Function that allows child controller access to model data + * after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 4.0.0 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + if ($this->getTask() === 'save2menu') { + $editState = []; + + $id = $model->getState('article.id'); + + $link = 'index.php?option=com_content&view=article'; + $type = 'component'; + + $editState['id'] = $id; + $editState['link'] = $link; + $editState['title'] = $model->getItem($id)->title; + $editState['type'] = $type; + $editState['request']['id'] = $id; + + $this->app->setUserState('com_menus.edit.item', array( + 'data' => $editState, + 'type' => $type, + 'link' => $link)); + + $this->setRedirect(Route::_('index.php?option=com_menus&view=item&client_id=0&menutype=mainmenu&layout=edit', false)); + } + } + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('filter_category_id'), 'int'); + + if ($categoryId) { + // If the category has been passed in the data or URL check it. + return $this->app->getIdentity()->authorise('core.create', 'com_content.category.' . $categoryId); + } + + // In the absence of better information, revert to the component permissions. + return parent::allowAdd(); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $user = $this->app->getIdentity(); + + // Zero record (id:0), return component edit permission by calling parent controller method + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Check edit on the record asset (explicit or inherited) + if ($user->authorise('core.edit', 'com_content.article.' . $recordId)) { + return true; + } + + // Check edit own on the record asset (explicit or inherited) + if ($user->authorise('core.edit.own', 'com_content.article.' . $recordId)) { + // Existing record already has an owner, get it + $record = $this->getModel()->getItem($recordId); + + if (empty($record)) { + return false; + } + + // Grant if current user is owner of the record + return $user->id == $record->created_by; + } + + return false; + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 1.6 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ + $model = $this->getModel('Article', 'Administrator', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_content&view=articles' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } } diff --git a/administrator/components/com_content/src/Controller/ArticlesController.php b/administrator/components/com_content/src/Controller/ArticlesController.php index b70165bedd53f..d535383a018c4 100644 --- a/administrator/components/com_content/src/Controller/ArticlesController.php +++ b/administrator/components/com_content/src/Controller/ArticlesController.php @@ -1,4 +1,5 @@ input->get('view') == 'featured') - { - $this->view_list = 'featured'; - } - - $this->registerTask('unfeatured', 'featured'); - } - - /** - * Method to toggle the featured setting of a list of articles. - * - * @return void - * - * @since 1.6 - */ - public function featured() - { - // Check for request forgeries - $this->checkToken(); - - $user = $this->app->getIdentity(); - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('featured' => 1, 'unfeatured' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - $redirectUrl = 'index.php?option=com_content&view=' . $this->view_list . $this->getRedirectToListAppend(); - - // Access checks. - foreach ($ids as $i => $id) - { - // Remove zero value resulting from input filter - if ($id === 0) - { - unset($ids[$i]); - - continue; - } - - if (!$user->authorise('core.edit.state', 'com_content.article.' . (int) $id)) - { - // Prune items that you can't change. - unset($ids[$i]); - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice'); - } - } - - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'error'); - - $this->setRedirect(Route::_($redirectUrl, false)); - - return; - } - - // Get the model. - /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ - $model = $this->getModel(); - - // Publish the items. - if (!$model->featured($ids, $value)) - { - $this->setRedirect(Route::_($redirectUrl, false), $model->getError(), 'error'); - - return; - } - - if ($value == 1) - { - $message = Text::plural('COM_CONTENT_N_ITEMS_FEATURED', count($ids)); - } - else - { - $message = Text::plural('COM_CONTENT_N_ITEMS_UNFEATURED', count($ids)); - } - - $this->setRedirect(Route::_($redirectUrl, false), $message); - } - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config The array of possible config values. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel - * - * @since 1.6 - */ - public function getModel($name = 'Article', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to get the JSON-encoded amount of published articles - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('articles'); - - $model->setState('filter.published', 1); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_CONTENT_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_CONTENT_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // Articles default form can come from the articles or featured view. + // Adjust the redirect view on the value of 'view' in the request. + if ($this->input->get('view') == 'featured') { + $this->view_list = 'featured'; + } + + $this->registerTask('unfeatured', 'featured'); + } + + /** + * Method to toggle the featured setting of a list of articles. + * + * @return void + * + * @since 1.6 + */ + public function featured() + { + // Check for request forgeries + $this->checkToken(); + + $user = $this->app->getIdentity(); + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('featured' => 1, 'unfeatured' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + $redirectUrl = 'index.php?option=com_content&view=' . $this->view_list . $this->getRedirectToListAppend(); + + // Access checks. + foreach ($ids as $i => $id) { + // Remove zero value resulting from input filter + if ($id === 0) { + unset($ids[$i]); + + continue; + } + + if (!$user->authorise('core.edit.state', 'com_content.article.' . (int) $id)) { + // Prune items that you can't change. + unset($ids[$i]); + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice'); + } + } + + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'error'); + + $this->setRedirect(Route::_($redirectUrl, false)); + + return; + } + + // Get the model. + /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ + $model = $this->getModel(); + + // Publish the items. + if (!$model->featured($ids, $value)) { + $this->setRedirect(Route::_($redirectUrl, false), $model->getError(), 'error'); + + return; + } + + if ($value == 1) { + $message = Text::plural('COM_CONTENT_N_ITEMS_FEATURED', count($ids)); + } else { + $message = Text::plural('COM_CONTENT_N_ITEMS_UNFEATURED', count($ids)); + } + + $this->setRedirect(Route::_($redirectUrl, false), $message); + } + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since 1.6 + */ + public function getModel($name = 'Article', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to get the JSON-encoded amount of published articles + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('articles'); + + $model->setState('filter.published', 1); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_CONTENT_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_CONTENT_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } } diff --git a/administrator/components/com_content/src/Controller/DisplayController.php b/administrator/components/com_content/src/Controller/DisplayController.php index a36e2ec75ee16..6292b3c5f9dc0 100644 --- a/administrator/components/com_content/src/Controller/DisplayController.php +++ b/administrator/components/com_content/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'articles'); - $layout = $this->input->get('layout', 'articles'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return BaseController|boolean This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', 'articles'); + $layout = $this->input->get('layout', 'articles'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($view == 'article' && $layout == 'edit' && !$this->checkEditId('com_content.edit.article', $id)) - { - // 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', $id), 'error'); - } + // Check for edit form. + if ($view == 'article' && $layout == 'edit' && !$this->checkEditId('com_content.edit.article', $id)) { + // 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', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_content&view=articles', false)); + $this->setRedirect(Route::_('index.php?option=com_content&view=articles', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/administrator/components/com_content/src/Controller/FeaturedController.php b/administrator/components/com_content/src/Controller/FeaturedController.php index 4f47f55b3bca6..3233b472990af 100644 --- a/administrator/components/com_content/src/Controller/FeaturedController.php +++ b/administrator/components/com_content/src/Controller/FeaturedController.php @@ -1,4 +1,5 @@ checkToken(); + /** + * Removes an item. + * + * @return void + * + * @since 1.6 + */ + public function delete() + { + // Check for request forgeries + $this->checkToken(); - $user = $this->app->getIdentity(); - $ids = (array) $this->input->get('cid', array(), 'int'); + $user = $this->app->getIdentity(); + $ids = (array) $this->input->get('cid', array(), 'int'); - // Access checks. - foreach ($ids as $i => $id) - { - // Remove zero value resulting from input filter - if ($id === 0) - { - unset($ids[$i]); + // Access checks. + foreach ($ids as $i => $id) { + // Remove zero value resulting from input filter + if ($id === 0) { + unset($ids[$i]); - continue; - } + continue; + } - if (!$user->authorise('core.delete', 'com_content.article.' . (int) $id)) - { - // Prune items that you can't delete. - unset($ids[$i]); - $this->app->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'notice'); - } - } + if (!$user->authorise('core.delete', 'com_content.article.' . (int) $id)) { + // Prune items that you can't delete. + unset($ids[$i]); + $this->app->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'notice'); + } + } - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'error'); - } - else - { - /** @var \Joomla\Component\Content\Administrator\Model\FeatureModel $model */ - $model = $this->getModel(); + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'error'); + } else { + /** @var \Joomla\Component\Content\Administrator\Model\FeatureModel $model */ + $model = $this->getModel(); - // Remove the items. - if (!$model->featured($ids, 0)) - { - $this->app->enqueueMessage($model->getError(), 'error'); - } - } + // Remove the items. + if (!$model->featured($ids, 0)) { + $this->app->enqueueMessage($model->getError(), 'error'); + } + } - $this->setRedirect('index.php?option=com_content&view=featured'); - } + $this->setRedirect('index.php?option=com_content&view=featured'); + } - /** - * Method to publish a list of articles. - * - * @return void - * - * @since 1.0 - */ - public function publish() - { - parent::publish(); + /** + * Method to publish a list of articles. + * + * @return void + * + * @since 1.0 + */ + public function publish() + { + parent::publish(); - $this->setRedirect('index.php?option=com_content&view=featured'); - } + $this->setRedirect('index.php?option=com_content&view=featured'); + } - /** - * Method to get a model object, loading it if required. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 1.6 - */ - public function getModel($name = 'Feature', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Feature', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_content/src/Event/Model/FeatureEvent.php b/administrator/components/com_content/src/Event/Model/FeatureEvent.php index eddcede227c72..124150f62d4f1 100644 --- a/administrator/components/com_content/src/Event/Model/FeatureEvent.php +++ b/administrator/components/com_content/src/Event/Model/FeatureEvent.php @@ -1,4 +1,5 @@ name is required but has not been provided"); - } + /** + * Constructor. + * + * @param string $name The event name. + * @param array $arguments The event arguments. + * + * @throws BadMethodCallException + * + * @since 4.0.0 + */ + public function __construct($name, array $arguments = array()) + { + if (!isset($arguments['extension'])) { + throw new BadMethodCallException("Argument 'extension' of event $this->name is required but has not been provided"); + } - if (!isset($arguments['extension']) || !is_string($arguments['extension'])) - { - throw new BadMethodCallException("Argument 'extension' of event $this->name is not of type 'string'"); - } + if (!isset($arguments['extension']) || !is_string($arguments['extension'])) { + throw new BadMethodCallException("Argument 'extension' of event $this->name is not of type 'string'"); + } - if (strpos($arguments['extension'], '.') === false) - { - throw new BadMethodCallException("Argument 'extension' of event $this->name has wrong format. Valid format: 'component.section'"); - } + if (strpos($arguments['extension'], '.') === false) { + throw new BadMethodCallException("Argument 'extension' of event $this->name has wrong format. Valid format: 'component.section'"); + } - if (!\array_key_exists('extensionName', $arguments) || !\array_key_exists('section', $arguments)) - { - $parts = explode('.', $arguments['extension']); + if (!\array_key_exists('extensionName', $arguments) || !\array_key_exists('section', $arguments)) { + $parts = explode('.', $arguments['extension']); - $arguments['extensionName'] = $arguments['extensionName'] ?? $parts[0]; - $arguments['section'] = $arguments['section'] ?? $parts[1]; - } + $arguments['extensionName'] = $arguments['extensionName'] ?? $parts[0]; + $arguments['section'] = $arguments['section'] ?? $parts[1]; + } - if (!isset($arguments['pks']) || !is_array($arguments['pks'])) - { - throw new BadMethodCallException("Argument 'pks' of event $this->name is not of type 'array'"); - } + if (!isset($arguments['pks']) || !is_array($arguments['pks'])) { + throw new BadMethodCallException("Argument 'pks' of event $this->name is not of type 'array'"); + } - if (!isset($arguments['value']) || !is_numeric($arguments['value'])) - { - throw new BadMethodCallException("Argument 'value' of event $this->name is not of type 'numeric'"); - } + if (!isset($arguments['value']) || !is_numeric($arguments['value'])) { + throw new BadMethodCallException("Argument 'value' of event $this->name is not of type 'numeric'"); + } - $arguments['value'] = (int) $arguments['value']; + $arguments['value'] = (int) $arguments['value']; - if ($arguments['value'] !== 0 && $arguments['value'] !== 1) - { - throw new BadMethodCallException("Argument 'value' of event $this->name is not 0 or 1"); - } + if ($arguments['value'] !== 0 && $arguments['value'] !== 1) { + throw new BadMethodCallException("Argument 'value' of event $this->name is not 0 or 1"); + } - parent::__construct($name, $arguments); - } + parent::__construct($name, $arguments); + } - /** - * Set used parameter to true - * - * @param bool $value The value to set - * - * @return void - * - * @since 4.0.0 - */ - public function setAbort(string $reason) - { - $this->arguments['abort'] = true; - $this->arguments['abortReason'] = $reason; - } + /** + * Set used parameter to true + * + * @param bool $value The value to set + * + * @return void + * + * @since 4.0.0 + */ + public function setAbort(string $reason) + { + $this->arguments['abort'] = true; + $this->arguments['abortReason'] = $reason; + } } diff --git a/administrator/components/com_content/src/Extension/ContentComponent.php b/administrator/components/com_content/src/Extension/ContentComponent.php index 3d2a03733eab8..0d9a3cbb8c1d8 100644 --- a/administrator/components/com_content/src/Extension/ContentComponent.php +++ b/administrator/components/com_content/src/Extension/ContentComponent.php @@ -1,4 +1,5 @@ true, - 'core.state' => true, - ]; - - /** - * The trashed condition - * - * @since 4.0.0 - */ - const CONDITION_NAMES = [ - self::CONDITION_PUBLISHED => 'JPUBLISHED', - self::CONDITION_UNPUBLISHED => 'JUNPUBLISHED', - self::CONDITION_ARCHIVED => 'JARCHIVED', - self::CONDITION_TRASHED => 'JTRASHED', - ]; - - /** - * The archived condition - * - * @since 4.0.0 - */ - const CONDITION_ARCHIVED = 2; - - /** - * The published condition - * - * @since 4.0.0 - */ - const CONDITION_PUBLISHED = 1; - - /** - * The unpublished condition - * - * @since 4.0.0 - */ - const CONDITION_UNPUBLISHED = 0; - - /** - * The trashed condition - * - * @since 4.0.0 - */ - const CONDITION_TRASHED = -2; - - /** - * Booting the extension. This is the function to set up the environment of the extension like - * registering new class loaders, etc. - * - * If required, some initial set up can be done from services of the container, eg. - * registering HTML services. - * - * @param ContainerInterface $container The container - * - * @return void - * - * @since 4.0.0 - */ - public function boot(ContainerInterface $container) - { - $this->getRegistry()->register('contentadministrator', new AdministratorService); - $this->getRegistry()->register('contenticon', new Icon); - - // The layout joomla.content.icons does need a general icon service - $this->getRegistry()->register('icon', $this->getRegistry()->getService('contenticon')); - } - - /** - * Returns a valid section for the given section. If it is not valid then null - * is returned. - * - * @param string $section The section to get the mapping for - * @param object $item The item - * - * @return string|null The new section - * - * @since 4.0.0 - */ - public function validateSection($section, $item = null) - { - if (Factory::getApplication()->isClient('site')) - { - // On the front end we need to map some sections - switch ($section) - { - // Editing an article - case 'form': - - // Category list view - case 'featured': - case 'category': - $section = 'article'; - } - } - - if ($section != 'article') - { - // We don't know other sections - return null; - } - - return $section; - } - - /** - * Returns valid contexts - * - * @return array - * - * @since 4.0.0 - */ - public function getContexts(): array - { - Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); - - $contexts = array( - 'com_content.article' => Text::_('COM_CONTENT'), - 'com_content.categories' => Text::_('JCATEGORY') - ); - - return $contexts; - } - - /** - * Returns valid contexts - * - * @return array - * - * @since 4.0.0 - */ - public function getWorkflowContexts(): array - { - Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); - - $contexts = array( - 'com_content.article' => Text::_('COM_CONTENT') - ); - - return $contexts; - } - - /** - * Returns the workflow context based on the given category section - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - public function getCategoryWorkflowContext(?string $section = null): string - { - $context = $this->getWorkflowContexts(); - - return array_key_first($context); - } - - /** - * Returns the table for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getTableNameForSection(string $section = null) - { - return '#__content'; - } - - /** - * Returns a table name for the state association - * - * @param string $section An optional section to separate different areas in the component - * - * @return string - * - * @since 4.0.0 - */ - public function getWorkflowTableBySection(?string $section = null): string - { - return '#__content'; - } - - /** - * Returns the model name, based on the context - * - * @param string $context The context of the workflow - * - * @return string - */ - public function getModelName($context): string - { - $parts = explode('.', $context); - - if (count($parts) < 2) - { - return ''; - } - - array_shift($parts); - - $modelname = array_shift($parts); - - if ($modelname === 'article' && Factory::getApplication()->isClient('site')) - { - return 'Form'; - } - elseif ($modelname === 'featured' && Factory::getApplication()->isClient('administrator')) - { - return 'Article'; - } - - return ucfirst($modelname); - } - - /** - * Method to filter transitions by given id of state. - * - * @param array $transitions The Transitions to filter - * @param int $pk Id of the state - * - * @return array - * - * @since 4.0.0 - */ - public function filterTransitions(array $transitions, int $pk): array - { - return ContentHelper::filterTransitions($transitions, $pk); - } - - /** - * Adds Count Items for Category Manager. - * - * @param \stdClass[] $items The category objects - * @param string $section The section - * - * @return void - * - * @since 4.0.0 - */ - public function countItems(array $items, string $section) - { - $config = (object) array( - 'related_tbl' => 'content', - 'state_col' => 'state', - 'group_col' => 'catid', - 'relation_type' => 'category_or_group', - 'uses_workflows' => true, - 'workflows_component' => 'com_content' - ); - - LibraryContentHelper::countRelations($items, $config); - } - - /** - * Adds Count Items for Tag Manager. - * - * @param \stdClass[] $items The content objects - * @param string $extension The name of the active view. - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function countTagItems(array $items, string $extension) - { - $parts = explode('.', $extension); - $section = count($parts) > 1 ? $parts[1] : null; - - $config = (object) array( - 'related_tbl' => ($section === 'category' ? 'categories' : 'content'), - 'state_col' => ($section === 'category' ? 'published' : 'state'), - 'group_col' => 'tag_id', - 'extension' => $extension, - 'relation_type' => 'tag_assigments', - ); - - LibraryContentHelper::countRelations($items, $config); - } - - /** - * Prepares the category form - * - * @param Form $form The form to prepare - * @param array|object $data The form data - * - * @return void - */ - public function prepareForm(Form $form, $data) - { - ContentHelper::onPrepareForm($form, $data); - } + use AssociationServiceTrait; + use RouterServiceTrait; + use HTMLRegistryAwareTrait; + use WorkflowServiceTrait; + use CategoryServiceTrait, TagServiceTrait { + CategoryServiceTrait::getTableNameForSection insteadof TagServiceTrait; + CategoryServiceTrait::getStateColumnForSection insteadof TagServiceTrait; + } + + /** @var array Supported functionality */ + protected $supportedFunctionality = [ + 'core.featured' => true, + 'core.state' => true, + ]; + + /** + * The trashed condition + * + * @since 4.0.0 + */ + const CONDITION_NAMES = [ + self::CONDITION_PUBLISHED => 'JPUBLISHED', + self::CONDITION_UNPUBLISHED => 'JUNPUBLISHED', + self::CONDITION_ARCHIVED => 'JARCHIVED', + self::CONDITION_TRASHED => 'JTRASHED', + ]; + + /** + * The archived condition + * + * @since 4.0.0 + */ + const CONDITION_ARCHIVED = 2; + + /** + * The published condition + * + * @since 4.0.0 + */ + const CONDITION_PUBLISHED = 1; + + /** + * The unpublished condition + * + * @since 4.0.0 + */ + const CONDITION_UNPUBLISHED = 0; + + /** + * The trashed condition + * + * @since 4.0.0 + */ + const CONDITION_TRASHED = -2; + + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('contentadministrator', new AdministratorService()); + $this->getRegistry()->register('contenticon', new Icon()); + + // The layout joomla.content.icons does need a general icon service + $this->getRegistry()->register('icon', $this->getRegistry()->getService('contenticon')); + } + + /** + * Returns a valid section for the given section. If it is not valid then null + * is returned. + * + * @param string $section The section to get the mapping for + * @param object $item The item + * + * @return string|null The new section + * + * @since 4.0.0 + */ + public function validateSection($section, $item = null) + { + if (Factory::getApplication()->isClient('site')) { + // On the front end we need to map some sections + switch ($section) { + // Editing an article + case 'form': + // Category list view + case 'featured': + case 'category': + $section = 'article'; + } + } + + if ($section != 'article') { + // We don't know other sections + return null; + } + + return $section; + } + + /** + * Returns valid contexts + * + * @return array + * + * @since 4.0.0 + */ + public function getContexts(): array + { + Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); + + $contexts = array( + 'com_content.article' => Text::_('COM_CONTENT'), + 'com_content.categories' => Text::_('JCATEGORY') + ); + + return $contexts; + } + + /** + * Returns valid contexts + * + * @return array + * + * @since 4.0.0 + */ + public function getWorkflowContexts(): array + { + Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); + + $contexts = array( + 'com_content.article' => Text::_('COM_CONTENT') + ); + + return $contexts; + } + + /** + * Returns the workflow context based on the given category section + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + public function getCategoryWorkflowContext(?string $section = null): string + { + $context = $this->getWorkflowContexts(); + + return array_key_first($context); + } + + /** + * Returns the table for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getTableNameForSection(string $section = null) + { + return '#__content'; + } + + /** + * Returns a table name for the state association + * + * @param string $section An optional section to separate different areas in the component + * + * @return string + * + * @since 4.0.0 + */ + public function getWorkflowTableBySection(?string $section = null): string + { + return '#__content'; + } + + /** + * Returns the model name, based on the context + * + * @param string $context The context of the workflow + * + * @return string + */ + public function getModelName($context): string + { + $parts = explode('.', $context); + + if (count($parts) < 2) { + return ''; + } + + array_shift($parts); + + $modelname = array_shift($parts); + + if ($modelname === 'article' && Factory::getApplication()->isClient('site')) { + return 'Form'; + } elseif ($modelname === 'featured' && Factory::getApplication()->isClient('administrator')) { + return 'Article'; + } + + return ucfirst($modelname); + } + + /** + * Method to filter transitions by given id of state. + * + * @param array $transitions The Transitions to filter + * @param int $pk Id of the state + * + * @return array + * + * @since 4.0.0 + */ + public function filterTransitions(array $transitions, int $pk): array + { + return ContentHelper::filterTransitions($transitions, $pk); + } + + /** + * Adds Count Items for Category Manager. + * + * @param \stdClass[] $items The category objects + * @param string $section The section + * + * @return void + * + * @since 4.0.0 + */ + public function countItems(array $items, string $section) + { + $config = (object) array( + 'related_tbl' => 'content', + 'state_col' => 'state', + 'group_col' => 'catid', + 'relation_type' => 'category_or_group', + 'uses_workflows' => true, + 'workflows_component' => 'com_content' + ); + + LibraryContentHelper::countRelations($items, $config); + } + + /** + * Adds Count Items for Tag Manager. + * + * @param \stdClass[] $items The content objects + * @param string $extension The name of the active view. + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function countTagItems(array $items, string $extension) + { + $parts = explode('.', $extension); + $section = count($parts) > 1 ? $parts[1] : null; + + $config = (object) array( + 'related_tbl' => ($section === 'category' ? 'categories' : 'content'), + 'state_col' => ($section === 'category' ? 'published' : 'state'), + 'group_col' => 'tag_id', + 'extension' => $extension, + 'relation_type' => 'tag_assigments', + ); + + LibraryContentHelper::countRelations($items, $config); + } + + /** + * Prepares the category form + * + * @param Form $form The form to prepare + * @param array|object $data The form data + * + * @return void + */ + public function prepareForm(Form $form, $data) + { + ContentHelper::onPrepareForm($form, $data); + } } diff --git a/administrator/components/com_content/src/Field/AssocField.php b/administrator/components/com_content/src/Field/AssocField.php index 7746abeaa8f1b..a9f915b95ba5c 100644 --- a/administrator/components/com_content/src/Field/AssocField.php +++ b/administrator/components/com_content/src/Field/AssocField.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see AssocField::setup() - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - if (!Associations::isEnabled()) - { - return false; - } + /** + * Method to attach a Form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see AssocField::setup() + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + if (!Associations::isEnabled()) { + return false; + } - return parent::setup($element, $value, $group); - } + return parent::setup($element, $value, $group); + } } diff --git a/administrator/components/com_content/src/Field/Modal/ArticleField.php b/administrator/components/com_content/src/Field/Modal/ArticleField.php index d37a1c3296a67..55cf20ed29b5a 100644 --- a/administrator/components/com_content/src/Field/Modal/ArticleField.php +++ b/administrator/components/com_content/src/Field/Modal/ArticleField.php @@ -1,4 +1,5 @@ element['new'] == 'true'); - $allowEdit = ((string) $this->element['edit'] == 'true'); - $allowClear = ((string) $this->element['clear'] != 'false'); - $allowSelect = ((string) $this->element['select'] != 'false'); - $allowPropagate = ((string) $this->element['propagate'] == 'true'); - - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - - // Load language - Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); - - // The active article id field. - $value = (int) $this->value ?: ''; - - // Create the modal id. - $modalId = 'Article_' . $this->id; - - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - - // Add the modal field script to the document head. - $wa->useScript('field.modal-fields'); - - // Script to proxy the select modal function to the modal-fields.js file. - if ($allowSelect) - { - static $scriptSelect = null; - - if (is_null($scriptSelect)) - { - $scriptSelect = array(); - } - - if (!isset($scriptSelect[$this->id])) - { - $wa->addInlineScript(" + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'Modal_Article'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $allowNew = ((string) $this->element['new'] == 'true'); + $allowEdit = ((string) $this->element['edit'] == 'true'); + $allowClear = ((string) $this->element['clear'] != 'false'); + $allowSelect = ((string) $this->element['select'] != 'false'); + $allowPropagate = ((string) $this->element['propagate'] == 'true'); + + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + + // Load language + Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); + + // The active article id field. + $value = (int) $this->value ?: ''; + + // Create the modal id. + $modalId = 'Article_' . $this->id; + + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + + // Add the modal field script to the document head. + $wa->useScript('field.modal-fields'); + + // Script to proxy the select modal function to the modal-fields.js file. + if ($allowSelect) { + static $scriptSelect = null; + + if (is_null($scriptSelect)) { + $scriptSelect = array(); + } + + if (!isset($scriptSelect[$this->id])) { + $wa->addInlineScript( + " window.jSelectArticle_" . $this->id . " = function (id, title, catid, object, url, language) { window.processModalSelect('Article', '" . $this->id . "', id, title, catid, object, url, language); }", - [], - ['type' => 'module'] - ); - - Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); - - $scriptSelect[$this->id] = true; - } - } - - // Setup variables for display. - $linkArticles = 'index.php?option=com_content&view=articles&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - $linkArticle = 'index.php?option=com_content&view=article&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - - if (isset($this->element['language'])) - { - $linkArticles .= '&forcedLanguage=' . $this->element['language']; - $linkArticle .= '&forcedLanguage=' . $this->element['language']; - $modalTitle = Text::_('COM_CONTENT_SELECT_AN_ARTICLE') . ' — ' . $this->element['label']; - } - else - { - $modalTitle = Text::_('COM_CONTENT_SELECT_AN_ARTICLE'); - } - - $urlSelect = $linkArticles . '&function=jSelectArticle_' . $this->id; - $urlEdit = $linkArticle . '&task=article.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; - $urlNew = $linkArticle . '&task=article.add'; - - if ($value) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__content')) - ->where($db->quoteName('id') . ' = :value') - ->bind(':value', $value, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $title = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - $title = empty($title) ? Text::_('COM_CONTENT_SELECT_AN_ARTICLE') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); - - // The current article display field. - $html = ''; - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - $html .= ''; - - // Select article button - if ($allowSelect) - { - $html .= '' - . ' ' . Text::_('JSELECT') - . ''; - } - - // New article button - if ($allowNew) - { - $html .= '' - . ' ' . Text::_('JACTION_CREATE') - . ''; - } - - // Edit article button - if ($allowEdit) - { - $html .= '' - . ' ' . Text::_('JACTION_EDIT') - . ''; - } - - // Clear article button - if ($allowClear) - { - $html .= '' - . ' ' . Text::_('JCLEAR') - . ''; - } - - // Propagate article button - if ($allowPropagate && count($languages) > 2) - { - // Strip off language tag at the end - $tagLength = (int) strlen($this->element['language']); - $callbackFunctionStem = substr("jSelectArticle_" . $this->id, 0, -$tagLength); - - $html .= '' - . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') - . ''; - } - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - // Select article modal - if ($allowSelect) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalSelect' . $modalId, - array( - 'title' => $modalTitle, - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - } - - // New article modal - if ($allowNew) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalNew' . $modalId, - array( - 'title' => Text::_('COM_CONTENT_NEW_ARTICLE'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlNew, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Edit article modal - if ($allowEdit) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalEdit' . $modalId, - array( - 'title' => Text::_('COM_CONTENT_EDIT_ARTICLE'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlEdit, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Note: class='required' for client side validation. - $class = $this->required ? ' class="required modal-value"' : ''; - - $html .= ''; - - return $html; - } - - /** - * Method to get the field label markup. - * - * @return string The field label markup. - * - * @since 3.4 - */ - protected function getLabel() - { - return str_replace($this->id, $this->id . '_name', parent::getLabel()); - } + [], + ['type' => 'module'] + ); + + Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); + + $scriptSelect[$this->id] = true; + } + } + + // Setup variables for display. + $linkArticles = 'index.php?option=com_content&view=articles&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + $linkArticle = 'index.php?option=com_content&view=article&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + + if (isset($this->element['language'])) { + $linkArticles .= '&forcedLanguage=' . $this->element['language']; + $linkArticle .= '&forcedLanguage=' . $this->element['language']; + $modalTitle = Text::_('COM_CONTENT_SELECT_AN_ARTICLE') . ' — ' . $this->element['label']; + } else { + $modalTitle = Text::_('COM_CONTENT_SELECT_AN_ARTICLE'); + } + + $urlSelect = $linkArticles . '&function=jSelectArticle_' . $this->id; + $urlEdit = $linkArticle . '&task=article.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; + $urlNew = $linkArticle . '&task=article.add'; + + if ($value) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__content')) + ->where($db->quoteName('id') . ' = :value') + ->bind(':value', $value, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $title = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + $title = empty($title) ? Text::_('COM_CONTENT_SELECT_AN_ARTICLE') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + + // The current article display field. + $html = ''; + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + $html .= ''; + + // Select article button + if ($allowSelect) { + $html .= '' + . ' ' . Text::_('JSELECT') + . ''; + } + + // New article button + if ($allowNew) { + $html .= '' + . ' ' . Text::_('JACTION_CREATE') + . ''; + } + + // Edit article button + if ($allowEdit) { + $html .= '' + . ' ' . Text::_('JACTION_EDIT') + . ''; + } + + // Clear article button + if ($allowClear) { + $html .= '' + . ' ' . Text::_('JCLEAR') + . ''; + } + + // Propagate article button + if ($allowPropagate && count($languages) > 2) { + // Strip off language tag at the end + $tagLength = (int) strlen($this->element['language']); + $callbackFunctionStem = substr("jSelectArticle_" . $this->id, 0, -$tagLength); + + $html .= '' + . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') + . ''; + } + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + // Select article modal + if ($allowSelect) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalSelect' . $modalId, + array( + 'title' => $modalTitle, + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + } + + // New article modal + if ($allowNew) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalNew' . $modalId, + array( + 'title' => Text::_('COM_CONTENT_NEW_ARTICLE'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlNew, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Edit article modal + if ($allowEdit) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalEdit' . $modalId, + array( + 'title' => Text::_('COM_CONTENT_EDIT_ARTICLE'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlEdit, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Note: class='required' for client side validation. + $class = $this->required ? ' class="required modal-value"' : ''; + + $html .= ''; + + return $html; + } + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + * + * @since 3.4 + */ + protected function getLabel() + { + return str_replace($this->id, $this->id . '_name', parent::getLabel()); + } } diff --git a/administrator/components/com_content/src/Field/VotelistField.php b/administrator/components/com_content/src/Field/VotelistField.php index 5aa280b44cd76..5e96a4d63afd2 100644 --- a/administrator/components/com_content/src/Field/VotelistField.php +++ b/administrator/components/com_content/src/Field/VotelistField.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - // Requires vote plugin enabled - if (!PluginHelper::isEnabled('content', 'vote')) - { - return false; - } + /** + * Method to attach a form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + // Requires vote plugin enabled + if (!PluginHelper::isEnabled('content', 'vote')) { + return false; + } - return parent::setup($element, $value, $group); - } + return parent::setup($element, $value, $group); + } } diff --git a/administrator/components/com_content/src/Field/VoteradioField.php b/administrator/components/com_content/src/Field/VoteradioField.php index c9e8bd696e9d3..677436902ca7b 100644 --- a/administrator/components/com_content/src/Field/VoteradioField.php +++ b/administrator/components/com_content/src/Field/VoteradioField.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - // Requires vote plugin enabled - if (!PluginHelper::isEnabled('content', 'vote')) - { - return false; - } + /** + * Method to attach a form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + // Requires vote plugin enabled + if (!PluginHelper::isEnabled('content', 'vote')) { + return false; + } - return parent::setup($element, $value, $group); - } + return parent::setup($element, $value, $group); + } } diff --git a/administrator/components/com_content/src/Helper/AssociationsHelper.php b/administrator/components/com_content/src/Helper/AssociationsHelper.php index 4640ae4aafba7..dfd0b2685d0fb 100644 --- a/administrator/components/com_content/src/Helper/AssociationsHelper.php +++ b/administrator/components/com_content/src/Helper/AssociationsHelper.php @@ -1,4 +1,5 @@ getType($typeName); - - $context = $this->extension . '.item'; - $catidField = 'catid'; - - if ($typeName === 'category') - { - $context = 'com_categories.item'; - $catidField = ''; - } - - // Get the associations. - $associations = Associations::getAssociations( - $this->extension, - $type['tables']['a'], - $context, - $id, - 'id', - 'alias', - $catidField - ); - - return $associations; - } - - /** - * Get item information - * - * @param string $typeName The item type - * @param int $id The id of item for which we need the associated items - * - * @return Table|null - * - * @since 3.7.0 - */ - public function getItem($typeName, $id) - { - if (empty($id)) - { - return null; - } - - $table = null; - - switch ($typeName) - { - case 'article': - $table = Table::getInstance('Content'); - break; - - case 'category': - $table = Table::getInstance('Category'); - break; - } - - if (is_null($table)) - { - return null; - } - - $table->load($id); - - return $table; - } - - /** - * Get information about the type - * - * @param string $typeName The item type - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getType($typeName = '') - { - $fields = $this->getFieldsTemplate(); - $tables = array(); - $joins = array(); - $support = $this->getSupportTemplate(); - $title = ''; - - if (in_array($typeName, $this->itemTypes)) - { - switch ($typeName) - { - case 'article': - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['category'] = true; - $support['save2copy'] = true; - - $tables = array( - 'a' => '#__content' - ); - - $title = 'article'; - break; - - case 'category': - $fields['created_user_id'] = 'a.created_user_id'; - $fields['ordering'] = 'a.lft'; - $fields['level'] = 'a.level'; - $fields['catid'] = ''; - $fields['state'] = 'a.published'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['level'] = true; - - $tables = array( - 'a' => '#__categories' - ); - - $title = 'category'; - break; - } - } - - return array( - 'fields' => $fields, - 'support' => $support, - 'tables' => $tables, - 'joins' => $joins, - 'title' => $title - ); - } + /** + * The extension name + * + * @var array $extension + * + * @since 3.7.0 + */ + protected $extension = 'com_content'; + + /** + * Array of item types + * + * @var array $itemTypes + * + * @since 3.7.0 + */ + protected $itemTypes = array('article', 'category'); + + /** + * Has the extension association support + * + * @var boolean $associationsSupport + * + * @since 3.7.0 + */ + protected $associationsSupport = true; + + /** + * Method to get the associations for a given item. + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 4.0.0 + */ + public function getAssociationsForItem($id = 0, $view = null) + { + return AssociationHelper::getAssociations($id, $view); + } + + /** + * Get the associated items for an item + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public function getAssociations($typeName, $id) + { + $type = $this->getType($typeName); + + $context = $this->extension . '.item'; + $catidField = 'catid'; + + if ($typeName === 'category') { + $context = 'com_categories.item'; + $catidField = ''; + } + + // Get the associations. + $associations = Associations::getAssociations( + $this->extension, + $type['tables']['a'], + $context, + $id, + 'id', + 'alias', + $catidField + ); + + return $associations; + } + + /** + * Get item information + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return Table|null + * + * @since 3.7.0 + */ + public function getItem($typeName, $id) + { + if (empty($id)) { + return null; + } + + $table = null; + + switch ($typeName) { + case 'article': + $table = Table::getInstance('Content'); + break; + + case 'category': + $table = Table::getInstance('Category'); + break; + } + + if (is_null($table)) { + return null; + } + + $table->load($id); + + return $table; + } + + /** + * Get information about the type + * + * @param string $typeName The item type + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getType($typeName = '') + { + $fields = $this->getFieldsTemplate(); + $tables = array(); + $joins = array(); + $support = $this->getSupportTemplate(); + $title = ''; + + if (in_array($typeName, $this->itemTypes)) { + switch ($typeName) { + case 'article': + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['category'] = true; + $support['save2copy'] = true; + + $tables = array( + 'a' => '#__content' + ); + + $title = 'article'; + break; + + case 'category': + $fields['created_user_id'] = 'a.created_user_id'; + $fields['ordering'] = 'a.lft'; + $fields['level'] = 'a.level'; + $fields['catid'] = ''; + $fields['state'] = 'a.published'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['level'] = true; + + $tables = array( + 'a' => '#__categories' + ); + + $title = 'category'; + break; + } + } + + return array( + 'fields' => $fields, + 'support' => $support, + 'tables' => $tables, + 'joins' => $joins, + 'title' => $title + ); + } } diff --git a/administrator/components/com_content/src/Helper/ContentHelper.php b/administrator/components/com_content/src/Helper/ContentHelper.php index 3d26fc915fc6d..b94ea332856cd 100644 --- a/administrator/components/com_content/src/Helper/ContentHelper.php +++ b/administrator/components/com_content/src/Helper/ContentHelper.php @@ -1,4 +1,5 @@ getQuery(true); - - $query->select('id') - ->from($db->quoteName('#__content')) - ->where($db->quoteName('state') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $states = $db->loadResult(); - - return empty($states); - } - - /** - * Method to filter transitions by given id of state - * - * @param array $transitions Array of transitions - * @param int $pk Id of state - * @param int $workflowId Id of the workflow - * - * @return array - * - * @since 4.0.0 - */ - public static function filterTransitions(array $transitions, int $pk, int $workflowId = 0): array - { - return array_values( - array_filter( - $transitions, - function ($var) use ($pk, $workflowId) - { - return in_array($var['from_stage_id'], [-1, $pk]) && $workflowId == $var['workflow_id']; - } - ) - ); - } - - /** - * Prepares a form - * - * @param Form $form The form to change - * @param array|object $data The form data - * - * @return void - */ - public static function onPrepareForm(Form $form, $data) - { - if ($form->getName() != 'com_categories.categorycom_content') - { - return; - } - - $db = Factory::getDbo(); - - $data = (array) $data; - - // Make workflows translatable - Factory::getLanguage()->load('com_workflow', JPATH_ADMINISTRATOR); - - $form->setFieldAttribute('workflow_id', 'default', 'inherit'); - - $component = Factory::getApplication()->bootComponent('com_content'); - - if (!$component instanceof WorkflowServiceInterface - || !$component->isWorkflowActive('com_content.article')) - { - $form->removeField('workflow_id', 'params'); - - return; - } - - $query = $db->getQuery(true); - - $query->select($db->quoteName('title')) - ->from($db->quoteName('#__workflows')) - ->where( - [ - $db->quoteName('default') . ' = 1', - $db->quoteName('published') . ' = 1', - $db->quoteName('extension') . ' = ' . $db->quote('com_content.article'), - ] - ); - - $defaulttitle = $db->setQuery($query)->loadResult(); - - $option = Text::_('COM_WORKFLOW_INHERIT_WORKFLOW_NEW'); - - if (!empty($data['id'])) - { - $category = new Category($db); - - $categories = $category->getPath((int) $data['id']); - - // Remove the current category, because we search for inheritance from parent. - array_pop($categories); - - $option = Text::sprintf('COM_WORKFLOW_INHERIT_WORKFLOW', Text::_($defaulttitle)); - - if (!empty($categories)) - { - $categories = array_reverse($categories); - - $query = $db->getQuery(true); - - $query->select($db->quoteName('title')) - ->from($db->quoteName('#__workflows')) - ->where( - [ - $db->quoteName('id') . ' = :workflowId', - $db->quoteName('published') . ' = 1', - $db->quoteName('extension') . ' = ' . $db->quote('com_content.article'), - ] - ) - ->bind(':workflowId', $workflow_id, ParameterType::INTEGER); - - $db->setQuery($query); - - foreach ($categories as $cat) - { - $cat->params = new Registry($cat->params); - - $workflow_id = $cat->params->get('workflow_id'); - - if ($workflow_id == 'inherit') - { - continue; - } - elseif ($workflow_id == 'use_default') - { - break; - } - elseif ($workflow_id = (int) $workflow_id) - { - $title = $db->loadResult(); - - if (!is_null($title)) - { - $option = Text::sprintf('COM_WORKFLOW_INHERIT_WORKFLOW', Text::_($title)); - - break; - } - } - } - } - } + /** + * Check if state can be deleted + * + * @param int $id Id of state to delete + * + * @return boolean + * + * @since 4.0.0 + */ + public static function canDeleteState(int $id): bool + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->select('id') + ->from($db->quoteName('#__content')) + ->where($db->quoteName('state') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $states = $db->loadResult(); + + return empty($states); + } + + /** + * Method to filter transitions by given id of state + * + * @param array $transitions Array of transitions + * @param int $pk Id of state + * @param int $workflowId Id of the workflow + * + * @return array + * + * @since 4.0.0 + */ + public static function filterTransitions(array $transitions, int $pk, int $workflowId = 0): array + { + return array_values( + array_filter( + $transitions, + function ($var) use ($pk, $workflowId) { + return in_array($var['from_stage_id'], [-1, $pk]) && $workflowId == $var['workflow_id']; + } + ) + ); + } + + /** + * Prepares a form + * + * @param Form $form The form to change + * @param array|object $data The form data + * + * @return void + */ + public static function onPrepareForm(Form $form, $data) + { + if ($form->getName() != 'com_categories.categorycom_content') { + return; + } + + $db = Factory::getDbo(); + + $data = (array) $data; + + // Make workflows translatable + Factory::getLanguage()->load('com_workflow', JPATH_ADMINISTRATOR); + + $form->setFieldAttribute('workflow_id', 'default', 'inherit'); + + $component = Factory::getApplication()->bootComponent('com_content'); + + if ( + !$component instanceof WorkflowServiceInterface + || !$component->isWorkflowActive('com_content.article') + ) { + $form->removeField('workflow_id', 'params'); + + return; + } + + $query = $db->getQuery(true); + + $query->select($db->quoteName('title')) + ->from($db->quoteName('#__workflows')) + ->where( + [ + $db->quoteName('default') . ' = 1', + $db->quoteName('published') . ' = 1', + $db->quoteName('extension') . ' = ' . $db->quote('com_content.article'), + ] + ); + + $defaulttitle = $db->setQuery($query)->loadResult(); + + $option = Text::_('COM_WORKFLOW_INHERIT_WORKFLOW_NEW'); + + if (!empty($data['id'])) { + $category = new Category($db); + + $categories = $category->getPath((int) $data['id']); + + // Remove the current category, because we search for inheritance from parent. + array_pop($categories); + + $option = Text::sprintf('COM_WORKFLOW_INHERIT_WORKFLOW', Text::_($defaulttitle)); + + if (!empty($categories)) { + $categories = array_reverse($categories); + + $query = $db->getQuery(true); + + $query->select($db->quoteName('title')) + ->from($db->quoteName('#__workflows')) + ->where( + [ + $db->quoteName('id') . ' = :workflowId', + $db->quoteName('published') . ' = 1', + $db->quoteName('extension') . ' = ' . $db->quote('com_content.article'), + ] + ) + ->bind(':workflowId', $workflow_id, ParameterType::INTEGER); + + $db->setQuery($query); + + foreach ($categories as $cat) { + $cat->params = new Registry($cat->params); + + $workflow_id = $cat->params->get('workflow_id'); + + if ($workflow_id == 'inherit') { + continue; + } elseif ($workflow_id == 'use_default') { + break; + } elseif ($workflow_id = (int) $workflow_id) { + $title = $db->loadResult(); + + if (!is_null($title)) { + $option = Text::sprintf('COM_WORKFLOW_INHERIT_WORKFLOW', Text::_($title)); + + break; + } + } + } + } + } - $field = $form->getField('workflow_id', 'params'); + $field = $form->getField('workflow_id', 'params'); - $field->addOption($option, ['value' => 'inherit']); + $field->addOption($option, ['value' => 'inherit']); - $field->addOption(Text::sprintf('COM_WORKFLOW_USE_DEFAULT_WORKFLOW', Text::_($defaulttitle)), ['value' => 'use_default']); + $field->addOption(Text::sprintf('COM_WORKFLOW_USE_DEFAULT_WORKFLOW', Text::_($defaulttitle)), ['value' => 'use_default']); - $field->addOption('- ' . Text::_('COM_CONTENT_WORKFLOWS') . ' -', ['disabled' => 'true']); - } + $field->addOption('- ' . Text::_('COM_CONTENT_WORKFLOWS') . ' -', ['disabled' => 'true']); + } } diff --git a/administrator/components/com_content/src/Model/ArticleModel.php b/administrator/components/com_content/src/Model/ArticleModel.php index 99982f0875fd7..98de89b6fe10a 100644 --- a/administrator/components/com_content/src/Model/ArticleModel.php +++ b/administrator/components/com_content/src/Model/ArticleModel.php @@ -1,4 +1,5 @@ 'content'], - $config['events_map'] - ); - - parent::__construct($config, $factory, $formFactory); - - // Set the featured status change events - $this->event_before_change_featured = $config['event_before_change_featured'] ?? $this->event_before_change_featured; - $this->event_before_change_featured = $this->event_before_change_featured ?? 'onContentBeforeChangeFeatured'; - $this->event_after_change_featured = $config['event_after_change_featured'] ?? $this->event_after_change_featured; - $this->event_after_change_featured = $this->event_after_change_featured ?? 'onContentAfterChangeFeatured'; - - $this->setUpWorkflow('com_content.article'); - } - - /** - * Function that can be overridden to do any data cleanup after batch copying data - * - * @param TableInterface $table The table object containing the newly created item - * @param integer $newId The id of the new item - * @param integer $oldId The original item id - * - * @return void - * - * @since 3.8.12 - */ - protected function cleanupPostBatchCopy(TableInterface $table, $newId, $oldId) - { - // Check if the article was featured and update the #__content_frontpage table - if ($table->featured == 1) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('featured_up'), - $db->quoteName('featured_down'), - ] - ) - ->from($db->quoteName('#__content_frontpage')) - ->where($db->quoteName('content_id') . ' = :oldId') - ->bind(':oldId', $oldId, ParameterType::INTEGER); - - $featured = $db->setQuery($query)->loadObject(); - - if ($featured) - { - $query = $db->getQuery(true) - ->insert($db->quoteName('#__content_frontpage')) - ->values(':newId, 0, :featuredUp, :featuredDown') - ->bind(':newId', $newId, ParameterType::INTEGER) - ->bind(':featuredUp', $featured->featured_up, $featured->featured_up ? ParameterType::STRING : ParameterType::NULL) - ->bind(':featuredDown', $featured->featured_down, $featured->featured_down ? ParameterType::STRING : ParameterType::NULL); - - $db->setQuery($query); - $db->execute(); - } - } - - $this->workflowCleanupBatchMove($oldId, $newId); - - $oldItem = $this->getTable(); - $oldItem->load($oldId); - $fields = FieldsHelper::getFields('com_content.article', $oldItem, true); - - $fieldsData = array(); - - if (!empty($fields)) - { - $fieldsData['com_fields'] = array(); - - foreach ($fields as $field) - { - $fieldsData['com_fields'][$field->name] = $field->rawvalue; - } - } - - Factory::getApplication()->triggerEvent('onContentAfterSave', array('com_content.article', &$this->table, false, $fieldsData)); - } - - /** - * Batch move categories to a new category. - * - * @param integer $value The new category ID. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True on success. - * - * @since 3.8.6 - */ - protected function batchMove($value, $pks, $contexts) - { - if (empty($this->batchSet)) - { - // Set some needed variables. - $this->user = Factory::getUser(); - $this->table = $this->getTable(); - $this->tableClassName = get_class($this->table); - $this->contentType = new UCMType; - $this->type = $this->contentType->getTypeByTable($this->tableClassName); - } - - $categoryId = (int) $value; - - if (!$this->checkCategoryId($categoryId)) - { - return false; - } - - PluginHelper::importPlugin('system'); - - // Parent exists so we proceed - foreach ($pks as $pk) - { - if (!$this->user->authorise('core.edit', $contexts[$pk])) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); - - return false; - } - - // Check that the row actually exists - if (!$this->table->load($pk)) - { - if ($error = $this->table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Not fatal error - $this->setError(Text::sprintf('JLIB_APPLICATION_ERROR_BATCH_MOVE_ROW_NOT_FOUND', $pk)); - continue; - } - } - - $fields = FieldsHelper::getFields('com_content.article', $this->table, true); - - $fieldsData = array(); - - if (!empty($fields)) - { - $fieldsData['com_fields'] = array(); - - foreach ($fields as $field) - { - $fieldsData['com_fields'][$field->name] = $field->rawvalue; - } - } - - // Set the new category ID - $this->table->catid = $categoryId; - - // We don't want to modify tags - so remove the associated tags helper - if ($this->table instanceof TaggableTableInterface) - { - $this->table->clearTagsHelper(); - } - - // Check the row. - if (!$this->table->check()) - { - $this->setError($this->table->getError()); - - return false; - } - - // Store the row. - if (!$this->table->store()) - { - $this->setError($this->table->getError()); - - return false; - } - - // Run event for moved article - Factory::getApplication()->triggerEvent('onContentAfterSave', array('com_content.article', &$this->table, false, $fieldsData)); - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canDelete($record) - { - if (empty($record->id) || ($record->state != -2)) - { - return false; - } - - return Factory::getUser()->authorise('core.delete', 'com_content.article.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state edited. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - - // Check for existing article. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', 'com_content.article.' . (int) $record->id); - } - - // New article, so check against the category. - if (!empty($record->catid)) - { - return $user->authorise('core.edit.state', 'com_content.category.' . (int) $record->catid); - } - - // Default to component settings if neither article nor category known. - return parent::canEditState($record); - } - - /** - * Prepare and sanitise the table data prior to saving. - * - * @param \Joomla\CMS\Table\Table $table A Table object. - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - // Set the publish date to now - if ($table->state == Workflow::CONDITION_PUBLISHED && (int) $table->publish_up == 0) - { - $table->publish_up = Factory::getDate()->toSql(); - } - - if ($table->state == Workflow::CONDITION_PUBLISHED && intval($table->publish_down) == 0) - { - $table->publish_down = null; - } - - // Increment the content version number. - $table->version++; - - // Reorder the articles within the category so the new article is first - if (empty($table->id)) - { - $table->reorder('catid = ' . (int) $table->catid . ' AND state >= 0'); - } - } - - /** - * Method to change the published state of one or more records. - * - * @param array &$pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function publish(&$pks, $value = 1) - { - $this->workflowBeforeStageChange(); - - return parent::publish($pks, $value); - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - */ - public function getItem($pk = null) - { - if ($item = parent::getItem($pk)) - { - // Convert the params field to an array. - $registry = new Registry($item->attribs); - $item->attribs = $registry->toArray(); - - // Convert the metadata field to an array. - $registry = new Registry($item->metadata); - $item->metadata = $registry->toArray(); - - // Convert the images field to an array. - $registry = new Registry($item->images); - $item->images = $registry->toArray(); - - // Convert the urls field to an array. - $registry = new Registry($item->urls); - $item->urls = $registry->toArray(); - - $item->articletext = ($item->fulltext !== null && trim($item->fulltext) != '') ? $item->introtext . '
    ' . $item->fulltext : $item->introtext; - - if (!empty($item->id)) - { - $item->tags = new TagsHelper; - $item->tags->getTagIds($item->id, 'com_content.article'); - - $item->featured_up = null; - $item->featured_down = null; - - if ($item->featured) - { - // Get featured dates. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('featured_up'), - $db->quoteName('featured_down'), - ] - ) - ->from($db->quoteName('#__content_frontpage')) - ->where($db->quoteName('content_id') . ' = :id') - ->bind(':id', $item->id, ParameterType::INTEGER); - - $featured = $db->setQuery($query)->loadObject(); - - if ($featured) - { - $item->featured_up = $featured->featured_up; - $item->featured_down = $featured->featured_down; - } - } - } - } - - // Load associated content items - $assoc = Associations::isEnabled(); - - if ($assoc) - { - $item->associations = array(); - - if ($item->id != null) - { - $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $item->id); - - foreach ($associations as $tag => $association) - { - $item->associations[$tag] = $association->id; - } - } - } - - return $item; - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - $app = Factory::getApplication(); - - // Get the form. - $form = $this->loadForm('com_content.article', 'article', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Object uses for checking edit state permission of article - $record = new \stdClass; - - // Get ID of the article from input, for frontend, we use a_id while backend uses id - $articleIdFromInput = $app->isClient('site') - ? $app->input->getInt('a_id', 0) - : $app->input->getInt('id', 0); - - // On edit article, we get ID of article from article.id state, but on save, we use data from input - $id = (int) $this->getState('article.id', $articleIdFromInput); - - $record->id = $id; - - // For new articles we load the potential state + associations - if ($id == 0 && $formField = $form->getField('catid')) - { - $assignedCatids = $data['catid'] ?? $form->getValue('catid'); - - $assignedCatids = is_array($assignedCatids) - ? (int) reset($assignedCatids) - : (int) $assignedCatids; - - // Try to get the category from the category field - if (empty($assignedCatids)) - { - $assignedCatids = $formField->getAttribute('default', null); - - if (!$assignedCatids) - { - // Choose the first category available - $catOptions = $formField->options; - - if ($catOptions && !empty($catOptions[0]->value)) - { - $assignedCatids = (int) $catOptions[0]->value; - } - } - } - - // Activate the reload of the form when category is changed - $form->setFieldAttribute('catid', 'refresh-enabled', true); - $form->setFieldAttribute('catid', 'refresh-cat-id', $assignedCatids); - $form->setFieldAttribute('catid', 'refresh-section', 'article'); - - // Store ID of the category uses for edit state permission check - $record->catid = $assignedCatids; - } - else - { - // Get the category which the article is being added to - if (!empty($data['catid'])) - { - $catId = (int) $data['catid']; - } - else - { - $catIds = $form->getValue('catid'); - - $catId = is_array($catIds) - ? (int) reset($catIds) - : (int) $catIds; - - if (!$catId) - { - $catId = (int) $form->getFieldAttribute('catid', 'default', 0); - } - } - - $record->catid = $catId; - } - - // Modify the form based on Edit State access controls. - if (!$this->canEditState($record)) - { - // Disable fields for display. - $form->setFieldAttribute('featured', 'disabled', 'true'); - $form->setFieldAttribute('featured_up', 'disabled', 'true'); - $form->setFieldAttribute('featured_down', 'disabled', 'true'); - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('publish_up', 'disabled', 'true'); - $form->setFieldAttribute('publish_down', 'disabled', 'true'); - $form->setFieldAttribute('state', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is an article you can edit. - $form->setFieldAttribute('featured', 'filter', 'unset'); - $form->setFieldAttribute('featured_up', 'filter', 'unset'); - $form->setFieldAttribute('featured_down', 'filter', 'unset'); - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('publish_up', 'filter', 'unset'); - $form->setFieldAttribute('publish_down', 'filter', 'unset'); - $form->setFieldAttribute('state', 'filter', 'unset'); - } - - // Don't allow to change the created_by user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_by', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_content.edit.article.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Pre-select some filters (Status, Category, Language, Access) in edit form if those have been selected in Article Manager: Articles - if ($this->getState('article.id') == 0) - { - $filters = (array) $app->getUserState('com_content.articles.filter'); - $data->set( - 'state', - $app->input->getInt( - 'state', - ((isset($filters['published']) && $filters['published'] !== '') ? $filters['published'] : null) - ) - ); - $data->set('catid', $app->input->getInt('catid', (!empty($filters['category_id']) ? $filters['category_id'] : null))); - - if ($app->isClient('administrator')) - { - $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); - } - - $data->set('access', - $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) - ); - } - } - - // If there are params fieldsets in the form it will fail with a registry object - if (isset($data->params) && $data->params instanceof Registry) - { - $data->params = $data->params->toArray(); - } - - $this->preprocessData('com_content.article', $data); - - return $data; - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see \Joomla\CMS\Form\FormRule - * @see JFilterInput - * @since 3.7.0 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', 'com_content')) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $app = Factory::getApplication(); - $input = $app->input; - $filter = InputFilter::getInstance(); - - if (isset($data['metadata']) && isset($data['metadata']['author'])) - { - $data['metadata']['author'] = $filter->clean($data['metadata']['author'], 'TRIM'); - } - - if (isset($data['created_by_alias'])) - { - $data['created_by_alias'] = $filter->clean($data['created_by_alias'], 'TRIM'); - } - - if (isset($data['images']) && is_array($data['images'])) - { - $registry = new Registry($data['images']); - - $data['images'] = (string) $registry; - } - - $this->workflowBeforeSave(); - - // Create new category, if needed. - $createCategory = true; - - if (is_null($data['catid'])) - { - // When there is no catid passed don't try to create one - $createCategory = false; - } - - // If category ID is provided, check if it's valid. - if (is_numeric($data['catid']) && $data['catid']) - { - $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_content'); - } - - // Save New Category - if ($createCategory && $this->canCreateCategory()) - { - $category = [ - // Remove #new# prefix, if exists. - 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], - 'parent_id' => 1, - 'extension' => 'com_content', - 'language' => $data['language'], - 'published' => 1, - ]; - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ - $categoryModel = Factory::getApplication()->bootComponent('com_categories') - ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); - - // Create new category. - if (!$categoryModel->save($category)) - { - $this->setError($categoryModel->getError()); - - return false; - } - - // Get the Category ID. - $data['catid'] = $categoryModel->getState('category.id'); - } - - if (isset($data['urls']) && is_array($data['urls'])) - { - $check = $input->post->get('jform', array(), 'array'); - - foreach ($data['urls'] as $i => $url) - { - if ($url != false && ($i == 'urla' || $i == 'urlb' || $i == 'urlc')) - { - if (preg_match('~^#[a-zA-Z]{1}[a-zA-Z0-9-_:.]*$~', $check['urls'][$i]) == 1) - { - $data['urls'][$i] = $check['urls'][$i]; - } - else - { - $data['urls'][$i] = PunycodeHelper::urlToPunycode($url); - } - } - } - - unset($check); - - $registry = new Registry($data['urls']); - - $data['urls'] = (string) $registry; - } - - // Alter the title for save as copy - if ($input->get('task') == 'save2copy') - { - $origTable = $this->getTable(); - - if ($app->isClient('site')) - { - $origTable->load($input->getInt('a_id')); - - if ($origTable->title === $data['title']) - { - /** - * If title of article is not changed, set alias to original article alias so that Joomla! will generate - * new Title and Alias for the copied article - */ - $data['alias'] = $origTable->alias; - } - else - { - $data['alias'] = ''; - } - } - else - { - $origTable->load($input->getInt('id')); - } - - if ($data['title'] == $origTable->title) - { - list($title, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['title']); - $data['title'] = $title; - $data['alias'] = $alias; - } - elseif ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - } - - // Automatic handling of alias for empty fields - if (in_array($input->get('task'), array('apply', 'save', 'save2new')) && (!isset($data['id']) || (int) $data['id'] == 0)) - { - if ($data['alias'] == null) - { - if ($app->get('unicodeslugs') == 1) - { - $data['alias'] = OutputFilter::stringUrlUnicodeSlug($data['title']); - } - else - { - $data['alias'] = OutputFilter::stringURLSafe($data['title']); - } - - $table = $this->getTable(); - - if ($table->load(array('alias' => $data['alias'], 'catid' => $data['catid']))) - { - $msg = Text::_('COM_CONTENT_SAVE_WARNING'); - } - - list($title, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['title']); - $data['alias'] = $alias; - - if (isset($msg)) - { - $app->enqueueMessage($msg, 'warning'); - } - } - } - - if (parent::save($data)) - { - // Check if featured is set and if not managed by workflow - if (isset($data['featured']) && !$this->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article')) - { - if (!$this->featured( - $this->getState($this->getName() . '.id'), - $data['featured'], - $data['featured_up'] ?? null, - $data['featured_down'] ?? null - )) - { - return false; - } - } - - $this->workflowAfterSave($data); - - return true; - } - - return false; - } - - /** - * Method to toggle the featured setting of articles. - * - * @param array $pks The ids of the items to toggle. - * @param integer $value The value to toggle to. - * @param string|Date $featuredUp The date which item featured up. - * @param string|Date $featuredDown The date which item featured down. - * - * @return boolean True on success. - */ - public function featured($pks, $value = 0, $featuredUp = null, $featuredDown = null) - { - // Sanitize the ids. - $pks = (array) $pks; - $pks = ArrayHelper::toInteger($pks); - $value = (int) $value; - $context = $this->option . '.' . $this->name; - - $this->workflowBeforeStageChange(); - - // Include the plugins for the change of state event. - PluginHelper::importPlugin($this->events_map['featured']); - - // Convert empty strings to null for the query. - if ($featuredUp === '') - { - $featuredUp = null; - } - - if ($featuredDown === '') - { - $featuredDown = null; - } - - if (empty($pks)) - { - $this->setError(Text::_('COM_CONTENT_NO_ITEM_SELECTED')); - - return false; - } - - $table = $this->getTable('Featured', 'Administrator'); - - // Trigger the before change state event. - $eventResult = Factory::getApplication()->getDispatcher()->dispatch( - $this->event_before_change_featured, - AbstractEvent::create( - $this->event_before_change_featured, - [ - 'eventClass' => 'Joomla\Component\Content\Administrator\Event\Model\FeatureEvent', - 'subject' => $this, - 'extension' => $context, - 'pks' => $pks, - 'value' => $value, - ] - ) - ); - - if ($eventResult->getArgument('abort', false)) - { - $this->setError(Text::_($eventResult->getArgument('abortReason'))); - - return false; - } - - try - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->update($db->quoteName('#__content')) - ->set($db->quoteName('featured') . ' = :featured') - ->whereIn($db->quoteName('id'), $pks) - ->bind(':featured', $value, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - if ($value === 0) - { - // Adjust the mapping table. - // Clear the existing features settings. - $query = $db->getQuery(true) - ->delete($db->quoteName('#__content_frontpage')) - ->whereIn($db->quoteName('content_id'), $pks); - $db->setQuery($query); - $db->execute(); - } - else - { - // First, we find out which of our new featured articles are already featured. - $query = $db->getQuery(true) - ->select($db->quoteName('content_id')) - ->from($db->quoteName('#__content_frontpage')) - ->whereIn($db->quoteName('content_id'), $pks); - $db->setQuery($query); - - $oldFeatured = $db->loadColumn(); - - // Update old featured articles - if (count($oldFeatured)) - { - $query = $db->getQuery(true) - ->update($db->quoteName('#__content_frontpage')) - ->set( - [ - $db->quoteName('featured_up') . ' = :featuredUp', - $db->quoteName('featured_down') . ' = :featuredDown', - ] - ) - ->whereIn($db->quoteName('content_id'), $oldFeatured) - ->bind(':featuredUp', $featuredUp, $featuredUp ? ParameterType::STRING : ParameterType::NULL) - ->bind(':featuredDown', $featuredDown, $featuredDown ? ParameterType::STRING : ParameterType::NULL); - $db->setQuery($query); - $db->execute(); - } - - // We diff the arrays to get a list of the articles that are newly featured - $newFeatured = array_diff($pks, $oldFeatured); - - // Featuring. - if ($newFeatured) - { - $query = $db->getQuery(true) - ->insert($db->quoteName('#__content_frontpage')) - ->columns( - [ - $db->quoteName('content_id'), - $db->quoteName('ordering'), - $db->quoteName('featured_up'), - $db->quoteName('featured_down'), - ] - ); - - $dataTypes = [ - ParameterType::INTEGER, - ParameterType::INTEGER, - $featuredUp ? ParameterType::STRING : ParameterType::NULL, - $featuredDown ? ParameterType::STRING : ParameterType::NULL, - ]; - - foreach ($newFeatured as $pk) - { - $query->values(implode(',', $query->bindArray([$pk, 0, $featuredUp, $featuredDown], $dataTypes))); - } - - $db->setQuery($query); - $db->execute(); - } - } - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $table->reorder(); - - // Trigger the change state event. - Factory::getApplication()->getDispatcher()->dispatch( - $this->event_after_change_featured, - AbstractEvent::create( - $this->event_after_change_featured, - [ - 'eventClass' => 'Joomla\Component\Content\Administrator\Event\Model\FeatureEvent', - 'subject' => $this, - 'extension' => $context, - 'pks' => $pks, - 'value' => $value, - ] - ) - ); - - $this->cleanCache(); - - return true; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - return [ - $this->getDatabase()->quoteName('catid') . ' = ' . (int) $table->catid, - ]; - } - - /** - * Allows preprocessing of the Form object. - * - * @param Form $form The form object - * @param array $data The data to be merged into the form object - * @param string $group The plugin group to be executed - * - * @return void - * - * @since 3.0 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - if ($this->canCreateCategory()) - { - $form->setFieldAttribute('catid', 'allowAdd', 'true'); - - // Add a prefix for categories created on the fly. - $form->setFieldAttribute('catid', 'customPrefix', '#new#'); - } - - // Association content items - if (Associations::isEnabled()) - { - $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); - - if (count($languages) > 1) - { - $addform = new \SimpleXMLElement('
    '); - $fields = $addform->addChild('fields'); - $fields->addAttribute('name', 'associations'); - $fieldset = $fields->addChild('fieldset'); - $fieldset->addAttribute('name', 'item_associations'); - - foreach ($languages as $language) - { - $field = $fieldset->addChild('field'); - $field->addAttribute('name', $language->lang_code); - $field->addAttribute('type', 'modal_article'); - $field->addAttribute('language', $language->lang_code); - $field->addAttribute('label', $language->title); - $field->addAttribute('translate_label', 'false'); - $field->addAttribute('select', 'true'); - $field->addAttribute('new', 'true'); - $field->addAttribute('edit', 'true'); - $field->addAttribute('clear', 'true'); - $field->addAttribute('propagate', 'true'); - } - - $form->load($addform, false); - } - } - - $this->workflowPreprocessForm($form, $data); - - parent::preprocessForm($form, $data, $group); - } - - /** - * Custom clean the cache of com_content and content modules - * - * @param string $group The cache group - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_content'); - parent::cleanCache('mod_articles_archive'); - parent::cleanCache('mod_articles_categories'); - parent::cleanCache('mod_articles_category'); - parent::cleanCache('mod_articles_latest'); - parent::cleanCache('mod_articles_news'); - parent::cleanCache('mod_articles_popular'); - } - - /** - * Void hit function for pagebreak when editing content from frontend - * - * @return void - * - * @since 3.6.0 - */ - public function hit() - { - } - - /** - * Is the user allowed to create an on the fly category? - * - * @return boolean - * - * @since 3.6.1 - */ - private function canCreateCategory() - { - return Factory::getUser()->authorise('core.create', 'com_content'); - } - - /** - * Delete #__content_frontpage items if the deleted articles was featured - * - * @param object $pks The primary key related to the contents that was deleted. - * - * @return boolean - * - * @since 3.7.0 - */ - public function delete(&$pks) - { - $return = parent::delete($pks); - - if ($return) - { - // Now check to see if this articles was featured if so delete it from the #__content_frontpage table - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__content_frontpage')) - ->whereIn($db->quoteName('content_id'), $pks); - $db->setQuery($query); - $db->execute(); - - $this->workflow->deleteAssociation($pks); - } - - return $return; - } + use WorkflowBehaviorTrait; + use VersionableModelTrait; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_CONTENT'; + + /** + * The type alias for this content type (for example, 'com_content.article'). + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_content.article'; + + /** + * The context used for the associations table + * + * @var string + * @since 3.4.4 + */ + protected $associationsContext = 'com_content.item'; + + /** + * The event to trigger before changing featured status one or more items. + * + * @var string + * @since 4.0.0 + */ + protected $event_before_change_featured = null; + + /** + * The event to trigger after changing featured status one or more items. + * + * @var string + * @since 4.0.0 + */ + protected $event_after_change_featured = null; + + /** + * Constructor. + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * @param FormFactoryInterface $formFactory The form factory. + * + * @since 1.6 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null) + { + $config['events_map'] = $config['events_map'] ?? []; + + $config['events_map'] = array_merge( + ['featured' => 'content'], + $config['events_map'] + ); + + parent::__construct($config, $factory, $formFactory); + + // Set the featured status change events + $this->event_before_change_featured = $config['event_before_change_featured'] ?? $this->event_before_change_featured; + $this->event_before_change_featured = $this->event_before_change_featured ?? 'onContentBeforeChangeFeatured'; + $this->event_after_change_featured = $config['event_after_change_featured'] ?? $this->event_after_change_featured; + $this->event_after_change_featured = $this->event_after_change_featured ?? 'onContentAfterChangeFeatured'; + + $this->setUpWorkflow('com_content.article'); + } + + /** + * Function that can be overridden to do any data cleanup after batch copying data + * + * @param TableInterface $table The table object containing the newly created item + * @param integer $newId The id of the new item + * @param integer $oldId The original item id + * + * @return void + * + * @since 3.8.12 + */ + protected function cleanupPostBatchCopy(TableInterface $table, $newId, $oldId) + { + // Check if the article was featured and update the #__content_frontpage table + if ($table->featured == 1) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('featured_up'), + $db->quoteName('featured_down'), + ] + ) + ->from($db->quoteName('#__content_frontpage')) + ->where($db->quoteName('content_id') . ' = :oldId') + ->bind(':oldId', $oldId, ParameterType::INTEGER); + + $featured = $db->setQuery($query)->loadObject(); + + if ($featured) { + $query = $db->getQuery(true) + ->insert($db->quoteName('#__content_frontpage')) + ->values(':newId, 0, :featuredUp, :featuredDown') + ->bind(':newId', $newId, ParameterType::INTEGER) + ->bind(':featuredUp', $featured->featured_up, $featured->featured_up ? ParameterType::STRING : ParameterType::NULL) + ->bind(':featuredDown', $featured->featured_down, $featured->featured_down ? ParameterType::STRING : ParameterType::NULL); + + $db->setQuery($query); + $db->execute(); + } + } + + $this->workflowCleanupBatchMove($oldId, $newId); + + $oldItem = $this->getTable(); + $oldItem->load($oldId); + $fields = FieldsHelper::getFields('com_content.article', $oldItem, true); + + $fieldsData = array(); + + if (!empty($fields)) { + $fieldsData['com_fields'] = array(); + + foreach ($fields as $field) { + $fieldsData['com_fields'][$field->name] = $field->rawvalue; + } + } + + Factory::getApplication()->triggerEvent('onContentAfterSave', array('com_content.article', &$this->table, false, $fieldsData)); + } + + /** + * Batch move categories to a new category. + * + * @param integer $value The new category ID. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True on success. + * + * @since 3.8.6 + */ + protected function batchMove($value, $pks, $contexts) + { + if (empty($this->batchSet)) { + // Set some needed variables. + $this->user = Factory::getUser(); + $this->table = $this->getTable(); + $this->tableClassName = get_class($this->table); + $this->contentType = new UCMType(); + $this->type = $this->contentType->getTypeByTable($this->tableClassName); + } + + $categoryId = (int) $value; + + if (!$this->checkCategoryId($categoryId)) { + return false; + } + + PluginHelper::importPlugin('system'); + + // Parent exists so we proceed + foreach ($pks as $pk) { + if (!$this->user->authorise('core.edit', $contexts[$pk])) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); + + return false; + } + + // Check that the row actually exists + if (!$this->table->load($pk)) { + if ($error = $this->table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Not fatal error + $this->setError(Text::sprintf('JLIB_APPLICATION_ERROR_BATCH_MOVE_ROW_NOT_FOUND', $pk)); + continue; + } + } + + $fields = FieldsHelper::getFields('com_content.article', $this->table, true); + + $fieldsData = array(); + + if (!empty($fields)) { + $fieldsData['com_fields'] = array(); + + foreach ($fields as $field) { + $fieldsData['com_fields'][$field->name] = $field->rawvalue; + } + } + + // Set the new category ID + $this->table->catid = $categoryId; + + // We don't want to modify tags - so remove the associated tags helper + if ($this->table instanceof TaggableTableInterface) { + $this->table->clearTagsHelper(); + } + + // Check the row. + if (!$this->table->check()) { + $this->setError($this->table->getError()); + + return false; + } + + // Store the row. + if (!$this->table->store()) { + $this->setError($this->table->getError()); + + return false; + } + + // Run event for moved article + Factory::getApplication()->triggerEvent('onContentAfterSave', array('com_content.article', &$this->table, false, $fieldsData)); + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || ($record->state != -2)) { + return false; + } + + return Factory::getUser()->authorise('core.delete', 'com_content.article.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state edited. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + + // Check for existing article. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', 'com_content.article.' . (int) $record->id); + } + + // New article, so check against the category. + if (!empty($record->catid)) { + return $user->authorise('core.edit.state', 'com_content.category.' . (int) $record->catid); + } + + // Default to component settings if neither article nor category known. + return parent::canEditState($record); + } + + /** + * Prepare and sanitise the table data prior to saving. + * + * @param \Joomla\CMS\Table\Table $table A Table object. + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + // Set the publish date to now + if ($table->state == Workflow::CONDITION_PUBLISHED && (int) $table->publish_up == 0) { + $table->publish_up = Factory::getDate()->toSql(); + } + + if ($table->state == Workflow::CONDITION_PUBLISHED && intval($table->publish_down) == 0) { + $table->publish_down = null; + } + + // Increment the content version number. + $table->version++; + + // Reorder the articles within the category so the new article is first + if (empty($table->id)) { + $table->reorder('catid = ' . (int) $table->catid . ' AND state >= 0'); + } + } + + /** + * Method to change the published state of one or more records. + * + * @param array &$pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function publish(&$pks, $value = 1) + { + $this->workflowBeforeStageChange(); + + return parent::publish($pks, $value); + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + */ + public function getItem($pk = null) + { + if ($item = parent::getItem($pk)) { + // Convert the params field to an array. + $registry = new Registry($item->attribs); + $item->attribs = $registry->toArray(); + + // Convert the metadata field to an array. + $registry = new Registry($item->metadata); + $item->metadata = $registry->toArray(); + + // Convert the images field to an array. + $registry = new Registry($item->images); + $item->images = $registry->toArray(); + + // Convert the urls field to an array. + $registry = new Registry($item->urls); + $item->urls = $registry->toArray(); + + $item->articletext = ($item->fulltext !== null && trim($item->fulltext) != '') ? $item->introtext . '
    ' . $item->fulltext : $item->introtext; + + if (!empty($item->id)) { + $item->tags = new TagsHelper(); + $item->tags->getTagIds($item->id, 'com_content.article'); + + $item->featured_up = null; + $item->featured_down = null; + + if ($item->featured) { + // Get featured dates. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('featured_up'), + $db->quoteName('featured_down'), + ] + ) + ->from($db->quoteName('#__content_frontpage')) + ->where($db->quoteName('content_id') . ' = :id') + ->bind(':id', $item->id, ParameterType::INTEGER); + + $featured = $db->setQuery($query)->loadObject(); + + if ($featured) { + $item->featured_up = $featured->featured_up; + $item->featured_down = $featured->featured_down; + } + } + } + } + + // Load associated content items + $assoc = Associations::isEnabled(); + + if ($assoc) { + $item->associations = array(); + + if ($item->id != null) { + $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $item->id); + + foreach ($associations as $tag => $association) { + $item->associations[$tag] = $association->id; + } + } + } + + return $item; + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + $app = Factory::getApplication(); + + // Get the form. + $form = $this->loadForm('com_content.article', 'article', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Object uses for checking edit state permission of article + $record = new \stdClass(); + + // Get ID of the article from input, for frontend, we use a_id while backend uses id + $articleIdFromInput = $app->isClient('site') + ? $app->input->getInt('a_id', 0) + : $app->input->getInt('id', 0); + + // On edit article, we get ID of article from article.id state, but on save, we use data from input + $id = (int) $this->getState('article.id', $articleIdFromInput); + + $record->id = $id; + + // For new articles we load the potential state + associations + if ($id == 0 && $formField = $form->getField('catid')) { + $assignedCatids = $data['catid'] ?? $form->getValue('catid'); + + $assignedCatids = is_array($assignedCatids) + ? (int) reset($assignedCatids) + : (int) $assignedCatids; + + // Try to get the category from the category field + if (empty($assignedCatids)) { + $assignedCatids = $formField->getAttribute('default', null); + + if (!$assignedCatids) { + // Choose the first category available + $catOptions = $formField->options; + + if ($catOptions && !empty($catOptions[0]->value)) { + $assignedCatids = (int) $catOptions[0]->value; + } + } + } + + // Activate the reload of the form when category is changed + $form->setFieldAttribute('catid', 'refresh-enabled', true); + $form->setFieldAttribute('catid', 'refresh-cat-id', $assignedCatids); + $form->setFieldAttribute('catid', 'refresh-section', 'article'); + + // Store ID of the category uses for edit state permission check + $record->catid = $assignedCatids; + } else { + // Get the category which the article is being added to + if (!empty($data['catid'])) { + $catId = (int) $data['catid']; + } else { + $catIds = $form->getValue('catid'); + + $catId = is_array($catIds) + ? (int) reset($catIds) + : (int) $catIds; + + if (!$catId) { + $catId = (int) $form->getFieldAttribute('catid', 'default', 0); + } + } + + $record->catid = $catId; + } + + // Modify the form based on Edit State access controls. + if (!$this->canEditState($record)) { + // Disable fields for display. + $form->setFieldAttribute('featured', 'disabled', 'true'); + $form->setFieldAttribute('featured_up', 'disabled', 'true'); + $form->setFieldAttribute('featured_down', 'disabled', 'true'); + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('publish_up', 'disabled', 'true'); + $form->setFieldAttribute('publish_down', 'disabled', 'true'); + $form->setFieldAttribute('state', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is an article you can edit. + $form->setFieldAttribute('featured', 'filter', 'unset'); + $form->setFieldAttribute('featured_up', 'filter', 'unset'); + $form->setFieldAttribute('featured_down', 'filter', 'unset'); + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('publish_up', 'filter', 'unset'); + $form->setFieldAttribute('publish_down', 'filter', 'unset'); + $form->setFieldAttribute('state', 'filter', 'unset'); + } + + // Don't allow to change the created_by user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_by', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_content.edit.article.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Pre-select some filters (Status, Category, Language, Access) in edit form if those have been selected in Article Manager: Articles + if ($this->getState('article.id') == 0) { + $filters = (array) $app->getUserState('com_content.articles.filter'); + $data->set( + 'state', + $app->input->getInt( + 'state', + ((isset($filters['published']) && $filters['published'] !== '') ? $filters['published'] : null) + ) + ); + $data->set('catid', $app->input->getInt('catid', (!empty($filters['category_id']) ? $filters['category_id'] : null))); + + if ($app->isClient('administrator')) { + $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); + } + + $data->set( + 'access', + $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) + ); + } + } + + // If there are params fieldsets in the form it will fail with a registry object + if (isset($data->params) && $data->params instanceof Registry) { + $data->params = $data->params->toArray(); + } + + $this->preprocessData('com_content.article', $data); + + return $data; + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see \Joomla\CMS\Form\FormRule + * @see JFilterInput + * @since 3.7.0 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', 'com_content')) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $app = Factory::getApplication(); + $input = $app->input; + $filter = InputFilter::getInstance(); + + if (isset($data['metadata']) && isset($data['metadata']['author'])) { + $data['metadata']['author'] = $filter->clean($data['metadata']['author'], 'TRIM'); + } + + if (isset($data['created_by_alias'])) { + $data['created_by_alias'] = $filter->clean($data['created_by_alias'], 'TRIM'); + } + + if (isset($data['images']) && is_array($data['images'])) { + $registry = new Registry($data['images']); + + $data['images'] = (string) $registry; + } + + $this->workflowBeforeSave(); + + // Create new category, if needed. + $createCategory = true; + + if (is_null($data['catid'])) { + // When there is no catid passed don't try to create one + $createCategory = false; + } + + // If category ID is provided, check if it's valid. + if (is_numeric($data['catid']) && $data['catid']) { + $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_content'); + } + + // Save New Category + if ($createCategory && $this->canCreateCategory()) { + $category = [ + // Remove #new# prefix, if exists. + 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], + 'parent_id' => 1, + 'extension' => 'com_content', + 'language' => $data['language'], + 'published' => 1, + ]; + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ + $categoryModel = Factory::getApplication()->bootComponent('com_categories') + ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); + + // Create new category. + if (!$categoryModel->save($category)) { + $this->setError($categoryModel->getError()); + + return false; + } + + // Get the Category ID. + $data['catid'] = $categoryModel->getState('category.id'); + } + + if (isset($data['urls']) && is_array($data['urls'])) { + $check = $input->post->get('jform', array(), 'array'); + + foreach ($data['urls'] as $i => $url) { + if ($url != false && ($i == 'urla' || $i == 'urlb' || $i == 'urlc')) { + if (preg_match('~^#[a-zA-Z]{1}[a-zA-Z0-9-_:.]*$~', $check['urls'][$i]) == 1) { + $data['urls'][$i] = $check['urls'][$i]; + } else { + $data['urls'][$i] = PunycodeHelper::urlToPunycode($url); + } + } + } + + unset($check); + + $registry = new Registry($data['urls']); + + $data['urls'] = (string) $registry; + } + + // Alter the title for save as copy + if ($input->get('task') == 'save2copy') { + $origTable = $this->getTable(); + + if ($app->isClient('site')) { + $origTable->load($input->getInt('a_id')); + + if ($origTable->title === $data['title']) { + /** + * If title of article is not changed, set alias to original article alias so that Joomla! will generate + * new Title and Alias for the copied article + */ + $data['alias'] = $origTable->alias; + } else { + $data['alias'] = ''; + } + } else { + $origTable->load($input->getInt('id')); + } + + if ($data['title'] == $origTable->title) { + list($title, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['title']); + $data['title'] = $title; + $data['alias'] = $alias; + } elseif ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + } + + // Automatic handling of alias for empty fields + if (in_array($input->get('task'), array('apply', 'save', 'save2new')) && (!isset($data['id']) || (int) $data['id'] == 0)) { + if ($data['alias'] == null) { + if ($app->get('unicodeslugs') == 1) { + $data['alias'] = OutputFilter::stringUrlUnicodeSlug($data['title']); + } else { + $data['alias'] = OutputFilter::stringURLSafe($data['title']); + } + + $table = $this->getTable(); + + if ($table->load(array('alias' => $data['alias'], 'catid' => $data['catid']))) { + $msg = Text::_('COM_CONTENT_SAVE_WARNING'); + } + + list($title, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['title']); + $data['alias'] = $alias; + + if (isset($msg)) { + $app->enqueueMessage($msg, 'warning'); + } + } + } + + if (parent::save($data)) { + // Check if featured is set and if not managed by workflow + if (isset($data['featured']) && !$this->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article')) { + if ( + !$this->featured( + $this->getState($this->getName() . '.id'), + $data['featured'], + $data['featured_up'] ?? null, + $data['featured_down'] ?? null + ) + ) { + return false; + } + } + + $this->workflowAfterSave($data); + + return true; + } + + return false; + } + + /** + * Method to toggle the featured setting of articles. + * + * @param array $pks The ids of the items to toggle. + * @param integer $value The value to toggle to. + * @param string|Date $featuredUp The date which item featured up. + * @param string|Date $featuredDown The date which item featured down. + * + * @return boolean True on success. + */ + public function featured($pks, $value = 0, $featuredUp = null, $featuredDown = null) + { + // Sanitize the ids. + $pks = (array) $pks; + $pks = ArrayHelper::toInteger($pks); + $value = (int) $value; + $context = $this->option . '.' . $this->name; + + $this->workflowBeforeStageChange(); + + // Include the plugins for the change of state event. + PluginHelper::importPlugin($this->events_map['featured']); + + // Convert empty strings to null for the query. + if ($featuredUp === '') { + $featuredUp = null; + } + + if ($featuredDown === '') { + $featuredDown = null; + } + + if (empty($pks)) { + $this->setError(Text::_('COM_CONTENT_NO_ITEM_SELECTED')); + + return false; + } + + $table = $this->getTable('Featured', 'Administrator'); + + // Trigger the before change state event. + $eventResult = Factory::getApplication()->getDispatcher()->dispatch( + $this->event_before_change_featured, + AbstractEvent::create( + $this->event_before_change_featured, + [ + 'eventClass' => 'Joomla\Component\Content\Administrator\Event\Model\FeatureEvent', + 'subject' => $this, + 'extension' => $context, + 'pks' => $pks, + 'value' => $value, + ] + ) + ); + + if ($eventResult->getArgument('abort', false)) { + $this->setError(Text::_($eventResult->getArgument('abortReason'))); + + return false; + } + + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__content')) + ->set($db->quoteName('featured') . ' = :featured') + ->whereIn($db->quoteName('id'), $pks) + ->bind(':featured', $value, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + if ($value === 0) { + // Adjust the mapping table. + // Clear the existing features settings. + $query = $db->getQuery(true) + ->delete($db->quoteName('#__content_frontpage')) + ->whereIn($db->quoteName('content_id'), $pks); + $db->setQuery($query); + $db->execute(); + } else { + // First, we find out which of our new featured articles are already featured. + $query = $db->getQuery(true) + ->select($db->quoteName('content_id')) + ->from($db->quoteName('#__content_frontpage')) + ->whereIn($db->quoteName('content_id'), $pks); + $db->setQuery($query); + + $oldFeatured = $db->loadColumn(); + + // Update old featured articles + if (count($oldFeatured)) { + $query = $db->getQuery(true) + ->update($db->quoteName('#__content_frontpage')) + ->set( + [ + $db->quoteName('featured_up') . ' = :featuredUp', + $db->quoteName('featured_down') . ' = :featuredDown', + ] + ) + ->whereIn($db->quoteName('content_id'), $oldFeatured) + ->bind(':featuredUp', $featuredUp, $featuredUp ? ParameterType::STRING : ParameterType::NULL) + ->bind(':featuredDown', $featuredDown, $featuredDown ? ParameterType::STRING : ParameterType::NULL); + $db->setQuery($query); + $db->execute(); + } + + // We diff the arrays to get a list of the articles that are newly featured + $newFeatured = array_diff($pks, $oldFeatured); + + // Featuring. + if ($newFeatured) { + $query = $db->getQuery(true) + ->insert($db->quoteName('#__content_frontpage')) + ->columns( + [ + $db->quoteName('content_id'), + $db->quoteName('ordering'), + $db->quoteName('featured_up'), + $db->quoteName('featured_down'), + ] + ); + + $dataTypes = [ + ParameterType::INTEGER, + ParameterType::INTEGER, + $featuredUp ? ParameterType::STRING : ParameterType::NULL, + $featuredDown ? ParameterType::STRING : ParameterType::NULL, + ]; + + foreach ($newFeatured as $pk) { + $query->values(implode(',', $query->bindArray([$pk, 0, $featuredUp, $featuredDown], $dataTypes))); + } + + $db->setQuery($query); + $db->execute(); + } + } + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $table->reorder(); + + // Trigger the change state event. + Factory::getApplication()->getDispatcher()->dispatch( + $this->event_after_change_featured, + AbstractEvent::create( + $this->event_after_change_featured, + [ + 'eventClass' => 'Joomla\Component\Content\Administrator\Event\Model\FeatureEvent', + 'subject' => $this, + 'extension' => $context, + 'pks' => $pks, + 'value' => $value, + ] + ) + ); + + $this->cleanCache(); + + return true; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + return [ + $this->getDatabase()->quoteName('catid') . ' = ' . (int) $table->catid, + ]; + } + + /** + * Allows preprocessing of the Form object. + * + * @param Form $form The form object + * @param array $data The data to be merged into the form object + * @param string $group The plugin group to be executed + * + * @return void + * + * @since 3.0 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + if ($this->canCreateCategory()) { + $form->setFieldAttribute('catid', 'allowAdd', 'true'); + + // Add a prefix for categories created on the fly. + $form->setFieldAttribute('catid', 'customPrefix', '#new#'); + } + + // Association content items + if (Associations::isEnabled()) { + $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); + + if (count($languages) > 1) { + $addform = new \SimpleXMLElement(''); + $fields = $addform->addChild('fields'); + $fields->addAttribute('name', 'associations'); + $fieldset = $fields->addChild('fieldset'); + $fieldset->addAttribute('name', 'item_associations'); + + foreach ($languages as $language) { + $field = $fieldset->addChild('field'); + $field->addAttribute('name', $language->lang_code); + $field->addAttribute('type', 'modal_article'); + $field->addAttribute('language', $language->lang_code); + $field->addAttribute('label', $language->title); + $field->addAttribute('translate_label', 'false'); + $field->addAttribute('select', 'true'); + $field->addAttribute('new', 'true'); + $field->addAttribute('edit', 'true'); + $field->addAttribute('clear', 'true'); + $field->addAttribute('propagate', 'true'); + } + + $form->load($addform, false); + } + } + + $this->workflowPreprocessForm($form, $data); + + parent::preprocessForm($form, $data, $group); + } + + /** + * Custom clean the cache of com_content and content modules + * + * @param string $group The cache group + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_content'); + parent::cleanCache('mod_articles_archive'); + parent::cleanCache('mod_articles_categories'); + parent::cleanCache('mod_articles_category'); + parent::cleanCache('mod_articles_latest'); + parent::cleanCache('mod_articles_news'); + parent::cleanCache('mod_articles_popular'); + } + + /** + * Void hit function for pagebreak when editing content from frontend + * + * @return void + * + * @since 3.6.0 + */ + public function hit() + { + } + + /** + * Is the user allowed to create an on the fly category? + * + * @return boolean + * + * @since 3.6.1 + */ + private function canCreateCategory() + { + return Factory::getUser()->authorise('core.create', 'com_content'); + } + + /** + * Delete #__content_frontpage items if the deleted articles was featured + * + * @param object $pks The primary key related to the contents that was deleted. + * + * @return boolean + * + * @since 3.7.0 + */ + public function delete(&$pks) + { + $return = parent::delete($pks); + + if ($return) { + // Now check to see if this articles was featured if so delete it from the #__content_frontpage table + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__content_frontpage')) + ->whereIn($db->quoteName('content_id'), $pks); + $db->setQuery($query); + $db->execute(); + + $this->workflow->deleteAssociation($pks); + } + + return $return; + } } diff --git a/administrator/components/com_content/src/Model/ArticlesModel.php b/administrator/components/com_content/src/Model/ArticlesModel.php index 33cd5215e9120..5ab7f15ab72aa 100644 --- a/administrator/components/com_content/src/Model/ArticlesModel.php +++ b/administrator/components/com_content/src/Model/ArticlesModel.php @@ -1,4 +1,5 @@ get('workflow_enabled')) - { - $form->removeField('stage', 'filter'); - } - else - { - $ordering = $form->getField('fullordering', 'list'); - - $ordering->addOption('JSTAGE_ASC', ['value' => 'ws.title ASC']); - $ordering->addOption('JSTAGE_DESC', ['value' => 'ws.title DESC']); - } - - return $form; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.id', $direction = 'desc') - { - $app = Factory::getApplication(); - - $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); - $this->setState('filter.search', $search); - - $featured = $this->getUserStateFromRequest($this->context . '.filter.featured', 'filter_featured', ''); - $this->setState('filter.featured', $featured); - - $published = $this->getUserStateFromRequest($this->context . '.filter.published', 'filter_published', ''); - $this->setState('filter.published', $published); - - $level = $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level'); - $this->setState('filter.level', $level); - - $language = $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', ''); - $this->setState('filter.language', $language); - - $formSubmitted = $app->input->post->get('form_submitted'); - - // Gets the value of a user state variable and sets it in the session - $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access'); - $this->getUserStateFromRequest($this->context . '.filter.author_id', 'filter_author_id'); - $this->getUserStateFromRequest($this->context . '.filter.category_id', 'filter_category_id'); - $this->getUserStateFromRequest($this->context . '.filter.tag', 'filter_tag', ''); - - if ($formSubmitted) - { - $access = $app->input->post->get('access'); - $this->setState('filter.access', $access); - - $authorId = $app->input->post->get('author_id'); - $this->setState('filter.author_id', $authorId); - - $categoryId = $app->input->post->get('category_id'); - $this->setState('filter.category_id', $categoryId); - - $tag = $app->input->post->get('tag'); - $this->setState('filter.tag', $tag); - } - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language - if (!empty($forcedLanguage)) - { - $this->setState('filter.language', $forcedLanguage); - $this->setState('filter.forcedLanguage', $forcedLanguage); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . serialize($this->getState('filter.access')); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . serialize($this->getState('filter.category_id')); - $id .= ':' . serialize($this->getState('filter.author_id')); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . serialize($this->getState('filter.tag')); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - $params = ComponentHelper::getParams('com_content'); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.asset_id'), - $db->quoteName('a.title'), - $db->quoteName('a.alias'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.catid'), - $db->quoteName('a.state'), - $db->quoteName('a.access'), - $db->quoteName('a.created'), - $db->quoteName('a.created_by'), - $db->quoteName('a.created_by_alias'), - $db->quoteName('a.modified'), - $db->quoteName('a.ordering'), - $db->quoteName('a.featured'), - $db->quoteName('a.language'), - $db->quoteName('a.hits'), - $db->quoteName('a.publish_up'), - $db->quoteName('a.publish_down'), - $db->quoteName('a.introtext'), - $db->quoteName('a.fulltext'), - $db->quoteName('a.note'), - $db->quoteName('a.images'), - $db->quoteName('a.metakey'), - $db->quoteName('a.metadesc'), - $db->quoteName('a.metadata'), - $db->quoteName('a.version'), - ] - ) - ) - ->select( - [ - $db->quoteName('fp.featured_up'), - $db->quoteName('fp.featured_down'), - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - $db->quoteName('uc.name', 'editor'), - $db->quoteName('ag.title', 'access_level'), - $db->quoteName('c.title', 'category_title'), - $db->quoteName('c.created_user_id', 'category_uid'), - $db->quoteName('c.level', 'category_level'), - $db->quoteName('c.published', 'category_published'), - $db->quoteName('parent.title', 'parent_category_title'), - $db->quoteName('parent.id', 'parent_category_id'), - $db->quoteName('parent.created_user_id', 'parent_category_uid'), - $db->quoteName('parent.level', 'parent_category_level'), - $db->quoteName('ua.name', 'author_name'), - $db->quoteName('wa.stage_id', 'stage_id'), - $db->quoteName('ws.title', 'stage_title'), - $db->quoteName('ws.workflow_id', 'workflow_id'), - $db->quoteName('w.title', 'workflow_title'), - ] - ) - ->from($db->quoteName('#__content', 'a')) - ->where($db->quoteName('wa.extension') . ' = ' . $db->quote('com_content.article')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) - ->join('LEFT', $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) - ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) - ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')) - ->join('INNER', $db->quoteName('#__workflow_associations', 'wa'), $db->quoteName('wa.item_id') . ' = ' . $db->quoteName('a.id')) - ->join('INNER', $db->quoteName('#__workflow_stages', 'ws'), $db->quoteName('ws.id') . ' = ' . $db->quoteName('wa.stage_id')) - ->join('INNER', $db->quoteName('#__workflows', 'w'), $db->quoteName('w.id') . ' = ' . $db->quoteName('ws.workflow_id')); - - if (PluginHelper::isEnabled('content', 'vote')) - { - $query->select( - [ - 'COALESCE(NULLIF(ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 0), 0), 0)' - . ' AS ' . $db->quoteName('rating'), - 'COALESCE(NULLIF(' . $db->quoteName('v.rating_count') . ', 0), 0) AS ' . $db->quoteName('rating_count'), - ] - ) - ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')); - } - - // Join over the associations. - if (Associations::isEnabled()) - { - $subQuery = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') - ->from($db->quoteName('#__associations', 'asso1')) - ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) - ->where( - [ - $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), - $db->quoteName('asso1.context') . ' = ' . $db->quote('com_content.item'), - ] - ); - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); - } - - // Filter by access level. - $access = $this->getState('filter.access'); - - if (is_numeric($access)) - { - $access = (int) $access; - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - elseif (is_array($access)) - { - $access = ArrayHelper::toInteger($access); - $query->whereIn($db->quoteName('a.access'), $access); - } - - // Filter by featured. - $featured = (string) $this->getState('filter.featured'); - - if (\in_array($featured, ['0','1'])) - { - $featured = (int) $featured; - $query->where($db->quoteName('a.featured') . ' = :featured') - ->bind(':featured', $featured, ParameterType::INTEGER); - } - - // Filter by access level on categories. - if (!$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - $query->whereIn($db->quoteName('c.access'), $groups); - } - - // Filter by published state - $workflowStage = (string) $this->getState('filter.stage'); - - if ($params->get('workflow_enabled') && is_numeric($workflowStage)) - { - $workflowStage = (int) $workflowStage; - $query->where($db->quoteName('wa.stage_id') . ' = :stage') - ->bind(':stage', $workflowStage, ParameterType::INTEGER); - } - - $published = (string) $this->getState('filter.published'); - - if ($published !== '*') - { - if (is_numeric($published)) - { - $state = (int) $published; - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif (!is_numeric($workflowStage)) - { - $query->whereIn( - $db->quoteName('a.state'), - [ - ContentComponent::CONDITION_PUBLISHED, - ContentComponent::CONDITION_UNPUBLISHED, - ] - ); - } - } - - // Filter by categories and by level - $categoryId = $this->getState('filter.category_id', array()); - $level = (int) $this->getState('filter.level'); - - if (!is_array($categoryId)) - { - $categoryId = $categoryId ? array($categoryId) : array(); - } - - // Case: Using both categories filter and by level filter - if (count($categoryId)) - { - $categoryId = ArrayHelper::toInteger($categoryId); - $categoryTable = Table::getInstance('Category', 'JTable'); - $subCatItemsWhere = array(); - - foreach ($categoryId as $key => $filter_catid) - { - $categoryTable->load($filter_catid); - - // Because values to $query->bind() are passed by reference, using $query->bindArray() here instead to prevent overwriting. - $valuesToBind = [$categoryTable->lft, $categoryTable->rgt]; - - if ($level) - { - $valuesToBind[] = $level + $categoryTable->level - 1; - } - - // Bind values and get parameter names. - $bounded = $query->bindArray($valuesToBind); - - $categoryWhere = $db->quoteName('c.lft') . ' >= ' . $bounded[0] . ' AND ' . $db->quoteName('c.rgt') . ' <= ' . $bounded[1]; - - if ($level) - { - $categoryWhere .= ' AND ' . $db->quoteName('c.level') . ' <= ' . $bounded[2]; - } - - $subCatItemsWhere[] = '(' . $categoryWhere . ')'; - } - - $query->where('(' . implode(' OR ', $subCatItemsWhere) . ')'); - } - - // Case: Using only the by level filter - elseif ($level = (int) $level) - { - $query->where($db->quoteName('c.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter by author - $authorId = $this->getState('filter.author_id'); - - if (is_numeric($authorId)) - { - $authorId = (int) $authorId; - $type = $this->getState('filter.author_id.include', true) ? ' = ' : ' <> '; - $query->where($db->quoteName('a.created_by') . $type . ':authorId') - ->bind(':authorId', $authorId, ParameterType::INTEGER); - } - elseif (is_array($authorId)) - { - // Check to see if by_me is in the array - if (\in_array('by_me', $authorId)) - - // Replace by_me with the current user id in the array - { - $authorId['by_me'] = $user->id; - } - - $authorId = ArrayHelper::toInteger($authorId); - $query->whereIn($db->quoteName('a.created_by'), $authorId); - } - - // Filter by search in title. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - elseif (stripos($search, 'author:') === 0) - { - $search = '%' . substr($search, 7) . '%'; - $query->where('(' . $db->quoteName('ua.name') . ' LIKE :search1 OR ' . $db->quoteName('ua.username') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - elseif (stripos($search, 'content:') === 0) - { - $search = '%' . substr($search, 8) . '%'; - $query->where('(' . $db->quoteName('a.introtext') . ' LIKE :search1 OR ' . $db->quoteName('a.fulltext') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where( - '(' . $db->quoteName('a.title') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2' - . ' OR ' . $db->quoteName('a.note') . ' LIKE :search3)' - ) - ->bind([':search1', ':search2', ':search3'], $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - - // Filter by a single or group of tags. - $tag = $this->getState('filter.tag'); - - // Run simplified query when filtering by one tag. - if (\is_array($tag) && \count($tag) === 1) - { - $tag = $tag[0]; - } - - if ($tag && \is_array($tag)) - { - $tag = ArrayHelper::toInteger($tag); - - $subQuery = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('content_item_id')) - ->from($db->quoteName('#__contentitem_tag_map')) - ->where( - [ - $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', - $db->quoteName('type_alias') . ' = ' . $db->quote('com_content.article'), - ] - ); - - $query->join( - 'INNER', - '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ); - } - elseif ($tag = (int) $tag) - { - $query->join( - 'INNER', - $db->quoteName('#__contentitem_tag_map', 'tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ) - ->where( - [ - $db->quoteName('tagmap.tag_id') . ' = :tag', - $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_content.article'), - ] - ) - ->bind(':tag', $tag, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 'a.id'); - $orderDirn = $this->state->get('list.direction', 'DESC'); - - if ($orderCol === 'a.ordering' || $orderCol === 'category_title') - { - $ordering = [ - $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), - $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), - ]; - } - else - { - $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); - } - - $query->order($ordering); - - return $query; - } - - /** - * Method to get all transitions at once for all articles - * - * @return array|boolean - * - * @since 4.0.0 - */ - public function getTransitions() - { - // Get a storage key. - $store = $this->getStoreId('getTransitions'); - - // Try to load the data from internal storage. - if (isset($this->cache[$store])) - { - return $this->cache[$store]; - } - - $db = $this->getDatabase(); - $user = Factory::getUser(); - - $items = $this->getItems(); - - if ($items === false) - { - return false; - } - - $stage_ids = ArrayHelper::getColumn($items, 'stage_id'); - $stage_ids = ArrayHelper::toInteger($stage_ids); - $stage_ids = array_values(array_unique(array_filter($stage_ids))); - - $workflow_ids = ArrayHelper::getColumn($items, 'workflow_id'); - $workflow_ids = ArrayHelper::toInteger($workflow_ids); - $workflow_ids = array_values(array_unique(array_filter($workflow_ids))); - - $this->cache[$store] = array(); - - try - { - if (count($stage_ids) || count($workflow_ids)) - { - Factory::getLanguage()->load('com_workflow', JPATH_ADMINISTRATOR); - - $query = $db->getQuery(true); - - $query ->select( - [ - $db->quoteName('t.id', 'value'), - $db->quoteName('t.title', 'text'), - $db->quoteName('t.from_stage_id'), - $db->quoteName('t.to_stage_id'), - $db->quoteName('s.id', 'stage_id'), - $db->quoteName('s.title', 'stage_title'), - $db->quoteName('t.workflow_id'), - ] - ) - ->from($db->quoteName('#__workflow_transitions', 't')) - ->innerJoin( - $db->quoteName('#__workflow_stages', 's'), - $db->quoteName('t.to_stage_id') . ' = ' . $db->quoteName('s.id') - ) - ->where( - [ - $db->quoteName('t.published') . ' = 1', - $db->quoteName('s.published') . ' = 1', - ] - ) - ->order($db->quoteName('t.ordering')); - - $where = []; - - if (count($stage_ids)) - { - $where[] = $db->quoteName('t.from_stage_id') . ' IN (' . implode(',', $query->bindArray($stage_ids)) . ')'; - } - - if (count($workflow_ids)) - { - $where[] = '(' . $db->quoteName('t.from_stage_id') . ' = -1 AND ' . $db->quoteName('t.workflow_id') . ' IN (' . implode(',', $query->bindArray($workflow_ids)) . '))'; - } - - $query->where('((' . implode(') OR (', $where) . '))'); - - $transitions = $db->setQuery($query)->loadAssocList(); - - foreach ($transitions as $key => $transition) - { - if (!$user->authorise('core.execute.transition', 'com_content.transition.' . (int) $transition['value'])) - { - unset($transitions[$key]); - } - - $transitions[$key]['text'] = Text::_($transition['text']); - } - - $this->cache[$store] = $transitions; - } - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $this->cache[$store]; - } - - /** - * Method to get a list of articles. - * Overridden to add item type alias. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 4.0.0 - */ - public function getItems() - { - $items = parent::getItems(); - - foreach ($items as $item) - { - $item->typeAlias = 'com_content.article'; - - if (isset($item->metadata)) - { - $registry = new Registry($item->metadata); - $item->metadata = $registry->toArray(); - } - } - - return $items; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + * @see \Joomla\CMS\MVC\Controller\BaseController + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'alias', 'a.alias', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'catid', 'a.catid', 'category_title', + 'state', 'a.state', + 'access', 'a.access', 'access_level', + 'created', 'a.created', + 'modified', 'a.modified', + 'created_by', 'a.created_by', + 'created_by_alias', 'a.created_by_alias', + 'ordering', 'a.ordering', + 'featured', 'a.featured', + 'featured_up', 'fp.featured_up', + 'featured_down', 'fp.featured_down', + 'language', 'a.language', + 'hits', 'a.hits', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'published', 'a.published', + 'author_id', + 'category_id', + 'level', + 'tag', + 'rating_count', 'rating', + 'stage', 'wa.stage_id', + 'ws.title' + ); + + if (Associations::isEnabled()) { + $config['filter_fields'][] = 'association'; + } + } + + parent::__construct($config); + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return \Joomla\CMS\Form\Form|null The Form object or null if the form can't be found + * + * @since 3.2 + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + $params = ComponentHelper::getParams('com_content'); + + if (!$params->get('workflow_enabled')) { + $form->removeField('stage', 'filter'); + } else { + $ordering = $form->getField('fullordering', 'list'); + + $ordering->addOption('JSTAGE_ASC', ['value' => 'ws.title ASC']); + $ordering->addOption('JSTAGE_DESC', ['value' => 'ws.title DESC']); + } + + return $form; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.id', $direction = 'desc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); + $this->setState('filter.search', $search); + + $featured = $this->getUserStateFromRequest($this->context . '.filter.featured', 'filter_featured', ''); + $this->setState('filter.featured', $featured); + + $published = $this->getUserStateFromRequest($this->context . '.filter.published', 'filter_published', ''); + $this->setState('filter.published', $published); + + $level = $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level'); + $this->setState('filter.level', $level); + + $language = $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', ''); + $this->setState('filter.language', $language); + + $formSubmitted = $app->input->post->get('form_submitted'); + + // Gets the value of a user state variable and sets it in the session + $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access'); + $this->getUserStateFromRequest($this->context . '.filter.author_id', 'filter_author_id'); + $this->getUserStateFromRequest($this->context . '.filter.category_id', 'filter_category_id'); + $this->getUserStateFromRequest($this->context . '.filter.tag', 'filter_tag', ''); + + if ($formSubmitted) { + $access = $app->input->post->get('access'); + $this->setState('filter.access', $access); + + $authorId = $app->input->post->get('author_id'); + $this->setState('filter.author_id', $authorId); + + $categoryId = $app->input->post->get('category_id'); + $this->setState('filter.category_id', $categoryId); + + $tag = $app->input->post->get('tag'); + $this->setState('filter.tag', $tag); + } + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language + if (!empty($forcedLanguage)) { + $this->setState('filter.language', $forcedLanguage); + $this->setState('filter.forcedLanguage', $forcedLanguage); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . serialize($this->getState('filter.access')); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . serialize($this->getState('filter.category_id')); + $id .= ':' . serialize($this->getState('filter.author_id')); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . serialize($this->getState('filter.tag')); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + $params = ComponentHelper::getParams('com_content'); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.asset_id'), + $db->quoteName('a.title'), + $db->quoteName('a.alias'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.catid'), + $db->quoteName('a.state'), + $db->quoteName('a.access'), + $db->quoteName('a.created'), + $db->quoteName('a.created_by'), + $db->quoteName('a.created_by_alias'), + $db->quoteName('a.modified'), + $db->quoteName('a.ordering'), + $db->quoteName('a.featured'), + $db->quoteName('a.language'), + $db->quoteName('a.hits'), + $db->quoteName('a.publish_up'), + $db->quoteName('a.publish_down'), + $db->quoteName('a.introtext'), + $db->quoteName('a.fulltext'), + $db->quoteName('a.note'), + $db->quoteName('a.images'), + $db->quoteName('a.metakey'), + $db->quoteName('a.metadesc'), + $db->quoteName('a.metadata'), + $db->quoteName('a.version'), + ] + ) + ) + ->select( + [ + $db->quoteName('fp.featured_up'), + $db->quoteName('fp.featured_down'), + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + $db->quoteName('uc.name', 'editor'), + $db->quoteName('ag.title', 'access_level'), + $db->quoteName('c.title', 'category_title'), + $db->quoteName('c.created_user_id', 'category_uid'), + $db->quoteName('c.level', 'category_level'), + $db->quoteName('c.published', 'category_published'), + $db->quoteName('parent.title', 'parent_category_title'), + $db->quoteName('parent.id', 'parent_category_id'), + $db->quoteName('parent.created_user_id', 'parent_category_uid'), + $db->quoteName('parent.level', 'parent_category_level'), + $db->quoteName('ua.name', 'author_name'), + $db->quoteName('wa.stage_id', 'stage_id'), + $db->quoteName('ws.title', 'stage_title'), + $db->quoteName('ws.workflow_id', 'workflow_id'), + $db->quoteName('w.title', 'workflow_title'), + ] + ) + ->from($db->quoteName('#__content', 'a')) + ->where($db->quoteName('wa.extension') . ' = ' . $db->quote('com_content.article')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) + ->join('LEFT', $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) + ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) + ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')) + ->join('INNER', $db->quoteName('#__workflow_associations', 'wa'), $db->quoteName('wa.item_id') . ' = ' . $db->quoteName('a.id')) + ->join('INNER', $db->quoteName('#__workflow_stages', 'ws'), $db->quoteName('ws.id') . ' = ' . $db->quoteName('wa.stage_id')) + ->join('INNER', $db->quoteName('#__workflows', 'w'), $db->quoteName('w.id') . ' = ' . $db->quoteName('ws.workflow_id')); + + if (PluginHelper::isEnabled('content', 'vote')) { + $query->select( + [ + 'COALESCE(NULLIF(ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 0), 0), 0)' + . ' AS ' . $db->quoteName('rating'), + 'COALESCE(NULLIF(' . $db->quoteName('v.rating_count') . ', 0), 0) AS ' . $db->quoteName('rating_count'), + ] + ) + ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')); + } + + // Join over the associations. + if (Associations::isEnabled()) { + $subQuery = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') + ->from($db->quoteName('#__associations', 'asso1')) + ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) + ->where( + [ + $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), + $db->quoteName('asso1.context') . ' = ' . $db->quote('com_content.item'), + ] + ); + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); + } + + // Filter by access level. + $access = $this->getState('filter.access'); + + if (is_numeric($access)) { + $access = (int) $access; + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } elseif (is_array($access)) { + $access = ArrayHelper::toInteger($access); + $query->whereIn($db->quoteName('a.access'), $access); + } + + // Filter by featured. + $featured = (string) $this->getState('filter.featured'); + + if (\in_array($featured, ['0','1'])) { + $featured = (int) $featured; + $query->where($db->quoteName('a.featured') . ' = :featured') + ->bind(':featured', $featured, ParameterType::INTEGER); + } + + // Filter by access level on categories. + if (!$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + $query->whereIn($db->quoteName('c.access'), $groups); + } + + // Filter by published state + $workflowStage = (string) $this->getState('filter.stage'); + + if ($params->get('workflow_enabled') && is_numeric($workflowStage)) { + $workflowStage = (int) $workflowStage; + $query->where($db->quoteName('wa.stage_id') . ' = :stage') + ->bind(':stage', $workflowStage, ParameterType::INTEGER); + } + + $published = (string) $this->getState('filter.published'); + + if ($published !== '*') { + if (is_numeric($published)) { + $state = (int) $published; + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif (!is_numeric($workflowStage)) { + $query->whereIn( + $db->quoteName('a.state'), + [ + ContentComponent::CONDITION_PUBLISHED, + ContentComponent::CONDITION_UNPUBLISHED, + ] + ); + } + } + + // Filter by categories and by level + $categoryId = $this->getState('filter.category_id', array()); + $level = (int) $this->getState('filter.level'); + + if (!is_array($categoryId)) { + $categoryId = $categoryId ? array($categoryId) : array(); + } + + // Case: Using both categories filter and by level filter + if (count($categoryId)) { + $categoryId = ArrayHelper::toInteger($categoryId); + $categoryTable = Table::getInstance('Category', 'JTable'); + $subCatItemsWhere = array(); + + foreach ($categoryId as $key => $filter_catid) { + $categoryTable->load($filter_catid); + + // Because values to $query->bind() are passed by reference, using $query->bindArray() here instead to prevent overwriting. + $valuesToBind = [$categoryTable->lft, $categoryTable->rgt]; + + if ($level) { + $valuesToBind[] = $level + $categoryTable->level - 1; + } + + // Bind values and get parameter names. + $bounded = $query->bindArray($valuesToBind); + + $categoryWhere = $db->quoteName('c.lft') . ' >= ' . $bounded[0] . ' AND ' . $db->quoteName('c.rgt') . ' <= ' . $bounded[1]; + + if ($level) { + $categoryWhere .= ' AND ' . $db->quoteName('c.level') . ' <= ' . $bounded[2]; + } + + $subCatItemsWhere[] = '(' . $categoryWhere . ')'; + } + + $query->where('(' . implode(' OR ', $subCatItemsWhere) . ')'); + } + + // Case: Using only the by level filter + elseif ($level = (int) $level) { + $query->where($db->quoteName('c.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter by author + $authorId = $this->getState('filter.author_id'); + + if (is_numeric($authorId)) { + $authorId = (int) $authorId; + $type = $this->getState('filter.author_id.include', true) ? ' = ' : ' <> '; + $query->where($db->quoteName('a.created_by') . $type . ':authorId') + ->bind(':authorId', $authorId, ParameterType::INTEGER); + } elseif (is_array($authorId)) { + // Check to see if by_me is in the array + if (\in_array('by_me', $authorId)) { + // Replace by_me with the current user id in the array + $authorId['by_me'] = $user->id; + } + + $authorId = ArrayHelper::toInteger($authorId); + $query->whereIn($db->quoteName('a.created_by'), $authorId); + } + + // Filter by search in title. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } elseif (stripos($search, 'author:') === 0) { + $search = '%' . substr($search, 7) . '%'; + $query->where('(' . $db->quoteName('ua.name') . ' LIKE :search1 OR ' . $db->quoteName('ua.username') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } elseif (stripos($search, 'content:') === 0) { + $search = '%' . substr($search, 8) . '%'; + $query->where('(' . $db->quoteName('a.introtext') . ' LIKE :search1 OR ' . $db->quoteName('a.fulltext') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where( + '(' . $db->quoteName('a.title') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2' + . ' OR ' . $db->quoteName('a.note') . ' LIKE :search3)' + ) + ->bind([':search1', ':search2', ':search3'], $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + + // Filter by a single or group of tags. + $tag = $this->getState('filter.tag'); + + // Run simplified query when filtering by one tag. + if (\is_array($tag) && \count($tag) === 1) { + $tag = $tag[0]; + } + + if ($tag && \is_array($tag)) { + $tag = ArrayHelper::toInteger($tag); + + $subQuery = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('content_item_id')) + ->from($db->quoteName('#__contentitem_tag_map')) + ->where( + [ + $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', + $db->quoteName('type_alias') . ' = ' . $db->quote('com_content.article'), + ] + ); + + $query->join( + 'INNER', + '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ); + } elseif ($tag = (int) $tag) { + $query->join( + 'INNER', + $db->quoteName('#__contentitem_tag_map', 'tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ) + ->where( + [ + $db->quoteName('tagmap.tag_id') . ' = :tag', + $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_content.article'), + ] + ) + ->bind(':tag', $tag, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 'a.id'); + $orderDirn = $this->state->get('list.direction', 'DESC'); + + if ($orderCol === 'a.ordering' || $orderCol === 'category_title') { + $ordering = [ + $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), + $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), + ]; + } else { + $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); + } + + $query->order($ordering); + + return $query; + } + + /** + * Method to get all transitions at once for all articles + * + * @return array|boolean + * + * @since 4.0.0 + */ + public function getTransitions() + { + // Get a storage key. + $store = $this->getStoreId('getTransitions'); + + // Try to load the data from internal storage. + if (isset($this->cache[$store])) { + return $this->cache[$store]; + } + + $db = $this->getDatabase(); + $user = Factory::getUser(); + + $items = $this->getItems(); + + if ($items === false) { + return false; + } + + $stage_ids = ArrayHelper::getColumn($items, 'stage_id'); + $stage_ids = ArrayHelper::toInteger($stage_ids); + $stage_ids = array_values(array_unique(array_filter($stage_ids))); + + $workflow_ids = ArrayHelper::getColumn($items, 'workflow_id'); + $workflow_ids = ArrayHelper::toInteger($workflow_ids); + $workflow_ids = array_values(array_unique(array_filter($workflow_ids))); + + $this->cache[$store] = array(); + + try { + if (count($stage_ids) || count($workflow_ids)) { + Factory::getLanguage()->load('com_workflow', JPATH_ADMINISTRATOR); + + $query = $db->getQuery(true); + + $query ->select( + [ + $db->quoteName('t.id', 'value'), + $db->quoteName('t.title', 'text'), + $db->quoteName('t.from_stage_id'), + $db->quoteName('t.to_stage_id'), + $db->quoteName('s.id', 'stage_id'), + $db->quoteName('s.title', 'stage_title'), + $db->quoteName('t.workflow_id'), + ] + ) + ->from($db->quoteName('#__workflow_transitions', 't')) + ->innerJoin( + $db->quoteName('#__workflow_stages', 's'), + $db->quoteName('t.to_stage_id') . ' = ' . $db->quoteName('s.id') + ) + ->where( + [ + $db->quoteName('t.published') . ' = 1', + $db->quoteName('s.published') . ' = 1', + ] + ) + ->order($db->quoteName('t.ordering')); + + $where = []; + + if (count($stage_ids)) { + $where[] = $db->quoteName('t.from_stage_id') . ' IN (' . implode(',', $query->bindArray($stage_ids)) . ')'; + } + + if (count($workflow_ids)) { + $where[] = '(' . $db->quoteName('t.from_stage_id') . ' = -1 AND ' . $db->quoteName('t.workflow_id') . ' IN (' . implode(',', $query->bindArray($workflow_ids)) . '))'; + } + + $query->where('((' . implode(') OR (', $where) . '))'); + + $transitions = $db->setQuery($query)->loadAssocList(); + + foreach ($transitions as $key => $transition) { + if (!$user->authorise('core.execute.transition', 'com_content.transition.' . (int) $transition['value'])) { + unset($transitions[$key]); + } + + $transitions[$key]['text'] = Text::_($transition['text']); + } + + $this->cache[$store] = $transitions; + } + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $this->cache[$store]; + } + + /** + * Method to get a list of articles. + * Overridden to add item type alias. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 4.0.0 + */ + public function getItems() + { + $items = parent::getItems(); + + foreach ($items as $item) { + $item->typeAlias = 'com_content.article'; + + if (isset($item->metadata)) { + $registry = new Registry($item->metadata); + $item->metadata = $registry->toArray(); + } + } + + return $items; + } } diff --git a/administrator/components/com_content/src/Model/FeatureModel.php b/administrator/components/com_content/src/Model/FeatureModel.php index 8eebd6ae4d32b..44a659e6a0220 100644 --- a/administrator/components/com_content/src/Model/FeatureModel.php +++ b/administrator/components/com_content/src/Model/FeatureModel.php @@ -1,4 +1,5 @@ setState('filter.featured', 1); - } + // Filter by featured articles. + $this->setState('filter.featured', 1); + } - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 4.0.0 - */ - protected function getListQuery() - { - $query = parent::getListQuery(); + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 4.0.0 + */ + protected function getListQuery() + { + $query = parent::getListQuery(); - $query->select($this->getDatabase()->quoteName('fp.ordering')); + $query->select($this->getDatabase()->quoteName('fp.ordering')); - return $query; - } + return $query; + } } diff --git a/administrator/components/com_content/src/Service/HTML/AdministratorService.php b/administrator/components/com_content/src/Service/HTML/AdministratorService.php index 82691d820b8e9..c06ef4fcd5d82 100644 --- a/administrator/components/com_content/src/Service/HTML/AdministratorService.php +++ b/administrator/components/com_content/src/Service/HTML/AdministratorService.php @@ -1,4 +1,5 @@ $associated) - { - $associations[$tag] = (int) $associated->id; - } + // Get the associations + if ($associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $articleid)) { + foreach ($associations as $tag => $associated) { + $associations[$tag] = (int) $associated->id; + } - // Get the associated menu items - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - 'c.*', - $db->quoteName('l.sef', 'lang_sef'), - $db->quoteName('l.lang_code'), - $db->quoteName('cat.title', 'category_title'), - $db->quoteName('l.image'), - $db->quoteName('l.title', 'language_title'), - ] - ) - ->from($db->quoteName('#__content', 'c')) - ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) - ->whereIn($db->quoteName('c.id'), array_values($associations)) - ->where($db->quoteName('c.id') . ' != :articleId') - ->bind(':articleId', $articleid, ParameterType::INTEGER); + // Get the associated menu items + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + 'c.*', + $db->quoteName('l.sef', 'lang_sef'), + $db->quoteName('l.lang_code'), + $db->quoteName('cat.title', 'category_title'), + $db->quoteName('l.image'), + $db->quoteName('l.title', 'language_title'), + ] + ) + ->from($db->quoteName('#__content', 'c')) + ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) + ->whereIn($db->quoteName('c.id'), array_values($associations)) + ->where($db->quoteName('c.id') . ' != :articleId') + ->bind(':articleId', $articleid, ParameterType::INTEGER); - $db->setQuery($query); + $db->setQuery($query); - try - { - $items = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500, $e); - } + try { + $items = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500, $e); + } - if ($items) - { - $languages = LanguageHelper::getContentLanguages(array(0, 1)); - $content_languages = array_column($languages, 'lang_code'); + if ($items) { + $languages = LanguageHelper::getContentLanguages(array(0, 1)); + $content_languages = array_column($languages, 'lang_code'); - foreach ($items as &$item) - { - if (in_array($item->lang_code, $content_languages)) - { - $text = $item->lang_code; - $url = Route::_('index.php?option=com_content&task=article.edit&id=' . (int) $item->id); - $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
    ' - . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
    ' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); - $classes = 'badge bg-secondary'; + foreach ($items as &$item) { + if (in_array($item->lang_code, $content_languages)) { + $text = $item->lang_code; + $url = Route::_('index.php?option=com_content&task=article.edit&id=' . (int) $item->id); + $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
    ' + . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
    ' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); + $classes = 'badge bg-secondary'; - $item->link = '' . $text . '' - . ''; - } - else - { - // Display warning if Content Language is trashed or deleted - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); - } - } - } + $item->link = '' . $text . '' + . ''; + } else { + // Display warning if Content Language is trashed or deleted + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); + } + } + } - $html = LayoutHelper::render('joomla.content.associations', $items); - } + $html = LayoutHelper::render('joomla.content.associations', $items); + } - return $html; - } + return $html; + } } diff --git a/administrator/components/com_content/src/Service/HTML/Icon.php b/administrator/components/com_content/src/Service/HTML/Icon.php index b70cbaf279d4d..0d4028062bb74 100644 --- a/administrator/components/com_content/src/Service/HTML/Icon.php +++ b/administrator/components/com_content/src/Service/HTML/Icon.php @@ -1,4 +1,5 @@ id; - - $text = ''; - - if ($params->get('show_icons')) - { - $text .= ''; - } - - $text .= Text::_('COM_CONTENT_NEW_ARTICLE'); - - // Add the button classes to the attribs array - if (isset($attribs['class'])) - { - $attribs['class'] .= ' btn btn-primary'; - } - else - { - $attribs['class'] = 'btn btn-primary'; - } - - $button = HTMLHelper::_('link', Route::_($url), $text, $attribs); - - return $button; - } - - /** - * Display an edit icon for the article. - * - * This icon will not display in a popup window, nor if the article is trashed. - * Edit access checks must be performed in the calling code. - * - * @param object $article The article information - * @param Registry $params The item parameters - * @param array $attribs Optional attributes for the link - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML for the article edit icon. - * - * @since 4.0.0 - */ - public function edit($article, $params, $attribs = array(), $legacy = false) - { - $user = Factory::getUser(); - $uri = Uri::getInstance(); - - // Ignore if in a popup window. - if ($params && $params->get('popup')) - { - return ''; - } - - // Ignore if the state is negative (trashed). - if (!in_array($article->state, [Workflow::CONDITION_UNPUBLISHED, Workflow::CONDITION_PUBLISHED])) - { - return ''; - } - - // Show checked_out icon if the article is checked out by a different user - if (property_exists($article, 'checked_out') - && property_exists($article, 'checked_out_time') - && !is_null($article->checked_out) - && $article->checked_out != $user->get('id')) - { - $checkoutUser = Factory::getUser($article->checked_out); - $date = HTMLHelper::_('date', $article->checked_out_time); - $tooltip = Text::sprintf('COM_CONTENT_CHECKED_OUT_BY', $checkoutUser->name) - . '
    ' . $date; - - $text = LayoutHelper::render('joomla.content.icons.edit_lock', array('article' => $article, 'tooltip' => $tooltip, 'legacy' => $legacy)); - - $attribs['aria-describedby'] = 'editarticle-' . (int) $article->id; - $output = HTMLHelper::_('link', '#', $text, $attribs); - - return $output; - } - - $contentUrl = RouteHelper::getArticleRoute($article->slug, $article->catid, $article->language); - $url = $contentUrl . '&task=article.edit&a_id=' . $article->id . '&return=' . base64_encode($uri); - - if ($article->state == Workflow::CONDITION_UNPUBLISHED) - { - $tooltip = Text::_('COM_CONTENT_EDIT_UNPUBLISHED_ARTICLE'); - } - else - { - $tooltip = Text::_('COM_CONTENT_EDIT_PUBLISHED_ARTICLE'); - } - - $text = LayoutHelper::render('joomla.content.icons.edit', array('article' => $article, 'tooltip' => $tooltip, 'legacy' => $legacy)); - - $attribs['aria-describedby'] = 'editarticle-' . (int) $article->id; - $output = HTMLHelper::_('link', Route::_($url), $text, $attribs); - - return $output; - } - - /** - * Method to generate a link to print an article - * - * @param Registry $params The item parameters - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML markup for the popup link - * - * @since 4.0.0 - */ - public function print_screen($params, $legacy = false) - { - $text = LayoutHelper::render('joomla.content.icons.print_screen', array('params' => $params, 'legacy' => $legacy)); - - return ''; - } + /** + * Method to generate a link to the create item page for the given category + * + * @param object $category The category information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML markup for the create item link + * + * @since 4.0.0 + */ + public function create($category, $params, $attribs = array(), $legacy = false) + { + $uri = Uri::getInstance(); + + $url = 'index.php?option=com_content&task=article.add&return=' . base64_encode($uri) . '&a_id=0&catid=' . $category->id; + + $text = ''; + + if ($params->get('show_icons')) { + $text .= ''; + } + + $text .= Text::_('COM_CONTENT_NEW_ARTICLE'); + + // Add the button classes to the attribs array + if (isset($attribs['class'])) { + $attribs['class'] .= ' btn btn-primary'; + } else { + $attribs['class'] = 'btn btn-primary'; + } + + $button = HTMLHelper::_('link', Route::_($url), $text, $attribs); + + return $button; + } + + /** + * Display an edit icon for the article. + * + * This icon will not display in a popup window, nor if the article is trashed. + * Edit access checks must be performed in the calling code. + * + * @param object $article The article information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML for the article edit icon. + * + * @since 4.0.0 + */ + public function edit($article, $params, $attribs = array(), $legacy = false) + { + $user = Factory::getUser(); + $uri = Uri::getInstance(); + + // Ignore if in a popup window. + if ($params && $params->get('popup')) { + return ''; + } + + // Ignore if the state is negative (trashed). + if (!in_array($article->state, [Workflow::CONDITION_UNPUBLISHED, Workflow::CONDITION_PUBLISHED])) { + return ''; + } + + // Show checked_out icon if the article is checked out by a different user + if ( + property_exists($article, 'checked_out') + && property_exists($article, 'checked_out_time') + && !is_null($article->checked_out) + && $article->checked_out != $user->get('id') + ) { + $checkoutUser = Factory::getUser($article->checked_out); + $date = HTMLHelper::_('date', $article->checked_out_time); + $tooltip = Text::sprintf('COM_CONTENT_CHECKED_OUT_BY', $checkoutUser->name) + . '
    ' . $date; + + $text = LayoutHelper::render('joomla.content.icons.edit_lock', array('article' => $article, 'tooltip' => $tooltip, 'legacy' => $legacy)); + + $attribs['aria-describedby'] = 'editarticle-' . (int) $article->id; + $output = HTMLHelper::_('link', '#', $text, $attribs); + + return $output; + } + + $contentUrl = RouteHelper::getArticleRoute($article->slug, $article->catid, $article->language); + $url = $contentUrl . '&task=article.edit&a_id=' . $article->id . '&return=' . base64_encode($uri); + + if ($article->state == Workflow::CONDITION_UNPUBLISHED) { + $tooltip = Text::_('COM_CONTENT_EDIT_UNPUBLISHED_ARTICLE'); + } else { + $tooltip = Text::_('COM_CONTENT_EDIT_PUBLISHED_ARTICLE'); + } + + $text = LayoutHelper::render('joomla.content.icons.edit', array('article' => $article, 'tooltip' => $tooltip, 'legacy' => $legacy)); + + $attribs['aria-describedby'] = 'editarticle-' . (int) $article->id; + $output = HTMLHelper::_('link', Route::_($url), $text, $attribs); + + return $output; + } + + /** + * Method to generate a link to print an article + * + * @param Registry $params The item parameters + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML markup for the popup link + * + * @since 4.0.0 + */ + public function print_screen($params, $legacy = false) + { + $text = LayoutHelper::render('joomla.content.icons.print_screen', array('params' => $params, 'legacy' => $legacy)); + + return ''; + } } diff --git a/administrator/components/com_content/src/Table/ArticleTable.php b/administrator/components/com_content/src/Table/ArticleTable.php index 35d8f589f7254..76debc0d924f7 100644 --- a/administrator/components/com_content/src/Table/ArticleTable.php +++ b/administrator/components/com_content/src/Table/ArticleTable.php @@ -1,4 +1,5 @@ getLayout() == 'pagebreak') - { - parent::display($tpl); - - return; - } - - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - $this->canDo = ContentHelper::getActions('com_content', 'article', $this->item->id); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // If we are forcing a language in modal (used for associations). - if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) - { - // Set the language field to the forcedLanguage and disable changing it. - $this->form->setValue('language', null, $forcedLanguage); - $this->form->setFieldAttribute('language', 'readonly', 'true'); - - // Only allow to select categories with All language or with the forced language. - $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); - - // Only allow to select tags with All language or with the forced language. - $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * - * @throws \Exception - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - $user = $this->getCurrentUser(); - $userId = $user->id; - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Built the actions for new and existing records. - $canDo = $this->canDo; - - $toolbar = Toolbar::getInstance(); - - ToolbarHelper::title( - Text::_('COM_CONTENT_PAGE_' . ($checkedOut ? 'VIEW_ARTICLE' : ($isNew ? 'ADD_ARTICLE' : 'EDIT_ARTICLE'))), - 'pencil-alt article-add' - ); - - // For new records, check the create permission. - if ($isNew && (count($user->getAuthorisedCategories('com_content', 'core.create')) > 0)) - { - $toolbar->apply('article.apply'); - - $saveGroup = $toolbar->dropdownButton('save-group'); - - $saveGroup->configure( - function (Toolbar $childBar) use ($user) - { - $childBar->save('article.save'); - - if ($user->authorise('core.create', 'com_menus.menu')) - { - $childBar->save('article.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); - } - - $childBar->save2new('article.save2new'); - } - ); - - $toolbar->cancel('article.cancel', 'JTOOLBAR_CANCEL'); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - if (!$checkedOut && $itemEditable) - { - $toolbar->apply('article.apply'); - } - - $saveGroup = $toolbar->dropdownButton('save-group'); - - $saveGroup->configure( - function (Toolbar $childBar) use ($checkedOut, $itemEditable, $canDo, $user) - { - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - $childBar->save('article.save'); - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $childBar->save2new('article.save2new'); - } - } - - // If checked out, we can still save2menu - if ($user->authorise('core.create', 'com_menus.menu')) - { - $childBar->save('article.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); - } - - // If checked out, we can still save - if ($canDo->get('core.create')) - { - $childBar->save2copy('article.save2copy'); - } - } - ); - - $toolbar->cancel('article.cancel', 'JTOOLBAR_CLOSE'); - - if (!$isNew) - { - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) - { - $toolbar->versions('com_content.article', $this->item->id); - } - - $url = RouteHelper::getArticleRoute($this->item->id . ':' . $this->item->alias, $this->item->catid, $this->item->language); - - $toolbar->preview(Route::link('site', $url, true), 'JGLOBAL_PREVIEW') - ->bodyHeight(80) - ->modalWidth(90); - - if (PluginHelper::isEnabled('system', 'jooa11y')) - { - $toolbar->jooa11y(Route::link('site', $url . '&jooa11y=1', true), 'JGLOBAL_JOOA11Y') - ->bodyHeight(80) - ->modalWidth(90); - } - - if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) - { - $toolbar->standardButton('contract') - ->text('JTOOLBAR_ASSOCIATIONS') - ->task('article.editAssociations'); - } - } - } - - $toolbar->divider(); - - ToolbarHelper::inlinehelp(); - - $toolbar->help('Articles:_Edit'); - } + /** + * The \JForm object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var object + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $canDo; + + /** + * Pagebreak TOC alias + * + * @var string + */ + protected $eName; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws \Exception + */ + public function display($tpl = null) + { + if ($this->getLayout() == 'pagebreak') { + parent::display($tpl); + + return; + } + + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + $this->canDo = ContentHelper::getActions('com_content', 'article', $this->item->id); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // If we are forcing a language in modal (used for associations). + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) { + // Set the language field to the forcedLanguage and disable changing it. + $this->form->setValue('language', null, $forcedLanguage); + $this->form->setFieldAttribute('language', 'readonly', 'true'); + + // Only allow to select categories with All language or with the forced language. + $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); + + // Only allow to select tags with All language or with the forced language. + $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * + * @throws \Exception + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Built the actions for new and existing records. + $canDo = $this->canDo; + + $toolbar = Toolbar::getInstance(); + + ToolbarHelper::title( + Text::_('COM_CONTENT_PAGE_' . ($checkedOut ? 'VIEW_ARTICLE' : ($isNew ? 'ADD_ARTICLE' : 'EDIT_ARTICLE'))), + 'pencil-alt article-add' + ); + + // For new records, check the create permission. + if ($isNew && (count($user->getAuthorisedCategories('com_content', 'core.create')) > 0)) { + $toolbar->apply('article.apply'); + + $saveGroup = $toolbar->dropdownButton('save-group'); + + $saveGroup->configure( + function (Toolbar $childBar) use ($user) { + $childBar->save('article.save'); + + if ($user->authorise('core.create', 'com_menus.menu')) { + $childBar->save('article.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); + } + + $childBar->save2new('article.save2new'); + } + ); + + $toolbar->cancel('article.cancel', 'JTOOLBAR_CANCEL'); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + if (!$checkedOut && $itemEditable) { + $toolbar->apply('article.apply'); + } + + $saveGroup = $toolbar->dropdownButton('save-group'); + + $saveGroup->configure( + function (Toolbar $childBar) use ($checkedOut, $itemEditable, $canDo, $user) { + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + $childBar->save('article.save'); + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $childBar->save2new('article.save2new'); + } + } + + // If checked out, we can still save2menu + if ($user->authorise('core.create', 'com_menus.menu')) { + $childBar->save('article.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); + } + + // If checked out, we can still save + if ($canDo->get('core.create')) { + $childBar->save2copy('article.save2copy'); + } + } + ); + + $toolbar->cancel('article.cancel', 'JTOOLBAR_CLOSE'); + + if (!$isNew) { + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) { + $toolbar->versions('com_content.article', $this->item->id); + } + + $url = RouteHelper::getArticleRoute($this->item->id . ':' . $this->item->alias, $this->item->catid, $this->item->language); + + $toolbar->preview(Route::link('site', $url, true), 'JGLOBAL_PREVIEW') + ->bodyHeight(80) + ->modalWidth(90); + + if (PluginHelper::isEnabled('system', 'jooa11y')) { + $toolbar->jooa11y(Route::link('site', $url . '&jooa11y=1', true), 'JGLOBAL_JOOA11Y') + ->bodyHeight(80) + ->modalWidth(90); + } + + if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) { + $toolbar->standardButton('contract') + ->text('JTOOLBAR_ASSOCIATIONS') + ->task('article.editAssociations'); + } + } + } + + $toolbar->divider(); + + ToolbarHelper::inlinehelp(); + + $toolbar->help('Articles:_Edit'); + } } diff --git a/administrator/components/com_content/src/View/Articles/HtmlView.php b/administrator/components/com_content/src/View/Articles/HtmlView.php index 8461c2980cb3d..223995a49b8d4 100644 --- a/administrator/components/com_content/src/View/Articles/HtmlView.php +++ b/administrator/components/com_content/src/View/Articles/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->vote = PluginHelper::isEnabled('content', 'vote'); - $this->hits = ComponentHelper::getParams('com_content')->get('record_hits', 1); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - if (ComponentHelper::getParams('com_content')->get('workflow_enabled')) - { - PluginHelper::importPlugin('workflow'); - - $this->transitions = $this->get('Transitions'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors')) || $this->transitions === false) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - else - { - // In article associations modal we need to remove language filter if forcing a language. - // We also need to change the category filter to show show categories with All or the forced language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. - $languageXml = new \SimpleXMLElement(''); - $this->filterForm->setField($languageXml, 'filter', true); - - // Also, unset the active language filter so the search tools is not open by default with this filter. - unset($this->activeFilters['language']); - - // One last changes needed is to change the category filter to just show categories with All language or with the forced language. - $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_content', 'category', $this->state->get('filter.category_id')); - $user = $this->getCurrentUser(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_CONTENT_ARTICLES_TITLE'), 'copy article'); - - if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) - { - $toolbar->addNew('article.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || \count($this->transitions))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if (\count($this->transitions)) - { - $childBar->separatorButton('transition-headline') - ->text('COM_CONTENT_RUN_TRANSITIONS') - ->buttonClass('text-center py-2 h3'); - - $cmd = "Joomla.submitbutton('articles.runTransition');"; - $messages = "{error: [Joomla.JText._('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST')]}"; - $alert = 'Joomla.renderMessages(' . $messages . ')'; - $cmd = 'if (document.adminForm.boxchecked.value == 0) { ' . $alert . ' } else { ' . $cmd . ' }'; - - foreach ($this->transitions as $transition) - { - $childBar->standardButton('transition') - ->text($transition['text']) - ->buttonClass('transition-' . (int) $transition['value']) - ->icon('icon-project-diagram') - ->onclick('document.adminForm.transition_id.value=' . (int) $transition['value'] . ';' . $cmd); - } - - $childBar->separatorButton('transition-separator'); - } - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('articles.publish')->listCheck(true); - - $childBar->unpublish('articles.unpublish')->listCheck(true); - - $childBar->standardButton('featured') - ->text('JFEATURE') - ->task('articles.featured') - ->listCheck(true); - - $childBar->standardButton('unfeatured') - ->text('JUNFEATURE') - ->task('articles.unfeatured') - ->listCheck(true); - - $childBar->archive('articles.archive')->listCheck(true); - - $childBar->checkin('articles.checkin')->listCheck(true); - - if ($this->state->get('filter.published') != ContentComponent::CONDITION_TRASHED) - { - $childBar->trash('articles.trash')->listCheck(true); - } - } - - // Add a batch button - if ($user->authorise('core.create', 'com_content') - && $user->authorise('core.edit', 'com_content') - && $user->authorise('core.execute.transition', 'com_content')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED && $canDo->get('core.delete')) - { - $toolbar->delete('articles.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($user->authorise('core.admin', 'com_content') || $user->authorise('core.options', 'com_content')) - { - $toolbar->preferences('com_content'); - } - - $toolbar->help('Articles'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * All transition, which can be executed of one if the items + * + * @var array + */ + protected $transitions = []; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->vote = PluginHelper::isEnabled('content', 'vote'); + $this->hits = ComponentHelper::getParams('com_content')->get('record_hits', 1); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + if (ComponentHelper::getParams('com_content')->get('workflow_enabled')) { + PluginHelper::importPlugin('workflow'); + + $this->transitions = $this->get('Transitions'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors')) || $this->transitions === false) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // In article associations modal we need to remove language filter if forcing a language. + // We also need to change the category filter to show show categories with All or the forced language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. + $languageXml = new \SimpleXMLElement(''); + $this->filterForm->setField($languageXml, 'filter', true); + + // Also, unset the active language filter so the search tools is not open by default with this filter. + unset($this->activeFilters['language']); + + // One last changes needed is to change the category filter to just show categories with All language or with the forced language. + $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_content', 'category', $this->state->get('filter.category_id')); + $user = $this->getCurrentUser(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_CONTENT_ARTICLES_TITLE'), 'copy article'); + + if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) { + $toolbar->addNew('article.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || \count($this->transitions))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if (\count($this->transitions)) { + $childBar->separatorButton('transition-headline') + ->text('COM_CONTENT_RUN_TRANSITIONS') + ->buttonClass('text-center py-2 h3'); + + $cmd = "Joomla.submitbutton('articles.runTransition');"; + $messages = "{error: [Joomla.JText._('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST')]}"; + $alert = 'Joomla.renderMessages(' . $messages . ')'; + $cmd = 'if (document.adminForm.boxchecked.value == 0) { ' . $alert . ' } else { ' . $cmd . ' }'; + + foreach ($this->transitions as $transition) { + $childBar->standardButton('transition') + ->text($transition['text']) + ->buttonClass('transition-' . (int) $transition['value']) + ->icon('icon-project-diagram') + ->onclick('document.adminForm.transition_id.value=' . (int) $transition['value'] . ';' . $cmd); + } + + $childBar->separatorButton('transition-separator'); + } + + if ($canDo->get('core.edit.state')) { + $childBar->publish('articles.publish')->listCheck(true); + + $childBar->unpublish('articles.unpublish')->listCheck(true); + + $childBar->standardButton('featured') + ->text('JFEATURE') + ->task('articles.featured') + ->listCheck(true); + + $childBar->standardButton('unfeatured') + ->text('JUNFEATURE') + ->task('articles.unfeatured') + ->listCheck(true); + + $childBar->archive('articles.archive')->listCheck(true); + + $childBar->checkin('articles.checkin')->listCheck(true); + + if ($this->state->get('filter.published') != ContentComponent::CONDITION_TRASHED) { + $childBar->trash('articles.trash')->listCheck(true); + } + } + + // Add a batch button + if ( + $user->authorise('core.create', 'com_content') + && $user->authorise('core.edit', 'com_content') + && $user->authorise('core.execute.transition', 'com_content') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED && $canDo->get('core.delete')) { + $toolbar->delete('articles.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($user->authorise('core.admin', 'com_content') || $user->authorise('core.options', 'com_content')) { + $toolbar->preferences('com_content'); + } + + $toolbar->help('Articles'); + } } diff --git a/administrator/components/com_content/src/View/Featured/HtmlView.php b/administrator/components/com_content/src/View/Featured/HtmlView.php index ca034b3767112..43b0adeba999e 100644 --- a/administrator/components/com_content/src/View/Featured/HtmlView.php +++ b/administrator/components/com_content/src/View/Featured/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->vote = PluginHelper::isEnabled('content', 'vote'); - $this->hits = ComponentHelper::getParams('com_content')->get('record_hits', 1); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - if (ComponentHelper::getParams('com_content')->get('workflow_enabled')) - { - PluginHelper::importPlugin('workflow'); - - $this->transitions = $this->get('Transitions'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_content', 'category', $this->state->get('filter.category_id')); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_CONTENT_FEATURED_TITLE'), 'star featured'); - - if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) - { - $toolbar->addNew('article.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || \count($this->transitions))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if (\count($this->transitions)) - { - $childBar->separatorButton('transition-headline') - ->text('COM_CONTENT_RUN_TRANSITIONS') - ->buttonClass('text-center py-2 h3'); - - $cmd = "Joomla.submitbutton('articles.runTransition');"; - $messages = "{error: [Joomla.JText._('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST')]}"; - $alert = 'Joomla.renderMessages(' . $messages . ')'; - $cmd = 'if (document.adminForm.boxchecked.value == 0) { ' . $alert . ' } else { ' . $cmd . ' }'; - - foreach ($this->transitions as $transition) - { - $childBar->standardButton('transition') - ->text($transition['text']) - ->buttonClass('transition-' . (int) $transition['value']) - ->icon('icon-project-diagram') - ->onclick('document.adminForm.transition_id.value=' . (int) $transition['value'] . ';' . $cmd); - } - - $childBar->separatorButton('transition-separator'); - } - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('articles.publish')->listCheck(true); - - $childBar->unpublish('articles.unpublish')->listCheck(true); - - $childBar->standardButton('unfeatured') - ->text('JUNFEATURE') - ->task('articles.unfeatured') - ->listCheck(true); - - $childBar->archive('articles.archive')->listCheck(true); - - $childBar->checkin('articles.checkin')->listCheck(true); - - if (!$this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED) - { - $childBar->trash('articles.trash')->listCheck(true); - } - } - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED && $canDo->get('core.delete')) - { - $toolbar->delete('articles.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($user->authorise('core.admin', 'com_content') || $user->authorise('core.options', 'com_content')) - { - $toolbar->preferences('com_content'); - } - - ToolbarHelper::help('Articles:_Featured'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * All transition, which can be executed of one if the items + * + * @var array + */ + protected $transitions = []; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->vote = PluginHelper::isEnabled('content', 'vote'); + $this->hits = ComponentHelper::getParams('com_content')->get('record_hits', 1); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + if (ComponentHelper::getParams('com_content')->get('workflow_enabled')) { + PluginHelper::importPlugin('workflow'); + + $this->transitions = $this->get('Transitions'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_content', 'category', $this->state->get('filter.category_id')); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_CONTENT_FEATURED_TITLE'), 'star featured'); + + if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) { + $toolbar->addNew('article.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || \count($this->transitions))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if (\count($this->transitions)) { + $childBar->separatorButton('transition-headline') + ->text('COM_CONTENT_RUN_TRANSITIONS') + ->buttonClass('text-center py-2 h3'); + + $cmd = "Joomla.submitbutton('articles.runTransition');"; + $messages = "{error: [Joomla.JText._('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST')]}"; + $alert = 'Joomla.renderMessages(' . $messages . ')'; + $cmd = 'if (document.adminForm.boxchecked.value == 0) { ' . $alert . ' } else { ' . $cmd . ' }'; + + foreach ($this->transitions as $transition) { + $childBar->standardButton('transition') + ->text($transition['text']) + ->buttonClass('transition-' . (int) $transition['value']) + ->icon('icon-project-diagram') + ->onclick('document.adminForm.transition_id.value=' . (int) $transition['value'] . ';' . $cmd); + } + + $childBar->separatorButton('transition-separator'); + } + + if ($canDo->get('core.edit.state')) { + $childBar->publish('articles.publish')->listCheck(true); + + $childBar->unpublish('articles.unpublish')->listCheck(true); + + $childBar->standardButton('unfeatured') + ->text('JUNFEATURE') + ->task('articles.unfeatured') + ->listCheck(true); + + $childBar->archive('articles.archive')->listCheck(true); + + $childBar->checkin('articles.checkin')->listCheck(true); + + if (!$this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED) { + $childBar->trash('articles.trash')->listCheck(true); + } + } + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED && $canDo->get('core.delete')) { + $toolbar->delete('articles.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($user->authorise('core.admin', 'com_content') || $user->authorise('core.options', 'com_content')) { + $toolbar->preferences('com_content'); + } + + ToolbarHelper::help('Articles:_Featured'); + } } diff --git a/administrator/components/com_content/tmpl/article/edit.php b/administrator/components/com_content/tmpl/article/edit.php index 29ce01e10f511..969455f185a16 100644 --- a/administrator/components/com_content/tmpl/article/edit.php +++ b/administrator/components/com_content/tmpl/article/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->getRegistry()->addExtensionRegistryFile('com_contenthistory'); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_contenthistory.admin-history-versions'); + ->useScript('form.validate') + ->useScript('com_contenthistory.admin-history-versions'); $this->configFieldsets = array('editorConfig'); $this->hiddenFieldsets = array('basic-limited'); @@ -42,15 +43,13 @@ $assoc = Associations::isEnabled(); $showArticleOptions = $params->get('show_article_options', 1); -if (!$assoc || !$showArticleOptions) -{ - $this->ignore_fieldsets[] = 'frontendassociations'; +if (!$assoc || !$showArticleOptions) { + $this->ignore_fieldsets[] = 'frontendassociations'; } -if (!$showArticleOptions) -{ - // Ignore fieldsets inside Options tab - $this->ignore_fieldsets = array_merge($this->ignore_fieldsets, ['attribs', 'basic', 'category', 'author', 'date', 'other']); +if (!$showArticleOptions) { + // Ignore fieldsets inside Options tab + $this->ignore_fieldsets = array_merge($this->ignore_fieldsets, ['attribs', 'basic', 'category', 'author', 'date', 'other']); } // In case of modal @@ -59,129 +58,129 @@ $tmpl = $isModal || $input->get('tmpl', '', 'cmd') === 'component' ? '&tmpl=component' : ''; ?> - - -
    - 'general', 'recall' => true, 'breakpoint' => 768]); ?> - - -
    -
    -
    -
    - form->getLabel('articletext'); ?> - form->getInput('articletext'); ?> -
    -
    -
    -
    - -
    -
    - - - - - get('show_urls_images_backend') == 1) : ?> - -
    -
    - -
    - form->getFieldsets()[$fieldset]->label); ?> -
    - form->renderFieldset($fieldset); ?> -
    -
    - -
    -
    - -
    - form->getFieldsets()[$fieldset]->label); ?> -
    - form->renderFieldset($fieldset); ?> -
    -
    - -
    -
    - - - - - - - - get('show_publishing_options', 1) == 1) : ?> - -
    -
    -
    - -
    - -
    -
    -
    -
    -
    - -
    - -
    -
    -
    -
    - - - - get('show_associations_edit', 1) == 1) : ?> - -
    - -
    - -
    -
    - - - - - - canDo->get('core.admin') && $params->get('show_configure_edit_options', 1) == 1) : ?> - -
    - -
    - form->renderFieldset('editorConfig'); ?> -
    -
    - - - - canDo->get('core.admin') && $params->get('show_permissions', 1) == 1) : ?> - -
    - -
    - form->getInput('rules'); ?> -
    -
    - - - - - - - get('show_publishing_options', 1) == 0) : ?> - form->getInput('id'); ?> - - - - - - - -
    + + +
    + 'general', 'recall' => true, 'breakpoint' => 768]); ?> + + +
    +
    +
    +
    + form->getLabel('articletext'); ?> + form->getInput('articletext'); ?> +
    +
    +
    +
    + +
    +
    + + + + + get('show_urls_images_backend') == 1) : ?> + +
    +
    + +
    + form->getFieldsets()[$fieldset]->label); ?> +
    + form->renderFieldset($fieldset); ?> +
    +
    + +
    +
    + +
    + form->getFieldsets()[$fieldset]->label); ?> +
    + form->renderFieldset($fieldset); ?> +
    +
    + +
    +
    + + + + + + + + get('show_publishing_options', 1) == 1) : ?> + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + + + + get('show_associations_edit', 1) == 1) : ?> + +
    + +
    + +
    +
    + + + + + + canDo->get('core.admin') && $params->get('show_configure_edit_options', 1) == 1) : ?> + +
    + +
    + form->renderFieldset('editorConfig'); ?> +
    +
    + + + + canDo->get('core.admin') && $params->get('show_permissions', 1) == 1) : ?> + +
    + +
    + form->getInput('rules'); ?> +
    +
    + + + + + + + get('show_publishing_options', 1) == 0) : ?> + form->getInput('id'); ?> + + + + + + + +
    diff --git a/administrator/components/com_content/tmpl/article/modal.php b/administrator/components/com_content/tmpl/article/modal.php index 9147d5b5ed403..f6d9da1f17513 100644 --- a/administrator/components/com_content/tmpl/article/modal.php +++ b/administrator/components/com_content/tmpl/article/modal.php @@ -1,4 +1,5 @@
    - setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
    diff --git a/administrator/components/com_content/tmpl/article/pagebreak.php b/administrator/components/com_content/tmpl/article/pagebreak.php index 53a8211f88bf3..4612825d783f3 100644 --- a/administrator/components/com_content/tmpl/article/pagebreak.php +++ b/administrator/components/com_content/tmpl/article/pagebreak.php @@ -1,4 +1,5 @@
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    - - - -
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    + + + +
    diff --git a/administrator/components/com_content/tmpl/articles/default.php b/administrator/components/com_content/tmpl/articles/default.php index cfbecfcc46a14..5645f3c3323ad 100644 --- a/administrator/components/com_content/tmpl/articles/default.php +++ b/administrator/components/com_content/tmpl/articles/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $app = Factory::getApplication(); $user = Factory::getUser(); @@ -36,27 +37,19 @@ $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'a.ordering'; -if (strpos($listOrder, 'publish_up') !== false) -{ - $orderingColumn = 'publish_up'; -} -elseif (strpos($listOrder, 'publish_down') !== false) -{ - $orderingColumn = 'publish_down'; -} -elseif (strpos($listOrder, 'modified') !== false) -{ - $orderingColumn = 'modified'; -} -else -{ - $orderingColumn = 'created'; +if (strpos($listOrder, 'publish_up') !== false) { + $orderingColumn = 'publish_up'; +} elseif (strpos($listOrder, 'publish_down') !== false) { + $orderingColumn = 'publish_down'; +} elseif (strpos($listOrder, 'modified') !== false) { + $orderingColumn = 'modified'; +} else { + $orderingColumn = 'created'; } -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_content&task=articles.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_content&task=articles.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $workflow_enabled = ComponentHelper::getParams('com_content')->get('workflow_enabled'); @@ -64,9 +57,8 @@ $workflow_featured = false; if ($workflow_enabled) : - // @todo move the script to a file -$js = <<getRegistry()->addExtensionRegistryFile('com_workflow'); -$wa->useScript('com_workflow.admin-items-workflow-buttons') - ->addInlineScript($js, [], ['type' => 'module']); - -$workflow_state = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.state', 'com_content.article'); -$workflow_featured = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article'); + $wa->getRegistry()->addExtensionRegistryFile('com_workflow'); + $wa->useScript('com_workflow.admin-items-workflow-buttons') + ->addInlineScript($js, [], ['type' => 'module']); + $workflow_state = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.state', 'com_content.article'); + $workflow_featured = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article'); endif; $assoc = Associations::isEnabled(); ?>
    -
    -
    -
    - $this)); - ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - - - - - - hits) : ?> - - - vote) : ?> - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : - $item->max_ordering = 0; - $canEdit = $user->authorise('core.edit', 'com_content.article.' . $item->id); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canEditOwn = $user->authorise('core.edit.own', 'com_content.article.' . $item->id) && $item->created_by == $userId; - $canChange = $user->authorise('core.edit.state', 'com_content.article.' . $item->id) && $canCheckin; - $canEditCat = $user->authorise('core.edit', 'com_content.category.' . $item->catid); - $canEditOwnCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->catid) && $item->category_uid == $userId; - $canEditParCat = $user->authorise('core.edit', 'com_content.category.' . $item->parent_category_id); - $canEditOwnParCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->parent_category_id) && $item->parent_category_uid == $userId; +
    +
    +
    + $this)); + ?> + items)) : ?> +
    + + +
    + +
    - , - , - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    + + + + + + + + + + + + + + + + + + + + + hits) : ?> + + + vote) : ?> + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : + $item->max_ordering = 0; + $canEdit = $user->authorise('core.edit', 'com_content.article.' . $item->id); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', 'com_content.article.' . $item->id) && $item->created_by == $userId; + $canChange = $user->authorise('core.edit.state', 'com_content.article.' . $item->id) && $canCheckin; + $canEditCat = $user->authorise('core.edit', 'com_content.category.' . $item->catid); + $canEditOwnCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->catid) && $item->category_uid == $userId; + $canEditParCat = $user->authorise('core.edit', 'com_content.category.' . $item->parent_category_id); + $canEditOwnParCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->parent_category_id) && $item->parent_category_uid == $userId; - $transitions = ContentHelper::filterTransitions($this->transitions, (int) $item->stage_id, (int) $item->workflow_id); + $transitions = ContentHelper::filterTransitions($this->transitions, (int) $item->stage_id, (int) $item->workflow_id); - $transition_ids = ArrayHelper::getColumn($transitions, 'value'); - $transition_ids = ArrayHelper::toInteger($transition_ids); + $transition_ids = ArrayHelper::getColumn($transitions, 'value'); + $transition_ids = ArrayHelper::toInteger($transition_ids); - ?> - - - - - + + + + - - + + - + - - - - - - - - - - - hits) : ?> - - - vote) : ?> - - - - - - - -
    + , + , + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    - id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - $transitions, - 'title' => Text::_($item->stage_title), - 'tip_content' => Text::sprintf('JWORKFLOW', Text::_($item->workflow_title)), - 'id' => 'workflow-' . $item->id, - 'task' => 'articles.runTransition' - ]; + ?> +
    + id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + $transitions, + 'title' => Text::_($item->stage_title), + 'tip_content' => Text::sprintf('JWORKFLOW', Text::_($item->workflow_title)), + 'id' => 'workflow-' . $item->id, + 'task' => 'articles.runTransition' + ]; - echo (new TransitionButton($options)) - ->render(0, $i); - ?> -
    - stage_title); ?> -
    -
    - 'articles.', - 'disabled' => $workflow_featured || !$canChange, - 'id' => 'featured-' . $item->id - ]; + echo (new TransitionButton($options)) + ->render(0, $i); + ?> +
    + stage_title); ?> +
    +
    + 'articles.', + 'disabled' => $workflow_featured || !$canChange, + 'id' => 'featured-' . $item->id + ]; - echo (new FeaturedButton) - ->render((int) $item->featured, $i, $options, $item->featured_up, $item->featured_down); - ?> - - 'articles.', - 'disabled' => $workflow_state || !$canChange, - 'id' => 'state-' . $item->id, - 'category_published' => $item->category_published - ]; + echo (new FeaturedButton()) + ->render((int) $item->featured, $i, $options, $item->featured_up, $item->featured_down); + ?> + + 'articles.', + 'disabled' => $workflow_state || !$canChange, + 'id' => 'state-' . $item->id, + 'category_published' => $item->category_published + ]; - echo (new PublishedButton)->render((int) $item->state, $i, $options, $item->publish_up, $item->publish_down); - ?> - -
    - checked_out) : ?> - editor, $item->checked_out_time, 'articles.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
    - note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - -
    -
    - parent_category_id . '&extension=com_content'); - $CurrentCatUrl = Route::_('index.php?option=com_categories&task=category.edit&id=' . $item->catid . '&extension=com_content'); - $EditCatTxt = Text::_('COM_CONTENT_EDIT_CATEGORY'); - echo Text::_('JCATEGORY') . ': '; - if ($item->category_level != '1') : - if ($item->parent_category_level != '1') : - echo ' » '; - endif; - endif; - if (Factory::getLanguage()->isRtl()) - { - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - echo $this->escape($item->category_title); - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - if ($item->category_level != '1') : - echo ' « '; - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo $this->escape($item->parent_category_title); - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - endif; - } - else - { - if ($item->category_level != '1') : - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo $this->escape($item->parent_category_title); - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo ' » '; - endif; - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - echo $this->escape($item->category_title); - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - } - if ($item->category_published < '1') : - echo $item->category_published == '0' ? ' (' . Text::_('JUNPUBLISHED') . ')' : ' (' . Text::_('JTRASHED') . ')'; - endif; - ?> -
    -
    -
    - escape($item->access_level); ?> - - created_by != 0) : ?> - - escape($item->author_name); ?> - - - - - created_by_alias) : ?> -
    escape($item->created_by_alias)); ?>
    - -
    - association) : ?> - id); ?> - - - - - {$orderingColumn}; - echo $date > 0 ? HTMLHelper::_('date', $date, Text::_('DATE_FORMAT_LC4')) : '-'; - ?> - - - hits; ?> - - - - rating_count; ?> - - - - rating; ?> - - - id; ?> -
    + echo (new PublishedButton())->render((int) $item->state, $i, $options, $item->publish_up, $item->publish_down); + ?> + + +
    + checked_out) : ?> + editor, $item->checked_out_time, 'articles.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
    + note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
    +
    + parent_category_id . '&extension=com_content'); + $CurrentCatUrl = Route::_('index.php?option=com_categories&task=category.edit&id=' . $item->catid . '&extension=com_content'); + $EditCatTxt = Text::_('COM_CONTENT_EDIT_CATEGORY'); + echo Text::_('JCATEGORY') . ': '; + if ($item->category_level != '1') : + if ($item->parent_category_level != '1') : + echo ' » '; + endif; + endif; + if (Factory::getLanguage()->isRtl()) { + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + echo $this->escape($item->category_title); + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + if ($item->category_level != '1') : + echo ' « '; + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo $this->escape($item->parent_category_title); + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + endif; + } else { + if ($item->category_level != '1') : + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo $this->escape($item->parent_category_title); + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo ' » '; + endif; + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + echo $this->escape($item->category_title); + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + } + if ($item->category_published < '1') : + echo $item->category_published == '0' ? ' (' . Text::_('JUNPUBLISHED') . ')' : ' (' . Text::_('JTRASHED') . ')'; + endif; + ?> +
    +
    + + + escape($item->access_level); ?> + + + created_by != 0) : ?> + + escape($item->author_name); ?> + + + + + created_by_alias) : ?> +
    escape($item->created_by_alias)); ?>
    + + + + + association) : ?> + id); ?> + + + + + + + + + + {$orderingColumn}; + echo $date > 0 ? HTMLHelper::_('date', $date, Text::_('DATE_FORMAT_LC4')) : '-'; + ?> + + hits) : ?> + + + hits; ?> + + + + vote) : ?> + + + rating_count; ?> + + + + + rating; ?> + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_content') - && $user->authorise('core.edit', 'com_content') - && $user->authorise('core.edit.state', 'com_content')) : ?> - Text::_('COM_CONTENT_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - + + authorise('core.create', 'com_content') + && $user->authorise('core.edit', 'com_content') + && $user->authorise('core.edit.state', 'com_content') +) : ?> + Text::_('COM_CONTENT_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + - - - + + + - - - -
    -
    -
    + + + + + +
    diff --git a/administrator/components/com_content/tmpl/articles/default_batch_body.php b/administrator/components/com_content/tmpl/articles/default_batch_body.php index eaa3e08bbea6a..0f838accf462c 100644 --- a/administrator/components/com_content/tmpl/articles/default_batch_body.php +++ b/administrator/components/com_content/tmpl/articles/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Component\ComponentHelper; @@ -21,39 +23,39 @@ ?>
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    - = 0) : ?> -
    -
    - 'com_content']); ?> -
    -
    - -
    -
    - -
    -
    - authorise('core.admin', 'com_content') && $params->get('workflow_enabled')) : ?> -
    -
    - 'com_content']); ?> -
    -
    - -
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + = 0) : ?> +
    +
    + 'com_content']); ?> +
    +
    + +
    +
    + +
    +
    + authorise('core.admin', 'com_content') && $params->get('workflow_enabled')) : ?> +
    +
    + 'com_content']); ?> +
    +
    + +
    diff --git a/administrator/components/com_content/tmpl/articles/default_batch_footer.php b/administrator/components/com_content/tmpl/articles/default_batch_footer.php index 3fe481e8ec80c..fd221e578f496 100644 --- a/administrator/components/com_content/tmpl/articles/default_batch_footer.php +++ b/administrator/components/com_content/tmpl/articles/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; @@ -16,8 +18,8 @@ ?> diff --git a/administrator/components/com_content/tmpl/articles/emptystate.php b/administrator/components/com_content/tmpl/articles/emptystate.php index 222805bc2f92f..331fd161984ba 100644 --- a/administrator/components/com_content/tmpl/articles/emptystate.php +++ b/administrator/components/com_content/tmpl/articles/emptystate.php @@ -1,4 +1,5 @@ 'COM_CONTENT', - 'formURL' => 'index.php?option=com_content&view=articles', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Adding_a_new_article', - 'icon' => 'icon-copy article', + 'textPrefix' => 'COM_CONTENT', + 'formURL' => 'index.php?option=com_content&view=articles', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Adding_a_new_article', + 'icon' => 'icon-copy article', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_content&task=article.add'; +if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_content&task=article.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_content/tmpl/articles/modal.php b/administrator/components/com_content/tmpl/articles/modal.php index 2ed1cdc81c11d..d99d647c4641c 100644 --- a/administrator/components/com_content/tmpl/articles/modal.php +++ b/administrator/components/com_content/tmpl/articles/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if ($app->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('core') - ->useScript('multiselect') - ->useScript('com_content.admin-articles-modal'); + ->useScript('multiselect') + ->useScript('com_content.admin-articles-modal'); $function = $app->input->getCmd('function', 'jSelectArticle'); $editor = $app->input->getCmd('editor', ''); @@ -38,140 +38,132 @@ $onclick = $this->escape($function); $multilang = Multilanguage::isEnabled(); -if (!empty($editor)) -{ - // This view is used also in com_menus. Load the xtd script only if the editor is set! - $this->document->addScriptOptions('xtd-articles', array('editor' => $editor)); - $onclick = "jSelectArticle"; +if (!empty($editor)) { + // This view is used also in com_menus. Load the xtd script only if the editor is set! + $this->document->addScriptOptions('xtd-articles', array('editor' => $editor)); + $onclick = "jSelectArticle"; } ?>
    -
    + - $this)); ?> + $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - ); - ?> - items as $i => $item) : ?> - language && $multilang) - { - $tag = strlen($item->language); - if ($tag == 5) - { - $lang = substr($item->language, 0, 2); - } - elseif ($tag == 6) - { - $lang = substr($item->language, 0, 3); - } - else { - $lang = ''; - } - } - elseif (!$multilang) - { - $lang = ''; - } - ?> - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - -
    - - - - - escape($onclick) . '"' - . ' data-id="' . $item->id . '"' - . ' data-title="' . $this->escape($item->title) . '"' - . ' data-cat-id="' . $this->escape($item->catid) . '"' - . ' data-uri="' . $this->escape(RouteHelper::getArticleRoute($item->id, $item->catid, $item->language)) . '"' - . ' data-language="' . $this->escape($lang) . '"'; - ?> - > - escape($item->title); ?> - -
    - note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - -
    -
    - escape($item->category_title); ?> -
    -
    - escape($item->access_level); ?> - - - - created, Text::_('DATE_FORMAT_LC4')); ?> - - id; ?> -
    + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + ); + ?> + items as $i => $item) : ?> + language && $multilang) { + $tag = strlen($item->language); + if ($tag == 5) { + $lang = substr($item->language, 0, 2); + } elseif ($tag == 6) { + $lang = substr($item->language, 0, 3); + } else { + $lang = ''; + } + } elseif (!$multilang) { + $lang = ''; + } + ?> + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + +
    + + + + + escape($onclick) . '"' + . ' data-id="' . $item->id . '"' + . ' data-title="' . $this->escape($item->title) . '"' + . ' data-cat-id="' . $this->escape($item->catid) . '"' + . ' data-uri="' . $this->escape(RouteHelper::getArticleRoute($item->id, $item->catid, $item->language)) . '"' + . ' data-language="' . $this->escape($lang) . '"'; + ?> + > + escape($item->title); ?> + +
    + note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
    +
    + escape($item->category_title); ?> +
    +
    + escape($item->access_level); ?> + + + + created, Text::_('DATE_FORMAT_LC4')); ?> + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - + + + + -
    +
    diff --git a/administrator/components/com_content/tmpl/featured/default.php b/administrator/components/com_content/tmpl/featured/default.php index e719e334c5d2c..d0f9714e2494b 100644 --- a/administrator/components/com_content/tmpl/featured/default.php +++ b/administrator/components/com_content/tmpl/featured/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $app = Factory::getApplication(); $user = Factory::getUser(); @@ -36,28 +37,20 @@ $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'fp.ordering'; -if (strpos($listOrder, 'publish_up') !== false) -{ - $orderingColumn = 'publish_up'; -} -elseif (strpos($listOrder, 'publish_down') !== false) -{ - $orderingColumn = 'publish_down'; -} -elseif (strpos($listOrder, 'modified') !== false) -{ - $orderingColumn = 'modified'; -} -else -{ - $orderingColumn = 'created'; +if (strpos($listOrder, 'publish_up') !== false) { + $orderingColumn = 'publish_up'; +} elseif (strpos($listOrder, 'publish_down') !== false) { + $orderingColumn = 'publish_down'; +} elseif (strpos($listOrder, 'modified') !== false) { + $orderingColumn = 'modified'; +} else { + $orderingColumn = 'created'; } -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_content&task=featured.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_content&task=featured.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $workflow_enabled = ComponentHelper::getParams('com_content')->get('workflow_enabled'); @@ -65,9 +58,8 @@ $workflow_featured = false; if ($workflow_enabled) : - // @todo move the script to a file -$js = <<getRegistry()->addExtensionRegistryFile('com_workflow'); -$wa->useScript('com_workflow.admin-items-workflow-buttons') - ->addInlineScript($js, [], ['type' => 'module']); - -$workflow_state = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.state', 'com_content.article'); -$workflow_featured = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article'); + $wa->getRegistry()->addExtensionRegistryFile('com_workflow'); + $wa->useScript('com_workflow.admin-items-workflow-buttons') + ->addInlineScript($js, [], ['type' => 'module']); + $workflow_state = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.state', 'com_content.article'); + $workflow_featured = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article'); endif; $assoc = Associations::isEnabled(); @@ -95,332 +86,328 @@ ?>
    -
    -
    -
    - $this)); - ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - - - - - - hits) : ?> - - - vote) : ?> - - - - - - - class="js-draggable" data-url="" data-direction=""> - items); ?> - items as $i => $item) : - $item->max_ordering = 0; - $ordering = ($listOrder == 'fp.ordering'); - $assetId = 'com_content.article.' . $item->id; - $canCreate = $user->authorise('core.create', 'com_content.category.' . $item->catid); - $canEdit = $user->authorise('core.edit', 'com_content.article.' . $item->id); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_content.article.' . $item->id) && $canCheckin; - $canEditCat = $user->authorise('core.edit', 'com_content.category.' . $item->catid); - $canEditOwnCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->catid) && $item->category_uid == $userId; - $canEditParCat = $user->authorise('core.edit', 'com_content.category.' . $item->parent_category_id); - $canEditOwnParCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->parent_category_id) && $item->parent_category_uid == $userId; +
    +
    +
    + $this)); + ?> + items)) : ?> +
    + + +
    + +
    - , - , - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    + + + + + + + + + + + + + + + + + + + + + hits) : ?> + + + vote) : ?> + + + + + + + class="js-draggable" data-url="" data-direction=""> + items); ?> + items as $i => $item) : + $item->max_ordering = 0; + $ordering = ($listOrder == 'fp.ordering'); + $assetId = 'com_content.article.' . $item->id; + $canCreate = $user->authorise('core.create', 'com_content.category.' . $item->catid); + $canEdit = $user->authorise('core.edit', 'com_content.article.' . $item->id); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_content.article.' . $item->id) && $canCheckin; + $canEditCat = $user->authorise('core.edit', 'com_content.category.' . $item->catid); + $canEditOwnCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->catid) && $item->category_uid == $userId; + $canEditParCat = $user->authorise('core.edit', 'com_content.category.' . $item->parent_category_id); + $canEditOwnParCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->parent_category_id) && $item->parent_category_uid == $userId; - $transitions = ContentHelper::filterTransitions($this->transitions, (int) $item->stage_id, (int) $item->workflow_id); + $transitions = ContentHelper::filterTransitions($this->transitions, (int) $item->stage_id, (int) $item->workflow_id); - $transition_ids = ArrayHelper::getColumn($transitions, 'value'); - $transition_ids = ArrayHelper::toInteger($transition_ids); + $transition_ids = ArrayHelper::getColumn($transitions, 'value'); + $transition_ids = ArrayHelper::toInteger($transition_ids); - ?> - - - + + - - + + - - + + - + - - - - - - - - - - - hits) : ?> - - - vote) : ?> - - - - - - - -
    + , + , + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    - id, false, 'cid', 'cb', $item->title); ?> - - +
    + id, false, 'cid', 'cb', $item->title); ?> + + - - - - - - - - $transitions, - 'title' => Text::_($item->stage_title), - 'tip_content' => Text::sprintf('JWORKFLOW', Text::_($item->workflow_title)), - 'id' => 'workflow-' . $item->id, - 'task' => 'articles.runTransitions' - ]; + if (!$canChange) { + $iconClass = ' inactive'; + } elseif (!$saveOrder) { + $iconClass = ' inactive" title="' . Text::_('JORDERINGDISABLED'); + } + ?> + + + + + + + + $transitions, + 'title' => Text::_($item->stage_title), + 'tip_content' => Text::sprintf('JWORKFLOW', Text::_($item->workflow_title)), + 'id' => 'workflow-' . $item->id, + 'task' => 'articles.runTransitions' + ]; - echo (new TransitionButton($options)) - ->render(0, $i); - ?> -
    - stage_title); ?> -
    -
    - 'articles.', - 'disabled' => $workflow_featured || !$canChange, - 'id' => 'featured-' . $item->id - ]; + echo (new TransitionButton($options)) + ->render(0, $i); + ?> +
    + stage_title); ?> +
    +
    + 'articles.', + 'disabled' => $workflow_featured || !$canChange, + 'id' => 'featured-' . $item->id + ]; - echo (new FeaturedButton) - ->render((int) $item->featured, $i, $options, $item->featured_up, $item->featured_down); - ?> - - 'articles.', - 'disabled' => $workflow_state || !$canChange, - 'id' => 'state-' . $item->id, - 'category_published' => $item->category_published - ]; + echo (new FeaturedButton()) + ->render((int) $item->featured, $i, $options, $item->featured_up, $item->featured_down); + ?> + + 'articles.', + 'disabled' => $workflow_state || !$canChange, + 'id' => 'state-' . $item->id, + 'category_published' => $item->category_published + ]; - echo (new PublishedButton)->render((int) $item->state, $i, $options, $item->publish_up, $item->publish_down); - ?> - -
    - checked_out) : ?> - editor, $item->checked_out_time, 'articles.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
    - note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - -
    -
    - parent_category_id . '&extension=com_content'); - $CurrentCatUrl = Route::_('index.php?option=com_categories&task=category.edit&id=' . $item->catid . '&extension=com_content'); - $EditCatTxt = Text::_('COM_CONTENT_EDIT_CATEGORY'); - echo Text::_('JCATEGORY') . ': '; - if ($item->category_level != '1') : - if ($item->parent_category_level != '1') : - echo ' » '; - endif; - endif; - if (Factory::getLanguage()->isRtl()) - { - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - echo $this->escape($item->category_title); - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - if ($item->category_level != '1') : - echo ' « '; - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo $this->escape($item->parent_category_title); - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - endif; - } - else - { - if ($item->category_level != '1') : - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo $this->escape($item->parent_category_title); - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo ' » '; - endif; - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - echo $this->escape($item->category_title); - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - } - if ($item->category_published < '1') : - echo $item->category_published == '0' ? ' (' . Text::_('JUNPUBLISHED') . ')' : ' (' . Text::_('JTRASHED') . ')'; - endif; - ?> -
    -
    -
    - escape($item->access_level); ?> - - created_by != 0) : ?> - - escape($item->author_name); ?> - - - - - created_by_alias) : ?> -
    escape($item->created_by_alias)); ?>
    - -
    - association) : ?> - id); ?> - - - - - {$orderingColumn}; - echo $date > 0 ? HTMLHelper::_('date', $date, Text::_('DATE_FORMAT_LC4')) : '-'; - ?> - - - hits; ?> - - - - rating_count; ?> - - - - rating; ?> - - - id; ?> -
    + echo (new PublishedButton())->render((int) $item->state, $i, $options, $item->publish_up, $item->publish_down); + ?> + + +
    + checked_out) : ?> + editor, $item->checked_out_time, 'articles.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
    + note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
    +
    + parent_category_id . '&extension=com_content'); + $CurrentCatUrl = Route::_('index.php?option=com_categories&task=category.edit&id=' . $item->catid . '&extension=com_content'); + $EditCatTxt = Text::_('COM_CONTENT_EDIT_CATEGORY'); + echo Text::_('JCATEGORY') . ': '; + if ($item->category_level != '1') : + if ($item->parent_category_level != '1') : + echo ' » '; + endif; + endif; + if (Factory::getLanguage()->isRtl()) { + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + echo $this->escape($item->category_title); + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + if ($item->category_level != '1') : + echo ' « '; + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo $this->escape($item->parent_category_title); + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + endif; + } else { + if ($item->category_level != '1') : + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo $this->escape($item->parent_category_title); + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo ' » '; + endif; + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + echo $this->escape($item->category_title); + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + } + if ($item->category_published < '1') : + echo $item->category_published == '0' ? ' (' . Text::_('JUNPUBLISHED') . ')' : ' (' . Text::_('JTRASHED') . ')'; + endif; + ?> +
    +
    + + + escape($item->access_level); ?> + + + created_by != 0) : ?> + + escape($item->author_name); ?> + + + + + created_by_alias) : ?> +
    escape($item->created_by_alias)); ?>
    + + + + + association) : ?> + id); ?> + + + + + + + + + + {$orderingColumn}; + echo $date > 0 ? HTMLHelper::_('date', $date, Text::_('DATE_FORMAT_LC4')) : '-'; + ?> + + hits) : ?> + + + hits; ?> + + + + vote) : ?> + + + rating_count; ?> + + + + + rating; ?> + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - Text::_('JTOOLBAR_CHANGE_STATUS'), - 'footer' => $this->loadTemplate('stage_footer'), - ), - $this->loadTemplate('stage_body') - ); ?> + Text::_('JTOOLBAR_CHANGE_STATUS'), + 'footer' => $this->loadTemplate('stage_footer'), + ), + $this->loadTemplate('stage_body') + ); ?> - + - - - + + + - - - - -
    -
    -
    + + + + + + +
    diff --git a/administrator/components/com_content/tmpl/featured/default_stage_body.php b/administrator/components/com_content/tmpl/featured/default_stage_body.php index 3dadb99058ecc..d5f510e40a892 100644 --- a/administrator/components/com_content/tmpl/featured/default_stage_body.php +++ b/administrator/components/com_content/tmpl/featured/default_stage_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; @@ -13,11 +15,11 @@ ?>
    -
    -
    -

    -
    -
    -
    -
    +
    +
    +

    +
    +
    +
    +
    diff --git a/administrator/components/com_content/tmpl/featured/default_stage_footer.php b/administrator/components/com_content/tmpl/featured/default_stage_footer.php index 886538176cfc0..32ec9abcfb51f 100644 --- a/administrator/components/com_content/tmpl/featured/default_stage_footer.php +++ b/administrator/components/com_content/tmpl/featured/default_stage_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; @@ -16,8 +18,8 @@ ?> diff --git a/administrator/components/com_content/tmpl/featured/emptystate.php b/administrator/components/com_content/tmpl/featured/emptystate.php index 99bc733f48366..83748e92eacc7 100644 --- a/administrator/components/com_content/tmpl/featured/emptystate.php +++ b/administrator/components/com_content/tmpl/featured/emptystate.php @@ -1,4 +1,5 @@ 'COM_CONTENT', - 'formURL' => 'index.php?option=com_content&view=featured', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Adding_a_new_article', + 'textPrefix' => 'COM_CONTENT', + 'formURL' => 'index.php?option=com_content&view=featured', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Adding_a_new_article', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_content&task=article.add'; +if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_content&task=article.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_contenthistory/helpers/contenthistory.php b/administrator/components/com_contenthistory/helpers/contenthistory.php index 1c4b548e8b989..a025271c4298b 100644 --- a/administrator/components/com_contenthistory/helpers/contenthistory.php +++ b/administrator/components/com_contenthistory/helpers/contenthistory.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Contenthistory')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Contenthistory')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Contenthistory')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Contenthistory')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_contenthistory/src/Controller/DisplayController.php b/administrator/components/com_contenthistory/src/Controller/DisplayController.php index a7fbefd572536..7414a71ce4935 100644 --- a/administrator/components/com_contenthistory/src/Controller/DisplayController.php +++ b/administrator/components/com_contenthistory/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for getModel. + * + * @param string $name The name of the model + * @param string $prefix The prefix for the model + * @param array $config An additional array of parameters + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model + * + * @since 3.2 + */ + public function getModel($name = 'History', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } - /** - * Toggles the keep forever value for one or more history rows. If it was Yes, changes to No. If No, changes to Yes. - * - * @return void - * - * @since 3.2 - */ - public function keep() - { - $this->checkToken(); + /** + * Toggles the keep forever value for one or more history rows. If it was Yes, changes to No. If No, changes to Yes. + * + * @return void + * + * @since 3.2 + */ + public function keep() + { + $this->checkToken(); - // Get items to toggle keep forever from the request. - $cid = (array) $this->input->get('cid', array(), 'int'); + // Get items to toggle keep forever from the request. + $cid = (array) $this->input->get('cid', array(), 'int'); - // Remove zero values resulting from input filter - $cid = array_filter($cid); + // Remove zero values resulting from input filter + $cid = array_filter($cid); - if (empty($cid)) - { - $this->app->enqueueMessage(Text::_('COM_CONTENTHISTORY_NO_ITEM_SELECTED'), 'warning'); - } - else - { - // Get the model. - $model = $this->getModel(); + if (empty($cid)) { + $this->app->enqueueMessage(Text::_('COM_CONTENTHISTORY_NO_ITEM_SELECTED'), 'warning'); + } else { + // Get the model. + $model = $this->getModel(); - // Toggle keep forever status of the selected items. - if ($model->keep($cid)) - { - $this->setMessage(Text::plural('COM_CONTENTHISTORY_N_ITEMS_KEEP_TOGGLE', count($cid))); - } - else - { - $this->setMessage($model->getError(), 'error'); - } - } + // Toggle keep forever status of the selected items. + if ($model->keep($cid)) { + $this->setMessage(Text::plural('COM_CONTENTHISTORY_N_ITEMS_KEEP_TOGGLE', count($cid))); + } else { + $this->setMessage($model->getError(), 'error'); + } + } - $this->setRedirect( - Route::_( - 'index.php?option=com_contenthistory&view=history&layout=modal&tmpl=component&item_id=' - . $this->input->getCmd('item_id') . '&' . Session::getFormToken() . '=1', false - ) - ); - } + $this->setRedirect( + Route::_( + 'index.php?option=com_contenthistory&view=history&layout=modal&tmpl=component&item_id=' + . $this->input->getCmd('item_id') . '&' . Session::getFormToken() . '=1', + false + ) + ); + } - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - return '&layout=modal&tmpl=component&item_id=' . $this->input->get('item_id') . '&' . Session::getFormToken() . '=1'; - } + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + return '&layout=modal&tmpl=component&item_id=' . $this->input->get('item_id') . '&' . Session::getFormToken() . '=1'; + } } diff --git a/administrator/components/com_contenthistory/src/Controller/PreviewController.php b/administrator/components/com_contenthistory/src/Controller/PreviewController.php index 9c9b14b3bdf07..f30a22af36bcf 100644 --- a/administrator/components/com_contenthistory/src/Controller/PreviewController.php +++ b/administrator/components/com_contenthistory/src/Controller/PreviewController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for getModel. + * + * @param string $name The name of the model + * @param string $prefix The prefix for the model + * @param array $config An additional array of parameters + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model + * + * @since 3.2 + */ + public function getModel($name = 'Preview', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_contenthistory/src/Dispatcher/Dispatcher.php b/administrator/components/com_contenthistory/src/Dispatcher/Dispatcher.php index 61d79a98c955b..15452392ad5ff 100644 --- a/administrator/components/com_contenthistory/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_contenthistory/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getIdentity()->guest) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + * + * @throws \Exception|NotAllowed + */ + protected function checkAccess() + { + // Check the user has permission to access this component if in the backend + if ($this->app->getIdentity()->guest) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_contenthistory/src/Helper/ContenthistoryHelper.php b/administrator/components/com_contenthistory/src/Helper/ContenthistoryHelper.php index 18ab0f48624a9..d648b37b6386e 100644 --- a/administrator/components/com_contenthistory/src/Helper/ContenthistoryHelper.php +++ b/administrator/components/com_contenthistory/src/Helper/ContenthistoryHelper.php @@ -1,4 +1,5 @@ $value) - { - $result[$name] = $value; - - if (is_object($value)) - { - foreach ($value as $subName => $subValue) - { - $result[$subName] = $subValue; - } - } - } - - return $result; - } - - /** - * Method to decode JSON-encoded fields in a standard object. Used to unpack JSON strings in the content history data column. - * - * @param string $jsonString JSON String to convert to an object. - * - * @return \stdClass Object with any JSON-encoded fields unpacked. - * - * @since 3.2 - */ - public static function decodeFields($jsonString) - { - $object = json_decode($jsonString); - - if (is_object($object)) - { - foreach ($object as $name => $value) - { - if ($subObject = json_decode($value)) - { - $object->$name = $subObject; - } - } - } - - return $object; - } - - /** - * Method to get field labels for the fields in the JSON-encoded object. - * First we see if we can find translatable labels for the fields in the object. - * We translate any we can find and return an array in the format object->name => label. - * - * @param \stdClass $object Standard class object in the format name->value. - * @param ContentType $typesTable Table object with content history options. - * - * @return \stdClass Contains two associative arrays. - * $formValues->labels in the format name => label (for example, 'id' => 'Article ID'). - * $formValues->values in the format name => value (for example, 'state' => 'Published'. - * This translates the text from the selected option in the form. - * - * @since 3.2 - */ - public static function getFormValues($object, ContentType $typesTable) - { - $labels = array(); - $values = array(); - $expandedObjectArray = static::createObjectArray($object); - static::loadLanguageFiles($typesTable->type_alias); - - if ($formFile = static::getFormFile($typesTable)) - { - if ($xml = simplexml_load_file($formFile)) - { - // Now we need to get all of the labels from the form - $fieldArray = $xml->xpath('//field'); - $fieldArray = array_merge($fieldArray, $xml->xpath('//fields')); - - foreach ($fieldArray as $field) - { - if ($label = (string) $field->attributes()->label) - { - $labels[(string) $field->attributes()->name] = Text::_($label); - } - } - - // Get values for any list type fields - $listFieldArray = $xml->xpath('//field[@type="list" or @type="radio"]'); - - foreach ($listFieldArray as $field) - { - $name = (string) $field->attributes()->name; - - if (isset($expandedObjectArray[$name])) - { - $optionFieldArray = $field->xpath('option[@value="' . $expandedObjectArray[$name] . '"]'); - - $valueText = null; - - if (is_array($optionFieldArray) && count($optionFieldArray)) - { - $valueText = trim((string) $optionFieldArray[0]); - } - - $values[(string) $field->attributes()->name] = Text::_($valueText); - } - } - } - } - - $result = new \stdClass; - $result->labels = $labels; - $result->values = $values; - - return $result; - } - - /** - * Method to get the XML form file for this component. Used to get translated field names for history preview. - * - * @param ContentType $typesTable Table object with content history options. - * - * @return mixed \JModel object if successful, false if no model found. - * - * @since 3.2 - */ - public static function getFormFile(ContentType $typesTable) - { - // First, see if we have a file name in the $typesTable - $options = json_decode($typesTable->content_history_options); - - if (is_object($options) && isset($options->formFile) && File::exists(JPATH_ROOT . '/' . $options->formFile)) - { - $result = JPATH_ROOT . '/' . $options->formFile; - } - else - { - $aliasArray = explode('.', $typesTable->type_alias); - $component = ($aliasArray[1] == 'category') ? 'com_categories' : $aliasArray[0]; - $path = Folder::makeSafe(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/'); - array_shift($aliasArray); - $file = File::makeSafe(implode('.', $aliasArray) . '.xml'); - $result = File::exists($path . $file) ? $path . $file : false; - } - - return $result; - } - - /** - * Method to query the database using values from lookup objects. - * - * @param \stdClass $lookup The std object with the values needed to do the query. - * @param mixed $value The value used to find the matching title or name. Typically the id. - * - * @return mixed Value from database (for example, name or title) on success, false on failure. - * - * @since 3.2 - */ - public static function getLookupValue($lookup, $value) - { - $result = false; - - if (isset($lookup->sourceColumn) && isset($lookup->targetTable) && isset($lookup->targetColumn) && isset($lookup->displayColumn)) - { - $db = Factory::getDbo(); - $value = (int) $value; - $query = $db->getQuery(true); - $query->select($db->quoteName($lookup->displayColumn)) - ->from($db->quoteName($lookup->targetTable)) - ->where($db->quoteName($lookup->targetColumn) . ' = :value') - ->bind(':value', $value, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $result = $db->loadResult(); - } - catch (\Exception $e) - { - // Ignore any errors and just return false - return false; - } - } - - return $result; - } - - /** - * Method to remove fields from the object based on values entered in the #__content_types table. - * - * @param \stdClass $object Object to be passed to view layout file. - * @param ContentType $typeTable Table object with content history options. - * - * @return \stdClass object with hidden fields removed. - * - * @since 3.2 - */ - public static function hideFields($object, ContentType $typeTable) - { - if ($options = json_decode($typeTable->content_history_options)) - { - if (isset($options->hideFields) && is_array($options->hideFields)) - { - foreach ($options->hideFields as $field) - { - unset($object->$field); - } - } - } - - return $object; - } - - /** - * Method to load the language files for the component whose history is being viewed. - * - * @param string $typeAlias The type alias, for example 'com_content.article'. - * - * @return void - * - * @since 3.2 - */ - public static function loadLanguageFiles($typeAlias) - { - $aliasArray = explode('.', $typeAlias); - - if (is_array($aliasArray) && count($aliasArray) == 2) - { - $component = ($aliasArray[1] == 'category') ? 'com_categories' : $aliasArray[0]; - $lang = Factory::getLanguage(); - - /** - * Loading language file from the administrator/language directory then - * loading language file from the administrator/components/extension/language directory - */ - $lang->load($component, JPATH_ADMINISTRATOR) - || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); - - // Force loading of backend global language file - $lang->load('joomla', Path::clean(JPATH_ADMINISTRATOR)); - } - } - - /** - * Method to create object to pass to the layout. Format is as follows: - * field is std object with name, value. - * - * Value can be a std object with name, value pairs. - * - * @param \stdClass $object The std object from the JSON string. Can be nested 1 level deep. - * @param \stdClass $formValues Standard class of label and value in an associative array. - * - * @return \stdClass Object with translated labels where available - * - * @since 3.2 - */ - public static function mergeLabels($object, $formValues) - { - $result = new \stdClass; - - if ($object === null) - { - return $result; - } - - $labelsArray = $formValues->labels; - $valuesArray = $formValues->values; - - foreach ($object as $name => $value) - { - $result->$name = new \stdClass; - $result->$name->name = $name; - $result->$name->value = $valuesArray[$name] ?? $value; - $result->$name->label = $labelsArray[$name] ?? $name; - - if (is_object($value)) - { - $subObject = new \stdClass; - - foreach ($value as $subName => $subValue) - { - $subObject->$subName = new \stdClass; - $subObject->$subName->name = $subName; - $subObject->$subName->value = $valuesArray[$subName] ?? $subValue; - $subObject->$subName->label = $labelsArray[$subName] ?? $subName; - $result->$name->value = $subObject; - } - } - } - - return $result; - } - - /** - * Method to prepare the object for the preview and compare views. - * - * @param ContentHistory $table Table object loaded with data. - * - * @return \stdClass Object ready for the views. - * - * @since 3.2 - */ - public static function prepareData(ContentHistory $table) - { - $object = static::decodeFields($table->version_data); - $typesTable = Table::getInstance('ContentType', 'Joomla\\CMS\\Table\\'); - $typeAlias = explode('.', $table->item_id); - array_pop($typeAlias); - $typesTable->load(array('type_alias' => implode('.', $typeAlias))); - $formValues = static::getFormValues($object, $typesTable); - $object = static::mergeLabels($object, $formValues); - $object = static::hideFields($object, $typesTable); - $object = static::processLookupFields($object, $typesTable); - - return $object; - } - - /** - * Method to process any lookup values found in the content_history_options column for this table. - * This allows category title and user name to be displayed instead of the id column. - * - * @param \stdClass $object The std object from the JSON string. Can be nested 1 level deep. - * @param ContentType $typesTable Table object loaded with data. - * - * @return \stdClass Object with lookup values inserted. - * - * @since 3.2 - */ - public static function processLookupFields($object, ContentType $typesTable) - { - if ($options = json_decode($typesTable->content_history_options)) - { - if (isset($options->displayLookup) && is_array($options->displayLookup)) - { - foreach ($options->displayLookup as $lookup) - { - $sourceColumn = $lookup->sourceColumn ?? false; - $sourceValue = $object->$sourceColumn->value ?? false; - - if ($sourceColumn && $sourceValue && ($lookupValue = static::getLookupValue($lookup, $sourceValue))) - { - $object->$sourceColumn->value = $lookupValue; - } - } - } - } - - return $object; - } + /** + * Method to put all field names, including nested ones, in a single array for easy lookup. + * + * @param \stdClass $object Standard class object that may contain one level of nested objects. + * + * @return array Associative array of all field names, including ones in a nested object. + * + * @since 3.2 + */ + public static function createObjectArray($object) + { + $result = array(); + + if ($object === null) { + return $result; + } + + foreach ($object as $name => $value) { + $result[$name] = $value; + + if (is_object($value)) { + foreach ($value as $subName => $subValue) { + $result[$subName] = $subValue; + } + } + } + + return $result; + } + + /** + * Method to decode JSON-encoded fields in a standard object. Used to unpack JSON strings in the content history data column. + * + * @param string $jsonString JSON String to convert to an object. + * + * @return \stdClass Object with any JSON-encoded fields unpacked. + * + * @since 3.2 + */ + public static function decodeFields($jsonString) + { + $object = json_decode($jsonString); + + if (is_object($object)) { + foreach ($object as $name => $value) { + if ($subObject = json_decode($value)) { + $object->$name = $subObject; + } + } + } + + return $object; + } + + /** + * Method to get field labels for the fields in the JSON-encoded object. + * First we see if we can find translatable labels for the fields in the object. + * We translate any we can find and return an array in the format object->name => label. + * + * @param \stdClass $object Standard class object in the format name->value. + * @param ContentType $typesTable Table object with content history options. + * + * @return \stdClass Contains two associative arrays. + * $formValues->labels in the format name => label (for example, 'id' => 'Article ID'). + * $formValues->values in the format name => value (for example, 'state' => 'Published'. + * This translates the text from the selected option in the form. + * + * @since 3.2 + */ + public static function getFormValues($object, ContentType $typesTable) + { + $labels = array(); + $values = array(); + $expandedObjectArray = static::createObjectArray($object); + static::loadLanguageFiles($typesTable->type_alias); + + if ($formFile = static::getFormFile($typesTable)) { + if ($xml = simplexml_load_file($formFile)) { + // Now we need to get all of the labels from the form + $fieldArray = $xml->xpath('//field'); + $fieldArray = array_merge($fieldArray, $xml->xpath('//fields')); + + foreach ($fieldArray as $field) { + if ($label = (string) $field->attributes()->label) { + $labels[(string) $field->attributes()->name] = Text::_($label); + } + } + + // Get values for any list type fields + $listFieldArray = $xml->xpath('//field[@type="list" or @type="radio"]'); + + foreach ($listFieldArray as $field) { + $name = (string) $field->attributes()->name; + + if (isset($expandedObjectArray[$name])) { + $optionFieldArray = $field->xpath('option[@value="' . $expandedObjectArray[$name] . '"]'); + + $valueText = null; + + if (is_array($optionFieldArray) && count($optionFieldArray)) { + $valueText = trim((string) $optionFieldArray[0]); + } + + $values[(string) $field->attributes()->name] = Text::_($valueText); + } + } + } + } + + $result = new \stdClass(); + $result->labels = $labels; + $result->values = $values; + + return $result; + } + + /** + * Method to get the XML form file for this component. Used to get translated field names for history preview. + * + * @param ContentType $typesTable Table object with content history options. + * + * @return mixed \JModel object if successful, false if no model found. + * + * @since 3.2 + */ + public static function getFormFile(ContentType $typesTable) + { + // First, see if we have a file name in the $typesTable + $options = json_decode($typesTable->content_history_options); + + if (is_object($options) && isset($options->formFile) && File::exists(JPATH_ROOT . '/' . $options->formFile)) { + $result = JPATH_ROOT . '/' . $options->formFile; + } else { + $aliasArray = explode('.', $typesTable->type_alias); + $component = ($aliasArray[1] == 'category') ? 'com_categories' : $aliasArray[0]; + $path = Folder::makeSafe(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/'); + array_shift($aliasArray); + $file = File::makeSafe(implode('.', $aliasArray) . '.xml'); + $result = File::exists($path . $file) ? $path . $file : false; + } + + return $result; + } + + /** + * Method to query the database using values from lookup objects. + * + * @param \stdClass $lookup The std object with the values needed to do the query. + * @param mixed $value The value used to find the matching title or name. Typically the id. + * + * @return mixed Value from database (for example, name or title) on success, false on failure. + * + * @since 3.2 + */ + public static function getLookupValue($lookup, $value) + { + $result = false; + + if (isset($lookup->sourceColumn) && isset($lookup->targetTable) && isset($lookup->targetColumn) && isset($lookup->displayColumn)) { + $db = Factory::getDbo(); + $value = (int) $value; + $query = $db->getQuery(true); + $query->select($db->quoteName($lookup->displayColumn)) + ->from($db->quoteName($lookup->targetTable)) + ->where($db->quoteName($lookup->targetColumn) . ' = :value') + ->bind(':value', $value, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $result = $db->loadResult(); + } catch (\Exception $e) { + // Ignore any errors and just return false + return false; + } + } + + return $result; + } + + /** + * Method to remove fields from the object based on values entered in the #__content_types table. + * + * @param \stdClass $object Object to be passed to view layout file. + * @param ContentType $typeTable Table object with content history options. + * + * @return \stdClass object with hidden fields removed. + * + * @since 3.2 + */ + public static function hideFields($object, ContentType $typeTable) + { + if ($options = json_decode($typeTable->content_history_options)) { + if (isset($options->hideFields) && is_array($options->hideFields)) { + foreach ($options->hideFields as $field) { + unset($object->$field); + } + } + } + + return $object; + } + + /** + * Method to load the language files for the component whose history is being viewed. + * + * @param string $typeAlias The type alias, for example 'com_content.article'. + * + * @return void + * + * @since 3.2 + */ + public static function loadLanguageFiles($typeAlias) + { + $aliasArray = explode('.', $typeAlias); + + if (is_array($aliasArray) && count($aliasArray) == 2) { + $component = ($aliasArray[1] == 'category') ? 'com_categories' : $aliasArray[0]; + $lang = Factory::getLanguage(); + + /** + * Loading language file from the administrator/language directory then + * loading language file from the administrator/components/extension/language directory + */ + $lang->load($component, JPATH_ADMINISTRATOR) + || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); + + // Force loading of backend global language file + $lang->load('joomla', Path::clean(JPATH_ADMINISTRATOR)); + } + } + + /** + * Method to create object to pass to the layout. Format is as follows: + * field is std object with name, value. + * + * Value can be a std object with name, value pairs. + * + * @param \stdClass $object The std object from the JSON string. Can be nested 1 level deep. + * @param \stdClass $formValues Standard class of label and value in an associative array. + * + * @return \stdClass Object with translated labels where available + * + * @since 3.2 + */ + public static function mergeLabels($object, $formValues) + { + $result = new \stdClass(); + + if ($object === null) { + return $result; + } + + $labelsArray = $formValues->labels; + $valuesArray = $formValues->values; + + foreach ($object as $name => $value) { + $result->$name = new \stdClass(); + $result->$name->name = $name; + $result->$name->value = $valuesArray[$name] ?? $value; + $result->$name->label = $labelsArray[$name] ?? $name; + + if (is_object($value)) { + $subObject = new \stdClass(); + + foreach ($value as $subName => $subValue) { + $subObject->$subName = new \stdClass(); + $subObject->$subName->name = $subName; + $subObject->$subName->value = $valuesArray[$subName] ?? $subValue; + $subObject->$subName->label = $labelsArray[$subName] ?? $subName; + $result->$name->value = $subObject; + } + } + } + + return $result; + } + + /** + * Method to prepare the object for the preview and compare views. + * + * @param ContentHistory $table Table object loaded with data. + * + * @return \stdClass Object ready for the views. + * + * @since 3.2 + */ + public static function prepareData(ContentHistory $table) + { + $object = static::decodeFields($table->version_data); + $typesTable = Table::getInstance('ContentType', 'Joomla\\CMS\\Table\\'); + $typeAlias = explode('.', $table->item_id); + array_pop($typeAlias); + $typesTable->load(array('type_alias' => implode('.', $typeAlias))); + $formValues = static::getFormValues($object, $typesTable); + $object = static::mergeLabels($object, $formValues); + $object = static::hideFields($object, $typesTable); + $object = static::processLookupFields($object, $typesTable); + + return $object; + } + + /** + * Method to process any lookup values found in the content_history_options column for this table. + * This allows category title and user name to be displayed instead of the id column. + * + * @param \stdClass $object The std object from the JSON string. Can be nested 1 level deep. + * @param ContentType $typesTable Table object loaded with data. + * + * @return \stdClass Object with lookup values inserted. + * + * @since 3.2 + */ + public static function processLookupFields($object, ContentType $typesTable) + { + if ($options = json_decode($typesTable->content_history_options)) { + if (isset($options->displayLookup) && is_array($options->displayLookup)) { + foreach ($options->displayLookup as $lookup) { + $sourceColumn = $lookup->sourceColumn ?? false; + $sourceValue = $object->$sourceColumn->value ?? false; + + if ($sourceColumn && $sourceValue && ($lookupValue = static::getLookupValue($lookup, $sourceValue))) { + $object->$sourceColumn->value = $lookupValue; + } + } + } + } + + return $object; + } } diff --git a/administrator/components/com_contenthistory/src/Model/CompareModel.php b/administrator/components/com_contenthistory/src/Model/CompareModel.php index f9536f923ec05..75ca3484ddcc9 100644 --- a/administrator/components/com_contenthistory/src/Model/CompareModel.php +++ b/administrator/components/com_contenthistory/src/Model/CompareModel.php @@ -1,4 +1,5 @@ input; - - /** @var ContentHistory $table1 */ - $table1 = $this->getTable('ContentHistory'); - - /** @var ContentHistory $table2 */ - $table2 = $this->getTable('ContentHistory'); - - $id1 = $input->getInt('id1'); - $id2 = $input->getInt('id2'); - - if (!$id1 || \is_array($id1) || !$id2 || \is_array($id2)) - { - $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_INVALID_ID')); - - return false; - } - - $result = array(); - - if (!$table1->load($id1) || !$table2->load($id2)) - { - $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_VERSION_NOT_FOUND')); - - // Assume a failure to load the content means broken data, abort mission - return false; - } - - // Get the first history record's content type record so we can check ACL - /** @var ContentType $contentTypeTable */ - $contentTypeTable = $this->getTable('ContentType'); - $typeAlias = explode('.', $table1->item_id); - array_pop($typeAlias); - $typeAlias = implode('.', $typeAlias); - - if (!$contentTypeTable->load(array('type_alias' => $typeAlias))) - { - $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_FAILED_LOADING_CONTENT_TYPE')); - - // Assume a failure to load the content type means broken data, abort mission - return false; - } - - $user = Factory::getUser(); - - // Access check - if (!$user->authorise('core.edit', $table1->item_id) && !$this->canEdit($table1)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $nullDate = $this->getDatabase()->getNullDate(); - - foreach (array($table1, $table2) as $table) - { - $object = new \stdClass; - $object->data = ContenthistoryHelper::prepareData($table); - $object->version_note = $table->version_note; - - // Let's use custom calendars when present - $object->save_date = HTMLHelper::_('date', $table->save_date, Text::_('DATE_FORMAT_LC6')); - - $dateProperties = array ( - 'modified_time', - 'created_time', - 'modified', - 'created', - 'checked_out_time', - 'publish_up', - 'publish_down', - ); - - foreach ($dateProperties as $dateProperty) - { - if (property_exists($object->data, $dateProperty) - && $object->data->$dateProperty->value !== null - && $object->data->$dateProperty->value !== $nullDate) - { - $object->data->$dateProperty->value = HTMLHelper::_( - 'date', - $object->data->$dateProperty->value, - Text::_('DATE_FORMAT_LC6') - ); - } - } - - $result[] = $object; - } - - return $result; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.2 - */ - public function getTable($type = 'Contenthistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - - /** - * Method to test whether a record is editable - * - * @param ContentHistory $record A Table object. - * - * @return boolean True if allowed to edit the record. Defaults to the permission set in the component. - * - * @since 3.6 - */ - protected function canEdit($record) - { - $result = false; - - if (!empty($record->item_id)) - { - /** - * Make sure user has edit privileges for this content item. Note that we use edit permissions - * for the content item, not delete permissions for the content history row. - */ - $user = Factory::getUser(); - $result = $user->authorise('core.edit', $record->item_id); - - // Finally try session (this catches edit.own case too) - if (!$result) - { - /** @var ContentType $contentTypeTable */ - $contentTypeTable = $this->getTable('ContentType'); - - $typeAlias = explode('.', $record->item_id); - $id = array_pop($typeAlias); - $typeAlias = implode('.', $typeAlias); - $contentTypeTable->load(array('type_alias' => $typeAlias)); - $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); - $result = in_array((int) $id, $typeEditables); - } - } - - return $result; - } + /** + * Method to get a version history row. + * + * @return array|boolean On success, array of populated tables. False on failure. + * + * @since 3.2 + * + * @throws NotAllowed Thrown if not authorised to edit an item + */ + public function getItems() + { + $input = Factory::getApplication()->input; + + /** @var ContentHistory $table1 */ + $table1 = $this->getTable('ContentHistory'); + + /** @var ContentHistory $table2 */ + $table2 = $this->getTable('ContentHistory'); + + $id1 = $input->getInt('id1'); + $id2 = $input->getInt('id2'); + + if (!$id1 || \is_array($id1) || !$id2 || \is_array($id2)) { + $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_INVALID_ID')); + + return false; + } + + $result = array(); + + if (!$table1->load($id1) || !$table2->load($id2)) { + $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_VERSION_NOT_FOUND')); + + // Assume a failure to load the content means broken data, abort mission + return false; + } + + // Get the first history record's content type record so we can check ACL + /** @var ContentType $contentTypeTable */ + $contentTypeTable = $this->getTable('ContentType'); + $typeAlias = explode('.', $table1->item_id); + array_pop($typeAlias); + $typeAlias = implode('.', $typeAlias); + + if (!$contentTypeTable->load(array('type_alias' => $typeAlias))) { + $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_FAILED_LOADING_CONTENT_TYPE')); + + // Assume a failure to load the content type means broken data, abort mission + return false; + } + + $user = Factory::getUser(); + + // Access check + if (!$user->authorise('core.edit', $table1->item_id) && !$this->canEdit($table1)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $nullDate = $this->getDatabase()->getNullDate(); + + foreach (array($table1, $table2) as $table) { + $object = new \stdClass(); + $object->data = ContenthistoryHelper::prepareData($table); + $object->version_note = $table->version_note; + + // Let's use custom calendars when present + $object->save_date = HTMLHelper::_('date', $table->save_date, Text::_('DATE_FORMAT_LC6')); + + $dateProperties = array ( + 'modified_time', + 'created_time', + 'modified', + 'created', + 'checked_out_time', + 'publish_up', + 'publish_down', + ); + + foreach ($dateProperties as $dateProperty) { + if ( + property_exists($object->data, $dateProperty) + && $object->data->$dateProperty->value !== null + && $object->data->$dateProperty->value !== $nullDate + ) { + $object->data->$dateProperty->value = HTMLHelper::_( + 'date', + $object->data->$dateProperty->value, + Text::_('DATE_FORMAT_LC6') + ); + } + } + + $result[] = $object; + } + + return $result; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.2 + */ + public function getTable($type = 'Contenthistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + + /** + * Method to test whether a record is editable + * + * @param ContentHistory $record A Table object. + * + * @return boolean True if allowed to edit the record. Defaults to the permission set in the component. + * + * @since 3.6 + */ + protected function canEdit($record) + { + $result = false; + + if (!empty($record->item_id)) { + /** + * Make sure user has edit privileges for this content item. Note that we use edit permissions + * for the content item, not delete permissions for the content history row. + */ + $user = Factory::getUser(); + $result = $user->authorise('core.edit', $record->item_id); + + // Finally try session (this catches edit.own case too) + if (!$result) { + /** @var ContentType $contentTypeTable */ + $contentTypeTable = $this->getTable('ContentType'); + + $typeAlias = explode('.', $record->item_id); + $id = array_pop($typeAlias); + $typeAlias = implode('.', $typeAlias); + $contentTypeTable->load(array('type_alias' => $typeAlias)); + $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); + $result = in_array((int) $id, $typeEditables); + } + } + + return $result; + } } diff --git a/administrator/components/com_contenthistory/src/Model/HistoryModel.php b/administrator/components/com_contenthistory/src/Model/HistoryModel.php index ca2976b273d9f..a41502e25adf6 100644 --- a/administrator/components/com_contenthistory/src/Model/HistoryModel.php +++ b/administrator/components/com_contenthistory/src/Model/HistoryModel.php @@ -1,4 +1,5 @@ item_id)) - { - return false; - } - - /** - * Make sure user has edit privileges for this content item. Note that we use edit permissions - * for the content item, not delete permissions for the content history row. - */ - $user = Factory::getUser(); - - if ($user->authorise('core.edit', $record->item_id)) - { - return true; - } - - // Finally try session (this catches edit.own case too) - /** @var ContentType $contentTypeTable */ - $contentTypeTable = $this->getTable('ContentType'); - - $typeAlias = explode('.', $record->item_id); - $id = array_pop($typeAlias); - $typeAlias = implode('.', $typeAlias); - $contentTypeTable->load(array('type_alias' => $typeAlias)); - $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); - $result = in_array((int) $id, $typeEditables); - - return $result; - } - - /** - * Method to test whether a history record can be deleted. Note that we check whether we have edit permissions - * for the content item row. - * - * @param ContentHistory $record A Table object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 3.6 - */ - protected function canDelete($record) - { - return $this->canEdit($record); - } - - /** - * Method to delete one or more records from content history table. - * - * @param array $pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 3.2 - */ - public function delete(&$pks) - { - $pks = (array) $pks; - $table = $this->getTable(); - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if ((int) $table->keep_forever === 1) - { - unset($pks[$i]); - continue; - } - - if ($this->canEdit($table)) - { - if (!$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - $error = $this->getError(); - - if ($error) - { - try - { - Log::add($error, Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage($error, 'warning'); - } - - return false; - } - else - { - try - { - Log::add(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'warning'); - } - - return false; - } - } - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.4.5 - * - * @throws NotAllowed Thrown if not authorised to edit an item - */ - public function getItems() - { - $items = parent::getItems(); - $user = Factory::getUser(); - - if ($items === false) - { - return false; - } - - // This should be an array with at least one element - if (!is_array($items) || !isset($items[0])) - { - return $items; - } - - // Access check - if (!$user->authorise('core.edit', $items[0]->item_id) && !$this->canEdit($items[0])) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - return $items; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.2 - */ - public function getTable($type = 'ContentHistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - /** - * Method to toggle on and off the keep forever value for one or more records from content history table. - * - * @param array $pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 3.2 - */ - public function keep(&$pks) - { - $pks = (array) $pks; - $table = $this->getTable(); - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if ($this->canEdit($table)) - { - $table->keep_forever = $table->keep_forever ? 0 : 1; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - $error = $this->getError(); - - if ($error) - { - try - { - Log::add($error, Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage($error, 'warning'); - } - - return false; - } - else - { - try - { - Log::add(Text::_('COM_CONTENTHISTORY_ERROR_KEEP_NOT_PERMITTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONTENTHISTORY_ERROR_KEEP_NOT_PERMITTED'), 'warning'); - } - - return false; - } - } - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 3.2 - */ - protected function populateState($ordering = 'h.save_date', $direction = 'DESC') - { - $input = Factory::getApplication()->input; - $itemId = $input->get('item_id', '', 'string'); - - $this->setState('item_id', $itemId); - $this->setState('sha1_hash', $this->getSha1Hash()); - - // Load the parameters. - $params = ComponentHelper::getParams('com_contenthistory'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 3.2 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $itemId = $this->getState('item_id'); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('h.version_id'), - $db->quoteName('h.item_id'), - $db->quoteName('h.version_note'), - $db->quoteName('h.save_date'), - $db->quoteName('h.editor_user_id'), - $db->quoteName('h.character_count'), - $db->quoteName('h.sha1_hash'), - $db->quoteName('h.version_data'), - $db->quoteName('h.keep_forever'), - ] - ) - ) - ->from($db->quoteName('#__history', 'h')) - ->where($db->quoteName('h.item_id') . ' = :itemid') - ->bind(':itemid', $itemId, ParameterType::STRING) - - // Join over the users for the editor - ->select($db->quoteName('uc.name', 'editor')) - ->join('LEFT', - $db->quoteName('#__users', 'uc'), - $db->quoteName('uc.id') . ' = ' . $db->quoteName('h.editor_user_id') - ); - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering'); - $orderDirn = $this->state->get('list.direction'); - $query->order($db->quoteName($orderCol) . $orderDirn); - - return $query; - } - - /** - * Get the sha1 hash value for the current item being edited. - * - * @return string sha1 hash of row data - * - * @since 3.2 - */ - protected function getSha1Hash() - { - $result = false; - $item_id = Factory::getApplication()->input->getCmd('item_id', ''); - $typeAlias = explode('.', $item_id); - Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/' . $typeAlias[0] . '/tables'); - $typeTable = $this->getTable('ContentType'); - $typeTable->load(['type_alias' => $typeAlias[0] . '.' . $typeAlias[1]]); - $contentTable = $typeTable->getContentTable(); - - if ($contentTable && $contentTable->load($typeAlias[2])) - { - $helper = new CMSHelper; - - $dataObject = $helper->getDataObject($contentTable); - $result = $this->getTable('ContentHistory')->getSha1(json_encode($dataObject), $typeTable); - } - - return $result; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'version_id', + 'h.version_id', + 'version_note', + 'h.version_note', + 'save_date', + 'h.save_date', + 'editor_user_id', + 'h.editor_user_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to test whether a record is editable + * + * @param ContentHistory $record A Table object. + * + * @return boolean True if allowed to edit the record. Defaults to the permission set in the component. + * + * @since 3.2 + */ + protected function canEdit($record) + { + if (empty($record->item_id)) { + return false; + } + + /** + * Make sure user has edit privileges for this content item. Note that we use edit permissions + * for the content item, not delete permissions for the content history row. + */ + $user = Factory::getUser(); + + if ($user->authorise('core.edit', $record->item_id)) { + return true; + } + + // Finally try session (this catches edit.own case too) + /** @var ContentType $contentTypeTable */ + $contentTypeTable = $this->getTable('ContentType'); + + $typeAlias = explode('.', $record->item_id); + $id = array_pop($typeAlias); + $typeAlias = implode('.', $typeAlias); + $contentTypeTable->load(array('type_alias' => $typeAlias)); + $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); + $result = in_array((int) $id, $typeEditables); + + return $result; + } + + /** + * Method to test whether a history record can be deleted. Note that we check whether we have edit permissions + * for the content item row. + * + * @param ContentHistory $record A Table object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 3.6 + */ + protected function canDelete($record) + { + return $this->canEdit($record); + } + + /** + * Method to delete one or more records from content history table. + * + * @param array $pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 3.2 + */ + public function delete(&$pks) + { + $pks = (array) $pks; + $table = $this->getTable(); + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if ((int) $table->keep_forever === 1) { + unset($pks[$i]); + continue; + } + + if ($this->canEdit($table)) { + if (!$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + $error = $this->getError(); + + if ($error) { + try { + Log::add($error, Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage($error, 'warning'); + } + + return false; + } else { + try { + Log::add(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'warning'); + } + + return false; + } + } + } else { + $this->setError($table->getError()); + + return false; + } + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.4.5 + * + * @throws NotAllowed Thrown if not authorised to edit an item + */ + public function getItems() + { + $items = parent::getItems(); + $user = Factory::getUser(); + + if ($items === false) { + return false; + } + + // This should be an array with at least one element + if (!is_array($items) || !isset($items[0])) { + return $items; + } + + // Access check + if (!$user->authorise('core.edit', $items[0]->item_id) && !$this->canEdit($items[0])) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + return $items; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.2 + */ + public function getTable($type = 'ContentHistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + /** + * Method to toggle on and off the keep forever value for one or more records from content history table. + * + * @param array $pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 3.2 + */ + public function keep(&$pks) + { + $pks = (array) $pks; + $table = $this->getTable(); + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if ($this->canEdit($table)) { + $table->keep_forever = $table->keep_forever ? 0 : 1; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + $error = $this->getError(); + + if ($error) { + try { + Log::add($error, Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage($error, 'warning'); + } + + return false; + } else { + try { + Log::add(Text::_('COM_CONTENTHISTORY_ERROR_KEEP_NOT_PERMITTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONTENTHISTORY_ERROR_KEEP_NOT_PERMITTED'), 'warning'); + } + + return false; + } + } + } else { + $this->setError($table->getError()); + + return false; + } + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.2 + */ + protected function populateState($ordering = 'h.save_date', $direction = 'DESC') + { + $input = Factory::getApplication()->input; + $itemId = $input->get('item_id', '', 'string'); + + $this->setState('item_id', $itemId); + $this->setState('sha1_hash', $this->getSha1Hash()); + + // Load the parameters. + $params = ComponentHelper::getParams('com_contenthistory'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 3.2 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $itemId = $this->getState('item_id'); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('h.version_id'), + $db->quoteName('h.item_id'), + $db->quoteName('h.version_note'), + $db->quoteName('h.save_date'), + $db->quoteName('h.editor_user_id'), + $db->quoteName('h.character_count'), + $db->quoteName('h.sha1_hash'), + $db->quoteName('h.version_data'), + $db->quoteName('h.keep_forever'), + ] + ) + ) + ->from($db->quoteName('#__history', 'h')) + ->where($db->quoteName('h.item_id') . ' = :itemid') + ->bind(':itemid', $itemId, ParameterType::STRING) + + // Join over the users for the editor + ->select($db->quoteName('uc.name', 'editor')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'uc'), + $db->quoteName('uc.id') . ' = ' . $db->quoteName('h.editor_user_id') + ); + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering'); + $orderDirn = $this->state->get('list.direction'); + $query->order($db->quoteName($orderCol) . $orderDirn); + + return $query; + } + + /** + * Get the sha1 hash value for the current item being edited. + * + * @return string sha1 hash of row data + * + * @since 3.2 + */ + protected function getSha1Hash() + { + $result = false; + $item_id = Factory::getApplication()->input->getCmd('item_id', ''); + $typeAlias = explode('.', $item_id); + Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/' . $typeAlias[0] . '/tables'); + $typeTable = $this->getTable('ContentType'); + $typeTable->load(['type_alias' => $typeAlias[0] . '.' . $typeAlias[1]]); + $contentTable = $typeTable->getContentTable(); + + if ($contentTable && $contentTable->load($typeAlias[2])) { + $helper = new CMSHelper(); + + $dataObject = $helper->getDataObject($contentTable); + $result = $this->getTable('ContentHistory')->getSha1(json_encode($dataObject), $typeTable); + } + + return $result; + } } diff --git a/administrator/components/com_contenthistory/src/Model/PreviewModel.php b/administrator/components/com_contenthistory/src/Model/PreviewModel.php index 7b10ed902f3db..f6276332541dc 100644 --- a/administrator/components/com_contenthistory/src/Model/PreviewModel.php +++ b/administrator/components/com_contenthistory/src/Model/PreviewModel.php @@ -1,4 +1,5 @@ getTable('ContentHistory'); - $versionId = Factory::getApplication()->input->getInt('version_id'); - - if (!$versionId || \is_array($versionId) || !$table->load($versionId)) - { - return false; - } - - $user = Factory::getUser(); - - // Access check - if (!$user->authorise('core.edit', $table->item_id) && !$this->canEdit($table)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $result = new \stdClass; - $result->version_note = $table->version_note; - $result->data = ContenthistoryHelper::prepareData($table); - - // Let's use custom calendars when present - $result->save_date = HTMLHelper::_('date', $table->save_date, Text::_('DATE_FORMAT_LC6')); - - $dateProperties = array ( - 'modified_time', - 'created_time', - 'modified', - 'created', - 'checked_out_time', - 'publish_up', - 'publish_down', - ); - - $nullDate = $this->getDatabase()->getNullDate(); - - foreach ($dateProperties as $dateProperty) - { - if (property_exists($result->data, $dateProperty) - && $result->data->$dateProperty->value !== null - && $result->data->$dateProperty->value !== $nullDate) - { - $result->data->$dateProperty->value = HTMLHelper::_( - 'date', - $result->data->$dateProperty->value, - Text::_('DATE_FORMAT_LC6') - ); - } - } - - return $result; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.2 - */ - public function getTable($type = 'ContentHistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - - /** - * Method to test whether a record is editable - * - * @param ContentHistory $record A Table object. - * - * @return boolean True if allowed to edit the record. Defaults to the permission set in the component. - * - * @since 3.6 - */ - protected function canEdit($record) - { - $result = false; - - if (!empty($record->item_id)) - { - /** - * Make sure user has edit privileges for this content item. Note that we use edit permissions - * for the content item, not delete permissions for the content history row. - */ - $user = Factory::getUser(); - $result = $user->authorise('core.edit', $record->item_id); - - // Finally try session (this catches edit.own case too) - if (!$result) - { - /** @var ContentType $contentTypeTable */ - $contentTypeTable = $this->getTable('ContentType'); - - $typeAlias = explode('.', $record->item_id); - $id = array_pop($typeAlias); - $typeAlias = implode('.', $typeAlias); - $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); - $result = in_array((int) $id, $typeEditables); - } - } - - return $result; - } + /** + * Method to get a version history row. + * + * @param integer $pk The id of the item + * + * @return \stdClass|boolean On success, standard object with row data. False on failure. + * + * @since 3.2 + * + * @throws NotAllowed Thrown if not authorised to edit an item + */ + public function getItem($pk = null) + { + /** @var ContentHistory $table */ + $table = $this->getTable('ContentHistory'); + $versionId = Factory::getApplication()->input->getInt('version_id'); + + if (!$versionId || \is_array($versionId) || !$table->load($versionId)) { + return false; + } + + $user = Factory::getUser(); + + // Access check + if (!$user->authorise('core.edit', $table->item_id) && !$this->canEdit($table)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $result = new \stdClass(); + $result->version_note = $table->version_note; + $result->data = ContenthistoryHelper::prepareData($table); + + // Let's use custom calendars when present + $result->save_date = HTMLHelper::_('date', $table->save_date, Text::_('DATE_FORMAT_LC6')); + + $dateProperties = array ( + 'modified_time', + 'created_time', + 'modified', + 'created', + 'checked_out_time', + 'publish_up', + 'publish_down', + ); + + $nullDate = $this->getDatabase()->getNullDate(); + + foreach ($dateProperties as $dateProperty) { + if ( + property_exists($result->data, $dateProperty) + && $result->data->$dateProperty->value !== null + && $result->data->$dateProperty->value !== $nullDate + ) { + $result->data->$dateProperty->value = HTMLHelper::_( + 'date', + $result->data->$dateProperty->value, + Text::_('DATE_FORMAT_LC6') + ); + } + } + + return $result; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.2 + */ + public function getTable($type = 'ContentHistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + + /** + * Method to test whether a record is editable + * + * @param ContentHistory $record A Table object. + * + * @return boolean True if allowed to edit the record. Defaults to the permission set in the component. + * + * @since 3.6 + */ + protected function canEdit($record) + { + $result = false; + + if (!empty($record->item_id)) { + /** + * Make sure user has edit privileges for this content item. Note that we use edit permissions + * for the content item, not delete permissions for the content history row. + */ + $user = Factory::getUser(); + $result = $user->authorise('core.edit', $record->item_id); + + // Finally try session (this catches edit.own case too) + if (!$result) { + /** @var ContentType $contentTypeTable */ + $contentTypeTable = $this->getTable('ContentType'); + + $typeAlias = explode('.', $record->item_id); + $id = array_pop($typeAlias); + $typeAlias = implode('.', $typeAlias); + $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); + $result = in_array((int) $id, $typeEditables); + } + } + + return $result; + } } diff --git a/administrator/components/com_contenthistory/src/View/Compare/HtmlView.php b/administrator/components/com_contenthistory/src/View/Compare/HtmlView.php index 8759e6357c9d6..c97014775c8a6 100644 --- a/administrator/components/com_contenthistory/src/View/Compare/HtmlView.php +++ b/administrator/components/com_contenthistory/src/View/Compare/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - parent::display($tpl); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + parent::display($tpl); + } } diff --git a/administrator/components/com_contenthistory/src/View/History/HtmlView.php b/administrator/components/com_contenthistory/src/View/History/HtmlView.php index 77bbc7eb2c289..beb8ee17951cb 100644 --- a/administrator/components/com_contenthistory/src/View/History/HtmlView.php +++ b/administrator/components/com_contenthistory/src/View/History/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->toolbar = $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page toolbar. - * - * @return Toolbar - * - * @since 4.0.0 - */ - protected function addToolbar(): Toolbar - { - /** @var Toolbar $toolbar */ - $toolbar = Factory::getContainer()->get(ToolbarFactoryInterface::class)->createToolbar('toolbar'); - - // Cache a session token for reuse throughout. - $token = Session::getFormToken(); - - // Clean up input to ensure a clean url. - $aliasArray = explode('.', $this->state->item_id); - $option = $aliasArray[1] == 'category' - ? 'com_categories&extension=' . implode('.', array_slice($aliasArray, 0, count($aliasArray) - 2)) - : $aliasArray[0]; - $filter = InputFilter::getInstance(); - $task = $filter->clean($aliasArray[1], 'cmd') . '.loadhistory'; - - // Build the final urls. - $loadUrl = Route::_('index.php?option=' . $filter->clean($option, 'cmd') . '&task=' . $task . '&' . $token . '=1'); - $previewUrl = Route::_('index.php?option=com_contenthistory&view=preview&layout=preview&tmpl=component&' . $token . '=1'); - $compareUrl = Route::_('index.php?option=com_contenthistory&view=compare&layout=compare&tmpl=component&' . $token . '=1'); - - $toolbar->basicButton('load') - ->attributes(['data-url' => $loadUrl]) - ->icon('icon-upload') - ->buttonClass('btn btn-success') - ->text('COM_CONTENTHISTORY_BUTTON_LOAD') - ->listCheck(true); - - $toolbar->basicButton('preview') - ->attributes(['data-url' => $previewUrl]) - ->icon('icon-search') - ->text('COM_CONTENTHISTORY_BUTTON_PREVIEW') - ->listCheck(true); - - $toolbar->basicButton('compare') - ->attributes(['data-url' => $compareUrl]) - ->icon('icon-search-plus') - ->text('COM_CONTENTHISTORY_BUTTON_COMPARE') - ->listCheck(true); - - $toolbar->basicButton('keep') - ->task('history.keep') - ->buttonClass('btn btn-inverse') - ->icon('icon-lock') - ->text('COM_CONTENTHISTORY_BUTTON_KEEP') - ->listCheck(true); - - $toolbar->basicButton('delete') - ->task('history.delete') - ->buttonClass('btn btn-danger') - ->icon('icon-times') - ->text('COM_CONTENTHISTORY_BUTTON_DELETE') - ->listCheck(true); - - return $toolbar; - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The model state + * + * @var Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->toolbar = $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page toolbar. + * + * @return Toolbar + * + * @since 4.0.0 + */ + protected function addToolbar(): Toolbar + { + /** @var Toolbar $toolbar */ + $toolbar = Factory::getContainer()->get(ToolbarFactoryInterface::class)->createToolbar('toolbar'); + + // Cache a session token for reuse throughout. + $token = Session::getFormToken(); + + // Clean up input to ensure a clean url. + $aliasArray = explode('.', $this->state->item_id); + $option = $aliasArray[1] == 'category' + ? 'com_categories&extension=' . implode('.', array_slice($aliasArray, 0, count($aliasArray) - 2)) + : $aliasArray[0]; + $filter = InputFilter::getInstance(); + $task = $filter->clean($aliasArray[1], 'cmd') . '.loadhistory'; + + // Build the final urls. + $loadUrl = Route::_('index.php?option=' . $filter->clean($option, 'cmd') . '&task=' . $task . '&' . $token . '=1'); + $previewUrl = Route::_('index.php?option=com_contenthistory&view=preview&layout=preview&tmpl=component&' . $token . '=1'); + $compareUrl = Route::_('index.php?option=com_contenthistory&view=compare&layout=compare&tmpl=component&' . $token . '=1'); + + $toolbar->basicButton('load') + ->attributes(['data-url' => $loadUrl]) + ->icon('icon-upload') + ->buttonClass('btn btn-success') + ->text('COM_CONTENTHISTORY_BUTTON_LOAD') + ->listCheck(true); + + $toolbar->basicButton('preview') + ->attributes(['data-url' => $previewUrl]) + ->icon('icon-search') + ->text('COM_CONTENTHISTORY_BUTTON_PREVIEW') + ->listCheck(true); + + $toolbar->basicButton('compare') + ->attributes(['data-url' => $compareUrl]) + ->icon('icon-search-plus') + ->text('COM_CONTENTHISTORY_BUTTON_COMPARE') + ->listCheck(true); + + $toolbar->basicButton('keep') + ->task('history.keep') + ->buttonClass('btn btn-inverse') + ->icon('icon-lock') + ->text('COM_CONTENTHISTORY_BUTTON_KEEP') + ->listCheck(true); + + $toolbar->basicButton('delete') + ->task('history.delete') + ->buttonClass('btn btn-danger') + ->icon('icon-times') + ->text('COM_CONTENTHISTORY_BUTTON_DELETE') + ->listCheck(true); + + return $toolbar; + } } diff --git a/administrator/components/com_contenthistory/src/View/Preview/HtmlView.php b/administrator/components/com_contenthistory/src/View/Preview/HtmlView.php index df61b6c29bac8..c73b50db46a0a 100644 --- a/administrator/components/com_contenthistory/src/View/Preview/HtmlView.php +++ b/administrator/components/com_contenthistory/src/View/Preview/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->item = $this->get('Item'); - if (false === $this->item) - { - Factory::getLanguage()->load('com_content', JPATH_SITE, null, true); + if (false === $this->item) { + Factory::getLanguage()->load('com_content', JPATH_SITE, null, true); - throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); - } + throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); + } - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - parent::display($tpl); - } + parent::display($tpl); + } } diff --git a/administrator/components/com_contenthistory/tmpl/compare/compare.php b/administrator/components/com_contenthistory/tmpl/compare/compare.php index 78e0a3fc2cd7f..c269fdde1fd74 100644 --- a/administrator/components/com_contenthistory/tmpl/compare/compare.php +++ b/administrator/components/com_contenthistory/tmpl/compare/compare.php @@ -1,4 +1,5 @@
    -

    +

    - - - - - - - - - - - - $value) : ?> - value) && isset($object2->$name->value) && $value->value != $object2->$name->value) : ?> - value)) : ?> - - - - value as $subName => $subValue) : ?> - $name->value->$subName->value ?? ''; ?> - value || $newSubValue) : ?> - value != $newSubValue) : ?> - - - - - - - - - - - - - - $name->value = is_object($object2->$name->value) ? json_encode($object2->$name->value) : $object2->$name->value; ?> - - - - - - - -
    - -
    - label; ?> -
      label; ?>value, ENT_COMPAT, 'UTF-8'); ?> 
    - label; ?> - value); ?>$name->value, ENT_COMPAT, 'UTF-8'); ?> 
    + + + + + + + + + + + + $value) : ?> + value) && isset($object2->$name->value) && $value->value != $object2->$name->value) : ?> + value)) : ?> + + + + value as $subName => $subValue) : ?> + $name->value->$subName->value ?? ''; ?> + value || $newSubValue) : ?> + value != $newSubValue) : ?> + + + + + + + + + + + + + + $name->value = is_object($object2->$name->value) ? json_encode($object2->$name->value) : $object2->$name->value; ?> + + + + + + + +
    + +
    + label; ?> +
      label; ?>value, ENT_COMPAT, 'UTF-8'); ?> 
    + label; ?> + value); ?>$name->value, ENT_COMPAT, 'UTF-8'); ?> 
    diff --git a/administrator/components/com_contenthistory/tmpl/history/modal.php b/administrator/components/com_contenthistory/tmpl/history/modal.php index 3f6f7cbf96d08..00dd1848e8751 100644 --- a/administrator/components/com_contenthistory/tmpl/history/modal.php +++ b/administrator/components/com_contenthistory/tmpl/history/modal.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('multiselect') - ->useScript('com_contenthistory.admin-history-modal'); + ->useScript('com_contenthistory.admin-history-modal'); ?>
    -
    - toolbar->render(); ?> -
    -
    - - - - - - - - - - - - - - - items as $item) : ?> - - - - - - - - - - - -
    - -
    - - - - - - - - - - - -
    - version_id, false, 'cid', 'cb', $item->save_date); ?> - - - save_date, Text::_('DATE_FORMAT_LC6')); ?> - - sha1_hash == $hash) : ?> - - - - version_note); ?> - - keep_forever) : ?> - - - - - - editor); ?> - - character_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> -
    +
    + toolbar->render(); ?> +
    + + + + + + + + + + + + + + + + items as $item) : ?> + + + + + + + + + + + +
    + +
    + + + + + + + + + + + +
    + version_id, false, 'cid', 'cb', $item->save_date); ?> + + + save_date, Text::_('DATE_FORMAT_LC6')); ?> + + sha1_hash == $hash) : ?> + + + + version_note); ?> + + keep_forever) : ?> + + + + + + editor); ?> + + character_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - + + + -
    +
    diff --git a/administrator/components/com_contenthistory/tmpl/preview/preview.php b/administrator/components/com_contenthistory/tmpl/preview/preview.php index a97b9f0457739..f6b6cc5ab9743 100644 --- a/administrator/components/com_contenthistory/tmpl/preview/preview.php +++ b/administrator/components/com_contenthistory/tmpl/preview/preview.php @@ -1,4 +1,5 @@
    -

    - item->save_date); ?> -

    - item->version_note) : ?> -

    - item->version_note); ?> -

    - +

    + item->save_date); ?> +

    + item->version_note) : ?> +

    + item->version_note); ?> +

    + - - - - - - - - - - item->data as $name => $value) : ?> - value)) : ?> - - - - value as $subName => $subValue) : ?> - - - - - - - - - - - - - - - -
    - -
    - label; ?> -
      label; ?>value; ?>
    label; ?>value; ?>
    + + + + + + + + + + item->data as $name => $value) : ?> + value)) : ?> + + + + value as $subName => $subValue) : ?> + + + + + + + + + + + + + + + +
    + +
    + label; ?> +
      label; ?>value; ?>
    label; ?>value; ?>
    diff --git a/administrator/components/com_cpanel/services/provider.php b/administrator/components/com_cpanel/services/provider.php index 0b42c914f2c14..064bc2bdf3e80 100644 --- a/administrator/components/com_cpanel/services/provider.php +++ b/administrator/components/com_cpanel/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Cpanel')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Cpanel')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Cpanel')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Cpanel')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_cpanel/src/Controller/DisplayController.php b/administrator/components/com_cpanel/src/Controller/DisplayController.php index 9a68c3f4ef416..460ed4f08689c 100644 --- a/administrator/components/com_cpanel/src/Controller/DisplayController.php +++ b/administrator/components/com_cpanel/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->set('tmpl', 'cpanel'); + /** + * Typical view method for MVC based architecture + * + * This function is provide as a default implementation, in most cases + * you will need to override it in your own controllers. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe url parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static An instance of the current object to support chaining. + * + * @since 3.0 + */ + public function display($cachable = false, $urlparams = array()) + { + /* + * Set the template - this will display cpanel.php + * from the selected admin template. + */ + $this->input->set('tmpl', 'cpanel'); - return parent::display($cachable, $urlparams); - } + return parent::display($cachable, $urlparams); + } - /** - * Method to add a module to a dashboard - * - * @since 4.0.0 - * - * @return void - */ - public function addModule() - { - $position = $this->input->get('position', 'cpanel'); - $function = $this->input->get('function'); + /** + * Method to add a module to a dashboard + * + * @since 4.0.0 + * + * @return void + */ + public function addModule() + { + $position = $this->input->get('position', 'cpanel'); + $function = $this->input->get('function'); - $appendLink = ''; + $appendLink = ''; - if ($function) - { - $appendLink .= '&function=' . $function; - } + if ($function) { + $appendLink .= '&function=' . $function; + } - if (substr($position, 0, 6) != 'cpanel') - { - $position = 'cpanel'; - } + if (substr($position, 0, 6) != 'cpanel') { + $position = 'cpanel'; + } - $this->app->setUserState('com_modules.modules.filter.position', $position); - $this->app->setUserState('com_modules.modules.client_id', '1'); + $this->app->setUserState('com_modules.modules.filter.position', $position); + $this->app->setUserState('com_modules.modules.client_id', '1'); - $this->setRedirect(Route::_('index.php?option=com_modules&view=select&tmpl=component&layout=modal' . $appendLink, false)); - } + $this->setRedirect(Route::_('index.php?option=com_modules&view=select&tmpl=component&layout=modal' . $appendLink, false)); + } } diff --git a/administrator/components/com_cpanel/src/Dispatcher/Dispatcher.php b/administrator/components/com_cpanel/src/Dispatcher/Dispatcher.php index 2dd0c8c74b23e..7492065d6e43b 100644 --- a/administrator/components/com_cpanel/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_cpanel/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->getCmd('dashboard', ''); - - $position = ApplicationHelper::stringURLSafe($dashboard); - - // Generate a title for the view cpanel - if (!empty($dashboard)) - { - $parts = explode('.', $dashboard); - $component = $parts[0]; - - if (strpos($component, 'com_') === false) - { - $component = 'com_' . $component; - } - - // Need to load the language file - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE) - || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); - $lang->load($component); - - // Lookup dashboard attributes from component manifest file - $manifestFile = JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml'; - - if (is_file($manifestFile)) - { - $manifest = simplexml_load_file($manifestFile); - - if ($dashboardManifests = $manifest->dashboards) - { - foreach ($dashboardManifests->children() as $dashboardManifest) - { - if ((string) $dashboardManifest === $dashboard) - { - $title = Text::_((string) $dashboardManifest->attributes()->title); - $icon = (string) $dashboardManifest->attributes()->icon; - - break; - } - } - } - } - - if (empty($title)) - { - // Try building a title - $prefix = strtoupper($component) . '_DASHBOARD'; - - $sectionkey = !empty($parts[1]) ? '_' . strtoupper($parts[1]) : ''; - $key = $prefix . $sectionkey . '_TITLE'; - $keyIcon = $prefix . $sectionkey . '_ICON'; - - // Search for a component title - if ($lang->hasKey($key)) - { - $title = Text::_($key); - } - else - { - // Try with a string from CPanel - $key = 'COM_CPANEL_DASHBOARD_' . $parts[0] . '_TITLE'; - - if ($lang->hasKey($key)) - { - $title = Text::_($key); - } - else - { - $title = Text::_('COM_CPANEL_DASHBOARD_BASE_TITLE'); - } - } - - // Define the icon - if (empty($parts[1])) - { - // Default core icons. - if ($parts[0] === 'components') - { - $icon = 'icon-puzzle-piece'; - } - elseif ($parts[0] === 'system') - { - $icon = 'icon-wrench'; - } - elseif ($parts[0] === 'help') - { - $icon = 'icon-info-circle'; - } - elseif ($lang->hasKey($keyIcon)) - { - $icon = Text::_($keyIcon); - } - else - { - $icon = 'icon-home'; - } - } - elseif ($lang->hasKey($keyIcon)) - { - $icon = Text::_($keyIcon); - } - } - } - else - { - // Home Dashboard - $title = Text::_('COM_CPANEL_DASHBOARD_BASE_TITLE'); - $icon = 'icon-home'; - } - - // Set toolbar items for the page - ToolbarHelper::title($title, $icon . ' cpanel'); - ToolbarHelper::help('screen.cpanel'); - - // Display the cpanel modules - $this->position = $position ? 'cpanel-' . $position : 'cpanel'; - $this->modules = ModuleHelper::getModules($this->position); - - $quickicons = $position ? 'icon-' . $position : 'icon'; - $this->quickicons = ModuleHelper::getModules($quickicons); - - parent::display($tpl); - } + /** + * Array of cpanel modules + * + * @var array + */ + protected $modules = null; + + /** + * Array of cpanel modules + * + * @var array + */ + protected $quickicons = null; + + /** + * Moduleposition to load + * + * @var string + */ + protected $position = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $dashboard = $app->input->getCmd('dashboard', ''); + + $position = ApplicationHelper::stringURLSafe($dashboard); + + // Generate a title for the view cpanel + if (!empty($dashboard)) { + $parts = explode('.', $dashboard); + $component = $parts[0]; + + if (strpos($component, 'com_') === false) { + $component = 'com_' . $component; + } + + // Need to load the language file + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE) + || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); + $lang->load($component); + + // Lookup dashboard attributes from component manifest file + $manifestFile = JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml'; + + if (is_file($manifestFile)) { + $manifest = simplexml_load_file($manifestFile); + + if ($dashboardManifests = $manifest->dashboards) { + foreach ($dashboardManifests->children() as $dashboardManifest) { + if ((string) $dashboardManifest === $dashboard) { + $title = Text::_((string) $dashboardManifest->attributes()->title); + $icon = (string) $dashboardManifest->attributes()->icon; + + break; + } + } + } + } + + if (empty($title)) { + // Try building a title + $prefix = strtoupper($component) . '_DASHBOARD'; + + $sectionkey = !empty($parts[1]) ? '_' . strtoupper($parts[1]) : ''; + $key = $prefix . $sectionkey . '_TITLE'; + $keyIcon = $prefix . $sectionkey . '_ICON'; + + // Search for a component title + if ($lang->hasKey($key)) { + $title = Text::_($key); + } else { + // Try with a string from CPanel + $key = 'COM_CPANEL_DASHBOARD_' . $parts[0] . '_TITLE'; + + if ($lang->hasKey($key)) { + $title = Text::_($key); + } else { + $title = Text::_('COM_CPANEL_DASHBOARD_BASE_TITLE'); + } + } + + // Define the icon + if (empty($parts[1])) { + // Default core icons. + if ($parts[0] === 'components') { + $icon = 'icon-puzzle-piece'; + } elseif ($parts[0] === 'system') { + $icon = 'icon-wrench'; + } elseif ($parts[0] === 'help') { + $icon = 'icon-info-circle'; + } elseif ($lang->hasKey($keyIcon)) { + $icon = Text::_($keyIcon); + } else { + $icon = 'icon-home'; + } + } elseif ($lang->hasKey($keyIcon)) { + $icon = Text::_($keyIcon); + } + } + } else { + // Home Dashboard + $title = Text::_('COM_CPANEL_DASHBOARD_BASE_TITLE'); + $icon = 'icon-home'; + } + + // Set toolbar items for the page + ToolbarHelper::title($title, $icon . ' cpanel'); + ToolbarHelper::help('screen.cpanel'); + + // Display the cpanel modules + $this->position = $position ? 'cpanel-' . $position : 'cpanel'; + $this->modules = ModuleHelper::getModules($this->position); + + $quickicons = $position ? 'icon-' . $position : 'icon'; + $this->quickicons = ModuleHelper::getModules($quickicons); + + parent::display($tpl); + } } diff --git a/administrator/components/com_cpanel/tmpl/cpanel/default.php b/administrator/components/com_cpanel/tmpl/cpanel/default.php index b11b265e6138c..20d1219de7c63 100644 --- a/administrator/components/com_cpanel/tmpl/cpanel/default.php +++ b/administrator/components/com_cpanel/tmpl/cpanel/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_cpanel.admin-cpanel') - ->useScript('com_cpanel.admin-addmodule'); + ->useScript('com_cpanel.admin-addmodule'); $user = Factory::getUser(); // Set up the bootstrap modal that will be used for all module editors echo HTMLHelper::_( - 'bootstrap.renderModal', - 'moduleDashboardAddModal', - array( - 'title' => Text::_('COM_CPANEL_ADD_MODULE_MODAL_TITLE'), - 'backdrop' => 'static', - 'url' => Route::_('index.php?option=com_cpanel&task=addModule&function=jSelectModuleType&position=' . $this->escape($this->position)), - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'footer' => '' - . '', - ) + 'bootstrap.renderModal', + 'moduleDashboardAddModal', + array( + 'title' => Text::_('COM_CPANEL_ADD_MODULE_MODAL_TITLE'), + 'backdrop' => 'static', + 'url' => Route::_('index.php?option=com_cpanel&task=addModule&function=jSelectModuleType&position=' . $this->escape($this->position)), + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'footer' => '' + . '', + ) ); ?>
    -
    -
    - quickicons) : - foreach ($this->quickicons as $iconmodule) - { - echo ModuleHelper::renderModule($iconmodule, array('style' => 'well')); - } - endif; - foreach ($this->modules as $module) - { - echo ModuleHelper::renderModule($module, array('style' => 'well')); - } - ?> - authorise('core.create', 'com_modules')) : ?> -
    -
    - -
    -
    - -
    -
    +
    +
    + quickicons) : + foreach ($this->quickicons as $iconmodule) { + echo ModuleHelper::renderModule($iconmodule, array('style' => 'well')); + } + endif; + foreach ($this->modules as $module) { + echo ModuleHelper::renderModule($module, array('style' => 'well')); + } + ?> + authorise('core.create', 'com_modules')) : ?> +
    +
    + +
    +
    + +
    +
    diff --git a/administrator/components/com_fields/helpers/fields.php b/administrator/components/com_fields/helpers/fields.php index 81d9091d5f76a..4c40ccee6b663 100644 --- a/administrator/components/com_fields/helpers/fields.php +++ b/administrator/components/com_fields/helpers/fields.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; /** diff --git a/administrator/components/com_fields/services/provider.php b/administrator/components/com_fields/services/provider.php index 1628402593709..bb7fc5dce97b2 100644 --- a/administrator/components/com_fields/services/provider.php +++ b/administrator/components/com_fields/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Fields')); - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Fields')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Fields')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Fields')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Fields')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Fields')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new FieldsComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new FieldsComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_fields/src/Controller/DisplayController.php b/administrator/components/com_fields/src/Controller/DisplayController.php index 97099ec795850..d5884bdeed53b 100644 --- a/administrator/components/com_fields/src/Controller/DisplayController.php +++ b/administrator/components/com_fields/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'fields'); - $id = $this->input->getInt('id'); + /** + * Typical view method for MVC based architecture + * + * This function is provide as a default implementation, in most cases + * you will need to override it in your own controllers. + * + * @param boolean $cachable If true, the view output will be cached + * @param array|bool $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()} + * + * @return BaseController|boolean A Controller object to support chaining. + * + * @since 3.7.0 + */ + public function display($cachable = false, $urlparams = false) + { + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'fields'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($vName == 'field' && !$this->checkEditId('com_fields.edit.field', $id)) - { - // 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', $id), 'error'); - } + // Check for edit form. + if ($vName == 'field' && !$this->checkEditId('com_fields.edit.field', $id)) { + // 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', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_fields&view=fields&context=' . $this->input->get('context'), false)); + $this->setRedirect(Route::_('index.php?option=com_fields&view=fields&context=' . $this->input->get('context'), false)); - return false; - } + return false; + } - return parent::display($cachable, $urlparams); - } + return parent::display($cachable, $urlparams); + } } diff --git a/administrator/components/com_fields/src/Controller/FieldController.php b/administrator/components/com_fields/src/Controller/FieldController.php index b0681ddb67185..6297a0aba5428 100644 --- a/administrator/components/com_fields/src/Controller/FieldController.php +++ b/administrator/components/com_fields/src/Controller/FieldController.php @@ -1,4 +1,5 @@ internalContext = $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'); - $parts = FieldsHelper::extract($this->internalContext); - $this->component = $parts ? $parts[0] : null; - } - - /** - * Method override to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 3.7.0 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', $this->component); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 3.7.0 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $user = $this->app->getIdentity(); - - // Zero record (id:0), return component edit permission by calling parent controller method - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Check edit on the record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->component . '.field.' . $recordId)) - { - return true; - } - - // Check edit own on the record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->component . '.field.' . $recordId)) - { - // Existing record already has an owner, get it - $record = $this->getModel()->getItem($recordId); - - if (empty($record)) - { - return false; - } - - // Grant if current user is owner of the record - return $user->id == $record->created_user_id; - } - - return false; - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 3.7.0 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('Field'); - - // Preset the redirect - $this->setRedirect('index.php?option=com_fields&view=fields&context=' . $this->internalContext); - - return parent::batch($model); - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 3.7.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - return parent::getRedirectToItemAppend($recordId) . '&context=' . $this->internalContext; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 3.7.0 - */ - protected function getRedirectToListAppend() - { - return parent::getRedirectToListAppend() . '&context=' . $this->internalContext; - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 3.7.0 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - $item = $model->getItem(); - - if (isset($item->params) && is_array($item->params)) - { - $registry = new Registry; - $registry->loadArray($item->params); - $item->params = (string) $registry; - } - } + /** + * @var string + */ + private $internalContext; + + /** + * @var string + */ + private $component; + + /** + * The prefix to use with controller messages. + * + * @var string + + * @since 3.7.0 + */ + protected $text_prefix = 'COM_FIELDS_FIELD'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 3.7.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->internalContext = $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'); + $parts = FieldsHelper::extract($this->internalContext); + $this->component = $parts ? $parts[0] : null; + } + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 3.7.0 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', $this->component); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 3.7.0 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $user = $this->app->getIdentity(); + + // Zero record (id:0), return component edit permission by calling parent controller method + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Check edit on the record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->component . '.field.' . $recordId)) { + return true; + } + + // Check edit own on the record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->component . '.field.' . $recordId)) { + // Existing record already has an owner, get it + $record = $this->getModel()->getItem($recordId); + + if (empty($record)) { + return false; + } + + // Grant if current user is owner of the record + return $user->id == $record->created_user_id; + } + + return false; + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 3.7.0 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('Field'); + + // Preset the redirect + $this->setRedirect('index.php?option=com_fields&view=fields&context=' . $this->internalContext); + + return parent::batch($model); + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 3.7.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + return parent::getRedirectToItemAppend($recordId) . '&context=' . $this->internalContext; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 3.7.0 + */ + protected function getRedirectToListAppend() + { + return parent::getRedirectToListAppend() . '&context=' . $this->internalContext; + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 3.7.0 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + $item = $model->getItem(); + + if (isset($item->params) && is_array($item->params)) { + $registry = new Registry(); + $registry->loadArray($item->params); + $item->params = (string) $registry; + } + } } diff --git a/administrator/components/com_fields/src/Controller/FieldsController.php b/administrator/components/com_fields/src/Controller/FieldsController.php index 9d7ad0c849212..20252b8089d38 100644 --- a/administrator/components/com_fields/src/Controller/FieldsController.php +++ b/administrator/components/com_fields/src/Controller/FieldsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for getModel. + * + * @param string $name The name of the model. + * @param string $prefix The prefix for the PHP class name. + * @param array $config Array of configuration parameters. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since 3.7.0 + */ + public function getModel($name = 'Field', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_fields/src/Controller/GroupController.php b/administrator/components/com_fields/src/Controller/GroupController.php index 1c644954aeed5..aed39c2bfda6f 100644 --- a/administrator/components/com_fields/src/Controller/GroupController.php +++ b/administrator/components/com_fields/src/Controller/GroupController.php @@ -1,4 +1,5 @@ input->getCmd('context')); - - if ($parts) - { - $this->component = $parts[0]; - } - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 3.7.0 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('Group'); - - // Preset the redirect - $this->setRedirect('index.php?option=com_fields&view=groups'); - - return parent::batch($model); - } - - /** - * Method override to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 3.7.0 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', $this->component); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 3.7.0 - */ - protected function allowEdit($data = array(), $key = 'parent_id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $user = $this->app->getIdentity(); - - // Zero record (parent_id:0), return component edit permission by calling parent controller method - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Check edit on the record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->component . '.fieldgroup.' . $recordId)) - { - return true; - } - - // Check edit own on the record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->component . '.fieldgroup.' . $recordId) || $user->authorise('core.edit.own', $this->component)) - { - // Existing record already has an owner, get it - $record = $this->getModel()->getItem($recordId); - - if (empty($record)) - { - return false; - } - - // Grant if current user is owner of the record - return $user->id == $record->created_by; - } - - return false; - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 3.7.0 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - $item = $model->getItem(); - - if (isset($item->params) && is_array($item->params)) - { - $registry = new Registry; - $registry->loadArray($item->params); - $item->params = (string) $registry; - } - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - $append .= '&context=' . $this->input->get('context'); - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&context=' . $this->input->get('context'); - - return $append; - } + /** + * The prefix to use with controller messages. + * + * @var string + + * @since 3.7.0 + */ + protected $text_prefix = 'COM_FIELDS_GROUP'; + + /** + * The component for which the group applies. + * + * @var string + * @since 3.7.0 + */ + private $component = ''; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 3.7.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $parts = FieldsHelper::extract($this->input->getCmd('context')); + + if ($parts) { + $this->component = $parts[0]; + } + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 3.7.0 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('Group'); + + // Preset the redirect + $this->setRedirect('index.php?option=com_fields&view=groups'); + + return parent::batch($model); + } + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 3.7.0 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', $this->component); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 3.7.0 + */ + protected function allowEdit($data = array(), $key = 'parent_id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $user = $this->app->getIdentity(); + + // Zero record (parent_id:0), return component edit permission by calling parent controller method + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Check edit on the record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->component . '.fieldgroup.' . $recordId)) { + return true; + } + + // Check edit own on the record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->component . '.fieldgroup.' . $recordId) || $user->authorise('core.edit.own', $this->component)) { + // Existing record already has an owner, get it + $record = $this->getModel()->getItem($recordId); + + if (empty($record)) { + return false; + } + + // Grant if current user is owner of the record + return $user->id == $record->created_by; + } + + return false; + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 3.7.0 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + $item = $model->getItem(); + + if (isset($item->params) && is_array($item->params)) { + $registry = new Registry(); + $registry->loadArray($item->params); + $item->params = (string) $registry; + } + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + $append .= '&context=' . $this->input->get('context'); + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&context=' . $this->input->get('context'); + + return $append; + } } diff --git a/administrator/components/com_fields/src/Controller/GroupsController.php b/administrator/components/com_fields/src/Controller/GroupsController.php index 247217bb79571..50e9f39c80ebf 100644 --- a/administrator/components/com_fields/src/Controller/GroupsController.php +++ b/administrator/components/com_fields/src/Controller/GroupsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for getModel. + * + * @param string $name The name of the model. + * @param string $prefix The prefix for the PHP class name. + * @param array $config Array of configuration parameters. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since 3.7.0 + */ + public function getModel($name = 'Group', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_fields/src/Dispatcher/Dispatcher.php b/administrator/components/com_fields/src/Dispatcher/Dispatcher.php index ba4e8515ae1b3..40cecc402e879 100644 --- a/administrator/components/com_fields/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_fields/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getUserStateFromRequest( - 'com_fields.groups.context', - 'context', - $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'), - 'CMD' - ); + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + */ + protected function checkAccess() + { + $context = $this->app->getUserStateFromRequest( + 'com_fields.groups.context', + 'context', + $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'), + 'CMD' + ); - $parts = FieldsHelper::extract($context); + $parts = FieldsHelper::extract($context); - if (!$parts || !$this->app->getIdentity()->authorise('core.manage', $parts[0])) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + if (!$parts || !$this->app->getIdentity()->authorise('core.manage', $parts[0])) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_fields/src/Extension/FieldsComponent.php b/administrator/components/com_fields/src/Extension/FieldsComponent.php index 68fa6b3bc9fb2..6709944fac394 100644 --- a/administrator/components/com_fields/src/Extension/FieldsComponent.php +++ b/administrator/components/com_fields/src/Extension/FieldsComponent.php @@ -1,4 +1,5 @@ getDatabase(); - - $query = $db->getQuery(true) - ->select('DISTINCT a.name AS text, a.element AS value') - ->from('#__extensions as a') - ->where('a.enabled >= 1') - ->where('a.type =' . $db->quote('component')); - - $items = $db->setQuery($query)->loadObjectList(); - - $options = []; - - if (count($items)) - { - $lang = Factory::getLanguage(); - - $components = []; - - // Search for components supporting Fieldgroups - suppose that these components support fields as well - foreach ($items as &$item) - { - $availableActions = Access::getActionsFromFile( - JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', - "/access/section[@name='fieldgroup']/" - ); - - if (!empty($availableActions)) - { - // Load language - $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; - $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) - || $lang->load($item->value . 'sys', $source); - - // Translate component name - $item->text = Text::_($item->text); - - $components[] = $item; - } - } - - if (empty($components)) - { - return []; - } - - foreach ($components as $component) - { - // Search for different contexts - $c = Factory::getApplication()->bootComponent($component->value); - - if ($c instanceof FieldsServiceInterface) - { - $contexts = $c->getContexts(); - - foreach ($contexts as $context) - { - $newOption = new \stdClass; - $newOption->value = strtolower($component->value . '.' . $context); - $newOption->text = $component->text . ' - ' . Text::_($context); - $options[] = $newOption; - } - } - else - { - $options[] = $component; - } - } - - // Sort by name - $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); - } - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $items); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'ComponentsFieldgroup'; + + /** + * Method to get a list of options for a list input. + * + * @return array An array of JHtml options. + * + * @since 3.7.0 + */ + protected function getOptions() + { + // Initialise variable. + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('DISTINCT a.name AS text, a.element AS value') + ->from('#__extensions as a') + ->where('a.enabled >= 1') + ->where('a.type =' . $db->quote('component')); + + $items = $db->setQuery($query)->loadObjectList(); + + $options = []; + + if (count($items)) { + $lang = Factory::getLanguage(); + + $components = []; + + // Search for components supporting Fieldgroups - suppose that these components support fields as well + foreach ($items as &$item) { + $availableActions = Access::getActionsFromFile( + JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', + "/access/section[@name='fieldgroup']/" + ); + + if (!empty($availableActions)) { + // Load language + $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; + $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) + || $lang->load($item->value . 'sys', $source); + + // Translate component name + $item->text = Text::_($item->text); + + $components[] = $item; + } + } + + if (empty($components)) { + return []; + } + + foreach ($components as $component) { + // Search for different contexts + $c = Factory::getApplication()->bootComponent($component->value); + + if ($c instanceof FieldsServiceInterface) { + $contexts = $c->getContexts(); + + foreach ($contexts as $context) { + $newOption = new \stdClass(); + $newOption->value = strtolower($component->value . '.' . $context); + $newOption->text = $component->text . ' - ' . Text::_($context); + $options[] = $newOption; + } + } else { + $options[] = $component; + } + } + + // Sort by name + $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $items); + + return $options; + } } diff --git a/administrator/components/com_fields/src/Field/ComponentsFieldsField.php b/administrator/components/com_fields/src/Field/ComponentsFieldsField.php index b55336b2316e3..a2c60901f2136 100644 --- a/administrator/components/com_fields/src/Field/ComponentsFieldsField.php +++ b/administrator/components/com_fields/src/Field/ComponentsFieldsField.php @@ -1,4 +1,5 @@ getDatabase(); - - $query = $db->getQuery(true) - ->select('DISTINCT a.name AS text, a.element AS value') - ->from('#__extensions as a') - ->where('a.enabled >= 1') - ->where('a.type =' . $db->quote('component')); - - $items = $db->setQuery($query)->loadObjectList(); - - $options = []; - - if (count($items)) - { - $lang = Factory::getLanguage(); - - $components = []; - - // Search for components supporting Fieldgroups - suppose that these components support fields as well - foreach ($items as &$item) - { - $availableActions = Access::getActionsFromFile( - JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', - "/access/section[@name='fieldgroup']/" - ); - - if (!empty($availableActions)) - { - // Load language - $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; - $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) - || $lang->load($item->value . 'sys', $source); - - // Translate component name - $item->text = Text::_($item->text); - - $components[] = $item; - } - } - - if (empty($components)) - { - return []; - } - - foreach ($components as $component) - { - // Search for different contexts - $c = Factory::getApplication()->bootComponent($component->value); - - if ($c instanceof FieldsServiceInterface) - { - $contexts = $c->getContexts(); - - foreach ($contexts as $context) - { - $newOption = new \stdClass; - $newOption->value = strtolower($component->value . '.' . $context); - $newOption->text = $component->text . ' - ' . Text::_($context); - $options[] = $newOption; - } - } - else - { - $options[] = $component; - } - } - - // Sort by name - $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); - } - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $items); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'ComponentsFields'; + + /** + * Method to get a list of options for a list input. + * + * @return array An array of JHtml options. + * + * @since 3.7.0 + */ + protected function getOptions() + { + // Initialise variable. + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('DISTINCT a.name AS text, a.element AS value') + ->from('#__extensions as a') + ->where('a.enabled >= 1') + ->where('a.type =' . $db->quote('component')); + + $items = $db->setQuery($query)->loadObjectList(); + + $options = []; + + if (count($items)) { + $lang = Factory::getLanguage(); + + $components = []; + + // Search for components supporting Fieldgroups - suppose that these components support fields as well + foreach ($items as &$item) { + $availableActions = Access::getActionsFromFile( + JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', + "/access/section[@name='fieldgroup']/" + ); + + if (!empty($availableActions)) { + // Load language + $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; + $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) + || $lang->load($item->value . 'sys', $source); + + // Translate component name + $item->text = Text::_($item->text); + + $components[] = $item; + } + } + + if (empty($components)) { + return []; + } + + foreach ($components as $component) { + // Search for different contexts + $c = Factory::getApplication()->bootComponent($component->value); + + if ($c instanceof FieldsServiceInterface) { + $contexts = $c->getContexts(); + + foreach ($contexts as $context) { + $newOption = new \stdClass(); + $newOption->value = strtolower($component->value . '.' . $context); + $newOption->text = $component->text . ' - ' . Text::_($context); + $options[] = $newOption; + } + } else { + $options[] = $component; + } + } + + // Sort by name + $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $items); + + return $options; + } } diff --git a/administrator/components/com_fields/src/Field/FieldLayoutField.php b/administrator/components/com_fields/src/Field/FieldLayoutField.php index bf45f58c8107c..e2a59e6d8d161 100644 --- a/administrator/components/com_fields/src/Field/FieldLayoutField.php +++ b/administrator/components/com_fields/src/Field/FieldLayoutField.php @@ -1,4 +1,5 @@ form->getValue('context')); - $extension = $extension[0]; - - if ($extension) - { - // Get the database object and a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Build the query. - $query->select('element, name') - ->from('#__extensions') - ->where('client_id = 0') - ->where('type = ' . $db->quote('template')) - ->where('enabled = 1'); - - // Set the query and load the templates. - $db->setQuery($query); - $templates = $db->loadObjectList('element'); - - // Build the search paths for component layouts. - $component_path = Path::clean(JPATH_SITE . '/components/' . $extension . '/layouts/field'); - - // Prepare array of component layouts - $component_layouts = array(); - - // Prepare the grouped list - $groups = array(); - - // Add "Use Default" - $groups[]['items'][] = HTMLHelper::_('select.option', '', Text::_('JOPTION_USE_DEFAULT')); - - // Add the layout options from the component path. - if (is_dir($component_path) && ($component_layouts = Folder::files($component_path, '^[^_]*\.php$', false, true))) - { - // Create the group for the component - $groups['_'] = array(); - $groups['_']['id'] = $this->id . '__'; - $groups['_']['text'] = Text::sprintf('JOPTION_FROM_COMPONENT'); - $groups['_']['items'] = array(); - - foreach ($component_layouts as $i => $file) - { - // Add an option to the component group - $value = basename($file, '.php'); - $component_layouts[$i] = $value; - - if ($value === 'render') - { - continue; - } - - $groups['_']['items'][] = HTMLHelper::_('select.option', $value, $value); - } - } - - // Loop on all templates - if ($templates) - { - foreach ($templates as $template) - { - $files = array(); - $template_paths = array( - Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/' . $extension . '/field'), - Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/com_fields/field'), - Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/field'), - ); - - // Add the layout options from the template paths. - foreach ($template_paths as $template_path) - { - if (is_dir($template_path)) - { - $files = array_merge($files, Folder::files($template_path, '^[^_]*\.php$', false, true)); - } - } - - foreach ($files as $i => $file) - { - $value = basename($file, '.php'); - - // Remove the default "render.php" or layout files that exist in the component folder - if ($value === 'render' || in_array($value, $component_layouts)) - { - unset($files[$i]); - } - } - - if (count($files)) - { - // Create the group for the template - $groups[$template->name] = array(); - $groups[$template->name]['id'] = $this->id . '_' . $template->element; - $groups[$template->name]['text'] = Text::sprintf('JOPTION_FROM_TEMPLATE', $template->name); - $groups[$template->name]['items'] = array(); - - foreach ($files as $file) - { - // Add an option to the template group - $value = basename($file, '.php'); - $groups[$template->name]['items'][] = HTMLHelper::_('select.option', $value, $value); - } - } - } - } - - // Compute attributes for the grouped list - $attr = $this->element['size'] ? ' size="' . (int) $this->element['size'] . '"' : ''; - $attr .= $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; - - // Prepare HTML code - $html = array(); - - // Compute the current selected values - $selected = array($this->value); - - // Add a grouped list - $html[] = HTMLHelper::_( - 'select.groupedlist', $groups, $this->name, - array('id' => $this->id, 'group.id' => 'id', 'list.attr' => $attr, 'list.select' => $selected) - ); - - return implode($html); - } - - return ''; - } + /** + * The form field type. + * + * @var string + * @since 3.9.0 + */ + protected $type = 'FieldLayout'; + + /** + * Method to get the field input for a field layout field. + * + * @return string The field input. + * + * @since 3.9.0 + */ + protected function getInput() + { + $extension = explode('.', $this->form->getValue('context')); + $extension = $extension[0]; + + if ($extension) { + // Get the database object and a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Build the query. + $query->select('element, name') + ->from('#__extensions') + ->where('client_id = 0') + ->where('type = ' . $db->quote('template')) + ->where('enabled = 1'); + + // Set the query and load the templates. + $db->setQuery($query); + $templates = $db->loadObjectList('element'); + + // Build the search paths for component layouts. + $component_path = Path::clean(JPATH_SITE . '/components/' . $extension . '/layouts/field'); + + // Prepare array of component layouts + $component_layouts = array(); + + // Prepare the grouped list + $groups = array(); + + // Add "Use Default" + $groups[]['items'][] = HTMLHelper::_('select.option', '', Text::_('JOPTION_USE_DEFAULT')); + + // Add the layout options from the component path. + if (is_dir($component_path) && ($component_layouts = Folder::files($component_path, '^[^_]*\.php$', false, true))) { + // Create the group for the component + $groups['_'] = array(); + $groups['_']['id'] = $this->id . '__'; + $groups['_']['text'] = Text::sprintf('JOPTION_FROM_COMPONENT'); + $groups['_']['items'] = array(); + + foreach ($component_layouts as $i => $file) { + // Add an option to the component group + $value = basename($file, '.php'); + $component_layouts[$i] = $value; + + if ($value === 'render') { + continue; + } + + $groups['_']['items'][] = HTMLHelper::_('select.option', $value, $value); + } + } + + // Loop on all templates + if ($templates) { + foreach ($templates as $template) { + $files = array(); + $template_paths = array( + Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/' . $extension . '/field'), + Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/com_fields/field'), + Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/field'), + ); + + // Add the layout options from the template paths. + foreach ($template_paths as $template_path) { + if (is_dir($template_path)) { + $files = array_merge($files, Folder::files($template_path, '^[^_]*\.php$', false, true)); + } + } + + foreach ($files as $i => $file) { + $value = basename($file, '.php'); + + // Remove the default "render.php" or layout files that exist in the component folder + if ($value === 'render' || in_array($value, $component_layouts)) { + unset($files[$i]); + } + } + + if (count($files)) { + // Create the group for the template + $groups[$template->name] = array(); + $groups[$template->name]['id'] = $this->id . '_' . $template->element; + $groups[$template->name]['text'] = Text::sprintf('JOPTION_FROM_TEMPLATE', $template->name); + $groups[$template->name]['items'] = array(); + + foreach ($files as $file) { + // Add an option to the template group + $value = basename($file, '.php'); + $groups[$template->name]['items'][] = HTMLHelper::_('select.option', $value, $value); + } + } + } + } + + // Compute attributes for the grouped list + $attr = $this->element['size'] ? ' size="' . (int) $this->element['size'] . '"' : ''; + $attr .= $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + + // Prepare HTML code + $html = array(); + + // Compute the current selected values + $selected = array($this->value); + + // Add a grouped list + $html[] = HTMLHelper::_( + 'select.groupedlist', + $groups, + $this->name, + array('id' => $this->id, 'group.id' => 'id', 'list.attr' => $attr, 'list.select' => $selected) + ); + + return implode($html); + } + + return ''; + } } diff --git a/administrator/components/com_fields/src/Field/FieldcontextsField.php b/administrator/components/com_fields/src/Field/FieldcontextsField.php index 3f223313bad9b..6f9fbffb7c1a0 100644 --- a/administrator/components/com_fields/src/Field/FieldcontextsField.php +++ b/administrator/components/com_fields/src/Field/FieldcontextsField.php @@ -1,4 +1,5 @@ getOptions() ? parent::getInput() : ''; - } + /** + * Method to get the field input markup for a generic list. + * Use the multiple attribute to enable multiselect. + * + * @return string The field input markup. + * + * @since 3.7.0 + */ + protected function getInput() + { + return $this->getOptions() ? parent::getInput() : ''; + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 3.7.0 - */ - protected function getOptions() - { - $parts = explode('.', $this->value); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.7.0 + */ + protected function getOptions() + { + $parts = explode('.', $this->value); - $component = Factory::getApplication()->bootComponent($parts[0]); + $component = Factory::getApplication()->bootComponent($parts[0]); - if ($component instanceof FieldsServiceInterface) - { - return $component->getContexts(); - } + if ($component instanceof FieldsServiceInterface) { + return $component->getContexts(); + } - return []; - } + return []; + } } diff --git a/administrator/components/com_fields/src/Field/FieldgroupsField.php b/administrator/components/com_fields/src/Field/FieldgroupsField.php index c8a8b24746f68..94c9cc2e0cf19 100644 --- a/administrator/components/com_fields/src/Field/FieldgroupsField.php +++ b/administrator/components/com_fields/src/Field/FieldgroupsField.php @@ -1,4 +1,5 @@ element['context']; - $states = $this->element['state'] ?: '0,1'; - $states = ArrayHelper::toInteger(explode(',', $states)); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.7.0 + */ + protected function getOptions() + { + $context = (string) $this->element['context']; + $states = $this->element['state'] ?: '0,1'; + $states = ArrayHelper::toInteger(explode(',', $states)); - $user = Factory::getUser(); - $viewlevels = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); + $user = Factory::getUser(); + $viewlevels = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('title', 'text'), - $db->quoteName('id', 'value'), - $db->quoteName('state'), - ] - ); - $query->from($db->quoteName('#__fields_groups')); - $query->whereIn($db->quoteName('state'), $states); - $query->where($db->quoteName('context') . ' = :context'); - $query->whereIn($db->quoteName('access'), $viewlevels); - $query->order('ordering asc, id asc'); - $query->bind(':context', $context); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('title', 'text'), + $db->quoteName('id', 'value'), + $db->quoteName('state'), + ] + ); + $query->from($db->quoteName('#__fields_groups')); + $query->whereIn($db->quoteName('state'), $states); + $query->where($db->quoteName('context') . ' = :context'); + $query->whereIn($db->quoteName('access'), $viewlevels); + $query->order('ordering asc, id asc'); + $query->bind(':context', $context); - $db->setQuery($query); - $options = $db->loadObjectList(); + $db->setQuery($query); + $options = $db->loadObjectList(); - foreach ($options AS $option) - { - if ($option->state == 0) - { - $option->text = '[' . $option->text . ']'; - } + foreach ($options as $option) { + if ($option->state == 0) { + $option->text = '[' . $option->text . ']'; + } - if ($option->state == 2) - { - $option->text = '{' . $option->text . '}'; - } - } + if ($option->state == 2) { + $option->text = '{' . $option->text . '}'; + } + } - return array_merge(parent::getOptions(), $options); - } + return array_merge(parent::getOptions(), $options); + } } diff --git a/administrator/components/com_fields/src/Field/SectionField.php b/administrator/components/com_fields/src/Field/SectionField.php index 2c3549c7b1a3e..89ce923a03518 100644 --- a/administrator/components/com_fields/src/Field/SectionField.php +++ b/administrator/components/com_fields/src/Field/SectionField.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 3.7.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $return = parent::setup($element, $value, $group); + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 3.7.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $return = parent::setup($element, $value, $group); - // Onchange must always be the change context function - $this->onchange = 'Joomla.fieldsChangeContext(this.value);'; + // Onchange must always be the change context function + $this->onchange = 'Joomla.fieldsChangeContext(this.value);'; - return $return; - } + return $return; + } - /** - * Method to get the field input markup for a generic list. - * Use the multiple attribute to enable multiselect. - * - * @return string The field input markup. - * - * @since 3.7.0 - */ - protected function getInput() - { - Factory::getApplication()->getDocument()->getWebAssetManager() - ->useScript('com_fields.admin-field-changecontext'); + /** + * Method to get the field input markup for a generic list. + * Use the multiple attribute to enable multiselect. + * + * @return string The field input markup. + * + * @since 3.7.0 + */ + protected function getInput() + { + Factory::getApplication()->getDocument()->getWebAssetManager() + ->useScript('com_fields.admin-field-changecontext'); - return parent::getInput(); - } + return parent::getInput(); + } } diff --git a/administrator/components/com_fields/src/Field/SubfieldsField.php b/administrator/components/com_fields/src/Field/SubfieldsField.php index 1640d6cb1cb58..04a09e05a0673 100644 --- a/administrator/components/com_fields/src/Field/SubfieldsField.php +++ b/administrator/components/com_fields/src/Field/SubfieldsField.php @@ -1,4 +1,5 @@ context])) - { - static::$customFieldsCache[$this->context] = FieldsHelper::getFields($this->context, null, false, null, true); - } - - // Iterate over the custom fields for this context - foreach (static::$customFieldsCache[$this->context] as $customField) - { - // Skip our own subform type. We won't have subform in subform. - if ($customField->type == 'subform') - { - continue; - } - - $options[] = HTMLHelper::_( - 'select.option', - $customField->id, - ($customField->title . ' (' . $customField->type . ')') - ); - } - - // Sorting the fields based on the text which is displayed - usort( - $options, - function ($a, $b) - { - return strcmp($a->text, $b->text); - } - ); - - if (count($options) == 0) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_FIELDS_NO_FIELDS_TO_CREATE_SUBFORM_FIELD_WARNING'), 'warning'); - } - - return $options; - } - - /** - * Method to attach a JForm object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $return = parent::setup($element, $value, $group); - - if ($return) - { - $this->context = (string) $this->element['context']; - } - - return $return; - } + /** + * The name of this Field type. + * + * @var string + * + * @since 4.0.0 + */ + public $type = 'Subfields'; + + /** + * Configuration option for this field type to could filter the displayed custom field instances + * by a given context. Default value empty string. If given empty string, displays all custom fields. + * + * @var string + * + * @since 4.0.0 + */ + protected $context = ''; + + /** + * Array to do a fast in-memory caching of all custom field items. Used to not bother the + * FieldsHelper with a call every time this field is being rendered. + * + * @var array + * + * @since 4.0.0 + */ + protected static $customFieldsCache = array(); + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 4.0.0 + */ + protected function getOptions() + { + $options = parent::getOptions(); + + // Check whether we have a result for this context yet + if (!isset(static::$customFieldsCache[$this->context])) { + static::$customFieldsCache[$this->context] = FieldsHelper::getFields($this->context, null, false, null, true); + } + + // Iterate over the custom fields for this context + foreach (static::$customFieldsCache[$this->context] as $customField) { + // Skip our own subform type. We won't have subform in subform. + if ($customField->type == 'subform') { + continue; + } + + $options[] = HTMLHelper::_( + 'select.option', + $customField->id, + ($customField->title . ' (' . $customField->type . ')') + ); + } + + // Sorting the fields based on the text which is displayed + usort( + $options, + function ($a, $b) { + return strcmp($a->text, $b->text); + } + ); + + if (count($options) == 0) { + Factory::getApplication()->enqueueMessage(Text::_('COM_FIELDS_NO_FIELDS_TO_CREATE_SUBFORM_FIELD_WARNING'), 'warning'); + } + + return $options; + } + + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $return = parent::setup($element, $value, $group); + + if ($return) { + $this->context = (string) $this->element['context']; + } + + return $return; + } } diff --git a/administrator/components/com_fields/src/Field/TypeField.php b/administrator/components/com_fields/src/Field/TypeField.php index d48b1aefa780d..6207eee88b8f2 100644 --- a/administrator/components/com_fields/src/Field/TypeField.php +++ b/administrator/components/com_fields/src/Field/TypeField.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 3.7.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $return = parent::setup($element, $value, $group); + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 3.7.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $return = parent::setup($element, $value, $group); - $this->onchange = 'Joomla.typeHasChanged(this);'; + $this->onchange = 'Joomla.typeHasChanged(this);'; - return $return; - } + return $return; + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 3.7.0 - */ - protected function getOptions() - { - $options = parent::getOptions(); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.7.0 + */ + protected function getOptions() + { + $options = parent::getOptions(); - $fieldTypes = FieldsHelper::getFieldTypes(); + $fieldTypes = FieldsHelper::getFieldTypes(); - foreach ($fieldTypes as $fieldType) - { - $options[] = HTMLHelper::_('select.option', $fieldType['type'], $fieldType['label']); - } + foreach ($fieldTypes as $fieldType) { + $options[] = HTMLHelper::_('select.option', $fieldType['type'], $fieldType['label']); + } - // Sorting the fields based on the text which is displayed - usort( - $options, - function ($a, $b) - { - return strcmp($a->text, $b->text); - } - ); + // Sorting the fields based on the text which is displayed + usort( + $options, + function ($a, $b) { + return strcmp($a->text, $b->text); + } + ); - // Load scripts - Factory::getApplication()->getDocument()->getWebAssetManager() - ->useScript('com_fields.admin-field-typehaschanged') - ->useScript('webcomponent.core-loader'); + // Load scripts + Factory::getApplication()->getDocument()->getWebAssetManager() + ->useScript('com_fields.admin-field-typehaschanged') + ->useScript('webcomponent.core-loader'); - return $options; - } + return $options; + } } diff --git a/administrator/components/com_fields/src/Helper/FieldsHelper.php b/administrator/components/com_fields/src/Helper/FieldsHelper.php index e2f39866ab86b..242bf787f3763 100644 --- a/administrator/components/com_fields/src/Helper/FieldsHelper.php +++ b/administrator/components/com_fields/src/Helper/FieldsHelper.php @@ -1,4 +1,5 @@ bootComponent($parts[0]); - - if ($component instanceof FieldsServiceInterface) - { - $newSection = $component->validateSection($parts[1], $item); - } - - if ($newSection) - { - $parts[1] = $newSection; - } - - return $parts; - } - - /** - * Returns the fields for the given context. - * If the item is an object the returned fields do have an additional field - * "value" which represents the value for the given item. If the item has an - * assigned_cat_ids field, then additionally fields which belong to that - * category will be returned. - * Should the value being prepared to be shown in an HTML context then - * prepareValue must be set to true. No further escaping needs to be done. - * The values of the fields can be overridden by an associative array where the keys - * have to be a name and its corresponding value. - * - * @param string $context The context of the content passed to the helper - * @param null $item The item being edited in the form - * @param int|bool $prepareValue (if int is display event): 1 - AfterTitle, 2 - BeforeDisplay, 3 - AfterDisplay, 0 - OFF - * @param array|null $valuesToOverride The values to override - * @param bool $includeSubformFields Should I include fields marked as Only Use In Subform? - * - * @return array - * - * @throws \Exception - * @since 3.7.0 - */ - public static function getFields( - $context, $item = null, $prepareValue = false, array $valuesToOverride = null, bool $includeSubformFields = false - ) - { - if (self::$fieldsCache === null) - { - // Load the model - self::$fieldsCache = Factory::getApplication()->bootComponent('com_fields') - ->getMVCFactory()->createModel('Fields', 'Administrator', ['ignore_request' => true]); - - self::$fieldsCache->setState('filter.state', 1); - self::$fieldsCache->setState('list.limit', 0); - } - - if ($includeSubformFields) - { - self::$fieldsCache->setState('filter.only_use_in_subform', ''); - } - else - { - self::$fieldsCache->setState('filter.only_use_in_subform', 0); - } - - if (is_array($item)) - { - $item = (object) $item; - } - - if (Multilanguage::isEnabled() && isset($item->language) && $item->language != '*') - { - self::$fieldsCache->setState('filter.language', array('*', $item->language)); - } - - self::$fieldsCache->setState('filter.context', $context); - self::$fieldsCache->setState('filter.assigned_cat_ids', array()); - - /* - * If item has assigned_cat_ids parameter display only fields which - * belong to the category - */ - if ($item && (isset($item->catid) || isset($item->fieldscatid))) - { - $assignedCatIds = $item->catid ?? $item->fieldscatid; - - if (!is_array($assignedCatIds)) - { - $assignedCatIds = explode(',', $assignedCatIds); - } - - // Fields without any category assigned should show as well - $assignedCatIds[] = 0; - - self::$fieldsCache->setState('filter.assigned_cat_ids', $assignedCatIds); - } - - $fields = self::$fieldsCache->getItems(); - - if ($fields === false) - { - return array(); - } - - if ($item && isset($item->id)) - { - if (self::$fieldCache === null) - { - self::$fieldCache = Factory::getApplication()->bootComponent('com_fields') - ->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); - } - - $fieldIds = array_map( - function ($f) - { - return $f->id; - }, - $fields - ); - - $fieldValues = self::$fieldCache->getFieldValues($fieldIds, $item->id); - - $new = array(); - - foreach ($fields as $key => $original) - { - /* - * Doing a clone, otherwise fields for different items will - * always reference to the same object - */ - $field = clone $original; - - if ($valuesToOverride && array_key_exists($field->name, $valuesToOverride)) - { - $field->value = $valuesToOverride[$field->name]; - } - elseif ($valuesToOverride && array_key_exists($field->id, $valuesToOverride)) - { - $field->value = $valuesToOverride[$field->id]; - } - elseif (array_key_exists($field->id, $fieldValues)) - { - $field->value = $fieldValues[$field->id]; - } - - if (!isset($field->value) || $field->value === '') - { - $field->value = $field->default_value; - } - - $field->rawvalue = $field->value; - - // If boolean prepare, if int, it is the event type: 1 - After Title, 2 - Before Display Content, 3 - After Display Content, 0 - Do not prepare - if ($prepareValue && (is_bool($prepareValue) || $prepareValue === (int) $field->params->get('display', '2'))) - { - PluginHelper::importPlugin('fields'); - - /* - * On before field prepare - * Event allow plugins to modify the output of the field before it is prepared - */ - Factory::getApplication()->triggerEvent('onCustomFieldsBeforePrepareField', array($context, $item, &$field)); - - // Gathering the value for the field - $value = Factory::getApplication()->triggerEvent('onCustomFieldsPrepareField', array($context, $item, &$field)); - - if (is_array($value)) - { - $value = implode(' ', $value); - } - - /* - * On after field render - * Event allows plugins to modify the output of the prepared field - */ - Factory::getApplication()->triggerEvent('onCustomFieldsAfterPrepareField', array($context, $item, $field, &$value)); - - // Assign the value - $field->value = $value; - } - - $new[$key] = $field; - } - - $fields = $new; - } - - return $fields; - } - - /** - * Renders the layout file and data on the context and does a fall back to - * Fields afterwards. - * - * @param string $context The context of the content passed to the helper - * @param string $layoutFile layoutFile - * @param array $displayData displayData - * - * @return NULL|string - * - * @since 3.7.0 - */ - public static function render($context, $layoutFile, $displayData) - { - $value = ''; - - /* - * Because the layout refreshes the paths before the render function is - * called, so there is no way to load the layout overrides in the order - * template -> context -> fields. - * If there is no override in the context then we need to call the - * layout from Fields. - */ - if ($parts = self::extract($context)) - { - // Trying to render the layout on the component from the context - $value = LayoutHelper::render($layoutFile, $displayData, null, array('component' => $parts[0], 'client' => 0)); - } - - if ($value == '') - { - // Trying to render the layout on Fields itself - $value = LayoutHelper::render($layoutFile, $displayData, null, array('component' => 'com_fields','client' => 0)); - } - - return $value; - } - - /** - * PrepareForm - * - * @param string $context The context of the content passed to the helper - * @param Form $form form - * @param object $data data. - * - * @return boolean - * - * @since 3.7.0 - */ - public static function prepareForm($context, Form $form, $data) - { - // Extracting the component and section - $parts = self::extract($context); - - if (! $parts) - { - return true; - } - - $context = $parts[0] . '.' . $parts[1]; - - // When no fields available return here - $fields = self::getFields($parts[0] . '.' . $parts[1], new CMSObject); - - if (! $fields) - { - return true; - } - - $component = $parts[0]; - $section = $parts[1]; - - $assignedCatids = $data->catid ?? $data->fieldscatid ?? $form->getValue('catid'); - - // Account for case that a submitted form has a multi-value category id field (e.g. a filtering form), just use the first category - $assignedCatids = is_array($assignedCatids) - ? (int) reset($assignedCatids) - : (int) $assignedCatids; - - if (!$assignedCatids && $formField = $form->getField('catid')) - { - $assignedCatids = $formField->getAttribute('default', null); - - if (!$assignedCatids) - { - // Choose the first category available - $catOptions = $formField->options; - - if ($catOptions && !empty($catOptions[0]->value)) - { - $assignedCatids = (int) $catOptions[0]->value; - } - } - - $data->fieldscatid = $assignedCatids; - } - - /* - * If there is a catid field we need to reload the page when the catid - * is changed - */ - if ($form->getField('catid') && $parts[0] != 'com_fields') - { - /* - * Setting some parameters for the category field - */ - $form->setFieldAttribute('catid', 'refresh-enabled', true); - $form->setFieldAttribute('catid', 'refresh-cat-id', $assignedCatids); - $form->setFieldAttribute('catid', 'refresh-section', $section); - } - - // Getting the fields - $fields = self::getFields($parts[0] . '.' . $parts[1], $data); - - if (!$fields) - { - return true; - } - - $fieldTypes = self::getFieldTypes(); - - // Creating the dom - $xml = new \DOMDocument('1.0', 'UTF-8'); - $fieldsNode = $xml->appendChild(new \DOMElement('form'))->appendChild(new \DOMElement('fields')); - $fieldsNode->setAttribute('name', 'com_fields'); - - // Organizing the fields according to their group - $fieldsPerGroup = array(0 => array()); - - foreach ($fields as $field) - { - if (!array_key_exists($field->type, $fieldTypes)) - { - // Field type is not available - continue; - } - - if (!array_key_exists($field->group_id, $fieldsPerGroup)) - { - $fieldsPerGroup[$field->group_id] = array(); - } - - if ($path = $fieldTypes[$field->type]['path']) - { - // Add the lookup path for the field - FormHelper::addFieldPath($path); - } - - if ($path = $fieldTypes[$field->type]['rules']) - { - // Add the lookup path for the rule - FormHelper::addRulePath($path); - } - - $fieldsPerGroup[$field->group_id][] = $field; - } - - $model = Factory::getApplication()->bootComponent('com_fields') - ->getMVCFactory()->createModel('Groups', 'Administrator', ['ignore_request' => true]); - $model->setState('filter.context', $context); - - /** - * $model->getItems() would only return existing groups, but we also - * have the 'default' group with id 0 which is not in the database, - * so we create it virtually here. - */ - $defaultGroup = new \stdClass; - $defaultGroup->id = 0; - $defaultGroup->title = ''; - $defaultGroup->description = ''; - $iterateGroups = array_merge(array($defaultGroup), $model->getItems()); - - // Looping through the groups - foreach ($iterateGroups as $group) - { - if (empty($fieldsPerGroup[$group->id])) - { - continue; - } - - // Defining the field set - /** @var \DOMElement $fieldset */ - $fieldset = $fieldsNode->appendChild(new \DOMElement('fieldset')); - $fieldset->setAttribute('name', 'fields-' . $group->id); - $fieldset->setAttribute('addfieldpath', '/administrator/components/' . $component . '/models/fields'); - $fieldset->setAttribute('addrulepath', '/administrator/components/' . $component . '/models/rules'); - - $label = $group->title; - $description = $group->description; - - if (!$label) - { - $key = strtoupper($component . '_FIELDS_' . $section . '_LABEL'); - - if (!Factory::getLanguage()->hasKey($key)) - { - $key = 'JGLOBAL_FIELDS'; - } - - $label = $key; - } - - if (!$description) - { - $key = strtoupper($component . '_FIELDS_' . $section . '_DESC'); - - if (Factory::getLanguage()->hasKey($key)) - { - $description = $key; - } - } - - $fieldset->setAttribute('label', $label); - $fieldset->setAttribute('description', strip_tags($description)); - - // Looping through the fields for that context - foreach ($fieldsPerGroup[$group->id] as $field) - { - try - { - Factory::getApplication()->triggerEvent('onCustomFieldsPrepareDom', array($field, $fieldset, $form)); - - /* - * If the field belongs to an assigned_cat_id but the assigned_cat_ids in the data - * is not known, set the required flag to false on any circumstance. - */ - if (!$assignedCatids && !empty($field->assigned_cat_ids) && $form->getField($field->name)) - { - $form->setFieldAttribute($field->name, 'required', 'false'); - } - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - // When the field set is empty, then remove it - if (!$fieldset->hasChildNodes()) - { - $fieldsNode->removeChild($fieldset); - } - } - - // Loading the XML fields string into the form - $form->load($xml->saveXML()); - - $model = Factory::getApplication()->bootComponent('com_fields') - ->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); - - if ((!isset($data->id) || !$data->id) && Factory::getApplication()->input->getCmd('controller') == 'modules' - && Factory::getApplication()->isClient('site')) - { - // Modules on front end editing don't have data and an id set - $data->id = Factory::getApplication()->input->getInt('id'); - } - - // Looping through the fields again to set the value - if (!isset($data->id) || !$data->id) - { - return true; - } - - foreach ($fields as $field) - { - $value = $model->getFieldValue($field->id, $data->id); - - if ($value === null) - { - continue; - } - - if (!is_array($value) && $value !== '') - { - // Function getField doesn't cache the fields, so we try to do it only when necessary - $formField = $form->getField($field->name, 'com_fields'); - - if ($formField && $formField->forceMultiple) - { - $value = (array) $value; - } - } - - // Setting the value on the field - $form->setValue($field->name, 'com_fields', $value); - } - - return true; - } - - /** - * Return a boolean if the actual logged in user can edit the given field value. - * - * @param \stdClass $field The field - * - * @return boolean - * - * @since 3.7.0 - */ - public static function canEditFieldValue($field) - { - $parts = self::extract($field->context); - - return Factory::getUser()->authorise('core.edit.value', $parts[0] . '.field.' . (int) $field->id); - } - - /** - * Return a boolean based on field (and field group) display / show_on settings - * - * @param \stdClass $field The field - * - * @return boolean - * - * @since 3.8.7 - */ - public static function displayFieldOnForm($field) - { - $app = Factory::getApplication(); - - // Detect if the field should be shown at all - if ($field->params->get('show_on') == 1 && $app->isClient('administrator')) - { - return false; - } - elseif ($field->params->get('show_on') == 2 && $app->isClient('site')) - { - return false; - } - - if (!self::canEditFieldValue($field)) - { - $fieldDisplayReadOnly = $field->params->get('display_readonly', '2'); - - if ($fieldDisplayReadOnly == '2') - { - // Inherit from field group display read-only setting - $groupModel = $app->bootComponent('com_fields') - ->getMVCFactory()->createModel('Group', 'Administrator', ['ignore_request' => true]); - $groupDisplayReadOnly = $groupModel->getItem($field->group_id)->params->get('display_readonly', '1'); - $fieldDisplayReadOnly = $groupDisplayReadOnly; - } - - if ($fieldDisplayReadOnly == '0') - { - // Do not display field on form when field is read-only - return false; - } - } - - // Display field on form - return true; - } - - /** - * Gets assigned categories ids for a field - * - * @param \stdClass[] $fieldId The field ID - * - * @return array Array with the assigned category ids - * - * @since 4.0.0 - */ - public static function getAssignedCategoriesIds($fieldId) - { - $fieldId = (int) $fieldId; - - if (!$fieldId) - { - return array(); - } - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - $query->select($db->quoteName('a.category_id')) - ->from($db->quoteName('#__fields_categories', 'a')) - ->where('a.field_id = ' . $fieldId); - - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Gets assigned categories titles for a field - * - * @param \stdClass[] $fieldId The field ID - * - * @return array Array with the assigned categories - * - * @since 3.7.0 - */ - public static function getAssignedCategoriesTitles($fieldId) - { - $fieldId = (int) $fieldId; - - if (!$fieldId) - { - return []; - } - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - $query->select($db->quoteName('c.title')) - ->from($db->quoteName('#__fields_categories', 'a')) - ->join('INNER', $db->quoteName('#__categories', 'c') . ' ON a.category_id = c.id') - ->where($db->quoteName('field_id') . ' = :fieldid') - ->bind(':fieldid', $fieldId, ParameterType::INTEGER); - - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Gets the fields system plugin extension id. - * - * @return integer The fields system plugin extension id. - * - * @since 3.7.0 - */ - public static function getFieldsPluginId() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('fields')); - $db->setQuery($query); - - try - { - $result = (int) $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - $result = 0; - } - - return $result; - } - - /** - * Loads the fields plugins and returns an array of field types from the plugins. - * - * The returned array contains arrays with the following keys: - * - label: The label of the field - * - type: The type of the field - * - path: The path of the folder where the field can be found - * - * @return array - * - * @since 3.7.0 - */ - public static function getFieldTypes() - { - PluginHelper::importPlugin('fields'); - $eventData = Factory::getApplication()->triggerEvent('onCustomFieldsGetTypes'); - - $data = array(); - - foreach ($eventData as $fields) - { - foreach ($fields as $fieldDescription) - { - if (!array_key_exists('path', $fieldDescription)) - { - $fieldDescription['path'] = null; - } - - if (!array_key_exists('rules', $fieldDescription)) - { - $fieldDescription['rules'] = null; - } - - $data[$fieldDescription['type']] = $fieldDescription; - } - } - - return $data; - } - - /** - * Clears the internal cache for the custom fields. - * - * @return void - * - * @since 3.8.0 - */ - public static function clearFieldsCache() - { - self::$fieldCache = null; - self::$fieldsCache = null; - } + /** + * @var FieldsModel + */ + private static $fieldsCache = null; + + /** + * @var FieldsModel + */ + private static $fieldCache = null; + + /** + * Extracts the component and section from the context string which has to + * be in the format component.context. + * + * @param string $contextString contextString + * @param object $item optional item object + * + * @return array|null + * + * @since 3.7.0 + */ + public static function extract($contextString, $item = null) + { + $parts = explode('.', $contextString, 2); + + if (count($parts) < 2) { + return null; + } + + $newSection = ''; + + $component = Factory::getApplication()->bootComponent($parts[0]); + + if ($component instanceof FieldsServiceInterface) { + $newSection = $component->validateSection($parts[1], $item); + } + + if ($newSection) { + $parts[1] = $newSection; + } + + return $parts; + } + + /** + * Returns the fields for the given context. + * If the item is an object the returned fields do have an additional field + * "value" which represents the value for the given item. If the item has an + * assigned_cat_ids field, then additionally fields which belong to that + * category will be returned. + * Should the value being prepared to be shown in an HTML context then + * prepareValue must be set to true. No further escaping needs to be done. + * The values of the fields can be overridden by an associative array where the keys + * have to be a name and its corresponding value. + * + * @param string $context The context of the content passed to the helper + * @param null $item The item being edited in the form + * @param int|bool $prepareValue (if int is display event): 1 - AfterTitle, 2 - BeforeDisplay, 3 - AfterDisplay, 0 - OFF + * @param array|null $valuesToOverride The values to override + * @param bool $includeSubformFields Should I include fields marked as Only Use In Subform? + * + * @return array + * + * @throws \Exception + * @since 3.7.0 + */ + public static function getFields( + $context, + $item = null, + $prepareValue = false, + array $valuesToOverride = null, + bool $includeSubformFields = false + ) { + if (self::$fieldsCache === null) { + // Load the model + self::$fieldsCache = Factory::getApplication()->bootComponent('com_fields') + ->getMVCFactory()->createModel('Fields', 'Administrator', ['ignore_request' => true]); + + self::$fieldsCache->setState('filter.state', 1); + self::$fieldsCache->setState('list.limit', 0); + } + + if ($includeSubformFields) { + self::$fieldsCache->setState('filter.only_use_in_subform', ''); + } else { + self::$fieldsCache->setState('filter.only_use_in_subform', 0); + } + + if (is_array($item)) { + $item = (object) $item; + } + + if (Multilanguage::isEnabled() && isset($item->language) && $item->language != '*') { + self::$fieldsCache->setState('filter.language', array('*', $item->language)); + } + + self::$fieldsCache->setState('filter.context', $context); + self::$fieldsCache->setState('filter.assigned_cat_ids', array()); + + /* + * If item has assigned_cat_ids parameter display only fields which + * belong to the category + */ + if ($item && (isset($item->catid) || isset($item->fieldscatid))) { + $assignedCatIds = $item->catid ?? $item->fieldscatid; + + if (!is_array($assignedCatIds)) { + $assignedCatIds = explode(',', $assignedCatIds); + } + + // Fields without any category assigned should show as well + $assignedCatIds[] = 0; + + self::$fieldsCache->setState('filter.assigned_cat_ids', $assignedCatIds); + } + + $fields = self::$fieldsCache->getItems(); + + if ($fields === false) { + return array(); + } + + if ($item && isset($item->id)) { + if (self::$fieldCache === null) { + self::$fieldCache = Factory::getApplication()->bootComponent('com_fields') + ->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); + } + + $fieldIds = array_map( + function ($f) { + return $f->id; + }, + $fields + ); + + $fieldValues = self::$fieldCache->getFieldValues($fieldIds, $item->id); + + $new = array(); + + foreach ($fields as $key => $original) { + /* + * Doing a clone, otherwise fields for different items will + * always reference to the same object + */ + $field = clone $original; + + if ($valuesToOverride && array_key_exists($field->name, $valuesToOverride)) { + $field->value = $valuesToOverride[$field->name]; + } elseif ($valuesToOverride && array_key_exists($field->id, $valuesToOverride)) { + $field->value = $valuesToOverride[$field->id]; + } elseif (array_key_exists($field->id, $fieldValues)) { + $field->value = $fieldValues[$field->id]; + } + + if (!isset($field->value) || $field->value === '') { + $field->value = $field->default_value; + } + + $field->rawvalue = $field->value; + + // If boolean prepare, if int, it is the event type: 1 - After Title, 2 - Before Display Content, 3 - After Display Content, 0 - Do not prepare + if ($prepareValue && (is_bool($prepareValue) || $prepareValue === (int) $field->params->get('display', '2'))) { + PluginHelper::importPlugin('fields'); + + /* + * On before field prepare + * Event allow plugins to modify the output of the field before it is prepared + */ + Factory::getApplication()->triggerEvent('onCustomFieldsBeforePrepareField', array($context, $item, &$field)); + + // Gathering the value for the field + $value = Factory::getApplication()->triggerEvent('onCustomFieldsPrepareField', array($context, $item, &$field)); + + if (is_array($value)) { + $value = implode(' ', $value); + } + + /* + * On after field render + * Event allows plugins to modify the output of the prepared field + */ + Factory::getApplication()->triggerEvent('onCustomFieldsAfterPrepareField', array($context, $item, $field, &$value)); + + // Assign the value + $field->value = $value; + } + + $new[$key] = $field; + } + + $fields = $new; + } + + return $fields; + } + + /** + * Renders the layout file and data on the context and does a fall back to + * Fields afterwards. + * + * @param string $context The context of the content passed to the helper + * @param string $layoutFile layoutFile + * @param array $displayData displayData + * + * @return NULL|string + * + * @since 3.7.0 + */ + public static function render($context, $layoutFile, $displayData) + { + $value = ''; + + /* + * Because the layout refreshes the paths before the render function is + * called, so there is no way to load the layout overrides in the order + * template -> context -> fields. + * If there is no override in the context then we need to call the + * layout from Fields. + */ + if ($parts = self::extract($context)) { + // Trying to render the layout on the component from the context + $value = LayoutHelper::render($layoutFile, $displayData, null, array('component' => $parts[0], 'client' => 0)); + } + + if ($value == '') { + // Trying to render the layout on Fields itself + $value = LayoutHelper::render($layoutFile, $displayData, null, array('component' => 'com_fields','client' => 0)); + } + + return $value; + } + + /** + * PrepareForm + * + * @param string $context The context of the content passed to the helper + * @param Form $form form + * @param object $data data. + * + * @return boolean + * + * @since 3.7.0 + */ + public static function prepareForm($context, Form $form, $data) + { + // Extracting the component and section + $parts = self::extract($context); + + if (! $parts) { + return true; + } + + $context = $parts[0] . '.' . $parts[1]; + + // When no fields available return here + $fields = self::getFields($parts[0] . '.' . $parts[1], new CMSObject()); + + if (! $fields) { + return true; + } + + $component = $parts[0]; + $section = $parts[1]; + + $assignedCatids = $data->catid ?? $data->fieldscatid ?? $form->getValue('catid'); + + // Account for case that a submitted form has a multi-value category id field (e.g. a filtering form), just use the first category + $assignedCatids = is_array($assignedCatids) + ? (int) reset($assignedCatids) + : (int) $assignedCatids; + + if (!$assignedCatids && $formField = $form->getField('catid')) { + $assignedCatids = $formField->getAttribute('default', null); + + if (!$assignedCatids) { + // Choose the first category available + $catOptions = $formField->options; + + if ($catOptions && !empty($catOptions[0]->value)) { + $assignedCatids = (int) $catOptions[0]->value; + } + } + + $data->fieldscatid = $assignedCatids; + } + + /* + * If there is a catid field we need to reload the page when the catid + * is changed + */ + if ($form->getField('catid') && $parts[0] != 'com_fields') { + /* + * Setting some parameters for the category field + */ + $form->setFieldAttribute('catid', 'refresh-enabled', true); + $form->setFieldAttribute('catid', 'refresh-cat-id', $assignedCatids); + $form->setFieldAttribute('catid', 'refresh-section', $section); + } + + // Getting the fields + $fields = self::getFields($parts[0] . '.' . $parts[1], $data); + + if (!$fields) { + return true; + } + + $fieldTypes = self::getFieldTypes(); + + // Creating the dom + $xml = new \DOMDocument('1.0', 'UTF-8'); + $fieldsNode = $xml->appendChild(new \DOMElement('form'))->appendChild(new \DOMElement('fields')); + $fieldsNode->setAttribute('name', 'com_fields'); + + // Organizing the fields according to their group + $fieldsPerGroup = array(0 => array()); + + foreach ($fields as $field) { + if (!array_key_exists($field->type, $fieldTypes)) { + // Field type is not available + continue; + } + + if (!array_key_exists($field->group_id, $fieldsPerGroup)) { + $fieldsPerGroup[$field->group_id] = array(); + } + + if ($path = $fieldTypes[$field->type]['path']) { + // Add the lookup path for the field + FormHelper::addFieldPath($path); + } + + if ($path = $fieldTypes[$field->type]['rules']) { + // Add the lookup path for the rule + FormHelper::addRulePath($path); + } + + $fieldsPerGroup[$field->group_id][] = $field; + } + + $model = Factory::getApplication()->bootComponent('com_fields') + ->getMVCFactory()->createModel('Groups', 'Administrator', ['ignore_request' => true]); + $model->setState('filter.context', $context); + + /** + * $model->getItems() would only return existing groups, but we also + * have the 'default' group with id 0 which is not in the database, + * so we create it virtually here. + */ + $defaultGroup = new \stdClass(); + $defaultGroup->id = 0; + $defaultGroup->title = ''; + $defaultGroup->description = ''; + $iterateGroups = array_merge(array($defaultGroup), $model->getItems()); + + // Looping through the groups + foreach ($iterateGroups as $group) { + if (empty($fieldsPerGroup[$group->id])) { + continue; + } + + // Defining the field set + /** @var \DOMElement $fieldset */ + $fieldset = $fieldsNode->appendChild(new \DOMElement('fieldset')); + $fieldset->setAttribute('name', 'fields-' . $group->id); + $fieldset->setAttribute('addfieldpath', '/administrator/components/' . $component . '/models/fields'); + $fieldset->setAttribute('addrulepath', '/administrator/components/' . $component . '/models/rules'); + + $label = $group->title; + $description = $group->description; + + if (!$label) { + $key = strtoupper($component . '_FIELDS_' . $section . '_LABEL'); + + if (!Factory::getLanguage()->hasKey($key)) { + $key = 'JGLOBAL_FIELDS'; + } + + $label = $key; + } + + if (!$description) { + $key = strtoupper($component . '_FIELDS_' . $section . '_DESC'); + + if (Factory::getLanguage()->hasKey($key)) { + $description = $key; + } + } + + $fieldset->setAttribute('label', $label); + $fieldset->setAttribute('description', strip_tags($description)); + + // Looping through the fields for that context + foreach ($fieldsPerGroup[$group->id] as $field) { + try { + Factory::getApplication()->triggerEvent('onCustomFieldsPrepareDom', array($field, $fieldset, $form)); + + /* + * If the field belongs to an assigned_cat_id but the assigned_cat_ids in the data + * is not known, set the required flag to false on any circumstance. + */ + if (!$assignedCatids && !empty($field->assigned_cat_ids) && $form->getField($field->name)) { + $form->setFieldAttribute($field->name, 'required', 'false'); + } + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + // When the field set is empty, then remove it + if (!$fieldset->hasChildNodes()) { + $fieldsNode->removeChild($fieldset); + } + } + + // Loading the XML fields string into the form + $form->load($xml->saveXML()); + + $model = Factory::getApplication()->bootComponent('com_fields') + ->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); + + if ( + (!isset($data->id) || !$data->id) && Factory::getApplication()->input->getCmd('controller') == 'modules' + && Factory::getApplication()->isClient('site') + ) { + // Modules on front end editing don't have data and an id set + $data->id = Factory::getApplication()->input->getInt('id'); + } + + // Looping through the fields again to set the value + if (!isset($data->id) || !$data->id) { + return true; + } + + foreach ($fields as $field) { + $value = $model->getFieldValue($field->id, $data->id); + + if ($value === null) { + continue; + } + + if (!is_array($value) && $value !== '') { + // Function getField doesn't cache the fields, so we try to do it only when necessary + $formField = $form->getField($field->name, 'com_fields'); + + if ($formField && $formField->forceMultiple) { + $value = (array) $value; + } + } + + // Setting the value on the field + $form->setValue($field->name, 'com_fields', $value); + } + + return true; + } + + /** + * Return a boolean if the actual logged in user can edit the given field value. + * + * @param \stdClass $field The field + * + * @return boolean + * + * @since 3.7.0 + */ + public static function canEditFieldValue($field) + { + $parts = self::extract($field->context); + + return Factory::getUser()->authorise('core.edit.value', $parts[0] . '.field.' . (int) $field->id); + } + + /** + * Return a boolean based on field (and field group) display / show_on settings + * + * @param \stdClass $field The field + * + * @return boolean + * + * @since 3.8.7 + */ + public static function displayFieldOnForm($field) + { + $app = Factory::getApplication(); + + // Detect if the field should be shown at all + if ($field->params->get('show_on') == 1 && $app->isClient('administrator')) { + return false; + } elseif ($field->params->get('show_on') == 2 && $app->isClient('site')) { + return false; + } + + if (!self::canEditFieldValue($field)) { + $fieldDisplayReadOnly = $field->params->get('display_readonly', '2'); + + if ($fieldDisplayReadOnly == '2') { + // Inherit from field group display read-only setting + $groupModel = $app->bootComponent('com_fields') + ->getMVCFactory()->createModel('Group', 'Administrator', ['ignore_request' => true]); + $groupDisplayReadOnly = $groupModel->getItem($field->group_id)->params->get('display_readonly', '1'); + $fieldDisplayReadOnly = $groupDisplayReadOnly; + } + + if ($fieldDisplayReadOnly == '0') { + // Do not display field on form when field is read-only + return false; + } + } + + // Display field on form + return true; + } + + /** + * Gets assigned categories ids for a field + * + * @param \stdClass[] $fieldId The field ID + * + * @return array Array with the assigned category ids + * + * @since 4.0.0 + */ + public static function getAssignedCategoriesIds($fieldId) + { + $fieldId = (int) $fieldId; + + if (!$fieldId) { + return array(); + } + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->select($db->quoteName('a.category_id')) + ->from($db->quoteName('#__fields_categories', 'a')) + ->where('a.field_id = ' . $fieldId); + + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Gets assigned categories titles for a field + * + * @param \stdClass[] $fieldId The field ID + * + * @return array Array with the assigned categories + * + * @since 3.7.0 + */ + public static function getAssignedCategoriesTitles($fieldId) + { + $fieldId = (int) $fieldId; + + if (!$fieldId) { + return []; + } + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->select($db->quoteName('c.title')) + ->from($db->quoteName('#__fields_categories', 'a')) + ->join('INNER', $db->quoteName('#__categories', 'c') . ' ON a.category_id = c.id') + ->where($db->quoteName('field_id') . ' = :fieldid') + ->bind(':fieldid', $fieldId, ParameterType::INTEGER); + + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Gets the fields system plugin extension id. + * + * @return integer The fields system plugin extension id. + * + * @since 3.7.0 + */ + public static function getFieldsPluginId() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('fields')); + $db->setQuery($query); + + try { + $result = (int) $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + $result = 0; + } + + return $result; + } + + /** + * Loads the fields plugins and returns an array of field types from the plugins. + * + * The returned array contains arrays with the following keys: + * - label: The label of the field + * - type: The type of the field + * - path: The path of the folder where the field can be found + * + * @return array + * + * @since 3.7.0 + */ + public static function getFieldTypes() + { + PluginHelper::importPlugin('fields'); + $eventData = Factory::getApplication()->triggerEvent('onCustomFieldsGetTypes'); + + $data = array(); + + foreach ($eventData as $fields) { + foreach ($fields as $fieldDescription) { + if (!array_key_exists('path', $fieldDescription)) { + $fieldDescription['path'] = null; + } + + if (!array_key_exists('rules', $fieldDescription)) { + $fieldDescription['rules'] = null; + } + + $data[$fieldDescription['type']] = $fieldDescription; + } + } + + return $data; + } + + /** + * Clears the internal cache for the custom fields. + * + * @return void + * + * @since 3.8.0 + */ + public static function clearFieldsCache() + { + self::$fieldCache = null; + self::$fieldsCache = null; + } } diff --git a/administrator/components/com_fields/src/Model/FieldModel.php b/administrator/components/com_fields/src/Model/FieldModel.php index d8af27d19629f..100bcee4e7406 100644 --- a/administrator/components/com_fields/src/Model/FieldModel.php +++ b/administrator/components/com_fields/src/Model/FieldModel.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage' - ); - - /** - * @var array - * - * @since 3.7.0 - */ - private $valueCache = array(); - - /** - * Constructor - * - * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). - * @param MVCFactoryInterface $factory The factory. - * - * @since 3.7.0 - * @throws \Exception - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null) - { - parent::__construct($config, $factory); - - $this->typeAlias = Factory::getApplication()->input->getCmd('context', 'com_content.article') . '.field'; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success, False on error. - * - * @since 3.7.0 - */ - public function save($data) - { - $field = null; - - if (isset($data['id']) && $data['id']) - { - $field = $this->getItem($data['id']); - } - - if (!isset($data['label']) && isset($data['params']['label'])) - { - $data['label'] = $data['params']['label']; - - unset($data['params']['label']); - } - - // Alter the title for save as copy - $input = Factory::getApplication()->input; - - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['title'] == $origTable->title) - { - list($title, $name) = $this->generateNewTitle($data['group_id'], $data['name'], $data['title']); - $data['title'] = $title; - $data['label'] = $title; - $data['name'] = $name; - } - else - { - if ($data['name'] == $origTable->name) - { - $data['name'] = ''; - } - } - - $data['state'] = 0; - } - - // Load the fields plugins, perhaps they want to do something - PluginHelper::importPlugin('fields'); - - $message = $this->checkDefaultValue($data); - - if ($message !== true) - { - $this->setError($message); - - return false; - } - - if (!parent::save($data)) - { - return false; - } - - // Save the assigned categories into #__fields_categories - $db = $this->getDatabase(); - $id = (int) $this->getState('field.id'); - - /** - * If the field is only used in subform, set Category to None automatically so that it will only be displayed - * as part of SubForm on add/edit item screen - */ - if (!empty($data['only_use_in_subform'])) - { - $cats = [-1]; - } - else - { - $cats = isset($data['assigned_cat_ids']) ? (array) $data['assigned_cat_ids'] : array(); - $cats = ArrayHelper::toInteger($cats); - } - - $assignedCatIds = array(); - - foreach ($cats as $cat) - { - // If we have found the 'JNONE' category, remove all other from the result and break. - if ($cat == '-1') - { - $assignedCatIds = array('-1'); - break; - } - - if ($cat) - { - $assignedCatIds[] = $cat; - } - } - - // First delete all assigned categories - $query = $db->getQuery(true); - $query->delete('#__fields_categories') - ->where($db->quoteName('field_id') . ' = :fieldid') - ->bind(':fieldid', $id, ParameterType::INTEGER); - - $db->setQuery($query); - $db->execute(); - - // Inset new assigned categories - $tupel = new \stdClass; - $tupel->field_id = $id; - - foreach ($assignedCatIds as $catId) - { - $tupel->category_id = $catId; - $db->insertObject('#__fields_categories', $tupel); - } - - /** - * If the options have changed, delete the values. This should only apply for list, checkboxes and radio - * custom field types, because when their options are being changed, their values might get invalid, because - * e.g. there is a value selected from a list, which is not part of the list anymore. Hence we need to delete - * all values that are not part of the options anymore. Note: The only field types with fieldparams+options - * are those above listed plus the subfields type. And we do explicitly not want the values to be deleted - * when the options of a subfields field are getting changed. - */ - if ($field && in_array($field->type, array('list', 'checkboxes', 'radio'), true) - && isset($data['fieldparams']['options']) && isset($field->fieldparams['options'])) - { - $oldParams = $this->getParams($field->fieldparams['options']); - $newParams = $this->getParams($data['fieldparams']['options']); - - if (is_object($oldParams) && is_object($newParams) && $oldParams != $newParams) - { - // Get new values. - $names = array_column((array) $newParams, 'value'); - - $fieldId = (int) $field->id; - $query = $db->getQuery(true); - $query->delete($db->quoteName('#__fields_values')) - ->where($db->quoteName('field_id') . ' = :fieldid') - ->bind(':fieldid', $fieldId, ParameterType::INTEGER); - - // If new values are set, delete only old values. Otherwise delete all values. - if ($names) - { - $query->whereNotIn($db->quoteName('value'), $names, ParameterType::STRING); - } - - $db->setQuery($query); - $db->execute(); - } - } - - FieldsHelper::clearFieldsCache(); - - return true; - } - - - /** - * Checks if the default value is valid for the given data. If a string is returned then - * it can be assumed that the default value is invalid. - * - * @param array $data The data. - * - * @return true|string true if valid, a string containing the exception message when not. - * - * @since 3.7.0 - */ - private function checkDefaultValue($data) - { - // Empty default values are correct - if (empty($data['default_value']) && $data['default_value'] !== '0') - { - return true; - } - - $types = FieldsHelper::getFieldTypes(); - - // Check if type exists - if (!array_key_exists($data['type'], $types)) - { - return true; - } - - $path = $types[$data['type']]['rules']; - - // Add the path for the rules of the plugin when available - if ($path) - { - // Add the lookup path for the rule - FormHelper::addRulePath($path); - } - - // Create the fields object - $obj = (object) $data; - $obj->params = new Registry($obj->params); - $obj->fieldparams = new Registry(!empty($obj->fieldparams) ? $obj->fieldparams : array()); - - // Prepare the dom - $dom = new \DOMDocument; - $node = $dom->appendChild(new \DOMElement('form')); - - // Trigger the event to create the field dom node - $form = new Form($data['context']); - $form->setDatabase($this->getDatabase()); - Factory::getApplication()->triggerEvent('onCustomFieldsPrepareDom', array($obj, $node, $form)); - - // Check if a node is created - if (!$node->firstChild) - { - return true; - } - - // Define the type either from the field or from the data - $type = $node->firstChild->getAttribute('validate') ? : $data['type']; - - // Load the rule - $rule = FormHelper::loadRuleType($type); - - // When no rule exists, we allow the default value - if (!$rule) - { - return true; - } - - if ($rule instanceof DatabaseAwareInterface) - { - try - { - $rule->setDatabase($this->getDatabase()); - } - catch (DatabaseNotFoundException $e) - { - @trigger_error(sprintf('Database must be set, this will not be caught anymore in 5.0.'), E_USER_DEPRECATED); - $rule->setDatabase(Factory::getContainer()->get(DatabaseInterface::class)); - } - } - - try - { - // Perform the check - $result = $rule->test(simplexml_import_dom($node->firstChild), $data['default_value']); - - // Check if the test succeeded - return $result === true ? : Text::_('COM_FIELDS_FIELD_INVALID_DEFAULT_VALUE'); - } - catch (\UnexpectedValueException $e) - { - return $e->getMessage(); - } - } - - /** - * Converts the unknown params into an object. - * - * @param mixed $params The params. - * - * @return \stdClass Object on success, false on failure. - * - * @since 3.7.0 - */ - private function getParams($params) - { - if (is_string($params)) - { - $params = json_decode($params); - } - - if (is_array($params)) - { - $params = (object) $params; - } - - return $params; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 3.7.0 - */ - public function getItem($pk = null) - { - $result = parent::getItem($pk); - - if ($result) - { - // Prime required properties. - if (empty($result->id)) - { - $result->context = Factory::getApplication()->input->getCmd('context', $this->getState('field.context')); - } - - if (property_exists($result, 'fieldparams') && $result->fieldparams !== null) - { - $registry = new Registry; - - if ($result->fieldparams) - { - $registry->loadString($result->fieldparams); - } - - $result->fieldparams = $registry->toArray(); - } - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $fieldId = (int) $result->id; - $query->select($db->quoteName('category_id')) - ->from($db->quoteName('#__fields_categories')) - ->where($db->quoteName('field_id') . ' = :fieldid') - ->bind(':fieldid', $fieldId, ParameterType::INTEGER); - - $db->setQuery($query); - $result->assigned_cat_ids = $db->loadColumn() ?: array(0); - } - - return $result; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.7.0 - * @throws \Exception - */ - public function getTable($name = 'Field', $prefix = 'Administrator', $options = array()) - { - // Default to text type - $table = parent::getTable($name, $prefix, $options); - $table->type = 'text'; - - return $table; - } - - /** - * Method to change the title & name. - * - * @param integer $categoryId The id of the category. - * @param string $name The name. - * @param string $title The title. - * - * @return array Contains the modified title and name. - * - * @since 3.7.0 - */ - protected function generateNewTitle($categoryId, $name, $title) - { - // Alter the title & name - $table = $this->getTable(); - - while ($table->load(array('name' => $name))) - { - $title = StringHelper::increment($title); - $name = StringHelper::increment($name, 'dash'); - } - - return array( - $title, - $name, - ); - } - - /** - * Method to delete one or more records. - * - * @param array $pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 3.7.0 - */ - public function delete(&$pks) - { - $success = parent::delete($pks); - - if ($success) - { - $pks = (array) $pks; - $pks = ArrayHelper::toInteger($pks); - $pks = array_filter($pks); - - if (!empty($pks)) - { - // Delete Values - $query = $this->getDatabase()->getQuery(true); - - $query->delete($query->quoteName('#__fields_values')) - ->whereIn($query->quoteName('field_id'), $pks); - - $this->getDatabase()->setQuery($query)->execute(); - - // Delete Assigned Categories - $query = $this->getDatabase()->getQuery(true); - - $query->delete($query->quoteName('#__fields_categories')) - ->whereIn($query->quoteName('field_id'), $pks); - - $this->getDatabase()->setQuery($query)->execute(); - } - } - - return $success; - } - - /** - * Abstract method for getting the form from the model. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 3.7.0 - */ - public function getForm($data = array(), $loadData = true) - { - $context = $this->getState('field.context'); - $jinput = Factory::getApplication()->input; - - // A workaround to get the context into the model for save requests. - if (empty($context) && isset($data['context'])) - { - $context = $data['context']; - $parts = FieldsHelper::extract($context); - - $this->setState('field.context', $context); - - if ($parts) - { - $this->setState('field.component', $parts[0]); - $this->setState('field.section', $parts[1]); - } - } - - if (isset($data['type'])) - { - // This is needed that the plugins can determine the type - $this->setState('field.type', $data['type']); - } - - // Load the fields plugin that they can add additional parameters to the form - PluginHelper::importPlugin('fields'); - - // Get the form. - $form = $this->loadForm( - 'com_fields.field.' . $context, 'field', - array( - 'control' => 'jform', - 'load_data' => true, - ) - ); - - if (empty($form)) - { - return false; - } - - // Modify the form based on Edit State access controls. - if (empty($data['context'])) - { - $data['context'] = $context; - } - - $fieldId = $jinput->get('id'); - $assetKey = $this->state->get('field.component') . '.field.' . $fieldId; - - if (!Factory::getUser()->authorise('core.edit.state', $assetKey)) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('state', 'disabled', 'true'); - - // Disable fields while saving. The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('state', 'filter', 'unset'); - } - - // Don't allow to change the created_user_id user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_user_id', 'filter', 'unset'); - } - - // In case we are editing a field, field type cannot be changed, so some extra handling below is needed - if ($fieldId) - { - $fieldType = $form->getField('type'); - - if ($fieldType->value == 'subform') - { - // Only Use In subform should not be available for subform field type, so we remove it - $form->removeField('only_use_in_subform'); - } - else - { - // Field type could not be changed, so remove showon attribute to avoid js errors - $form->setFieldAttribute('only_use_in_subform', 'showon', ''); - } - } - - return $form; - } - - /** - * Setting the value for the given field id, context and item id. - * - * @param string $fieldId The field ID. - * @param string $itemId The ID of the item. - * @param string $value The value. - * - * @return boolean - * - * @since 3.7.0 - */ - public function setFieldValue($fieldId, $itemId, $value) - { - $field = $this->getItem($fieldId); - $params = $field->params; - - if (is_array($params)) - { - $params = new Registry($params); - } - - // Don't save the value when the user is not authorized to change it - if (!$field || !FieldsHelper::canEditFieldValue($field)) - { - return false; - } - - $needsDelete = false; - $needsInsert = false; - $needsUpdate = false; - - $oldValue = $this->getFieldValue($fieldId, $itemId); - $value = (array) $value; - - if ($oldValue === null) - { - // No records available, doing normal insert - $needsInsert = true; - } - elseif (count($value) == 1 && count((array) $oldValue) == 1) - { - // Only a single row value update can be done when not empty - $needsUpdate = is_array($value[0]) ? count($value[0]) : strlen($value[0]); - $needsDelete = !$needsUpdate; - } - else - { - // Multiple values, we need to purge the data and do a new - // insert - $needsDelete = true; - $needsInsert = true; - } - - if ($needsDelete) - { - $fieldId = (int) $fieldId; - - // Deleting the existing record as it is a reset - $query = $this->getDatabase()->getQuery(true); - - $query->delete($query->quoteName('#__fields_values')) - ->where($query->quoteName('field_id') . ' = :fieldid') - ->where($query->quoteName('item_id') . ' = :itemid') - ->bind(':fieldid', $fieldId, ParameterType::INTEGER) - ->bind(':itemid', $itemId); - - $this->getDatabase()->setQuery($query)->execute(); - } - - if ($needsInsert) - { - $newObj = new \stdClass; - - $newObj->field_id = (int) $fieldId; - $newObj->item_id = $itemId; - - foreach ($value as $v) - { - $newObj->value = $v; - - $this->getDatabase()->insertObject('#__fields_values', $newObj); - } - } - - if ($needsUpdate) - { - $updateObj = new \stdClass; - - $updateObj->field_id = (int) $fieldId; - $updateObj->item_id = $itemId; - $updateObj->value = reset($value); - - $this->getDatabase()->updateObject('#__fields_values', $updateObj, array('field_id', 'item_id')); - } - - $this->valueCache = array(); - FieldsHelper::clearFieldsCache(); - - return true; - } - - /** - * Returning the value for the given field id, context and item id. - * - * @param string $fieldId The field ID. - * @param string $itemId The ID of the item. - * - * @return NULL|string - * - * @since 3.7.0 - */ - public function getFieldValue($fieldId, $itemId) - { - $values = $this->getFieldValues(array($fieldId), $itemId); - - if (array_key_exists($fieldId, $values)) - { - return $values[$fieldId]; - } - - return null; - } - - /** - * Returning the values for the given field ids, context and item id. - * - * @param array $fieldIds The field Ids. - * @param string $itemId The ID of the item. - * - * @return NULL|array - * - * @since 3.7.0 - */ - public function getFieldValues(array $fieldIds, $itemId) - { - if (!$fieldIds) - { - return array(); - } - - // Create a unique key for the cache - $key = md5(serialize($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); - - $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') - ->bind(':itemid', $itemId); - - // Fetch the row from the database - $rows = $this->getDatabase()->setQuery($query)->loadObjectList(); - - $data = array(); - - // Fill the data container from the database rows - foreach ($rows as $row) - { - // If there are multiple values for a field, create an array - if (array_key_exists($row->field_id, $data)) - { - // Transform it to an array - if (!is_array($data[$row->field_id])) - { - $data[$row->field_id] = array($data[$row->field_id]); - } - - // Set the value in the array - $data[$row->field_id][] = $row->value; - - // Go to the next row, otherwise the value gets overwritten in the data container - continue; - } - - // Set the value - $data[$row->field_id] = $row->value; - } - - // Assign it to the internal cache - $this->valueCache[$key] = $data; - } - - // Return the value from the cache - return $this->valueCache[$key]; - } - - /** - * Cleaning up the values for the given item on the context. - * - * @param string $context The context. - * @param string $itemId The Item ID. - * - * @return void - * - * @since 3.7.0 - */ - 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'); - - $query = $this->getDatabase()->getQuery(true); - - $query->delete($query->quoteName('#__fields_values')) - ->where($query->quoteName('field_id') . ' IN (' . $fieldsQuery . ')') - ->where($query->quoteName('item_id') . ' = :itemid') - ->bind(':itemid', $itemId) - ->bind(':context', $context); - - $this->getDatabase()->setQuery($query)->execute(); - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission for the component. - * - * @since 3.7.0 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->state != -2) - { - return false; - } - - $parts = FieldsHelper::extract($record->context); - - return Factory::getUser()->authorise('core.delete', $parts[0] . '.field.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission for the - * component. - * - * @since 3.7.0 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - $parts = FieldsHelper::extract($record->context); - - // Check for existing field. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $parts[0] . '.field.' . (int) $record->id); - } - - return $user->authorise('core.edit.state', $parts[0]); - } - - /** - * Stock method to auto-populate the model state. - * - * @return void - * - * @since 3.7.0 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState($this->getName() . '.id', $pk); - - $context = $app->input->get('context', 'com_content.article'); - $this->setState('field.context', $context); - $parts = FieldsHelper::extract($context); - - // Extract the component name - $this->setState('field.component', $parts[0]); - - // Extract the optional section name - $this->setState('field.section', (count($parts) > 1) ? $parts[1] : null); - - // Load the parameters. - $params = ComponentHelper::getParams('com_fields'); - $this->setState('params', $params); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param Table $table A Table object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 3.7.0 - */ - protected function getReorderConditions($table) - { - $db = $this->getDatabase(); - - return [ - $db->quoteName('context') . ' = ' . $db->quote($table->context), - ]; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 3.7.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_fields.edit.field.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Pre-select some filters (Status, Language, Access) in edit form - // if those have been selected in Category Manager - if (!$data->id) - { - // Check for which context the Category Manager is used and - // get selected fields - $filters = (array) $app->getUserState('com_fields.fields.filter'); - - $data->set('state', $app->input->getInt('state', ((isset($filters['state']) && $filters['state'] !== '') ? $filters['state'] : null))); - $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); - $data->set('group_id', $app->input->getString('group_id', (!empty($filters['group_id']) ? $filters['group_id'] : null))); - $data->set( - 'access', - $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) - ); - - // Set the type if available from the request - $data->set('type', $app->input->getWord('type', $this->state->get('field.type', $data->get('type')))); - } - - if ($data->label && !isset($data->params['label'])) - { - $data->params['label'] = $data->label; - } - } - - $this->preprocessData('com_fields.field', $data); - - return $data; - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see JFormRule - * @see JFilterInput - * @since 3.9.23 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', 'com_fields')) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to allow derived classes to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 3.7.0 - * - * @throws \Exception if there is an error in the form event. - * - * @see \Joomla\CMS\Form\FormField - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $component = $this->state->get('field.component'); - $section = $this->state->get('field.section'); - $dataObject = $data; - - if (is_array($dataObject)) - { - $dataObject = (object) $dataObject; - } - - if (isset($dataObject->type)) - { - $form->setFieldAttribute('type', 'component', $component); - - // Not allowed to change the type of an existing record - if ($dataObject->id) - { - $form->setFieldAttribute('type', 'readonly', 'true'); - } - - // Allow to override the default value label and description through the plugin - $key = 'PLG_FIELDS_' . strtoupper($dataObject->type) . '_DEFAULT_VALUE_LABEL'; - - if (Factory::getLanguage()->hasKey($key)) - { - $form->setFieldAttribute('default_value', 'label', $key); - } - - $key = 'PLG_FIELDS_' . strtoupper($dataObject->type) . '_DEFAULT_VALUE_DESC'; - - if (Factory::getLanguage()->hasKey($key)) - { - $form->setFieldAttribute('default_value', 'description', $key); - } - - // Remove placeholder field on list fields - if ($dataObject->type == 'list') - { - $form->removeField('hint', 'params'); - } - } - - // Get the categories for this component (and optionally this section, if available) - $cat = ( - function () use ($component, $section) { - // Get the CategoryService for this component - $componentObject = $this->bootComponent($component); - - if (!$componentObject instanceof CategoryServiceInterface) - { - // No CategoryService -> no categories - return null; - } - - $cat = null; - - // Try to get the categories for this component and section - try - { - $cat = $componentObject->getCategory([], $section ?: ''); - } - catch (SectionNotFoundException $e) - { - // Not found for component and section -> Now try once more without the section, so only component - try - { - $cat = $componentObject->getCategory(); - } - catch (SectionNotFoundException $e) - { - // If we haven't found it now, return (no categories available for this component) - return null; - } - } - - // So we found categories for at least the component, return them - return $cat; - } - )(); - - // If we found categories, and if the root category has children, set them in the form - if ($cat && $cat->get('root')->hasChildren()) - { - $form->setFieldAttribute('assigned_cat_ids', 'extension', $cat->getExtension()); - } - else - { - // Else remove the field from the form - $form->removeField('assigned_cat_ids'); - } - - $form->setFieldAttribute('type', 'component', $component); - $form->setFieldAttribute('group_id', 'context', $this->state->get('field.context')); - $form->setFieldAttribute('rules', 'component', $component); - - // Looking in the component forms folder for a specific section forms file - $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/forms/fields/' . $section . '.xml'); - - if (!file_exists($path)) - { - // Looking in the component models/forms folder for a specific section forms file - $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/fields/' . $section . '.xml'); - } - - if (file_exists($path)) - { - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE); - $lang->load($component, JPATH_BASE . '/components/' . $component); - - if (!$form->loadFile($path, false)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Clean the cache - * - * @param string $group The cache group - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 3.7.0 - */ - protected function cleanCache($group = null, $clientId = 0) - { - $context = Factory::getApplication()->input->get('context'); - - switch ($context) - { - case 'com_content': - parent::cleanCache('com_content'); - parent::cleanCache('mod_articles_archive'); - parent::cleanCache('mod_articles_categories'); - parent::cleanCache('mod_articles_category'); - parent::cleanCache('mod_articles_latest'); - parent::cleanCache('mod_articles_news'); - parent::cleanCache('mod_articles_popular'); - break; - default: - parent::cleanCache($context); - break; - } - } - - /** - * Batch copy fields to a new group. - * - * @param integer $value The new value matching a fields group. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return array|boolean new IDs if successful, false otherwise and internal error is set. - * - * @since 3.7.0 - */ - protected function batchCopy($value, $pks, $contexts) - { - // Set the variables - $user = Factory::getUser(); - $table = $this->getTable(); - $newIds = array(); - $component = $this->state->get('filter.component'); - $value = (int) $value; - - foreach ($pks as $pk) - { - if ($user->authorise('core.create', $component . '.fieldgroup.' . $value)) - { - $table->reset(); - $table->load($pk); - - $table->group_id = $value; - - // Reset the ID because we are making a copy - $table->id = 0; - - // Unpublish the new field - $table->state = 0; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Get the new item ID - $newId = $table->get('id'); - - // Add the new ID to the array - $newIds[$pk] = $newId; - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_CREATE')); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return $newIds; - } - - /** - * Batch move fields to a new group. - * - * @param integer $value The new value matching a fields group. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 3.7.0 - */ - protected function batchMove($value, $pks, $contexts) - { - // Set the variables - $user = Factory::getUser(); - $table = $this->getTable(); - $context = explode('.', Factory::getApplication()->getUserState('com_fields.fields.context')); - $value = (int) $value; - - foreach ($pks as $pk) - { - if ($user->authorise('core.edit', $context[0] . '.fieldgroup.' . $value)) - { - $table->reset(); - $table->load($pk); - - $table->group_id = $value; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } + /** + * @var null|string + * + * @since 3.7.0 + */ + public $typeAlias = null; + + /** + * @var string + * + * @since 3.7.0 + */ + protected $text_prefix = 'COM_FIELDS'; + + /** + * Batch copy/move command. If set to false, + * the batch copy/move command is not supported + * + * @var string + * @since 3.4 + */ + protected $batch_copymove = 'group_id'; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage' + ); + + /** + * @var array + * + * @since 3.7.0 + */ + private $valueCache = array(); + + /** + * Constructor + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * + * @since 3.7.0 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + parent::__construct($config, $factory); + + $this->typeAlias = Factory::getApplication()->input->getCmd('context', 'com_content.article') . '.field'; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success, False on error. + * + * @since 3.7.0 + */ + public function save($data) + { + $field = null; + + if (isset($data['id']) && $data['id']) { + $field = $this->getItem($data['id']); + } + + if (!isset($data['label']) && isset($data['params']['label'])) { + $data['label'] = $data['params']['label']; + + unset($data['params']['label']); + } + + // Alter the title for save as copy + $input = Factory::getApplication()->input; + + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['title'] == $origTable->title) { + list($title, $name) = $this->generateNewTitle($data['group_id'], $data['name'], $data['title']); + $data['title'] = $title; + $data['label'] = $title; + $data['name'] = $name; + } else { + if ($data['name'] == $origTable->name) { + $data['name'] = ''; + } + } + + $data['state'] = 0; + } + + // Load the fields plugins, perhaps they want to do something + PluginHelper::importPlugin('fields'); + + $message = $this->checkDefaultValue($data); + + if ($message !== true) { + $this->setError($message); + + return false; + } + + if (!parent::save($data)) { + return false; + } + + // Save the assigned categories into #__fields_categories + $db = $this->getDatabase(); + $id = (int) $this->getState('field.id'); + + /** + * If the field is only used in subform, set Category to None automatically so that it will only be displayed + * as part of SubForm on add/edit item screen + */ + if (!empty($data['only_use_in_subform'])) { + $cats = [-1]; + } else { + $cats = isset($data['assigned_cat_ids']) ? (array) $data['assigned_cat_ids'] : array(); + $cats = ArrayHelper::toInteger($cats); + } + + $assignedCatIds = array(); + + foreach ($cats as $cat) { + // If we have found the 'JNONE' category, remove all other from the result and break. + if ($cat == '-1') { + $assignedCatIds = array('-1'); + break; + } + + if ($cat) { + $assignedCatIds[] = $cat; + } + } + + // First delete all assigned categories + $query = $db->getQuery(true); + $query->delete('#__fields_categories') + ->where($db->quoteName('field_id') . ' = :fieldid') + ->bind(':fieldid', $id, ParameterType::INTEGER); + + $db->setQuery($query); + $db->execute(); + + // Inset new assigned categories + $tupel = new \stdClass(); + $tupel->field_id = $id; + + foreach ($assignedCatIds as $catId) { + $tupel->category_id = $catId; + $db->insertObject('#__fields_categories', $tupel); + } + + /** + * If the options have changed, delete the values. This should only apply for list, checkboxes and radio + * custom field types, because when their options are being changed, their values might get invalid, because + * e.g. there is a value selected from a list, which is not part of the list anymore. Hence we need to delete + * all values that are not part of the options anymore. Note: The only field types with fieldparams+options + * are those above listed plus the subfields type. And we do explicitly not want the values to be deleted + * when the options of a subfields field are getting changed. + */ + if ( + $field && in_array($field->type, array('list', 'checkboxes', 'radio'), true) + && isset($data['fieldparams']['options']) && isset($field->fieldparams['options']) + ) { + $oldParams = $this->getParams($field->fieldparams['options']); + $newParams = $this->getParams($data['fieldparams']['options']); + + if (is_object($oldParams) && is_object($newParams) && $oldParams != $newParams) { + // Get new values. + $names = array_column((array) $newParams, 'value'); + + $fieldId = (int) $field->id; + $query = $db->getQuery(true); + $query->delete($db->quoteName('#__fields_values')) + ->where($db->quoteName('field_id') . ' = :fieldid') + ->bind(':fieldid', $fieldId, ParameterType::INTEGER); + + // If new values are set, delete only old values. Otherwise delete all values. + if ($names) { + $query->whereNotIn($db->quoteName('value'), $names, ParameterType::STRING); + } + + $db->setQuery($query); + $db->execute(); + } + } + + FieldsHelper::clearFieldsCache(); + + return true; + } + + + /** + * Checks if the default value is valid for the given data. If a string is returned then + * it can be assumed that the default value is invalid. + * + * @param array $data The data. + * + * @return true|string true if valid, a string containing the exception message when not. + * + * @since 3.7.0 + */ + private function checkDefaultValue($data) + { + // Empty default values are correct + if (empty($data['default_value']) && $data['default_value'] !== '0') { + return true; + } + + $types = FieldsHelper::getFieldTypes(); + + // Check if type exists + if (!array_key_exists($data['type'], $types)) { + return true; + } + + $path = $types[$data['type']]['rules']; + + // Add the path for the rules of the plugin when available + if ($path) { + // Add the lookup path for the rule + FormHelper::addRulePath($path); + } + + // Create the fields object + $obj = (object) $data; + $obj->params = new Registry($obj->params); + $obj->fieldparams = new Registry(!empty($obj->fieldparams) ? $obj->fieldparams : array()); + + // Prepare the dom + $dom = new \DOMDocument(); + $node = $dom->appendChild(new \DOMElement('form')); + + // Trigger the event to create the field dom node + $form = new Form($data['context']); + $form->setDatabase($this->getDatabase()); + Factory::getApplication()->triggerEvent('onCustomFieldsPrepareDom', array($obj, $node, $form)); + + // Check if a node is created + if (!$node->firstChild) { + return true; + } + + // Define the type either from the field or from the data + $type = $node->firstChild->getAttribute('validate') ? : $data['type']; + + // Load the rule + $rule = FormHelper::loadRuleType($type); + + // When no rule exists, we allow the default value + if (!$rule) { + return true; + } + + if ($rule instanceof DatabaseAwareInterface) { + try { + $rule->setDatabase($this->getDatabase()); + } catch (DatabaseNotFoundException $e) { + @trigger_error(sprintf('Database must be set, this will not be caught anymore in 5.0.'), E_USER_DEPRECATED); + $rule->setDatabase(Factory::getContainer()->get(DatabaseInterface::class)); + } + } + + try { + // Perform the check + $result = $rule->test(simplexml_import_dom($node->firstChild), $data['default_value']); + + // Check if the test succeeded + return $result === true ? : Text::_('COM_FIELDS_FIELD_INVALID_DEFAULT_VALUE'); + } catch (\UnexpectedValueException $e) { + return $e->getMessage(); + } + } + + /** + * Converts the unknown params into an object. + * + * @param mixed $params The params. + * + * @return \stdClass Object on success, false on failure. + * + * @since 3.7.0 + */ + private function getParams($params) + { + if (is_string($params)) { + $params = json_decode($params); + } + + if (is_array($params)) { + $params = (object) $params; + } + + return $params; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 3.7.0 + */ + public function getItem($pk = null) + { + $result = parent::getItem($pk); + + if ($result) { + // Prime required properties. + if (empty($result->id)) { + $result->context = Factory::getApplication()->input->getCmd('context', $this->getState('field.context')); + } + + if (property_exists($result, 'fieldparams') && $result->fieldparams !== null) { + $registry = new Registry(); + + if ($result->fieldparams) { + $registry->loadString($result->fieldparams); + } + + $result->fieldparams = $registry->toArray(); + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $fieldId = (int) $result->id; + $query->select($db->quoteName('category_id')) + ->from($db->quoteName('#__fields_categories')) + ->where($db->quoteName('field_id') . ' = :fieldid') + ->bind(':fieldid', $fieldId, ParameterType::INTEGER); + + $db->setQuery($query); + $result->assigned_cat_ids = $db->loadColumn() ?: array(0); + } + + return $result; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.7.0 + * @throws \Exception + */ + public function getTable($name = 'Field', $prefix = 'Administrator', $options = array()) + { + // Default to text type + $table = parent::getTable($name, $prefix, $options); + $table->type = 'text'; + + return $table; + } + + /** + * Method to change the title & name. + * + * @param integer $categoryId The id of the category. + * @param string $name The name. + * @param string $title The title. + * + * @return array Contains the modified title and name. + * + * @since 3.7.0 + */ + protected function generateNewTitle($categoryId, $name, $title) + { + // Alter the title & name + $table = $this->getTable(); + + while ($table->load(array('name' => $name))) { + $title = StringHelper::increment($title); + $name = StringHelper::increment($name, 'dash'); + } + + return array( + $title, + $name, + ); + } + + /** + * Method to delete one or more records. + * + * @param array $pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 3.7.0 + */ + public function delete(&$pks) + { + $success = parent::delete($pks); + + if ($success) { + $pks = (array) $pks; + $pks = ArrayHelper::toInteger($pks); + $pks = array_filter($pks); + + if (!empty($pks)) { + // Delete Values + $query = $this->getDatabase()->getQuery(true); + + $query->delete($query->quoteName('#__fields_values')) + ->whereIn($query->quoteName('field_id'), $pks); + + $this->getDatabase()->setQuery($query)->execute(); + + // Delete Assigned Categories + $query = $this->getDatabase()->getQuery(true); + + $query->delete($query->quoteName('#__fields_categories')) + ->whereIn($query->quoteName('field_id'), $pks); + + $this->getDatabase()->setQuery($query)->execute(); + } + } + + return $success; + } + + /** + * Abstract method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 3.7.0 + */ + public function getForm($data = array(), $loadData = true) + { + $context = $this->getState('field.context'); + $jinput = Factory::getApplication()->input; + + // A workaround to get the context into the model for save requests. + if (empty($context) && isset($data['context'])) { + $context = $data['context']; + $parts = FieldsHelper::extract($context); + + $this->setState('field.context', $context); + + if ($parts) { + $this->setState('field.component', $parts[0]); + $this->setState('field.section', $parts[1]); + } + } + + if (isset($data['type'])) { + // This is needed that the plugins can determine the type + $this->setState('field.type', $data['type']); + } + + // Load the fields plugin that they can add additional parameters to the form + PluginHelper::importPlugin('fields'); + + // Get the form. + $form = $this->loadForm( + 'com_fields.field.' . $context, + 'field', + array( + 'control' => 'jform', + 'load_data' => true, + ) + ); + + if (empty($form)) { + return false; + } + + // Modify the form based on Edit State access controls. + if (empty($data['context'])) { + $data['context'] = $context; + } + + $fieldId = $jinput->get('id'); + $assetKey = $this->state->get('field.component') . '.field.' . $fieldId; + + if (!Factory::getUser()->authorise('core.edit.state', $assetKey)) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('state', 'disabled', 'true'); + + // Disable fields while saving. The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('state', 'filter', 'unset'); + } + + // Don't allow to change the created_user_id user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_user_id', 'filter', 'unset'); + } + + // In case we are editing a field, field type cannot be changed, so some extra handling below is needed + if ($fieldId) { + $fieldType = $form->getField('type'); + + if ($fieldType->value == 'subform') { + // Only Use In subform should not be available for subform field type, so we remove it + $form->removeField('only_use_in_subform'); + } else { + // Field type could not be changed, so remove showon attribute to avoid js errors + $form->setFieldAttribute('only_use_in_subform', 'showon', ''); + } + } + + return $form; + } + + /** + * Setting the value for the given field id, context and item id. + * + * @param string $fieldId The field ID. + * @param string $itemId The ID of the item. + * @param string $value The value. + * + * @return boolean + * + * @since 3.7.0 + */ + public function setFieldValue($fieldId, $itemId, $value) + { + $field = $this->getItem($fieldId); + $params = $field->params; + + if (is_array($params)) { + $params = new Registry($params); + } + + // Don't save the value when the user is not authorized to change it + if (!$field || !FieldsHelper::canEditFieldValue($field)) { + return false; + } + + $needsDelete = false; + $needsInsert = false; + $needsUpdate = false; + + $oldValue = $this->getFieldValue($fieldId, $itemId); + $value = (array) $value; + + if ($oldValue === null) { + // No records available, doing normal insert + $needsInsert = true; + } elseif (count($value) == 1 && count((array) $oldValue) == 1) { + // Only a single row value update can be done when not empty + $needsUpdate = is_array($value[0]) ? count($value[0]) : strlen($value[0]); + $needsDelete = !$needsUpdate; + } else { + // Multiple values, we need to purge the data and do a new + // insert + $needsDelete = true; + $needsInsert = true; + } + + if ($needsDelete) { + $fieldId = (int) $fieldId; + + // Deleting the existing record as it is a reset + $query = $this->getDatabase()->getQuery(true); + + $query->delete($query->quoteName('#__fields_values')) + ->where($query->quoteName('field_id') . ' = :fieldid') + ->where($query->quoteName('item_id') . ' = :itemid') + ->bind(':fieldid', $fieldId, ParameterType::INTEGER) + ->bind(':itemid', $itemId); + + $this->getDatabase()->setQuery($query)->execute(); + } + + if ($needsInsert) { + $newObj = new \stdClass(); + + $newObj->field_id = (int) $fieldId; + $newObj->item_id = $itemId; + + foreach ($value as $v) { + $newObj->value = $v; + + $this->getDatabase()->insertObject('#__fields_values', $newObj); + } + } + + if ($needsUpdate) { + $updateObj = new \stdClass(); + + $updateObj->field_id = (int) $fieldId; + $updateObj->item_id = $itemId; + $updateObj->value = reset($value); + + $this->getDatabase()->updateObject('#__fields_values', $updateObj, array('field_id', 'item_id')); + } + + $this->valueCache = array(); + FieldsHelper::clearFieldsCache(); + + return true; + } + + /** + * Returning the value for the given field id, context and item id. + * + * @param string $fieldId The field ID. + * @param string $itemId The ID of the item. + * + * @return NULL|string + * + * @since 3.7.0 + */ + public function getFieldValue($fieldId, $itemId) + { + $values = $this->getFieldValues(array($fieldId), $itemId); + + if (array_key_exists($fieldId, $values)) { + return $values[$fieldId]; + } + + return null; + } + + /** + * Returning the values for the given field ids, context and item id. + * + * @param array $fieldIds The field Ids. + * @param string $itemId The ID of the item. + * + * @return NULL|array + * + * @since 3.7.0 + */ + public function getFieldValues(array $fieldIds, $itemId) + { + if (!$fieldIds) { + return array(); + } + + // Create a unique key for the cache + $key = md5(serialize($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); + + $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') + ->bind(':itemid', $itemId); + + // Fetch the row from the database + $rows = $this->getDatabase()->setQuery($query)->loadObjectList(); + + $data = array(); + + // Fill the data container from the database rows + foreach ($rows as $row) { + // If there are multiple values for a field, create an array + if (array_key_exists($row->field_id, $data)) { + // Transform it to an array + if (!is_array($data[$row->field_id])) { + $data[$row->field_id] = array($data[$row->field_id]); + } + + // Set the value in the array + $data[$row->field_id][] = $row->value; + + // Go to the next row, otherwise the value gets overwritten in the data container + continue; + } + + // Set the value + $data[$row->field_id] = $row->value; + } + + // Assign it to the internal cache + $this->valueCache[$key] = $data; + } + + // Return the value from the cache + return $this->valueCache[$key]; + } + + /** + * Cleaning up the values for the given item on the context. + * + * @param string $context The context. + * @param string $itemId The Item ID. + * + * @return void + * + * @since 3.7.0 + */ + 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'); + + $query = $this->getDatabase()->getQuery(true); + + $query->delete($query->quoteName('#__fields_values')) + ->where($query->quoteName('field_id') . ' IN (' . $fieldsQuery . ')') + ->where($query->quoteName('item_id') . ' = :itemid') + ->bind(':itemid', $itemId) + ->bind(':context', $context); + + $this->getDatabase()->setQuery($query)->execute(); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 3.7.0 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->state != -2) { + return false; + } + + $parts = FieldsHelper::extract($record->context); + + return Factory::getUser()->authorise('core.delete', $parts[0] . '.field.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission for the + * component. + * + * @since 3.7.0 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + $parts = FieldsHelper::extract($record->context); + + // Check for existing field. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $parts[0] . '.field.' . (int) $record->id); + } + + return $user->authorise('core.edit.state', $parts[0]); + } + + /** + * Stock method to auto-populate the model state. + * + * @return void + * + * @since 3.7.0 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState($this->getName() . '.id', $pk); + + $context = $app->input->get('context', 'com_content.article'); + $this->setState('field.context', $context); + $parts = FieldsHelper::extract($context); + + // Extract the component name + $this->setState('field.component', $parts[0]); + + // Extract the optional section name + $this->setState('field.section', (count($parts) > 1) ? $parts[1] : null); + + // Load the parameters. + $params = ComponentHelper::getParams('com_fields'); + $this->setState('params', $params); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param Table $table A Table object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 3.7.0 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('context') . ' = ' . $db->quote($table->context), + ]; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 3.7.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_fields.edit.field.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Pre-select some filters (Status, Language, Access) in edit form + // if those have been selected in Category Manager + if (!$data->id) { + // Check for which context the Category Manager is used and + // get selected fields + $filters = (array) $app->getUserState('com_fields.fields.filter'); + + $data->set('state', $app->input->getInt('state', ((isset($filters['state']) && $filters['state'] !== '') ? $filters['state'] : null))); + $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); + $data->set('group_id', $app->input->getString('group_id', (!empty($filters['group_id']) ? $filters['group_id'] : null))); + $data->set( + 'access', + $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) + ); + + // Set the type if available from the request + $data->set('type', $app->input->getWord('type', $this->state->get('field.type', $data->get('type')))); + } + + if ($data->label && !isset($data->params['label'])) { + $data->params['label'] = $data->label; + } + } + + $this->preprocessData('com_fields.field', $data); + + return $data; + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see JFormRule + * @see JFilterInput + * @since 3.9.23 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', 'com_fields')) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to allow derived classes to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 3.7.0 + * + * @throws \Exception if there is an error in the form event. + * + * @see \Joomla\CMS\Form\FormField + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $component = $this->state->get('field.component'); + $section = $this->state->get('field.section'); + $dataObject = $data; + + if (is_array($dataObject)) { + $dataObject = (object) $dataObject; + } + + if (isset($dataObject->type)) { + $form->setFieldAttribute('type', 'component', $component); + + // Not allowed to change the type of an existing record + if ($dataObject->id) { + $form->setFieldAttribute('type', 'readonly', 'true'); + } + + // Allow to override the default value label and description through the plugin + $key = 'PLG_FIELDS_' . strtoupper($dataObject->type) . '_DEFAULT_VALUE_LABEL'; + + if (Factory::getLanguage()->hasKey($key)) { + $form->setFieldAttribute('default_value', 'label', $key); + } + + $key = 'PLG_FIELDS_' . strtoupper($dataObject->type) . '_DEFAULT_VALUE_DESC'; + + if (Factory::getLanguage()->hasKey($key)) { + $form->setFieldAttribute('default_value', 'description', $key); + } + + // Remove placeholder field on list fields + if ($dataObject->type == 'list') { + $form->removeField('hint', 'params'); + } + } + + // Get the categories for this component (and optionally this section, if available) + $cat = ( + function () use ($component, $section) { + // Get the CategoryService for this component + $componentObject = $this->bootComponent($component); + + if (!$componentObject instanceof CategoryServiceInterface) { + // No CategoryService -> no categories + return null; + } + + $cat = null; + + // Try to get the categories for this component and section + try { + $cat = $componentObject->getCategory([], $section ?: ''); + } catch (SectionNotFoundException $e) { + // Not found for component and section -> Now try once more without the section, so only component + try { + $cat = $componentObject->getCategory(); + } catch (SectionNotFoundException $e) { + // If we haven't found it now, return (no categories available for this component) + return null; + } + } + + // So we found categories for at least the component, return them + return $cat; + } + )(); + + // If we found categories, and if the root category has children, set them in the form + if ($cat && $cat->get('root')->hasChildren()) { + $form->setFieldAttribute('assigned_cat_ids', 'extension', $cat->getExtension()); + } else { + // Else remove the field from the form + $form->removeField('assigned_cat_ids'); + } + + $form->setFieldAttribute('type', 'component', $component); + $form->setFieldAttribute('group_id', 'context', $this->state->get('field.context')); + $form->setFieldAttribute('rules', 'component', $component); + + // Looking in the component forms folder for a specific section forms file + $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/forms/fields/' . $section . '.xml'); + + if (!file_exists($path)) { + // Looking in the component models/forms folder for a specific section forms file + $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/fields/' . $section . '.xml'); + } + + if (file_exists($path)) { + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE); + $lang->load($component, JPATH_BASE . '/components/' . $component); + + if (!$form->loadFile($path, false)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Clean the cache + * + * @param string $group The cache group + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 3.7.0 + */ + protected function cleanCache($group = null, $clientId = 0) + { + $context = Factory::getApplication()->input->get('context'); + + switch ($context) { + case 'com_content': + parent::cleanCache('com_content'); + parent::cleanCache('mod_articles_archive'); + parent::cleanCache('mod_articles_categories'); + parent::cleanCache('mod_articles_category'); + parent::cleanCache('mod_articles_latest'); + parent::cleanCache('mod_articles_news'); + parent::cleanCache('mod_articles_popular'); + break; + default: + parent::cleanCache($context); + break; + } + } + + /** + * Batch copy fields to a new group. + * + * @param integer $value The new value matching a fields group. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return array|boolean new IDs if successful, false otherwise and internal error is set. + * + * @since 3.7.0 + */ + protected function batchCopy($value, $pks, $contexts) + { + // Set the variables + $user = Factory::getUser(); + $table = $this->getTable(); + $newIds = array(); + $component = $this->state->get('filter.component'); + $value = (int) $value; + + foreach ($pks as $pk) { + if ($user->authorise('core.create', $component . '.fieldgroup.' . $value)) { + $table->reset(); + $table->load($pk); + + $table->group_id = $value; + + // Reset the ID because we are making a copy + $table->id = 0; + + // Unpublish the new field + $table->state = 0; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Get the new item ID + $newId = $table->get('id'); + + // Add the new ID to the array + $newIds[$pk] = $newId; + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_CREATE')); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return $newIds; + } + + /** + * Batch move fields to a new group. + * + * @param integer $value The new value matching a fields group. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 3.7.0 + */ + protected function batchMove($value, $pks, $contexts) + { + // Set the variables + $user = Factory::getUser(); + $table = $this->getTable(); + $context = explode('.', Factory::getApplication()->getUserState('com_fields.fields.context')); + $value = (int) $value; + + foreach ($pks as $pk) { + if ($user->authorise('core.edit', $context[0] . '.fieldgroup.' . $value)) { + $table->reset(); + $table->load($pk); + + $table->group_id = $value; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } } diff --git a/administrator/components/com_fields/src/Model/FieldsModel.php b/administrator/components/com_fields/src/Model/FieldsModel.php index 9bcb237868609..727b0c9976166 100644 --- a/administrator/components/com_fields/src/Model/FieldsModel.php +++ b/administrator/components/com_fields/src/Model/FieldsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.context', 'context', 'com_content.article', 'CMD'); - $this->setState('filter.context', $context); - - // Split context into component and optional section - $parts = FieldsHelper::extract($context); - - if ($parts) - { - $this->setState('filter.component', $parts[0]); - $this->setState('filter.section', $parts[1]); - } - } - - /** - * Method to get a store id based on the model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id An identifier string to generate the store id. - * - * @return string A store id. - * - * @since 3.7.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.context'); - $id .= ':' . serialize($this->getState('filter.assigned_cat_ids')); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.group_id'); - $id .= ':' . serialize($this->getState('filter.language')); - $id .= ':' . $this->getState('filter.only_use_in_subform'); - - return parent::getStoreId($id); - } - - /** - * Method to get a DatabaseQuery object for retrieving the data set from a database. - * - * @return \Joomla\Database\DatabaseQuery A DatabaseQuery object to retrieve the data set. - * - * @since 3.7.0 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - $app = Factory::getApplication(); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'DISTINCT a.id, a.title, a.name, a.checked_out, a.checked_out_time, a.note' . - ', a.state, a.access, a.created_time, a.created_user_id, a.ordering, a.language' . - ', a.fieldparams, a.params, a.type, a.default_value, a.context, a.group_id' . - ', a.label, a.description, a.required, a.only_use_in_subform' - ) - ); - $query->from('#__fields AS a'); - - // Join over the language - $query->select('l.title AS language_title, l.image AS language_image') - ->join('LEFT', $db->quoteName('#__languages') . ' AS l ON l.lang_code = a.language'); - - // Join over the users for the checked out user. - $query->select('uc.name AS editor')->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); - - // Join over the asset groups. - $query->select('ag.title AS access_level')->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); - - // Join over the users for the author. - $query->select('ua.name AS author_name')->join('LEFT', '#__users AS ua ON ua.id = a.created_user_id'); - - // Join over the field groups. - $query->select('g.title AS group_title, g.access as group_access, g.state AS group_state, g.note as group_note'); - $query->join('LEFT', '#__fields_groups AS g ON g.id = a.group_id'); - - // Filter by context - if ($context = $this->getState('filter.context')) - { - $query->where($db->quoteName('a.context') . ' = :context') - ->bind(':context', $context); - } - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - if (is_array($access)) - { - $access = ArrayHelper::toInteger($access); - $query->whereIn($db->quoteName('a.access'), $access); - } - else - { - $access = (int) $access; - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - } - - if (($categories = $this->getState('filter.assigned_cat_ids')) && $context) - { - $categories = (array) $categories; - $categories = ArrayHelper::toInteger($categories); - $parts = FieldsHelper::extract($context); - - if ($parts) - { - // Get the categories for this component (and optionally this section, if available) - $cat = ( - function () use ($parts) { - // Get the CategoryService for this component - $componentObject = $this->bootComponent($parts[0]); - - if (!$componentObject instanceof CategoryServiceInterface) - { - // No CategoryService -> no categories - return null; - } - - $cat = null; - - // Try to get the categories for this component and section - try - { - $cat = $componentObject->getCategory([], $parts[1] ?: ''); - } - catch (SectionNotFoundException $e) - { - // Not found for component and section -> Now try once more without the section, so only component - try - { - $cat = $componentObject->getCategory(); - } - catch (SectionNotFoundException $e) - { - // If we haven't found it now, return (no categories available for this component) - return null; - } - } - - // So we found categories for at least the component, return them - return $cat; - } - )(); - - if ($cat) - { - foreach ($categories as $assignedCatIds) - { - // Check if we have the actual category - $parent = $cat->get($assignedCatIds); - - if ($parent) - { - $categories[] = (int) $parent->id; - - // Traverse the tree up to get all the fields which are attached to a parent - while ($parent->getParent() && $parent->getParent()->id != 'root') - { - $parent = $parent->getParent(); - $categories[] = (int) $parent->id; - } - } - } - } - } - - $categories = array_unique($categories); - - // Join over the assigned categories - $query->join('LEFT', $db->quoteName('#__fields_categories') . ' AS fc ON fc.field_id = a.id'); - - if (in_array('0', $categories)) - { - $query->where( - '(' . - $db->quoteName('fc.category_id') . ' IS NULL OR ' . - $db->quoteName('fc.category_id') . ' IN (' . implode(',', $query->bindArray(array_values($categories), ParameterType::INTEGER)) . ')' . - ')' - ); - } - else - { - $query->whereIn($db->quoteName('fc.category_id'), $categories); - } - } - - // Implement View Level Access - if (!$app->isClient('administrator') || !$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.group_id') . ' = 0', - $db->quoteName('g.access') . ' IN (' . implode(',', $query->bindArray($groups, ParameterType::INTEGER)) . ')' - ], - 'OR' - ); - } - - // Filter by state - $state = $this->getState('filter.state'); - - // Include group state only when not on on back end list - $includeGroupState = !$app->isClient('administrator') || - $app->input->get('option') != 'com_fields' || - $app->input->get('view') != 'fields'; - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - - if ($includeGroupState) - { - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.group_id') . ' = 0', - $db->quoteName('g.state') . ' = :gstate', - ], - 'OR' - ) - ->bind(':gstate', $state, ParameterType::INTEGER); - } - } - elseif (!$state) - { - $query->whereIn($db->quoteName('a.state'), [0, 1]); - - if ($includeGroupState) - { - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.group_id') . ' = 0', - $db->quoteName('g.state') . ' IN (' . implode(',', $query->bindArray([0, 1], ParameterType::INTEGER)) . ')' - ], - 'OR' - ); - } - } - - $groupId = $this->getState('filter.group_id'); - - if (is_numeric($groupId)) - { - $groupId = (int) $groupId; - $query->where($db->quoteName('a.group_id') . ' = :groupid') - ->bind(':groupid', $groupId, ParameterType::INTEGER); - } - - $onlyUseInSubForm = $this->getState('filter.only_use_in_subform'); - - if (is_numeric($onlyUseInSubForm)) - { - $onlyUseInSubForm = (int) $onlyUseInSubForm; - $query->where($db->quoteName('a.only_use_in_subform') . ' = :only_use_in_subform') - ->bind(':only_use_in_subform', $onlyUseInSubForm, ParameterType::INTEGER); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $search, ParameterType::INTEGER); - } - elseif (stripos($search, 'author:') === 0) - { - $search = '%' . substr($search, 7) . '%'; - $query->where( - '(' . - $db->quoteName('ua.name') . ' LIKE :name OR ' . - $db->quoteName('ua.username') . ' LIKE :username' . - ')' - ) - ->bind(':name', $search) - ->bind(':username', $search); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where( - '(' . - $db->quoteName('a.title') . ' LIKE :title OR ' . - $db->quoteName('a.name') . ' LIKE :sname OR ' . - $db->quoteName('a.note') . ' LIKE :note' . - ')' - ) - ->bind(':title', $search) - ->bind(':sname', $search) - ->bind(':note', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $language = (array) $language; - - $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); - } - - // Add the list ordering clause - $listOrdering = $this->state->get('list.ordering', 'a.ordering'); - $orderDirn = $this->state->get('list.direction', 'ASC'); - - $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); - - return $query; - } - - /** - * Gets an array of objects from the results of database query. - * - * @param string $query The query. - * @param integer $limitstart Offset. - * @param integer $limit The number of records. - * - * @return array An array of results. - * - * @since 3.7.0 - * @throws \RuntimeException - */ - protected function _getList($query, $limitstart = 0, $limit = 0) - { - $result = parent::_getList($query, $limitstart, $limit); - - if (is_array($result)) - { - foreach ($result as $field) - { - $field->fieldparams = new Registry($field->fieldparams); - $field->params = new Registry($field->params); - } - } - - return $result; - } - - /** - * Get the filter form - * - * @param array $data data - * @param boolean $loadData load current data - * - * @return \Joomla\CMS\Form\Form|bool the Form object or false - * - * @since 3.7.0 - */ - public function getFilterForm($data = array(), $loadData = true) - { - $form = parent::getFilterForm($data, $loadData); - - if ($form) - { - $form->setValue('context', null, $this->getState('filter.context')); - $form->setFieldAttribute('group_id', 'context', $this->getState('filter.context'), 'filter'); - $form->setFieldAttribute('assigned_cat_ids', 'extension', $this->state->get('filter.component'), 'filter'); - } - - return $form; - } - - /** - * Get the groups for the batch method - * - * @return array An array of groups - * - * @since 3.7.0 - */ - public function getGroups() - { - $user = Factory::getUser(); - $viewlevels = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); - $context = $this->state->get('filter.context'); - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('title', 'text'), - $db->quoteName('id', 'value'), - $db->quoteName('state'), - ] - ); - $query->from($db->quoteName('#__fields_groups')); - $query->whereIn($db->quoteName('state'), [0, 1]); - $query->where($db->quoteName('context') . ' = :context'); - $query->whereIn($db->quoteName('access'), $viewlevels); - $query->bind(':context', $context); - - $db->setQuery($query); - - return $db->loadObjectList(); - } + /** + * Constructor + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * + * @since 3.7.0 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'type', 'a.type', + 'name', 'a.name', + 'state', 'a.state', + 'access', 'a.access', + 'access_level', + 'only_use_in_subform', + 'language', 'a.language', + 'ordering', 'a.ordering', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'created_time', 'a.created_time', + 'created_user_id', 'a.created_user_id', + 'group_title', 'g.title', + 'category_id', 'a.category_id', + 'group_id', 'a.group_id', + 'assigned_cat_ids' + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.7.0 + */ + protected function populateState($ordering = null, $direction = null) + { + // List state information. + parent::populateState('a.ordering', 'asc'); + + $context = $this->getUserStateFromRequest($this->context . '.context', 'context', 'com_content.article', 'CMD'); + $this->setState('filter.context', $context); + + // Split context into component and optional section + $parts = FieldsHelper::extract($context); + + if ($parts) { + $this->setState('filter.component', $parts[0]); + $this->setState('filter.section', $parts[1]); + } + } + + /** + * Method to get a store id based on the model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id An identifier string to generate the store id. + * + * @return string A store id. + * + * @since 3.7.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.context'); + $id .= ':' . serialize($this->getState('filter.assigned_cat_ids')); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.group_id'); + $id .= ':' . serialize($this->getState('filter.language')); + $id .= ':' . $this->getState('filter.only_use_in_subform'); + + return parent::getStoreId($id); + } + + /** + * Method to get a DatabaseQuery object for retrieving the data set from a database. + * + * @return \Joomla\Database\DatabaseQuery A DatabaseQuery object to retrieve the data set. + * + * @since 3.7.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + $app = Factory::getApplication(); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'DISTINCT a.id, a.title, a.name, a.checked_out, a.checked_out_time, a.note' . + ', a.state, a.access, a.created_time, a.created_user_id, a.ordering, a.language' . + ', a.fieldparams, a.params, a.type, a.default_value, a.context, a.group_id' . + ', a.label, a.description, a.required, a.only_use_in_subform' + ) + ); + $query->from('#__fields AS a'); + + // Join over the language + $query->select('l.title AS language_title, l.image AS language_image') + ->join('LEFT', $db->quoteName('#__languages') . ' AS l ON l.lang_code = a.language'); + + // Join over the users for the checked out user. + $query->select('uc.name AS editor')->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); + + // Join over the asset groups. + $query->select('ag.title AS access_level')->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); + + // Join over the users for the author. + $query->select('ua.name AS author_name')->join('LEFT', '#__users AS ua ON ua.id = a.created_user_id'); + + // Join over the field groups. + $query->select('g.title AS group_title, g.access as group_access, g.state AS group_state, g.note as group_note'); + $query->join('LEFT', '#__fields_groups AS g ON g.id = a.group_id'); + + // Filter by context + if ($context = $this->getState('filter.context')) { + $query->where($db->quoteName('a.context') . ' = :context') + ->bind(':context', $context); + } + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + if (is_array($access)) { + $access = ArrayHelper::toInteger($access); + $query->whereIn($db->quoteName('a.access'), $access); + } else { + $access = (int) $access; + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + } + + if (($categories = $this->getState('filter.assigned_cat_ids')) && $context) { + $categories = (array) $categories; + $categories = ArrayHelper::toInteger($categories); + $parts = FieldsHelper::extract($context); + + if ($parts) { + // Get the categories for this component (and optionally this section, if available) + $cat = ( + function () use ($parts) { + // Get the CategoryService for this component + $componentObject = $this->bootComponent($parts[0]); + + if (!$componentObject instanceof CategoryServiceInterface) { + // No CategoryService -> no categories + return null; + } + + $cat = null; + + // Try to get the categories for this component and section + try { + $cat = $componentObject->getCategory([], $parts[1] ?: ''); + } catch (SectionNotFoundException $e) { + // Not found for component and section -> Now try once more without the section, so only component + try { + $cat = $componentObject->getCategory(); + } catch (SectionNotFoundException $e) { + // If we haven't found it now, return (no categories available for this component) + return null; + } + } + + // So we found categories for at least the component, return them + return $cat; + } + )(); + + if ($cat) { + foreach ($categories as $assignedCatIds) { + // Check if we have the actual category + $parent = $cat->get($assignedCatIds); + + if ($parent) { + $categories[] = (int) $parent->id; + + // Traverse the tree up to get all the fields which are attached to a parent + while ($parent->getParent() && $parent->getParent()->id != 'root') { + $parent = $parent->getParent(); + $categories[] = (int) $parent->id; + } + } + } + } + } + + $categories = array_unique($categories); + + // Join over the assigned categories + $query->join('LEFT', $db->quoteName('#__fields_categories') . ' AS fc ON fc.field_id = a.id'); + + if (in_array('0', $categories)) { + $query->where( + '(' . + $db->quoteName('fc.category_id') . ' IS NULL OR ' . + $db->quoteName('fc.category_id') . ' IN (' . implode(',', $query->bindArray(array_values($categories), ParameterType::INTEGER)) . ')' . + ')' + ); + } else { + $query->whereIn($db->quoteName('fc.category_id'), $categories); + } + } + + // Implement View Level Access + if (!$app->isClient('administrator') || !$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.group_id') . ' = 0', + $db->quoteName('g.access') . ' IN (' . implode(',', $query->bindArray($groups, ParameterType::INTEGER)) . ')' + ], + 'OR' + ); + } + + // Filter by state + $state = $this->getState('filter.state'); + + // Include group state only when not on on back end list + $includeGroupState = !$app->isClient('administrator') || + $app->input->get('option') != 'com_fields' || + $app->input->get('view') != 'fields'; + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + + if ($includeGroupState) { + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.group_id') . ' = 0', + $db->quoteName('g.state') . ' = :gstate', + ], + 'OR' + ) + ->bind(':gstate', $state, ParameterType::INTEGER); + } + } elseif (!$state) { + $query->whereIn($db->quoteName('a.state'), [0, 1]); + + if ($includeGroupState) { + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.group_id') . ' = 0', + $db->quoteName('g.state') . ' IN (' . implode(',', $query->bindArray([0, 1], ParameterType::INTEGER)) . ')' + ], + 'OR' + ); + } + } + + $groupId = $this->getState('filter.group_id'); + + if (is_numeric($groupId)) { + $groupId = (int) $groupId; + $query->where($db->quoteName('a.group_id') . ' = :groupid') + ->bind(':groupid', $groupId, ParameterType::INTEGER); + } + + $onlyUseInSubForm = $this->getState('filter.only_use_in_subform'); + + if (is_numeric($onlyUseInSubForm)) { + $onlyUseInSubForm = (int) $onlyUseInSubForm; + $query->where($db->quoteName('a.only_use_in_subform') . ' = :only_use_in_subform') + ->bind(':only_use_in_subform', $onlyUseInSubForm, ParameterType::INTEGER); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $search, ParameterType::INTEGER); + } elseif (stripos($search, 'author:') === 0) { + $search = '%' . substr($search, 7) . '%'; + $query->where( + '(' . + $db->quoteName('ua.name') . ' LIKE :name OR ' . + $db->quoteName('ua.username') . ' LIKE :username' . + ')' + ) + ->bind(':name', $search) + ->bind(':username', $search); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where( + '(' . + $db->quoteName('a.title') . ' LIKE :title OR ' . + $db->quoteName('a.name') . ' LIKE :sname OR ' . + $db->quoteName('a.note') . ' LIKE :note' . + ')' + ) + ->bind(':title', $search) + ->bind(':sname', $search) + ->bind(':note', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $language = (array) $language; + + $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); + } + + // Add the list ordering clause + $listOrdering = $this->state->get('list.ordering', 'a.ordering'); + $orderDirn = $this->state->get('list.direction', 'ASC'); + + $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); + + return $query; + } + + /** + * Gets an array of objects from the results of database query. + * + * @param string $query The query. + * @param integer $limitstart Offset. + * @param integer $limit The number of records. + * + * @return array An array of results. + * + * @since 3.7.0 + * @throws \RuntimeException + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $result = parent::_getList($query, $limitstart, $limit); + + if (is_array($result)) { + foreach ($result as $field) { + $field->fieldparams = new Registry($field->fieldparams); + $field->params = new Registry($field->params); + } + } + + return $result; + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return \Joomla\CMS\Form\Form|bool the Form object or false + * + * @since 3.7.0 + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + if ($form) { + $form->setValue('context', null, $this->getState('filter.context')); + $form->setFieldAttribute('group_id', 'context', $this->getState('filter.context'), 'filter'); + $form->setFieldAttribute('assigned_cat_ids', 'extension', $this->state->get('filter.component'), 'filter'); + } + + return $form; + } + + /** + * Get the groups for the batch method + * + * @return array An array of groups + * + * @since 3.7.0 + */ + public function getGroups() + { + $user = Factory::getUser(); + $viewlevels = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); + $context = $this->state->get('filter.context'); + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('title', 'text'), + $db->quoteName('id', 'value'), + $db->quoteName('state'), + ] + ); + $query->from($db->quoteName('#__fields_groups')); + $query->whereIn($db->quoteName('state'), [0, 1]); + $query->where($db->quoteName('context') . ' = :context'); + $query->whereIn($db->quoteName('access'), $viewlevels); + $query->bind(':context', $context); + + $db->setQuery($query); + + return $db->loadObjectList(); + } } diff --git a/administrator/components/com_fields/src/Model/GroupModel.php b/administrator/components/com_fields/src/Model/GroupModel.php index 81c1c2b4947cc..6f8fc12e443d5 100644 --- a/administrator/components/com_fields/src/Model/GroupModel.php +++ b/administrator/components/com_fields/src/Model/GroupModel.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage' - ); - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success, False on error. - * - * @since 3.7.0 - */ - public function save($data) - { - // Alter the title for save as copy - $input = Factory::getApplication()->input; - - // Save new group as unpublished - if ($input->get('task') == 'save2copy') - { - $data['state'] = 0; - } - - return parent::save($data); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.7.0 - * @throws \Exception - */ - public function getTable($name = 'Group', $prefix = 'Administrator', $options = array()) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Abstract method for getting the form from the model. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return mixed A Form object on success, false on failure - * - * @since 3.7.0 - */ - public function getForm($data = array(), $loadData = true) - { - $context = $this->getState('filter.context'); - $jinput = Factory::getApplication()->input; - - if (empty($context) && isset($data['context'])) - { - $context = $data['context']; - $this->setState('filter.context', $context); - } - - // Get the form. - $form = $this->loadForm( - 'com_fields.group.' . $context, 'group', - array( - 'control' => 'jform', - 'load_data' => $loadData, - ) - ); - - if (empty($form)) - { - return false; - } - - // Modify the form based on Edit State access controls. - if (empty($data['context'])) - { - $data['context'] = $context; - } - - $user = Factory::getUser(); - - if (!$user->authorise('core.edit.state', $context . '.fieldgroup.' . $jinput->get('id'))) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('state', 'disabled', 'true'); - - // Disable fields while saving. The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('state', 'filter', 'unset'); - } - - // Don't allow to change the created_by user if not allowed to access com_users. - if (!$user->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_by', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission for the component. - * - * @since 3.7.0 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->state != -2) - { - return false; - } - - return Factory::getUser()->authorise('core.delete', $record->context . '.fieldgroup.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission for the - * component. - * - * @since 3.7.0 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - - // Check for existing fieldgroup. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $record->context . '.fieldgroup.' . (int) $record->id); - } - - // Default to component settings. - return $user->authorise('core.edit.state', $record->context); - } - - /** - * Auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 3.7.0 - */ - protected function populateState() - { - parent::populateState(); - - $context = Factory::getApplication()->getUserStateFromRequest('com_fields.groups.context', 'context', 'com_fields', 'CMD'); - $this->setState('filter.context', $context); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param Table $table A Table object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 3.7.0 - */ - protected function getReorderConditions($table) - { - $db = $this->getDatabase(); - - return [ - $db->quoteName('context') . ' = ' . $db->quote($table->context), - ]; - } - - /** - * Method to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @see \Joomla\CMS\Form\FormField - * @since 3.7.0 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - parent::preprocessForm($form, $data, $group); - - $parts = FieldsHelper::extract($this->state->get('filter.context')); - - // Extract the component name - $component = $parts[0]; - - // Extract the optional section name - $section = (count($parts) > 1) ? $parts[1] : null; - - if ($parts) - { - // Set the access control rules field component value. - $form->setFieldAttribute('rules', 'component', $component); - } - - if ($section !== null) - { - // Looking first in the component models/forms folder - $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/fieldgroup/' . $section . '.xml'); - - if (file_exists($path)) - { - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE); - $lang->load($component, JPATH_BASE . '/components/' . $component); - - if (!$form->loadFile($path, false)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - } - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see JFormRule - * @see JFilterInput - * @since 3.9.23 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', 'com_fields')) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 3.7.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_fields.edit.group.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Pre-select some filters (Status, Language, Access) in edit form if those have been selected in Field Group Manager - if (!$data->id) - { - // Check for which context the Field Group Manager is used and get selected fields - $context = substr($app->getUserState('com_fields.groups.filter.context', ''), 4); - $filters = (array) $app->getUserState('com_fields.groups.' . $context . '.filter'); - - $data->set( - 'state', - $app->input->getInt('state', (!empty($filters['state']) ? $filters['state'] : null)) - ); - $data->set( - 'language', - $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null)) - ); - $data->set( - 'access', - $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) - ); - } - } - - $this->preprocessData('com_fields.group', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 3.7.0 - */ - public function getItem($pk = null) - { - if ($item = parent::getItem($pk)) - { - // Prime required properties. - if (empty($item->id)) - { - $item->context = $this->getState('filter.context'); - } - - if (property_exists($item, 'params')) - { - $item->params = new Registry($item->params); - } - } - - return $item; - } - - /** - * Clean the cache - * - * @param string $group The cache group - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 3.7.0 - */ - protected function cleanCache($group = null, $clientId = 0) - { - $context = Factory::getApplication()->input->get('context'); - - parent::cleanCache($context); - } + /** + * @var null|string + * + * @since 3.7.0 + */ + public $typeAlias = null; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage' + ); + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success, False on error. + * + * @since 3.7.0 + */ + public function save($data) + { + // Alter the title for save as copy + $input = Factory::getApplication()->input; + + // Save new group as unpublished + if ($input->get('task') == 'save2copy') { + $data['state'] = 0; + } + + return parent::save($data); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.7.0 + * @throws \Exception + */ + public function getTable($name = 'Group', $prefix = 'Administrator', $options = array()) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Abstract method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return mixed A Form object on success, false on failure + * + * @since 3.7.0 + */ + public function getForm($data = array(), $loadData = true) + { + $context = $this->getState('filter.context'); + $jinput = Factory::getApplication()->input; + + if (empty($context) && isset($data['context'])) { + $context = $data['context']; + $this->setState('filter.context', $context); + } + + // Get the form. + $form = $this->loadForm( + 'com_fields.group.' . $context, + 'group', + array( + 'control' => 'jform', + 'load_data' => $loadData, + ) + ); + + if (empty($form)) { + return false; + } + + // Modify the form based on Edit State access controls. + if (empty($data['context'])) { + $data['context'] = $context; + } + + $user = Factory::getUser(); + + if (!$user->authorise('core.edit.state', $context . '.fieldgroup.' . $jinput->get('id'))) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('state', 'disabled', 'true'); + + // Disable fields while saving. The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('state', 'filter', 'unset'); + } + + // Don't allow to change the created_by user if not allowed to access com_users. + if (!$user->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_by', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 3.7.0 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->state != -2) { + return false; + } + + return Factory::getUser()->authorise('core.delete', $record->context . '.fieldgroup.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission for the + * component. + * + * @since 3.7.0 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + + // Check for existing fieldgroup. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $record->context . '.fieldgroup.' . (int) $record->id); + } + + // Default to component settings. + return $user->authorise('core.edit.state', $record->context); + } + + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.7.0 + */ + protected function populateState() + { + parent::populateState(); + + $context = Factory::getApplication()->getUserStateFromRequest('com_fields.groups.context', 'context', 'com_fields', 'CMD'); + $this->setState('filter.context', $context); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param Table $table A Table object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 3.7.0 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('context') . ' = ' . $db->quote($table->context), + ]; + } + + /** + * Method to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @see \Joomla\CMS\Form\FormField + * @since 3.7.0 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + parent::preprocessForm($form, $data, $group); + + $parts = FieldsHelper::extract($this->state->get('filter.context')); + + // Extract the component name + $component = $parts[0]; + + // Extract the optional section name + $section = (count($parts) > 1) ? $parts[1] : null; + + if ($parts) { + // Set the access control rules field component value. + $form->setFieldAttribute('rules', 'component', $component); + } + + if ($section !== null) { + // Looking first in the component models/forms folder + $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/fieldgroup/' . $section . '.xml'); + + if (file_exists($path)) { + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE); + $lang->load($component, JPATH_BASE . '/components/' . $component); + + if (!$form->loadFile($path, false)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + } + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see JFormRule + * @see JFilterInput + * @since 3.9.23 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', 'com_fields')) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 3.7.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_fields.edit.group.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Pre-select some filters (Status, Language, Access) in edit form if those have been selected in Field Group Manager + if (!$data->id) { + // Check for which context the Field Group Manager is used and get selected fields + $context = substr($app->getUserState('com_fields.groups.filter.context', ''), 4); + $filters = (array) $app->getUserState('com_fields.groups.' . $context . '.filter'); + + $data->set( + 'state', + $app->input->getInt('state', (!empty($filters['state']) ? $filters['state'] : null)) + ); + $data->set( + 'language', + $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null)) + ); + $data->set( + 'access', + $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) + ); + } + } + + $this->preprocessData('com_fields.group', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 3.7.0 + */ + public function getItem($pk = null) + { + if ($item = parent::getItem($pk)) { + // Prime required properties. + if (empty($item->id)) { + $item->context = $this->getState('filter.context'); + } + + if (property_exists($item, 'params')) { + $item->params = new Registry($item->params); + } + } + + return $item; + } + + /** + * Clean the cache + * + * @param string $group The cache group + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 3.7.0 + */ + protected function cleanCache($group = null, $clientId = 0) + { + $context = Factory::getApplication()->input->get('context'); + + parent::cleanCache($context); + } } diff --git a/administrator/components/com_fields/src/Model/GroupsModel.php b/administrator/components/com_fields/src/Model/GroupsModel.php index 2669f0834171e..57b0e07d5e8ee 100644 --- a/administrator/components/com_fields/src/Model/GroupsModel.php +++ b/administrator/components/com_fields/src/Model/GroupsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.context', 'context', 'com_content', 'CMD'); - $this->setState('filter.context', $context); - } - - /** - * Method to get a store id based on the model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id An identifier string to generate the store id. - * - * @return string A store id. - * - * @since 3.7.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.context'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . print_r($this->getState('filter.language'), true); - - return parent::getStoreId($id); - } - - /** - * Method to get a DatabaseQuery object for retrieving the data set from a database. - * - * @return \Joomla\Database\DatabaseQuery A DatabaseQuery object to retrieve the data set. - * - * @since 3.7.0 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - // Select the required fields from the table. - $query->select($this->getState('list.select', 'a.*')); - $query->from('#__fields_groups AS a'); - - // Join over the language - $query->select('l.title AS language_title, l.image AS language_image') - ->join('LEFT', $db->quoteName('#__languages') . ' AS l ON l.lang_code = a.language'); - - // Join over the users for the checked out user. - $query->select('uc.name AS editor')->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); - - // Join over the asset groups. - $query->select('ag.title AS access_level')->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); - - // Join over the users for the author. - $query->select('ua.name AS author_name')->join('LEFT', '#__users AS ua ON ua.id = a.created_by'); - - // Filter by context - if ($context = $this->getState('filter.context', 'com_fields')) - { - $query->where($db->quoteName('a.context') . ' = :context') - ->bind(':context', $context); - } - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - if (is_array($access)) - { - $access = ArrayHelper::toInteger($access); - $query->whereIn($db->quoteName('a.access'), $access); - } - else - { - $access = (int) $access; - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - } - - // Filter by published state - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif (!$state) - { - $query->whereIn($db->quoteName('a.state'), [0, 1]); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':id', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where($db->quoteName('a.title') . ' LIKE :search') - ->bind(':search', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $language = (array) $language; - - $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); - } - - // Add the list ordering clause - $listOrdering = $this->getState('list.ordering', 'a.ordering'); - $listDirn = $db->escape($this->getState('list.direction', 'ASC')); - - $query->order($db->escape($listOrdering) . ' ' . $listDirn); - - return $query; - } - - /** - * Gets an array of objects from the results of database query. - * - * @param string $query The query. - * @param integer $limitstart Offset. - * @param integer $limit The number of records. - * - * @return array An array of results. - * - * @since 3.8.7 - * @throws \RuntimeException - */ - protected function _getList($query, $limitstart = 0, $limit = 0) - { - $result = parent::_getList($query, $limitstart, $limit); - - if (is_array($result)) - { - foreach ($result as $group) - { - $group->params = new Registry($group->params); - } - } - - return $result; - } + /** + * Context string for the model type. This is used to handle uniqueness + * when dealing with the getStoreId() method and caching data structures. + * + * @var string + * @since 3.7.0 + */ + protected $context = 'com_fields.groups'; + + /** + * Constructor + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * + * @since 3.7.0 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'type', 'a.type', + 'state', 'a.state', + 'access', 'a.access', + 'access_level', + 'language', 'a.language', + 'ordering', 'a.ordering', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'created', 'a.created', + 'created_by', 'a.created_by', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.7.0 + */ + protected function populateState($ordering = null, $direction = null) + { + // List state information. + parent::populateState('a.ordering', 'asc'); + + $context = $this->getUserStateFromRequest($this->context . '.context', 'context', 'com_content', 'CMD'); + $this->setState('filter.context', $context); + } + + /** + * Method to get a store id based on the model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id An identifier string to generate the store id. + * + * @return string A store id. + * + * @since 3.7.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.context'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . print_r($this->getState('filter.language'), true); + + return parent::getStoreId($id); + } + + /** + * Method to get a DatabaseQuery object for retrieving the data set from a database. + * + * @return \Joomla\Database\DatabaseQuery A DatabaseQuery object to retrieve the data set. + * + * @since 3.7.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + // Select the required fields from the table. + $query->select($this->getState('list.select', 'a.*')); + $query->from('#__fields_groups AS a'); + + // Join over the language + $query->select('l.title AS language_title, l.image AS language_image') + ->join('LEFT', $db->quoteName('#__languages') . ' AS l ON l.lang_code = a.language'); + + // Join over the users for the checked out user. + $query->select('uc.name AS editor')->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); + + // Join over the asset groups. + $query->select('ag.title AS access_level')->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); + + // Join over the users for the author. + $query->select('ua.name AS author_name')->join('LEFT', '#__users AS ua ON ua.id = a.created_by'); + + // Filter by context + if ($context = $this->getState('filter.context', 'com_fields')) { + $query->where($db->quoteName('a.context') . ' = :context') + ->bind(':context', $context); + } + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + if (is_array($access)) { + $access = ArrayHelper::toInteger($access); + $query->whereIn($db->quoteName('a.access'), $access); + } else { + $access = (int) $access; + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + } + + // Filter by published state + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif (!$state) { + $query->whereIn($db->quoteName('a.state'), [0, 1]); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':id', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where($db->quoteName('a.title') . ' LIKE :search') + ->bind(':search', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $language = (array) $language; + + $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); + } + + // Add the list ordering clause + $listOrdering = $this->getState('list.ordering', 'a.ordering'); + $listDirn = $db->escape($this->getState('list.direction', 'ASC')); + + $query->order($db->escape($listOrdering) . ' ' . $listDirn); + + return $query; + } + + /** + * Gets an array of objects from the results of database query. + * + * @param string $query The query. + * @param integer $limitstart Offset. + * @param integer $limit The number of records. + * + * @return array An array of results. + * + * @since 3.8.7 + * @throws \RuntimeException + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $result = parent::_getList($query, $limitstart, $limit); + + if (is_array($result)) { + foreach ($result as $group) { + $group->params = new Registry($group->params); + } + } + + return $result; + } } diff --git a/administrator/components/com_fields/src/Plugin/FieldsListPlugin.php b/administrator/components/com_fields/src/Plugin/FieldsListPlugin.php index 4d112e36f9a76..354c78371f89f 100644 --- a/administrator/components/com_fields/src/Plugin/FieldsListPlugin.php +++ b/administrator/components/com_fields/src/Plugin/FieldsListPlugin.php @@ -1,4 +1,5 @@ setAttribute('validate', 'options'); + $fieldNode->setAttribute('validate', 'options'); - foreach ($this->getOptionsFromField($field) as $value => $name) - { - $option = new \DOMElement('option', htmlspecialchars($value, ENT_COMPAT, 'UTF-8')); - $option->textContent = htmlspecialchars(Text::_($name), ENT_COMPAT, 'UTF-8'); + foreach ($this->getOptionsFromField($field) as $value => $name) { + $option = new \DOMElement('option', htmlspecialchars($value, ENT_COMPAT, 'UTF-8')); + $option->textContent = htmlspecialchars(Text::_($name), ENT_COMPAT, 'UTF-8'); - $element = $fieldNode->appendChild($option); - $element->setAttribute('value', $value); - } + $element = $fieldNode->appendChild($option); + $element->setAttribute('value', $value); + } - return $fieldNode; - } + return $fieldNode; + } - /** - * Returns an array of key values to put in a list from the given field. - * - * @param \stdClass $field The field. - * - * @return array - * - * @since 3.7.0 - */ - public function getOptionsFromField($field) - { - $data = array(); + /** + * Returns an array of key values to put in a list from the given field. + * + * @param \stdClass $field The field. + * + * @return array + * + * @since 3.7.0 + */ + public function getOptionsFromField($field) + { + $data = array(); - // Fetch the options from the plugin - $params = clone $this->params; - $params->merge($field->fieldparams); + // Fetch the options from the plugin + $params = clone $this->params; + $params->merge($field->fieldparams); - foreach ($params->get('options', array()) as $option) - { - $op = (object) $option; - $data[$op->value] = $op->name; - } + foreach ($params->get('options', array()) as $option) { + $op = (object) $option; + $data[$op->value] = $op->name; + } - return $data; - } + return $data; + } } diff --git a/administrator/components/com_fields/src/Plugin/FieldsPlugin.php b/administrator/components/com_fields/src/Plugin/FieldsPlugin.php index 5d2554d0f171e..d29d52eeadfd7 100644 --- a/administrator/components/com_fields/src/Plugin/FieldsPlugin.php +++ b/administrator/components/com_fields/src/Plugin/FieldsPlugin.php @@ -1,4 +1,5 @@ _type . $this->_name])) - { - return $types_cache[$this->_type . $this->_name]; - } - - $types = array(); - - // The root of the plugin - $root = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name; - - foreach (Folder::files($root . '/tmpl', '.php') as $layout) - { - // Strip the extension - $layout = str_replace('.php', '', $layout); - - // The data array - $data = array(); - - // The language key - $key = strtoupper($layout); - - if ($key != strtoupper($this->_name)) - { - $key = strtoupper($this->_name) . '_' . $layout; - } - - // Needed attributes - $data['type'] = $layout; - - if ($this->app->getLanguage()->hasKey('PLG_FIELDS_' . $key . '_LABEL')) - { - $data['label'] = Text::sprintf('PLG_FIELDS_' . $key . '_LABEL', strtolower($key)); - - // Fix wrongly set parentheses in RTL languages - if ($this->app->getLanguage()->isRtl()) - { - $data['label'] = $data['label'] . '‎'; - } - } - else - { - $data['label'] = $key; - } - - $path = $root . '/fields'; - - // Add the path when it exists - if (file_exists($path)) - { - $data['path'] = $path; - } - - $path = $root . '/rules'; - - // Add the path when it exists - if (file_exists($path)) - { - $data['rules'] = $path; - } - - $types[] = $data; - } - - // Add to cache and return the data - $types_cache[$this->_type . $this->_name] = $types; - - return $types; - } - - /** - * Prepares the field value. - * - * @param string $context The context. - * @param \stdclass $item The item. - * @param \stdclass $field The field. - * - * @return string - * - * @since 3.7.0 - */ - public function onCustomFieldsPrepareField($context, $item, $field) - { - // Check if the field should be processed by us - if (!$this->isTypeSupported($field->type)) - { - return; - } - - // Merge the params from the plugin and field which has precedence - $fieldParams = clone $this->params; - $fieldParams->merge($field->fieldparams); - - // Get the path for the layout file - $path = PluginHelper::getLayoutPath('fields', $this->_name, $field->type); - - // Render the layout - ob_start(); - include $path; - $output = ob_get_clean(); - - // Return the output - return $output; - } - - /** - * Transforms the field into a DOM XML element and appends it as a child on the given parent. - * - * @param \stdClass $field The field. - * @param \DOMElement $parent The field node parent. - * @param Form $form The form. - * - * @return \DOMElement - * - * @since 3.7.0 - */ - public function onCustomFieldsPrepareDom($field, \DOMElement $parent, Form $form) - { - // Check if the field should be processed by us - if (!$this->isTypeSupported($field->type)) - { - return null; - } - - // Detect if the field is configured to be displayed on the form - if (!FieldsHelper::displayFieldOnForm($field)) - { - return null; - } - - // Create the node - $node = $parent->appendChild(new \DOMElement('field')); - - // Set the attributes - $node->setAttribute('name', $field->name); - $node->setAttribute('type', $field->type); - $node->setAttribute('label', $field->label); - $node->setAttribute('labelclass', $field->params->get('label_class', '')); - $node->setAttribute('description', $field->description); - $node->setAttribute('class', $field->params->get('class', '')); - $node->setAttribute('hint', $field->params->get('hint', '')); - $node->setAttribute('required', $field->required ? 'true' : 'false'); - - if ($layout = $field->params->get('form_layout')) - { - $node->setAttribute('layout', $layout); - } - - if ($field->default_value !== '') - { - $defaultNode = $node->appendChild(new \DOMElement('default')); - $defaultNode->appendChild(new \DOMCdataSection($field->default_value)); - } - - // Combine the two params - $params = clone $this->params; - $params->merge($field->fieldparams); - - // Set the specific field parameters - foreach ($params->toArray() as $key => $param) - { - if (is_array($param)) - { - // Multidimensional arrays (eg. list options) can't be transformed properly - $param = count($param) == count($param, COUNT_RECURSIVE) ? implode(',', $param) : ''; - } - - if ($param === '' || (!is_string($param) && !is_numeric($param))) - { - continue; - } - - $node->setAttribute($key, $param); - } - - // Check if it is allowed to edit the field - if (!FieldsHelper::canEditFieldValue($field)) - { - $node->setAttribute('disabled', 'true'); - } - - // Return the node - return $node; - } - - /** - * The form event. Load additional parameters when available into the field form. - * Only when the type of the form is of interest. - * - * @param Form $form The form - * @param \stdClass $data The data - * - * @return void - * - * @since 3.7.0 - */ - public function onContentPrepareForm(Form $form, $data) - { - $path = $this->getFormPath($form, $data); - - if ($path === null) - { - return; - } - - // Load the specific plugin parameters - $form->load(file_get_contents($path), true, '/form/*'); - } - - /** - * Returns the path of the XML definition file for the field parameters - * - * @param Form $form The form - * @param \stdClass $data The data - * - * @return string - * - * @since 4.0.0 - */ - protected function getFormPath(Form $form, $data) - { - // Check if the field form is calling us - if (strpos($form->getName(), 'com_fields.field') !== 0) - { - return null; - } - - // Ensure it is an object - $formData = (object) $data; - - // Gather the type - $type = $form->getValue('type'); - - if (!empty($formData->type)) - { - $type = $formData->type; - } - - // Not us - if (!$this->isTypeSupported($type)) - { - return null; - } - - $path = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/params/' . $type . '.xml'; - - // Check if params file exists - if (!file_exists($path)) - { - return null; - } - - return $path; - } - - /** - * Returns true if the given type is supported by the plugin. - * - * @param string $type The type - * - * @return boolean - * - * @since 3.7.0 - */ - protected function isTypeSupported($type) - { - foreach ($this->onCustomFieldsGetTypes() as $typeSpecification) - { - if ($type == $typeSpecification['type']) - { - return true; - } - } - - return false; - } + /** + * Affects constructor behavior. If true, language files will be loaded automatically. + * + * @var boolean + * @since 3.7.0 + */ + protected $autoloadLanguage = true; + + /** + * Application object. + * + * @var \Joomla\CMS\Application\CMSApplication + * @since 4.0.0 + */ + protected $app; + + /** + * Returns the custom fields types. + * + * @return string[][] + * + * @since 3.7.0 + */ + public function onCustomFieldsGetTypes() + { + // Cache filesystem access / checks + static $types_cache = array(); + + if (isset($types_cache[$this->_type . $this->_name])) { + return $types_cache[$this->_type . $this->_name]; + } + + $types = array(); + + // The root of the plugin + $root = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name; + + foreach (Folder::files($root . '/tmpl', '.php') as $layout) { + // Strip the extension + $layout = str_replace('.php', '', $layout); + + // The data array + $data = array(); + + // The language key + $key = strtoupper($layout); + + if ($key != strtoupper($this->_name)) { + $key = strtoupper($this->_name) . '_' . $layout; + } + + // Needed attributes + $data['type'] = $layout; + + if ($this->app->getLanguage()->hasKey('PLG_FIELDS_' . $key . '_LABEL')) { + $data['label'] = Text::sprintf('PLG_FIELDS_' . $key . '_LABEL', strtolower($key)); + + // Fix wrongly set parentheses in RTL languages + if ($this->app->getLanguage()->isRtl()) { + $data['label'] = $data['label'] . '‎'; + } + } else { + $data['label'] = $key; + } + + $path = $root . '/fields'; + + // Add the path when it exists + if (file_exists($path)) { + $data['path'] = $path; + } + + $path = $root . '/rules'; + + // Add the path when it exists + if (file_exists($path)) { + $data['rules'] = $path; + } + + $types[] = $data; + } + + // Add to cache and return the data + $types_cache[$this->_type . $this->_name] = $types; + + return $types; + } + + /** + * Prepares the field value. + * + * @param string $context The context. + * @param \stdclass $item The item. + * @param \stdclass $field The field. + * + * @return string + * + * @since 3.7.0 + */ + public function onCustomFieldsPrepareField($context, $item, $field) + { + // Check if the field should be processed by us + if (!$this->isTypeSupported($field->type)) { + return; + } + + // Merge the params from the plugin and field which has precedence + $fieldParams = clone $this->params; + $fieldParams->merge($field->fieldparams); + + // Get the path for the layout file + $path = PluginHelper::getLayoutPath('fields', $this->_name, $field->type); + + // Render the layout + ob_start(); + include $path; + $output = ob_get_clean(); + + // Return the output + return $output; + } + + /** + * Transforms the field into a DOM XML element and appends it as a child on the given parent. + * + * @param \stdClass $field The field. + * @param \DOMElement $parent The field node parent. + * @param Form $form The form. + * + * @return \DOMElement + * + * @since 3.7.0 + */ + public function onCustomFieldsPrepareDom($field, \DOMElement $parent, Form $form) + { + // Check if the field should be processed by us + if (!$this->isTypeSupported($field->type)) { + return null; + } + + // Detect if the field is configured to be displayed on the form + if (!FieldsHelper::displayFieldOnForm($field)) { + return null; + } + + // Create the node + $node = $parent->appendChild(new \DOMElement('field')); + + // Set the attributes + $node->setAttribute('name', $field->name); + $node->setAttribute('type', $field->type); + $node->setAttribute('label', $field->label); + $node->setAttribute('labelclass', $field->params->get('label_class', '')); + $node->setAttribute('description', $field->description); + $node->setAttribute('class', $field->params->get('class', '')); + $node->setAttribute('hint', $field->params->get('hint', '')); + $node->setAttribute('required', $field->required ? 'true' : 'false'); + + if ($layout = $field->params->get('form_layout')) { + $node->setAttribute('layout', $layout); + } + + if ($field->default_value !== '') { + $defaultNode = $node->appendChild(new \DOMElement('default')); + $defaultNode->appendChild(new \DOMCdataSection($field->default_value)); + } + + // Combine the two params + $params = clone $this->params; + $params->merge($field->fieldparams); + + // Set the specific field parameters + foreach ($params->toArray() as $key => $param) { + if (is_array($param)) { + // Multidimensional arrays (eg. list options) can't be transformed properly + $param = count($param) == count($param, COUNT_RECURSIVE) ? implode(',', $param) : ''; + } + + if ($param === '' || (!is_string($param) && !is_numeric($param))) { + continue; + } + + $node->setAttribute($key, $param); + } + + // Check if it is allowed to edit the field + if (!FieldsHelper::canEditFieldValue($field)) { + $node->setAttribute('disabled', 'true'); + } + + // Return the node + return $node; + } + + /** + * The form event. Load additional parameters when available into the field form. + * Only when the type of the form is of interest. + * + * @param Form $form The form + * @param \stdClass $data The data + * + * @return void + * + * @since 3.7.0 + */ + public function onContentPrepareForm(Form $form, $data) + { + $path = $this->getFormPath($form, $data); + + if ($path === null) { + return; + } + + // Load the specific plugin parameters + $form->load(file_get_contents($path), true, '/form/*'); + } + + /** + * Returns the path of the XML definition file for the field parameters + * + * @param Form $form The form + * @param \stdClass $data The data + * + * @return string + * + * @since 4.0.0 + */ + protected function getFormPath(Form $form, $data) + { + // Check if the field form is calling us + if (strpos($form->getName(), 'com_fields.field') !== 0) { + return null; + } + + // Ensure it is an object + $formData = (object) $data; + + // Gather the type + $type = $form->getValue('type'); + + if (!empty($formData->type)) { + $type = $formData->type; + } + + // Not us + if (!$this->isTypeSupported($type)) { + return null; + } + + $path = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/params/' . $type . '.xml'; + + // Check if params file exists + if (!file_exists($path)) { + return null; + } + + return $path; + } + + /** + * Returns true if the given type is supported by the plugin. + * + * @param string $type The type + * + * @return boolean + * + * @since 3.7.0 + */ + protected function isTypeSupported($type) + { + foreach ($this->onCustomFieldsGetTypes() as $typeSpecification) { + if ($type == $typeSpecification['type']) { + return true; + } + } + + return false; + } } diff --git a/administrator/components/com_fields/src/Table/FieldTable.php b/administrator/components/com_fields/src/Table/FieldTable.php index 20953310ebe4c..d11c23cdeac45 100644 --- a/administrator/components/com_fields/src/Table/FieldTable.php +++ b/administrator/components/com_fields/src/Table/FieldTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - } - - /** - * Method to bind an associative array or object to the JTable instance.This - * method only binds properties that are publicly accessible and optionally - * takes an array of properties to ignore when binding. - * - * @param mixed $src An associative array or object to bind to the JTable instance. - * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean True on success. - * - * @since 3.7.0 - * @throws \InvalidArgumentException - */ - public function bind($src, $ignore = '') - { - if (isset($src['params']) && is_array($src['params'])) - { - $registry = new Registry; - $registry->loadArray($src['params']); - $src['params'] = (string) $registry; - } - - if (isset($src['fieldparams']) && is_array($src['fieldparams'])) - { - // Make sure $registry->options contains no duplicates when the field type is subform - if (isset($src['type']) && $src['type'] == 'subform' && isset($src['fieldparams']['options'])) - { - // Fast lookup map to check which custom field ids we have already seen - $seen_customfields = array(); - - // Container for the new $src['fieldparams']['options'] - $options = array(); - - // Iterate through the old options - $i = 0; - - foreach ($src['fieldparams']['options'] as $option) - { - // Check whether we have not yet seen this custom field id - if (!isset($seen_customfields[$option['customfield']])) - { - // We haven't, so add it to the final options - $seen_customfields[$option['customfield']] = true; - $options['option' . $i] = $option; - $i++; - } - } - - // And replace the options with the deduplicated ones. - $src['fieldparams']['options'] = $options; - } - - $registry = new Registry; - $registry->loadArray($src['fieldparams']); - $src['fieldparams'] = (string) $registry; - } - - // Bind the rules. - if (isset($src['rules']) && is_array($src['rules'])) - { - $rules = new Rules($src['rules']); - $this->setRules($rules); - } - - return parent::bind($src, $ignore); - } - - /** - * Method to perform sanity checks on the JTable instance properties to ensure - * they are safe to store in the database. Child classes should override this - * method to make sure the data they are storing in the database is safe and - * as expected before storage. - * - * @return boolean True if the instance is sane and able to be stored in the database. - * - * @link https://docs.joomla.org/Special:MyLanguage/JTable/check - * @since 3.7.0 - */ - public function check() - { - // Check for valid name - if (trim($this->title) == '') - { - $this->setError(Text::_('COM_FIELDS_MUSTCONTAIN_A_TITLE_FIELD')); - - return false; - } - - if (empty($this->name)) - { - $this->name = $this->title; - } - - $this->name = ApplicationHelper::stringURLSafe($this->name, $this->language); - - if (trim(str_replace('-', '', $this->name)) == '') - { - $this->name = StringHelper::increment($this->name, 'dash'); - } - - $this->name = str_replace(',', '-', $this->name); - - // Verify that the name is unique - $table = new static($this->_db); - - if ($table->load(array('name' => $this->name)) && ($table->id != $this->id || $this->id == 0)) - { - $this->setError(Text::_('COM_FIELDS_ERROR_UNIQUE_NAME')); - - return false; - } - - $this->name = str_replace(',', '-', $this->name); - - if (empty($this->type)) - { - $this->type = 'text'; - } - - if (empty($this->fieldparams)) - { - $this->fieldparams = '{}'; - } - - $date = Factory::getDate()->toSql(); - $user = Factory::getUser(); - - // Set created date if not set. - if (!(int) $this->created_time) - { - $this->created_time = $date; - } - - if ($this->id) - { - // Existing item - $this->modified_time = $date; - $this->modified_by = $user->get('id'); - } - else - { - if (!(int) $this->modified_time) - { - $this->modified_time = $this->created_time; - } - - if (empty($this->created_user_id)) - { - $this->created_user_id = $user->get('id'); - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_user_id; - } - } - - if (empty($this->group_id)) - { - $this->group_id = 0; - } - - return true; - } - - /** - * Overloaded store function - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return mixed False on failure, positive integer on success. - * - * @see Table::store() - * @since 4.0.0 - */ - public function store($updateNulls = true) - { - return parent::store($updateNulls); - } - - /** - * Method to compute the default name of the asset. - * The default name is in the form table_name.id - * where id is the value of the primary key of the table. - * - * @return string - * - * @since 3.7.0 - */ - protected function _getAssetName() - { - $contextArray = explode('.', $this->context); - - return $contextArray[0] . '.field.' . (int) $this->id; - } - - /** - * Method to return the title to use for the asset table. In - * tracking the assets a title is kept for each asset so that there is some - * context available in a unified access manager. Usually this would just - * return $this->title or $this->name or whatever is being used for the - * primary name of the row. If this method is not overridden, the asset name is used. - * - * @return string The string to use as the title in the asset table. - * - * @link https://docs.joomla.org/Special:MyLanguage/JTable/getAssetTitle - * @since 3.7.0 - */ - protected function _getAssetTitle() - { - return $this->title; - } - - /** - * Method to get the parent asset under which to register this one. - * By default, all assets are registered to the ROOT node with ID, - * which will default to 1 if none exists. - * The extended class can define a table and id to lookup. If the - * asset does not exist it will be created. - * - * @param Table $table A Table object for the asset parent. - * @param integer $id Id to look up - * - * @return integer - * - * @since 3.7.0 - */ - protected function _getAssetParentId(Table $table = null, $id = null) - { - $contextArray = explode('.', $this->context); - $component = $contextArray[0]; - - if ($this->group_id) - { - $assetId = $this->getAssetId($component . '.fieldgroup.' . (int) $this->group_id); - - if ($assetId) - { - return $assetId; - } - } - else - { - $assetId = $this->getAssetId($component); - - if ($assetId) - { - return $assetId; - } - } - - return parent::_getAssetParentId($table, $id); - } - - /** - * Returns an asset id for the given name or false. - * - * @param string $name The asset name - * - * @return number|boolean - * - * @since 3.7.0 - */ - private function getAssetId($name) - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__assets')) - ->where($db->quoteName('name') . ' = :name') - ->bind(':name', $name); - - // Get the asset id from the database. - $db->setQuery($query); - - $assetId = null; - - if ($result = $db->loadResult()) - { - $assetId = (int) $result; - - if ($assetId) - { - return $assetId; - } - } - - return false; - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Class constructor. + * + * @param DatabaseDriver $db DatabaseDriver object. + * + * @since 3.7.0 + */ + public function __construct($db = null) + { + parent::__construct('#__fields', 'id', $db); + + $this->setColumnAlias('published', 'state'); + } + + /** + * Method to bind an associative array or object to the JTable instance.This + * method only binds properties that are publicly accessible and optionally + * takes an array of properties to ignore when binding. + * + * @param mixed $src An associative array or object to bind to the JTable instance. + * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success. + * + * @since 3.7.0 + * @throws \InvalidArgumentException + */ + public function bind($src, $ignore = '') + { + if (isset($src['params']) && is_array($src['params'])) { + $registry = new Registry(); + $registry->loadArray($src['params']); + $src['params'] = (string) $registry; + } + + if (isset($src['fieldparams']) && is_array($src['fieldparams'])) { + // Make sure $registry->options contains no duplicates when the field type is subform + if (isset($src['type']) && $src['type'] == 'subform' && isset($src['fieldparams']['options'])) { + // Fast lookup map to check which custom field ids we have already seen + $seen_customfields = array(); + + // Container for the new $src['fieldparams']['options'] + $options = array(); + + // Iterate through the old options + $i = 0; + + foreach ($src['fieldparams']['options'] as $option) { + // Check whether we have not yet seen this custom field id + if (!isset($seen_customfields[$option['customfield']])) { + // We haven't, so add it to the final options + $seen_customfields[$option['customfield']] = true; + $options['option' . $i] = $option; + $i++; + } + } + + // And replace the options with the deduplicated ones. + $src['fieldparams']['options'] = $options; + } + + $registry = new Registry(); + $registry->loadArray($src['fieldparams']); + $src['fieldparams'] = (string) $registry; + } + + // Bind the rules. + if (isset($src['rules']) && is_array($src['rules'])) { + $rules = new Rules($src['rules']); + $this->setRules($rules); + } + + return parent::bind($src, $ignore); + } + + /** + * Method to perform sanity checks on the JTable instance properties to ensure + * they are safe to store in the database. Child classes should override this + * method to make sure the data they are storing in the database is safe and + * as expected before storage. + * + * @return boolean True if the instance is sane and able to be stored in the database. + * + * @link https://docs.joomla.org/Special:MyLanguage/JTable/check + * @since 3.7.0 + */ + public function check() + { + // Check for valid name + if (trim($this->title) == '') { + $this->setError(Text::_('COM_FIELDS_MUSTCONTAIN_A_TITLE_FIELD')); + + return false; + } + + if (empty($this->name)) { + $this->name = $this->title; + } + + $this->name = ApplicationHelper::stringURLSafe($this->name, $this->language); + + if (trim(str_replace('-', '', $this->name)) == '') { + $this->name = StringHelper::increment($this->name, 'dash'); + } + + $this->name = str_replace(',', '-', $this->name); + + // Verify that the name is unique + $table = new static($this->_db); + + if ($table->load(array('name' => $this->name)) && ($table->id != $this->id || $this->id == 0)) { + $this->setError(Text::_('COM_FIELDS_ERROR_UNIQUE_NAME')); + + return false; + } + + $this->name = str_replace(',', '-', $this->name); + + if (empty($this->type)) { + $this->type = 'text'; + } + + if (empty($this->fieldparams)) { + $this->fieldparams = '{}'; + } + + $date = Factory::getDate()->toSql(); + $user = Factory::getUser(); + + // Set created date if not set. + if (!(int) $this->created_time) { + $this->created_time = $date; + } + + if ($this->id) { + // Existing item + $this->modified_time = $date; + $this->modified_by = $user->get('id'); + } else { + if (!(int) $this->modified_time) { + $this->modified_time = $this->created_time; + } + + if (empty($this->created_user_id)) { + $this->created_user_id = $user->get('id'); + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_user_id; + } + } + + if (empty($this->group_id)) { + $this->group_id = 0; + } + + return true; + } + + /** + * Overloaded store function + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return mixed False on failure, positive integer on success. + * + * @see Table::store() + * @since 4.0.0 + */ + public function store($updateNulls = true) + { + return parent::store($updateNulls); + } + + /** + * Method to compute the default name of the asset. + * The default name is in the form table_name.id + * where id is the value of the primary key of the table. + * + * @return string + * + * @since 3.7.0 + */ + protected function _getAssetName() + { + $contextArray = explode('.', $this->context); + + return $contextArray[0] . '.field.' . (int) $this->id; + } + + /** + * Method to return the title to use for the asset table. In + * tracking the assets a title is kept for each asset so that there is some + * context available in a unified access manager. Usually this would just + * return $this->title or $this->name or whatever is being used for the + * primary name of the row. If this method is not overridden, the asset name is used. + * + * @return string The string to use as the title in the asset table. + * + * @link https://docs.joomla.org/Special:MyLanguage/JTable/getAssetTitle + * @since 3.7.0 + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Method to get the parent asset under which to register this one. + * By default, all assets are registered to the ROOT node with ID, + * which will default to 1 if none exists. + * The extended class can define a table and id to lookup. If the + * asset does not exist it will be created. + * + * @param Table $table A Table object for the asset parent. + * @param integer $id Id to look up + * + * @return integer + * + * @since 3.7.0 + */ + protected function _getAssetParentId(Table $table = null, $id = null) + { + $contextArray = explode('.', $this->context); + $component = $contextArray[0]; + + if ($this->group_id) { + $assetId = $this->getAssetId($component . '.fieldgroup.' . (int) $this->group_id); + + if ($assetId) { + return $assetId; + } + } else { + $assetId = $this->getAssetId($component); + + if ($assetId) { + return $assetId; + } + } + + return parent::_getAssetParentId($table, $id); + } + + /** + * Returns an asset id for the given name or false. + * + * @param string $name The asset name + * + * @return number|boolean + * + * @since 3.7.0 + */ + private function getAssetId($name) + { + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('name') . ' = :name') + ->bind(':name', $name); + + // Get the asset id from the database. + $db->setQuery($query); + + $assetId = null; + + if ($result = $db->loadResult()) { + $assetId = (int) $result; + + if ($assetId) { + return $assetId; + } + } + + return false; + } } diff --git a/administrator/components/com_fields/src/Table/GroupTable.php b/administrator/components/com_fields/src/Table/GroupTable.php index 474572238e4b6..de185f084f668 100644 --- a/administrator/components/com_fields/src/Table/GroupTable.php +++ b/administrator/components/com_fields/src/Table/GroupTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - } - - /** - * Method to bind an associative array or object to the JTable instance.This - * method only binds properties that are publicly accessible and optionally - * takes an array of properties to ignore when binding. - * - * @param mixed $src An associative array or object to bind to the JTable instance. - * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean True on success. - * - * @since 3.7.0 - * @throws \InvalidArgumentException - */ - public function bind($src, $ignore = '') - { - if (isset($src['params']) && is_array($src['params'])) - { - $registry = new Registry; - $registry->loadArray($src['params']); - $src['params'] = (string) $registry; - } - - // Bind the rules. - if (isset($src['rules']) && is_array($src['rules'])) - { - $rules = new Rules($src['rules']); - $this->setRules($rules); - } - - return parent::bind($src, $ignore); - } - - /** - * Method to perform sanity checks on the JTable instance properties to ensure - * they are safe to store in the database. Child classes should override this - * method to make sure the data they are storing in the database is safe and - * as expected before storage. - * - * @return boolean True if the instance is sane and able to be stored in the database. - * - * @link https://docs.joomla.org/Special:MyLanguage/JTable/check - * @since 3.7.0 - */ - public function check() - { - // Check for a title. - if (trim($this->title) == '') - { - $this->setError(Text::_('COM_FIELDS_MUSTCONTAIN_A_TITLE_GROUP')); - - return false; - } - - $date = Factory::getDate()->toSql(); - $user = Factory::getUser(); - - // Set created date if not set. - if (!(int) $this->created) - { - $this->created = $date; - } - - if ($this->id) - { - $this->modified = $date; - $this->modified_by = $user->get('id'); - } - else - { - if (!(int) $this->modified) - { - $this->modified = $this->created; - } - - if (empty($this->created_by)) - { - $this->created_by = $user->get('id'); - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - } - - if ($this->params === null) - { - $this->params = '{}'; - } - - return true; - } - - /** - * Overloaded store function - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return mixed False on failure, positive integer on success. - * - * @see Table::store() - * @since 4.0.0 - */ - public function store($updateNulls = true) - { - return parent::store($updateNulls); - } - - /** - * Method to compute the default name of the asset. - * The default name is in the form table_name.id - * where id is the value of the primary key of the table. - * - * @return string - * - * @since 3.7.0 - */ - protected function _getAssetName() - { - $component = explode('.', $this->context); - - return $component[0] . '.fieldgroup.' . (int) $this->id; - } - - /** - * Method to return the title to use for the asset table. In - * tracking the assets a title is kept for each asset so that there is some - * context available in a unified access manager. Usually this would just - * return $this->title or $this->name or whatever is being used for the - * primary name of the row. If this method is not overridden, the asset name is used. - * - * @return string The string to use as the title in the asset table. - * - * @link https://docs.joomla.org/Special:MyLanguage/JTable/getAssetTitle - * @since 3.7.0 - */ - protected function _getAssetTitle() - { - return $this->title; - } - - /** - * Method to get the parent asset under which to register this one. - * By default, all assets are registered to the ROOT node with ID, - * which will default to 1 if none exists. - * The extended class can define a table and id to lookup. If the - * asset does not exist it will be created. - * - * @param Table $table A Table object for the asset parent. - * @param integer $id Id to look up - * - * @return integer - * - * @since 3.7.0 - */ - protected function _getAssetParentId(Table $table = null, $id = null) - { - $component = explode('.', $this->context); - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__assets')) - ->where($db->quoteName('name') . ' = :name') - ->bind(':name', $component[0]); - $db->setQuery($query); - - if ($assetId = (int) $db->loadResult()) - { - return $assetId; - } - - return parent::_getAssetParentId($table, $id); - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Class constructor. + * + * @param DatabaseDriver $db DatabaseDriver object. + * + * @since 3.7.0 + */ + public function __construct($db = null) + { + parent::__construct('#__fields_groups', 'id', $db); + + $this->setColumnAlias('published', 'state'); + } + + /** + * Method to bind an associative array or object to the JTable instance.This + * method only binds properties that are publicly accessible and optionally + * takes an array of properties to ignore when binding. + * + * @param mixed $src An associative array or object to bind to the JTable instance. + * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success. + * + * @since 3.7.0 + * @throws \InvalidArgumentException + */ + public function bind($src, $ignore = '') + { + if (isset($src['params']) && is_array($src['params'])) { + $registry = new Registry(); + $registry->loadArray($src['params']); + $src['params'] = (string) $registry; + } + + // Bind the rules. + if (isset($src['rules']) && is_array($src['rules'])) { + $rules = new Rules($src['rules']); + $this->setRules($rules); + } + + return parent::bind($src, $ignore); + } + + /** + * Method to perform sanity checks on the JTable instance properties to ensure + * they are safe to store in the database. Child classes should override this + * method to make sure the data they are storing in the database is safe and + * as expected before storage. + * + * @return boolean True if the instance is sane and able to be stored in the database. + * + * @link https://docs.joomla.org/Special:MyLanguage/JTable/check + * @since 3.7.0 + */ + public function check() + { + // Check for a title. + if (trim($this->title) == '') { + $this->setError(Text::_('COM_FIELDS_MUSTCONTAIN_A_TITLE_GROUP')); + + return false; + } + + $date = Factory::getDate()->toSql(); + $user = Factory::getUser(); + + // Set created date if not set. + if (!(int) $this->created) { + $this->created = $date; + } + + if ($this->id) { + $this->modified = $date; + $this->modified_by = $user->get('id'); + } else { + if (!(int) $this->modified) { + $this->modified = $this->created; + } + + if (empty($this->created_by)) { + $this->created_by = $user->get('id'); + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + } + + if ($this->params === null) { + $this->params = '{}'; + } + + return true; + } + + /** + * Overloaded store function + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return mixed False on failure, positive integer on success. + * + * @see Table::store() + * @since 4.0.0 + */ + public function store($updateNulls = true) + { + return parent::store($updateNulls); + } + + /** + * Method to compute the default name of the asset. + * The default name is in the form table_name.id + * where id is the value of the primary key of the table. + * + * @return string + * + * @since 3.7.0 + */ + protected function _getAssetName() + { + $component = explode('.', $this->context); + + return $component[0] . '.fieldgroup.' . (int) $this->id; + } + + /** + * Method to return the title to use for the asset table. In + * tracking the assets a title is kept for each asset so that there is some + * context available in a unified access manager. Usually this would just + * return $this->title or $this->name or whatever is being used for the + * primary name of the row. If this method is not overridden, the asset name is used. + * + * @return string The string to use as the title in the asset table. + * + * @link https://docs.joomla.org/Special:MyLanguage/JTable/getAssetTitle + * @since 3.7.0 + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Method to get the parent asset under which to register this one. + * By default, all assets are registered to the ROOT node with ID, + * which will default to 1 if none exists. + * The extended class can define a table and id to lookup. If the + * asset does not exist it will be created. + * + * @param Table $table A Table object for the asset parent. + * @param integer $id Id to look up + * + * @return integer + * + * @since 3.7.0 + */ + protected function _getAssetParentId(Table $table = null, $id = null) + { + $component = explode('.', $this->context); + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('name') . ' = :name') + ->bind(':name', $component[0]); + $db->setQuery($query); + + if ($assetId = (int) $db->loadResult()) { + return $assetId; + } + + return parent::_getAssetParentId($table, $id); + } } diff --git a/administrator/components/com_fields/src/View/Field/HtmlView.php b/administrator/components/com_fields/src/View/Field/HtmlView.php index 0b8e8a209cb23..43b6c58f0a936 100644 --- a/administrator/components/com_fields/src/View/Field/HtmlView.php +++ b/administrator/components/com_fields/src/View/Field/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - $this->canDo = ContentHelper::getActions($this->state->get('field.component'), 'field', $this->item->id); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - Factory::getApplication()->input->set('hidemainmenu', true); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Adds the toolbar. - * - * @return void - * - * @since 3.7.0 - */ - protected function addToolbar() - { - $component = $this->state->get('field.component'); - $section = $this->state->get('field.section'); - $userId = $this->getCurrentUser()->get('id'); - $canDo = $this->canDo; - - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Avoid nonsense situation. - if ($component == 'com_fields') - { - return; - } - - // Load component language file - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_ADMINISTRATOR) - || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); - - $title = Text::sprintf('COM_FIELDS_VIEW_FIELD_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE', Text::_(strtoupper($component))); - - // Prepare the toolbar. - ToolbarHelper::title( - $title, - 'puzzle field-' . ($isNew ? 'add' : 'edit') . ' ' . substr($component, 4) . ($section ? "-$section" : '') . '-field-' . - ($isNew ? 'add' : 'edit') - ); - - // For new records, check the create permission. - if ($isNew) - { - ToolbarHelper::apply('field.apply'); - - ToolbarHelper::saveGroup( - [ - ['save', 'field.save'], - ['save2new', 'field.save2new'] - ], - 'btn-success' - ); - - ToolbarHelper::cancel('field.cancel'); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - $toolbarButtons = []; - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - ToolbarHelper::apply('field.apply'); - - $toolbarButtons[] = ['save', 'field.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'field.save2new']; - } - } - - // If an existing item, can save to a copy. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'field.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('field.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::help('Component:_New_or_Edit_Field'); - } + /** + * @var \Joomla\CMS\Form\Form + * + * @since 3.7.0 + */ + protected $form; + + /** + * @var CMSObject + * + * @since 3.7.0 + */ + protected $item; + + /** + * @var CMSObject + * + * @since 3.7.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see HtmlView::loadTemplate() + * + * @since 3.7.0 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + $this->canDo = ContentHelper::getActions($this->state->get('field.component'), 'field', $this->item->id); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + Factory::getApplication()->input->set('hidemainmenu', true); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Adds the toolbar. + * + * @return void + * + * @since 3.7.0 + */ + protected function addToolbar() + { + $component = $this->state->get('field.component'); + $section = $this->state->get('field.section'); + $userId = $this->getCurrentUser()->get('id'); + $canDo = $this->canDo; + + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Avoid nonsense situation. + if ($component == 'com_fields') { + return; + } + + // Load component language file + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_ADMINISTRATOR) + || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); + + $title = Text::sprintf('COM_FIELDS_VIEW_FIELD_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE', Text::_(strtoupper($component))); + + // Prepare the toolbar. + ToolbarHelper::title( + $title, + 'puzzle field-' . ($isNew ? 'add' : 'edit') . ' ' . substr($component, 4) . ($section ? "-$section" : '') . '-field-' . + ($isNew ? 'add' : 'edit') + ); + + // For new records, check the create permission. + if ($isNew) { + ToolbarHelper::apply('field.apply'); + + ToolbarHelper::saveGroup( + [ + ['save', 'field.save'], + ['save2new', 'field.save2new'] + ], + 'btn-success' + ); + + ToolbarHelper::cancel('field.cancel'); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + $toolbarButtons = []; + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + ToolbarHelper::apply('field.apply'); + + $toolbarButtons[] = ['save', 'field.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'field.save2new']; + } + } + + // If an existing item, can save to a copy. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'field.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('field.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::help('Component:_New_or_Edit_Field'); + } } diff --git a/administrator/components/com_fields/src/View/Fields/HtmlView.php b/administrator/components/com_fields/src/View/Fields/HtmlView.php index e7bb2fdf8d170..dedb9e985c8ab 100644 --- a/administrator/components/com_fields/src/View/Fields/HtmlView.php +++ b/administrator/components/com_fields/src/View/Fields/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Display a warning if the fields system plugin is disabled - if (!PluginHelper::isEnabled('system', 'fields')) - { - $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . FieldsHelper::getFieldsPluginId()); - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_FIELDS_SYSTEM_PLUGIN_NOT_ENABLED', $link), 'warning'); - } - - // Only add toolbar when not in modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - - parent::display($tpl); - } - - /** - * Adds the toolbar. - * - * @return void - * - * @since 3.7.0 - */ - protected function addToolbar() - { - $fieldId = $this->state->get('filter.field_id'); - $component = $this->state->get('filter.component'); - $section = $this->state->get('filter.section'); - $canDo = ContentHelper::getActions($component, 'field', $fieldId); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - // Avoid nonsense situation. - if ($component == 'com_fields') - { - return; - } - - // Load extension language file - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_ADMINISTRATOR) - || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); - - $title = Text::sprintf('COM_FIELDS_VIEW_FIELDS_TITLE', Text::_(strtoupper($component))); - - // Prepare the toolbar. - ToolbarHelper::title($title, 'puzzle-piece fields ' . substr($component, 4) . ($section ? "-$section" : '') . '-fields'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('field.add'); - } - - if ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('fields.publish')->listCheck(true); - - $childBar->unpublish('fields.unpublish')->listCheck(true); - - $childBar->archive('fields.archive')->listCheck(true); - } - - if ($this->getCurrentUser()->authorise('core.admin')) - { - $childBar->checkin('fields.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && !$this->state->get('filter.state') == -2) - { - $childBar->trash('fields.trash')->listCheck(true); - } - - // Add a batch button - if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete', $component)) - { - $toolbar->delete('fields.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences($component); - } - - $toolbar->help('Component:_Fields'); - } + /** + * @var \Joomla\CMS\Form\Form + * + * @since 3.7.0 + */ + public $filterForm; + + /** + * @var array + * + * @since 3.7.0 + */ + public $activeFilters; + + /** + * @var array + * + * @since 3.7.0 + */ + protected $items; + + /** + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.7.0 + */ + protected $pagination; + + /** + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.7.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see \Joomla\CMS\MVC\View\HtmlView::loadTemplate() + * + * @since 3.7.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Display a warning if the fields system plugin is disabled + if (!PluginHelper::isEnabled('system', 'fields')) { + $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . FieldsHelper::getFieldsPluginId()); + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_FIELDS_SYSTEM_PLUGIN_NOT_ENABLED', $link), 'warning'); + } + + // Only add toolbar when not in modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } + + parent::display($tpl); + } + + /** + * Adds the toolbar. + * + * @return void + * + * @since 3.7.0 + */ + protected function addToolbar() + { + $fieldId = $this->state->get('filter.field_id'); + $component = $this->state->get('filter.component'); + $section = $this->state->get('filter.section'); + $canDo = ContentHelper::getActions($component, 'field', $fieldId); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + // Avoid nonsense situation. + if ($component == 'com_fields') { + return; + } + + // Load extension language file + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_ADMINISTRATOR) + || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); + + $title = Text::sprintf('COM_FIELDS_VIEW_FIELDS_TITLE', Text::_(strtoupper($component))); + + // Prepare the toolbar. + ToolbarHelper::title($title, 'puzzle-piece fields ' . substr($component, 4) . ($section ? "-$section" : '') . '-fields'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('field.add'); + } + + if ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('fields.publish')->listCheck(true); + + $childBar->unpublish('fields.unpublish')->listCheck(true); + + $childBar->archive('fields.archive')->listCheck(true); + } + + if ($this->getCurrentUser()->authorise('core.admin')) { + $childBar->checkin('fields.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && !$this->state->get('filter.state') == -2) { + $childBar->trash('fields.trash')->listCheck(true); + } + + // Add a batch button + if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete', $component)) { + $toolbar->delete('fields.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences($component); + } + + $toolbar->help('Component:_Fields'); + } } diff --git a/administrator/components/com_fields/src/View/Group/HtmlView.php b/administrator/components/com_fields/src/View/Group/HtmlView.php index 7e1421f78ee71..52ff8ad29ac9c 100644 --- a/administrator/components/com_fields/src/View/Group/HtmlView.php +++ b/administrator/components/com_fields/src/View/Group/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - $component = ''; - $parts = FieldsHelper::extract($this->state->get('filter.context')); - - if ($parts) - { - $component = $parts[0]; - } - - $this->canDo = ContentHelper::getActions($component, 'fieldgroup', $this->item->id); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - Factory::getApplication()->input->set('hidemainmenu', true); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Adds the toolbar. - * - * @return void - * - * @since 3.7.0 - */ - protected function addToolbar() - { - $component = ''; - $parts = FieldsHelper::extract($this->state->get('filter.context')); - - if ($parts) - { - $component = $parts[0]; - } - - $userId = $this->getCurrentUser()->get('id'); - $canDo = $this->canDo; - - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Avoid nonsense situation. - if ($component == 'com_fields') - { - return; - } - - // Load component language file - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_ADMINISTRATOR) - || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); - - $title = Text::sprintf('COM_FIELDS_VIEW_GROUP_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE', Text::_(strtoupper($component))); - - // Prepare the toolbar. - ToolbarHelper::title( - $title, - 'puzzle-piece field-' . ($isNew ? 'add' : 'edit') . ' ' . substr($component, 4) . '-group-' . - ($isNew ? 'add' : 'edit') - ); - - $toolbarButtons = []; - - // For new records, check the create permission. - if ($isNew) - { - ToolbarHelper::apply('group.apply'); - - ToolbarHelper::saveGroup( - [ - ['save', 'group.save'], - ['save2new', 'group.save2new'] - ], - 'btn-success' - ); - - ToolbarHelper::cancel('group.cancel'); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - $toolbarButtons = []; - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - ToolbarHelper::apply('group.apply'); - - $toolbarButtons[] = ['save', 'group.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'group.save2new']; - } - } - - // If an existing item, can save to a copy. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'group.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::help('Component:_New_or_Edit_Field_Group'); - } + /** + * @var \Joomla\CMS\Form\Form + * + * @since 3.7.0 + */ + protected $form; + + /** + * @var CMSObject + * + * @since 3.7.0 + */ + protected $item; + + /** + * @var CMSObject + * + * @since 3.7.0 + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * + * @since 3.7.0 + */ + protected $canDo; + + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see JViewLegacy::loadTemplate() + * + * @since 3.7.0 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + $component = ''; + $parts = FieldsHelper::extract($this->state->get('filter.context')); + + if ($parts) { + $component = $parts[0]; + } + + $this->canDo = ContentHelper::getActions($component, 'fieldgroup', $this->item->id); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + Factory::getApplication()->input->set('hidemainmenu', true); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Adds the toolbar. + * + * @return void + * + * @since 3.7.0 + */ + protected function addToolbar() + { + $component = ''; + $parts = FieldsHelper::extract($this->state->get('filter.context')); + + if ($parts) { + $component = $parts[0]; + } + + $userId = $this->getCurrentUser()->get('id'); + $canDo = $this->canDo; + + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Avoid nonsense situation. + if ($component == 'com_fields') { + return; + } + + // Load component language file + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_ADMINISTRATOR) + || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); + + $title = Text::sprintf('COM_FIELDS_VIEW_GROUP_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE', Text::_(strtoupper($component))); + + // Prepare the toolbar. + ToolbarHelper::title( + $title, + 'puzzle-piece field-' . ($isNew ? 'add' : 'edit') . ' ' . substr($component, 4) . '-group-' . + ($isNew ? 'add' : 'edit') + ); + + $toolbarButtons = []; + + // For new records, check the create permission. + if ($isNew) { + ToolbarHelper::apply('group.apply'); + + ToolbarHelper::saveGroup( + [ + ['save', 'group.save'], + ['save2new', 'group.save2new'] + ], + 'btn-success' + ); + + ToolbarHelper::cancel('group.cancel'); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + $toolbarButtons = []; + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + ToolbarHelper::apply('group.apply'); + + $toolbarButtons[] = ['save', 'group.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'group.save2new']; + } + } + + // If an existing item, can save to a copy. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'group.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::help('Component:_New_or_Edit_Field_Group'); + } } diff --git a/administrator/components/com_fields/src/View/Groups/HtmlView.php b/administrator/components/com_fields/src/View/Groups/HtmlView.php index 2c80ab45f2cec..6c899e6c8da35 100644 --- a/administrator/components/com_fields/src/View/Groups/HtmlView.php +++ b/administrator/components/com_fields/src/View/Groups/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Display a warning if the fields system plugin is disabled - if (!PluginHelper::isEnabled('system', 'fields')) - { - $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . FieldsHelper::getFieldsPluginId()); - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_FIELDS_SYSTEM_PLUGIN_NOT_ENABLED', $link), 'warning'); - } - - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - - parent::display($tpl); - } - - /** - * Adds the toolbar. - * - * @return void - * - * @since 3.7.0 - */ - protected function addToolbar() - { - $groupId = $this->state->get('filter.group_id'); - $component = ''; - $parts = FieldsHelper::extract($this->state->get('filter.context')); - - if ($parts) - { - $component = $parts[0]; - } - - $canDo = ContentHelper::getActions($component, 'fieldgroup', $groupId); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - // Avoid nonsense situation. - if ($component == 'com_fields') - { - return; - } - - // Load component language file - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_ADMINISTRATOR) - || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); - - $title = Text::sprintf('COM_FIELDS_VIEW_GROUPS_TITLE', Text::_(strtoupper($component))); - - // Prepare the toolbar. - ToolbarHelper::title($title, 'puzzle-piece fields ' . substr($component, 4) . '-groups'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('group.add'); - } - - if ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('groups.publish')->listCheck(true); - - $childBar->unpublish('groups.unpublish')->listCheck(true); - - $childBar->archive('groups.archive')->listCheck(true); - } - - if ($this->getCurrentUser()->authorise('core.admin')) - { - $childBar->checkin('groups.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && !$this->state->get('filter.state') == -2) - { - $childBar->trash('groups.trash')->listCheck(true); - } - - // Add a batch button - if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete', $component)) - { - $toolbar->delete('groups.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences($component); - } - - $toolbar->help('Component:_Field_Groups'); - } + /** + * @var \Joomla\CMS\Form\Form + * + * @since 3.7.0 + */ + public $filterForm; + + /** + * @var array + * + * @since 3.7.0 + */ + public $activeFilters; + + /** + * @var array + * + * @since 3.7.0 + */ + protected $items; + + /** + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.7.0 + */ + protected $pagination; + + /** + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.7.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see HtmlView::loadTemplate() + * + * @since 3.7.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Display a warning if the fields system plugin is disabled + if (!PluginHelper::isEnabled('system', 'fields')) { + $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . FieldsHelper::getFieldsPluginId()); + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_FIELDS_SYSTEM_PLUGIN_NOT_ENABLED', $link), 'warning'); + } + + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + + parent::display($tpl); + } + + /** + * Adds the toolbar. + * + * @return void + * + * @since 3.7.0 + */ + protected function addToolbar() + { + $groupId = $this->state->get('filter.group_id'); + $component = ''; + $parts = FieldsHelper::extract($this->state->get('filter.context')); + + if ($parts) { + $component = $parts[0]; + } + + $canDo = ContentHelper::getActions($component, 'fieldgroup', $groupId); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + // Avoid nonsense situation. + if ($component == 'com_fields') { + return; + } + + // Load component language file + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_ADMINISTRATOR) + || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); + + $title = Text::sprintf('COM_FIELDS_VIEW_GROUPS_TITLE', Text::_(strtoupper($component))); + + // Prepare the toolbar. + ToolbarHelper::title($title, 'puzzle-piece fields ' . substr($component, 4) . '-groups'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('group.add'); + } + + if ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('groups.publish')->listCheck(true); + + $childBar->unpublish('groups.unpublish')->listCheck(true); + + $childBar->archive('groups.archive')->listCheck(true); + } + + if ($this->getCurrentUser()->authorise('core.admin')) { + $childBar->checkin('groups.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && !$this->state->get('filter.state') == -2) { + $childBar->trash('groups.trash')->listCheck(true); + } + + // Add a batch button + if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete', $component)) { + $toolbar->delete('groups.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences($component); + } + + $toolbar->help('Component:_Field_Groups'); + } } diff --git a/administrator/components/com_fields/tmpl/field/edit.php b/administrator/components/com_fields/tmpl/field/edit.php index 1a6f95a144c16..61171f19fbbd9 100644 --- a/administrator/components/com_fields/tmpl/field/edit.php +++ b/administrator/components/com_fields/tmpl/field/edit.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -22,78 +24,79 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_fields.admin-field-edit'); + ->useScript('form.validate') + ->useScript('com_fields.admin-field-edit'); ?>
    - + -
    - 'general', 'recall' => true, 'breakpoint' => 768]); ?> - -
    -
    - form->renderField('type'); ?> - form->renderField('name'); ?> - form->renderField('label'); ?> - form->renderField('description'); ?> - form->renderField('required'); ?> - form->renderField('only_use_in_subform'); ?> - form->renderField('default_value'); ?> +
    + 'general', 'recall' => true, 'breakpoint' => 768]); ?> + +
    +
    + form->renderField('type'); ?> + form->renderField('name'); ?> + form->renderField('label'); ?> + form->renderField('description'); ?> + form->renderField('required'); ?> + form->renderField('only_use_in_subform'); ?> + form->renderField('default_value'); ?> - form->getFieldsets('fieldparams') as $name => $fieldSet) : ?> - form->getFieldset($name) as $field) : ?> - renderField(); ?> - - -
    -
    - set('fields', - array( - array( - 'published', - 'state', - 'enabled', - ), - 'group_id', - 'assigned_cat_ids', - 'access', - 'language', - 'note', - ) - ); ?> - - set('fields', null); ?> -
    -
    - - set('ignore_fieldsets', array('fieldparams')); ?> - - -
    - -
    - - form->renderField('searchindexing'); ?> -
    -
    - - canDo->get('core.admin')) : ?> - -
    - -
    - form->getInput('rules'); ?> -
    -
    - - - - form->getInput('context'); ?> - - -
    + form->getFieldsets('fieldparams') as $name => $fieldSet) : ?> + form->getFieldset($name) as $field) : ?> + renderField(); ?> + + +
    +
    + set( + 'fields', + array( + array( + 'published', + 'state', + 'enabled', + ), + 'group_id', + 'assigned_cat_ids', + 'access', + 'language', + 'note', + ) + ); ?> + + set('fields', null); ?> +
    +
    + + set('ignore_fieldsets', array('fieldparams')); ?> + + +
    + +
    + + form->renderField('searchindexing'); ?> +
    +
    + + canDo->get('core.admin')) : ?> + +
    + +
    + form->getInput('rules'); ?> +
    +
    + + + + form->getInput('context'); ?> + + +
    diff --git a/administrator/components/com_fields/tmpl/fields/default.php b/administrator/components/com_fields/tmpl/fields/default.php index 39808d489be6a..0a2b0033b6178 100644 --- a/administrator/components/com_fields/tmpl/fields/default.php +++ b/administrator/components/com_fields/tmpl/fields/default.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Categories\Categories; @@ -21,7 +23,7 @@ /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $app = Factory::getApplication(); $user = Factory::getUser(); @@ -38,184 +40,185 @@ $category = Categories::getInstance(str_replace('com_', '', $component) . '.' . $section); // If there is no category for the component and section, then check the component only -if (!$category) -{ - $category = Categories::getInstance(str_replace('com_', '', $component)); +if (!$category) { + $category = Categories::getInstance(str_replace('com_', '', $component)); } -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_fields&task=fields.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_fields&task=fields.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $searchToolsOptions = []; // Only show field contexts filter if there are more than one option -if (count($this->filterForm->getField('context')->options) > 1) -{ - $searchToolsOptions['selectorFieldName'] = 'context'; +if (count($this->filterForm->getField('context')->options) > 1) { + $searchToolsOptions['selectorFieldName'] = 'context'; } ?>
    -
    -
    -
    - $this, 'options' => $searchToolsOptions)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : ?> - - authorise('core.edit', $component . '.field.' . $item->id); ?> - authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); ?> - authorise('core.edit.own', $component . '.field.' . $item->id) && $item->created_user_id == $userId; ?> - authorise('core.edit.state', $component . '.field.' . $item->id) && $canCheckin; ?> - - - - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - - - - - -
    - id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - - - - - - state, $i, 'fields.', $canChange, 'cb'); ?> - -
    - checked_out) : ?> - editor, $item->checked_out_time, 'fields.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
    - note)) : ?> - escape($item->name)); ?> - - escape($item->name), $this->escape($item->note)); ?> - -
    - only_use_in_subform) : ?> -
    - -
    - -
    - - id); ?> - - - - id); ?> - - -
    - -
    -
    - escape($item->type); ?> - - escape($item->group_title); ?> - - escape($item->access_level); ?> - - - - id; ?> -
    +
    +
    +
    + $this, 'options' => $searchToolsOptions)); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : ?> + + authorise('core.edit', $component . '.field.' . $item->id); ?> + authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); ?> + authorise('core.edit.own', $component . '.field.' . $item->id) && $item->created_user_id == $userId; ?> + authorise('core.edit.state', $component . '.field.' . $item->id) && $canCheckin; ?> + + + + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + + + + + +
    + id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + + + + + state, $i, 'fields.', $canChange, 'cb'); ?> + +
    + checked_out) : ?> + editor, $item->checked_out_time, 'fields.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
    + note)) : ?> + escape($item->name)); ?> + + escape($item->name), $this->escape($item->note)); ?> + +
    + only_use_in_subform) : ?> +
    + +
    + +
    + + id); ?> + + + + id); ?> + + +
    + +
    +
    + escape($item->type); ?> + + escape($item->group_title); ?> + + escape($item->access_level); ?> + + + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', $component) - && $user->authorise('core.edit', $component) - && $user->authorise('core.edit.state', $component)) : ?> - Text::_('COM_FIELDS_VIEW_FIELDS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer') - ), - $this->loadTemplate('batch_body') - ); ?> - - - - - -
    -
    -
    + + authorise('core.create', $component) + && $user->authorise('core.edit', $component) + && $user->authorise('core.edit.state', $component) +) : ?> + Text::_('COM_FIELDS_VIEW_FIELDS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer') + ), + $this->loadTemplate('batch_body') + ); ?> + + + + + +
    +
    +
    diff --git a/administrator/components/com_fields/tmpl/fields/default_batch_body.php b/administrator/components/com_fields/tmpl/fields/default_batch_body.php index 700f26699adcb..c5b1e0c000253 100644 --- a/administrator/components/com_fields/tmpl/fields/default_batch_body.php +++ b/administrator/components/com_fields/tmpl/fields/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\HTML\HTMLHelper; @@ -22,43 +24,43 @@ ?>
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -
    - - -
    - -
    -
    - - -
    -
    -
    -
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    + + +
    + +
    +
    + + +
    +
    +
    +
    diff --git a/administrator/components/com_fields/tmpl/fields/default_batch_footer.php b/administrator/components/com_fields/tmpl/fields/default_batch_footer.php index 512cb848f01ad..b48333c6a5466 100644 --- a/administrator/components/com_fields/tmpl/fields/default_batch_footer.php +++ b/administrator/components/com_fields/tmpl/fields/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/administrator/components/com_fields/tmpl/fields/modal.php b/administrator/components/com_fields/tmpl/fields/modal.php index 5f7d1c11620f2..15a39f1c485ee 100644 --- a/administrator/components/com_fields/tmpl/fields/modal.php +++ b/administrator/components/com_fields/tmpl/fields/modal.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -15,9 +17,8 @@ use Joomla\CMS\Router\Route; use Joomla\CMS\Session\Session; -if (Factory::getApplication()->isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if (Factory::getApplication()->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ @@ -30,93 +31,93 @@ ?>
    -
    + - $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', - ); - foreach ($this->items as $i => $item) : - ?> - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - -
    - - - - - escape($item->title); ?> - - group_id ? $this->escape($item->group_title) : Text::_('JNONE'); ?> - - type; ?> - - escape($item->access_level); ?> - - - - id; ?> -
    + $this)); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', + ); + foreach ($this->items as $i => $item) : + ?> + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + +
    + + + + + escape($item->title); ?> + + group_id ? $this->escape($item->group_title) : Text::_('JNONE'); ?> + + type; ?> + + escape($item->access_level); ?> + + + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - + + + -
    +
    diff --git a/administrator/components/com_fields/tmpl/group/edit.php b/administrator/components/com_fields/tmpl/group/edit.php index 45761b81a852d..cf6f0c1d9baca 100644 --- a/administrator/components/com_fields/tmpl/group/edit.php +++ b/administrator/components/com_fields/tmpl/group/edit.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -17,7 +19,7 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $input = $app->input; @@ -27,56 +29,57 @@ ?>
    - -
    - 'general', 'recall' => true, 'breakpoint' => 768]); ?> - -
    -
    - form->renderField('label'); ?> - form->renderField('description'); ?> -
    -
    - set('fields', - array( - array( - 'published', - 'state', - 'enabled', - ), - 'access', - 'language', - 'note', - ) - ); ?> - - set('fields', null); ?> -
    -
    - - -
    - -
    - -
    -
    - - set('ignore_fieldsets', array('fieldparams')); ?> - - canDo->get('core.admin')) : ?> - -
    - -
    - form->getInput('rules'); ?> -
    -
    - - - - form->getInput('context'); ?> - - -
    + +
    + 'general', 'recall' => true, 'breakpoint' => 768]); ?> + +
    +
    + form->renderField('label'); ?> + form->renderField('description'); ?> +
    +
    + set( + 'fields', + array( + array( + 'published', + 'state', + 'enabled', + ), + 'access', + 'language', + 'note', + ) + ); ?> + + set('fields', null); ?> +
    +
    + + +
    + +
    + +
    +
    + + set('ignore_fieldsets', array('fieldparams')); ?> + + canDo->get('core.admin')) : ?> + +
    + +
    + form->getInput('rules'); ?> +
    +
    + + + + form->getInput('context'); ?> + + +
    diff --git a/administrator/components/com_fields/tmpl/groups/default.php b/administrator/components/com_fields/tmpl/groups/default.php index ecc9a5bf4b0e2..49c9477e37219 100644 --- a/administrator/components/com_fields/tmpl/groups/default.php +++ b/administrator/components/com_fields/tmpl/groups/default.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -20,7 +22,7 @@ /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $app = Factory::getApplication(); $user = Factory::getUser(); @@ -29,9 +31,8 @@ $component = ''; $parts = FieldsHelper::extract($this->state->get('filter.context')); -if ($parts) -{ - $component = $this->escape($parts[0]); +if ($parts) { + $component = $this->escape($parts[0]); } $listOrder = $this->escape($this->state->get('list.ordering')); @@ -39,10 +40,9 @@ $ordering = ($listOrder == 'a.ordering'); $saveOrder = ($listOrder == 'a.ordering' && strtolower($listDirn) == 'asc'); -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_fields&task=groups.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_fields&task=groups.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $context = $this->escape($this->state->get('filter.context')); @@ -50,140 +50,143 @@ $searchToolsOptions = []; // Only show field contexts filter if there are more than one option -if (count($this->filterForm->getField('context')->options) > 1) -{ - $searchToolsOptions['selectorFieldName'] = 'context'; +if (count($this->filterForm->getField('context')->options) > 1) { + $searchToolsOptions['selectorFieldName'] = 'context'; } ?>
    -
    -
    -
    - $this, 'options' => $searchToolsOptions)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : ?> - - authorise('core.edit', $component . '.fieldgroup.' . $item->id); ?> - authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); ?> - authorise('core.edit.own', $component . '.fieldgroup.' . $item->id) && $item->created_by == $userId; ?> - authorise('core.edit.state', $component . '.fieldgroup.' . $item->id) && $canCheckin; ?> - - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - -
    - id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - - - - - - state, $i, 'groups.', $canChange, 'cb'); ?> - -
    - checked_out) : ?> - editor, $item->checked_out_time, 'groups.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
    - note) : ?> - escape($item->note)); ?> - -
    -
    -
    - escape($item->access_level); ?> - - - - id; ?> -
    +
    +
    +
    + $this, 'options' => $searchToolsOptions)); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : ?> + + authorise('core.edit', $component . '.fieldgroup.' . $item->id); ?> + authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); ?> + authorise('core.edit.own', $component . '.fieldgroup.' . $item->id) && $item->created_by == $userId; ?> + authorise('core.edit.state', $component . '.fieldgroup.' . $item->id) && $canCheckin; ?> + + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + +
    + id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + + + + + state, $i, 'groups.', $canChange, 'cb'); ?> + +
    + checked_out) : ?> + editor, $item->checked_out_time, 'groups.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
    + note) : ?> + escape($item->note)); ?> + +
    +
    +
    + escape($item->access_level); ?> + + + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', $component) - && $user->authorise('core.edit', $component) - && $user->authorise('core.edit.state', $component)) : ?> - Text::_('COM_FIELDS_VIEW_GROUPS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer') - ), - $this->loadTemplate('batch_body') - ); ?> - - - - - -
    -
    -
    + + authorise('core.create', $component) + && $user->authorise('core.edit', $component) + && $user->authorise('core.edit.state', $component) +) : ?> + Text::_('COM_FIELDS_VIEW_GROUPS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer') + ), + $this->loadTemplate('batch_body') + ); ?> + + + + + +
    +
    +
    diff --git a/administrator/components/com_fields/tmpl/groups/default_batch_body.php b/administrator/components/com_fields/tmpl/groups/default_batch_body.php index 9b44ea54a4396..97e7bad090dc2 100644 --- a/administrator/components/com_fields/tmpl/groups/default_batch_body.php +++ b/administrator/components/com_fields/tmpl/groups/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Multilanguage; @@ -13,18 +15,18 @@ ?>
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    diff --git a/administrator/components/com_fields/tmpl/groups/default_batch_footer.php b/administrator/components/com_fields/tmpl/groups/default_batch_footer.php index a0b7215782176..bc7cd3530c4e6 100644 --- a/administrator/components/com_fields/tmpl/groups/default_batch_footer.php +++ b/administrator/components/com_fields/tmpl/groups/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/administrator/components/com_finder/helpers/indexer/adapter.php b/administrator/components/com_finder/helpers/indexer/adapter.php index 02316348db69a..e00a38c6d6fcf 100644 --- a/administrator/components/com_finder/helpers/indexer/adapter.php +++ b/administrator/components/com_finder/helpers/indexer/adapter.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Finder')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Finder')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Finder')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Finder')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Finder')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Finder')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new FinderComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new FinderComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_finder/src/Controller/DisplayController.php b/administrator/components/com_finder/src/Controller/DisplayController.php index aaa94590f34a9..22e6eacbc0b19 100644 --- a/administrator/components/com_finder/src/Controller/DisplayController.php +++ b/administrator/components/com_finder/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'index', 'word'); - $layout = $this->input->get('layout', 'index', 'word'); - $filterId = $this->input->get('filter_id', null, 'int'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean A Controller object to support chaining or false on failure. + * + * @since 2.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', 'index', 'word'); + $layout = $this->input->get('layout', 'index', 'word'); + $filterId = $this->input->get('filter_id', null, 'int'); - if ($view === 'index') - { - $pluginEnabled = PluginHelper::isEnabled('content', 'finder'); + if ($view === 'index') { + $pluginEnabled = PluginHelper::isEnabled('content', 'finder'); - if (!$pluginEnabled) - { - $finderPluginId = FinderHelper::getFinderPluginId(); - $link = HTMLHelper::_( - 'link', - '#plugin' . $finderPluginId . 'Modal', - Text::_('COM_FINDER_CONTENT_PLUGIN'), - 'class="alert-link" data-bs-toggle="modal" id="title-' . $finderPluginId . '"' - ); - $this->app->enqueueMessage(Text::sprintf('COM_FINDER_INDEX_PLUGIN_CONTENT_NOT_ENABLED_LINK', $link), 'warning'); - } - } + if (!$pluginEnabled) { + $finderPluginId = FinderHelper::getFinderPluginId(); + $link = HTMLHelper::_( + 'link', + '#plugin' . $finderPluginId . 'Modal', + Text::_('COM_FINDER_CONTENT_PLUGIN'), + 'class="alert-link" data-bs-toggle="modal" id="title-' . $finderPluginId . '"' + ); + $this->app->enqueueMessage(Text::sprintf('COM_FINDER_INDEX_PLUGIN_CONTENT_NOT_ENABLED_LINK', $link), 'warning'); + } + } - // Check for edit form. - 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'); - } + // Check for edit form. + 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->setRedirect(Route::_('index.php?option=com_finder&view=filters', false)); + $this->setRedirect(Route::_('index.php?option=com_finder&view=filters', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/administrator/components/com_finder/src/Controller/FilterController.php b/administrator/components/com_finder/src/Controller/FilterController.php index bd85d98034368..b3b5085363872 100644 --- a/administrator/components/com_finder/src/Controller/FilterController.php +++ b/administrator/components/com_finder/src/Controller/FilterController.php @@ -1,4 +1,5 @@ checkToken(); - - /** @var \Joomla\Component\Finder\Administrator\Model\FilterModel $model */ - $model = $this->getModel(); - $table = $model->getTable(); - $data = $this->input->post->get('jform', array(), 'array'); - $checkin = $table->hasField('checked_out'); - $context = "$this->option.edit.$this->context"; - $task = $this->getTask(); - - // Determine the name of the primary key for the data. - if (empty($key)) - { - $key = $table->getKeyName(); - } - - // To avoid data collisions the urlVar may be different from the primary key. - if (empty($urlVar)) - { - $urlVar = $key; - } - - $recordId = $this->input->get($urlVar, '', 'int'); - - if (!$this->checkEditId($context, $recordId)) - { - // Somehow the person just went to the form and tried to save it. We don't allow that. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $recordId), 'error'); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); - - return false; - } - - // Populate the row id from the session. - $data[$key] = $recordId; - - // The save2copy task needs to be handled slightly differently. - if ($task === 'save2copy') - { - // Check-in the original row. - if ($checkin && $model->checkin($data[$key]) === false) - { - // Check-in failed. Go back to the item and display a notice. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); - } - - $this->setRedirect('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $urlVar)); - - return false; - } - - // Reset the ID and then treat the request as for Apply. - $data[$key] = 0; - $task = 'apply'; - } - - // Access check. - if (!$this->allowSave($data, $key)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); - - return false; - } - - // Validate the posted data. - // Sometimes the form needs some posted data, such as for plugins and modules. - $form = $model->getForm($data, false); - - if (!$form) - { - $this->app->enqueueMessage($model->getError(), 'error'); - - return false; - } - - // Test whether the data is valid. - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $this->app->setUserState($context . '.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) - ); - - return false; - } - - // Get and sanitize the filter data. - $validData['data'] = $this->input->post->get('t', array(), 'array'); - $validData['data'] = array_unique($validData['data']); - $validData['data'] = ArrayHelper::toInteger($validData['data']); - - // Remove any values of zero. - if (array_search(0, $validData['data'], true)) - { - unset($validData['data'][array_search(0, $validData['data'], true)]); - } - - // Attempt to save the data. - if (!$model->save($validData)) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $validData); - - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) - ); - - return false; - } - - // Save succeeded, so check-in the record. - if ($checkin && $model->checkin($validData[$key]) === false) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $validData); - - // Check-in failed, so go back to the record and display a notice. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); - $this->setRedirect('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key)); - - return false; - } - - $this->setMessage( - Text::_( - ($this->app->getLanguage()->hasKey($this->text_prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS') - ? $this->text_prefix : 'JLIB_APPLICATION') . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS' - ) - ); - - // Redirect the user and adjust session state based on the chosen task. - switch ($task) - { - case 'apply': - // Set the record data in the session. - $recordId = $model->getState($this->context . '.id'); - $this->holdEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - $model->checkout($recordId); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) - ); - - break; - - case 'save2new': - // Clear the record id and data from the session. - $this->releaseEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend(null, $key), false) - ); - - break; - - default: - // Clear the record id and data from the session. - $this->releaseEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - - // Redirect to the list screen. - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false) - ); - - break; - } - - // Invoke the postSave method to allow for the child class to access the model. - $this->postSaveHook($model, $validData); - - return true; - } + /** + * Method to save a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 2.5 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + /** @var \Joomla\Component\Finder\Administrator\Model\FilterModel $model */ + $model = $this->getModel(); + $table = $model->getTable(); + $data = $this->input->post->get('jform', array(), 'array'); + $checkin = $table->hasField('checked_out'); + $context = "$this->option.edit.$this->context"; + $task = $this->getTask(); + + // Determine the name of the primary key for the data. + if (empty($key)) { + $key = $table->getKeyName(); + } + + // To avoid data collisions the urlVar may be different from the primary key. + if (empty($urlVar)) { + $urlVar = $key; + } + + $recordId = $this->input->get($urlVar, '', 'int'); + + if (!$this->checkEditId($context, $recordId)) { + // Somehow the person just went to the form and tried to save it. We don't allow that. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $recordId), 'error'); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); + + return false; + } + + // Populate the row id from the session. + $data[$key] = $recordId; + + // The save2copy task needs to be handled slightly differently. + if ($task === 'save2copy') { + // Check-in the original row. + if ($checkin && $model->checkin($data[$key]) === false) { + // Check-in failed. Go back to the item and display a notice. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); + } + + $this->setRedirect('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $urlVar)); + + return false; + } + + // Reset the ID and then treat the request as for Apply. + $data[$key] = 0; + $task = 'apply'; + } + + // Access check. + if (!$this->allowSave($data, $key)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); + + return false; + } + + // Validate the posted data. + // Sometimes the form needs some posted data, such as for plugins and modules. + $form = $model->getForm($data, false); + + if (!$form) { + $this->app->enqueueMessage($model->getError(), 'error'); + + return false; + } + + // Test whether the data is valid. + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $this->app->setUserState($context . '.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) + ); + + return false; + } + + // Get and sanitize the filter data. + $validData['data'] = $this->input->post->get('t', array(), 'array'); + $validData['data'] = array_unique($validData['data']); + $validData['data'] = ArrayHelper::toInteger($validData['data']); + + // Remove any values of zero. + if (array_search(0, $validData['data'], true)) { + unset($validData['data'][array_search(0, $validData['data'], true)]); + } + + // Attempt to save the data. + if (!$model->save($validData)) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $validData); + + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) + ); + + return false; + } + + // Save succeeded, so check-in the record. + if ($checkin && $model->checkin($validData[$key]) === false) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $validData); + + // Check-in failed, so go back to the record and display a notice. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); + $this->setRedirect('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key)); + + return false; + } + + $this->setMessage( + Text::_( + ($this->app->getLanguage()->hasKey($this->text_prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS') + ? $this->text_prefix : 'JLIB_APPLICATION') . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS' + ) + ); + + // Redirect the user and adjust session state based on the chosen task. + switch ($task) { + case 'apply': + // Set the record data in the session. + $recordId = $model->getState($this->context . '.id'); + $this->holdEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + $model->checkout($recordId); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) + ); + + break; + + case 'save2new': + // Clear the record id and data from the session. + $this->releaseEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend(null, $key), false) + ); + + break; + + default: + // Clear the record id and data from the session. + $this->releaseEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + + // Redirect to the list screen. + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false) + ); + + break; + } + + // Invoke the postSave method to allow for the child class to access the model. + $this->postSaveHook($model, $validData); + + return true; + } } diff --git a/administrator/components/com_finder/src/Controller/FiltersController.php b/administrator/components/com_finder/src/Controller/FiltersController.php index ade78adb5ffd5..40e82d937b9a7 100644 --- a/administrator/components/com_finder/src/Controller/FiltersController.php +++ b/administrator/components/com_finder/src/Controller/FiltersController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 2.5 + */ + public function getModel($name = 'Filter', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_finder/src/Controller/IndexController.php b/administrator/components/com_finder/src/Controller/IndexController.php index 2c620bf8863ac..7d1dc18db2ba4 100644 --- a/administrator/components/com_finder/src/Controller/IndexController.php +++ b/administrator/components/com_finder/src/Controller/IndexController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to optimise the index by removing orphaned entries. - * - * @return boolean True on success. - * - * @since 4.2.0 - */ - public function optimise() - { - $this->checkToken(); - - // Optimise the index by first running the garbage collection - PluginHelper::importPlugin('finder'); - $this->app->triggerEvent('onFinderGarbageCollection'); - - // Now run the optimisation method from the indexer - $indexer = new Indexer; - $indexer->optimize(); - - $message = Text::_('COM_FINDER_INDEX_OPTIMISE_FINISHED'); - $this->setRedirect('index.php?option=com_finder&view=index', $message); - - return true; - } - - /** - * Method to purge all indexed links from the database. - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function purge() - { - $this->checkToken(); - - // Remove the script time limit. - @set_time_limit(0); - - /** @var \Joomla\Component\Finder\Administrator\Model\IndexModel $model */ - $model = $this->getModel('Index', 'Administrator'); - - // Attempt to purge the index. - $return = $model->purge(); - - if (!$return) - { - $message = Text::_('COM_FINDER_INDEX_PURGE_FAILED', $model->getError()); - $this->setRedirect('index.php?option=com_finder&view=index', $message); - - return false; - } - else - { - $message = Text::_('COM_FINDER_INDEX_PURGE_SUCCESS'); - $this->setRedirect('index.php?option=com_finder&view=index', $message); - - return true; - } - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 2.5 + */ + public function getModel($name = 'Index', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to optimise the index by removing orphaned entries. + * + * @return boolean True on success. + * + * @since 4.2.0 + */ + public function optimise() + { + $this->checkToken(); + + // Optimise the index by first running the garbage collection + PluginHelper::importPlugin('finder'); + $this->app->triggerEvent('onFinderGarbageCollection'); + + // Now run the optimisation method from the indexer + $indexer = new Indexer(); + $indexer->optimize(); + + $message = Text::_('COM_FINDER_INDEX_OPTIMISE_FINISHED'); + $this->setRedirect('index.php?option=com_finder&view=index', $message); + + return true; + } + + /** + * Method to purge all indexed links from the database. + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function purge() + { + $this->checkToken(); + + // Remove the script time limit. + @set_time_limit(0); + + /** @var \Joomla\Component\Finder\Administrator\Model\IndexModel $model */ + $model = $this->getModel('Index', 'Administrator'); + + // Attempt to purge the index. + $return = $model->purge(); + + if (!$return) { + $message = Text::_('COM_FINDER_INDEX_PURGE_FAILED', $model->getError()); + $this->setRedirect('index.php?option=com_finder&view=index', $message); + + return false; + } else { + $message = Text::_('COM_FINDER_INDEX_PURGE_SUCCESS'); + $this->setRedirect('index.php?option=com_finder&view=index', $message); + + return true; + } + } } diff --git a/administrator/components/com_finder/src/Controller/IndexerController.php b/administrator/components/com_finder/src/Controller/IndexerController.php index 28ef045c49fdd..d217d4768f9c5 100644 --- a/administrator/components/com_finder/src/Controller/IndexerController.php +++ b/administrator/components/com_finder/src/Controller/IndexerController.php @@ -1,4 +1,5 @@ get('enable_logging', '0')) - { - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'indexer.php'; - Log::addLogger($options); - } - - // Log the start - try - { - Log::add('Starting the indexer', Log::INFO); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - // We don't want this form to be cached. - $this->app->allowCache(false); - - // Put in a buffer to silence noise. - ob_start(); - - // Reset the indexer state. - Indexer::resetState(); - - // Import the finder plugins. - PluginHelper::importPlugin('finder'); - - // Add the indexer language to \JS - Text::script('COM_FINDER_AN_ERROR_HAS_OCCURRED'); - Text::script('COM_FINDER_NO_ERROR_RETURNED'); - - // Start the indexer. - try - { - // Trigger the onStartIndex event. - $this->app->triggerEvent('onStartIndex'); - - // Get the indexer state. - $state = Indexer::getState(); - $state->start = 1; - - // Send the response. - static::sendResponse($state); - } - - // Catch an exception and return the response. - catch (\Exception $e) - { - static::sendResponse($e); - } - } - - /** - * Method to run the next batch of content through the indexer. - * - * @return void - * - * @since 2.5 - */ - public function batch() - { - // Check for a valid token. If invalid, send a 403 with the error message. - if (!Session::checkToken('request')) - { - static::sendResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); - - return; - } - - $params = ComponentHelper::getParams('com_finder'); - - if ($params->get('enable_logging', '0')) - { - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'indexer.php'; - Log::addLogger($options); - } - - // Log the start - try - { - Log::add('Starting the indexer batch process', Log::INFO); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - // We don't want this form to be cached. - $this->app->allowCache(false); - - // Put in a buffer to silence noise. - ob_start(); - - // Remove the script time limit. - @set_time_limit(0); - - // Get the indexer state. - $state = Indexer::getState(); - - // Reset the batch offset. - $state->batchOffset = 0; - - // Update the indexer state. - Indexer::setState($state); - - // Import the finder plugins. - PluginHelper::importPlugin('finder'); - - /* - * We are going to swap out the raw document object with an HTML document - * in order to work around some plugins that don't do proper environment - * checks before trying to use HTML document functions. - */ - $lang = Factory::getLanguage(); - - // Get the document properties. - $attributes = array ( - 'charset' => 'utf-8', - 'lineend' => 'unix', - 'tab' => ' ', - 'language' => $lang->getTag(), - 'direction' => $lang->isRtl() ? 'rtl' : 'ltr' - ); - - // Start the indexer. - try - { - // Trigger the onBeforeIndex event. - $this->app->triggerEvent('onBeforeIndex'); - - // Trigger the onBuildIndex event. - $this->app->triggerEvent('onBuildIndex'); - - // Get the indexer state. - $state = Indexer::getState(); - $state->start = 0; - $state->complete = 0; - - // Log batch completion and memory high-water mark. - try - { - Log::add('Batch completed, peak memory usage: ' . number_format(memory_get_peak_usage(true)) . ' bytes', Log::INFO); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - // Send the response. - static::sendResponse($state); - } - - // Catch an exception and return the response. - catch (\Exception $e) - { - // Send the response. - static::sendResponse($e); - } - } - - /** - * Method to optimize the index and perform any necessary cleanup. - * - * @return void - * - * @since 2.5 - */ - public function optimize() - { - // Check for a valid token. If invalid, send a 403 with the error message. - if (!Session::checkToken('request')) - { - static::sendResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); - - return; - } - - // We don't want this form to be cached. - $this->app->allowCache(false); - - // Put in a buffer to silence noise. - ob_start(); - - // Import the finder plugins. - PluginHelper::importPlugin('finder'); - - try - { - // Optimize the index - $indexer = new Indexer; - $indexer->optimize(); - - // Get the indexer state. - $state = Indexer::getState(); - $state->start = 0; - $state->complete = 1; - - // Send the response. - static::sendResponse($state); - } - - // Catch an exception and return the response. - catch (\Exception $e) - { - static::sendResponse($e); - } - } - - /** - * Method to handle a send a \JSON response. The body parameter - * can be an \Exception object for when an error has occurred or - * a CMSObject for a good response. - * - * @param \Joomla\CMS\Object\CMSObject|\Exception $data CMSObject on success, \Exception on error. [optional] - * - * @return void - * - * @since 2.5 - */ - public static function sendResponse($data = null) - { - $app = Factory::getApplication(); - - $params = ComponentHelper::getParams('com_finder'); - - if ($params->get('enable_logging', '0')) - { - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'indexer.php'; - Log::addLogger($options); - } - - // Send the assigned error code if we are catching an exception. - if ($data instanceof \Exception) - { - try - { - Log::add($data->getMessage(), Log::ERROR); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - $app->setHeader('status', $data->getCode()); - } - - // Create the response object. - $response = new Response($data); - - if (\JDEBUG) - { - // Add the buffer and memory usage - $response->buffer = ob_get_contents(); - $response->memory = memory_get_usage(true); - } - - // Send the JSON response. - echo json_encode($response); - } + /** + * Method to start the indexer. + * + * @return void + * + * @since 2.5 + */ + public function start() + { + // Check for a valid token. If invalid, send a 403 with the error message. + if (!Session::checkToken('request')) { + static::sendResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); + + return; + } + + $params = ComponentHelper::getParams('com_finder'); + + if ($params->get('enable_logging', '0')) { + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'indexer.php'; + Log::addLogger($options); + } + + // Log the start + try { + Log::add('Starting the indexer', Log::INFO); + } catch (\RuntimeException $exception) { + // Informational log only + } + + // We don't want this form to be cached. + $this->app->allowCache(false); + + // Put in a buffer to silence noise. + ob_start(); + + // Reset the indexer state. + Indexer::resetState(); + + // Import the finder plugins. + PluginHelper::importPlugin('finder'); + + // Add the indexer language to \JS + Text::script('COM_FINDER_AN_ERROR_HAS_OCCURRED'); + Text::script('COM_FINDER_NO_ERROR_RETURNED'); + + // Start the indexer. + try { + // Trigger the onStartIndex event. + $this->app->triggerEvent('onStartIndex'); + + // Get the indexer state. + $state = Indexer::getState(); + $state->start = 1; + + // Send the response. + static::sendResponse($state); + } + + // Catch an exception and return the response. + catch (\Exception $e) { + static::sendResponse($e); + } + } + + /** + * Method to run the next batch of content through the indexer. + * + * @return void + * + * @since 2.5 + */ + public function batch() + { + // Check for a valid token. If invalid, send a 403 with the error message. + if (!Session::checkToken('request')) { + static::sendResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); + + return; + } + + $params = ComponentHelper::getParams('com_finder'); + + if ($params->get('enable_logging', '0')) { + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'indexer.php'; + Log::addLogger($options); + } + + // Log the start + try { + Log::add('Starting the indexer batch process', Log::INFO); + } catch (\RuntimeException $exception) { + // Informational log only + } + + // We don't want this form to be cached. + $this->app->allowCache(false); + + // Put in a buffer to silence noise. + ob_start(); + + // Remove the script time limit. + @set_time_limit(0); + + // Get the indexer state. + $state = Indexer::getState(); + + // Reset the batch offset. + $state->batchOffset = 0; + + // Update the indexer state. + Indexer::setState($state); + + // Import the finder plugins. + PluginHelper::importPlugin('finder'); + + /* + * We are going to swap out the raw document object with an HTML document + * in order to work around some plugins that don't do proper environment + * checks before trying to use HTML document functions. + */ + $lang = Factory::getLanguage(); + + // Get the document properties. + $attributes = array ( + 'charset' => 'utf-8', + 'lineend' => 'unix', + 'tab' => ' ', + 'language' => $lang->getTag(), + 'direction' => $lang->isRtl() ? 'rtl' : 'ltr' + ); + + // Start the indexer. + try { + // Trigger the onBeforeIndex event. + $this->app->triggerEvent('onBeforeIndex'); + + // Trigger the onBuildIndex event. + $this->app->triggerEvent('onBuildIndex'); + + // Get the indexer state. + $state = Indexer::getState(); + $state->start = 0; + $state->complete = 0; + + // Log batch completion and memory high-water mark. + try { + Log::add('Batch completed, peak memory usage: ' . number_format(memory_get_peak_usage(true)) . ' bytes', Log::INFO); + } catch (\RuntimeException $exception) { + // Informational log only + } + + // Send the response. + static::sendResponse($state); + } + + // Catch an exception and return the response. + catch (\Exception $e) { + // Send the response. + static::sendResponse($e); + } + } + + /** + * Method to optimize the index and perform any necessary cleanup. + * + * @return void + * + * @since 2.5 + */ + public function optimize() + { + // Check for a valid token. If invalid, send a 403 with the error message. + if (!Session::checkToken('request')) { + static::sendResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); + + return; + } + + // We don't want this form to be cached. + $this->app->allowCache(false); + + // Put in a buffer to silence noise. + ob_start(); + + // Import the finder plugins. + PluginHelper::importPlugin('finder'); + + try { + // Optimize the index + $indexer = new Indexer(); + $indexer->optimize(); + + // Get the indexer state. + $state = Indexer::getState(); + $state->start = 0; + $state->complete = 1; + + // Send the response. + static::sendResponse($state); + } + + // Catch an exception and return the response. + catch (\Exception $e) { + static::sendResponse($e); + } + } + + /** + * Method to handle a send a \JSON response. The body parameter + * can be an \Exception object for when an error has occurred or + * a CMSObject for a good response. + * + * @param \Joomla\CMS\Object\CMSObject|\Exception $data CMSObject on success, \Exception on error. [optional] + * + * @return void + * + * @since 2.5 + */ + public static function sendResponse($data = null) + { + $app = Factory::getApplication(); + + $params = ComponentHelper::getParams('com_finder'); + + if ($params->get('enable_logging', '0')) { + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'indexer.php'; + Log::addLogger($options); + } + + // Send the assigned error code if we are catching an exception. + if ($data instanceof \Exception) { + try { + Log::add($data->getMessage(), Log::ERROR); + } catch (\RuntimeException $exception) { + // Informational log only + } + + $app->setHeader('status', $data->getCode()); + } + + // Create the response object. + $response = new Response($data); + + if (\JDEBUG) { + // Add the buffer and memory usage + $response->buffer = ob_get_contents(); + $response->memory = memory_get_usage(true); + } + + // Send the JSON response. + echo json_encode($response); + } } diff --git a/administrator/components/com_finder/src/Controller/MapsController.php b/administrator/components/com_finder/src/Controller/MapsController.php index 149520edb4066..44269e33c4912 100644 --- a/administrator/components/com_finder/src/Controller/MapsController.php +++ b/administrator/components/com_finder/src/Controller/MapsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Maps', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_finder/src/Controller/SearchesController.php b/administrator/components/com_finder/src/Controller/SearchesController.php index 325893b9eede0..c976644231116 100644 --- a/administrator/components/com_finder/src/Controller/SearchesController.php +++ b/administrator/components/com_finder/src/Controller/SearchesController.php @@ -1,4 +1,5 @@ getModel('Searches'); - - if (!$model->reset()) - { - $this->app->enqueueMessage($model->getError(), 'error'); - } - - $this->setRedirect('index.php?option=com_finder&view=searches'); - } + /** + * Method to reset the search log table. + * + * @return void + */ + public function reset() + { + // Check for request forgeries. + Session::checkToken() or jexit(Text::_('JINVALID_TOKEN')); + + $model = $this->getModel('Searches'); + + if (!$model->reset()) { + $this->app->enqueueMessage($model->getError(), 'error'); + } + + $this->setRedirect('index.php?option=com_finder&view=searches'); + } } diff --git a/administrator/components/com_finder/src/Extension/FinderComponent.php b/administrator/components/com_finder/src/Extension/FinderComponent.php index 65944b880778a..5dfea81a201f6 100644 --- a/administrator/components/com_finder/src/Extension/FinderComponent.php +++ b/administrator/components/com_finder/src/Extension/FinderComponent.php @@ -1,4 +1,5 @@ setDatabase($container->get(DatabaseInterface::class)); - - $this->getRegistry()->register('finder', $finder); - - $filter = new Filter; - $filter->setDatabase($container->get(DatabaseInterface::class)); - - $this->getRegistry()->register('filter', $filter); - - $this->getRegistry()->register('query', new Query); - } + use RouterServiceTrait; + use HTMLRegistryAwareTrait; + + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $finder = new Finder(); + $finder->setDatabase($container->get(DatabaseInterface::class)); + + $this->getRegistry()->register('finder', $finder); + + $filter = new Filter(); + $filter->setDatabase($container->get(DatabaseInterface::class)); + + $this->getRegistry()->register('filter', $filter); + + $this->getRegistry()->register('query', new Query()); + } } diff --git a/administrator/components/com_finder/src/Field/BranchesField.php b/administrator/components/com_finder/src/Field/BranchesField.php index 6d3994ff76b79..46f7769c1537e 100644 --- a/administrator/components/com_finder/src/Field/BranchesField.php +++ b/administrator/components/com_finder/src/Field/BranchesField.php @@ -1,4 +1,5 @@ bootComponent('com_finder'); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.5 + */ + public function getOptions() + { + Factory::getApplication()->bootComponent('com_finder'); - return HTMLHelper::_('finder.mapslist'); - } + return HTMLHelper::_('finder.mapslist'); + } } diff --git a/administrator/components/com_finder/src/Field/ContentmapField.php b/administrator/components/com_finder/src/Field/ContentmapField.php index 8a71a3f710f43..884802e87fea7 100644 --- a/administrator/components/com_finder/src/Field/ContentmapField.php +++ b/administrator/components/com_finder/src/Field/ContentmapField.php @@ -1,4 +1,5 @@ getDatabase(); - - // Main query. - $query = $db->getQuery(true) - ->select($db->quoteName('a.title', 'text')) - ->select($db->quoteName('a.id', 'value')) - ->select($db->quoteName('a.parent_id')) - ->select($db->quoteName('a.level')) - ->from($db->quoteName('#__finder_taxonomy', 'a')) - ->where($db->quoteName('a.parent_id') . ' <> 0') - ->order('a.title ASC'); - - $db->setQuery($query); - - try - { - $contentMap = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - return []; - } - - // Build the grouped list array. - if ($contentMap) - { - $parents = []; - - foreach ($contentMap as $item) - { - if (!isset($parents[$item->parent_id])) - { - $parents[$item->parent_id] = []; - } - - $parents[$item->parent_id][] = $item; - } - - foreach ($parents[1] as $branch) - { - $groups[$branch->text] = $this->prepareLevel($branch->value, $parents); - } - } - - // Merge any additional groups in the XML definition. - $groups = array_merge(parent::getGroups(), $groups); - - return $groups; - } - - /** - * Indenting and translating options for the list - * - * @param int $parent Parent ID to process - * @param array $parents Array of arrays of items with parent IDs as keys - * - * @return array The indented list of entries for this branch - * - * @since 4.1.5 - */ - private function prepareLevel($parent, $parents) - { - $lang = Factory::getLanguage(); - $entries = []; - - foreach ($parents[$parent] as $item) - { - $levelPrefix = str_repeat('- ', $item->level - 1); - - if (trim($item->text, '*') === 'Language') - { - $text = LanguageHelper::branchLanguageTitle($item->text); - } - else - { - $key = LanguageHelper::branchSingular($item->text); - $text = $lang->hasKey($key) ? Text::_($key) : $item->text; - } - - $entries[] = HTMLHelper::_('select.option', $item->value, $levelPrefix . $text); - - if (isset($parents[$item->value])) - { - $entries = array_merge($entries, $this->prepareLevel($item->value, $parents)); - } - } - - return $entries; - } + /** + * The form field type. + * + * @var string + * @since 3.6.0 + */ + public $type = 'ContentMap'; + + /** + * Method to get the list of content map options grouped by first level. + * + * @return array The field option objects as a nested array in groups. + * + * @since 3.6.0 + */ + protected function getGroups() + { + $groups = array(); + + // Get the database object and a new query object. + $db = $this->getDatabase(); + + // Main query. + $query = $db->getQuery(true) + ->select($db->quoteName('a.title', 'text')) + ->select($db->quoteName('a.id', 'value')) + ->select($db->quoteName('a.parent_id')) + ->select($db->quoteName('a.level')) + ->from($db->quoteName('#__finder_taxonomy', 'a')) + ->where($db->quoteName('a.parent_id') . ' <> 0') + ->order('a.title ASC'); + + $db->setQuery($query); + + try { + $contentMap = $db->loadObjectList(); + } catch (\RuntimeException $e) { + return []; + } + + // Build the grouped list array. + if ($contentMap) { + $parents = []; + + foreach ($contentMap as $item) { + if (!isset($parents[$item->parent_id])) { + $parents[$item->parent_id] = []; + } + + $parents[$item->parent_id][] = $item; + } + + foreach ($parents[1] as $branch) { + $groups[$branch->text] = $this->prepareLevel($branch->value, $parents); + } + } + + // Merge any additional groups in the XML definition. + $groups = array_merge(parent::getGroups(), $groups); + + return $groups; + } + + /** + * Indenting and translating options for the list + * + * @param int $parent Parent ID to process + * @param array $parents Array of arrays of items with parent IDs as keys + * + * @return array The indented list of entries for this branch + * + * @since 4.1.5 + */ + private function prepareLevel($parent, $parents) + { + $lang = Factory::getLanguage(); + $entries = []; + + foreach ($parents[$parent] as $item) { + $levelPrefix = str_repeat('- ', $item->level - 1); + + if (trim($item->text, '*') === 'Language') { + $text = LanguageHelper::branchLanguageTitle($item->text); + } else { + $key = LanguageHelper::branchSingular($item->text); + $text = $lang->hasKey($key) ? Text::_($key) : $item->text; + } + + $entries[] = HTMLHelper::_('select.option', $item->value, $levelPrefix . $text); + + if (isset($parents[$item->value])) { + $entries = array_merge($entries, $this->prepareLevel($item->value, $parents)); + } + } + + return $entries; + } } diff --git a/administrator/components/com_finder/src/Field/ContenttypesField.php b/administrator/components/com_finder/src/Field/ContenttypesField.php index cefd638be56c2..0ea469c8acbaa 100644 --- a/administrator/components/com_finder/src/Field/ContenttypesField.php +++ b/administrator/components/com_finder/src/Field/ContenttypesField.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('id', 'value')) - ->select($db->quoteName('title', 'text')) - ->from($db->quoteName('#__finder_types')); + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('id', 'value')) + ->select($db->quoteName('title', 'text')) + ->from($db->quoteName('#__finder_types')); - // Get the options. - $db->setQuery($query); + // Get the options. + $db->setQuery($query); - try - { - $contentTypes = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } + try { + $contentTypes = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } - // Translate. - foreach ($contentTypes as $contentType) - { - $key = LanguageHelper::branchSingular($contentType->text); - $contentType->translatedText = $lang->hasKey($key) ? Text::_($key) : $contentType->text; - } + // Translate. + foreach ($contentTypes as $contentType) { + $key = LanguageHelper::branchSingular($contentType->text); + $contentType->translatedText = $lang->hasKey($key) ? Text::_($key) : $contentType->text; + } - // Order by title. - $contentTypes = ArrayHelper::sortObjects($contentTypes, 'translatedText', 1, true, true); + // Order by title. + $contentTypes = ArrayHelper::sortObjects($contentTypes, 'translatedText', 1, true, true); - // Convert the values to options. - foreach ($contentTypes as $contentType) - { - $options[] = HTMLHelper::_('select.option', $contentType->value, $contentType->translatedText); - } + // Convert the values to options. + foreach ($contentTypes as $contentType) { + $options[] = HTMLHelper::_('select.option', $contentType->value, $contentType->translatedText); + } - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); - return $options; - } + return $options; + } } diff --git a/administrator/components/com_finder/src/Field/SearchfilterField.php b/administrator/components/com_finder/src/Field/SearchfilterField.php index d784b75cc1f7d..e86a2e253c60b 100644 --- a/administrator/components/com_finder/src/Field/SearchfilterField.php +++ b/administrator/components/com_finder/src/Field/SearchfilterField.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select('f.title AS text, f.filter_id AS value') - ->from($db->quoteName('#__finder_filters') . ' AS f') - ->where('f.state = 1') - ->order('f.title ASC'); - $db->setQuery($query); - $options = $db->loadObjectList(); - - array_unshift($options, HTMLHelper::_('select.option', '', Text::_('COM_FINDER_SELECT_SEARCH_FILTER'), 'value', 'text')); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 2.5 + */ + protected $type = 'SearchFilter'; + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 2.5 + */ + public function getOptions() + { + // Build the query. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('f.title AS text, f.filter_id AS value') + ->from($db->quoteName('#__finder_filters') . ' AS f') + ->where('f.state = 1') + ->order('f.title ASC'); + $db->setQuery($query); + $options = $db->loadObjectList(); + + array_unshift($options, HTMLHelper::_('select.option', '', Text::_('COM_FINDER_SELECT_SEARCH_FILTER'), 'value', 'text')); + + return $options; + } } diff --git a/administrator/components/com_finder/src/Helper/FinderHelper.php b/administrator/components/com_finder/src/Helper/FinderHelper.php index ca51836df7381..2e263268d0fc6 100644 --- a/administrator/components/com_finder/src/Helper/FinderHelper.php +++ b/administrator/components/com_finder/src/Helper/FinderHelper.php @@ -1,4 +1,5 @@ extension_id : 0; - } + return $pluginRecord !== null ? $pluginRecord->extension_id : 0; + } } diff --git a/administrator/components/com_finder/src/Helper/LanguageHelper.php b/administrator/components/com_finder/src/Helper/LanguageHelper.php index a6e5edf8e3ab7..86ea0cec7f674 100644 --- a/administrator/components/com_finder/src/Helper/LanguageHelper.php +++ b/administrator/components/com_finder/src/Helper/LanguageHelper.php @@ -1,4 +1,5 @@ getLanguage(); - - if ($language->hasKey('PLG_FINDER_QUERY_FILTER_BRANCH_S_' . $return) || JDEBUG) - { - return 'PLG_FINDER_QUERY_FILTER_BRANCH_S_' . $return; - } - - return $branchName; - } - - /** - * Method to return the language name for a language taxonomy branch. - * - * @param string $branchName Language branch name. - * - * @return string The language title. - * - * @since 3.6.0 - */ - public static function branchLanguageTitle($branchName) - { - $title = $branchName; - - if ($branchName === '*') - { - $title = Text::_('JALL_LANGUAGE'); - } - else - { - $languages = CMSLanguageHelper::getLanguages('lang_code'); - - if (isset($languages[$branchName])) - { - $title = $languages[$branchName]->title; - } - } - - return $title; - } - - /** - * Method to load Smart Search component language file. - * - * @return void - * - * @since 2.5 - */ - public static function loadComponentLanguage() - { - Factory::getLanguage()->load('com_finder', JPATH_SITE); - } - - /** - * Method to load Smart Search plugin language files. - * - * @return void - * - * @since 2.5 - */ - public static function loadPluginLanguage() - { - static $loaded = false; - - // If already loaded, don't load again. - if ($loaded) - { - return; - } - - $loaded = true; - - // Get array of all the enabled Smart Search plugin names. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select(array($db->quoteName('name'), $db->quoteName('element'))) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('finder')) - ->where($db->quoteName('enabled') . ' = 1'); - $db->setQuery($query); - $plugins = $db->loadObjectList(); - - if (empty($plugins)) - { - return; - } - - // Load generic language strings. - $lang = Factory::getLanguage(); - $lang->load('plg_content_finder', JPATH_ADMINISTRATOR); - - // Load language file for each plugin. - foreach ($plugins as $plugin) - { - $lang->load($plugin->name, JPATH_ADMINISTRATOR) - || $lang->load($plugin->name, JPATH_PLUGINS . '/finder/' . $plugin->element); - } - } + /** + * Method to return a plural language code for a taxonomy branch. + * + * @param string $branchName Branch title. + * + * @return string Language key code. + * + * @since 2.5 + */ + public static function branchPlural($branchName) + { + $return = preg_replace('/[^a-zA-Z0-9]+/', '_', strtoupper($branchName)); + + if ($return !== '_') { + return 'PLG_FINDER_QUERY_FILTER_BRANCH_P_' . $return; + } + + return $branchName; + } + + /** + * Method to return a singular language code for a taxonomy branch. + * + * @param string $branchName Branch name. + * + * @return string Language key code. + * + * @since 2.5 + */ + public static function branchSingular($branchName) + { + $return = preg_replace('/[^a-zA-Z0-9]+/', '_', strtoupper($branchName)); + $language = Factory::getApplication()->getLanguage(); + + if ($language->hasKey('PLG_FINDER_QUERY_FILTER_BRANCH_S_' . $return) || JDEBUG) { + return 'PLG_FINDER_QUERY_FILTER_BRANCH_S_' . $return; + } + + return $branchName; + } + + /** + * Method to return the language name for a language taxonomy branch. + * + * @param string $branchName Language branch name. + * + * @return string The language title. + * + * @since 3.6.0 + */ + public static function branchLanguageTitle($branchName) + { + $title = $branchName; + + if ($branchName === '*') { + $title = Text::_('JALL_LANGUAGE'); + } else { + $languages = CMSLanguageHelper::getLanguages('lang_code'); + + if (isset($languages[$branchName])) { + $title = $languages[$branchName]->title; + } + } + + return $title; + } + + /** + * Method to load Smart Search component language file. + * + * @return void + * + * @since 2.5 + */ + public static function loadComponentLanguage() + { + Factory::getLanguage()->load('com_finder', JPATH_SITE); + } + + /** + * Method to load Smart Search plugin language files. + * + * @return void + * + * @since 2.5 + */ + public static function loadPluginLanguage() + { + static $loaded = false; + + // If already loaded, don't load again. + if ($loaded) { + return; + } + + $loaded = true; + + // Get array of all the enabled Smart Search plugin names. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select(array($db->quoteName('name'), $db->quoteName('element'))) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('finder')) + ->where($db->quoteName('enabled') . ' = 1'); + $db->setQuery($query); + $plugins = $db->loadObjectList(); + + if (empty($plugins)) { + return; + } + + // Load generic language strings. + $lang = Factory::getLanguage(); + $lang->load('plg_content_finder', JPATH_ADMINISTRATOR); + + // Load language file for each plugin. + foreach ($plugins as $plugin) { + $lang->load($plugin->name, JPATH_ADMINISTRATOR) + || $lang->load($plugin->name, JPATH_PLUGINS . '/finder/' . $plugin->element); + } + } } diff --git a/administrator/components/com_finder/src/Indexer/Adapter.php b/administrator/components/com_finder/src/Indexer/Adapter.php index 0d7d9b341bbe9..16b9d6b56c466 100644 --- a/administrator/components/com_finder/src/Indexer/Adapter.php +++ b/administrator/components/com_finder/src/Indexer/Adapter.php @@ -1,4 +1,5 @@ type_id = $this->getTypeId(); - - // Add the content type if it doesn't exist and is set. - if (empty($this->type_id) && !empty($this->type_title)) - { - $this->type_id = Helper::addContentType($this->type_title, $this->mime); - } - - // Check for a layout override. - if ($this->params->get('layout')) - { - $this->layout = $this->params->get('layout'); - } - - // Get the indexer object - $this->indexer = new Indexer($this->db); - } - - /** - * Method to get the adapter state and push it into the indexer. - * - * @return void - * - * @since 2.5 - * @throws Exception on error. - */ - public function onStartIndex() - { - // Get the indexer state. - $iState = Indexer::getState(); - - // Get the number of content items. - $total = (int) $this->getContentCount(); - - // Add the content count to the total number of items. - $iState->totalItems += $total; - - // Populate the indexer state information for the adapter. - $iState->pluginState[$this->context]['total'] = $total; - $iState->pluginState[$this->context]['offset'] = 0; - - // Set the indexer state. - Indexer::setState($iState); - } - - /** - * Method to prepare for the indexer to be run. This method will often - * be used to include dependencies and things of that nature. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on error. - */ - public function onBeforeIndex() - { - // Get the indexer and adapter state. - $iState = Indexer::getState(); - $aState = $iState->pluginState[$this->context]; - - // Check the progress of the indexer and the adapter. - if ($iState->batchOffset == $iState->batchSize || $aState['offset'] == $aState['total']) - { - return true; - } - - // Run the setup method. - return $this->setup(); - } - - /** - * Method to index a batch of content items. This method can be called by - * the indexer many times throughout the indexing process depending on how - * much content is available for indexing. It is important to track the - * progress correctly so we can display it to the user. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on error. - */ - public function onBuildIndex() - { - // Get the indexer and adapter state. - $iState = Indexer::getState(); - $aState = $iState->pluginState[$this->context]; - - // Check the progress of the indexer and the adapter. - if ($iState->batchOffset == $iState->batchSize || $aState['offset'] == $aState['total']) - { - return true; - } - - // Get the batch offset and size. - $offset = (int) $aState['offset']; - $limit = (int) ($iState->batchSize - $iState->batchOffset); - - // Get the content items to index. - $items = $this->getItems($offset, $limit); - - // Iterate through the items and index them. - for ($i = 0, $n = count($items); $i < $n; $i++) - { - // Index the item. - $this->index($items[$i]); - - // Adjust the offsets. - $offset++; - $iState->batchOffset++; - $iState->totalItems--; - } - - // Update the indexer state. - $aState['offset'] = $offset; - $iState->pluginState[$this->context] = $aState; - Indexer::setState($iState); - - return true; - } - - /** - * Method to remove outdated index entries - * - * @return integer - * - * @since 4.2.0 - */ - public function onFinderGarbageCollection() - { - $db = $this->db; - $type_id = $this->getTypeId(); - - $query = $db->getQuery(true); - $subquery = $db->getQuery(true); - $subquery->select('CONCAT(' . $db->quote($this->getUrl('', $this->extension, $this->layout)) . ', id)') - ->from($db->quoteName($this->table)); - $query->select($db->quoteName('l.link_id')) - ->from($db->quoteName('#__finder_links', 'l')) - ->where($db->quoteName('l.type_id') . ' = ' . $type_id) - ->where($db->quoteName('l.url') . ' LIKE ' . $db->quote($this->getUrl('%', $this->extension, $this->layout))) - ->where($db->quoteName('l.url') . ' NOT IN (' . $subquery . ')'); - $db->setQuery($query); - $items = $db->loadColumn(); - - foreach ($items as $item) - { - $this->indexer->remove($item); - } - - return count($items); - } - - /** - * Method to change the value of a content item's property in the links - * table. This is used to synchronize published and access states that - * are changed when not editing an item directly. - * - * @param string $id The ID of the item to change. - * @param string $property The property that is being changed. - * @param integer $value The new value of that property. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function change($id, $property, $value) - { - // Check for a property we know how to handle. - if ($property !== 'state' && $property !== 'access') - { - return true; - } - - // Get the URL for the content id. - $item = $this->db->quote($this->getUrl($id, $this->extension, $this->layout)); - - // Update the content items. - $query = $this->db->getQuery(true) - ->update($this->db->quoteName('#__finder_links')) - ->set($this->db->quoteName($property) . ' = ' . (int) $value) - ->where($this->db->quoteName('url') . ' = ' . $item); - $this->db->setQuery($query); - $this->db->execute(); - - return true; - } - - /** - * Method to index an item. - * - * @param Result $item The item to index as a Result object. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - abstract protected function index(Result $item); - - /** - * Method to reindex an item. - * - * @param integer $id The ID of the item to reindex. - * - * @return void - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function reindex($id) - { - // Run the setup method. - $this->setup(); - - // Remove the old item. - $this->remove($id, false); - - // Get the item. - $item = $this->getItem($id); - - // Index the item. - $this->index($item); - - Taxonomy::removeOrphanNodes(); - } - - /** - * Method to remove an item from the index. - * - * @param string $id The ID of the item to remove. - * @param bool $removeTaxonomies Remove empty taxonomies - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function remove($id, $removeTaxonomies = true) - { - // Get the item's URL - $url = $this->db->quote($this->getUrl($id, $this->extension, $this->layout)); - - // Get the link ids for the content items. - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('link_id')) - ->from($this->db->quoteName('#__finder_links')) - ->where($this->db->quoteName('url') . ' = ' . $url); - $this->db->setQuery($query); - $items = $this->db->loadColumn(); - - // Check the items. - if (empty($items)) - { - Factory::getApplication()->triggerEvent('onFinderIndexAfterDelete', array($id)); - - return true; - } - - // Remove the items. - foreach ($items as $item) - { - $this->indexer->remove($item, $removeTaxonomies); - } - - return true; - } - - /** - * Method to setup the adapter before indexing. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - * @throws Exception on database error. - */ - abstract protected function setup(); - - /** - * Method to update index data on category access level changes - * - * @param Table $row A Table object - * - * @return void - * - * @since 2.5 - */ - protected function categoryAccessChange($row) - { - $query = clone $this->getStateQuery(); - $query->where('c.id = ' . (int) $row->id); - - // Get the access level. - $this->db->setQuery($query); - $items = $this->db->loadObjectList(); - - // Adjust the access level for each item within the category. - foreach ($items as $item) - { - // Set the access level. - $temp = max($item->access, $row->access); - - // Update the item. - $this->change((int) $item->id, 'access', $temp); - } - } - - /** - * Method to update index data on category access level changes - * - * @param array $pks A list of primary key ids of the content that has changed state. - * @param integer $value The value of the state that the content has been changed to. - * - * @return void - * - * @since 2.5 - */ - protected function categoryStateChange($pks, $value) - { - /* - * The item's published state is tied to the category - * published state so we need to look up all published states - * before we change anything. - */ - foreach ($pks as $pk) - { - $query = clone $this->getStateQuery(); - $query->where('c.id = ' . (int) $pk); - - // Get the published states. - $this->db->setQuery($query); - $items = $this->db->loadObjectList(); - - // Adjust the state for each item within the category. - foreach ($items as $item) - { - // Translate the state. - $temp = $this->translateState($item->state, $value); - - // Update the item. - $this->change($item->id, 'state', $temp); - } - } - } - - /** - * Method to check the existing access level for categories - * - * @param Table $row A Table object - * - * @return void - * - * @since 2.5 - */ - protected function checkCategoryAccess($row) - { - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('access')) - ->from($this->db->quoteName('#__categories')) - ->where($this->db->quoteName('id') . ' = ' . (int) $row->id); - $this->db->setQuery($query); - - // Store the access level to determine if it changes - $this->old_cataccess = $this->db->loadResult(); - } - - /** - * Method to check the existing access level for items - * - * @param Table $row A Table object - * - * @return void - * - * @since 2.5 - */ - protected function checkItemAccess($row) - { - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('access')) - ->from($this->db->quoteName($this->table)) - ->where($this->db->quoteName('id') . ' = ' . (int) $row->id); - $this->db->setQuery($query); - - // Store the access level to determine if it changes - $this->old_access = $this->db->loadResult(); - } - - /** - * Method to get the number of content items available to index. - * - * @return integer The number of content items available to index. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getContentCount() - { - $return = 0; - - // Get the list query. - $query = $this->getListQuery(); - - // Check if the query is valid. - if (empty($query)) - { - return $return; - } - - // Tweak the SQL query to make the total lookup faster. - if ($query instanceof QueryInterface) - { - $query = clone $query; - $query->clear('select') - ->select('COUNT(*)') - ->clear('order'); - } - - // Get the total number of content items to index. - $this->db->setQuery($query); - - return (int) $this->db->loadResult(); - } - - /** - * Method to get a content item to index. - * - * @param integer $id The id of the content item. - * - * @return Result A Result object. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getItem($id) - { - // Get the list query and add the extra WHERE clause. - $query = $this->getListQuery(); - $query->where('a.id = ' . (int) $id); - - // Get the item to index. - $this->db->setQuery($query); - $item = $this->db->loadAssoc(); - - // Convert the item to a result object. - $item = ArrayHelper::toObject((array) $item, Result::class); - - // Set the item type. - $item->type_id = $this->type_id; - - // Set the item layout. - $item->layout = $this->layout; - - return $item; - } - - /** - * Method to get a list of content items to index. - * - * @param integer $offset The list offset. - * @param integer $limit The list limit. - * @param QueryInterface $query A QueryInterface object. [optional] - * - * @return Result[] An array of Result objects. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getItems($offset, $limit, $query = null) - { - // Get the content items to index. - $this->db->setQuery($this->getListQuery($query)->setLimit($limit, $offset)); - $items = $this->db->loadAssocList(); - - foreach ($items as &$item) - { - $item = ArrayHelper::toObject($item, Result::class); - - // Set the item type. - $item->type_id = $this->type_id; - - // Set the mime type. - $item->mime = $this->mime; - - // Set the item layout. - $item->layout = $this->layout; - } - - return $items; - } - - /** - * Method to get the SQL query used to retrieve the list of content items. - * - * @param mixed $query A QueryInterface object. [optional] - * - * @return QueryInterface A database object. - * - * @since 2.5 - */ - protected function getListQuery($query = null) - { - // Check if we can use the supplied SQL query. - return $query instanceof QueryInterface ? $query : $this->db->getQuery(true); - } - - /** - * Method to get the plugin type - * - * @param integer $id The plugin ID - * - * @return string The plugin type - * - * @since 2.5 - */ - protected function getPluginType($id) - { - // Prepare the query - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('element')) - ->from($this->db->quoteName('#__extensions')) - ->where($this->db->quoteName('extension_id') . ' = ' . (int) $id); - $this->db->setQuery($query); - - return $this->db->loadResult(); - } - - /** - * Method to get a SQL query to load the published and access states for - * an article and category. - * - * @return QueryInterface A database object. - * - * @since 2.5 - */ - protected function getStateQuery() - { - $query = $this->db->getQuery(true); - - // Item ID - $query->select('a.id'); - - // Item and category published state - $query->select('a.' . $this->state_field . ' AS state, c.published AS cat_state'); - - // Item and category access levels - $query->select('a.access, c.access AS cat_access') - ->from($this->table . ' AS a') - ->join('LEFT', '#__categories AS c ON c.id = a.catid'); - - return $query; - } - - /** - * Method to get the query clause for getting items to update by time. - * - * @param string $time The modified timestamp. - * - * @return QueryInterface A database object. - * - * @since 2.5 - */ - protected function getUpdateQueryByTime($time) - { - // Build an SQL query based on the modified time. - $query = $this->db->getQuery(true) - ->where('a.modified >= ' . $this->db->quote($time)); - - return $query; - } - - /** - * Method to get the query clause for getting items to update by id. - * - * @param array $ids The ids to load. - * - * @return QueryInterface A database object. - * - * @since 2.5 - */ - protected function getUpdateQueryByIds($ids) - { - // Build an SQL query based on the item ids. - $query = $this->db->getQuery(true) - ->where('a.id IN(' . implode(',', $ids) . ')'); - - return $query; - } - - /** - * Method to get the type id for the adapter content. - * - * @return integer The numeric type id for the content. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getTypeId() - { - // Get the type id from the database. - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('id')) - ->from($this->db->quoteName('#__finder_types')) - ->where($this->db->quoteName('title') . ' = ' . $this->db->quote($this->type_title)); - $this->db->setQuery($query); - - return (int) $this->db->loadResult(); - } - - /** - * Method to get the URL for the item. The URL is how we look up the link - * in the Finder index. - * - * @param integer $id The id of the item. - * @param string $extension The extension the category is in. - * @param string $view The view for the URL. - * - * @return string The URL of the item. - * - * @since 2.5 - */ - protected function getUrl($id, $extension, $view) - { - return 'index.php?option=' . $extension . '&view=' . $view . '&id=' . $id; - } - - /** - * Method to get the page title of any menu item that is linked to the - * content item, if it exists and is set. - * - * @param string $url The URL of the item. - * - * @return mixed The title on success, null if not found. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getItemMenuTitle($url) - { - $return = null; - - // Set variables - $user = Factory::getUser(); - $groups = implode(',', $user->getAuthorisedViewLevels()); - - // Build a query to get the menu params. - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('params')) - ->from($this->db->quoteName('#__menu')) - ->where($this->db->quoteName('link') . ' = ' . $this->db->quote($url)) - ->where($this->db->quoteName('published') . ' = 1') - ->where($this->db->quoteName('access') . ' IN (' . $groups . ')'); - - // Get the menu params from the database. - $this->db->setQuery($query); - $params = $this->db->loadResult(); - - // Check the results. - if (empty($params)) - { - return $return; - } - - // Instantiate the params. - $params = json_decode($params); - - // Get the page title if it is set. - if (isset($params->page_title) && $params->page_title) - { - $return = $params->page_title; - } - - return $return; - } - - /** - * Method to update index data on access level changes - * - * @param Table $row A Table object - * - * @return void - * - * @since 2.5 - */ - protected function itemAccessChange($row) - { - $query = clone $this->getStateQuery(); - $query->where('a.id = ' . (int) $row->id); - - // Get the access level. - $this->db->setQuery($query); - $item = $this->db->loadObject(); - - // Set the access level. - $temp = max($row->access, $item->cat_access); - - // Update the item. - $this->change((int) $row->id, 'access', $temp); - } - - /** - * Method to update index data on published state changes - * - * @param array $pks A list of primary key ids of the content that has changed state. - * @param integer $value The value of the state that the content has been changed to. - * - * @return void - * - * @since 2.5 - */ - protected function itemStateChange($pks, $value) - { - /* - * The item's published state is tied to the category - * published state so we need to look up all published states - * before we change anything. - */ - foreach ($pks as $pk) - { - $query = clone $this->getStateQuery(); - $query->where('a.id = ' . (int) $pk); - - // Get the published states. - $this->db->setQuery($query); - $item = $this->db->loadObject(); - - // Translate the state. - $temp = $this->translateState($value, $item->cat_state); - - // Update the item. - $this->change($pk, 'state', $temp); - } - } - - /** - * Method to update index data when a plugin is disabled - * - * @param array $pks A list of primary key ids of the content that has changed state. - * - * @return void - * - * @since 2.5 - */ - protected function pluginDisable($pks) - { - // Since multiple plugins may be disabled at a time, we need to check first - // that we're handling the appropriate one for the context - foreach ($pks as $pk) - { - if ($this->getPluginType($pk) == strtolower($this->context)) - { - // Get all of the items to unindex them - $query = clone $this->getStateQuery(); - $this->db->setQuery($query); - $items = $this->db->loadColumn(); - - // Remove each item - foreach ($items as $item) - { - $this->remove($item); - } - } - } - } - - /** - * Method to translate the native content states into states that the - * indexer can use. - * - * @param integer $item The item state. - * @param integer $category The category state. [optional] - * - * @return integer The translated indexer state. - * - * @since 2.5 - */ - protected function translateState($item, $category = null) - { - // If category is present, factor in its states as well - if ($category !== null && $category == 0) - { - $item = 0; - } - - // Translate the state - switch ($item) - { - // Published and archived items only should return a published state - case 1: - case 2: - return 1; - - // All other states should return an unpublished state - default: - return 0; - } - } + /** + * The context is somewhat arbitrary but it must be unique or there will be + * conflicts when managing plugin/indexer state. A good best practice is to + * use the plugin name suffix as the context. For example, if the plugin is + * named 'plgFinderContent', the context could be 'Content'. + * + * @var string + * @since 2.5 + */ + protected $context; + + /** + * The extension name. + * + * @var string + * @since 2.5 + */ + protected $extension; + + /** + * The sublayout to use when rendering the results. + * + * @var string + * @since 2.5 + */ + protected $layout; + + /** + * The mime type of the content the adapter indexes. + * + * @var string + * @since 2.5 + */ + protected $mime; + + /** + * The access level of an item before save. + * + * @var integer + * @since 2.5 + */ + protected $old_access; + + /** + * The access level of a category before save. + * + * @var integer + * @since 2.5 + */ + protected $old_cataccess; + + /** + * The type of content the adapter indexes. + * + * @var string + * @since 2.5 + */ + protected $type_title; + + /** + * The type id of the content. + * + * @var integer + * @since 2.5 + */ + protected $type_id; + + /** + * The database object. + * + * @var DatabaseInterface + * @since 2.5 + */ + protected $db; + + /** + * The table name. + * + * @var string + * @since 2.5 + */ + protected $table; + + /** + * The indexer object. + * + * @var Indexer + * @since 3.0 + */ + protected $indexer; + + /** + * The field the published state is stored in. + * + * @var string + * @since 2.5 + */ + protected $state_field = 'state'; + + /** + * Method to instantiate the indexer adapter. + * + * @param object $subject The object to observe. + * @param array $config An array that holds the plugin configuration. + * + * @since 2.5 + */ + public function __construct(&$subject, $config) + { + // Call the parent constructor. + parent::__construct($subject, $config); + + // Get the type id. + $this->type_id = $this->getTypeId(); + + // Add the content type if it doesn't exist and is set. + if (empty($this->type_id) && !empty($this->type_title)) { + $this->type_id = Helper::addContentType($this->type_title, $this->mime); + } + + // Check for a layout override. + if ($this->params->get('layout')) { + $this->layout = $this->params->get('layout'); + } + + // Get the indexer object + $this->indexer = new Indexer($this->db); + } + + /** + * Method to get the adapter state and push it into the indexer. + * + * @return void + * + * @since 2.5 + * @throws Exception on error. + */ + public function onStartIndex() + { + // Get the indexer state. + $iState = Indexer::getState(); + + // Get the number of content items. + $total = (int) $this->getContentCount(); + + // Add the content count to the total number of items. + $iState->totalItems += $total; + + // Populate the indexer state information for the adapter. + $iState->pluginState[$this->context]['total'] = $total; + $iState->pluginState[$this->context]['offset'] = 0; + + // Set the indexer state. + Indexer::setState($iState); + } + + /** + * Method to prepare for the indexer to be run. This method will often + * be used to include dependencies and things of that nature. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on error. + */ + public function onBeforeIndex() + { + // Get the indexer and adapter state. + $iState = Indexer::getState(); + $aState = $iState->pluginState[$this->context]; + + // Check the progress of the indexer and the adapter. + if ($iState->batchOffset == $iState->batchSize || $aState['offset'] == $aState['total']) { + return true; + } + + // Run the setup method. + return $this->setup(); + } + + /** + * Method to index a batch of content items. This method can be called by + * the indexer many times throughout the indexing process depending on how + * much content is available for indexing. It is important to track the + * progress correctly so we can display it to the user. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on error. + */ + public function onBuildIndex() + { + // Get the indexer and adapter state. + $iState = Indexer::getState(); + $aState = $iState->pluginState[$this->context]; + + // Check the progress of the indexer and the adapter. + if ($iState->batchOffset == $iState->batchSize || $aState['offset'] == $aState['total']) { + return true; + } + + // Get the batch offset and size. + $offset = (int) $aState['offset']; + $limit = (int) ($iState->batchSize - $iState->batchOffset); + + // Get the content items to index. + $items = $this->getItems($offset, $limit); + + // Iterate through the items and index them. + for ($i = 0, $n = count($items); $i < $n; $i++) { + // Index the item. + $this->index($items[$i]); + + // Adjust the offsets. + $offset++; + $iState->batchOffset++; + $iState->totalItems--; + } + + // Update the indexer state. + $aState['offset'] = $offset; + $iState->pluginState[$this->context] = $aState; + Indexer::setState($iState); + + return true; + } + + /** + * Method to remove outdated index entries + * + * @return integer + * + * @since 4.2.0 + */ + public function onFinderGarbageCollection() + { + $db = $this->db; + $type_id = $this->getTypeId(); + + $query = $db->getQuery(true); + $subquery = $db->getQuery(true); + $subquery->select('CONCAT(' . $db->quote($this->getUrl('', $this->extension, $this->layout)) . ', id)') + ->from($db->quoteName($this->table)); + $query->select($db->quoteName('l.link_id')) + ->from($db->quoteName('#__finder_links', 'l')) + ->where($db->quoteName('l.type_id') . ' = ' . $type_id) + ->where($db->quoteName('l.url') . ' LIKE ' . $db->quote($this->getUrl('%', $this->extension, $this->layout))) + ->where($db->quoteName('l.url') . ' NOT IN (' . $subquery . ')'); + $db->setQuery($query); + $items = $db->loadColumn(); + + foreach ($items as $item) { + $this->indexer->remove($item); + } + + return count($items); + } + + /** + * Method to change the value of a content item's property in the links + * table. This is used to synchronize published and access states that + * are changed when not editing an item directly. + * + * @param string $id The ID of the item to change. + * @param string $property The property that is being changed. + * @param integer $value The new value of that property. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function change($id, $property, $value) + { + // Check for a property we know how to handle. + if ($property !== 'state' && $property !== 'access') { + return true; + } + + // Get the URL for the content id. + $item = $this->db->quote($this->getUrl($id, $this->extension, $this->layout)); + + // Update the content items. + $query = $this->db->getQuery(true) + ->update($this->db->quoteName('#__finder_links')) + ->set($this->db->quoteName($property) . ' = ' . (int) $value) + ->where($this->db->quoteName('url') . ' = ' . $item); + $this->db->setQuery($query); + $this->db->execute(); + + return true; + } + + /** + * Method to index an item. + * + * @param Result $item The item to index as a Result object. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + abstract protected function index(Result $item); + + /** + * Method to reindex an item. + * + * @param integer $id The ID of the item to reindex. + * + * @return void + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function reindex($id) + { + // Run the setup method. + $this->setup(); + + // Remove the old item. + $this->remove($id, false); + + // Get the item. + $item = $this->getItem($id); + + // Index the item. + $this->index($item); + + Taxonomy::removeOrphanNodes(); + } + + /** + * Method to remove an item from the index. + * + * @param string $id The ID of the item to remove. + * @param bool $removeTaxonomies Remove empty taxonomies + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function remove($id, $removeTaxonomies = true) + { + // Get the item's URL + $url = $this->db->quote($this->getUrl($id, $this->extension, $this->layout)); + + // Get the link ids for the content items. + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('link_id')) + ->from($this->db->quoteName('#__finder_links')) + ->where($this->db->quoteName('url') . ' = ' . $url); + $this->db->setQuery($query); + $items = $this->db->loadColumn(); + + // Check the items. + if (empty($items)) { + Factory::getApplication()->triggerEvent('onFinderIndexAfterDelete', array($id)); + + return true; + } + + // Remove the items. + foreach ($items as $item) { + $this->indexer->remove($item, $removeTaxonomies); + } + + return true; + } + + /** + * Method to setup the adapter before indexing. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + * @throws Exception on database error. + */ + abstract protected function setup(); + + /** + * Method to update index data on category access level changes + * + * @param Table $row A Table object + * + * @return void + * + * @since 2.5 + */ + protected function categoryAccessChange($row) + { + $query = clone $this->getStateQuery(); + $query->where('c.id = ' . (int) $row->id); + + // Get the access level. + $this->db->setQuery($query); + $items = $this->db->loadObjectList(); + + // Adjust the access level for each item within the category. + foreach ($items as $item) { + // Set the access level. + $temp = max($item->access, $row->access); + + // Update the item. + $this->change((int) $item->id, 'access', $temp); + } + } + + /** + * Method to update index data on category access level changes + * + * @param array $pks A list of primary key ids of the content that has changed state. + * @param integer $value The value of the state that the content has been changed to. + * + * @return void + * + * @since 2.5 + */ + protected function categoryStateChange($pks, $value) + { + /* + * The item's published state is tied to the category + * published state so we need to look up all published states + * before we change anything. + */ + foreach ($pks as $pk) { + $query = clone $this->getStateQuery(); + $query->where('c.id = ' . (int) $pk); + + // Get the published states. + $this->db->setQuery($query); + $items = $this->db->loadObjectList(); + + // Adjust the state for each item within the category. + foreach ($items as $item) { + // Translate the state. + $temp = $this->translateState($item->state, $value); + + // Update the item. + $this->change($item->id, 'state', $temp); + } + } + } + + /** + * Method to check the existing access level for categories + * + * @param Table $row A Table object + * + * @return void + * + * @since 2.5 + */ + protected function checkCategoryAccess($row) + { + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('access')) + ->from($this->db->quoteName('#__categories')) + ->where($this->db->quoteName('id') . ' = ' . (int) $row->id); + $this->db->setQuery($query); + + // Store the access level to determine if it changes + $this->old_cataccess = $this->db->loadResult(); + } + + /** + * Method to check the existing access level for items + * + * @param Table $row A Table object + * + * @return void + * + * @since 2.5 + */ + protected function checkItemAccess($row) + { + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('access')) + ->from($this->db->quoteName($this->table)) + ->where($this->db->quoteName('id') . ' = ' . (int) $row->id); + $this->db->setQuery($query); + + // Store the access level to determine if it changes + $this->old_access = $this->db->loadResult(); + } + + /** + * Method to get the number of content items available to index. + * + * @return integer The number of content items available to index. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getContentCount() + { + $return = 0; + + // Get the list query. + $query = $this->getListQuery(); + + // Check if the query is valid. + if (empty($query)) { + return $return; + } + + // Tweak the SQL query to make the total lookup faster. + if ($query instanceof QueryInterface) { + $query = clone $query; + $query->clear('select') + ->select('COUNT(*)') + ->clear('order'); + } + + // Get the total number of content items to index. + $this->db->setQuery($query); + + return (int) $this->db->loadResult(); + } + + /** + * Method to get a content item to index. + * + * @param integer $id The id of the content item. + * + * @return Result A Result object. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getItem($id) + { + // Get the list query and add the extra WHERE clause. + $query = $this->getListQuery(); + $query->where('a.id = ' . (int) $id); + + // Get the item to index. + $this->db->setQuery($query); + $item = $this->db->loadAssoc(); + + // Convert the item to a result object. + $item = ArrayHelper::toObject((array) $item, Result::class); + + // Set the item type. + $item->type_id = $this->type_id; + + // Set the item layout. + $item->layout = $this->layout; + + return $item; + } + + /** + * Method to get a list of content items to index. + * + * @param integer $offset The list offset. + * @param integer $limit The list limit. + * @param QueryInterface $query A QueryInterface object. [optional] + * + * @return Result[] An array of Result objects. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getItems($offset, $limit, $query = null) + { + // Get the content items to index. + $this->db->setQuery($this->getListQuery($query)->setLimit($limit, $offset)); + $items = $this->db->loadAssocList(); + + foreach ($items as &$item) { + $item = ArrayHelper::toObject($item, Result::class); + + // Set the item type. + $item->type_id = $this->type_id; + + // Set the mime type. + $item->mime = $this->mime; + + // Set the item layout. + $item->layout = $this->layout; + } + + return $items; + } + + /** + * Method to get the SQL query used to retrieve the list of content items. + * + * @param mixed $query A QueryInterface object. [optional] + * + * @return QueryInterface A database object. + * + * @since 2.5 + */ + protected function getListQuery($query = null) + { + // Check if we can use the supplied SQL query. + return $query instanceof QueryInterface ? $query : $this->db->getQuery(true); + } + + /** + * Method to get the plugin type + * + * @param integer $id The plugin ID + * + * @return string The plugin type + * + * @since 2.5 + */ + protected function getPluginType($id) + { + // Prepare the query + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('element')) + ->from($this->db->quoteName('#__extensions')) + ->where($this->db->quoteName('extension_id') . ' = ' . (int) $id); + $this->db->setQuery($query); + + return $this->db->loadResult(); + } + + /** + * Method to get a SQL query to load the published and access states for + * an article and category. + * + * @return QueryInterface A database object. + * + * @since 2.5 + */ + protected function getStateQuery() + { + $query = $this->db->getQuery(true); + + // Item ID + $query->select('a.id'); + + // Item and category published state + $query->select('a.' . $this->state_field . ' AS state, c.published AS cat_state'); + + // Item and category access levels + $query->select('a.access, c.access AS cat_access') + ->from($this->table . ' AS a') + ->join('LEFT', '#__categories AS c ON c.id = a.catid'); + + return $query; + } + + /** + * Method to get the query clause for getting items to update by time. + * + * @param string $time The modified timestamp. + * + * @return QueryInterface A database object. + * + * @since 2.5 + */ + protected function getUpdateQueryByTime($time) + { + // Build an SQL query based on the modified time. + $query = $this->db->getQuery(true) + ->where('a.modified >= ' . $this->db->quote($time)); + + return $query; + } + + /** + * Method to get the query clause for getting items to update by id. + * + * @param array $ids The ids to load. + * + * @return QueryInterface A database object. + * + * @since 2.5 + */ + protected function getUpdateQueryByIds($ids) + { + // Build an SQL query based on the item ids. + $query = $this->db->getQuery(true) + ->where('a.id IN(' . implode(',', $ids) . ')'); + + return $query; + } + + /** + * Method to get the type id for the adapter content. + * + * @return integer The numeric type id for the content. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getTypeId() + { + // Get the type id from the database. + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__finder_types')) + ->where($this->db->quoteName('title') . ' = ' . $this->db->quote($this->type_title)); + $this->db->setQuery($query); + + return (int) $this->db->loadResult(); + } + + /** + * Method to get the URL for the item. The URL is how we look up the link + * in the Finder index. + * + * @param integer $id The id of the item. + * @param string $extension The extension the category is in. + * @param string $view The view for the URL. + * + * @return string The URL of the item. + * + * @since 2.5 + */ + protected function getUrl($id, $extension, $view) + { + return 'index.php?option=' . $extension . '&view=' . $view . '&id=' . $id; + } + + /** + * Method to get the page title of any menu item that is linked to the + * content item, if it exists and is set. + * + * @param string $url The URL of the item. + * + * @return mixed The title on success, null if not found. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getItemMenuTitle($url) + { + $return = null; + + // Set variables + $user = Factory::getUser(); + $groups = implode(',', $user->getAuthorisedViewLevels()); + + // Build a query to get the menu params. + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('params')) + ->from($this->db->quoteName('#__menu')) + ->where($this->db->quoteName('link') . ' = ' . $this->db->quote($url)) + ->where($this->db->quoteName('published') . ' = 1') + ->where($this->db->quoteName('access') . ' IN (' . $groups . ')'); + + // Get the menu params from the database. + $this->db->setQuery($query); + $params = $this->db->loadResult(); + + // Check the results. + if (empty($params)) { + return $return; + } + + // Instantiate the params. + $params = json_decode($params); + + // Get the page title if it is set. + if (isset($params->page_title) && $params->page_title) { + $return = $params->page_title; + } + + return $return; + } + + /** + * Method to update index data on access level changes + * + * @param Table $row A Table object + * + * @return void + * + * @since 2.5 + */ + protected function itemAccessChange($row) + { + $query = clone $this->getStateQuery(); + $query->where('a.id = ' . (int) $row->id); + + // Get the access level. + $this->db->setQuery($query); + $item = $this->db->loadObject(); + + // Set the access level. + $temp = max($row->access, $item->cat_access); + + // Update the item. + $this->change((int) $row->id, 'access', $temp); + } + + /** + * Method to update index data on published state changes + * + * @param array $pks A list of primary key ids of the content that has changed state. + * @param integer $value The value of the state that the content has been changed to. + * + * @return void + * + * @since 2.5 + */ + protected function itemStateChange($pks, $value) + { + /* + * The item's published state is tied to the category + * published state so we need to look up all published states + * before we change anything. + */ + foreach ($pks as $pk) { + $query = clone $this->getStateQuery(); + $query->where('a.id = ' . (int) $pk); + + // Get the published states. + $this->db->setQuery($query); + $item = $this->db->loadObject(); + + // Translate the state. + $temp = $this->translateState($value, $item->cat_state); + + // Update the item. + $this->change($pk, 'state', $temp); + } + } + + /** + * Method to update index data when a plugin is disabled + * + * @param array $pks A list of primary key ids of the content that has changed state. + * + * @return void + * + * @since 2.5 + */ + protected function pluginDisable($pks) + { + // Since multiple plugins may be disabled at a time, we need to check first + // that we're handling the appropriate one for the context + foreach ($pks as $pk) { + if ($this->getPluginType($pk) == strtolower($this->context)) { + // Get all of the items to unindex them + $query = clone $this->getStateQuery(); + $this->db->setQuery($query); + $items = $this->db->loadColumn(); + + // Remove each item + foreach ($items as $item) { + $this->remove($item); + } + } + } + } + + /** + * Method to translate the native content states into states that the + * indexer can use. + * + * @param integer $item The item state. + * @param integer $category The category state. [optional] + * + * @return integer The translated indexer state. + * + * @since 2.5 + */ + protected function translateState($item, $category = null) + { + // If category is present, factor in its states as well + if ($category !== null && $category == 0) { + $item = 0; + } + + // Translate the state + switch ($item) { + // Published and archived items only should return a published state + case 1: + case 2: + return 1; + + // All other states should return an unpublished state + default: + return 0; + } + } } diff --git a/administrator/components/com_finder/src/Indexer/Helper.php b/administrator/components/com_finder/src/Indexer/Helper.php index 12042f11cabeb..591fc0bf78db6 100644 --- a/administrator/components/com_finder/src/Indexer/Helper.php +++ b/administrator/components/com_finder/src/Indexer/Helper.php @@ -1,4 +1,5 @@ parse($input); - } - - /** - * Method to tokenize a text string. - * - * @param string $input The input to tokenize. - * @param string $lang The language of the input. - * @param boolean $phrase Flag to indicate whether input could be a phrase. [optional] - * - * @return Token[] An array of Token objects. - * - * @since 2.5 - */ - public static function tokenize($input, $lang, $phrase = false) - { - static $cache = [], $tuplecount; - static $multilingual; - static $defaultLanguage; - - if (!$tuplecount) - { - $params = ComponentHelper::getParams('com_finder'); - $tuplecount = $params->get('tuplecount', 1); - } - - if (is_null($multilingual)) - { - $multilingual = Multilanguage::isEnabled(); - $config = ComponentHelper::getParams('com_finder'); - - if ($config->get('language_default', '') == '') - { - $defaultLang = '*'; - } - elseif ($config->get('language_default', '') == '-1') - { - $defaultLang = self::getDefaultLanguage(); - } - else - { - $defaultLang = $config->get('language_default'); - } - - /* - * The default language always has the language code '*'. - * In order to not overwrite the language code of the language - * object that we are using, we are cloning it here. - */ - $obj = Language::getInstance($defaultLang); - $defaultLanguage = clone $obj; - $defaultLanguage->language = '*'; - } - - if (!$multilingual || $lang == '*') - { - $language = $defaultLanguage; - } - else - { - $language = Language::getInstance($lang); - } - - if (!isset($cache[$lang])) - { - $cache[$lang] = []; - } - - $tokens = array(); - $terms = $language->tokenise($input); - - // @todo: array_filter removes any number 0's from the terms. Not sure this is entirely intended - $terms = array_filter($terms); - $terms = array_values($terms); - - /* - * If we have to handle the input as a phrase, that means we don't - * tokenize the individual terms and we do not create the two and three - * term combinations. The phrase must contain more than one word! - */ - if ($phrase === true && count($terms) > 1) - { - // Create tokens from the phrase. - $tokens[] = new Token($terms, $language->language, $language->spacer); - } - else - { - // Create tokens from the terms. - for ($i = 0, $n = count($terms); $i < $n; $i++) - { - if (isset($cache[$lang][$terms[$i]])) - { - $tokens[] = $cache[$lang][$terms[$i]]; - } - else - { - $token = new Token($terms[$i], $language->language); - $tokens[] = $token; - $cache[$lang][$terms[$i]] = $token; - } - } - - // Create multi-word phrase tokens from the individual words. - if ($tuplecount > 1) - { - for ($i = 0, $n = count($tokens); $i < $n; $i++) - { - $temp = array($tokens[$i]->term); - - // Create tokens for 2 to $tuplecount length phrases - for ($j = 1; $j < $tuplecount; $j++) - { - if ($i + $j >= $n || !isset($tokens[$i + $j])) - { - break; - } - - $temp[] = $tokens[$i + $j]->term; - $key = implode('::', $temp); - - if (isset($cache[$lang][$key])) - { - $tokens[] = $cache[$lang][$key]; - } - else - { - $token = new Token($temp, $language->language, $language->spacer); - $token->derived = true; - $tokens[] = $token; - $cache[$lang][$key] = $token; - } - } - } - } - } - - // Prevent the cache to fill up the memory - while (count($cache[$lang]) > 1024) - { - /** - * We want to cache the most common words/tokens. At the same time - * we don't want to cache too much. The most common words will also - * be early in the text, so we are dropping all terms/tokens which - * have been cached later. - */ - array_pop($cache[$lang]); - } - - return $tokens; - } - - /** - * Method to get the base word of a token. - * - * @param string $token The token to stem. - * @param string $lang The language of the token. - * - * @return string The root token. - * - * @since 2.5 - */ - public static function stem($token, $lang) - { - static $multilingual; - static $defaultStemmer; - - if (is_null($multilingual)) - { - $multilingual = Multilanguage::isEnabled(); - $config = ComponentHelper::getParams('com_finder'); - - if ($config->get('language_default', '') == '') - { - $defaultStemmer = Language::getInstance('*'); - } - elseif ($config->get('language_default', '') == '-1') - { - $defaultStemmer = Language::getInstance(self::getDefaultLanguage()); - } - else - { - $defaultStemmer = Language::getInstance($config->get('language_default')); - } - } - - if (!$multilingual || $lang == '*') - { - $language = $defaultStemmer; - } - else - { - $language = Language::getInstance($lang); - } - - return $language->stem($token); - } - - /** - * Method to add a content type to the database. - * - * @param string $title The type of content. For example: PDF - * @param string $mime The mime type of the content. For example: PDF [optional] - * - * @return integer The id of the content type. - * - * @since 2.5 - * @throws Exception on database error. - */ - public static function addContentType($title, $mime = null) - { - static $types; - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - // Check if the types are loaded. - if (empty($types)) - { - // Build the query to get the types. - $query->select('*') - ->from($db->quoteName('#__finder_types')); - - // Get the types. - $db->setQuery($query); - $types = $db->loadObjectList('title'); - } - - // Check if the type already exists. - if (isset($types[$title])) - { - return (int) $types[$title]->id; - } - - // Add the type. - $query->clear() - ->insert($db->quoteName('#__finder_types')) - ->columns(array($db->quoteName('title'), $db->quoteName('mime'))) - ->values($db->quote($title) . ', ' . $db->quote($mime)); - $db->setQuery($query); - $db->execute(); - - // Return the new id. - return (int) $db->insertid(); - } - - /** - * Method to check if a token is common in a language. - * - * @param string $token The token to test. - * @param string $lang The language to reference. - * - * @return boolean True if common, false otherwise. - * - * @since 2.5 - */ - public static function isCommon($token, $lang) - { - static $data, $default, $multilingual; - - if (is_null($multilingual)) - { - $multilingual = Multilanguage::isEnabled(); - $config = ComponentHelper::getParams('com_finder'); - - if ($config->get('language_default', '') == '') - { - $default = '*'; - } - elseif ($config->get('language_default', '') == '-1') - { - $default = self::getPrimaryLanguage(self::getDefaultLanguage()); - } - else - { - $default = self::getPrimaryLanguage($config->get('language_default')); - } - } - - if (!$multilingual || $lang == '*') - { - $lang = $default; - } - - // Load the common tokens for the language if necessary. - if (!isset($data[$lang])) - { - $data[$lang] = self::getCommonWords($lang); - } - - // Check if the token is in the common array. - return in_array($token, $data[$lang], true); - } - - /** - * Method to get an array of common terms for a language. - * - * @param string $lang The language to use. - * - * @return array Array of common terms. - * - * @since 2.5 - * @throws Exception on database error. - */ - public static function getCommonWords($lang) - { - $db = Factory::getDbo(); - - // Create the query to load all the common terms for the language. - $query = $db->getQuery(true) - ->select($db->quoteName('term')) - ->from($db->quoteName('#__finder_terms_common')) - ->where($db->quoteName('language') . ' = ' . $db->quote($lang)); - - // Load all of the common terms for the language. - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Method to get the default language for the site. - * - * @return string The default language string. - * - * @since 2.5 - */ - public static function getDefaultLanguage() - { - static $lang; - - // We need to go to com_languages to get the site default language, it's the best we can guess. - if (empty($lang)) - { - $lang = ComponentHelper::getParams('com_languages')->get('site', 'en-GB'); - } - - return $lang; - } - - /** - * Method to parse a language/locale key and return a simple language string. - * - * @param string $lang The language/locale key. For example: en-GB - * - * @return string The simple language string. For example: en - * - * @since 2.5 - */ - public static function getPrimaryLanguage($lang) - { - static $data; - - // Only parse the identifier if necessary. - if (!isset($data[$lang])) - { - if (is_callable(array('Locale', 'getPrimaryLanguage'))) - { - // Get the language key using the Locale package. - $data[$lang] = \Locale::getPrimaryLanguage($lang); - } - else - { - // Get the language key using string position. - $data[$lang] = StringHelper::substr($lang, 0, StringHelper::strpos($lang, '-')); - } - } - - return $data[$lang]; - } - - /** - * Method to get extra data for a content before being indexed. This is how - * we add Comments, Tags, Labels, etc. that should be available to Finder. - * - * @param Result $item The item to index as a Result object. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - * @throws Exception on database error. - */ - public static function getContentExtras(Result $item) - { - // Load the finder plugin group. - PluginHelper::importPlugin('finder'); - - Factory::getApplication()->triggerEvent('onPrepareFinderContent', array(&$item)); - - return true; - } - - /** - * Add custom fields for the item to the Result object - * - * @param Result $item Result object to add the custom fields to - * @param string $context Context of the item in the custom fields - * - * @return void - * - * @since 4.2.0 - */ - public static function addCustomFields(Result $item, $context) - { - $obj = new \stdClass; - $obj->id = $item->id; - - $fields = FieldsHelper::getFields($context, $obj, true); - - foreach ($fields as $field) - { - $searchindex = $field->params->get('searchindex', 0); - - // We want to add this field to the search index - if ($searchindex == 1 || $searchindex == 3) - { - $name = 'jsfield_' . $field->name; - $item->$name = $field->value; - $item->addInstruction(Indexer::META_CONTEXT, $name); - } - - // We want to add this field as a taxonomy - if (($searchindex == 2 || $searchindex == 3) && $field->value) - { - $item->addTaxonomy($field->title, $field->value, $field->state, $field->access, $field->language); - } - } - } - - /** - * Method to process content text using the onContentPrepare event trigger. - * - * @param string $text The content to process. - * @param Registry $params The parameters object. [optional] - * @param Result $item The item which get prepared. [optional] - * - * @return string The processed content. - * - * @since 2.5 - */ - public static function prepareContent($text, $params = null, Result $item = null) - { - static $loaded; - - // Load the content plugins if necessary. - if (empty($loaded)) - { - PluginHelper::importPlugin('content'); - $loaded = true; - } - - // Instantiate the parameter object if necessary. - if (!($params instanceof Registry)) - { - $registry = new Registry($params); - $params = $registry; - } - - // Create a mock content object. - $content = Table::getInstance('Content'); - $content->text = $text; - - if ($item) - { - $content->bind((array) $item); - $content->bind($item->getElements()); - } - - if ($item && !empty($item->context)) - { - $content->context = $item->context; - } - - // Fire the onContentPrepare event. - Factory::getApplication()->triggerEvent('onContentPrepare', array('com_finder.indexer', &$content, &$params, 0)); - - return $content->text; - } + /** + * Method to parse input into plain text. + * + * @param string $input The raw input. + * @param string $format The format of the input. [optional] + * + * @return string The parsed input. + * + * @since 2.5 + * @throws Exception on invalid parser. + */ + public static function parse($input, $format = 'html') + { + // Get a parser for the specified format and parse the input. + return Parser::getInstance($format)->parse($input); + } + + /** + * Method to tokenize a text string. + * + * @param string $input The input to tokenize. + * @param string $lang The language of the input. + * @param boolean $phrase Flag to indicate whether input could be a phrase. [optional] + * + * @return Token[] An array of Token objects. + * + * @since 2.5 + */ + public static function tokenize($input, $lang, $phrase = false) + { + static $cache = [], $tuplecount; + static $multilingual; + static $defaultLanguage; + + if (!$tuplecount) { + $params = ComponentHelper::getParams('com_finder'); + $tuplecount = $params->get('tuplecount', 1); + } + + if (is_null($multilingual)) { + $multilingual = Multilanguage::isEnabled(); + $config = ComponentHelper::getParams('com_finder'); + + if ($config->get('language_default', '') == '') { + $defaultLang = '*'; + } elseif ($config->get('language_default', '') == '-1') { + $defaultLang = self::getDefaultLanguage(); + } else { + $defaultLang = $config->get('language_default'); + } + + /* + * The default language always has the language code '*'. + * In order to not overwrite the language code of the language + * object that we are using, we are cloning it here. + */ + $obj = Language::getInstance($defaultLang); + $defaultLanguage = clone $obj; + $defaultLanguage->language = '*'; + } + + if (!$multilingual || $lang == '*') { + $language = $defaultLanguage; + } else { + $language = Language::getInstance($lang); + } + + if (!isset($cache[$lang])) { + $cache[$lang] = []; + } + + $tokens = array(); + $terms = $language->tokenise($input); + + // @todo: array_filter removes any number 0's from the terms. Not sure this is entirely intended + $terms = array_filter($terms); + $terms = array_values($terms); + + /* + * If we have to handle the input as a phrase, that means we don't + * tokenize the individual terms and we do not create the two and three + * term combinations. The phrase must contain more than one word! + */ + if ($phrase === true && count($terms) > 1) { + // Create tokens from the phrase. + $tokens[] = new Token($terms, $language->language, $language->spacer); + } else { + // Create tokens from the terms. + for ($i = 0, $n = count($terms); $i < $n; $i++) { + if (isset($cache[$lang][$terms[$i]])) { + $tokens[] = $cache[$lang][$terms[$i]]; + } else { + $token = new Token($terms[$i], $language->language); + $tokens[] = $token; + $cache[$lang][$terms[$i]] = $token; + } + } + + // Create multi-word phrase tokens from the individual words. + if ($tuplecount > 1) { + for ($i = 0, $n = count($tokens); $i < $n; $i++) { + $temp = array($tokens[$i]->term); + + // Create tokens for 2 to $tuplecount length phrases + for ($j = 1; $j < $tuplecount; $j++) { + if ($i + $j >= $n || !isset($tokens[$i + $j])) { + break; + } + + $temp[] = $tokens[$i + $j]->term; + $key = implode('::', $temp); + + if (isset($cache[$lang][$key])) { + $tokens[] = $cache[$lang][$key]; + } else { + $token = new Token($temp, $language->language, $language->spacer); + $token->derived = true; + $tokens[] = $token; + $cache[$lang][$key] = $token; + } + } + } + } + } + + // Prevent the cache to fill up the memory + while (count($cache[$lang]) > 1024) { + /** + * We want to cache the most common words/tokens. At the same time + * we don't want to cache too much. The most common words will also + * be early in the text, so we are dropping all terms/tokens which + * have been cached later. + */ + array_pop($cache[$lang]); + } + + return $tokens; + } + + /** + * Method to get the base word of a token. + * + * @param string $token The token to stem. + * @param string $lang The language of the token. + * + * @return string The root token. + * + * @since 2.5 + */ + public static function stem($token, $lang) + { + static $multilingual; + static $defaultStemmer; + + if (is_null($multilingual)) { + $multilingual = Multilanguage::isEnabled(); + $config = ComponentHelper::getParams('com_finder'); + + if ($config->get('language_default', '') == '') { + $defaultStemmer = Language::getInstance('*'); + } elseif ($config->get('language_default', '') == '-1') { + $defaultStemmer = Language::getInstance(self::getDefaultLanguage()); + } else { + $defaultStemmer = Language::getInstance($config->get('language_default')); + } + } + + if (!$multilingual || $lang == '*') { + $language = $defaultStemmer; + } else { + $language = Language::getInstance($lang); + } + + return $language->stem($token); + } + + /** + * Method to add a content type to the database. + * + * @param string $title The type of content. For example: PDF + * @param string $mime The mime type of the content. For example: PDF [optional] + * + * @return integer The id of the content type. + * + * @since 2.5 + * @throws Exception on database error. + */ + public static function addContentType($title, $mime = null) + { + static $types; + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + // Check if the types are loaded. + if (empty($types)) { + // Build the query to get the types. + $query->select('*') + ->from($db->quoteName('#__finder_types')); + + // Get the types. + $db->setQuery($query); + $types = $db->loadObjectList('title'); + } + + // Check if the type already exists. + if (isset($types[$title])) { + return (int) $types[$title]->id; + } + + // Add the type. + $query->clear() + ->insert($db->quoteName('#__finder_types')) + ->columns(array($db->quoteName('title'), $db->quoteName('mime'))) + ->values($db->quote($title) . ', ' . $db->quote($mime)); + $db->setQuery($query); + $db->execute(); + + // Return the new id. + return (int) $db->insertid(); + } + + /** + * Method to check if a token is common in a language. + * + * @param string $token The token to test. + * @param string $lang The language to reference. + * + * @return boolean True if common, false otherwise. + * + * @since 2.5 + */ + public static function isCommon($token, $lang) + { + static $data, $default, $multilingual; + + if (is_null($multilingual)) { + $multilingual = Multilanguage::isEnabled(); + $config = ComponentHelper::getParams('com_finder'); + + if ($config->get('language_default', '') == '') { + $default = '*'; + } elseif ($config->get('language_default', '') == '-1') { + $default = self::getPrimaryLanguage(self::getDefaultLanguage()); + } else { + $default = self::getPrimaryLanguage($config->get('language_default')); + } + } + + if (!$multilingual || $lang == '*') { + $lang = $default; + } + + // Load the common tokens for the language if necessary. + if (!isset($data[$lang])) { + $data[$lang] = self::getCommonWords($lang); + } + + // Check if the token is in the common array. + return in_array($token, $data[$lang], true); + } + + /** + * Method to get an array of common terms for a language. + * + * @param string $lang The language to use. + * + * @return array Array of common terms. + * + * @since 2.5 + * @throws Exception on database error. + */ + public static function getCommonWords($lang) + { + $db = Factory::getDbo(); + + // Create the query to load all the common terms for the language. + $query = $db->getQuery(true) + ->select($db->quoteName('term')) + ->from($db->quoteName('#__finder_terms_common')) + ->where($db->quoteName('language') . ' = ' . $db->quote($lang)); + + // Load all of the common terms for the language. + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Method to get the default language for the site. + * + * @return string The default language string. + * + * @since 2.5 + */ + public static function getDefaultLanguage() + { + static $lang; + + // We need to go to com_languages to get the site default language, it's the best we can guess. + if (empty($lang)) { + $lang = ComponentHelper::getParams('com_languages')->get('site', 'en-GB'); + } + + return $lang; + } + + /** + * Method to parse a language/locale key and return a simple language string. + * + * @param string $lang The language/locale key. For example: en-GB + * + * @return string The simple language string. For example: en + * + * @since 2.5 + */ + public static function getPrimaryLanguage($lang) + { + static $data; + + // Only parse the identifier if necessary. + if (!isset($data[$lang])) { + if (is_callable(array('Locale', 'getPrimaryLanguage'))) { + // Get the language key using the Locale package. + $data[$lang] = \Locale::getPrimaryLanguage($lang); + } else { + // Get the language key using string position. + $data[$lang] = StringHelper::substr($lang, 0, StringHelper::strpos($lang, '-')); + } + } + + return $data[$lang]; + } + + /** + * Method to get extra data for a content before being indexed. This is how + * we add Comments, Tags, Labels, etc. that should be available to Finder. + * + * @param Result $item The item to index as a Result object. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + * @throws Exception on database error. + */ + public static function getContentExtras(Result $item) + { + // Load the finder plugin group. + PluginHelper::importPlugin('finder'); + + Factory::getApplication()->triggerEvent('onPrepareFinderContent', array(&$item)); + + return true; + } + + /** + * Add custom fields for the item to the Result object + * + * @param Result $item Result object to add the custom fields to + * @param string $context Context of the item in the custom fields + * + * @return void + * + * @since 4.2.0 + */ + public static function addCustomFields(Result $item, $context) + { + $obj = new \stdClass(); + $obj->id = $item->id; + + $fields = FieldsHelper::getFields($context, $obj, true); + + foreach ($fields as $field) { + $searchindex = $field->params->get('searchindex', 0); + + // We want to add this field to the search index + if ($searchindex == 1 || $searchindex == 3) { + $name = 'jsfield_' . $field->name; + $item->$name = $field->value; + $item->addInstruction(Indexer::META_CONTEXT, $name); + } + + // We want to add this field as a taxonomy + if (($searchindex == 2 || $searchindex == 3) && $field->value) { + $item->addTaxonomy($field->title, $field->value, $field->state, $field->access, $field->language); + } + } + } + + /** + * Method to process content text using the onContentPrepare event trigger. + * + * @param string $text The content to process. + * @param Registry $params The parameters object. [optional] + * @param Result $item The item which get prepared. [optional] + * + * @return string The processed content. + * + * @since 2.5 + */ + public static function prepareContent($text, $params = null, Result $item = null) + { + static $loaded; + + // Load the content plugins if necessary. + if (empty($loaded)) { + PluginHelper::importPlugin('content'); + $loaded = true; + } + + // Instantiate the parameter object if necessary. + if (!($params instanceof Registry)) { + $registry = new Registry($params); + $params = $registry; + } + + // Create a mock content object. + $content = Table::getInstance('Content'); + $content->text = $text; + + if ($item) { + $content->bind((array) $item); + $content->bind($item->getElements()); + } + + if ($item && !empty($item->context)) { + $content->context = $item->context; + } + + // Fire the onContentPrepare event. + Factory::getApplication()->triggerEvent('onContentPrepare', array('com_finder.indexer', &$content, &$params, 0)); + + return $content->text; + } } diff --git a/administrator/components/com_finder/src/Indexer/Indexer.php b/administrator/components/com_finder/src/Indexer/Indexer.php index 9625c424b94b2..b35e9153d3a54 100644 --- a/administrator/components/com_finder/src/Indexer/Indexer.php +++ b/administrator/components/com_finder/src/Indexer/Indexer.php @@ -1,4 +1,5 @@ get(DatabaseInterface::class); - } - - $this->db = $db; - - // Set up query template for addTokensToDb - $this->addTokensToDbQueryTemplate = $db->getQuery(true)->insert($db->quoteName('#__finder_tokens')) - ->columns( - array( - $db->quoteName('term'), - $db->quoteName('stem'), - $db->quoteName('common'), - $db->quoteName('phrase'), - $db->quoteName('weight'), - $db->quoteName('context'), - $db->quoteName('language') - ) - ); - } - - /** - * Method to get the indexer state. - * - * @return object The indexer state object. - * - * @since 2.5 - */ - public static function getState() - { - // First, try to load from the internal state. - if ((bool) static::$state) - { - return static::$state; - } - - // If we couldn't load from the internal state, try the session. - $session = Factory::getSession(); - $data = $session->get('_finder.state', null); - - // If the state is empty, load the values for the first time. - if (empty($data)) - { - $data = new CMSObject; - - // Load the default configuration options. - $data->options = ComponentHelper::getParams('com_finder'); - - // Setup the weight lookup information. - $data->weights = array( - self::TITLE_CONTEXT => round($data->options->get('title_multiplier', 1.7), 2), - self::TEXT_CONTEXT => round($data->options->get('text_multiplier', 0.7), 2), - self::META_CONTEXT => round($data->options->get('meta_multiplier', 1.2), 2), - self::PATH_CONTEXT => round($data->options->get('path_multiplier', 2.0), 2), - self::MISC_CONTEXT => round($data->options->get('misc_multiplier', 0.3), 2) - ); - - // Set the current time as the start time. - $data->startTime = Factory::getDate()->toSql(); - - // Set the remaining default values. - $data->batchSize = (int) $data->options->get('batch_size', 50); - $data->batchOffset = 0; - $data->totalItems = 0; - $data->pluginState = array(); - } - - // Setup the profiler if debugging is enabled. - if (Factory::getApplication()->get('debug')) - { - static::$profiler = Profiler::getInstance('FinderIndexer'); - } - - // Set the state. - static::$state = $data; - - return static::$state; - } - - /** - * Method to set the indexer state. - * - * @param CMSObject $data A new indexer state object. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - */ - public static function setState($data) - { - // Check the state object. - if (empty($data) || !$data instanceof CMSObject) - { - return false; - } - - // Set the new internal state. - static::$state = $data; - - // Set the new session state. - Factory::getSession()->set('_finder.state', $data); - - return true; - } - - /** - * Method to reset the indexer state. - * - * @return void - * - * @since 2.5 - */ - public static function resetState() - { - // Reset the internal state to null. - self::$state = null; - - // Reset the session state to null. - Factory::getSession()->set('_finder.state', null); - } - - /** - * Method to index a content item. - * - * @param Result $item The content item to index. - * @param string $format The format of the content. [optional] - * - * @return integer The ID of the record in the links table. - * - * @since 2.5 - * @throws \Exception on database error. - */ - public function index($item, $format = 'html') - { - // Mark beforeIndexing in the profiler. - static::$profiler ? static::$profiler->mark('beforeIndexing') : null; - $db = $this->db; - $serverType = strtolower($db->getServerType()); - - // Check if the item is in the database. - $query = $db->getQuery(true) - ->select($db->quoteName('link_id') . ', ' . $db->quoteName('md5sum')) - ->from($db->quoteName('#__finder_links')) - ->where($db->quoteName('url') . ' = ' . $db->quote($item->url)); - - // Load the item from the database. - $db->setQuery($query); - $link = $db->loadObject(); - - // Get the indexer state. - $state = static::getState(); - - // Get the signatures of the item. - $curSig = static::getSignature($item); - $oldSig = $link->md5sum ?? null; - - // Get the other item information. - $linkId = empty($link->link_id) ? null : $link->link_id; - $isNew = empty($link->link_id); - - // Check the signatures. If they match, the item is up to date. - if (!$isNew && $curSig == $oldSig) - { - return $linkId; - } - - /* - * If the link already exists, flush all the term maps for the item. - * Maps are stored in 16 tables so we need to iterate through and flush - * each table one at a time. - */ - if (!$isNew) - { - // Flush the maps for the link. - $query->clear() - ->delete($db->quoteName('#__finder_links_terms')) - ->where($db->quoteName('link_id') . ' = ' . (int) $linkId); - $db->setQuery($query); - $db->execute(); - - // Remove the taxonomy maps. - Taxonomy::removeMaps($linkId); - } - - // Mark afterUnmapping in the profiler. - static::$profiler ? static::$profiler->mark('afterUnmapping') : null; - - // Perform cleanup on the item data. - $item->publish_start_date = (int) $item->publish_start_date != 0 ? $item->publish_start_date : null; - $item->publish_end_date = (int) $item->publish_end_date != 0 ? $item->publish_end_date : null; - $item->start_date = (int) $item->start_date != 0 ? $item->start_date : null; - $item->end_date = (int) $item->end_date != 0 ? $item->end_date : null; - - // Prepare the item description. - $item->description = Helper::parse($item->summary ?? ''); - - /* - * Now, we need to enter the item into the links table. If the item - * already exists in the database, we need to use an UPDATE query. - * Otherwise, we need to use an INSERT to get the link id back. - */ - $entry = new \stdClass; - $entry->url = $item->url; - $entry->route = $item->route; - $entry->title = $item->title; - - // We are shortening the description in order to not run into length issues with this field - $entry->description = StringHelper::substr($item->description, 0, 32000); - $entry->indexdate = Factory::getDate()->toSql(); - $entry->state = (int) $item->state; - $entry->access = (int) $item->access; - $entry->language = $item->language; - $entry->type_id = (int) $item->type_id; - $entry->object = ''; - $entry->publish_start_date = $item->publish_start_date; - $entry->publish_end_date = $item->publish_end_date; - $entry->start_date = $item->start_date; - $entry->end_date = $item->end_date; - $entry->list_price = (double) ($item->list_price ?: 0); - $entry->sale_price = (double) ($item->sale_price ?: 0); - - if ($isNew) - { - // Insert the link and get its id. - $db->insertObject('#__finder_links', $entry); - $linkId = (int) $db->insertid(); - } - else - { - // Update the link. - $entry->link_id = $linkId; - $db->updateObject('#__finder_links', $entry, 'link_id'); - } - - // Set up the variables we will need during processing. - $count = 0; - - // Mark afterLinking in the profiler. - static::$profiler ? static::$profiler->mark('afterLinking') : null; - - // Truncate the tokens tables. - $db->truncateTable('#__finder_tokens'); - - // Truncate the tokens aggregate table. - $db->truncateTable('#__finder_tokens_aggregate'); - - /* - * Process the item's content. The items can customize their - * processing instructions to define extra properties to process - * or rearrange how properties are weighted. - */ - foreach ($item->getInstructions() as $group => $properties) - { - // Iterate through the properties of the group. - foreach ($properties as $property) - { - // Check if the property exists in the item. - if (empty($item->$property)) - { - continue; - } - - // Tokenize the property. - if (is_array($item->$property)) - { - // Tokenize an array of content and add it to the database. - foreach ($item->$property as $ip) - { - /* - * If the group is path, we need to a few extra processing - * steps to strip the extension and convert slashes and dashes - * to spaces. - */ - if ($group === static::PATH_CONTEXT) - { - $ip = File::stripExt($ip); - $ip = str_replace(array('/', '-'), ' ', $ip); - } - - // Tokenize a string of content and add it to the database. - $count += $this->tokenizeToDb($ip, $group, $item->language, $format); - - // Check if we're approaching the memory limit of the token table. - if ($count > static::$state->options->get('memory_table_limit', 30000)) - { - $this->toggleTables(false); - } - } - } - else - { - /* - * If the group is path, we need to a few extra processing - * steps to strip the extension and convert slashes and dashes - * to spaces. - */ - if ($group === static::PATH_CONTEXT) - { - $item->$property = File::stripExt($item->$property); - $item->$property = str_replace('/', ' ', $item->$property); - $item->$property = str_replace('-', ' ', $item->$property); - } - - // Tokenize a string of content and add it to the database. - $count += $this->tokenizeToDb($item->$property, $group, $item->language, $format); - - // Check if we're approaching the memory limit of the token table. - if ($count > static::$state->options->get('memory_table_limit', 30000)) - { - $this->toggleTables(false); - } - } - } - } - - /* - * Process the item's taxonomy. The items can customize their - * taxonomy mappings to define extra properties to map. - */ - foreach ($item->getTaxonomy() as $branch => $nodes) - { - // Iterate through the nodes and map them to the branch. - foreach ($nodes as $node) - { - // Add the node to the tree. - if ($node->nested) - { - $nodeId = Taxonomy::addNestedNode($branch, $node->node, $node->state, $node->access, $node->language); - } - else - { - $nodeId = Taxonomy::addNode($branch, $node->title, $node->state, $node->access, $node->language); - } - - // Add the link => node map. - Taxonomy::addMap($linkId, $nodeId); - $node->id = $nodeId; - } - } - - // Mark afterProcessing in the profiler. - static::$profiler ? static::$profiler->mark('afterProcessing') : null; - - /* - * At this point, all of the item's content has been parsed, tokenized - * and inserted into the #__finder_tokens table. Now, we need to - * aggregate all the data into that table into a more usable form. The - * aggregated data will be inserted into #__finder_tokens_aggregate - * table. - */ - $query = 'INSERT INTO ' . $db->quoteName('#__finder_tokens_aggregate') . - ' (' . $db->quoteName('term_id') . - ', ' . $db->quoteName('term') . - ', ' . $db->quoteName('stem') . - ', ' . $db->quoteName('common') . - ', ' . $db->quoteName('phrase') . - ', ' . $db->quoteName('term_weight') . - ', ' . $db->quoteName('context') . - ', ' . $db->quoteName('context_weight') . - ', ' . $db->quoteName('total_weight') . - ', ' . $db->quoteName('language') . ')' . - ' SELECT' . - ' COALESCE(t.term_id, 0), t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context,' . - ' ROUND( t1.weight * COUNT( t2.term ) * %F, 8 ) AS context_weight, 0, t1.language' . - ' FROM (' . - ' SELECT DISTINCT t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context, t1.language' . - ' FROM ' . $db->quoteName('#__finder_tokens') . ' AS t1' . - ' WHERE t1.context = %d' . - ' ) AS t1' . - ' JOIN ' . $db->quoteName('#__finder_tokens') . ' AS t2 ON t2.term = t1.term AND t2.language = t1.language' . - ' LEFT JOIN ' . $db->quoteName('#__finder_terms') . ' AS t ON t.term = t1.term AND t.language = t1.language' . - ' WHERE t2.context = %d' . - ' GROUP BY t1.term, t.term_id, t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context, t1.language' . - ' ORDER BY t1.term DESC'; - - // Iterate through the contexts and aggregate the tokens per context. - foreach ($state->weights as $context => $multiplier) - { - // Run the query to aggregate the tokens for this context.. - $db->setQuery(sprintf($query, $multiplier, $context, $context)); - $db->execute(); - } - - // Mark afterAggregating in the profiler. - static::$profiler ? static::$profiler->mark('afterAggregating') : null; - - /* - * When we pulled down all of the aggregate data, we did a LEFT JOIN - * over the terms table to try to find all the term ids that - * already exist for our tokens. If any of the rows in the aggregate - * table have a term of 0, then no term record exists for that - * term so we need to add it to the terms table. - */ - $db->setQuery( - 'INSERT INTO ' . $db->quoteName('#__finder_terms') . - ' (' . $db->quoteName('term') . - ', ' . $db->quoteName('stem') . - ', ' . $db->quoteName('common') . - ', ' . $db->quoteName('phrase') . - ', ' . $db->quoteName('weight') . - ', ' . $db->quoteName('soundex') . - ', ' . $db->quoteName('language') . ')' . - ' SELECT ta.term, ta.stem, ta.common, ta.phrase, ta.term_weight, SOUNDEX(ta.term), ta.language' . - ' FROM ' . $db->quoteName('#__finder_tokens_aggregate') . ' AS ta' . - ' WHERE ta.term_id = 0' . - ' GROUP BY ta.term, ta.stem, ta.common, ta.phrase, ta.term_weight, SOUNDEX(ta.term), ta.language' - ); - $db->execute(); - - /* - * Now, we just inserted a bunch of new records into the terms table - * so we need to go back and update the aggregate table with all the - * new term ids. - */ - $query = $db->getQuery(true) - ->update($db->quoteName('#__finder_tokens_aggregate', 'ta')) - ->innerJoin($db->quoteName('#__finder_terms', 't'), 't.term = ta.term AND t.language = ta.language') - ->where('ta.term_id = 0'); - - if ($serverType == 'mysql') - { - $query->set($db->quoteName('ta.term_id') . ' = ' . $db->quoteName('t.term_id')); - } - else - { - $query->set($db->quoteName('term_id') . ' = ' . $db->quoteName('t.term_id')); - } - - $db->setQuery($query); - $db->execute(); - - // Mark afterTerms in the profiler. - static::$profiler ? static::$profiler->mark('afterTerms') : null; - - /* - * After we've made sure that all of the terms are in the terms table - * and the aggregate table has the correct term ids, we need to update - * the links counter for each term by one. - */ - $query->clear() - ->update($db->quoteName('#__finder_terms', 't')) - ->innerJoin($db->quoteName('#__finder_tokens_aggregate', 'ta'), 'ta.term_id = t.term_id'); - - if ($serverType == 'mysql') - { - $query->set($db->quoteName('t.links') . ' = t.links + 1'); - } - else - { - $query->set($db->quoteName('links') . ' = t.links + 1'); - } - - $db->setQuery($query); - $db->execute(); - - // Mark afterTerms in the profiler. - static::$profiler ? static::$profiler->mark('afterTerms') : null; - - /* - * At this point, the aggregate table contains a record for each - * term in each context. So, we're going to pull down all of that - * data while grouping the records by term and add all of the - * sub-totals together to arrive at the final total for each token for - * this link. Then, we insert all of that data into the mapping table. - */ - $db->setQuery( - 'INSERT INTO ' . $db->quoteName('#__finder_links_terms') . - ' (' . $db->quoteName('link_id') . - ', ' . $db->quoteName('term_id') . - ', ' . $db->quoteName('weight') . ')' . - ' SELECT ' . (int) $linkId . ', ' . $db->quoteName('term_id') . ',' . - ' ROUND(SUM(' . $db->quoteName('context_weight') . '), 8)' . - ' FROM ' . $db->quoteName('#__finder_tokens_aggregate') . - ' GROUP BY ' . $db->quoteName('term') . ', ' . $db->quoteName('term_id') . - ' ORDER BY ' . $db->quoteName('term') . ' DESC' - ); - $db->execute(); - - // Mark afterMapping in the profiler. - static::$profiler ? static::$profiler->mark('afterMapping') : null; - - // Update the signature. - $object = serialize($item); - $query->clear() - ->update($db->quoteName('#__finder_links')) - ->set($db->quoteName('md5sum') . ' = :md5sum') - ->set($db->quoteName('object') . ' = :object') - ->where($db->quoteName('link_id') . ' = :linkid') - ->bind(':md5sum', $curSig) - ->bind(':object', $object, ParameterType::LARGE_OBJECT) - ->bind(':linkid', $linkId, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - // Mark afterSigning in the profiler. - static::$profiler ? static::$profiler->mark('afterSigning') : null; - - // Truncate the tokens tables. - $db->truncateTable('#__finder_tokens'); - - // Truncate the tokens aggregate table. - $db->truncateTable('#__finder_tokens_aggregate'); - - // Toggle the token tables back to memory tables. - $this->toggleTables(true); - - // Mark afterTruncating in the profiler. - static::$profiler ? static::$profiler->mark('afterTruncating') : null; - - // Trigger a plugin event after indexing - PluginHelper::importPlugin('finder'); - Factory::getApplication()->triggerEvent('onFinderIndexAfterIndex', array($item, $linkId)); - - return $linkId; - } - - /** - * Method to remove a link from the index. - * - * @param integer $linkId The id of the link. - * @param bool $removeTaxonomies Remove empty taxonomies - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - public function remove($linkId, $removeTaxonomies = true) - { - $db = $this->db; - $query = $db->getQuery(true); - $linkId = (int) $linkId; - - // Update the link counts for the terms. - $query->clear() - ->update($db->quoteName('#__finder_terms', 't')) - ->join('INNER', $db->quoteName('#__finder_links_terms', 'm'), $db->quoteName('m.term_id') . ' = ' . $db->quoteName('t.term_id')) - ->set($db->quoteName('links') . ' = ' . $db->quoteName('links') . ' - 1') - ->where($db->quoteName('m.link_id') . ' = :linkid') - ->bind(':linkid', $linkId, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - // Remove all records from the mapping tables. - $query->clear() - ->delete($db->quoteName('#__finder_links_terms')) - ->where($db->quoteName('link_id') . ' = :linkid') - ->bind(':linkid', $linkId, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - // Delete all orphaned terms. - $query->clear() - ->delete($db->quoteName('#__finder_terms')) - ->where($db->quoteName('links') . ' <= 0'); - $db->setQuery($query)->execute(); - - // Delete the link from the index. - $query->clear() - ->delete($db->quoteName('#__finder_links')) - ->where($db->quoteName('link_id') . ' = :linkid') - ->bind(':linkid', $linkId, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - // Remove the taxonomy maps. - Taxonomy::removeMaps($linkId); - - // Remove the orphaned taxonomy nodes. - if ($removeTaxonomies) - { - Taxonomy::removeOrphanNodes(); - } - - PluginHelper::importPlugin('finder'); - Factory::getApplication()->triggerEvent('onFinderIndexAfterDelete', array($linkId)); - - return true; - } - - /** - * Method to optimize the index. We use this method to remove unused terms - * and any other optimizations that might be necessary. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - public function optimize() - { - // Get the database object. - $db = $this->db; - $serverType = strtolower($db->getServerType()); - $query = $db->getQuery(true); - - // Delete all orphaned terms. - $query->delete($db->quoteName('#__finder_terms')) - ->where($db->quoteName('links') . ' <= 0'); - $db->setQuery($query); - $db->execute(); - - // Delete all broken links. (Links missing the object) - $query = $db->getQuery(true) - ->delete('#__finder_links') - ->where($db->quoteName('object') . ' = ' . $db->quote('')); - $db->setQuery($query); - $db->execute(); - - // Delete all orphaned mappings of terms to links - $query2 = $db->getQuery(true) - ->select($db->quoteName('link_id')) - ->from($db->quoteName('#__finder_links')); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__finder_links_terms')) - ->where($db->quoteName('link_id') . ' NOT IN (' . $query2 . ')'); - $db->setQuery($query); - $db->execute(); - - // Delete all orphaned terms - $query2 = $db->getQuery(true) - ->select($db->quoteName('term_id')) - ->from($db->quoteName('#__finder_links_terms')); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__finder_terms')) - ->where($db->quoteName('term_id') . ' NOT IN (' . $query2 . ')'); - $db->setQuery($query); - $db->execute(); - - // Delete all orphaned taxonomies - Taxonomy::removeOrphanMaps(); - Taxonomy::removeOrphanNodes(); - - // Optimize the tables. - $tables = [ - '#__finder_links', - '#__finder_links_terms', - '#__finder_filters', - '#__finder_terms_common', - '#__finder_types', - '#__finder_taxonomy_map', - '#__finder_taxonomy' - ]; - - foreach ($tables as $table) - { - if ($serverType == 'mysql') - { - $db->setQuery('OPTIMIZE TABLE ' . $db->quoteName($table)); - $db->execute(); - } - else - { - $db->setQuery('VACUUM ' . $db->quoteName($table)); - $db->execute(); - $db->setQuery('REINDEX TABLE ' . $db->quoteName($table)); - $db->execute(); - } - } - - return true; - } - - /** - * Method to get a content item's signature. - * - * @param object $item The content item to index. - * - * @return string The content item's signature. - * - * @since 2.5 - */ - protected static function getSignature($item) - { - // Get the indexer state. - $state = static::getState(); - - // Get the relevant configuration variables. - $config = array( - $state->weights, - $state->options->get('stem', 1), - $state->options->get('stemmer', 'porter_en') - ); - - return md5(serialize(array($item, $config))); - } - - /** - * Method to parse input, tokenize it, and then add it to the database. - * - * @param mixed $input String or resource to use as input. A resource input will automatically be chunked to conserve - * memory. Strings will be chunked if longer than 2K in size. - * @param integer $context The context of the input. See context constants. - * @param string $lang The language of the input. - * @param string $format The format of the input. - * - * @return integer The number of tokens extracted from the input. - * - * @since 2.5 - */ - protected function tokenizeToDb($input, $context, $lang, $format) - { - $count = 0; - $buffer = null; - - if (empty($input)) - { - return $count; - } - - // If the input is a resource, batch the process out. - if (is_resource($input)) - { - // Batch the process out to avoid memory limits. - while (!feof($input)) - { - // Read into the buffer. - $buffer .= fread($input, 2048); - - /* - * If we haven't reached the end of the file, seek to the last - * space character and drop whatever is after that to make sure - * we didn't truncate a term while reading the input. - */ - if (!feof($input)) - { - // Find the last space character. - $ls = strrpos($buffer, ' '); - - // Adjust string based on the last space character. - if ($ls) - { - // Truncate the string to the last space character. - $string = substr($buffer, 0, $ls); - - // Adjust the buffer based on the last space for the next iteration and trim. - $buffer = StringHelper::trim(substr($buffer, $ls)); - } - // No space character was found. - else - { - $string = $buffer; - } - } - // We've reached the end of the file, so parse whatever remains. - else - { - $string = $buffer; - } - - // Parse, tokenise and add tokens to the database. - $count = $this->tokenizeToDbShort($string, $context, $lang, $format, $count); - - unset($string); - } - - return $count; - } - - // Parse, tokenise and add tokens to the database. - $count = $this->tokenizeToDbShort($input, $context, $lang, $format, $count); - - return $count; - } - - /** - * Method to parse input, tokenise it, then add the tokens to the database. - * - * @param string $input String to parse, tokenise and add to database. - * @param integer $context The context of the input. See context constants. - * @param string $lang The language of the input. - * @param string $format The format of the input. - * @param integer $count The number of tokens processed so far. - * - * @return integer Cumulative number of tokens extracted from the input so far. - * - * @since 3.7.0 - */ - private function tokenizeToDbShort($input, $context, $lang, $format, $count) - { - // Parse the input. - $input = Helper::parse($input, $format); - - // Check the input. - if (empty($input)) - { - return $count; - } - - // Tokenize the input. - $tokens = Helper::tokenize($input, $lang); - - if (count($tokens) == 0) - { - return $count; - } - - // Add the tokens to the database. - $count += $this->addTokensToDb($tokens, $context); - - // Check if we're approaching the memory limit of the token table. - if ($count > static::$state->options->get('memory_table_limit', 10000)) - { - $this->toggleTables(false); - } - - return $count; - } - - /** - * Method to add a set of tokens to the database. - * - * @param Token[]|Token $tokens An array or single Token object. - * @param mixed $context The context of the tokens. See context constants. [optional] - * - * @return integer The number of tokens inserted into the database. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function addTokensToDb($tokens, $context = '') - { - static $filterCommon, $filterNumeric; - - if (is_null($filterCommon)) - { - $params = ComponentHelper::getParams('com_finder'); - $filterCommon = $params->get('filter_commonwords', false); - $filterNumeric = $params->get('filter_numerics', false); - } - - // Get the database object. - $db = $this->db; - - $query = clone $this->addTokensToDbQueryTemplate; - - // Check if a single FinderIndexerToken object was given and make it to be an array of FinderIndexerToken objects - $tokens = is_array($tokens) ? $tokens : array($tokens); - - // Count the number of token values. - $values = 0; - - // Break into chunks of no more than 128 items - $chunks = array_chunk($tokens, 128); - - foreach ($chunks as $tokens) - { - $query->clear('values'); - - foreach ($tokens as $token) - { - // Database size for a term field - if ($token->length > 75) - { - continue; - } - - if ($filterCommon && $token->common) - { - continue; - } - - if ($filterNumeric && $token->numeric) - { - continue; - } - - $query->values( - $db->quote($token->term) . ', ' - . $db->quote($token->stem) . ', ' - . (int) $token->common . ', ' - . (int) $token->phrase . ', ' - . $db->quote($token->weight) . ', ' - . (int) $context . ', ' - . $db->quote($token->language) - ); - ++$values; - } - - // Only execute the query if there are tokens to insert - if ($query->values !== null) - { - $db->setQuery($query)->execute(); - } - - // Check if we're approaching the memory limit of the token table. - if ($values > static::$state->options->get('memory_table_limit', 10000)) - { - $this->toggleTables(false); - } - } - - return $values; - } - - /** - * Method to switch the token tables from Memory tables to Disk tables - * when they are close to running out of memory. - * Since this is not supported/implemented in all DB-drivers, the default is a stub method, which simply returns true. - * - * @param boolean $memory Flag to control how they should be toggled. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function toggleTables($memory) - { - if (strtolower($this->db->getServerType()) != 'mysql') - { - return true; - } - - static $state; - - // Get the database adapter. - $db = $this->db; - - // Check if we are setting the tables to the Memory engine. - if ($memory === true && $state !== true) - { - // Set the tokens table to Memory. - $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens') . ' ENGINE = MEMORY'); - $db->execute(); - - // Set the tokens aggregate table to Memory. - $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens_aggregate') . ' ENGINE = MEMORY'); - $db->execute(); - - // Set the internal state. - $state = $memory; - } - // We must be setting the tables to the InnoDB engine. - elseif ($memory === false && $state !== false) - { - // Set the tokens table to InnoDB. - $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens') . ' ENGINE = INNODB'); - $db->execute(); - - // Set the tokens aggregate table to InnoDB. - $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens_aggregate') . ' ENGINE = INNODB'); - $db->execute(); - - // Set the internal state. - $state = $memory; - } - - return true; - } + /** + * The title context identifier. + * + * @var integer + * @since 2.5 + */ + const TITLE_CONTEXT = 1; + + /** + * The text context identifier. + * + * @var integer + * @since 2.5 + */ + const TEXT_CONTEXT = 2; + + /** + * The meta context identifier. + * + * @var integer + * @since 2.5 + */ + const META_CONTEXT = 3; + + /** + * The path context identifier. + * + * @var integer + * @since 2.5 + */ + const PATH_CONTEXT = 4; + + /** + * The misc context identifier. + * + * @var integer + * @since 2.5 + */ + const MISC_CONTEXT = 5; + + /** + * The indexer state object. + * + * @var CMSObject + * @since 2.5 + */ + public static $state; + + /** + * The indexer profiler object. + * + * @var Profiler + * @since 2.5 + */ + public static $profiler; + + /** + * Database driver cache. + * + * @var \Joomla\Database\DatabaseDriver + * @since 3.8.0 + */ + protected $db; + + /** + * Reusable Query Template. To be used with clone. + * + * @var QueryInterface + * @since 3.8.0 + */ + protected $addTokensToDbQueryTemplate; + + /** + * Indexer constructor. + * + * @param DatabaseInterface $db The database + * + * @since 3.8.0 + */ + public function __construct(DatabaseInterface $db = null) + { + if ($db === null) { + @trigger_error(sprintf('Database will be mandatory in 5.0.'), E_USER_DEPRECATED); + $db = Factory::getContainer()->get(DatabaseInterface::class); + } + + $this->db = $db; + + // Set up query template for addTokensToDb + $this->addTokensToDbQueryTemplate = $db->getQuery(true)->insert($db->quoteName('#__finder_tokens')) + ->columns( + array( + $db->quoteName('term'), + $db->quoteName('stem'), + $db->quoteName('common'), + $db->quoteName('phrase'), + $db->quoteName('weight'), + $db->quoteName('context'), + $db->quoteName('language') + ) + ); + } + + /** + * Method to get the indexer state. + * + * @return object The indexer state object. + * + * @since 2.5 + */ + public static function getState() + { + // First, try to load from the internal state. + if ((bool) static::$state) { + return static::$state; + } + + // If we couldn't load from the internal state, try the session. + $session = Factory::getSession(); + $data = $session->get('_finder.state', null); + + // If the state is empty, load the values for the first time. + if (empty($data)) { + $data = new CMSObject(); + + // Load the default configuration options. + $data->options = ComponentHelper::getParams('com_finder'); + + // Setup the weight lookup information. + $data->weights = array( + self::TITLE_CONTEXT => round($data->options->get('title_multiplier', 1.7), 2), + self::TEXT_CONTEXT => round($data->options->get('text_multiplier', 0.7), 2), + self::META_CONTEXT => round($data->options->get('meta_multiplier', 1.2), 2), + self::PATH_CONTEXT => round($data->options->get('path_multiplier', 2.0), 2), + self::MISC_CONTEXT => round($data->options->get('misc_multiplier', 0.3), 2) + ); + + // Set the current time as the start time. + $data->startTime = Factory::getDate()->toSql(); + + // Set the remaining default values. + $data->batchSize = (int) $data->options->get('batch_size', 50); + $data->batchOffset = 0; + $data->totalItems = 0; + $data->pluginState = array(); + } + + // Setup the profiler if debugging is enabled. + if (Factory::getApplication()->get('debug')) { + static::$profiler = Profiler::getInstance('FinderIndexer'); + } + + // Set the state. + static::$state = $data; + + return static::$state; + } + + /** + * Method to set the indexer state. + * + * @param CMSObject $data A new indexer state object. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + */ + public static function setState($data) + { + // Check the state object. + if (empty($data) || !$data instanceof CMSObject) { + return false; + } + + // Set the new internal state. + static::$state = $data; + + // Set the new session state. + Factory::getSession()->set('_finder.state', $data); + + return true; + } + + /** + * Method to reset the indexer state. + * + * @return void + * + * @since 2.5 + */ + public static function resetState() + { + // Reset the internal state to null. + self::$state = null; + + // Reset the session state to null. + Factory::getSession()->set('_finder.state', null); + } + + /** + * Method to index a content item. + * + * @param Result $item The content item to index. + * @param string $format The format of the content. [optional] + * + * @return integer The ID of the record in the links table. + * + * @since 2.5 + * @throws \Exception on database error. + */ + public function index($item, $format = 'html') + { + // Mark beforeIndexing in the profiler. + static::$profiler ? static::$profiler->mark('beforeIndexing') : null; + $db = $this->db; + $serverType = strtolower($db->getServerType()); + + // Check if the item is in the database. + $query = $db->getQuery(true) + ->select($db->quoteName('link_id') . ', ' . $db->quoteName('md5sum')) + ->from($db->quoteName('#__finder_links')) + ->where($db->quoteName('url') . ' = ' . $db->quote($item->url)); + + // Load the item from the database. + $db->setQuery($query); + $link = $db->loadObject(); + + // Get the indexer state. + $state = static::getState(); + + // Get the signatures of the item. + $curSig = static::getSignature($item); + $oldSig = $link->md5sum ?? null; + + // Get the other item information. + $linkId = empty($link->link_id) ? null : $link->link_id; + $isNew = empty($link->link_id); + + // Check the signatures. If they match, the item is up to date. + if (!$isNew && $curSig == $oldSig) { + return $linkId; + } + + /* + * If the link already exists, flush all the term maps for the item. + * Maps are stored in 16 tables so we need to iterate through and flush + * each table one at a time. + */ + if (!$isNew) { + // Flush the maps for the link. + $query->clear() + ->delete($db->quoteName('#__finder_links_terms')) + ->where($db->quoteName('link_id') . ' = ' . (int) $linkId); + $db->setQuery($query); + $db->execute(); + + // Remove the taxonomy maps. + Taxonomy::removeMaps($linkId); + } + + // Mark afterUnmapping in the profiler. + static::$profiler ? static::$profiler->mark('afterUnmapping') : null; + + // Perform cleanup on the item data. + $item->publish_start_date = (int) $item->publish_start_date != 0 ? $item->publish_start_date : null; + $item->publish_end_date = (int) $item->publish_end_date != 0 ? $item->publish_end_date : null; + $item->start_date = (int) $item->start_date != 0 ? $item->start_date : null; + $item->end_date = (int) $item->end_date != 0 ? $item->end_date : null; + + // Prepare the item description. + $item->description = Helper::parse($item->summary ?? ''); + + /* + * Now, we need to enter the item into the links table. If the item + * already exists in the database, we need to use an UPDATE query. + * Otherwise, we need to use an INSERT to get the link id back. + */ + $entry = new \stdClass(); + $entry->url = $item->url; + $entry->route = $item->route; + $entry->title = $item->title; + + // We are shortening the description in order to not run into length issues with this field + $entry->description = StringHelper::substr($item->description, 0, 32000); + $entry->indexdate = Factory::getDate()->toSql(); + $entry->state = (int) $item->state; + $entry->access = (int) $item->access; + $entry->language = $item->language; + $entry->type_id = (int) $item->type_id; + $entry->object = ''; + $entry->publish_start_date = $item->publish_start_date; + $entry->publish_end_date = $item->publish_end_date; + $entry->start_date = $item->start_date; + $entry->end_date = $item->end_date; + $entry->list_price = (double) ($item->list_price ?: 0); + $entry->sale_price = (double) ($item->sale_price ?: 0); + + if ($isNew) { + // Insert the link and get its id. + $db->insertObject('#__finder_links', $entry); + $linkId = (int) $db->insertid(); + } else { + // Update the link. + $entry->link_id = $linkId; + $db->updateObject('#__finder_links', $entry, 'link_id'); + } + + // Set up the variables we will need during processing. + $count = 0; + + // Mark afterLinking in the profiler. + static::$profiler ? static::$profiler->mark('afterLinking') : null; + + // Truncate the tokens tables. + $db->truncateTable('#__finder_tokens'); + + // Truncate the tokens aggregate table. + $db->truncateTable('#__finder_tokens_aggregate'); + + /* + * Process the item's content. The items can customize their + * processing instructions to define extra properties to process + * or rearrange how properties are weighted. + */ + foreach ($item->getInstructions() as $group => $properties) { + // Iterate through the properties of the group. + foreach ($properties as $property) { + // Check if the property exists in the item. + if (empty($item->$property)) { + continue; + } + + // Tokenize the property. + if (is_array($item->$property)) { + // Tokenize an array of content and add it to the database. + foreach ($item->$property as $ip) { + /* + * If the group is path, we need to a few extra processing + * steps to strip the extension and convert slashes and dashes + * to spaces. + */ + if ($group === static::PATH_CONTEXT) { + $ip = File::stripExt($ip); + $ip = str_replace(array('/', '-'), ' ', $ip); + } + + // Tokenize a string of content and add it to the database. + $count += $this->tokenizeToDb($ip, $group, $item->language, $format); + + // Check if we're approaching the memory limit of the token table. + if ($count > static::$state->options->get('memory_table_limit', 30000)) { + $this->toggleTables(false); + } + } + } else { + /* + * If the group is path, we need to a few extra processing + * steps to strip the extension and convert slashes and dashes + * to spaces. + */ + if ($group === static::PATH_CONTEXT) { + $item->$property = File::stripExt($item->$property); + $item->$property = str_replace('/', ' ', $item->$property); + $item->$property = str_replace('-', ' ', $item->$property); + } + + // Tokenize a string of content and add it to the database. + $count += $this->tokenizeToDb($item->$property, $group, $item->language, $format); + + // Check if we're approaching the memory limit of the token table. + if ($count > static::$state->options->get('memory_table_limit', 30000)) { + $this->toggleTables(false); + } + } + } + } + + /* + * Process the item's taxonomy. The items can customize their + * taxonomy mappings to define extra properties to map. + */ + foreach ($item->getTaxonomy() as $branch => $nodes) { + // Iterate through the nodes and map them to the branch. + foreach ($nodes as $node) { + // Add the node to the tree. + if ($node->nested) { + $nodeId = Taxonomy::addNestedNode($branch, $node->node, $node->state, $node->access, $node->language); + } else { + $nodeId = Taxonomy::addNode($branch, $node->title, $node->state, $node->access, $node->language); + } + + // Add the link => node map. + Taxonomy::addMap($linkId, $nodeId); + $node->id = $nodeId; + } + } + + // Mark afterProcessing in the profiler. + static::$profiler ? static::$profiler->mark('afterProcessing') : null; + + /* + * At this point, all of the item's content has been parsed, tokenized + * and inserted into the #__finder_tokens table. Now, we need to + * aggregate all the data into that table into a more usable form. The + * aggregated data will be inserted into #__finder_tokens_aggregate + * table. + */ + $query = 'INSERT INTO ' . $db->quoteName('#__finder_tokens_aggregate') . + ' (' . $db->quoteName('term_id') . + ', ' . $db->quoteName('term') . + ', ' . $db->quoteName('stem') . + ', ' . $db->quoteName('common') . + ', ' . $db->quoteName('phrase') . + ', ' . $db->quoteName('term_weight') . + ', ' . $db->quoteName('context') . + ', ' . $db->quoteName('context_weight') . + ', ' . $db->quoteName('total_weight') . + ', ' . $db->quoteName('language') . ')' . + ' SELECT' . + ' COALESCE(t.term_id, 0), t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context,' . + ' ROUND( t1.weight * COUNT( t2.term ) * %F, 8 ) AS context_weight, 0, t1.language' . + ' FROM (' . + ' SELECT DISTINCT t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context, t1.language' . + ' FROM ' . $db->quoteName('#__finder_tokens') . ' AS t1' . + ' WHERE t1.context = %d' . + ' ) AS t1' . + ' JOIN ' . $db->quoteName('#__finder_tokens') . ' AS t2 ON t2.term = t1.term AND t2.language = t1.language' . + ' LEFT JOIN ' . $db->quoteName('#__finder_terms') . ' AS t ON t.term = t1.term AND t.language = t1.language' . + ' WHERE t2.context = %d' . + ' GROUP BY t1.term, t.term_id, t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context, t1.language' . + ' ORDER BY t1.term DESC'; + + // Iterate through the contexts and aggregate the tokens per context. + foreach ($state->weights as $context => $multiplier) { + // Run the query to aggregate the tokens for this context.. + $db->setQuery(sprintf($query, $multiplier, $context, $context)); + $db->execute(); + } + + // Mark afterAggregating in the profiler. + static::$profiler ? static::$profiler->mark('afterAggregating') : null; + + /* + * When we pulled down all of the aggregate data, we did a LEFT JOIN + * over the terms table to try to find all the term ids that + * already exist for our tokens. If any of the rows in the aggregate + * table have a term of 0, then no term record exists for that + * term so we need to add it to the terms table. + */ + $db->setQuery( + 'INSERT INTO ' . $db->quoteName('#__finder_terms') . + ' (' . $db->quoteName('term') . + ', ' . $db->quoteName('stem') . + ', ' . $db->quoteName('common') . + ', ' . $db->quoteName('phrase') . + ', ' . $db->quoteName('weight') . + ', ' . $db->quoteName('soundex') . + ', ' . $db->quoteName('language') . ')' . + ' SELECT ta.term, ta.stem, ta.common, ta.phrase, ta.term_weight, SOUNDEX(ta.term), ta.language' . + ' FROM ' . $db->quoteName('#__finder_tokens_aggregate') . ' AS ta' . + ' WHERE ta.term_id = 0' . + ' GROUP BY ta.term, ta.stem, ta.common, ta.phrase, ta.term_weight, SOUNDEX(ta.term), ta.language' + ); + $db->execute(); + + /* + * Now, we just inserted a bunch of new records into the terms table + * so we need to go back and update the aggregate table with all the + * new term ids. + */ + $query = $db->getQuery(true) + ->update($db->quoteName('#__finder_tokens_aggregate', 'ta')) + ->innerJoin($db->quoteName('#__finder_terms', 't'), 't.term = ta.term AND t.language = ta.language') + ->where('ta.term_id = 0'); + + if ($serverType == 'mysql') { + $query->set($db->quoteName('ta.term_id') . ' = ' . $db->quoteName('t.term_id')); + } else { + $query->set($db->quoteName('term_id') . ' = ' . $db->quoteName('t.term_id')); + } + + $db->setQuery($query); + $db->execute(); + + // Mark afterTerms in the profiler. + static::$profiler ? static::$profiler->mark('afterTerms') : null; + + /* + * After we've made sure that all of the terms are in the terms table + * and the aggregate table has the correct term ids, we need to update + * the links counter for each term by one. + */ + $query->clear() + ->update($db->quoteName('#__finder_terms', 't')) + ->innerJoin($db->quoteName('#__finder_tokens_aggregate', 'ta'), 'ta.term_id = t.term_id'); + + if ($serverType == 'mysql') { + $query->set($db->quoteName('t.links') . ' = t.links + 1'); + } else { + $query->set($db->quoteName('links') . ' = t.links + 1'); + } + + $db->setQuery($query); + $db->execute(); + + // Mark afterTerms in the profiler. + static::$profiler ? static::$profiler->mark('afterTerms') : null; + + /* + * At this point, the aggregate table contains a record for each + * term in each context. So, we're going to pull down all of that + * data while grouping the records by term and add all of the + * sub-totals together to arrive at the final total for each token for + * this link. Then, we insert all of that data into the mapping table. + */ + $db->setQuery( + 'INSERT INTO ' . $db->quoteName('#__finder_links_terms') . + ' (' . $db->quoteName('link_id') . + ', ' . $db->quoteName('term_id') . + ', ' . $db->quoteName('weight') . ')' . + ' SELECT ' . (int) $linkId . ', ' . $db->quoteName('term_id') . ',' . + ' ROUND(SUM(' . $db->quoteName('context_weight') . '), 8)' . + ' FROM ' . $db->quoteName('#__finder_tokens_aggregate') . + ' GROUP BY ' . $db->quoteName('term') . ', ' . $db->quoteName('term_id') . + ' ORDER BY ' . $db->quoteName('term') . ' DESC' + ); + $db->execute(); + + // Mark afterMapping in the profiler. + static::$profiler ? static::$profiler->mark('afterMapping') : null; + + // Update the signature. + $object = serialize($item); + $query->clear() + ->update($db->quoteName('#__finder_links')) + ->set($db->quoteName('md5sum') . ' = :md5sum') + ->set($db->quoteName('object') . ' = :object') + ->where($db->quoteName('link_id') . ' = :linkid') + ->bind(':md5sum', $curSig) + ->bind(':object', $object, ParameterType::LARGE_OBJECT) + ->bind(':linkid', $linkId, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + // Mark afterSigning in the profiler. + static::$profiler ? static::$profiler->mark('afterSigning') : null; + + // Truncate the tokens tables. + $db->truncateTable('#__finder_tokens'); + + // Truncate the tokens aggregate table. + $db->truncateTable('#__finder_tokens_aggregate'); + + // Toggle the token tables back to memory tables. + $this->toggleTables(true); + + // Mark afterTruncating in the profiler. + static::$profiler ? static::$profiler->mark('afterTruncating') : null; + + // Trigger a plugin event after indexing + PluginHelper::importPlugin('finder'); + Factory::getApplication()->triggerEvent('onFinderIndexAfterIndex', array($item, $linkId)); + + return $linkId; + } + + /** + * Method to remove a link from the index. + * + * @param integer $linkId The id of the link. + * @param bool $removeTaxonomies Remove empty taxonomies + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + public function remove($linkId, $removeTaxonomies = true) + { + $db = $this->db; + $query = $db->getQuery(true); + $linkId = (int) $linkId; + + // Update the link counts for the terms. + $query->clear() + ->update($db->quoteName('#__finder_terms', 't')) + ->join('INNER', $db->quoteName('#__finder_links_terms', 'm'), $db->quoteName('m.term_id') . ' = ' . $db->quoteName('t.term_id')) + ->set($db->quoteName('links') . ' = ' . $db->quoteName('links') . ' - 1') + ->where($db->quoteName('m.link_id') . ' = :linkid') + ->bind(':linkid', $linkId, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + // Remove all records from the mapping tables. + $query->clear() + ->delete($db->quoteName('#__finder_links_terms')) + ->where($db->quoteName('link_id') . ' = :linkid') + ->bind(':linkid', $linkId, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + // Delete all orphaned terms. + $query->clear() + ->delete($db->quoteName('#__finder_terms')) + ->where($db->quoteName('links') . ' <= 0'); + $db->setQuery($query)->execute(); + + // Delete the link from the index. + $query->clear() + ->delete($db->quoteName('#__finder_links')) + ->where($db->quoteName('link_id') . ' = :linkid') + ->bind(':linkid', $linkId, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + // Remove the taxonomy maps. + Taxonomy::removeMaps($linkId); + + // Remove the orphaned taxonomy nodes. + if ($removeTaxonomies) { + Taxonomy::removeOrphanNodes(); + } + + PluginHelper::importPlugin('finder'); + Factory::getApplication()->triggerEvent('onFinderIndexAfterDelete', array($linkId)); + + return true; + } + + /** + * Method to optimize the index. We use this method to remove unused terms + * and any other optimizations that might be necessary. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + public function optimize() + { + // Get the database object. + $db = $this->db; + $serverType = strtolower($db->getServerType()); + $query = $db->getQuery(true); + + // Delete all orphaned terms. + $query->delete($db->quoteName('#__finder_terms')) + ->where($db->quoteName('links') . ' <= 0'); + $db->setQuery($query); + $db->execute(); + + // Delete all broken links. (Links missing the object) + $query = $db->getQuery(true) + ->delete('#__finder_links') + ->where($db->quoteName('object') . ' = ' . $db->quote('')); + $db->setQuery($query); + $db->execute(); + + // Delete all orphaned mappings of terms to links + $query2 = $db->getQuery(true) + ->select($db->quoteName('link_id')) + ->from($db->quoteName('#__finder_links')); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__finder_links_terms')) + ->where($db->quoteName('link_id') . ' NOT IN (' . $query2 . ')'); + $db->setQuery($query); + $db->execute(); + + // Delete all orphaned terms + $query2 = $db->getQuery(true) + ->select($db->quoteName('term_id')) + ->from($db->quoteName('#__finder_links_terms')); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__finder_terms')) + ->where($db->quoteName('term_id') . ' NOT IN (' . $query2 . ')'); + $db->setQuery($query); + $db->execute(); + + // Delete all orphaned taxonomies + Taxonomy::removeOrphanMaps(); + Taxonomy::removeOrphanNodes(); + + // Optimize the tables. + $tables = [ + '#__finder_links', + '#__finder_links_terms', + '#__finder_filters', + '#__finder_terms_common', + '#__finder_types', + '#__finder_taxonomy_map', + '#__finder_taxonomy' + ]; + + foreach ($tables as $table) { + if ($serverType == 'mysql') { + $db->setQuery('OPTIMIZE TABLE ' . $db->quoteName($table)); + $db->execute(); + } else { + $db->setQuery('VACUUM ' . $db->quoteName($table)); + $db->execute(); + $db->setQuery('REINDEX TABLE ' . $db->quoteName($table)); + $db->execute(); + } + } + + return true; + } + + /** + * Method to get a content item's signature. + * + * @param object $item The content item to index. + * + * @return string The content item's signature. + * + * @since 2.5 + */ + protected static function getSignature($item) + { + // Get the indexer state. + $state = static::getState(); + + // Get the relevant configuration variables. + $config = array( + $state->weights, + $state->options->get('stem', 1), + $state->options->get('stemmer', 'porter_en') + ); + + return md5(serialize(array($item, $config))); + } + + /** + * Method to parse input, tokenize it, and then add it to the database. + * + * @param mixed $input String or resource to use as input. A resource input will automatically be chunked to conserve + * memory. Strings will be chunked if longer than 2K in size. + * @param integer $context The context of the input. See context constants. + * @param string $lang The language of the input. + * @param string $format The format of the input. + * + * @return integer The number of tokens extracted from the input. + * + * @since 2.5 + */ + protected function tokenizeToDb($input, $context, $lang, $format) + { + $count = 0; + $buffer = null; + + if (empty($input)) { + return $count; + } + + // If the input is a resource, batch the process out. + if (is_resource($input)) { + // Batch the process out to avoid memory limits. + while (!feof($input)) { + // Read into the buffer. + $buffer .= fread($input, 2048); + + /* + * If we haven't reached the end of the file, seek to the last + * space character and drop whatever is after that to make sure + * we didn't truncate a term while reading the input. + */ + if (!feof($input)) { + // Find the last space character. + $ls = strrpos($buffer, ' '); + + // Adjust string based on the last space character. + if ($ls) { + // Truncate the string to the last space character. + $string = substr($buffer, 0, $ls); + + // Adjust the buffer based on the last space for the next iteration and trim. + $buffer = StringHelper::trim(substr($buffer, $ls)); + } + // No space character was found. + else { + $string = $buffer; + } + } + // We've reached the end of the file, so parse whatever remains. + else { + $string = $buffer; + } + + // Parse, tokenise and add tokens to the database. + $count = $this->tokenizeToDbShort($string, $context, $lang, $format, $count); + + unset($string); + } + + return $count; + } + + // Parse, tokenise and add tokens to the database. + $count = $this->tokenizeToDbShort($input, $context, $lang, $format, $count); + + return $count; + } + + /** + * Method to parse input, tokenise it, then add the tokens to the database. + * + * @param string $input String to parse, tokenise and add to database. + * @param integer $context The context of the input. See context constants. + * @param string $lang The language of the input. + * @param string $format The format of the input. + * @param integer $count The number of tokens processed so far. + * + * @return integer Cumulative number of tokens extracted from the input so far. + * + * @since 3.7.0 + */ + private function tokenizeToDbShort($input, $context, $lang, $format, $count) + { + // Parse the input. + $input = Helper::parse($input, $format); + + // Check the input. + if (empty($input)) { + return $count; + } + + // Tokenize the input. + $tokens = Helper::tokenize($input, $lang); + + if (count($tokens) == 0) { + return $count; + } + + // Add the tokens to the database. + $count += $this->addTokensToDb($tokens, $context); + + // Check if we're approaching the memory limit of the token table. + if ($count > static::$state->options->get('memory_table_limit', 10000)) { + $this->toggleTables(false); + } + + return $count; + } + + /** + * Method to add a set of tokens to the database. + * + * @param Token[]|Token $tokens An array or single Token object. + * @param mixed $context The context of the tokens. See context constants. [optional] + * + * @return integer The number of tokens inserted into the database. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function addTokensToDb($tokens, $context = '') + { + static $filterCommon, $filterNumeric; + + if (is_null($filterCommon)) { + $params = ComponentHelper::getParams('com_finder'); + $filterCommon = $params->get('filter_commonwords', false); + $filterNumeric = $params->get('filter_numerics', false); + } + + // Get the database object. + $db = $this->db; + + $query = clone $this->addTokensToDbQueryTemplate; + + // Check if a single FinderIndexerToken object was given and make it to be an array of FinderIndexerToken objects + $tokens = is_array($tokens) ? $tokens : array($tokens); + + // Count the number of token values. + $values = 0; + + // Break into chunks of no more than 128 items + $chunks = array_chunk($tokens, 128); + + foreach ($chunks as $tokens) { + $query->clear('values'); + + foreach ($tokens as $token) { + // Database size for a term field + if ($token->length > 75) { + continue; + } + + if ($filterCommon && $token->common) { + continue; + } + + if ($filterNumeric && $token->numeric) { + continue; + } + + $query->values( + $db->quote($token->term) . ', ' + . $db->quote($token->stem) . ', ' + . (int) $token->common . ', ' + . (int) $token->phrase . ', ' + . $db->quote($token->weight) . ', ' + . (int) $context . ', ' + . $db->quote($token->language) + ); + ++$values; + } + + // Only execute the query if there are tokens to insert + if ($query->values !== null) { + $db->setQuery($query)->execute(); + } + + // Check if we're approaching the memory limit of the token table. + if ($values > static::$state->options->get('memory_table_limit', 10000)) { + $this->toggleTables(false); + } + } + + return $values; + } + + /** + * Method to switch the token tables from Memory tables to Disk tables + * when they are close to running out of memory. + * Since this is not supported/implemented in all DB-drivers, the default is a stub method, which simply returns true. + * + * @param boolean $memory Flag to control how they should be toggled. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function toggleTables($memory) + { + if (strtolower($this->db->getServerType()) != 'mysql') { + return true; + } + + static $state; + + // Get the database adapter. + $db = $this->db; + + // Check if we are setting the tables to the Memory engine. + if ($memory === true && $state !== true) { + // Set the tokens table to Memory. + $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens') . ' ENGINE = MEMORY'); + $db->execute(); + + // Set the tokens aggregate table to Memory. + $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens_aggregate') . ' ENGINE = MEMORY'); + $db->execute(); + + // Set the internal state. + $state = $memory; + } + // We must be setting the tables to the InnoDB engine. + elseif ($memory === false && $state !== false) { + // Set the tokens table to InnoDB. + $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens') . ' ENGINE = INNODB'); + $db->execute(); + + // Set the tokens aggregate table to InnoDB. + $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens_aggregate') . ' ENGINE = INNODB'); + $db->execute(); + + // Set the internal state. + $state = $memory; + } + + return true; + } } diff --git a/administrator/components/com_finder/src/Indexer/Language.php b/administrator/components/com_finder/src/Indexer/Language.php index 6f78889df6cf2..cc5c937ba0635 100644 --- a/administrator/components/com_finder/src/Indexer/Language.php +++ b/administrator/components/com_finder/src/Indexer/Language.php @@ -1,4 +1,5 @@ language = $locale; - } - - // Use our generic language handler if no language is set - if ($this->language === null) - { - $this->language = '*'; - } - - try - { - $this->stemmer = StemmerFactory::create($this->language); - } - catch (NotFoundException $e) - { - // We don't have a stemmer for the language - } - } - - /** - * Method to get a language support object. - * - * @param string $language The language of the support object. - * - * @return Language A Language instance. - * - * @since 4.0.0 - */ - public static function getInstance($language) - { - if (isset(self::$instances[$language])) - { - return self::$instances[$language]; - } - - $locale = '*'; - - if ($language !== '*') - { - $locale = Helper::getPrimaryLanguage($language); - $class = '\\Joomla\\Component\\Finder\\Administrator\\Indexer\\Language\\' . ucfirst($locale); - - if (class_exists($class)) - { - self::$instances[$language] = new $class; - - return self::$instances[$language]; - } - } - - self::$instances[$language] = new self($locale); - - return self::$instances[$language]; - } - - /** - * Method to tokenise a text string. - * - * @param string $input The input to tokenise. - * - * @return array An array of term strings. - * - * @since 4.0.0 - */ - public function tokenise($input) - { - $quotes = html_entity_decode('‘’'', ENT_QUOTES, 'UTF-8'); - - /* - * Parsing the string input into terms is a multi-step process. - * - * Regexes: - * 1. Remove everything except letters, numbers, quotes, apostrophe, plus, dash, period, and comma. - * 2. Remove plus, dash, period, and comma characters located before letter characters. - * 3. Remove plus, dash, period, and comma characters located after other characters. - * 4. Remove plus, period, and comma characters enclosed in alphabetical characters. Ungreedy. - * 5. Remove orphaned apostrophe, plus, dash, period, and comma characters. - * 6. Remove orphaned quote characters. - * 7. Replace the assorted single quotation marks with the ASCII standard single quotation. - * 8. Remove multiple space characters and replaces with a single space. - */ - $input = StringHelper::strtolower($input); - $input = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,]+#mui', ' ', $input); - $input = preg_replace('#(^|\s)[+-.,]+([\pL\pM]+)#mui', ' $1', $input); - $input = preg_replace('#([\pL\pM\pN]+)[+-.,]+(\s|$)#mui', '$1 ', $input); - $input = preg_replace('#([\pL\pM]+)[+.,]+([\pL\pM]+)#muiU', '$1 $2', $input); - $input = preg_replace('#(^|\s)[\'+-.,]+(\s|$)#mui', ' ', $input); - $input = preg_replace('#(^|\s)[\p{Pi}\p{Pf}]+(\s|$)#mui', ' ', $input); - $input = preg_replace('#[' . $quotes . ']+#mui', '\'', $input); - $input = preg_replace('#\s+#mui', ' ', $input); - $input = trim($input); - - // Explode the normalized string to get the terms. - $terms = explode(' ', $input); - - return $terms; - } - - /** - * Method to stem a token. - * - * @param string $token The token to stem. - * - * @return string The stemmed token. - * - * @since 4.0.0 - */ - public function stem($token) - { - if ($this->stemmer !== null) - { - return $this->stemmer->stem($token); - } - - return $token; - } + /** + * Language support instances container. + * + * @var Language[] + * @since 4.0.0 + */ + protected static $instances = array(); + + /** + * Language locale of the class + * + * @var string + * @since 4.0.0 + */ + public $language; + + /** + * Spacer to use between terms + * + * @var string + * @since 4.0.0 + */ + public $spacer = ' '; + + /** + * The stemmer object. + * + * @var Stemmer + * @since 4.0.0 + */ + protected $stemmer = null; + + /** + * Method to construct the language object. + * + * @since 4.0.0 + */ + public function __construct($locale = null) + { + if ($locale !== null) { + $this->language = $locale; + } + + // Use our generic language handler if no language is set + if ($this->language === null) { + $this->language = '*'; + } + + try { + $this->stemmer = StemmerFactory::create($this->language); + } catch (NotFoundException $e) { + // We don't have a stemmer for the language + } + } + + /** + * Method to get a language support object. + * + * @param string $language The language of the support object. + * + * @return Language A Language instance. + * + * @since 4.0.0 + */ + public static function getInstance($language) + { + if (isset(self::$instances[$language])) { + return self::$instances[$language]; + } + + $locale = '*'; + + if ($language !== '*') { + $locale = Helper::getPrimaryLanguage($language); + $class = '\\Joomla\\Component\\Finder\\Administrator\\Indexer\\Language\\' . ucfirst($locale); + + if (class_exists($class)) { + self::$instances[$language] = new $class(); + + return self::$instances[$language]; + } + } + + self::$instances[$language] = new self($locale); + + return self::$instances[$language]; + } + + /** + * Method to tokenise a text string. + * + * @param string $input The input to tokenise. + * + * @return array An array of term strings. + * + * @since 4.0.0 + */ + public function tokenise($input) + { + $quotes = html_entity_decode('‘’'', ENT_QUOTES, 'UTF-8'); + + /* + * Parsing the string input into terms is a multi-step process. + * + * Regexes: + * 1. Remove everything except letters, numbers, quotes, apostrophe, plus, dash, period, and comma. + * 2. Remove plus, dash, period, and comma characters located before letter characters. + * 3. Remove plus, dash, period, and comma characters located after other characters. + * 4. Remove plus, period, and comma characters enclosed in alphabetical characters. Ungreedy. + * 5. Remove orphaned apostrophe, plus, dash, period, and comma characters. + * 6. Remove orphaned quote characters. + * 7. Replace the assorted single quotation marks with the ASCII standard single quotation. + * 8. Remove multiple space characters and replaces with a single space. + */ + $input = StringHelper::strtolower($input); + $input = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,]+#mui', ' ', $input); + $input = preg_replace('#(^|\s)[+-.,]+([\pL\pM]+)#mui', ' $1', $input); + $input = preg_replace('#([\pL\pM\pN]+)[+-.,]+(\s|$)#mui', '$1 ', $input); + $input = preg_replace('#([\pL\pM]+)[+.,]+([\pL\pM]+)#muiU', '$1 $2', $input); + $input = preg_replace('#(^|\s)[\'+-.,]+(\s|$)#mui', ' ', $input); + $input = preg_replace('#(^|\s)[\p{Pi}\p{Pf}]+(\s|$)#mui', ' ', $input); + $input = preg_replace('#[' . $quotes . ']+#mui', '\'', $input); + $input = preg_replace('#\s+#mui', ' ', $input); + $input = trim($input); + + // Explode the normalized string to get the terms. + $terms = explode(' ', $input); + + return $terms; + } + + /** + * Method to stem a token. + * + * @param string $token The token to stem. + * + * @return string The stemmed token. + * + * @since 4.0.0 + */ + public function stem($token) + { + if ($this->stemmer !== null) { + return $this->stemmer->stem($token); + } + + return $token; + } } diff --git a/administrator/components/com_finder/src/Indexer/Language/El.php b/administrator/components/com_finder/src/Indexer/Language/El.php index 895feecba0c2d..76668e8e68ae6 100644 --- a/administrator/components/com_finder/src/Indexer/Language/El.php +++ b/administrator/components/com_finder/src/Indexer/Language/El.php @@ -1,4 +1,5 @@ toUpperCase($token, $wCase); - - // Stop-word removal - $stop_words = '/^(ΕΚΟ|ΑΒΑ|ΑΓΑ|ΑΓΗ|ΑΓΩ|ΑΔΗ|ΑΔΩ|ΑΕ|ΑΕΙ|ΑΘΩ|ΑΙ|ΑΙΚ|ΑΚΗ|ΑΚΟΜΑ|ΑΚΟΜΗ|ΑΚΡΙΒΩΣ|ΑΛΑ|ΑΛΗΘΕΙΑ|ΑΛΗΘΙΝΑ|ΑΛΛΑΧΟΥ|ΑΛΛΙΩΣ|ΑΛΛΙΩΤΙΚΑ|' - . 'ΑΛΛΟΙΩΣ|ΑΛΛΟΙΩΤΙΚΑ|ΑΛΛΟΤΕ|ΑΛΤ|ΑΛΩ|ΑΜΑ|ΑΜΕ|ΑΜΕΣΑ|ΑΜΕΣΩΣ|ΑΜΩ|ΑΝ|ΑΝΑ|ΑΝΑΜΕΣΑ|ΑΝΑΜΕΤΑΞΥ|ΑΝΕΥ|ΑΝΤΙ|ΑΝΤΙΠΕΡΑ|ΑΝΤΙΣ|ΑΝΩ|ΑΝΩΤΕΡΩ|ΑΞΑΦΝΑ|' - . 'ΑΠ|ΑΠΕΝΑΝΤΙ|ΑΠΟ|ΑΠΟΨΕ|ΑΠΩ|ΑΡΑ|ΑΡΑΓΕ|ΑΡΕ|ΑΡΚ|ΑΡΚΕΤΑ|ΑΡΛ|ΑΡΜ|ΑΡΤ|ΑΡΥ|ΑΡΩ|ΑΣ|ΑΣΑ|ΑΣΟ|ΑΤΑ|ΑΤΕ|ΑΤΗ|ΑΤΙ|ΑΤΜ|ΑΤΟ|ΑΥΡΙΟ|ΑΦΗ|ΑΦΟΤΟΥ|ΑΦΟΥ|' - . 'ΑΧ|ΑΧΕ|ΑΧΟ|ΑΨΑ|ΑΨΕ|ΑΨΗ|ΑΨΥ|ΑΩΕ|ΑΩΟ|ΒΑΝ|ΒΑΤ|ΒΑΧ|ΒΕΑ|ΒΕΒΑΙΟΤΑΤΑ|ΒΗΞ|ΒΙΑ|ΒΙΕ|ΒΙΗ|ΒΙΟ|ΒΟΗ|ΒΟΩ|ΒΡΕ|ΓΑ|ΓΑΒ|ΓΑΡ|ΓΕΝ|ΓΕΣ||ΓΗ|ΓΗΝ|ΓΙ|ΓΙΑ|' - . 'ΓΙΕ|ΓΙΝ|ΓΙΟ|ΓΚΙ|ΓΙΑΤΙ|ΓΚΥ|ΓΟΗ|ΓΟΟ|ΓΡΗΓΟΡΑ|ΓΡΙ|ΓΡΥ|ΓΥΗ|ΓΥΡΩ|ΔΑ|ΔΕ|ΔΕΗ|ΔΕΙ|ΔΕΝ|ΔΕΣ|ΔΗ|ΔΗΘΕΝ|ΔΗΛΑΔΗ|ΔΗΩ|ΔΙ|ΔΙΑ|ΔΙΑΡΚΩΣ|ΔΙΟΛΟΥ|ΔΙΣ|' - . 'ΔΙΧΩΣ|ΔΟΛ|ΔΟΝ|ΔΡΑ|ΔΡΥ|ΔΡΧ|ΔΥΕ|ΔΥΟ|ΔΩ|ΕΑΜ|ΕΑΝ|ΕΑΡ|ΕΘΗ|ΕΙ|ΕΙΔΕΜΗ|ΕΙΘΕ|ΕΙΜΑΙ|ΕΙΜΑΣΤΕ|ΕΙΝΑΙ|ΕΙΣ|ΕΙΣΑΙ|ΕΙΣΑΣΤΕ|ΕΙΣΤΕ|ΕΙΤΕ|ΕΙΧΑ|ΕΙΧΑΜΕ|' - . 'ΕΙΧΑΝ|ΕΙΧΑΤΕ|ΕΙΧΕ|ΕΙΧΕΣ|ΕΚ|ΕΚΕΙ|ΕΛΑ|ΕΛΙ|ΕΜΠ|ΕΝ|ΕΝΤΕΛΩΣ|ΕΝΤΟΣ|ΕΝΤΩΜΕΤΑΞΥ|ΕΝΩ|ΕΞ|ΕΞΑΦΝΑ|ΕΞΙ|ΕΞΙΣΟΥ|ΕΞΩ|ΕΟΚ|ΕΠΑΝΩ|ΕΠΕΙΔΗ|ΕΠΕΙΤΑ|ΕΠΗ|' - . 'ΕΠΙ|ΕΠΙΣΗΣ|ΕΠΟΜΕΝΩΣ|ΕΡΑ|ΕΣ|ΕΣΑΣ|ΕΣΕ|ΕΣΕΙΣ|ΕΣΕΝΑ|ΕΣΗ|ΕΣΤΩ|ΕΣΥ|ΕΣΩ|ΕΤΙ|ΕΤΣΙ|ΕΥ|ΕΥΑ|ΕΥΓΕ|ΕΥΘΥΣ|ΕΥΤΥΧΩΣ|ΕΦΕ|ΕΦΕΞΗΣ|ΕΦΤ|ΕΧΕ|ΕΧΕΙ|' - . 'ΕΧΕΙΣ|ΕΧΕΤΕ|ΕΧΘΕΣ|ΕΧΟΜΕ|ΕΧΟΥΜΕ|ΕΧΟΥΝ|ΕΧΤΕΣ|ΕΧΩ|ΕΩΣ|ΖΕΑ|ΖΕΗ|ΖΕΙ|ΖΕΝ|ΖΗΝ|ΖΩ|Η|ΗΔΗ|ΗΔΥ|ΗΘΗ|ΗΛΟ|ΗΜΙ|ΗΠΑ|ΗΣΑΣΤΕ|ΗΣΟΥΝ|ΗΤΑ|ΗΤΑΝ|ΗΤΑΝΕ|' - . 'ΗΤΟΙ|ΗΤΤΟΝ|ΗΩ|ΘΑ|ΘΥΕ|ΘΩΡ|Ι|ΙΑ|ΙΒΟ|ΙΔΗ|ΙΔΙΩΣ|ΙΕ|ΙΙ|ΙΙΙ|ΙΚΑ|ΙΛΟ|ΙΜΑ|ΙΝΑ|ΙΝΩ|ΙΞΕ|ΙΞΟ|ΙΟ|ΙΟΙ|ΙΣΑ|ΙΣΑΜΕ|ΙΣΕ|ΙΣΗ|ΙΣΙΑ|ΙΣΟ|ΙΣΩΣ|ΙΩΒ|ΙΩΝ|' - . 'ΙΩΣ|ΙΑΝ|ΚΑΘ|ΚΑΘΕ|ΚΑΘΕΤΙ|ΚΑΘΟΛΟΥ|ΚΑΘΩΣ|ΚΑΙ|ΚΑΝ|ΚΑΠΟΤΕ|ΚΑΠΟΥ|ΚΑΠΩΣ|ΚΑΤ|ΚΑΤΑ|ΚΑΤΙ|ΚΑΤΙΤΙ|ΚΑΤΟΠΙΝ|ΚΑΤΩ|ΚΑΩ|ΚΒΟ|ΚΕΑ|ΚΕΙ|ΚΕΝ|ΚΙ|ΚΙΜ|' - . 'ΚΙΟΛΑΣ|ΚΙΤ|ΚΙΧ|ΚΚΕ|ΚΛΙΣΕ|ΚΛΠ|ΚΟΚ|ΚΟΝΤΑ|ΚΟΧ|ΚΤΛ|ΚΥΡ|ΚΥΡΙΩΣ|ΚΩ|ΚΩΝ|ΛΑ|ΛΕΑ|ΛΕΝ|ΛΕΟ|ΛΙΑ|ΛΙΓΑΚΙ|ΛΙΓΟΥΛΑΚΙ|ΛΙΓΟ|ΛΙΓΩΤΕΡΟ|ΛΙΟ|ΛΙΡ|ΛΟΓΩ|' - . 'ΛΟΙΠΑ|ΛΟΙΠΟΝ|ΛΟΣ|ΛΣ|ΛΥΩ|ΜΑ|ΜΑΖΙ|ΜΑΚΑΡΙ|ΜΑΛΙΣΤΑ|ΜΑΛΛΟΝ|ΜΑΝ|ΜΑΞ|ΜΑΣ|ΜΑΤ|ΜΕ|ΜΕΘΑΥΡΙΟ|ΜΕΙ|ΜΕΙΟΝ|ΜΕΛ|ΜΕΛΕΙ|ΜΕΛΛΕΤΑΙ|ΜΕΜΙΑΣ|ΜΕΝ|ΜΕΣ|' - . 'ΜΕΣΑ|ΜΕΤ|ΜΕΤΑ|ΜΕΤΑΞΥ|ΜΕΧΡΙ|ΜΗ|ΜΗΔΕ|ΜΗΝ|ΜΗΠΩΣ|ΜΗΤΕ|ΜΙ|ΜΙΞ|ΜΙΣ|ΜΜΕ|ΜΝΑ|ΜΟΒ|ΜΟΛΙΣ|ΜΟΛΟΝΟΤΙ|ΜΟΝΑΧΑ|ΜΟΝΟΜΙΑΣ|ΜΙΑ|ΜΟΥ|ΜΠΑ|ΜΠΟΡΕΙ|' - . 'ΜΠΟΡΟΥΝ|ΜΠΡΑΒΟ|ΜΠΡΟΣ|ΜΠΩ|ΜΥ|ΜΥΑ|ΜΥΝ|ΝΑ|ΝΑΕ|ΝΑΙ|ΝΑΟ|ΝΔ|ΝΕΐ|ΝΕΑ|ΝΕΕ|ΝΕΟ|ΝΙ|ΝΙΑ|ΝΙΚ|ΝΙΛ|ΝΙΝ|ΝΙΟ|ΝΤΑ|ΝΤΕ|ΝΤΙ|ΝΤΟ|ΝΥΝ|ΝΩΕ|ΝΩΡΙΣ|ΞΑΝΑ|' - . 'ΞΑΦΝΙΚΑ|ΞΕΩ|ΞΙ|Ο|ΟΑ|ΟΑΠ|ΟΔΟ|ΟΕ|ΟΖΟ|ΟΗΕ|ΟΙ|ΟΙΑ|ΟΙΗ|ΟΚΑ|ΟΛΟΓΥΡΑ|ΟΛΟΝΕΝ|ΟΛΟΤΕΛΑ|ΟΛΩΣΔΙΟΛΟΥ|ΟΜΩΣ|ΟΝ|ΟΝΕ|ΟΝΟ|ΟΠΑ|ΟΠΕ|ΟΠΗ|ΟΠΟ|' - . 'ΟΠΟΙΑΔΗΠΟΤΕ|ΟΠΟΙΑΝΔΗΠΟΤΕ|ΟΠΟΙΑΣΔΗΠΟΤΕ|ΟΠΟΙΔΗΠΟΤΕ|ΟΠΟΙΕΣΔΗΠΟΤΕ|ΟΠΟΙΟΔΗΠΟΤΕ|ΟΠΟΙΟΝΔΗΠΟΤΕ|ΟΠΟΙΟΣΔΗΠΟΤΕ|ΟΠΟΙΟΥΔΗΠΟΤΕ|ΟΠΟΙΟΥΣΔΗΠΟΤΕ|' - . 'ΟΠΟΙΩΝΔΗΠΟΤΕ|ΟΠΟΤΕΔΗΠΟΤΕ|ΟΠΟΥ|ΟΠΟΥΔΗΠΟΤΕ|ΟΠΩΣ|ΟΡΑ|ΟΡΕ|ΟΡΗ|ΟΡΟ|ΟΡΦ|ΟΡΩ|ΟΣΑ|ΟΣΑΔΗΠΟΤΕ|ΟΣΕ|ΟΣΕΣΔΗΠΟΤΕ|ΟΣΗΔΗΠΟΤΕ|ΟΣΗΝΔΗΠΟΤΕ|' - . 'ΟΣΗΣΔΗΠΟΤΕ|ΟΣΟΔΗΠΟΤΕ|ΟΣΟΙΔΗΠΟΤΕ|ΟΣΟΝΔΗΠΟΤΕ|ΟΣΟΣΔΗΠΟΤΕ|ΟΣΟΥΔΗΠΟΤΕ|ΟΣΟΥΣΔΗΠΟΤΕ|ΟΣΩΝΔΗΠΟΤΕ|ΟΤΑΝ|ΟΤΕ|ΟΤΙ|ΟΤΙΔΗΠΟΤΕ|ΟΥ|ΟΥΔΕ|ΟΥΚ|ΟΥΣ|' - . 'ΟΥΤΕ|ΟΥΦ|ΟΧΙ|ΟΨΑ|ΟΨΕ|ΟΨΗ|ΟΨΙ|ΟΨΟ|ΠΑ|ΠΑΛΙ|ΠΑΝ|ΠΑΝΤΟΤΕ|ΠΑΝΤΟΥ|ΠΑΝΤΩΣ|ΠΑΠ|ΠΑΡ|ΠΑΡΑ|ΠΕΙ|ΠΕΡ|ΠΕΡΑ|ΠΕΡΙ|ΠΕΡΙΠΟΥ|ΠΕΡΣΙ|ΠΕΡΥΣΙ|ΠΕΣ|ΠΙ|' - . 'ΠΙΑ|ΠΙΘΑΝΟΝ|ΠΙΚ|ΠΙΟ|ΠΙΣΩ|ΠΙΤ|ΠΙΩ|ΠΛΑΙ|ΠΛΕΟΝ|ΠΛΗΝ|ΠΛΩ|ΠΜ|ΠΟΑ|ΠΟΕ|ΠΟΛ|ΠΟΛΥ|ΠΟΠ|ΠΟΤΕ|ΠΟΥ|ΠΟΥΘΕ|ΠΟΥΘΕΝΑ|ΠΡΕΠΕΙ|ΠΡΙ|ΠΡΙΝ|ΠΡΟ|' - . 'ΠΡΟΚΕΙΜΕΝΟΥ|ΠΡΟΚΕΙΤΑΙ|ΠΡΟΠΕΡΣΙ|ΠΡΟΣ|ΠΡΟΤΟΥ|ΠΡΟΧΘΕΣ|ΠΡΟΧΤΕΣ|ΠΡΩΤΥΤΕΡΑ|ΠΥΑ|ΠΥΞ|ΠΥΟ|ΠΥΡ|ΠΧ|ΠΩ|ΠΩΛ|ΠΩΣ|ΡΑ|ΡΑΙ|ΡΑΠ|ΡΑΣ|ΡΕ|ΡΕΑ|ΡΕΕ|ΡΕΙ|' - . 'ΡΗΣ|ΡΘΩ|ΡΙΟ|ΡΟ|ΡΟΐ|ΡΟΕ|ΡΟΖ|ΡΟΗ|ΡΟΘ|ΡΟΙ|ΡΟΚ|ΡΟΛ|ΡΟΝ|ΡΟΣ|ΡΟΥ|ΣΑΙ|ΣΑΝ|ΣΑΟ|ΣΑΣ|ΣΕ|ΣΕΙΣ|ΣΕΚ|ΣΕΞ|ΣΕΡ|ΣΕΤ|ΣΕΦ|ΣΗΜΕΡΑ|ΣΙ|ΣΙΑ|ΣΙΓΑ|ΣΙΚ|' - . 'ΣΙΧ|ΣΚΙ|ΣΟΙ|ΣΟΚ|ΣΟΛ|ΣΟΝ|ΣΟΣ|ΣΟΥ|ΣΡΙ|ΣΤΑ|ΣΤΗ|ΣΤΗΝ|ΣΤΗΣ|ΣΤΙΣ|ΣΤΟ|ΣΤΟΝ|ΣΤΟΥ|ΣΤΟΥΣ|ΣΤΩΝ|ΣΥ|ΣΥΓΧΡΟΝΩΣ|ΣΥΝ|ΣΥΝΑΜΑ|ΣΥΝΕΠΩΣ|ΣΥΝΗΘΩΣ|' - . 'ΣΧΕΔΟΝ|ΣΩΣΤΑ|ΤΑ|ΤΑΔΕ|ΤΑΚ|ΤΑΝ|ΤΑΟ|ΤΑΥ|ΤΑΧΑ|ΤΑΧΑΤΕ|ΤΕ|ΤΕΙ|ΤΕΛ|ΤΕΛΙΚΑ|ΤΕΛΙΚΩΣ|ΤΕΣ|ΤΕΤ|ΤΖΟ|ΤΗ|ΤΗΛ|ΤΗΝ|ΤΗΣ|ΤΙ|ΤΙΚ|ΤΙΜ|ΤΙΠΟΤΑ|ΤΙΠΟΤΕ|' - . 'ΤΙΣ|ΤΝΤ|ΤΟ|ΤΟΙ|ΤΟΚ|ΤΟΜ|ΤΟΝ|ΤΟΠ|ΤΟΣ|ΤΟΣ?Ν|ΤΟΣΑ|ΤΟΣΕΣ|ΤΟΣΗ|ΤΟΣΗΝ|ΤΟΣΗΣ|ΤΟΣΟ|ΤΟΣΟΙ|ΤΟΣΟΝ|ΤΟΣΟΣ|ΤΟΣΟΥ|ΤΟΣΟΥΣ|ΤΟΤΕ|ΤΟΥ|ΤΟΥΛΑΧΙΣΤΟ|' - . 'ΤΟΥΛΑΧΙΣΤΟΝ|ΤΟΥΣ|ΤΣ|ΤΣΑ|ΤΣΕ|ΤΥΧΟΝ|ΤΩ|ΤΩΝ|ΤΩΡΑ|ΥΑΣ|ΥΒΑ|ΥΒΟ|ΥΙΕ|ΥΙΟ|ΥΛΑ|ΥΛΗ|ΥΝΙ|ΥΠ|ΥΠΕΡ|ΥΠΟ|ΥΠΟΨΗ|ΥΠΟΨΙΝ|ΥΣΤΕΡΑ|ΥΦΗ|ΥΨΗ|ΦΑ|ΦΑΐ|ΦΑΕ|' - . 'ΦΑΝ|ΦΑΞ|ΦΑΣ|ΦΑΩ|ΦΕΖ|ΦΕΙ|ΦΕΤΟΣ|ΦΕΥ|ΦΙ|ΦΙΛ|ΦΙΣ|ΦΟΞ|ΦΠΑ|ΦΡΙ|ΧΑ|ΧΑΗ|ΧΑΛ|ΧΑΝ|ΧΑΦ|ΧΕ|ΧΕΙ|ΧΘΕΣ|ΧΙ|ΧΙΑ|ΧΙΛ|ΧΙΟ|ΧΛΜ|ΧΜ|ΧΟΗ|ΧΟΛ|ΧΡΩ|ΧΤΕΣ|' - . 'ΧΩΡΙΣ|ΧΩΡΙΣΤΑ|ΨΕΣ|ΨΗΛΑ|ΨΙ|ΨΙΤ|Ω|ΩΑ|ΩΑΣ|ΩΔΕ|ΩΕΣ|ΩΘΩ|ΩΜΑ|ΩΜΕ|ΩΝ|ΩΟ|ΩΟΝ|ΩΟΥ|ΩΣ|ΩΣΑΝ|ΩΣΗ|ΩΣΟΤΟΥ|ΩΣΠΟΥ|ΩΣΤΕ|ΩΣΤΟΣΟ|ΩΤΑ|ΩΧ|ΩΩΝ)$/'; - - if (preg_match($stop_words, $token)) - { - return $this->toLowerCase($token, $wCase); - } - - // Vowels - $v = '(Α|Ε|Η|Ι|Ο|Υ|Ω)'; - - // Vowels without Y - $v2 = '(Α|Ε|Η|Ι|Ο|Ω)'; - - $test1 = true; - - // Step S1. 14 stems - $re = '/^(.+?)(ΙΖΑ|ΙΖΕΣ|ΙΖΕ|ΙΖΑΜΕ|ΙΖΑΤΕ|ΙΖΑΝ|ΙΖΑΝΕ|ΙΖΩ|ΙΖΕΙΣ|ΙΖΕΙ|ΙΖΟΥΜΕ|ΙΖΕΤΕ|ΙΖΟΥΝ|ΙΖΟΥΝΕ)$/'; - $exceptS1 = '/^(ΑΝΑΜΠΑ|ΕΜΠΑ|ΕΠΑ|ΞΑΝΑΠΑ|ΠΑ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ)$/'; - $exceptS2 = '/^(ΜΑΡΚ|ΚΟΡΝ|ΑΜΠΑΡ|ΑΡΡ|ΒΑΘΥΡΙ|ΒΑΡΚ|Β|ΒΟΛΒΟΡ|ΓΚΡ|ΓΛΥΚΟΡ|ΓΛΥΚΥΡ|ΙΜΠ|Λ|ΛΟΥ|ΜΑΡ|Μ|ΠΡ|ΜΠΡ|ΠΟΛΥΡ|Π|Ρ|ΠΙΠΕΡΟΡ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . 'I'; - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . 'IΖ'; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S2. 7 stems - $re = '/^(.+?)(ΩΘΗΚΑ|ΩΘΗΚΕΣ|ΩΘΗΚΕ|ΩΘΗΚΑΜΕ|ΩΘΗΚΑΤΕ|ΩΘΗΚΑΝ|ΩΘΗΚΑΝΕ)$/'; - $exceptS1 = '/^(ΑΛ|ΒΙ|ΕΝ|ΥΨ|ΛΙ|ΖΩ|Σ|Χ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . 'ΩΝ'; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S3. 7 stems - $re = '/^(.+?)(ΙΣΑ|ΙΣΕΣ|ΙΣΕ|ΙΣΑΜΕ|ΙΣΑΤΕ|ΙΣΑΝ|ΙΣΑΝΕ)$/'; - $exceptS1 = '/^(ΑΝΑΜΠΑ|ΑΘΡΟ|ΕΜΠΑ|ΕΣΕ|ΕΣΩΚΛΕ|ΕΠΑ|ΞΑΝΑΠΑ|ΕΠΕ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ|ΚΛΕ|ΧΑΡΤΟΠΑ|ΕΞΑΡΧΑ|ΜΕΤΕΠΕ|ΑΠΟΚΛΕ|ΑΠΕΚΛΕ|ΕΚΛΕ|ΠΕ|ΠΕΡΙΠΑ)$/'; - $exceptS2 = '/^(ΑΝ|ΑΦ|ΓΕ|ΓΙΓΑΝΤΟΑΦ|ΓΚΕ|ΔΗΜΟΚΡΑΤ|ΚΟΜ|ΓΚ|Μ|Π|ΠΟΥΚΑΜ|ΟΛΟ|ΛΑΡ)$/'; - - if ($token == "ΙΣΑ") - { - $token = "ΙΣ"; - - return $token; - } - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . 'Ι'; - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . 'ΙΣ'; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S4. 7 stems - $re = '/^(.+?)(ΙΣΩ|ΙΣΕΙΣ|ΙΣΕΙ|ΙΣΟΥΜΕ|ΙΣΕΤΕ|ΙΣΟΥΝ|ΙΣΟΥΝΕ)$/'; - $exceptS1 = '/^(ΑΝΑΜΠΑ|ΕΜΠΑ|ΕΣΕ|ΕΣΩΚΛΕ|ΕΠΑ|ΞΑΝΑΠΑ|ΕΠΕ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ|ΚΛΕ|ΧΑΡΤΟΠΑ|ΕΞΑΡΧΑ|ΜΕΤΕΠΕ|ΑΠΟΚΛΕ|ΑΠΕΚΛΕ|ΕΚΛΕ|ΠΕ|ΠΕΡΙΠΑ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . 'Ι'; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S5. 11 stems - $re = '/^(.+?)(ΙΣΤΟΣ|ΙΣΤΟΥ|ΙΣΤΟ|ΙΣΤΕ|ΙΣΤΟΙ|ΙΣΤΩΝ|ΙΣΤΟΥΣ|ΙΣΤΗ|ΙΣΤΗΣ|ΙΣΤΑ|ΙΣΤΕΣ)$/'; - $exceptS1 = '/^(Μ|Π|ΑΠ|ΑΡ|ΗΔ|ΚΤ|ΣΚ|ΣΧ|ΥΨ|ΦΑ|ΧΡ|ΧΤ|ΑΚΤ|ΑΟΡ|ΑΣΧ|ΑΤΑ|ΑΧΝ|ΑΧΤ|ΓΕΜ|ΓΥΡ|ΕΜΠ|ΕΥΠ|ΕΧΘ|ΗΦΑ|ΚΑΘ|ΚΑΚ|ΚΥΛ|ΛΥΓ|ΜΑΚ|ΜΕΓ|ΤΑΧ|ΦΙΛ|ΧΩΡ)$/'; - $exceptS2 = '/^(ΔΑΝΕ|ΣΥΝΑΘΡΟ|ΚΛΕ|ΣΕ|ΕΣΩΚΛΕ|ΑΣΕ|ΠΛΕ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . 'ΙΣΤ'; - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . 'Ι'; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S6. 6 stems - $re = '/^(.+?)(ΙΣΜΟ|ΙΣΜΟΙ|ΙΣΜΟΣ|ΙΣΜΟΥ|ΙΣΜΟΥΣ|ΙΣΜΩΝ)$/'; - $exceptS1 = '/^(ΑΓΝΩΣΤΙΚ|ΑΤΟΜΙΚ|ΓΝΩΣΤΙΚ|ΕΘΝΙΚ|ΕΚΛΕΚΤΙΚ|ΣΚΕΠΤΙΚ|ΤΟΠΙΚ)$/'; - $exceptS2 = '/^(ΣΕ|ΜΕΤΑΣΕ|ΜΙΚΡΟΣΕ|ΕΓΚΛΕ|ΑΠΟΚΛΕ)$/'; - $exceptS3 = '/^(ΔΑΝΕ|ΑΝΤΙΔΑΝΕ)$/'; - $exceptS4 = '/^(ΑΛΕΞΑΝΔΡΙΝ|ΒΥΖΑΝΤΙΝ|ΘΕΑΤΡΙΝ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = str_replace('ΙΚ', "", $token); - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . "ΙΣΜ"; - } - - if (preg_match($exceptS3, $token)) - { - $token = $token . "Ι"; - } - - if (preg_match($exceptS4, $token)) - { - $token = str_replace('ΙΝ', "", $token); - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S7. 4 stems - $re = '/^(.+?)(ΑΡΑΚΙ|ΑΡΑΚΙΑ|ΟΥΔΑΚΙ|ΟΥΔΑΚΙΑ)$/'; - $exceptS1 = '/^(Σ|Χ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . "AΡΑΚ"; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S8. 8 stems - $re = '/^(.+?)(ΑΚΙ|ΑΚΙΑ|ΙΤΣΑ|ΙΤΣΑΣ|ΙΤΣΕΣ|ΙΤΣΩΝ|ΑΡΑΚΙ|ΑΡΑΚΙΑ)$/'; - $exceptS1 = '/^(ΑΝΘΡ|ΒΑΜΒ|ΒΡ|ΚΑΙΜ|ΚΟΝ|ΚΟΡ|ΛΑΒΡ|ΛΟΥΛ|ΜΕΡ|ΜΟΥΣΤ|ΝΑΓΚΑΣ|ΠΛ|Ρ|ΡΥ|Σ|ΣΚ|ΣΟΚ|ΣΠΑΝ|ΤΖ|ΦΑΡΜ|Χ|' - . 'ΚΑΠΑΚ|ΑΛΙΣΦ|ΑΜΒΡ|ΑΝΘΡ|Κ|ΦΥΛ|ΚΑΤΡΑΠ|ΚΛΙΜ|ΜΑΛ|ΣΛΟΒ|Φ|ΣΦ|ΤΣΕΧΟΣΛΟΒ)$/'; - $exceptS2 = '/^(Β|ΒΑΛ|ΓΙΑΝ|ΓΛ|Ζ|ΗΓΟΥΜΕΝ|ΚΑΡΔ|ΚΟΝ|ΜΑΚΡΥΝ|ΝΥΦ|ΠΑΤΕΡ|Π|ΣΚ|ΤΟΣ|ΤΡΙΠΟΛ)$/'; - - // For words like ΠΛΟΥΣΙΟΚΟΡΙΤΣΑ, ΠΑΛΙΟΚΟΡΙΤΣΑ etc - $exceptS3 = '/(ΚΟΡ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . "ΑΚ"; - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . "ΙΤΣ"; - } - - if (preg_match($exceptS3, $token)) - { - $token = $token . "ΙΤΣ"; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S9. 3 stems - $re = '/^(.+?)(ΙΔΙΟ|ΙΔΙΑ|ΙΔΙΩΝ)$/'; - $exceptS1 = '/^(ΑΙΦΝ|ΙΡ|ΟΛΟ|ΨΑΛ)$/'; - $exceptS2 = '/(Ε|ΠΑΙΧΝ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . "ΙΔ"; - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . "ΙΔ"; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S10. 4 stems - $re = '/^(.+?)(ΙΣΚΟΣ|ΙΣΚΟΥ|ΙΣΚΟ|ΙΣΚΕ)$/'; - $exceptS1 = '/^(Δ|ΙΒ|ΜΗΝ|Ρ|ΦΡΑΓΚ|ΛΥΚ|ΟΒΕΛ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . "ΙΣΚ"; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step 1 - // step1list is used in Step 1. 41 stems - $step1list = Array(); - $step1list["ΦΑΓΙΑ"] = "ΦΑ"; - $step1list["ΦΑΓΙΟΥ"] = "ΦΑ"; - $step1list["ΦΑΓΙΩΝ"] = "ΦΑ"; - $step1list["ΣΚΑΓΙΑ"] = "ΣΚΑ"; - $step1list["ΣΚΑΓΙΟΥ"] = "ΣΚΑ"; - $step1list["ΣΚΑΓΙΩΝ"] = "ΣΚΑ"; - $step1list["ΟΛΟΓΙΟΥ"] = "ΟΛΟ"; - $step1list["ΟΛΟΓΙΑ"] = "ΟΛΟ"; - $step1list["ΟΛΟΓΙΩΝ"] = "ΟΛΟ"; - $step1list["ΣΟΓΙΟΥ"] = "ΣΟ"; - $step1list["ΣΟΓΙΑ"] = "ΣΟ"; - $step1list["ΣΟΓΙΩΝ"] = "ΣΟ"; - $step1list["ΤΑΤΟΓΙΑ"] = "ΤΑΤΟ"; - $step1list["ΤΑΤΟΓΙΟΥ"] = "ΤΑΤΟ"; - $step1list["ΤΑΤΟΓΙΩΝ"] = "ΤΑΤΟ"; - $step1list["ΚΡΕΑΣ"] = "ΚΡΕ"; - $step1list["ΚΡΕΑΤΟΣ"] = "ΚΡΕ"; - $step1list["ΚΡΕΑΤΑ"] = "ΚΡΕ"; - $step1list["ΚΡΕΑΤΩΝ"] = "ΚΡΕ"; - $step1list["ΠΕΡΑΣ"] = "ΠΕΡ"; - $step1list["ΠΕΡΑΤΟΣ"] = "ΠΕΡ"; - - // Added by Spyros. Also at $re in step1 - $step1list["ΠΕΡΑΤΗ"] = "ΠΕΡ"; - $step1list["ΠΕΡΑΤΑ"] = "ΠΕΡ"; - $step1list["ΠΕΡΑΤΩΝ"] = "ΠΕΡ"; - $step1list["ΤΕΡΑΣ"] = "ΤΕΡ"; - $step1list["ΤΕΡΑΤΟΣ"] = "ΤΕΡ"; - $step1list["ΤΕΡΑΤΑ"] = "ΤΕΡ"; - $step1list["ΤΕΡΑΤΩΝ"] = "ΤΕΡ"; - $step1list["ΦΩΣ"] = "ΦΩ"; - $step1list["ΦΩΤΟΣ"] = "ΦΩ"; - $step1list["ΦΩΤΑ"] = "ΦΩ"; - $step1list["ΦΩΤΩΝ"] = "ΦΩ"; - $step1list["ΚΑΘΕΣΤΩΣ"] = "ΚΑΘΕΣΤ"; - $step1list["ΚΑΘΕΣΤΩΤΟΣ"] = "ΚΑΘΕΣΤ"; - $step1list["ΚΑΘΕΣΤΩΤΑ"] = "ΚΑΘΕΣΤ"; - $step1list["ΚΑΘΕΣΤΩΤΩΝ"] = "ΚΑΘΕΣΤ"; - $step1list["ΓΕΓΟΝΟΣ"] = "ΓΕΓΟΝ"; - $step1list["ΓΕΓΟΝΟΤΟΣ"] = "ΓΕΓΟΝ"; - $step1list["ΓΕΓΟΝΟΤΑ"] = "ΓΕΓΟΝ"; - $step1list["ΓΕΓΟΝΟΤΩΝ"] = "ΓΕΓΟΝ"; - - $re = '/(.*)(ΦΑΓΙΑ|ΦΑΓΙΟΥ|ΦΑΓΙΩΝ|ΣΚΑΓΙΑ|ΣΚΑΓΙΟΥ|ΣΚΑΓΙΩΝ|ΟΛΟΓΙΟΥ|ΟΛΟΓΙΑ|ΟΛΟΓΙΩΝ|ΣΟΓΙΟΥ|ΣΟΓΙΑ|ΣΟΓΙΩΝ|ΤΑΤΟΓΙΑ|ΤΑΤΟΓΙΟΥ|ΤΑΤΟΓΙΩΝ|ΚΡΕΑΣ|ΚΡΕΑΤΟΣ|' - . 'ΚΡΕΑΤΑ|ΚΡΕΑΤΩΝ|ΠΕΡΑΣ|ΠΕΡΑΤΟΣ|ΠΕΡΑΤΗ|ΠΕΡΑΤΑ|ΠΕΡΑΤΩΝ|ΤΕΡΑΣ|ΤΕΡΑΤΟΣ|ΤΕΡΑΤΑ|ΤΕΡΑΤΩΝ|ΦΩΣ|ΦΩΤΟΣ|ΦΩΤΑ|ΦΩΤΩΝ|ΚΑΘΕΣΤΩΣ|ΚΑΘΕΣΤΩΤΟΣ|' - . 'ΚΑΘΕΣΤΩΤΑ|ΚΑΘΕΣΤΩΤΩΝ|ΓΕΓΟΝΟΣ|ΓΕΓΟΝΟΤΟΣ|ΓΕΓΟΝΟΤΑ|ΓΕΓΟΝΟΤΩΝ)$/'; - - if (preg_match($re, $token, $match)) - { - $stem = $match[1]; - $suffix = $match[2]; - $token = $stem . (array_key_exists($suffix, $step1list) ? $step1list[$suffix] : ''); - $test1 = false; - } - - // Step 2a. 2 stems - $re = '/^(.+?)(ΑΔΕΣ|ΑΔΩΝ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - $re = '/(ΟΚ|ΜΑΜ|ΜΑΝ|ΜΠΑΜΠ|ΠΑΤΕΡ|ΓΙΑΓΙ|ΝΤΑΝΤ|ΚΥΡ|ΘΕΙ|ΠΕΘΕΡ)$/'; - - if (!preg_match($re, $token)) - { - $token = $token . "ΑΔ"; - } - } - - // Step 2b. 2 stems - $re = '/^(.+?)(ΕΔΕΣ|ΕΔΩΝ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $exept2 = '/(ΟΠ|ΙΠ|ΕΜΠ|ΥΠ|ΓΗΠ|ΔΑΠ|ΚΡΑΣΠ|ΜΙΛ)$/'; - - if (preg_match($exept2, $token)) - { - $token = $token . 'ΕΔ'; - } - } - - // Step 2c - $re = '/^(.+?)(ΟΥΔΕΣ|ΟΥΔΩΝ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - - $exept3 = '/(ΑΡΚ|ΚΑΛΙΑΚ|ΠΕΤΑΛ|ΛΙΧ|ΠΛΕΞ|ΣΚ|Σ|ΦΛ|ΦΡ|ΒΕΛ|ΛΟΥΛ|ΧΝ|ΣΠ|ΤΡΑΓ|ΦΕ)$/'; - - if (preg_match($exept3, $token)) - { - $token = $token . 'ΟΥΔ'; - } - } - - // Step 2d - $re = '/^(.+?)(ΕΩΣ|ΕΩΝ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept4 = '/^(Θ|Δ|ΕΛ|ΓΑΛ|Ν|Π|ΙΔ|ΠΑΡ)$/'; - - if (preg_match($exept4, $token)) - { - $token = $token . 'Ε'; - } - } - - // Step 3 - $re = '/^(.+?)(ΙΑ|ΙΟΥ|ΙΩΝ)$/'; - - if (preg_match($re, $token, $fp)) - { - $stem = $fp[1]; - $token = $stem; - $re = '/' . $v . '$/'; - $test1 = false; - - if (preg_match($re, $token)) - { - $token = $stem . 'Ι'; - } - } - - // Step 4 - $re = '/^(.+?)(ΙΚΑ|ΙΚΟ|ΙΚΟΥ|ΙΚΩΝ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $re = '/' . $v . '$/'; - $exept5 = '/^(ΑΛ|ΑΔ|ΕΝΔ|ΑΜΑΝ|ΑΜΜΟΧΑΛ|ΗΘ|ΑΝΗΘ|ΑΝΤΙΔ|ΦΥΣ|ΒΡΩΜ|ΓΕΡ|ΕΞΩΔ|ΚΑΛΠ|ΚΑΛΛΙΝ|ΚΑΤΑΔ|ΜΟΥΛ|ΜΠΑΝ|ΜΠΑΓΙΑΤ|ΜΠΟΛ|ΜΠΟΣ|ΝΙΤ|ΞΙΚ|ΣΥΝΟΜΗΛ|ΠΕΤΣ|' - . 'ΠΙΤΣ|ΠΙΚΑΝΤ|ΠΛΙΑΤΣ|ΠΟΣΤΕΛΝ|ΠΡΩΤΟΔ|ΣΕΡΤ|ΣΥΝΑΔ|ΤΣΑΜ|ΥΠΟΔ|ΦΙΛΟΝ|ΦΥΛΟΔ|ΧΑΣ)$/'; - - if (preg_match($re, $token) || preg_match($exept5, $token)) - { - $token = $token . 'ΙΚ'; - } - } - - // Step 5a - $re = '/^(.+?)(ΑΜΕ)$/'; - $re2 = '/^(.+?)(ΑΓΑΜΕ|ΗΣΑΜΕ|ΟΥΣΑΜΕ|ΗΚΑΜΕ|ΗΘΗΚΑΜΕ)$/'; - - if ($token == "ΑΓΑΜΕ") - { - $token = "ΑΓΑΜ"; - } - - if (preg_match($re2, $token)) - { - preg_match($re2, $token, $match); - $token = $match[1]; - $test1 = false; - } - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept6 = '/^(ΑΝΑΠ|ΑΠΟΘ|ΑΠΟΚ|ΑΠΟΣΤ|ΒΟΥΒ|ΞΕΘ|ΟΥΛ|ΠΕΘ|ΠΙΚΡ|ΠΟΤ|ΣΙΧ|Χ)$/'; - - if (preg_match($exept6, $token)) - { - $token = $token . "ΑΜ"; - } - } - - // Step 5b - $re2 = '/^(.+?)(ΑΝΕ)$/'; - $re3 = '/^(.+?)(ΑΓΑΝΕ|ΗΣΑΝΕ|ΟΥΣΑΝΕ|ΙΟΝΤΑΝΕ|ΙΟΤΑΝΕ|ΙΟΥΝΤΑΝΕ|ΟΝΤΑΝΕ|ΟΤΑΝΕ|ΟΥΝΤΑΝΕ|ΗΚΑΝΕ|ΗΘΗΚΑΝΕ)$/'; - - if (preg_match($re3, $token)) - { - preg_match($re3, $token, $match); - $token = $match[1]; - $test1 = false; - $re3 = '/^(ΤΡ|ΤΣ)$/'; - - if (preg_match($re3, $token)) - { - $token = $token . "ΑΓΑΝ"; - } - } - - if (preg_match($re2, $token)) - { - preg_match($re2, $token, $match); - $token = $match[1]; - $test1 = false; - $re2 = '/' . $v2 . '$/'; - $exept7 = '/^(ΒΕΤΕΡ|ΒΟΥΛΚ|ΒΡΑΧΜ|Γ|ΔΡΑΔΟΥΜ|Θ|ΚΑΛΠΟΥΖ|ΚΑΣΤΕΛ|ΚΟΡΜΟΡ|ΛΑΟΠΛ|ΜΩΑΜΕΘ|Μ|ΜΟΥΣΟΥΛΜ|Ν|ΟΥΛ|Π|ΠΕΛΕΚ|ΠΛ|ΠΟΛΙΣ|ΠΟΡΤΟΛ|ΣΑΡΑΚΑΤΣ|ΣΟΥΛΤ|' - . 'ΤΣΑΡΛΑΤ|ΟΡΦ|ΤΣΙΓΓ|ΤΣΟΠ|ΦΩΤΟΣΤΕΦ|Χ|ΨΥΧΟΠΛ|ΑΓ|ΟΡΦ|ΓΑΛ|ΓΕΡ|ΔΕΚ|ΔΙΠΛ|ΑΜΕΡΙΚΑΝ|ΟΥΡ|ΠΙΘ|ΠΟΥΡΙΤ|Σ|ΖΩΝΤ|ΙΚ|ΚΑΣΤ|ΚΟΠ|ΛΙΧ|ΛΟΥΘΗΡ|ΜΑΙΝΤ|' - . 'ΜΕΛ|ΣΙΓ|ΣΠ|ΣΤΕΓ|ΤΡΑΓ|ΤΣΑΓ|Φ|ΕΡ|ΑΔΑΠ|ΑΘΙΓΓ|ΑΜΗΧ|ΑΝΙΚ|ΑΝΟΡΓ|ΑΠΗΓ|ΑΠΙΘ|ΑΤΣΙΓΓ|ΒΑΣ|ΒΑΣΚ|ΒΑΘΥΓΑΛ|ΒΙΟΜΗΧ|ΒΡΑΧΥΚ|ΔΙΑΤ|ΔΙΑΦ|ΕΝΟΡΓ|' - . 'ΘΥΣ|ΚΑΠΝΟΒΙΟΜΗΧ|ΚΑΤΑΓΑΛ|ΚΛΙΒ|ΚΟΙΛΑΡΦ|ΛΙΒ|ΜΕΓΛΟΒΙΟΜΗΧ|ΜΙΚΡΟΒΙΟΜΗΧ|ΝΤΑΒ|ΞΗΡΟΚΛΙΒ|ΟΛΙΓΟΔΑΜ|ΟΛΟΓΑΛ|ΠΕΝΤΑΡΦ|ΠΕΡΗΦ|ΠΕΡΙΤΡ|ΠΛΑΤ|' - . 'ΠΟΛΥΔΑΠ|ΠΟΛΥΜΗΧ|ΣΤΕΦ|ΤΑΒ|ΤΕΤ|ΥΠΕΡΗΦ|ΥΠΟΚΟΠ|ΧΑΜΗΛΟΔΑΠ|ΨΗΛΟΤΑΒ)$/'; - - if (preg_match($re2, $token) || preg_match($exept7, $token)) - { - $token = $token . "ΑΝ"; - } - } - - // Step 5c - $re3 = '/^(.+?)(ΕΤΕ)$/'; - $re4 = '/^(.+?)(ΗΣΕΤΕ)$/'; - - if (preg_match($re4, $token)) - { - preg_match($re4, $token, $match); - $token = $match[1]; - $test1 = false; - } - - if (preg_match($re3, $token)) - { - preg_match($re3, $token, $match); - $token = $match[1]; - $test1 = false; - $re3 = '/' . $v2 . '$/'; - $exept8 = '/(ΟΔ|ΑΙΡ|ΦΟΡ|ΤΑΘ|ΔΙΑΘ|ΣΧ|ΕΝΔ|ΕΥΡ|ΤΙΘ|ΥΠΕΡΘ|ΡΑΘ|ΕΝΘ|ΡΟΘ|ΣΘ|ΠΥΡ|ΑΙΝ|ΣΥΝΔ|ΣΥΝ|ΣΥΝΘ|ΧΩΡ|ΠΟΝ|ΒΡ|ΚΑΘ|ΕΥΘ|ΕΚΘ|ΝΕΤ|ΡΟΝ|ΑΡΚ|ΒΑΡ|ΒΟΛ|ΩΦΕΛ)$/'; - $exept9 = '/^(ΑΒΑΡ|ΒΕΝ|ΕΝΑΡ|ΑΒΡ|ΑΔ|ΑΘ|ΑΝ|ΑΠΛ|ΒΑΡΟΝ|ΝΤΡ|ΣΚ|ΚΟΠ|ΜΠΟΡ|ΝΙΦ|ΠΑΓ|ΠΑΡΑΚΑΛ|ΣΕΡΠ|ΣΚΕΛ|ΣΥΡΦ|ΤΟΚ|Υ|Δ|ΕΜ|ΘΑΡΡ|Θ)$/'; - - if (preg_match($re3, $token) || preg_match($exept8, $token) || preg_match($exept9, $token)) - { - $token = $token . "ΕΤ"; - } - } - - // Step 5d - $re = '/^(.+?)(ΟΝΤΑΣ|ΩΝΤΑΣ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept10 = '/^(ΑΡΧ)$/'; - $exept11 = '/(ΚΡΕ)$/'; - - if (preg_match($exept10, $token)) - { - $token = $token . "ΟΝΤ"; - } - - if (preg_match($exept11, $token)) - { - $token = $token . "ΩΝΤ"; - } - } - - // Step 5e - $re = '/^(.+?)(ΟΜΑΣΤΕ|ΙΟΜΑΣΤΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept11 = '/^(ΟΝ)$/'; - - if (preg_match($exept11, $token)) - { - $token = $token . "ΟΜΑΣΤ"; - } - } - - // Step 5f - $re = '/^(.+?)(ΕΣΤΕ)$/'; - $re2 = '/^(.+?)(ΙΕΣΤΕ)$/'; - - if (preg_match($re2, $token)) - { - preg_match($re2, $token, $match); - $token = $match[1]; - $test1 = false; - $re2 = '/^(Π|ΑΠ|ΣΥΜΠ|ΑΣΥΜΠ|ΑΚΑΤΑΠ|ΑΜΕΤΑΜΦ)$/'; - - if (preg_match($re2, $token)) - { - $token = $token . "ΙΕΣΤ"; - } - } - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept12 = '/^(ΑΛ|ΑΡ|ΕΚΤΕΛ|Ζ|Μ|Ξ|ΠΑΡΑΚΑΛ|ΑΡ|ΠΡΟ|ΝΙΣ)$/'; - - if (preg_match($exept12, $token)) - { - $token = $token . "ΕΣΤ"; - } - } - - // Step 5g - $re = '/^(.+?)(ΗΚΑ|ΗΚΕΣ|ΗΚΕ)$/'; - $re2 = '/^(.+?)(ΗΘΗΚΑ|ΗΘΗΚΕΣ|ΗΘΗΚΕ)$/'; - - if (preg_match($re2, $token)) - { - preg_match($re2, $token, $match); - $token = $match[1]; - $test1 = false; - } - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept13 = '/(ΣΚΩΛ|ΣΚΟΥΛ|ΝΑΡΘ|ΣΦ|ΟΘ|ΠΙΘ)$/'; - $exept14 = '/^(ΔΙΑΘ|Θ|ΠΑΡΑΚΑΤΑΘ|ΠΡΟΣΘ|ΣΥΝΘ|)$/'; - - if (preg_match($exept13, $token) || preg_match($exept14, $token)) - { - $token = $token . "ΗΚ"; - } - } - - // Step 5h - $re = '/^(.+?)(ΟΥΣΑ|ΟΥΣΕΣ|ΟΥΣΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept15 = '/^(ΦΑΡΜΑΚ|ΧΑΔ|ΑΓΚ|ΑΝΑΡΡ|ΒΡΟΜ|ΕΚΛΙΠ|ΛΑΜΠΙΔ|ΛΕΧ|Μ|ΠΑΤ|Ρ|Λ|ΜΕΔ|ΜΕΣΑΖ|ΥΠΟΤΕΙΝ|ΑΜ|ΑΙΘ|ΑΝΗΚ|ΔΕΣΠΟΖ|ΕΝΔΙΑΦΕΡ|ΔΕ|ΔΕΥΤΕΡΕΥ|ΚΑΘΑΡΕΥ|ΠΛΕ|ΤΣΑ)$/'; - $exept16 = '/(ΠΟΔΑΡ|ΒΛΕΠ|ΠΑΝΤΑΧ|ΦΡΥΔ|ΜΑΝΤΙΛ|ΜΑΛΛ|ΚΥΜΑΤ|ΛΑΧ|ΛΗΓ|ΦΑΓ|ΟΜ|ΠΡΩΤ)$/'; - - if (preg_match($exept15, $token) || preg_match($exept16, $token)) - { - $token = $token . "ΟΥΣ"; - } - } - - // Step 5i - $re = '/^(.+?)(ΑΓΑ|ΑΓΕΣ|ΑΓΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept17 = '/^(ΨΟΦ|ΝΑΥΛΟΧ)$/'; - $exept20 = '/(ΚΟΛΛ)$/'; - $exept18 = '/^(ΑΒΑΣΤ|ΠΟΛΥΦ|ΑΔΗΦ|ΠΑΜΦ|Ρ|ΑΣΠ|ΑΦ|ΑΜΑΛ|ΑΜΑΛΛΙ|ΑΝΥΣΤ|ΑΠΕΡ|ΑΣΠΑΡ|ΑΧΑΡ|ΔΕΡΒΕΝ|ΔΡΟΣΟΠ|ΞΕΦ|ΝΕΟΠ|ΝΟΜΟΤ|ΟΛΟΠ|ΟΜΟΤ|ΠΡΟΣΤ|ΠΡΟΣΩΠΟΠ|' - . 'ΣΥΜΠ|ΣΥΝΤ|Τ|ΥΠΟΤ|ΧΑΡ|ΑΕΙΠ|ΑΙΜΟΣΤ|ΑΝΥΠ|ΑΠΟΤ|ΑΡΤΙΠ|ΔΙΑΤ|ΕΝ|ΕΠΙΤ|ΚΡΟΚΑΛΟΠ|ΣΙΔΗΡΟΠ|Λ|ΝΑΥ|ΟΥΛΑΜ|ΟΥΡ|Π|ΤΡ|Μ)$/'; - $exept19 = '/(ΟΦ|ΠΕΛ|ΧΟΡΤ|ΛΛ|ΣΦ|ΡΠ|ΦΡ|ΠΡ|ΛΟΧ|ΣΜΗΝ)$/'; - - if ((preg_match($exept18, $token) || preg_match($exept19, $token)) - && !(preg_match($exept17, $token) || preg_match($exept20, $token))) - { - $token = $token . "ΑΓ"; - } - } - - // Step 5j - $re = '/^(.+?)(ΗΣΕ|ΗΣΟΥ|ΗΣΑ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept21 = '/^(Ν|ΧΕΡΣΟΝ|ΔΩΔΕΚΑΝ|ΕΡΗΜΟΝ|ΜΕΓΑΛΟΝ|ΕΠΤΑΝ)$/'; - - if (preg_match($exept21, $token)) - { - $token = $token . "ΗΣ"; - } - } - - // Step 5k - $re = '/^(.+?)(ΗΣΤΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept22 = '/^(ΑΣΒ|ΣΒ|ΑΧΡ|ΧΡ|ΑΠΛ|ΑΕΙΜΝ|ΔΥΣΧΡ|ΕΥΧΡ|ΚΟΙΝΟΧΡ|ΠΑΛΙΜΨ)$/'; - - if (preg_match($exept22, $token)) - { - $token = $token . "ΗΣΤ"; - } - } - - // Step 5l - $re = '/^(.+?)(ΟΥΝΕ|ΗΣΟΥΝΕ|ΗΘΟΥΝΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept23 = '/^(Ν|Ρ|ΣΠΙ|ΣΤΡΑΒΟΜΟΥΤΣ|ΚΑΚΟΜΟΥΤΣ|ΕΞΩΝ)$/'; - - if (preg_match($exept23, $token)) - { - $token = $token . "ΟΥΝ"; - } - } - - // Step 5m - $re = '/^(.+?)(ΟΥΜΕ|ΗΣΟΥΜΕ|ΗΘΟΥΜΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept24 = '/^(ΠΑΡΑΣΟΥΣ|Φ|Χ|ΩΡΙΟΠΛ|ΑΖ|ΑΛΛΟΣΟΥΣ|ΑΣΟΥΣ)$/'; - - if (preg_match($exept24, $token)) - { - $token = $token . "ΟΥΜ"; - } - } - - // Step 6 - $re = '/^(.+?)(ΜΑΤΑ|ΜΑΤΩΝ|ΜΑΤΟΣ)$/'; - $re2 = '/^(.+?)(Α|ΑΓΑΤΕ|ΑΓΑΝ|ΑΕΙ|ΑΜΑΙ|ΑΝ|ΑΣ|ΑΣΑΙ|ΑΤΑΙ|ΑΩ|Ε|ΕΙ|ΕΙΣ|ΕΙΤΕ|ΕΣΑΙ|ΕΣ|ΕΤΑΙ|Ι|ΙΕΜΑΙ|ΙΕΜΑΣΤΕ|ΙΕΤΑΙ|ΙΕΣΑΙ|ΙΕΣΑΣΤΕ|ΙΟΜΑΣΤΑΝ|ΙΟΜΟΥΝ|' - . 'ΙΟΜΟΥΝΑ|ΙΟΝΤΑΝ|ΙΟΝΤΟΥΣΑΝ|ΙΟΣΑΣΤΑΝ|ΙΟΣΑΣΤΕ|ΙΟΣΟΥΝ|ΙΟΣΟΥΝΑ|ΙΟΤΑΝ|ΙΟΥΜΑ|ΙΟΥΜΑΣΤΕ|ΙΟΥΝΤΑΙ|ΙΟΥΝΤΑΝ|Η|ΗΔΕΣ|ΗΔΩΝ|ΗΘΕΙ|ΗΘΕΙΣ|ΗΘΕΙΤΕ|' - . 'ΗΘΗΚΑΤΕ|ΗΘΗΚΑΝ|ΗΘΟΥΝ|ΗΘΩ|ΗΚΑΤΕ|ΗΚΑΝ|ΗΣ|ΗΣΑΝ|ΗΣΑΤΕ|ΗΣΕΙ|ΗΣΕΣ|ΗΣΟΥΝ|ΗΣΩ|Ο|ΟΙ|ΟΜΑΙ|ΟΜΑΣΤΑΝ|ΟΜΟΥΝ|ΟΜΟΥΝΑ|ΟΝΤΑΙ|ΟΝΤΑΝ|ΟΝΤΟΥΣΑΝ|ΟΣ|' - . 'ΟΣΑΣΤΑΝ|ΟΣΑΣΤΕ|ΟΣΟΥΝ|ΟΣΟΥΝΑ|ΟΤΑΝ|ΟΥ|ΟΥΜΑΙ|ΟΥΜΑΣΤΕ|ΟΥΝ|ΟΥΝΤΑΙ|ΟΥΝΤΑΝ|ΟΥΣ|ΟΥΣΑΝ|ΟΥΣΑΤΕ|Υ|ΥΣ|Ω|ΩΝ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1] . "ΜΑ"; - } - - if (preg_match($re2, $token) && $test1) - { - preg_match($re2, $token, $match); - $token = $match[1]; - } - - // Step 7 (ΠΑΡΑΘΕΤΙΚΑ) - $re = '/^(.+?)(ΕΣΤΕΡ|ΕΣΤΑΤ|ΟΤΕΡ|ΟΤΑΤ|ΥΤΕΡ|ΥΤΑΤ|ΩΤΕΡ|ΩΤΑΤ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - } - - return $this->toLowerCase($token, $wCase); - } - - /** - * Converts the token to uppercase, suppressing accents and diaeresis. The array $wCase contains a special map of - * the uppercase rule used to convert each character at each position. - * - * @param string $token Token to process - * @param array &$wCase Map of uppercase rules - * - * @return string - * - * @since 4.0.0 - */ - protected function toUpperCase($token, &$wCase) - { - $wCase = array_fill(0, mb_strlen($token, 'UTF-8'), 0); - $caseConvert = array( - "α" => 'Α', - "β" => 'Β', - "γ" => 'Γ', - "δ" => 'Δ', - "ε" => 'Ε', - "ζ" => 'Ζ', - "η" => 'Η', - "θ" => 'Θ', - "ι" => 'Ι', - "κ" => 'Κ', - "λ" => 'Λ', - "μ" => 'Μ', - "ν" => 'Ν', - "ξ" => 'Ξ', - "ο" => 'Ο', - "π" => 'Π', - "ρ" => 'Ρ', - "σ" => 'Σ', - "τ" => 'Τ', - "υ" => 'Υ', - "φ" => 'Φ', - "χ" => 'Χ', - "ψ" => 'Ψ', - "ω" => 'Ω', - "ά" => 'Α', - "έ" => 'Ε', - "ή" => 'Η', - "ί" => 'Ι', - "ό" => 'Ο', - "ύ" => 'Υ', - "ώ" => 'Ω', - "ς" => 'Σ', - "ϊ" => 'Ι', - "ϋ" => 'Ι', - "ΐ" => 'Ι', - "ΰ" => 'Υ', - ); - $newToken = ''; - - for ($i = 0; $i < mb_strlen($token); $i++) - { - $char = mb_substr($token, $i, 1); - $isLower = array_key_exists($char, $caseConvert); - - if (!$isLower) - { - $newToken .= $char; - - continue; - } - - $upperCase = $caseConvert[$char]; - $newToken .= $upperCase; - - $wCase[$i] = 1; - - if (in_array($char, ['ά', 'έ', 'ή', 'ί', 'ό', 'ύ', 'ώ', 'ς'])) - { - $wCase[$i] = 2; - } - - if (in_array($char, ['ϊ', 'ϋ'])) - { - $wCase[$i] = 3; - } - - if (in_array($char, ['ΐ', 'ΰ'])) - { - $wCase[$i] = 4; - } - } - - return $newToken; - } - - /** - * Converts the suppressed uppercase token back to lowercase, using the $wCase map to add back the accents, - * diaeresis and handle the special case of final sigma (different lowercase glyph than the regular sigma, only - * used at the end of words). - * - * @param string $token Token to process - * @param array $wCase Map of lowercase rules - * - * @return string - * - * @since 4.0.0 - */ - protected function toLowerCase($token, $wCase) - { - $newToken = ''; - - for ($i = 0; $i < mb_strlen($token); $i++) - { - $char = mb_substr($token, $i, 1); - - // Is $wCase not set at this position? We assume no case conversion ever took place. - if (!isset($wCase[$i])) - { - $newToken .= $char; - - continue; - } - - // The character was not case-converted - if ($wCase[$i] == 0) - { - $newToken .= $char; - - continue; - } - - // Case 1: Unaccented letter - if ($wCase[$i] == 1) - { - $newToken .= mb_strtolower($char); - - continue; - } - - // Case 2: Vowel with accent (tonos); or the special case of final sigma - if ($wCase[$i] == 2) - { - $charMap = [ - 'Α' => 'ά', - 'Ε' => 'έ', - 'Η' => 'ή', - 'Ι' => 'ί', - 'Ο' => 'ό', - 'Υ' => 'ύ', - 'Ω' => 'ώ', - 'Σ' => 'ς' - ]; - - $newToken .= $charMap[$char]; - - continue; - } - - // Case 3: vowels with diaeresis (dialytika) - if ($wCase[$i] == 3) - { - $charMap = [ - 'Ι' => 'ϊ', - 'Υ' => 'ϋ' - ]; - - $newToken .= $charMap[$char]; - - continue; - } - - // Case 4: vowels with both diaeresis (dialytika) and accent (tonos) - if ($wCase[$i] == 4) - { - $charMap = [ - 'Ι' => 'ΐ', - 'Υ' => 'ΰ' - ]; - - $newToken .= $charMap[$char]; - - continue; - } - - // This should never happen! - $newToken .= $char; - } - - return $newToken; - } + /** + * Language locale of the class + * + * @var string + * @since 4.0.0 + */ + public $language = 'el'; + + /** + * Method to construct the language object. + * + * @since 4.0.0 + */ + public function __construct($locale = null) + { + // Override parent constructor since we don't need to load an external stemmer + } + + /** + * Method to tokenise a text string. It takes into account the odd punctuation commonly used in Greek text, mapping + * it to ASCII punctuation. + * + * Reference: http://www.teicrete.gr/users/kutrulis/Glosika/Stixi.htm + * + * @param string $input The input to tokenise. + * + * @return array An array of term strings. + * + * @since 4.0.0 + */ + public function tokenise($input) + { + // Replace Greek calligraphic double quotes (various styles) to dumb double quotes + $input = str_replace(['“', '”', '„', '«' ,'»'], '"', $input); + + // Replace Greek calligraphic single quotes (various styles) to dumb single quotes + $input = str_replace(['‘','’','‚'], "'", $input); + + // Replace the middle dot (ano teleia) with a comma, adequate for the purpose of stemming + $input = str_replace('·', ',', $input); + + // Dot and dash (τελεία και παύλα), used to denote the end of a context at the end of a paragraph. + $input = str_replace('.–', '.', $input); + + // Ellipsis, two styles (separate dots or single glyph) + $input = str_replace(['...', '…'], '.', $input); + + // Cross. Marks the death date of a person. Removed. + $input = str_replace('†', '', $input); + + // Star. Reference, supposition word (in philology), birth date of a person. + $input = str_replace('*', '', $input); + + // Paragraph. Indicates change of subject. + $input = str_replace('§', '.', $input); + + // Plus/minus. Shows approximation. Not relevant for the stemmer, hence its conversion to a space. + $input = str_replace('±', ' ', $input); + + return parent::tokenise($input); + } + + /** + * Method to stem a token. + * + * @param string $token The token to stem. + * + * @return string The stemmed token. + * + * @since 4.0.0 + */ + public function stem($token) + { + $token = $this->toUpperCase($token, $wCase); + + // Stop-word removal + $stop_words = '/^(ΕΚΟ|ΑΒΑ|ΑΓΑ|ΑΓΗ|ΑΓΩ|ΑΔΗ|ΑΔΩ|ΑΕ|ΑΕΙ|ΑΘΩ|ΑΙ|ΑΙΚ|ΑΚΗ|ΑΚΟΜΑ|ΑΚΟΜΗ|ΑΚΡΙΒΩΣ|ΑΛΑ|ΑΛΗΘΕΙΑ|ΑΛΗΘΙΝΑ|ΑΛΛΑΧΟΥ|ΑΛΛΙΩΣ|ΑΛΛΙΩΤΙΚΑ|' + . 'ΑΛΛΟΙΩΣ|ΑΛΛΟΙΩΤΙΚΑ|ΑΛΛΟΤΕ|ΑΛΤ|ΑΛΩ|ΑΜΑ|ΑΜΕ|ΑΜΕΣΑ|ΑΜΕΣΩΣ|ΑΜΩ|ΑΝ|ΑΝΑ|ΑΝΑΜΕΣΑ|ΑΝΑΜΕΤΑΞΥ|ΑΝΕΥ|ΑΝΤΙ|ΑΝΤΙΠΕΡΑ|ΑΝΤΙΣ|ΑΝΩ|ΑΝΩΤΕΡΩ|ΑΞΑΦΝΑ|' + . 'ΑΠ|ΑΠΕΝΑΝΤΙ|ΑΠΟ|ΑΠΟΨΕ|ΑΠΩ|ΑΡΑ|ΑΡΑΓΕ|ΑΡΕ|ΑΡΚ|ΑΡΚΕΤΑ|ΑΡΛ|ΑΡΜ|ΑΡΤ|ΑΡΥ|ΑΡΩ|ΑΣ|ΑΣΑ|ΑΣΟ|ΑΤΑ|ΑΤΕ|ΑΤΗ|ΑΤΙ|ΑΤΜ|ΑΤΟ|ΑΥΡΙΟ|ΑΦΗ|ΑΦΟΤΟΥ|ΑΦΟΥ|' + . 'ΑΧ|ΑΧΕ|ΑΧΟ|ΑΨΑ|ΑΨΕ|ΑΨΗ|ΑΨΥ|ΑΩΕ|ΑΩΟ|ΒΑΝ|ΒΑΤ|ΒΑΧ|ΒΕΑ|ΒΕΒΑΙΟΤΑΤΑ|ΒΗΞ|ΒΙΑ|ΒΙΕ|ΒΙΗ|ΒΙΟ|ΒΟΗ|ΒΟΩ|ΒΡΕ|ΓΑ|ΓΑΒ|ΓΑΡ|ΓΕΝ|ΓΕΣ||ΓΗ|ΓΗΝ|ΓΙ|ΓΙΑ|' + . 'ΓΙΕ|ΓΙΝ|ΓΙΟ|ΓΚΙ|ΓΙΑΤΙ|ΓΚΥ|ΓΟΗ|ΓΟΟ|ΓΡΗΓΟΡΑ|ΓΡΙ|ΓΡΥ|ΓΥΗ|ΓΥΡΩ|ΔΑ|ΔΕ|ΔΕΗ|ΔΕΙ|ΔΕΝ|ΔΕΣ|ΔΗ|ΔΗΘΕΝ|ΔΗΛΑΔΗ|ΔΗΩ|ΔΙ|ΔΙΑ|ΔΙΑΡΚΩΣ|ΔΙΟΛΟΥ|ΔΙΣ|' + . 'ΔΙΧΩΣ|ΔΟΛ|ΔΟΝ|ΔΡΑ|ΔΡΥ|ΔΡΧ|ΔΥΕ|ΔΥΟ|ΔΩ|ΕΑΜ|ΕΑΝ|ΕΑΡ|ΕΘΗ|ΕΙ|ΕΙΔΕΜΗ|ΕΙΘΕ|ΕΙΜΑΙ|ΕΙΜΑΣΤΕ|ΕΙΝΑΙ|ΕΙΣ|ΕΙΣΑΙ|ΕΙΣΑΣΤΕ|ΕΙΣΤΕ|ΕΙΤΕ|ΕΙΧΑ|ΕΙΧΑΜΕ|' + . 'ΕΙΧΑΝ|ΕΙΧΑΤΕ|ΕΙΧΕ|ΕΙΧΕΣ|ΕΚ|ΕΚΕΙ|ΕΛΑ|ΕΛΙ|ΕΜΠ|ΕΝ|ΕΝΤΕΛΩΣ|ΕΝΤΟΣ|ΕΝΤΩΜΕΤΑΞΥ|ΕΝΩ|ΕΞ|ΕΞΑΦΝΑ|ΕΞΙ|ΕΞΙΣΟΥ|ΕΞΩ|ΕΟΚ|ΕΠΑΝΩ|ΕΠΕΙΔΗ|ΕΠΕΙΤΑ|ΕΠΗ|' + . 'ΕΠΙ|ΕΠΙΣΗΣ|ΕΠΟΜΕΝΩΣ|ΕΡΑ|ΕΣ|ΕΣΑΣ|ΕΣΕ|ΕΣΕΙΣ|ΕΣΕΝΑ|ΕΣΗ|ΕΣΤΩ|ΕΣΥ|ΕΣΩ|ΕΤΙ|ΕΤΣΙ|ΕΥ|ΕΥΑ|ΕΥΓΕ|ΕΥΘΥΣ|ΕΥΤΥΧΩΣ|ΕΦΕ|ΕΦΕΞΗΣ|ΕΦΤ|ΕΧΕ|ΕΧΕΙ|' + . 'ΕΧΕΙΣ|ΕΧΕΤΕ|ΕΧΘΕΣ|ΕΧΟΜΕ|ΕΧΟΥΜΕ|ΕΧΟΥΝ|ΕΧΤΕΣ|ΕΧΩ|ΕΩΣ|ΖΕΑ|ΖΕΗ|ΖΕΙ|ΖΕΝ|ΖΗΝ|ΖΩ|Η|ΗΔΗ|ΗΔΥ|ΗΘΗ|ΗΛΟ|ΗΜΙ|ΗΠΑ|ΗΣΑΣΤΕ|ΗΣΟΥΝ|ΗΤΑ|ΗΤΑΝ|ΗΤΑΝΕ|' + . 'ΗΤΟΙ|ΗΤΤΟΝ|ΗΩ|ΘΑ|ΘΥΕ|ΘΩΡ|Ι|ΙΑ|ΙΒΟ|ΙΔΗ|ΙΔΙΩΣ|ΙΕ|ΙΙ|ΙΙΙ|ΙΚΑ|ΙΛΟ|ΙΜΑ|ΙΝΑ|ΙΝΩ|ΙΞΕ|ΙΞΟ|ΙΟ|ΙΟΙ|ΙΣΑ|ΙΣΑΜΕ|ΙΣΕ|ΙΣΗ|ΙΣΙΑ|ΙΣΟ|ΙΣΩΣ|ΙΩΒ|ΙΩΝ|' + . 'ΙΩΣ|ΙΑΝ|ΚΑΘ|ΚΑΘΕ|ΚΑΘΕΤΙ|ΚΑΘΟΛΟΥ|ΚΑΘΩΣ|ΚΑΙ|ΚΑΝ|ΚΑΠΟΤΕ|ΚΑΠΟΥ|ΚΑΠΩΣ|ΚΑΤ|ΚΑΤΑ|ΚΑΤΙ|ΚΑΤΙΤΙ|ΚΑΤΟΠΙΝ|ΚΑΤΩ|ΚΑΩ|ΚΒΟ|ΚΕΑ|ΚΕΙ|ΚΕΝ|ΚΙ|ΚΙΜ|' + . 'ΚΙΟΛΑΣ|ΚΙΤ|ΚΙΧ|ΚΚΕ|ΚΛΙΣΕ|ΚΛΠ|ΚΟΚ|ΚΟΝΤΑ|ΚΟΧ|ΚΤΛ|ΚΥΡ|ΚΥΡΙΩΣ|ΚΩ|ΚΩΝ|ΛΑ|ΛΕΑ|ΛΕΝ|ΛΕΟ|ΛΙΑ|ΛΙΓΑΚΙ|ΛΙΓΟΥΛΑΚΙ|ΛΙΓΟ|ΛΙΓΩΤΕΡΟ|ΛΙΟ|ΛΙΡ|ΛΟΓΩ|' + . 'ΛΟΙΠΑ|ΛΟΙΠΟΝ|ΛΟΣ|ΛΣ|ΛΥΩ|ΜΑ|ΜΑΖΙ|ΜΑΚΑΡΙ|ΜΑΛΙΣΤΑ|ΜΑΛΛΟΝ|ΜΑΝ|ΜΑΞ|ΜΑΣ|ΜΑΤ|ΜΕ|ΜΕΘΑΥΡΙΟ|ΜΕΙ|ΜΕΙΟΝ|ΜΕΛ|ΜΕΛΕΙ|ΜΕΛΛΕΤΑΙ|ΜΕΜΙΑΣ|ΜΕΝ|ΜΕΣ|' + . 'ΜΕΣΑ|ΜΕΤ|ΜΕΤΑ|ΜΕΤΑΞΥ|ΜΕΧΡΙ|ΜΗ|ΜΗΔΕ|ΜΗΝ|ΜΗΠΩΣ|ΜΗΤΕ|ΜΙ|ΜΙΞ|ΜΙΣ|ΜΜΕ|ΜΝΑ|ΜΟΒ|ΜΟΛΙΣ|ΜΟΛΟΝΟΤΙ|ΜΟΝΑΧΑ|ΜΟΝΟΜΙΑΣ|ΜΙΑ|ΜΟΥ|ΜΠΑ|ΜΠΟΡΕΙ|' + . 'ΜΠΟΡΟΥΝ|ΜΠΡΑΒΟ|ΜΠΡΟΣ|ΜΠΩ|ΜΥ|ΜΥΑ|ΜΥΝ|ΝΑ|ΝΑΕ|ΝΑΙ|ΝΑΟ|ΝΔ|ΝΕΐ|ΝΕΑ|ΝΕΕ|ΝΕΟ|ΝΙ|ΝΙΑ|ΝΙΚ|ΝΙΛ|ΝΙΝ|ΝΙΟ|ΝΤΑ|ΝΤΕ|ΝΤΙ|ΝΤΟ|ΝΥΝ|ΝΩΕ|ΝΩΡΙΣ|ΞΑΝΑ|' + . 'ΞΑΦΝΙΚΑ|ΞΕΩ|ΞΙ|Ο|ΟΑ|ΟΑΠ|ΟΔΟ|ΟΕ|ΟΖΟ|ΟΗΕ|ΟΙ|ΟΙΑ|ΟΙΗ|ΟΚΑ|ΟΛΟΓΥΡΑ|ΟΛΟΝΕΝ|ΟΛΟΤΕΛΑ|ΟΛΩΣΔΙΟΛΟΥ|ΟΜΩΣ|ΟΝ|ΟΝΕ|ΟΝΟ|ΟΠΑ|ΟΠΕ|ΟΠΗ|ΟΠΟ|' + . 'ΟΠΟΙΑΔΗΠΟΤΕ|ΟΠΟΙΑΝΔΗΠΟΤΕ|ΟΠΟΙΑΣΔΗΠΟΤΕ|ΟΠΟΙΔΗΠΟΤΕ|ΟΠΟΙΕΣΔΗΠΟΤΕ|ΟΠΟΙΟΔΗΠΟΤΕ|ΟΠΟΙΟΝΔΗΠΟΤΕ|ΟΠΟΙΟΣΔΗΠΟΤΕ|ΟΠΟΙΟΥΔΗΠΟΤΕ|ΟΠΟΙΟΥΣΔΗΠΟΤΕ|' + . 'ΟΠΟΙΩΝΔΗΠΟΤΕ|ΟΠΟΤΕΔΗΠΟΤΕ|ΟΠΟΥ|ΟΠΟΥΔΗΠΟΤΕ|ΟΠΩΣ|ΟΡΑ|ΟΡΕ|ΟΡΗ|ΟΡΟ|ΟΡΦ|ΟΡΩ|ΟΣΑ|ΟΣΑΔΗΠΟΤΕ|ΟΣΕ|ΟΣΕΣΔΗΠΟΤΕ|ΟΣΗΔΗΠΟΤΕ|ΟΣΗΝΔΗΠΟΤΕ|' + . 'ΟΣΗΣΔΗΠΟΤΕ|ΟΣΟΔΗΠΟΤΕ|ΟΣΟΙΔΗΠΟΤΕ|ΟΣΟΝΔΗΠΟΤΕ|ΟΣΟΣΔΗΠΟΤΕ|ΟΣΟΥΔΗΠΟΤΕ|ΟΣΟΥΣΔΗΠΟΤΕ|ΟΣΩΝΔΗΠΟΤΕ|ΟΤΑΝ|ΟΤΕ|ΟΤΙ|ΟΤΙΔΗΠΟΤΕ|ΟΥ|ΟΥΔΕ|ΟΥΚ|ΟΥΣ|' + . 'ΟΥΤΕ|ΟΥΦ|ΟΧΙ|ΟΨΑ|ΟΨΕ|ΟΨΗ|ΟΨΙ|ΟΨΟ|ΠΑ|ΠΑΛΙ|ΠΑΝ|ΠΑΝΤΟΤΕ|ΠΑΝΤΟΥ|ΠΑΝΤΩΣ|ΠΑΠ|ΠΑΡ|ΠΑΡΑ|ΠΕΙ|ΠΕΡ|ΠΕΡΑ|ΠΕΡΙ|ΠΕΡΙΠΟΥ|ΠΕΡΣΙ|ΠΕΡΥΣΙ|ΠΕΣ|ΠΙ|' + . 'ΠΙΑ|ΠΙΘΑΝΟΝ|ΠΙΚ|ΠΙΟ|ΠΙΣΩ|ΠΙΤ|ΠΙΩ|ΠΛΑΙ|ΠΛΕΟΝ|ΠΛΗΝ|ΠΛΩ|ΠΜ|ΠΟΑ|ΠΟΕ|ΠΟΛ|ΠΟΛΥ|ΠΟΠ|ΠΟΤΕ|ΠΟΥ|ΠΟΥΘΕ|ΠΟΥΘΕΝΑ|ΠΡΕΠΕΙ|ΠΡΙ|ΠΡΙΝ|ΠΡΟ|' + . 'ΠΡΟΚΕΙΜΕΝΟΥ|ΠΡΟΚΕΙΤΑΙ|ΠΡΟΠΕΡΣΙ|ΠΡΟΣ|ΠΡΟΤΟΥ|ΠΡΟΧΘΕΣ|ΠΡΟΧΤΕΣ|ΠΡΩΤΥΤΕΡΑ|ΠΥΑ|ΠΥΞ|ΠΥΟ|ΠΥΡ|ΠΧ|ΠΩ|ΠΩΛ|ΠΩΣ|ΡΑ|ΡΑΙ|ΡΑΠ|ΡΑΣ|ΡΕ|ΡΕΑ|ΡΕΕ|ΡΕΙ|' + . 'ΡΗΣ|ΡΘΩ|ΡΙΟ|ΡΟ|ΡΟΐ|ΡΟΕ|ΡΟΖ|ΡΟΗ|ΡΟΘ|ΡΟΙ|ΡΟΚ|ΡΟΛ|ΡΟΝ|ΡΟΣ|ΡΟΥ|ΣΑΙ|ΣΑΝ|ΣΑΟ|ΣΑΣ|ΣΕ|ΣΕΙΣ|ΣΕΚ|ΣΕΞ|ΣΕΡ|ΣΕΤ|ΣΕΦ|ΣΗΜΕΡΑ|ΣΙ|ΣΙΑ|ΣΙΓΑ|ΣΙΚ|' + . 'ΣΙΧ|ΣΚΙ|ΣΟΙ|ΣΟΚ|ΣΟΛ|ΣΟΝ|ΣΟΣ|ΣΟΥ|ΣΡΙ|ΣΤΑ|ΣΤΗ|ΣΤΗΝ|ΣΤΗΣ|ΣΤΙΣ|ΣΤΟ|ΣΤΟΝ|ΣΤΟΥ|ΣΤΟΥΣ|ΣΤΩΝ|ΣΥ|ΣΥΓΧΡΟΝΩΣ|ΣΥΝ|ΣΥΝΑΜΑ|ΣΥΝΕΠΩΣ|ΣΥΝΗΘΩΣ|' + . 'ΣΧΕΔΟΝ|ΣΩΣΤΑ|ΤΑ|ΤΑΔΕ|ΤΑΚ|ΤΑΝ|ΤΑΟ|ΤΑΥ|ΤΑΧΑ|ΤΑΧΑΤΕ|ΤΕ|ΤΕΙ|ΤΕΛ|ΤΕΛΙΚΑ|ΤΕΛΙΚΩΣ|ΤΕΣ|ΤΕΤ|ΤΖΟ|ΤΗ|ΤΗΛ|ΤΗΝ|ΤΗΣ|ΤΙ|ΤΙΚ|ΤΙΜ|ΤΙΠΟΤΑ|ΤΙΠΟΤΕ|' + . 'ΤΙΣ|ΤΝΤ|ΤΟ|ΤΟΙ|ΤΟΚ|ΤΟΜ|ΤΟΝ|ΤΟΠ|ΤΟΣ|ΤΟΣ?Ν|ΤΟΣΑ|ΤΟΣΕΣ|ΤΟΣΗ|ΤΟΣΗΝ|ΤΟΣΗΣ|ΤΟΣΟ|ΤΟΣΟΙ|ΤΟΣΟΝ|ΤΟΣΟΣ|ΤΟΣΟΥ|ΤΟΣΟΥΣ|ΤΟΤΕ|ΤΟΥ|ΤΟΥΛΑΧΙΣΤΟ|' + . 'ΤΟΥΛΑΧΙΣΤΟΝ|ΤΟΥΣ|ΤΣ|ΤΣΑ|ΤΣΕ|ΤΥΧΟΝ|ΤΩ|ΤΩΝ|ΤΩΡΑ|ΥΑΣ|ΥΒΑ|ΥΒΟ|ΥΙΕ|ΥΙΟ|ΥΛΑ|ΥΛΗ|ΥΝΙ|ΥΠ|ΥΠΕΡ|ΥΠΟ|ΥΠΟΨΗ|ΥΠΟΨΙΝ|ΥΣΤΕΡΑ|ΥΦΗ|ΥΨΗ|ΦΑ|ΦΑΐ|ΦΑΕ|' + . 'ΦΑΝ|ΦΑΞ|ΦΑΣ|ΦΑΩ|ΦΕΖ|ΦΕΙ|ΦΕΤΟΣ|ΦΕΥ|ΦΙ|ΦΙΛ|ΦΙΣ|ΦΟΞ|ΦΠΑ|ΦΡΙ|ΧΑ|ΧΑΗ|ΧΑΛ|ΧΑΝ|ΧΑΦ|ΧΕ|ΧΕΙ|ΧΘΕΣ|ΧΙ|ΧΙΑ|ΧΙΛ|ΧΙΟ|ΧΛΜ|ΧΜ|ΧΟΗ|ΧΟΛ|ΧΡΩ|ΧΤΕΣ|' + . 'ΧΩΡΙΣ|ΧΩΡΙΣΤΑ|ΨΕΣ|ΨΗΛΑ|ΨΙ|ΨΙΤ|Ω|ΩΑ|ΩΑΣ|ΩΔΕ|ΩΕΣ|ΩΘΩ|ΩΜΑ|ΩΜΕ|ΩΝ|ΩΟ|ΩΟΝ|ΩΟΥ|ΩΣ|ΩΣΑΝ|ΩΣΗ|ΩΣΟΤΟΥ|ΩΣΠΟΥ|ΩΣΤΕ|ΩΣΤΟΣΟ|ΩΤΑ|ΩΧ|ΩΩΝ)$/'; + + if (preg_match($stop_words, $token)) { + return $this->toLowerCase($token, $wCase); + } + + // Vowels + $v = '(Α|Ε|Η|Ι|Ο|Υ|Ω)'; + + // Vowels without Y + $v2 = '(Α|Ε|Η|Ι|Ο|Ω)'; + + $test1 = true; + + // Step S1. 14 stems + $re = '/^(.+?)(ΙΖΑ|ΙΖΕΣ|ΙΖΕ|ΙΖΑΜΕ|ΙΖΑΤΕ|ΙΖΑΝ|ΙΖΑΝΕ|ΙΖΩ|ΙΖΕΙΣ|ΙΖΕΙ|ΙΖΟΥΜΕ|ΙΖΕΤΕ|ΙΖΟΥΝ|ΙΖΟΥΝΕ)$/'; + $exceptS1 = '/^(ΑΝΑΜΠΑ|ΕΜΠΑ|ΕΠΑ|ΞΑΝΑΠΑ|ΠΑ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ)$/'; + $exceptS2 = '/^(ΜΑΡΚ|ΚΟΡΝ|ΑΜΠΑΡ|ΑΡΡ|ΒΑΘΥΡΙ|ΒΑΡΚ|Β|ΒΟΛΒΟΡ|ΓΚΡ|ΓΛΥΚΟΡ|ΓΛΥΚΥΡ|ΙΜΠ|Λ|ΛΟΥ|ΜΑΡ|Μ|ΠΡ|ΜΠΡ|ΠΟΛΥΡ|Π|Ρ|ΠΙΠΕΡΟΡ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . 'I'; + } + + if (preg_match($exceptS2, $token)) { + $token = $token . 'IΖ'; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S2. 7 stems + $re = '/^(.+?)(ΩΘΗΚΑ|ΩΘΗΚΕΣ|ΩΘΗΚΕ|ΩΘΗΚΑΜΕ|ΩΘΗΚΑΤΕ|ΩΘΗΚΑΝ|ΩΘΗΚΑΝΕ)$/'; + $exceptS1 = '/^(ΑΛ|ΒΙ|ΕΝ|ΥΨ|ΛΙ|ΖΩ|Σ|Χ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . 'ΩΝ'; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S3. 7 stems + $re = '/^(.+?)(ΙΣΑ|ΙΣΕΣ|ΙΣΕ|ΙΣΑΜΕ|ΙΣΑΤΕ|ΙΣΑΝ|ΙΣΑΝΕ)$/'; + $exceptS1 = '/^(ΑΝΑΜΠΑ|ΑΘΡΟ|ΕΜΠΑ|ΕΣΕ|ΕΣΩΚΛΕ|ΕΠΑ|ΞΑΝΑΠΑ|ΕΠΕ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ|ΚΛΕ|ΧΑΡΤΟΠΑ|ΕΞΑΡΧΑ|ΜΕΤΕΠΕ|ΑΠΟΚΛΕ|ΑΠΕΚΛΕ|ΕΚΛΕ|ΠΕ|ΠΕΡΙΠΑ)$/'; + $exceptS2 = '/^(ΑΝ|ΑΦ|ΓΕ|ΓΙΓΑΝΤΟΑΦ|ΓΚΕ|ΔΗΜΟΚΡΑΤ|ΚΟΜ|ΓΚ|Μ|Π|ΠΟΥΚΑΜ|ΟΛΟ|ΛΑΡ)$/'; + + if ($token == "ΙΣΑ") { + $token = "ΙΣ"; + + return $token; + } + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . 'Ι'; + } + + if (preg_match($exceptS2, $token)) { + $token = $token . 'ΙΣ'; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S4. 7 stems + $re = '/^(.+?)(ΙΣΩ|ΙΣΕΙΣ|ΙΣΕΙ|ΙΣΟΥΜΕ|ΙΣΕΤΕ|ΙΣΟΥΝ|ΙΣΟΥΝΕ)$/'; + $exceptS1 = '/^(ΑΝΑΜΠΑ|ΕΜΠΑ|ΕΣΕ|ΕΣΩΚΛΕ|ΕΠΑ|ΞΑΝΑΠΑ|ΕΠΕ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ|ΚΛΕ|ΧΑΡΤΟΠΑ|ΕΞΑΡΧΑ|ΜΕΤΕΠΕ|ΑΠΟΚΛΕ|ΑΠΕΚΛΕ|ΕΚΛΕ|ΠΕ|ΠΕΡΙΠΑ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . 'Ι'; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S5. 11 stems + $re = '/^(.+?)(ΙΣΤΟΣ|ΙΣΤΟΥ|ΙΣΤΟ|ΙΣΤΕ|ΙΣΤΟΙ|ΙΣΤΩΝ|ΙΣΤΟΥΣ|ΙΣΤΗ|ΙΣΤΗΣ|ΙΣΤΑ|ΙΣΤΕΣ)$/'; + $exceptS1 = '/^(Μ|Π|ΑΠ|ΑΡ|ΗΔ|ΚΤ|ΣΚ|ΣΧ|ΥΨ|ΦΑ|ΧΡ|ΧΤ|ΑΚΤ|ΑΟΡ|ΑΣΧ|ΑΤΑ|ΑΧΝ|ΑΧΤ|ΓΕΜ|ΓΥΡ|ΕΜΠ|ΕΥΠ|ΕΧΘ|ΗΦΑ|ΚΑΘ|ΚΑΚ|ΚΥΛ|ΛΥΓ|ΜΑΚ|ΜΕΓ|ΤΑΧ|ΦΙΛ|ΧΩΡ)$/'; + $exceptS2 = '/^(ΔΑΝΕ|ΣΥΝΑΘΡΟ|ΚΛΕ|ΣΕ|ΕΣΩΚΛΕ|ΑΣΕ|ΠΛΕ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . 'ΙΣΤ'; + } + + if (preg_match($exceptS2, $token)) { + $token = $token . 'Ι'; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S6. 6 stems + $re = '/^(.+?)(ΙΣΜΟ|ΙΣΜΟΙ|ΙΣΜΟΣ|ΙΣΜΟΥ|ΙΣΜΟΥΣ|ΙΣΜΩΝ)$/'; + $exceptS1 = '/^(ΑΓΝΩΣΤΙΚ|ΑΤΟΜΙΚ|ΓΝΩΣΤΙΚ|ΕΘΝΙΚ|ΕΚΛΕΚΤΙΚ|ΣΚΕΠΤΙΚ|ΤΟΠΙΚ)$/'; + $exceptS2 = '/^(ΣΕ|ΜΕΤΑΣΕ|ΜΙΚΡΟΣΕ|ΕΓΚΛΕ|ΑΠΟΚΛΕ)$/'; + $exceptS3 = '/^(ΔΑΝΕ|ΑΝΤΙΔΑΝΕ)$/'; + $exceptS4 = '/^(ΑΛΕΞΑΝΔΡΙΝ|ΒΥΖΑΝΤΙΝ|ΘΕΑΤΡΙΝ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = str_replace('ΙΚ', "", $token); + } + + if (preg_match($exceptS2, $token)) { + $token = $token . "ΙΣΜ"; + } + + if (preg_match($exceptS3, $token)) { + $token = $token . "Ι"; + } + + if (preg_match($exceptS4, $token)) { + $token = str_replace('ΙΝ', "", $token); + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S7. 4 stems + $re = '/^(.+?)(ΑΡΑΚΙ|ΑΡΑΚΙΑ|ΟΥΔΑΚΙ|ΟΥΔΑΚΙΑ)$/'; + $exceptS1 = '/^(Σ|Χ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . "AΡΑΚ"; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S8. 8 stems + $re = '/^(.+?)(ΑΚΙ|ΑΚΙΑ|ΙΤΣΑ|ΙΤΣΑΣ|ΙΤΣΕΣ|ΙΤΣΩΝ|ΑΡΑΚΙ|ΑΡΑΚΙΑ)$/'; + $exceptS1 = '/^(ΑΝΘΡ|ΒΑΜΒ|ΒΡ|ΚΑΙΜ|ΚΟΝ|ΚΟΡ|ΛΑΒΡ|ΛΟΥΛ|ΜΕΡ|ΜΟΥΣΤ|ΝΑΓΚΑΣ|ΠΛ|Ρ|ΡΥ|Σ|ΣΚ|ΣΟΚ|ΣΠΑΝ|ΤΖ|ΦΑΡΜ|Χ|' + . 'ΚΑΠΑΚ|ΑΛΙΣΦ|ΑΜΒΡ|ΑΝΘΡ|Κ|ΦΥΛ|ΚΑΤΡΑΠ|ΚΛΙΜ|ΜΑΛ|ΣΛΟΒ|Φ|ΣΦ|ΤΣΕΧΟΣΛΟΒ)$/'; + $exceptS2 = '/^(Β|ΒΑΛ|ΓΙΑΝ|ΓΛ|Ζ|ΗΓΟΥΜΕΝ|ΚΑΡΔ|ΚΟΝ|ΜΑΚΡΥΝ|ΝΥΦ|ΠΑΤΕΡ|Π|ΣΚ|ΤΟΣ|ΤΡΙΠΟΛ)$/'; + + // For words like ΠΛΟΥΣΙΟΚΟΡΙΤΣΑ, ΠΑΛΙΟΚΟΡΙΤΣΑ etc + $exceptS3 = '/(ΚΟΡ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . "ΑΚ"; + } + + if (preg_match($exceptS2, $token)) { + $token = $token . "ΙΤΣ"; + } + + if (preg_match($exceptS3, $token)) { + $token = $token . "ΙΤΣ"; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S9. 3 stems + $re = '/^(.+?)(ΙΔΙΟ|ΙΔΙΑ|ΙΔΙΩΝ)$/'; + $exceptS1 = '/^(ΑΙΦΝ|ΙΡ|ΟΛΟ|ΨΑΛ)$/'; + $exceptS2 = '/(Ε|ΠΑΙΧΝ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . "ΙΔ"; + } + + if (preg_match($exceptS2, $token)) { + $token = $token . "ΙΔ"; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S10. 4 stems + $re = '/^(.+?)(ΙΣΚΟΣ|ΙΣΚΟΥ|ΙΣΚΟ|ΙΣΚΕ)$/'; + $exceptS1 = '/^(Δ|ΙΒ|ΜΗΝ|Ρ|ΦΡΑΓΚ|ΛΥΚ|ΟΒΕΛ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . "ΙΣΚ"; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step 1 + // step1list is used in Step 1. 41 stems + $step1list = array(); + $step1list["ΦΑΓΙΑ"] = "ΦΑ"; + $step1list["ΦΑΓΙΟΥ"] = "ΦΑ"; + $step1list["ΦΑΓΙΩΝ"] = "ΦΑ"; + $step1list["ΣΚΑΓΙΑ"] = "ΣΚΑ"; + $step1list["ΣΚΑΓΙΟΥ"] = "ΣΚΑ"; + $step1list["ΣΚΑΓΙΩΝ"] = "ΣΚΑ"; + $step1list["ΟΛΟΓΙΟΥ"] = "ΟΛΟ"; + $step1list["ΟΛΟΓΙΑ"] = "ΟΛΟ"; + $step1list["ΟΛΟΓΙΩΝ"] = "ΟΛΟ"; + $step1list["ΣΟΓΙΟΥ"] = "ΣΟ"; + $step1list["ΣΟΓΙΑ"] = "ΣΟ"; + $step1list["ΣΟΓΙΩΝ"] = "ΣΟ"; + $step1list["ΤΑΤΟΓΙΑ"] = "ΤΑΤΟ"; + $step1list["ΤΑΤΟΓΙΟΥ"] = "ΤΑΤΟ"; + $step1list["ΤΑΤΟΓΙΩΝ"] = "ΤΑΤΟ"; + $step1list["ΚΡΕΑΣ"] = "ΚΡΕ"; + $step1list["ΚΡΕΑΤΟΣ"] = "ΚΡΕ"; + $step1list["ΚΡΕΑΤΑ"] = "ΚΡΕ"; + $step1list["ΚΡΕΑΤΩΝ"] = "ΚΡΕ"; + $step1list["ΠΕΡΑΣ"] = "ΠΕΡ"; + $step1list["ΠΕΡΑΤΟΣ"] = "ΠΕΡ"; + + // Added by Spyros. Also at $re in step1 + $step1list["ΠΕΡΑΤΗ"] = "ΠΕΡ"; + $step1list["ΠΕΡΑΤΑ"] = "ΠΕΡ"; + $step1list["ΠΕΡΑΤΩΝ"] = "ΠΕΡ"; + $step1list["ΤΕΡΑΣ"] = "ΤΕΡ"; + $step1list["ΤΕΡΑΤΟΣ"] = "ΤΕΡ"; + $step1list["ΤΕΡΑΤΑ"] = "ΤΕΡ"; + $step1list["ΤΕΡΑΤΩΝ"] = "ΤΕΡ"; + $step1list["ΦΩΣ"] = "ΦΩ"; + $step1list["ΦΩΤΟΣ"] = "ΦΩ"; + $step1list["ΦΩΤΑ"] = "ΦΩ"; + $step1list["ΦΩΤΩΝ"] = "ΦΩ"; + $step1list["ΚΑΘΕΣΤΩΣ"] = "ΚΑΘΕΣΤ"; + $step1list["ΚΑΘΕΣΤΩΤΟΣ"] = "ΚΑΘΕΣΤ"; + $step1list["ΚΑΘΕΣΤΩΤΑ"] = "ΚΑΘΕΣΤ"; + $step1list["ΚΑΘΕΣΤΩΤΩΝ"] = "ΚΑΘΕΣΤ"; + $step1list["ΓΕΓΟΝΟΣ"] = "ΓΕΓΟΝ"; + $step1list["ΓΕΓΟΝΟΤΟΣ"] = "ΓΕΓΟΝ"; + $step1list["ΓΕΓΟΝΟΤΑ"] = "ΓΕΓΟΝ"; + $step1list["ΓΕΓΟΝΟΤΩΝ"] = "ΓΕΓΟΝ"; + + $re = '/(.*)(ΦΑΓΙΑ|ΦΑΓΙΟΥ|ΦΑΓΙΩΝ|ΣΚΑΓΙΑ|ΣΚΑΓΙΟΥ|ΣΚΑΓΙΩΝ|ΟΛΟΓΙΟΥ|ΟΛΟΓΙΑ|ΟΛΟΓΙΩΝ|ΣΟΓΙΟΥ|ΣΟΓΙΑ|ΣΟΓΙΩΝ|ΤΑΤΟΓΙΑ|ΤΑΤΟΓΙΟΥ|ΤΑΤΟΓΙΩΝ|ΚΡΕΑΣ|ΚΡΕΑΤΟΣ|' + . 'ΚΡΕΑΤΑ|ΚΡΕΑΤΩΝ|ΠΕΡΑΣ|ΠΕΡΑΤΟΣ|ΠΕΡΑΤΗ|ΠΕΡΑΤΑ|ΠΕΡΑΤΩΝ|ΤΕΡΑΣ|ΤΕΡΑΤΟΣ|ΤΕΡΑΤΑ|ΤΕΡΑΤΩΝ|ΦΩΣ|ΦΩΤΟΣ|ΦΩΤΑ|ΦΩΤΩΝ|ΚΑΘΕΣΤΩΣ|ΚΑΘΕΣΤΩΤΟΣ|' + . 'ΚΑΘΕΣΤΩΤΑ|ΚΑΘΕΣΤΩΤΩΝ|ΓΕΓΟΝΟΣ|ΓΕΓΟΝΟΤΟΣ|ΓΕΓΟΝΟΤΑ|ΓΕΓΟΝΟΤΩΝ)$/'; + + if (preg_match($re, $token, $match)) { + $stem = $match[1]; + $suffix = $match[2]; + $token = $stem . (array_key_exists($suffix, $step1list) ? $step1list[$suffix] : ''); + $test1 = false; + } + + // Step 2a. 2 stems + $re = '/^(.+?)(ΑΔΕΣ|ΑΔΩΝ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + $re = '/(ΟΚ|ΜΑΜ|ΜΑΝ|ΜΠΑΜΠ|ΠΑΤΕΡ|ΓΙΑΓΙ|ΝΤΑΝΤ|ΚΥΡ|ΘΕΙ|ΠΕΘΕΡ)$/'; + + if (!preg_match($re, $token)) { + $token = $token . "ΑΔ"; + } + } + + // Step 2b. 2 stems + $re = '/^(.+?)(ΕΔΕΣ|ΕΔΩΝ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $exept2 = '/(ΟΠ|ΙΠ|ΕΜΠ|ΥΠ|ΓΗΠ|ΔΑΠ|ΚΡΑΣΠ|ΜΙΛ)$/'; + + if (preg_match($exept2, $token)) { + $token = $token . 'ΕΔ'; + } + } + + // Step 2c + $re = '/^(.+?)(ΟΥΔΕΣ|ΟΥΔΩΝ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + + $exept3 = '/(ΑΡΚ|ΚΑΛΙΑΚ|ΠΕΤΑΛ|ΛΙΧ|ΠΛΕΞ|ΣΚ|Σ|ΦΛ|ΦΡ|ΒΕΛ|ΛΟΥΛ|ΧΝ|ΣΠ|ΤΡΑΓ|ΦΕ)$/'; + + if (preg_match($exept3, $token)) { + $token = $token . 'ΟΥΔ'; + } + } + + // Step 2d + $re = '/^(.+?)(ΕΩΣ|ΕΩΝ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept4 = '/^(Θ|Δ|ΕΛ|ΓΑΛ|Ν|Π|ΙΔ|ΠΑΡ)$/'; + + if (preg_match($exept4, $token)) { + $token = $token . 'Ε'; + } + } + + // Step 3 + $re = '/^(.+?)(ΙΑ|ΙΟΥ|ΙΩΝ)$/'; + + if (preg_match($re, $token, $fp)) { + $stem = $fp[1]; + $token = $stem; + $re = '/' . $v . '$/'; + $test1 = false; + + if (preg_match($re, $token)) { + $token = $stem . 'Ι'; + } + } + + // Step 4 + $re = '/^(.+?)(ΙΚΑ|ΙΚΟ|ΙΚΟΥ|ΙΚΩΝ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $re = '/' . $v . '$/'; + $exept5 = '/^(ΑΛ|ΑΔ|ΕΝΔ|ΑΜΑΝ|ΑΜΜΟΧΑΛ|ΗΘ|ΑΝΗΘ|ΑΝΤΙΔ|ΦΥΣ|ΒΡΩΜ|ΓΕΡ|ΕΞΩΔ|ΚΑΛΠ|ΚΑΛΛΙΝ|ΚΑΤΑΔ|ΜΟΥΛ|ΜΠΑΝ|ΜΠΑΓΙΑΤ|ΜΠΟΛ|ΜΠΟΣ|ΝΙΤ|ΞΙΚ|ΣΥΝΟΜΗΛ|ΠΕΤΣ|' + . 'ΠΙΤΣ|ΠΙΚΑΝΤ|ΠΛΙΑΤΣ|ΠΟΣΤΕΛΝ|ΠΡΩΤΟΔ|ΣΕΡΤ|ΣΥΝΑΔ|ΤΣΑΜ|ΥΠΟΔ|ΦΙΛΟΝ|ΦΥΛΟΔ|ΧΑΣ)$/'; + + if (preg_match($re, $token) || preg_match($exept5, $token)) { + $token = $token . 'ΙΚ'; + } + } + + // Step 5a + $re = '/^(.+?)(ΑΜΕ)$/'; + $re2 = '/^(.+?)(ΑΓΑΜΕ|ΗΣΑΜΕ|ΟΥΣΑΜΕ|ΗΚΑΜΕ|ΗΘΗΚΑΜΕ)$/'; + + if ($token == "ΑΓΑΜΕ") { + $token = "ΑΓΑΜ"; + } + + if (preg_match($re2, $token)) { + preg_match($re2, $token, $match); + $token = $match[1]; + $test1 = false; + } + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept6 = '/^(ΑΝΑΠ|ΑΠΟΘ|ΑΠΟΚ|ΑΠΟΣΤ|ΒΟΥΒ|ΞΕΘ|ΟΥΛ|ΠΕΘ|ΠΙΚΡ|ΠΟΤ|ΣΙΧ|Χ)$/'; + + if (preg_match($exept6, $token)) { + $token = $token . "ΑΜ"; + } + } + + // Step 5b + $re2 = '/^(.+?)(ΑΝΕ)$/'; + $re3 = '/^(.+?)(ΑΓΑΝΕ|ΗΣΑΝΕ|ΟΥΣΑΝΕ|ΙΟΝΤΑΝΕ|ΙΟΤΑΝΕ|ΙΟΥΝΤΑΝΕ|ΟΝΤΑΝΕ|ΟΤΑΝΕ|ΟΥΝΤΑΝΕ|ΗΚΑΝΕ|ΗΘΗΚΑΝΕ)$/'; + + if (preg_match($re3, $token)) { + preg_match($re3, $token, $match); + $token = $match[1]; + $test1 = false; + $re3 = '/^(ΤΡ|ΤΣ)$/'; + + if (preg_match($re3, $token)) { + $token = $token . "ΑΓΑΝ"; + } + } + + if (preg_match($re2, $token)) { + preg_match($re2, $token, $match); + $token = $match[1]; + $test1 = false; + $re2 = '/' . $v2 . '$/'; + $exept7 = '/^(ΒΕΤΕΡ|ΒΟΥΛΚ|ΒΡΑΧΜ|Γ|ΔΡΑΔΟΥΜ|Θ|ΚΑΛΠΟΥΖ|ΚΑΣΤΕΛ|ΚΟΡΜΟΡ|ΛΑΟΠΛ|ΜΩΑΜΕΘ|Μ|ΜΟΥΣΟΥΛΜ|Ν|ΟΥΛ|Π|ΠΕΛΕΚ|ΠΛ|ΠΟΛΙΣ|ΠΟΡΤΟΛ|ΣΑΡΑΚΑΤΣ|ΣΟΥΛΤ|' + . 'ΤΣΑΡΛΑΤ|ΟΡΦ|ΤΣΙΓΓ|ΤΣΟΠ|ΦΩΤΟΣΤΕΦ|Χ|ΨΥΧΟΠΛ|ΑΓ|ΟΡΦ|ΓΑΛ|ΓΕΡ|ΔΕΚ|ΔΙΠΛ|ΑΜΕΡΙΚΑΝ|ΟΥΡ|ΠΙΘ|ΠΟΥΡΙΤ|Σ|ΖΩΝΤ|ΙΚ|ΚΑΣΤ|ΚΟΠ|ΛΙΧ|ΛΟΥΘΗΡ|ΜΑΙΝΤ|' + . 'ΜΕΛ|ΣΙΓ|ΣΠ|ΣΤΕΓ|ΤΡΑΓ|ΤΣΑΓ|Φ|ΕΡ|ΑΔΑΠ|ΑΘΙΓΓ|ΑΜΗΧ|ΑΝΙΚ|ΑΝΟΡΓ|ΑΠΗΓ|ΑΠΙΘ|ΑΤΣΙΓΓ|ΒΑΣ|ΒΑΣΚ|ΒΑΘΥΓΑΛ|ΒΙΟΜΗΧ|ΒΡΑΧΥΚ|ΔΙΑΤ|ΔΙΑΦ|ΕΝΟΡΓ|' + . 'ΘΥΣ|ΚΑΠΝΟΒΙΟΜΗΧ|ΚΑΤΑΓΑΛ|ΚΛΙΒ|ΚΟΙΛΑΡΦ|ΛΙΒ|ΜΕΓΛΟΒΙΟΜΗΧ|ΜΙΚΡΟΒΙΟΜΗΧ|ΝΤΑΒ|ΞΗΡΟΚΛΙΒ|ΟΛΙΓΟΔΑΜ|ΟΛΟΓΑΛ|ΠΕΝΤΑΡΦ|ΠΕΡΗΦ|ΠΕΡΙΤΡ|ΠΛΑΤ|' + . 'ΠΟΛΥΔΑΠ|ΠΟΛΥΜΗΧ|ΣΤΕΦ|ΤΑΒ|ΤΕΤ|ΥΠΕΡΗΦ|ΥΠΟΚΟΠ|ΧΑΜΗΛΟΔΑΠ|ΨΗΛΟΤΑΒ)$/'; + + if (preg_match($re2, $token) || preg_match($exept7, $token)) { + $token = $token . "ΑΝ"; + } + } + + // Step 5c + $re3 = '/^(.+?)(ΕΤΕ)$/'; + $re4 = '/^(.+?)(ΗΣΕΤΕ)$/'; + + if (preg_match($re4, $token)) { + preg_match($re4, $token, $match); + $token = $match[1]; + $test1 = false; + } + + if (preg_match($re3, $token)) { + preg_match($re3, $token, $match); + $token = $match[1]; + $test1 = false; + $re3 = '/' . $v2 . '$/'; + $exept8 = '/(ΟΔ|ΑΙΡ|ΦΟΡ|ΤΑΘ|ΔΙΑΘ|ΣΧ|ΕΝΔ|ΕΥΡ|ΤΙΘ|ΥΠΕΡΘ|ΡΑΘ|ΕΝΘ|ΡΟΘ|ΣΘ|ΠΥΡ|ΑΙΝ|ΣΥΝΔ|ΣΥΝ|ΣΥΝΘ|ΧΩΡ|ΠΟΝ|ΒΡ|ΚΑΘ|ΕΥΘ|ΕΚΘ|ΝΕΤ|ΡΟΝ|ΑΡΚ|ΒΑΡ|ΒΟΛ|ΩΦΕΛ)$/'; + $exept9 = '/^(ΑΒΑΡ|ΒΕΝ|ΕΝΑΡ|ΑΒΡ|ΑΔ|ΑΘ|ΑΝ|ΑΠΛ|ΒΑΡΟΝ|ΝΤΡ|ΣΚ|ΚΟΠ|ΜΠΟΡ|ΝΙΦ|ΠΑΓ|ΠΑΡΑΚΑΛ|ΣΕΡΠ|ΣΚΕΛ|ΣΥΡΦ|ΤΟΚ|Υ|Δ|ΕΜ|ΘΑΡΡ|Θ)$/'; + + if (preg_match($re3, $token) || preg_match($exept8, $token) || preg_match($exept9, $token)) { + $token = $token . "ΕΤ"; + } + } + + // Step 5d + $re = '/^(.+?)(ΟΝΤΑΣ|ΩΝΤΑΣ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept10 = '/^(ΑΡΧ)$/'; + $exept11 = '/(ΚΡΕ)$/'; + + if (preg_match($exept10, $token)) { + $token = $token . "ΟΝΤ"; + } + + if (preg_match($exept11, $token)) { + $token = $token . "ΩΝΤ"; + } + } + + // Step 5e + $re = '/^(.+?)(ΟΜΑΣΤΕ|ΙΟΜΑΣΤΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept11 = '/^(ΟΝ)$/'; + + if (preg_match($exept11, $token)) { + $token = $token . "ΟΜΑΣΤ"; + } + } + + // Step 5f + $re = '/^(.+?)(ΕΣΤΕ)$/'; + $re2 = '/^(.+?)(ΙΕΣΤΕ)$/'; + + if (preg_match($re2, $token)) { + preg_match($re2, $token, $match); + $token = $match[1]; + $test1 = false; + $re2 = '/^(Π|ΑΠ|ΣΥΜΠ|ΑΣΥΜΠ|ΑΚΑΤΑΠ|ΑΜΕΤΑΜΦ)$/'; + + if (preg_match($re2, $token)) { + $token = $token . "ΙΕΣΤ"; + } + } + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept12 = '/^(ΑΛ|ΑΡ|ΕΚΤΕΛ|Ζ|Μ|Ξ|ΠΑΡΑΚΑΛ|ΑΡ|ΠΡΟ|ΝΙΣ)$/'; + + if (preg_match($exept12, $token)) { + $token = $token . "ΕΣΤ"; + } + } + + // Step 5g + $re = '/^(.+?)(ΗΚΑ|ΗΚΕΣ|ΗΚΕ)$/'; + $re2 = '/^(.+?)(ΗΘΗΚΑ|ΗΘΗΚΕΣ|ΗΘΗΚΕ)$/'; + + if (preg_match($re2, $token)) { + preg_match($re2, $token, $match); + $token = $match[1]; + $test1 = false; + } + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept13 = '/(ΣΚΩΛ|ΣΚΟΥΛ|ΝΑΡΘ|ΣΦ|ΟΘ|ΠΙΘ)$/'; + $exept14 = '/^(ΔΙΑΘ|Θ|ΠΑΡΑΚΑΤΑΘ|ΠΡΟΣΘ|ΣΥΝΘ|)$/'; + + if (preg_match($exept13, $token) || preg_match($exept14, $token)) { + $token = $token . "ΗΚ"; + } + } + + // Step 5h + $re = '/^(.+?)(ΟΥΣΑ|ΟΥΣΕΣ|ΟΥΣΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept15 = '/^(ΦΑΡΜΑΚ|ΧΑΔ|ΑΓΚ|ΑΝΑΡΡ|ΒΡΟΜ|ΕΚΛΙΠ|ΛΑΜΠΙΔ|ΛΕΧ|Μ|ΠΑΤ|Ρ|Λ|ΜΕΔ|ΜΕΣΑΖ|ΥΠΟΤΕΙΝ|ΑΜ|ΑΙΘ|ΑΝΗΚ|ΔΕΣΠΟΖ|ΕΝΔΙΑΦΕΡ|ΔΕ|ΔΕΥΤΕΡΕΥ|ΚΑΘΑΡΕΥ|ΠΛΕ|ΤΣΑ)$/'; + $exept16 = '/(ΠΟΔΑΡ|ΒΛΕΠ|ΠΑΝΤΑΧ|ΦΡΥΔ|ΜΑΝΤΙΛ|ΜΑΛΛ|ΚΥΜΑΤ|ΛΑΧ|ΛΗΓ|ΦΑΓ|ΟΜ|ΠΡΩΤ)$/'; + + if (preg_match($exept15, $token) || preg_match($exept16, $token)) { + $token = $token . "ΟΥΣ"; + } + } + + // Step 5i + $re = '/^(.+?)(ΑΓΑ|ΑΓΕΣ|ΑΓΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept17 = '/^(ΨΟΦ|ΝΑΥΛΟΧ)$/'; + $exept20 = '/(ΚΟΛΛ)$/'; + $exept18 = '/^(ΑΒΑΣΤ|ΠΟΛΥΦ|ΑΔΗΦ|ΠΑΜΦ|Ρ|ΑΣΠ|ΑΦ|ΑΜΑΛ|ΑΜΑΛΛΙ|ΑΝΥΣΤ|ΑΠΕΡ|ΑΣΠΑΡ|ΑΧΑΡ|ΔΕΡΒΕΝ|ΔΡΟΣΟΠ|ΞΕΦ|ΝΕΟΠ|ΝΟΜΟΤ|ΟΛΟΠ|ΟΜΟΤ|ΠΡΟΣΤ|ΠΡΟΣΩΠΟΠ|' + . 'ΣΥΜΠ|ΣΥΝΤ|Τ|ΥΠΟΤ|ΧΑΡ|ΑΕΙΠ|ΑΙΜΟΣΤ|ΑΝΥΠ|ΑΠΟΤ|ΑΡΤΙΠ|ΔΙΑΤ|ΕΝ|ΕΠΙΤ|ΚΡΟΚΑΛΟΠ|ΣΙΔΗΡΟΠ|Λ|ΝΑΥ|ΟΥΛΑΜ|ΟΥΡ|Π|ΤΡ|Μ)$/'; + $exept19 = '/(ΟΦ|ΠΕΛ|ΧΟΡΤ|ΛΛ|ΣΦ|ΡΠ|ΦΡ|ΠΡ|ΛΟΧ|ΣΜΗΝ)$/'; + + if ( + (preg_match($exept18, $token) || preg_match($exept19, $token)) + && !(preg_match($exept17, $token) || preg_match($exept20, $token)) + ) { + $token = $token . "ΑΓ"; + } + } + + // Step 5j + $re = '/^(.+?)(ΗΣΕ|ΗΣΟΥ|ΗΣΑ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept21 = '/^(Ν|ΧΕΡΣΟΝ|ΔΩΔΕΚΑΝ|ΕΡΗΜΟΝ|ΜΕΓΑΛΟΝ|ΕΠΤΑΝ)$/'; + + if (preg_match($exept21, $token)) { + $token = $token . "ΗΣ"; + } + } + + // Step 5k + $re = '/^(.+?)(ΗΣΤΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept22 = '/^(ΑΣΒ|ΣΒ|ΑΧΡ|ΧΡ|ΑΠΛ|ΑΕΙΜΝ|ΔΥΣΧΡ|ΕΥΧΡ|ΚΟΙΝΟΧΡ|ΠΑΛΙΜΨ)$/'; + + if (preg_match($exept22, $token)) { + $token = $token . "ΗΣΤ"; + } + } + + // Step 5l + $re = '/^(.+?)(ΟΥΝΕ|ΗΣΟΥΝΕ|ΗΘΟΥΝΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept23 = '/^(Ν|Ρ|ΣΠΙ|ΣΤΡΑΒΟΜΟΥΤΣ|ΚΑΚΟΜΟΥΤΣ|ΕΞΩΝ)$/'; + + if (preg_match($exept23, $token)) { + $token = $token . "ΟΥΝ"; + } + } + + // Step 5m + $re = '/^(.+?)(ΟΥΜΕ|ΗΣΟΥΜΕ|ΗΘΟΥΜΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept24 = '/^(ΠΑΡΑΣΟΥΣ|Φ|Χ|ΩΡΙΟΠΛ|ΑΖ|ΑΛΛΟΣΟΥΣ|ΑΣΟΥΣ)$/'; + + if (preg_match($exept24, $token)) { + $token = $token . "ΟΥΜ"; + } + } + + // Step 6 + $re = '/^(.+?)(ΜΑΤΑ|ΜΑΤΩΝ|ΜΑΤΟΣ)$/'; + $re2 = '/^(.+?)(Α|ΑΓΑΤΕ|ΑΓΑΝ|ΑΕΙ|ΑΜΑΙ|ΑΝ|ΑΣ|ΑΣΑΙ|ΑΤΑΙ|ΑΩ|Ε|ΕΙ|ΕΙΣ|ΕΙΤΕ|ΕΣΑΙ|ΕΣ|ΕΤΑΙ|Ι|ΙΕΜΑΙ|ΙΕΜΑΣΤΕ|ΙΕΤΑΙ|ΙΕΣΑΙ|ΙΕΣΑΣΤΕ|ΙΟΜΑΣΤΑΝ|ΙΟΜΟΥΝ|' + . 'ΙΟΜΟΥΝΑ|ΙΟΝΤΑΝ|ΙΟΝΤΟΥΣΑΝ|ΙΟΣΑΣΤΑΝ|ΙΟΣΑΣΤΕ|ΙΟΣΟΥΝ|ΙΟΣΟΥΝΑ|ΙΟΤΑΝ|ΙΟΥΜΑ|ΙΟΥΜΑΣΤΕ|ΙΟΥΝΤΑΙ|ΙΟΥΝΤΑΝ|Η|ΗΔΕΣ|ΗΔΩΝ|ΗΘΕΙ|ΗΘΕΙΣ|ΗΘΕΙΤΕ|' + . 'ΗΘΗΚΑΤΕ|ΗΘΗΚΑΝ|ΗΘΟΥΝ|ΗΘΩ|ΗΚΑΤΕ|ΗΚΑΝ|ΗΣ|ΗΣΑΝ|ΗΣΑΤΕ|ΗΣΕΙ|ΗΣΕΣ|ΗΣΟΥΝ|ΗΣΩ|Ο|ΟΙ|ΟΜΑΙ|ΟΜΑΣΤΑΝ|ΟΜΟΥΝ|ΟΜΟΥΝΑ|ΟΝΤΑΙ|ΟΝΤΑΝ|ΟΝΤΟΥΣΑΝ|ΟΣ|' + . 'ΟΣΑΣΤΑΝ|ΟΣΑΣΤΕ|ΟΣΟΥΝ|ΟΣΟΥΝΑ|ΟΤΑΝ|ΟΥ|ΟΥΜΑΙ|ΟΥΜΑΣΤΕ|ΟΥΝ|ΟΥΝΤΑΙ|ΟΥΝΤΑΝ|ΟΥΣ|ΟΥΣΑΝ|ΟΥΣΑΤΕ|Υ|ΥΣ|Ω|ΩΝ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1] . "ΜΑ"; + } + + if (preg_match($re2, $token) && $test1) { + preg_match($re2, $token, $match); + $token = $match[1]; + } + + // Step 7 (ΠΑΡΑΘΕΤΙΚΑ) + $re = '/^(.+?)(ΕΣΤΕΡ|ΕΣΤΑΤ|ΟΤΕΡ|ΟΤΑΤ|ΥΤΕΡ|ΥΤΑΤ|ΩΤΕΡ|ΩΤΑΤ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + } + + return $this->toLowerCase($token, $wCase); + } + + /** + * Converts the token to uppercase, suppressing accents and diaeresis. The array $wCase contains a special map of + * the uppercase rule used to convert each character at each position. + * + * @param string $token Token to process + * @param array &$wCase Map of uppercase rules + * + * @return string + * + * @since 4.0.0 + */ + protected function toUpperCase($token, &$wCase) + { + $wCase = array_fill(0, mb_strlen($token, 'UTF-8'), 0); + $caseConvert = array( + "α" => 'Α', + "β" => 'Β', + "γ" => 'Γ', + "δ" => 'Δ', + "ε" => 'Ε', + "ζ" => 'Ζ', + "η" => 'Η', + "θ" => 'Θ', + "ι" => 'Ι', + "κ" => 'Κ', + "λ" => 'Λ', + "μ" => 'Μ', + "ν" => 'Ν', + "ξ" => 'Ξ', + "ο" => 'Ο', + "π" => 'Π', + "ρ" => 'Ρ', + "σ" => 'Σ', + "τ" => 'Τ', + "υ" => 'Υ', + "φ" => 'Φ', + "χ" => 'Χ', + "ψ" => 'Ψ', + "ω" => 'Ω', + "ά" => 'Α', + "έ" => 'Ε', + "ή" => 'Η', + "ί" => 'Ι', + "ό" => 'Ο', + "ύ" => 'Υ', + "ώ" => 'Ω', + "ς" => 'Σ', + "ϊ" => 'Ι', + "ϋ" => 'Ι', + "ΐ" => 'Ι', + "ΰ" => 'Υ', + ); + $newToken = ''; + + for ($i = 0; $i < mb_strlen($token); $i++) { + $char = mb_substr($token, $i, 1); + $isLower = array_key_exists($char, $caseConvert); + + if (!$isLower) { + $newToken .= $char; + + continue; + } + + $upperCase = $caseConvert[$char]; + $newToken .= $upperCase; + + $wCase[$i] = 1; + + if (in_array($char, ['ά', 'έ', 'ή', 'ί', 'ό', 'ύ', 'ώ', 'ς'])) { + $wCase[$i] = 2; + } + + if (in_array($char, ['ϊ', 'ϋ'])) { + $wCase[$i] = 3; + } + + if (in_array($char, ['ΐ', 'ΰ'])) { + $wCase[$i] = 4; + } + } + + return $newToken; + } + + /** + * Converts the suppressed uppercase token back to lowercase, using the $wCase map to add back the accents, + * diaeresis and handle the special case of final sigma (different lowercase glyph than the regular sigma, only + * used at the end of words). + * + * @param string $token Token to process + * @param array $wCase Map of lowercase rules + * + * @return string + * + * @since 4.0.0 + */ + protected function toLowerCase($token, $wCase) + { + $newToken = ''; + + for ($i = 0; $i < mb_strlen($token); $i++) { + $char = mb_substr($token, $i, 1); + + // Is $wCase not set at this position? We assume no case conversion ever took place. + if (!isset($wCase[$i])) { + $newToken .= $char; + + continue; + } + + // The character was not case-converted + if ($wCase[$i] == 0) { + $newToken .= $char; + + continue; + } + + // Case 1: Unaccented letter + if ($wCase[$i] == 1) { + $newToken .= mb_strtolower($char); + + continue; + } + + // Case 2: Vowel with accent (tonos); or the special case of final sigma + if ($wCase[$i] == 2) { + $charMap = [ + 'Α' => 'ά', + 'Ε' => 'έ', + 'Η' => 'ή', + 'Ι' => 'ί', + 'Ο' => 'ό', + 'Υ' => 'ύ', + 'Ω' => 'ώ', + 'Σ' => 'ς' + ]; + + $newToken .= $charMap[$char]; + + continue; + } + + // Case 3: vowels with diaeresis (dialytika) + if ($wCase[$i] == 3) { + $charMap = [ + 'Ι' => 'ϊ', + 'Υ' => 'ϋ' + ]; + + $newToken .= $charMap[$char]; + + continue; + } + + // Case 4: vowels with both diaeresis (dialytika) and accent (tonos) + if ($wCase[$i] == 4) { + $charMap = [ + 'Ι' => 'ΐ', + 'Υ' => 'ΰ' + ]; + + $newToken .= $charMap[$char]; + + continue; + } + + // This should never happen! + $newToken .= $char; + } + + return $newToken; + } } diff --git a/administrator/components/com_finder/src/Indexer/Language/Zh.php b/administrator/components/com_finder/src/Indexer/Language/Zh.php index 7e5bcbfa00551..1804fcc3b1f30 100644 --- a/administrator/components/com_finder/src/Indexer/Language/Zh.php +++ b/administrator/components/com_finder/src/Indexer/Language/Zh.php @@ -1,4 +1,5 @@ clean($format, 'cmd'); - - // Only create one parser for each format. - if (isset(self::$instances[$format])) - { - return self::$instances[$format]; - } - - // Setup the adapter for the parser. - $class = '\\Joomla\\Component\\Finder\\Administrator\\Indexer\\Parser\\' . ucfirst($format); - - // Check if a parser exists for the format. - if (class_exists($class)) - { - self::$instances[$format] = new $class; - - return self::$instances[$format]; - } - - // Throw invalid format exception. - throw new \Exception(Text::sprintf('COM_FINDER_INDEXER_INVALID_PARSER', $format)); - } - - /** - * Method to parse input and extract the plain text. Because this method is - * called from both inside and outside the indexer, it needs to be able to - * batch out its parsing functionality to deal with the inefficiencies of - * regular expressions. We will parse recursively in 2KB chunks. - * - * @param string $input The input to parse. - * - * @return string The plain text input. - * - * @since 2.5 - */ - public function parse($input) - { - // If the input is less than 2KB we can parse it in one go. - if (strlen($input) <= 2048) - { - return $this->process($input); - } - - // Input is longer than 2Kb so parse it in chunks of 2Kb or less. - $start = 0; - $end = strlen($input); - $chunk = 2048; - $return = null; - - while ($start < $end) - { - // Setup the string. - $string = substr($input, $start, $chunk); - - // Find the last space character if we aren't at the end. - $ls = (($start + $chunk) < $end ? strrpos($string, ' ') : false); - - // Truncate to the last space character. - if ($ls !== false) - { - $string = substr($string, 0, $ls); - } - - // Adjust the start position for the next iteration. - $start += ($ls !== false ? ($ls + 1 - $chunk) + $chunk : $chunk); - - // Parse the chunk. - $return .= $this->process($string); - } - - return $return; - } - - /** - * Method to process input and extract the plain text. - * - * @param string $input The input to process. - * - * @return string The plain text input. - * - * @since 2.5 - */ - abstract protected function process($input); + /** + * Parser support instances container. + * + * @var Parser[] + * @since 4.0.0 + */ + protected static $instances = array(); + + /** + * Method to get a parser, creating it if necessary. + * + * @param string $format The type of parser to load. + * + * @return Parser A Parser instance. + * + * @since 2.5 + * @throws \Exception on invalid parser. + */ + public static function getInstance($format) + { + $format = InputFilter::getInstance()->clean($format, 'cmd'); + + // Only create one parser for each format. + if (isset(self::$instances[$format])) { + return self::$instances[$format]; + } + + // Setup the adapter for the parser. + $class = '\\Joomla\\Component\\Finder\\Administrator\\Indexer\\Parser\\' . ucfirst($format); + + // Check if a parser exists for the format. + if (class_exists($class)) { + self::$instances[$format] = new $class(); + + return self::$instances[$format]; + } + + // Throw invalid format exception. + throw new \Exception(Text::sprintf('COM_FINDER_INDEXER_INVALID_PARSER', $format)); + } + + /** + * Method to parse input and extract the plain text. Because this method is + * called from both inside and outside the indexer, it needs to be able to + * batch out its parsing functionality to deal with the inefficiencies of + * regular expressions. We will parse recursively in 2KB chunks. + * + * @param string $input The input to parse. + * + * @return string The plain text input. + * + * @since 2.5 + */ + public function parse($input) + { + // If the input is less than 2KB we can parse it in one go. + if (strlen($input) <= 2048) { + return $this->process($input); + } + + // Input is longer than 2Kb so parse it in chunks of 2Kb or less. + $start = 0; + $end = strlen($input); + $chunk = 2048; + $return = null; + + while ($start < $end) { + // Setup the string. + $string = substr($input, $start, $chunk); + + // Find the last space character if we aren't at the end. + $ls = (($start + $chunk) < $end ? strrpos($string, ' ') : false); + + // Truncate to the last space character. + if ($ls !== false) { + $string = substr($string, 0, $ls); + } + + // Adjust the start position for the next iteration. + $start += ($ls !== false ? ($ls + 1 - $chunk) + $chunk : $chunk); + + // Parse the chunk. + $return .= $this->process($string); + } + + return $return; + } + + /** + * Method to process input and extract the plain text. + * + * @param string $input The input to process. + * + * @return string The plain text input. + * + * @since 2.5 + */ + abstract protected function process($input); } diff --git a/administrator/components/com_finder/src/Indexer/Parser/Html.php b/administrator/components/com_finder/src/Indexer/Parser/Html.php index 2c59281085828..7183058e9c55f 100644 --- a/administrator/components/com_finder/src/Indexer/Parser/Html.php +++ b/administrator/components/com_finder/src/Indexer/Parser/Html.php @@ -1,4 +1,5 @@ and tags. Do this first - // because there might be '); - - // Decode HTML entities. - $input = html_entity_decode($input, ENT_QUOTES, 'UTF-8'); - - // Convert entities equivalent to spaces to actual spaces. - $input = str_replace(array(' ', ' '), ' ', $input); - - // Add a space before both the OPEN and CLOSE tags of BLOCK and LINE BREAKING elements, - // e.g. 'all

    mobile List

    ' will become 'all mobile List' - $input = preg_replace('/(<|<\/)(' . - 'address|article|aside|blockquote|br|canvas|dd|div|dl|dt|' . - 'fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|li|' . - 'main|nav|noscript|ol|output|p|pre|section|table|tfoot|ul|video' . - ')\b/i', ' $1$2', $input - ); - - // Strip HTML tags. - $input = strip_tags($input); - - return parent::parse($input); - } - - /** - * Method to process HTML input and extract the plain text. - * - * @param string $input The input to process. - * - * @return string The plain text input. - * - * @since 2.5 - */ - protected function process($input) - { - // Replace any amount of white space with a single space. - return preg_replace('#\s+#u', ' ', $input); - } - - /** - * Method to remove blocks of text between a start and an end tag. - * Each block removed is effectively replaced by a single space. - * - * Note: The start tag and the end tag must be different. - * Note: Blocks must not be nested. - * Note: This method will function correctly with multi-byte strings. - * - * @param string $input String to be processed. - * @param string $startTag String representing the start tag. - * @param string $endTag String representing the end tag. - * - * @return string with blocks removed. - * - * @since 3.4 - */ - private function removeBlocks($input, $startTag, $endTag) - { - $return = ''; - $offset = 0; - $startTagLength = strlen($startTag); - $endTagLength = strlen($endTag); - - // Find the first start tag. - $start = stripos($input, $startTag); - - // If no start tags were found, return the string unchanged. - if ($start === false) - { - return $input; - } - - // Look for all blocks defined by the start and end tags. - while ($start !== false) - { - // Accumulate the substring up to the start tag. - $return .= substr($input, $offset, $start - $offset) . ' '; - - // Look for an end tag corresponding to the start tag. - $end = stripos($input, $endTag, $start + $startTagLength); - - // If no corresponding end tag, leave the string alone. - if ($end === false) - { - // Fix the offset so part of the string is not duplicated. - $offset = $start; - break; - } - - // Advance the start position. - $offset = $end + $endTagLength; - - // Look for the next start tag and loop. - $start = stripos($input, $startTag, $offset); - } - - // Add in the final substring after the last end tag. - $return .= substr($input, $offset); - - return $return; - } + /** + * Method to parse input and extract the plain text. Because this method is + * called from both inside and outside the indexer, it needs to be able to + * batch out its parsing functionality to deal with the inefficiencies of + * regular expressions. We will parse recursively in 2KB chunks. + * + * @param string $input The input to parse. + * + * @return string The plain text input. + * + * @since 2.5 + */ + public function parse($input) + { + // Strip invalid UTF-8 characters. + $oldSetting = ini_get('mbstring.substitute_character'); + ini_set('mbstring.substitute_character', 'none'); + $input = mb_convert_encoding($input, 'UTF-8', 'UTF-8'); + ini_set('mbstring.substitute_character', $oldSetting); + + // Remove anything between and tags. Do this first + // because there might be '); + + // Decode HTML entities. + $input = html_entity_decode($input, ENT_QUOTES, 'UTF-8'); + + // Convert entities equivalent to spaces to actual spaces. + $input = str_replace(array(' ', ' '), ' ', $input); + + // Add a space before both the OPEN and CLOSE tags of BLOCK and LINE BREAKING elements, + // e.g. 'all

    mobile List

    ' will become 'all mobile List' + $input = preg_replace('/(<|<\/)(' . + 'address|article|aside|blockquote|br|canvas|dd|div|dl|dt|' . + 'fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|li|' . + 'main|nav|noscript|ol|output|p|pre|section|table|tfoot|ul|video' . + ')\b/i', ' $1$2', $input); + + // Strip HTML tags. + $input = strip_tags($input); + + return parent::parse($input); + } + + /** + * Method to process HTML input and extract the plain text. + * + * @param string $input The input to process. + * + * @return string The plain text input. + * + * @since 2.5 + */ + protected function process($input) + { + // Replace any amount of white space with a single space. + return preg_replace('#\s+#u', ' ', $input); + } + + /** + * Method to remove blocks of text between a start and an end tag. + * Each block removed is effectively replaced by a single space. + * + * Note: The start tag and the end tag must be different. + * Note: Blocks must not be nested. + * Note: This method will function correctly with multi-byte strings. + * + * @param string $input String to be processed. + * @param string $startTag String representing the start tag. + * @param string $endTag String representing the end tag. + * + * @return string with blocks removed. + * + * @since 3.4 + */ + private function removeBlocks($input, $startTag, $endTag) + { + $return = ''; + $offset = 0; + $startTagLength = strlen($startTag); + $endTagLength = strlen($endTag); + + // Find the first start tag. + $start = stripos($input, $startTag); + + // If no start tags were found, return the string unchanged. + if ($start === false) { + return $input; + } + + // Look for all blocks defined by the start and end tags. + while ($start !== false) { + // Accumulate the substring up to the start tag. + $return .= substr($input, $offset, $start - $offset) . ' '; + + // Look for an end tag corresponding to the start tag. + $end = stripos($input, $endTag, $start + $startTagLength); + + // If no corresponding end tag, leave the string alone. + if ($end === false) { + // Fix the offset so part of the string is not duplicated. + $offset = $start; + break; + } + + // Advance the start position. + $offset = $end + $endTagLength; + + // Look for the next start tag and loop. + $start = stripos($input, $startTag, $offset); + } + + // Add in the final substring after the last end tag. + $return .= substr($input, $offset); + + return $return; + } } diff --git a/administrator/components/com_finder/src/Indexer/Parser/Rtf.php b/administrator/components/com_finder/src/Indexer/Parser/Rtf.php index 3006ad8194159..60a4ecb9c8c14 100644 --- a/administrator/components/com_finder/src/Indexer/Parser/Rtf.php +++ b/administrator/components/com_finder/src/Indexer/Parser/Rtf.php @@ -1,4 +1,5 @@ get(DatabaseInterface::class); - } - - $this->setDatabase($db); - - // Get the input string. - $this->input = $options['input'] ?? ''; - - // Get the empty query setting. - $this->empty = isset($options['empty']) ? (bool) $options['empty'] : false; - - // Get the input language. - $this->language = !empty($options['language']) ? $options['language'] : Helper::getDefaultLanguage(); - - // Get the matching mode. - $this->mode = 'AND'; - - // Set the word matching mode - $this->wordmode = !empty($options['word_match']) ? $options['word_match'] : 'exact'; - - // Initialize the temporary date storage. - $this->dates = new Registry; - - // Populate the temporary date storage. - if (!empty($options['date1'])) - { - $this->dates->set('date1', $options['date1']); - } - - if (!empty($options['date2'])) - { - $this->dates->set('date2', $options['date2']); - } - - if (!empty($options['when1'])) - { - $this->dates->set('when1', $options['when1']); - } - - if (!empty($options['when2'])) - { - $this->dates->set('when2', $options['when2']); - } - - // Process the static taxonomy filters. - if (!empty($options['filter'])) - { - $this->processStaticTaxonomy($options['filter']); - } - - // Process the dynamic taxonomy filters. - if (!empty($options['filters'])) - { - $this->processDynamicTaxonomy($options['filters']); - } - - // Get the date filters. - $d1 = $this->dates->get('date1'); - $d2 = $this->dates->get('date2'); - $w1 = $this->dates->get('when1'); - $w2 = $this->dates->get('when2'); - - // Process the date filters. - if (!empty($d1) || !empty($d2)) - { - $this->processDates($d1, $d2, $w1, $w2); - } - - // Process the input string. - $this->processString($this->input, $this->language, $this->mode); - - // Get the number of matching terms. - foreach ($this->included as $token) - { - $this->terms += count($token->matches); - } - - // Remove the temporary date storage. - unset($this->dates); - - // Lastly, determine whether this query can return a result set. - - // Check if we have a query string. - if (!empty($this->input)) - { - $this->search = true; - } - // Check if we can search without a query string. - elseif ($this->empty && (!empty($this->filter) || !empty($this->filters) || !empty($this->date1) || !empty($this->date2))) - { - $this->search = true; - } - // We do not have a valid search query. - else - { - $this->search = false; - } - } - - /** - * Method to convert the query object into a URI string. - * - * @param string $base The base URI. [optional] - * - * @return string The complete query URI. - * - * @since 2.5 - */ - public function toUri($base = '') - { - // Set the base if not specified. - if ($base === '') - { - $base = 'index.php?option=com_finder&view=search'; - } - - // Get the base URI. - $uri = Uri::getInstance($base); - - // Add the static taxonomy filter if present. - if ((bool) $this->filter) - { - $uri->setVar('f', $this->filter); - } - - // Get the filters in the request. - $t = Factory::getApplication()->input->request->get('t', array(), 'array'); - - // Add the dynamic taxonomy filters if present. - if ((bool) $this->filters) - { - foreach ($this->filters as $nodes) - { - foreach ($nodes as $node) - { - if (!in_array($node, $t)) - { - continue; - } - - $uri->setVar('t[]', $node); - } - } - } - - // Add the input string if present. - if (!empty($this->input)) - { - $uri->setVar('q', $this->input); - } - - // Add the start date if present. - if (!empty($this->date1)) - { - $uri->setVar('d1', $this->date1); - } - - // Add the end date if present. - if (!empty($this->date2)) - { - $uri->setVar('d2', $this->date2); - } - - // Add the start date modifier if present. - if (!empty($this->when1)) - { - $uri->setVar('w1', $this->when1); - } - - // Add the end date modifier if present. - if (!empty($this->when2)) - { - $uri->setVar('w2', $this->when2); - } - - // Add a menu item id if one is not present. - if (!$uri->getVar('Itemid')) - { - // Get the menu item id. - $query = array( - 'view' => $uri->getVar('view'), - 'f' => $uri->getVar('f'), - 'q' => $uri->getVar('q'), - ); - - $item = RouteHelper::getItemid($query); - - // Add the menu item id if present. - if ($item !== null) - { - $uri->setVar('Itemid', $item); - } - } - - return $uri->toString(array('path', 'query')); - } - - /** - * Method to get a list of excluded search term ids. - * - * @return array An array of excluded term ids. - * - * @since 2.5 - */ - public function getExcludedTermIds() - { - $results = array(); - - // Iterate through the excluded tokens and compile the matching terms. - for ($i = 0, $c = count($this->excluded); $i < $c; $i++) - { - foreach ($this->excluded[$i]->matches as $match) - { - $results = array_merge($results, $match); - } - } - - // Sanitize the terms. - $results = array_unique($results); - - return ArrayHelper::toInteger($results); - } - - /** - * Method to get a list of included search term ids. - * - * @return array An array of included term ids. - * - * @since 2.5 - */ - public function getIncludedTermIds() - { - $results = array(); - - // Iterate through the included tokens and compile the matching terms. - for ($i = 0, $c = count($this->included); $i < $c; $i++) - { - // Check if we have any terms. - if (empty($this->included[$i]->matches)) - { - continue; - } - - // Get the term. - $term = $this->included[$i]->term; - - // Prepare the container for the term if necessary. - if (!array_key_exists($term, $results)) - { - $results[$term] = array(); - } - - // Add the matches to the stack. - foreach ($this->included[$i]->matches as $match) - { - $results[$term] = array_merge($results[$term], $match); - } - } - - // Sanitize the terms. - foreach ($results as $key => $value) - { - $results[$key] = array_unique($results[$key]); - $results[$key] = ArrayHelper::toInteger($results[$key]); - } - - return $results; - } - - /** - * Method to get a list of required search term ids. - * - * @return array An array of required term ids. - * - * @since 2.5 - */ - public function getRequiredTermIds() - { - $results = array(); - - // Iterate through the included tokens and compile the matching terms. - for ($i = 0, $c = count($this->included); $i < $c; $i++) - { - // Check if the token is required. - if ($this->included[$i]->required) - { - // Get the term. - $term = $this->included[$i]->term; - - // Prepare the container for the term if necessary. - if (!array_key_exists($term, $results)) - { - $results[$term] = array(); - } - - // Add the matches to the stack. - foreach ($this->included[$i]->matches as $match) - { - $results[$term] = array_merge($results[$term], $match); - } - } - } - - // Sanitize the terms. - foreach ($results as $key => $value) - { - $results[$key] = array_unique($results[$key]); - $results[$key] = ArrayHelper::toInteger($results[$key]); - } - - return $results; - } - - /** - * Method to process the static taxonomy input. The static taxonomy input - * comes in the form of a pre-defined search filter that is assigned to the - * search form. - * - * @param integer $filterId The id of static filter. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function processStaticTaxonomy($filterId) - { - // Get the database object. - $db = $this->getDatabase(); - - // Initialize user variables - $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); - - // Load the predefined filter. - $query = $db->getQuery(true) - ->select('f.data, f.params') - ->from($db->quoteName('#__finder_filters') . ' AS f') - ->where('f.filter_id = ' . (int) $filterId); - - $db->setQuery($query); - $return = $db->loadObject(); - - // Check the returned filter. - if (empty($return)) - { - return false; - } - - // Set the filter. - $this->filter = (int) $filterId; - - // Get a parameter object for the filter date options. - $registry = new Registry($return->params); - $params = $registry; - - // Set the dates if not already set. - $this->dates->def('d1', $params->get('d1')); - $this->dates->def('d2', $params->get('d2')); - $this->dates->def('w1', $params->get('w1')); - $this->dates->def('w2', $params->get('w2')); - - // Remove duplicates and sanitize. - $filters = explode(',', $return->data); - $filters = array_unique($filters); - $filters = ArrayHelper::toInteger($filters); - - // Remove any values of zero. - if (in_array(0, $filters, true) !== false) - { - unset($filters[array_search(0, $filters, true)]); - } - - // Check if we have any real input. - if (empty($filters)) - { - return true; - } - - /* - * Create the query to get filters from the database. We do this for - * two reasons: one, it allows us to ensure that the filters being used - * are real; two, we need to sort the filters by taxonomy branch. - */ - $query->clear() - ->select('t1.id, t1.title, t2.title AS branch') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') - ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') - ->where('t1.state = 1') - ->where('t1.access IN (' . $groups . ')') - ->where('t1.id IN (' . implode(',', $filters) . ')') - ->where('t2.state = 1') - ->where('t2.access IN (' . $groups . ')'); - - // Load the filters. - $db->setQuery($query); - $results = $db->loadObjectList(); - - // Sort the filter ids by branch. - foreach ($results as $result) - { - $this->filters[$result->branch][$result->title] = (int) $result->id; - } - - return true; - } - - /** - * Method to process the dynamic taxonomy input. The dynamic taxonomy input - * comes in the form of select fields that the user chooses from. The - * dynamic taxonomy input is processed AFTER the static taxonomy input - * because the dynamic options can be used to further narrow a static - * taxonomy filter. - * - * @param array $filters An array of taxonomy node ids. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function processDynamicTaxonomy($filters) - { - // Initialize user variables - $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); - - // Remove duplicates and sanitize. - $filters = array_unique($filters); - $filters = ArrayHelper::toInteger($filters); - - // Remove any values of zero. - if (in_array(0, $filters, true) !== false) - { - unset($filters[array_search(0, $filters, true)]); - } - - // Check if we have any real input. - if (empty($filters)) - { - return true; - } - - // Get the database object. - $db = $this->getDatabase(); - - $query = $db->getQuery(true); - - /* - * Create the query to get filters from the database. We do this for - * two reasons: one, it allows us to ensure that the filters being used - * are real; two, we need to sort the filters by taxonomy branch. - */ - $query->select('t1.id, t1.title, t2.title AS branch') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') - ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') - ->where('t1.state = 1') - ->where('t1.access IN (' . $groups . ')') - ->where('t1.id IN (' . implode(',', $filters) . ')') - ->where('t2.state = 1') - ->where('t2.access IN (' . $groups . ')'); - - // Load the filters. - $db->setQuery($query); - $results = $db->loadObjectList(); - - // Cleared filter branches. - $cleared = array(); - - /* - * Sort the filter ids by branch. Because these filters are designed to - * override and further narrow the items selected in the static filter, - * we will clear the values from the static filter on a branch by - * branch basis before adding the dynamic filters. So, if the static - * filter defines a type filter of "articles" and three "category" - * filters but the user only limits the category further, the category - * filters will be flushed but the type filters will not. - */ - foreach ($results as $result) - { - // Check if the branch has been cleared. - if (!in_array($result->branch, $cleared, true)) - { - // Clear the branch. - $this->filters[$result->branch] = array(); - - // Add the branch to the cleared list. - $cleared[] = $result->branch; - } - - // Add the filter to the list. - $this->filters[$result->branch][$result->title] = (int) $result->id; - } - - return true; - } - - /** - * Method to process the query date filters to determine start and end - * date limitations. - * - * @param string $date1 The first date filter. - * @param string $date2 The second date filter. - * @param string $when1 The first date modifier. - * @param string $when2 The second date modifier. - * - * @return boolean True on success. - * - * @since 2.5 - */ - protected function processDates($date1, $date2, $when1, $when2) - { - // Clean up the inputs. - $date1 = trim(StringHelper::strtolower($date1)); - $date2 = trim(StringHelper::strtolower($date2)); - $when1 = trim(StringHelper::strtolower($when1)); - $when2 = trim(StringHelper::strtolower($when2)); - - // Get the time offset. - $offset = Factory::getApplication()->get('offset'); - - // Array of allowed when values. - $whens = array('before', 'after', 'exact'); - - // The value of 'today' is a special case that we need to handle. - if ($date1 === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) - { - $date1 = Factory::getDate('now', $offset)->format('%Y-%m-%d'); - } - - // Try to parse the date string. - $date = Factory::getDate($date1, $offset); - - // Check if the date was parsed successfully. - if ($date->toUnix() !== null) - { - // Set the date filter. - $this->date1 = $date->toSql(); - $this->when1 = in_array($when1, $whens, true) ? $when1 : 'before'; - } - - // The value of 'today' is a special case that we need to handle. - if ($date2 === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) - { - $date2 = Factory::getDate('now', $offset)->format('%Y-%m-%d'); - } - - // Try to parse the date string. - $date = Factory::getDate($date2, $offset); - - // Check if the date was parsed successfully. - if ($date->toUnix() !== null) - { - // Set the date filter. - $this->date2 = $date->toSql(); - $this->when2 = in_array($when2, $whens, true) ? $when2 : 'before'; - } - - return true; - } - - /** - * Method to process the query input string and extract required, optional, - * and excluded tokens; taxonomy filters; and date filters. - * - * @param string $input The query input string. - * @param string $lang The query input language. - * @param string $mode The query matching mode. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function processString($input, $lang, $mode) - { - if ($input === null) - { - $input = ''; - } - - // Clean up the input string. - $input = html_entity_decode($input, ENT_QUOTES, 'UTF-8'); - $input = StringHelper::strtolower($input); - $input = preg_replace('#\s+#mi', ' ', $input); - $input = trim($input); - $debug = Factory::getApplication()->get('debug_lang'); - $params = ComponentHelper::getParams('com_finder'); - - /* - * First, we need to handle string based modifiers. String based - * modifiers could potentially include things like "category:blah" or - * "before:2009-10-21" or "type:article", etc. - */ - $patterns = array( - 'before' => Text::_('COM_FINDER_FILTER_WHEN_BEFORE'), - 'after' => Text::_('COM_FINDER_FILTER_WHEN_AFTER'), - ); - - // Add the taxonomy branch titles to the possible patterns. - foreach (Taxonomy::getBranchTitles() as $branch) - { - // Add the pattern. - $patterns[$branch] = StringHelper::strtolower(Text::_(LanguageHelper::branchSingular($branch))); - } - - // Container for search terms and phrases. - $terms = array(); - $phrases = array(); - - // Cleared filter branches. - $cleared = array(); - - /* - * Compile the suffix pattern. This is used to match the values of the - * filter input string. Single words can be input directly, multi-word - * values have to be wrapped in double quotes. - */ - $quotes = html_entity_decode('‘’'', ENT_QUOTES, 'UTF-8'); - $suffix = '(([\w\d' . $quotes . '-]+)|\"([\w\d\s' . $quotes . '-]+)\")'; - - /* - * Iterate through the possible filter patterns and search for matches. - * We need to match the key, colon, and a value pattern for the match - * to be valid. - */ - foreach ($patterns as $modifier => $pattern) - { - $matches = array(); - - if ($debug) - { - $pattern = substr($pattern, 2, -2); - } - - // Check if the filter pattern is in the input string. - if (preg_match('#' . $pattern . '\s*:\s*' . $suffix . '#mi', $input, $matches)) - { - // Get the value given to the modifier. - $value = $matches[3] ?? $matches[1]; - - // Now we have to handle the filter string. - switch ($modifier) - { - // Handle a before and after date filters. - case 'before': - case 'after': - // Get the time offset. - $offset = Factory::getApplication()->get('offset'); - - // Array of allowed when values. - $whens = array('before', 'after', 'exact'); - - // The value of 'today' is a special case that we need to handle. - if ($value === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) - { - $value = Factory::getDate('now', $offset)->format('%Y-%m-%d'); - } - - // Try to parse the date string. - $date = Factory::getDate($value, $offset); - - // Check if the date was parsed successfully. - if ($date->toUnix() !== null) - { - // Set the date filter. - $this->date1 = $date->toSql(); - $this->when1 = in_array($modifier, $whens, true) ? $modifier : 'before'; - } - - break; - - // Handle a taxonomy branch filter. - default: - // Try to find the node id. - $return = Taxonomy::getNodeByTitle($modifier, $value); - - // Check if the node id was found. - if ($return) - { - // Check if the branch has been cleared. - if (!in_array($modifier, $cleared, true)) - { - // Clear the branch. - $this->filters[$modifier] = array(); - - // Add the branch to the cleared list. - $cleared[] = $modifier; - } - - // Add the filter to the list. - $this->filters[$modifier][$return->title] = (int) $return->id; - } - - break; - } - - // Clean up the input string again. - $input = str_replace($matches[0], '', $input); - $input = preg_replace('#\s+#mi', ' ', $input); - $input = trim($input); - } - } - - /* - * Extract the tokens enclosed in double quotes so that we can handle - * them as phrases. - */ - if (StringHelper::strpos($input, '"') !== false) - { - $matches = array(); - - // Extract the tokens enclosed in double quotes. - if (preg_match_all('#\"([^"]+)\"#m', $input, $matches)) - { - /* - * One or more phrases were found so we need to iterate through - * them, tokenize them as phrases, and remove them from the raw - * input string before we move on to the next processing step. - */ - foreach ($matches[1] as $key => $match) - { - // Find the complete phrase in the input string. - $pos = StringHelper::strpos($input, $matches[0][$key]); - $len = StringHelper::strlen($matches[0][$key]); - - // Add any terms that are before this phrase to the stack. - if (trim(StringHelper::substr($input, 0, $pos))) - { - $terms = array_merge($terms, explode(' ', trim(StringHelper::substr($input, 0, $pos)))); - } - - // Strip out everything up to and including the phrase. - $input = StringHelper::substr($input, $pos + $len); - - // Clean up the input string again. - $input = preg_replace('#\s+#mi', ' ', $input); - $input = trim($input); - - // Get the number of words in the phrase. - $parts = explode(' ', $match); - $tuplecount = $params->get('tuplecount', 1); - - // Check if the phrase is longer than our $tuplecount. - if (count($parts) > $tuplecount && $tuplecount > 1) - { - $chunk = array_slice($parts, 0, $tuplecount); - $parts = array_slice($parts, $tuplecount); - - // If the chunk is not empty, add it as a phrase. - if (count($chunk)) - { - $phrases[] = implode(' ', $chunk); - $terms[] = implode(' ', $chunk); - } - - /* - * If the phrase is longer than $tuplecount words, we need to - * break it down into smaller chunks of phrases that - * are less than or equal to $tuplecount words. We overlap - * the chunks so that we can ensure that a match is - * found for the complete phrase and not just portions - * of it. - */ - for ($i = 0, $c = count($parts); $i < $c; $i++) - { - array_shift($chunk); - $chunk[] = array_shift($parts); - - // If the chunk is not empty, add it as a phrase. - if (count($chunk)) - { - $phrases[] = implode(' ', $chunk); - $terms[] = implode(' ', $chunk); - } - } - } - else - { - // The phrase is <= $tuplecount words so we can use it as is. - $phrases[] = $match; - $terms[] = $match; - } - } - } - } - - // Add the remaining terms if present. - if ((bool) $input) - { - $terms = array_merge($terms, explode(' ', $input)); - } - - // An array of our boolean operators. $operator => $translation - $operators = array( - 'AND' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_AND')), - 'OR' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_OR')), - 'NOT' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_NOT')), - ); - - // If language debugging is enabled you need to ignore the debug strings in matching. - if (JDEBUG) - { - $debugStrings = array('**', '??'); - $operators = str_replace($debugStrings, '', $operators); - } - - /* - * Iterate through the terms and perform any sorting that needs to be - * done based on boolean search operators. Terms that are before an - * and/or/not modifier have to be handled in relation to their operator. - */ - for ($i = 0, $c = count($terms); $i < $c; $i++) - { - // Check if the term is followed by an operator that we understand. - if (isset($terms[$i + 1]) && in_array($terms[$i + 1], $operators, true)) - { - // Get the operator mode. - $op = array_search($terms[$i + 1], $operators, true); - - // Handle the AND operator. - if ($op === 'AND' && isset($terms[$i + 2])) - { - // Tokenize the current term. - $token = Helper::tokenize($terms[$i], $lang, true); - - // @todo: The previous function call may return an array, which seems not to be handled by the next one, which expects an object - $token = $this->getTokenData(array_shift($token)); - - if ($params->get('filter_commonwords', 0) && $token->common) - { - continue; - } - - if ($params->get('filter_numeric', 0) && $token->numeric) - { - continue; - } - - // Set the required flag. - $token->required = true; - - // Add the current token to the stack. - $this->included[] = $token; - $this->highlight = array_merge($this->highlight, array_keys($token->matches)); - - // Skip the next token (the mode operator). - $this->operators[] = $terms[$i + 1]; - - // Tokenize the term after the next term (current plus two). - $other = Helper::tokenize($terms[$i + 2], $lang, true); - $other = $this->getTokenData(array_shift($other)); - - // Set the required flag. - $other->required = true; - - // Add the token after the next token to the stack. - $this->included[] = $other; - $this->highlight = array_merge($this->highlight, array_keys($other->matches)); - - // Remove the processed phrases if possible. - if (($pk = array_search($terms[$i], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - if (($pk = array_search($terms[$i + 2], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - // Remove the processed terms. - unset($terms[$i], $terms[$i + 1], $terms[$i + 2]); - - // Adjust the loop. - $i += 2; - } - // Handle the OR operator. - elseif ($op === 'OR' && isset($terms[$i + 2])) - { - // Tokenize the current term. - $token = Helper::tokenize($terms[$i], $lang, true); - $token = $this->getTokenData(array_shift($token)); - - if ($params->get('filter_commonwords', 0) && $token->common) - { - continue; - } - - if ($params->get('filter_numeric', 0) && $token->numeric) - { - continue; - } - - // Set the required flag. - $token->required = false; - - // Add the current token to the stack. - if ((bool) $token->matches) - { - $this->included[] = $token; - $this->highlight = array_merge($this->highlight, array_keys($token->matches)); - } - else - { - $this->ignored[] = $token; - } - - // Skip the next token (the mode operator). - $this->operators[] = $terms[$i + 1]; - - // Tokenize the term after the next term (current plus two). - $other = Helper::tokenize($terms[$i + 2], $lang, true); - $other = $this->getTokenData(array_shift($other)); - - // Set the required flag. - $other->required = false; - - // Add the token after the next token to the stack. - if ((bool) $other->matches) - { - $this->included[] = $other; - $this->highlight = array_merge($this->highlight, array_keys($other->matches)); - } - else - { - $this->ignored[] = $other; - } - - // Remove the processed phrases if possible. - if (($pk = array_search($terms[$i], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - if (($pk = array_search($terms[$i + 2], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - // Remove the processed terms. - unset($terms[$i], $terms[$i + 1], $terms[$i + 2]); - - // Adjust the loop. - $i += 2; - } - } - // Handle an orphaned OR operator. - elseif (isset($terms[$i + 1]) && array_search($terms[$i], $operators, true) === 'OR') - { - // Skip the next token (the mode operator). - $this->operators[] = $terms[$i]; - - // Tokenize the next term (current plus one). - $other = Helper::tokenize($terms[$i + 1], $lang, true); - $other = $this->getTokenData(array_shift($other)); - - if ($params->get('filter_commonwords', 0) && $other->common) - { - continue; - } - - if ($params->get('filter_numeric', 0) && $other->numeric) - { - continue; - } - - // Set the required flag. - $other->required = false; - - // Add the token after the next token to the stack. - if ((bool) $other->matches) - { - $this->included[] = $other; - $this->highlight = array_merge($this->highlight, array_keys($other->matches)); - } - else - { - $this->ignored[] = $other; - } - - // Remove the processed phrase if possible. - if (($pk = array_search($terms[$i + 1], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - // Remove the processed terms. - unset($terms[$i], $terms[$i + 1]); - - // Adjust the loop. - $i++; - } - // Handle the NOT operator. - elseif (isset($terms[$i + 1]) && array_search($terms[$i], $operators, true) === 'NOT') - { - // Skip the next token (the mode operator). - $this->operators[] = $terms[$i]; - - // Tokenize the next term (current plus one). - $other = Helper::tokenize($terms[$i + 1], $lang, true); - $other = $this->getTokenData(array_shift($other)); - - if ($params->get('filter_commonwords', 0) && $other->common) - { - continue; - } - - if ($params->get('filter_numeric', 0) && $other->numeric) - { - continue; - } - - // Set the required flag. - $other->required = false; - - // Add the next token to the stack. - if ((bool) $other->matches) - { - $this->excluded[] = $other; - } - else - { - $this->ignored[] = $other; - } - - // Remove the processed phrase if possible. - if (($pk = array_search($terms[$i + 1], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - // Remove the processed terms. - unset($terms[$i], $terms[$i + 1]); - - // Adjust the loop. - $i++; - } - } - - /* - * Iterate through any search phrases and tokenize them. We handle - * phrases as autonomous units and do not break them down into two and - * three word combinations. - */ - for ($i = 0, $c = count($phrases); $i < $c; $i++) - { - // Tokenize the phrase. - $token = Helper::tokenize($phrases[$i], $lang, true); - - if (!count($token)) - { - continue; - } - - $token = $this->getTokenData(array_shift($token)); - - if ($params->get('filter_commonwords', 0) && $token->common) - { - continue; - } - - if ($params->get('filter_numeric', 0) && $token->numeric) - { - continue; - } - - // Set the required flag. - $token->required = true; - - // Add the current token to the stack. - $this->included[] = $token; - $this->highlight = array_merge($this->highlight, array_keys($token->matches)); - - // Remove the processed term if possible. - if (($pk = array_search($phrases[$i], $terms, true)) !== false) - { - unset($terms[$pk]); - } - - // Remove the processed phrase. - unset($phrases[$i]); - } - - /* - * Handle any remaining tokens using the standard processing mechanism. - */ - if ((bool) $terms) - { - // Tokenize the terms. - $terms = implode(' ', $terms); - $tokens = Helper::tokenize($terms, $lang, false); - - // Make sure we are working with an array. - $tokens = is_array($tokens) ? $tokens : array($tokens); - - // Get the token data and required state for all the tokens. - foreach ($tokens as $token) - { - // Get the token data. - $token = $this->getTokenData($token); - - if ($params->get('filter_commonwords', 0) && $token->common) - { - continue; - } - - if ($params->get('filter_numerics', 0) && $token->numeric) - { - continue; - } - - // Set the required flag for the token. - $token->required = $mode === 'AND' ? (!$token->phrase) : false; - - // Add the token to the appropriate stack. - if ($token->required || (bool) $token->matches) - { - $this->included[] = $token; - $this->highlight = array_merge($this->highlight, array_keys($token->matches)); - } - else - { - $this->ignored[] = $token; - } - } - } - - return true; - } - - /** - * Method to get the base and similar term ids and, if necessary, suggested - * term data from the database. The terms ids are identified based on a - * 'like' match in MySQL and/or a common stem. If no term ids could be - * found, then we know that we will not be able to return any results for - * that term and we should try to find a similar term to use that we can - * match so that we can suggest the alternative search query to the user. - * - * @param Token $token A Token object. - * - * @return Token A Token object. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getTokenData($token) - { - // Get the database object. - $db = $this->getDatabase(); - - // Create a database query to build match the token. - $query = $db->getQuery(true) - ->select('t.term, t.term_id') - ->from('#__finder_terms AS t'); - - if ($token->phrase) - { - // Add the phrase to the query. - $query->where('t.term = ' . $db->quote($token->term)) - ->where('t.phrase = 1'); - } - else - { - // Add the term to the query. - - $searchTerm = $token->term; - $searchStem = $token->stem; - $term = $query->quoteName('t.term'); - $stem = $query->quoteName('t.stem'); - - if ($this->wordmode === 'begin') - { - $searchTerm .= '%'; - $searchStem .= '%'; - $query->where('(' . $term . ' LIKE :searchTerm OR ' . $stem . ' LIKE :searchStem)'); - } - elseif ($this->wordmode === 'fuzzy') - { - $searchTerm = '%' . $searchTerm . '%'; - $searchStem = '%' . $searchStem . '%'; - $query->where('(' . $term . ' LIKE :searchTerm OR ' . $stem . ' LIKE :searchStem)'); - } - else - { - $query->where('(' . $term . ' = :searchTerm OR ' . $stem . ' = :searchStem)'); - } - - $query->bind(':searchTerm', $searchTerm, ParameterType::STRING) - ->bind(':searchStem', $searchStem, ParameterType::STRING); - - $query->where('t.phrase = 0') - ->where('t.language IN (\'*\',' . $db->quote($token->language) . ')'); - } - - // Get the terms. - $db->setQuery($query); - $matches = $db->loadObjectList(); - - // Check the matching terms. - if ((bool) $matches) - { - // Add the matches to the token. - for ($i = 0, $c = count($matches); $i < $c; $i++) - { - if (!isset($token->matches[$matches[$i]->term])) - { - $token->matches[$matches[$i]->term] = array(); - } - - $token->matches[$matches[$i]->term][] = (int) $matches[$i]->term_id; - } - } - - // If no matches were found, try to find a similar but better token. - if (empty($token->matches)) - { - // Create a database query to get the similar terms. - $query->clear() - ->select('DISTINCT t.term_id AS id, t.term AS term') - ->from('#__finder_terms AS t') - // ->where('t.soundex = ' . soundex($db->quote($token->term))) - ->where('t.soundex = SOUNDEX(' . $db->quote($token->term) . ')') - ->where('t.phrase = ' . (int) $token->phrase); - - // Get the terms. - $db->setQuery($query); - $results = $db->loadObjectList(); - - // Check if any similar terms were found. - if (empty($results)) - { - return $token; - } - - // Stack for sorting the similar terms. - $suggestions = array(); - - // Get the levnshtein distance for all suggested terms. - foreach ($results as $sk => $st) - { - // Get the levenshtein distance between terms. - $distance = levenshtein($st->term, $token->term); - - // Make sure the levenshtein distance isn't over 50. - if ($distance < 50) - { - $suggestions[$sk] = $distance; - } - } - - // Sort the suggestions. - asort($suggestions, SORT_NUMERIC); - - // Get the closest match. - $keys = array_keys($suggestions); - $key = $keys[0]; - - // Add the suggested term. - $token->suggestion = $results[$key]->term; - } - - return $token; - } + use DatabaseAwareTrait; + + /** + * Flag to show whether the query can return results. + * + * @var boolean + * @since 2.5 + */ + public $search; + + /** + * The query input string. + * + * @var string + * @since 2.5 + */ + public $input; + + /** + * The language of the query. + * + * @var string + * @since 2.5 + */ + public $language; + + /** + * The query string matching mode. + * + * @var string + * @since 2.5 + */ + public $mode; + + /** + * The included tokens. + * + * @var Token[] + * @since 2.5 + */ + public $included = array(); + + /** + * The excluded tokens. + * + * @var Token[] + * @since 2.5 + */ + public $excluded = array(); + + /** + * The tokens to ignore because no matches exist. + * + * @var Token[] + * @since 2.5 + */ + public $ignored = array(); + + /** + * The operators used in the query input string. + * + * @var array + * @since 2.5 + */ + public $operators = array(); + + /** + * The terms to highlight as matches. + * + * @var array + * @since 2.5 + */ + public $highlight = array(); + + /** + * The number of matching terms for the query input. + * + * @var integer + * @since 2.5 + */ + public $terms; + + /** + * Allow empty searches + * + * @var boolean + * @since 4.0.0 + */ + public $empty; + + /** + * The static filter id. + * + * @var string + * @since 2.5 + */ + public $filter; + + /** + * The taxonomy filters. This is a multi-dimensional array of taxonomy + * branches as the first level and then the taxonomy nodes as the values. + * + * For example: + * $filters = array( + * 'Type' = array(10, 32, 29, 11, ...); + * 'Label' = array(20, 314, 349, 91, 82, ...); + * ... + * ); + * + * @var array + * @since 2.5 + */ + public $filters = array(); + + /** + * The start date filter. + * + * @var string + * @since 2.5 + */ + public $date1; + + /** + * The end date filter. + * + * @var string + * @since 2.5 + */ + public $date2; + + /** + * The start date filter modifier. + * + * @var string + * @since 2.5 + */ + public $when1; + + /** + * The end date filter modifier. + * + * @var string + * @since 2.5 + */ + public $when2; + + /** + * Match search terms exactly or with a LIKE scheme + * + * @var string + * @since 4.2.0 + */ + public $wordmode; + + /** + * Method to instantiate the query object. + * + * @param array $options An array of query options. + * + * @since 2.5 + * @throws Exception on database error. + */ + public function __construct($options, DatabaseInterface $db = null) + { + if ($db === null) { + @trigger_error(sprintf('Database will be mandatory in 5.0.'), E_USER_DEPRECATED); + $db = Factory::getContainer()->get(DatabaseInterface::class); + } + + $this->setDatabase($db); + + // Get the input string. + $this->input = $options['input'] ?? ''; + + // Get the empty query setting. + $this->empty = isset($options['empty']) ? (bool) $options['empty'] : false; + + // Get the input language. + $this->language = !empty($options['language']) ? $options['language'] : Helper::getDefaultLanguage(); + + // Get the matching mode. + $this->mode = 'AND'; + + // Set the word matching mode + $this->wordmode = !empty($options['word_match']) ? $options['word_match'] : 'exact'; + + // Initialize the temporary date storage. + $this->dates = new Registry(); + + // Populate the temporary date storage. + if (!empty($options['date1'])) { + $this->dates->set('date1', $options['date1']); + } + + if (!empty($options['date2'])) { + $this->dates->set('date2', $options['date2']); + } + + if (!empty($options['when1'])) { + $this->dates->set('when1', $options['when1']); + } + + if (!empty($options['when2'])) { + $this->dates->set('when2', $options['when2']); + } + + // Process the static taxonomy filters. + if (!empty($options['filter'])) { + $this->processStaticTaxonomy($options['filter']); + } + + // Process the dynamic taxonomy filters. + if (!empty($options['filters'])) { + $this->processDynamicTaxonomy($options['filters']); + } + + // Get the date filters. + $d1 = $this->dates->get('date1'); + $d2 = $this->dates->get('date2'); + $w1 = $this->dates->get('when1'); + $w2 = $this->dates->get('when2'); + + // Process the date filters. + if (!empty($d1) || !empty($d2)) { + $this->processDates($d1, $d2, $w1, $w2); + } + + // Process the input string. + $this->processString($this->input, $this->language, $this->mode); + + // Get the number of matching terms. + foreach ($this->included as $token) { + $this->terms += count($token->matches); + } + + // Remove the temporary date storage. + unset($this->dates); + + // Lastly, determine whether this query can return a result set. + + // Check if we have a query string. + if (!empty($this->input)) { + $this->search = true; + } + // Check if we can search without a query string. + elseif ($this->empty && (!empty($this->filter) || !empty($this->filters) || !empty($this->date1) || !empty($this->date2))) { + $this->search = true; + } + // We do not have a valid search query. + else { + $this->search = false; + } + } + + /** + * Method to convert the query object into a URI string. + * + * @param string $base The base URI. [optional] + * + * @return string The complete query URI. + * + * @since 2.5 + */ + public function toUri($base = '') + { + // Set the base if not specified. + if ($base === '') { + $base = 'index.php?option=com_finder&view=search'; + } + + // Get the base URI. + $uri = Uri::getInstance($base); + + // Add the static taxonomy filter if present. + if ((bool) $this->filter) { + $uri->setVar('f', $this->filter); + } + + // Get the filters in the request. + $t = Factory::getApplication()->input->request->get('t', array(), 'array'); + + // Add the dynamic taxonomy filters if present. + if ((bool) $this->filters) { + foreach ($this->filters as $nodes) { + foreach ($nodes as $node) { + if (!in_array($node, $t)) { + continue; + } + + $uri->setVar('t[]', $node); + } + } + } + + // Add the input string if present. + if (!empty($this->input)) { + $uri->setVar('q', $this->input); + } + + // Add the start date if present. + if (!empty($this->date1)) { + $uri->setVar('d1', $this->date1); + } + + // Add the end date if present. + if (!empty($this->date2)) { + $uri->setVar('d2', $this->date2); + } + + // Add the start date modifier if present. + if (!empty($this->when1)) { + $uri->setVar('w1', $this->when1); + } + + // Add the end date modifier if present. + if (!empty($this->when2)) { + $uri->setVar('w2', $this->when2); + } + + // Add a menu item id if one is not present. + if (!$uri->getVar('Itemid')) { + // Get the menu item id. + $query = array( + 'view' => $uri->getVar('view'), + 'f' => $uri->getVar('f'), + 'q' => $uri->getVar('q'), + ); + + $item = RouteHelper::getItemid($query); + + // Add the menu item id if present. + if ($item !== null) { + $uri->setVar('Itemid', $item); + } + } + + return $uri->toString(array('path', 'query')); + } + + /** + * Method to get a list of excluded search term ids. + * + * @return array An array of excluded term ids. + * + * @since 2.5 + */ + public function getExcludedTermIds() + { + $results = array(); + + // Iterate through the excluded tokens and compile the matching terms. + for ($i = 0, $c = count($this->excluded); $i < $c; $i++) { + foreach ($this->excluded[$i]->matches as $match) { + $results = array_merge($results, $match); + } + } + + // Sanitize the terms. + $results = array_unique($results); + + return ArrayHelper::toInteger($results); + } + + /** + * Method to get a list of included search term ids. + * + * @return array An array of included term ids. + * + * @since 2.5 + */ + public function getIncludedTermIds() + { + $results = array(); + + // Iterate through the included tokens and compile the matching terms. + for ($i = 0, $c = count($this->included); $i < $c; $i++) { + // Check if we have any terms. + if (empty($this->included[$i]->matches)) { + continue; + } + + // Get the term. + $term = $this->included[$i]->term; + + // Prepare the container for the term if necessary. + if (!array_key_exists($term, $results)) { + $results[$term] = array(); + } + + // Add the matches to the stack. + foreach ($this->included[$i]->matches as $match) { + $results[$term] = array_merge($results[$term], $match); + } + } + + // Sanitize the terms. + foreach ($results as $key => $value) { + $results[$key] = array_unique($results[$key]); + $results[$key] = ArrayHelper::toInteger($results[$key]); + } + + return $results; + } + + /** + * Method to get a list of required search term ids. + * + * @return array An array of required term ids. + * + * @since 2.5 + */ + public function getRequiredTermIds() + { + $results = array(); + + // Iterate through the included tokens and compile the matching terms. + for ($i = 0, $c = count($this->included); $i < $c; $i++) { + // Check if the token is required. + if ($this->included[$i]->required) { + // Get the term. + $term = $this->included[$i]->term; + + // Prepare the container for the term if necessary. + if (!array_key_exists($term, $results)) { + $results[$term] = array(); + } + + // Add the matches to the stack. + foreach ($this->included[$i]->matches as $match) { + $results[$term] = array_merge($results[$term], $match); + } + } + } + + // Sanitize the terms. + foreach ($results as $key => $value) { + $results[$key] = array_unique($results[$key]); + $results[$key] = ArrayHelper::toInteger($results[$key]); + } + + return $results; + } + + /** + * Method to process the static taxonomy input. The static taxonomy input + * comes in the form of a pre-defined search filter that is assigned to the + * search form. + * + * @param integer $filterId The id of static filter. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function processStaticTaxonomy($filterId) + { + // Get the database object. + $db = $this->getDatabase(); + + // Initialize user variables + $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); + + // Load the predefined filter. + $query = $db->getQuery(true) + ->select('f.data, f.params') + ->from($db->quoteName('#__finder_filters') . ' AS f') + ->where('f.filter_id = ' . (int) $filterId); + + $db->setQuery($query); + $return = $db->loadObject(); + + // Check the returned filter. + if (empty($return)) { + return false; + } + + // Set the filter. + $this->filter = (int) $filterId; + + // Get a parameter object for the filter date options. + $registry = new Registry($return->params); + $params = $registry; + + // Set the dates if not already set. + $this->dates->def('d1', $params->get('d1')); + $this->dates->def('d2', $params->get('d2')); + $this->dates->def('w1', $params->get('w1')); + $this->dates->def('w2', $params->get('w2')); + + // Remove duplicates and sanitize. + $filters = explode(',', $return->data); + $filters = array_unique($filters); + $filters = ArrayHelper::toInteger($filters); + + // Remove any values of zero. + if (in_array(0, $filters, true) !== false) { + unset($filters[array_search(0, $filters, true)]); + } + + // Check if we have any real input. + if (empty($filters)) { + return true; + } + + /* + * Create the query to get filters from the database. We do this for + * two reasons: one, it allows us to ensure that the filters being used + * are real; two, we need to sort the filters by taxonomy branch. + */ + $query->clear() + ->select('t1.id, t1.title, t2.title AS branch') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') + ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') + ->where('t1.state = 1') + ->where('t1.access IN (' . $groups . ')') + ->where('t1.id IN (' . implode(',', $filters) . ')') + ->where('t2.state = 1') + ->where('t2.access IN (' . $groups . ')'); + + // Load the filters. + $db->setQuery($query); + $results = $db->loadObjectList(); + + // Sort the filter ids by branch. + foreach ($results as $result) { + $this->filters[$result->branch][$result->title] = (int) $result->id; + } + + return true; + } + + /** + * Method to process the dynamic taxonomy input. The dynamic taxonomy input + * comes in the form of select fields that the user chooses from. The + * dynamic taxonomy input is processed AFTER the static taxonomy input + * because the dynamic options can be used to further narrow a static + * taxonomy filter. + * + * @param array $filters An array of taxonomy node ids. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function processDynamicTaxonomy($filters) + { + // Initialize user variables + $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); + + // Remove duplicates and sanitize. + $filters = array_unique($filters); + $filters = ArrayHelper::toInteger($filters); + + // Remove any values of zero. + if (in_array(0, $filters, true) !== false) { + unset($filters[array_search(0, $filters, true)]); + } + + // Check if we have any real input. + if (empty($filters)) { + return true; + } + + // Get the database object. + $db = $this->getDatabase(); + + $query = $db->getQuery(true); + + /* + * Create the query to get filters from the database. We do this for + * two reasons: one, it allows us to ensure that the filters being used + * are real; two, we need to sort the filters by taxonomy branch. + */ + $query->select('t1.id, t1.title, t2.title AS branch') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') + ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') + ->where('t1.state = 1') + ->where('t1.access IN (' . $groups . ')') + ->where('t1.id IN (' . implode(',', $filters) . ')') + ->where('t2.state = 1') + ->where('t2.access IN (' . $groups . ')'); + + // Load the filters. + $db->setQuery($query); + $results = $db->loadObjectList(); + + // Cleared filter branches. + $cleared = array(); + + /* + * Sort the filter ids by branch. Because these filters are designed to + * override and further narrow the items selected in the static filter, + * we will clear the values from the static filter on a branch by + * branch basis before adding the dynamic filters. So, if the static + * filter defines a type filter of "articles" and three "category" + * filters but the user only limits the category further, the category + * filters will be flushed but the type filters will not. + */ + foreach ($results as $result) { + // Check if the branch has been cleared. + if (!in_array($result->branch, $cleared, true)) { + // Clear the branch. + $this->filters[$result->branch] = array(); + + // Add the branch to the cleared list. + $cleared[] = $result->branch; + } + + // Add the filter to the list. + $this->filters[$result->branch][$result->title] = (int) $result->id; + } + + return true; + } + + /** + * Method to process the query date filters to determine start and end + * date limitations. + * + * @param string $date1 The first date filter. + * @param string $date2 The second date filter. + * @param string $when1 The first date modifier. + * @param string $when2 The second date modifier. + * + * @return boolean True on success. + * + * @since 2.5 + */ + protected function processDates($date1, $date2, $when1, $when2) + { + // Clean up the inputs. + $date1 = trim(StringHelper::strtolower($date1)); + $date2 = trim(StringHelper::strtolower($date2)); + $when1 = trim(StringHelper::strtolower($when1)); + $when2 = trim(StringHelper::strtolower($when2)); + + // Get the time offset. + $offset = Factory::getApplication()->get('offset'); + + // Array of allowed when values. + $whens = array('before', 'after', 'exact'); + + // The value of 'today' is a special case that we need to handle. + if ($date1 === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) { + $date1 = Factory::getDate('now', $offset)->format('%Y-%m-%d'); + } + + // Try to parse the date string. + $date = Factory::getDate($date1, $offset); + + // Check if the date was parsed successfully. + if ($date->toUnix() !== null) { + // Set the date filter. + $this->date1 = $date->toSql(); + $this->when1 = in_array($when1, $whens, true) ? $when1 : 'before'; + } + + // The value of 'today' is a special case that we need to handle. + if ($date2 === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) { + $date2 = Factory::getDate('now', $offset)->format('%Y-%m-%d'); + } + + // Try to parse the date string. + $date = Factory::getDate($date2, $offset); + + // Check if the date was parsed successfully. + if ($date->toUnix() !== null) { + // Set the date filter. + $this->date2 = $date->toSql(); + $this->when2 = in_array($when2, $whens, true) ? $when2 : 'before'; + } + + return true; + } + + /** + * Method to process the query input string and extract required, optional, + * and excluded tokens; taxonomy filters; and date filters. + * + * @param string $input The query input string. + * @param string $lang The query input language. + * @param string $mode The query matching mode. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function processString($input, $lang, $mode) + { + if ($input === null) { + $input = ''; + } + + // Clean up the input string. + $input = html_entity_decode($input, ENT_QUOTES, 'UTF-8'); + $input = StringHelper::strtolower($input); + $input = preg_replace('#\s+#mi', ' ', $input); + $input = trim($input); + $debug = Factory::getApplication()->get('debug_lang'); + $params = ComponentHelper::getParams('com_finder'); + + /* + * First, we need to handle string based modifiers. String based + * modifiers could potentially include things like "category:blah" or + * "before:2009-10-21" or "type:article", etc. + */ + $patterns = array( + 'before' => Text::_('COM_FINDER_FILTER_WHEN_BEFORE'), + 'after' => Text::_('COM_FINDER_FILTER_WHEN_AFTER'), + ); + + // Add the taxonomy branch titles to the possible patterns. + foreach (Taxonomy::getBranchTitles() as $branch) { + // Add the pattern. + $patterns[$branch] = StringHelper::strtolower(Text::_(LanguageHelper::branchSingular($branch))); + } + + // Container for search terms and phrases. + $terms = array(); + $phrases = array(); + + // Cleared filter branches. + $cleared = array(); + + /* + * Compile the suffix pattern. This is used to match the values of the + * filter input string. Single words can be input directly, multi-word + * values have to be wrapped in double quotes. + */ + $quotes = html_entity_decode('‘’'', ENT_QUOTES, 'UTF-8'); + $suffix = '(([\w\d' . $quotes . '-]+)|\"([\w\d\s' . $quotes . '-]+)\")'; + + /* + * Iterate through the possible filter patterns and search for matches. + * We need to match the key, colon, and a value pattern for the match + * to be valid. + */ + foreach ($patterns as $modifier => $pattern) { + $matches = array(); + + if ($debug) { + $pattern = substr($pattern, 2, -2); + } + + // Check if the filter pattern is in the input string. + if (preg_match('#' . $pattern . '\s*:\s*' . $suffix . '#mi', $input, $matches)) { + // Get the value given to the modifier. + $value = $matches[3] ?? $matches[1]; + + // Now we have to handle the filter string. + switch ($modifier) { + // Handle a before and after date filters. + case 'before': + case 'after': + // Get the time offset. + $offset = Factory::getApplication()->get('offset'); + + // Array of allowed when values. + $whens = array('before', 'after', 'exact'); + + // The value of 'today' is a special case that we need to handle. + if ($value === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) { + $value = Factory::getDate('now', $offset)->format('%Y-%m-%d'); + } + + // Try to parse the date string. + $date = Factory::getDate($value, $offset); + + // Check if the date was parsed successfully. + if ($date->toUnix() !== null) { + // Set the date filter. + $this->date1 = $date->toSql(); + $this->when1 = in_array($modifier, $whens, true) ? $modifier : 'before'; + } + + break; + + // Handle a taxonomy branch filter. + default: + // Try to find the node id. + $return = Taxonomy::getNodeByTitle($modifier, $value); + + // Check if the node id was found. + if ($return) { + // Check if the branch has been cleared. + if (!in_array($modifier, $cleared, true)) { + // Clear the branch. + $this->filters[$modifier] = array(); + + // Add the branch to the cleared list. + $cleared[] = $modifier; + } + + // Add the filter to the list. + $this->filters[$modifier][$return->title] = (int) $return->id; + } + + break; + } + + // Clean up the input string again. + $input = str_replace($matches[0], '', $input); + $input = preg_replace('#\s+#mi', ' ', $input); + $input = trim($input); + } + } + + /* + * Extract the tokens enclosed in double quotes so that we can handle + * them as phrases. + */ + if (StringHelper::strpos($input, '"') !== false) { + $matches = array(); + + // Extract the tokens enclosed in double quotes. + if (preg_match_all('#\"([^"]+)\"#m', $input, $matches)) { + /* + * One or more phrases were found so we need to iterate through + * them, tokenize them as phrases, and remove them from the raw + * input string before we move on to the next processing step. + */ + foreach ($matches[1] as $key => $match) { + // Find the complete phrase in the input string. + $pos = StringHelper::strpos($input, $matches[0][$key]); + $len = StringHelper::strlen($matches[0][$key]); + + // Add any terms that are before this phrase to the stack. + if (trim(StringHelper::substr($input, 0, $pos))) { + $terms = array_merge($terms, explode(' ', trim(StringHelper::substr($input, 0, $pos)))); + } + + // Strip out everything up to and including the phrase. + $input = StringHelper::substr($input, $pos + $len); + + // Clean up the input string again. + $input = preg_replace('#\s+#mi', ' ', $input); + $input = trim($input); + + // Get the number of words in the phrase. + $parts = explode(' ', $match); + $tuplecount = $params->get('tuplecount', 1); + + // Check if the phrase is longer than our $tuplecount. + if (count($parts) > $tuplecount && $tuplecount > 1) { + $chunk = array_slice($parts, 0, $tuplecount); + $parts = array_slice($parts, $tuplecount); + + // If the chunk is not empty, add it as a phrase. + if (count($chunk)) { + $phrases[] = implode(' ', $chunk); + $terms[] = implode(' ', $chunk); + } + + /* + * If the phrase is longer than $tuplecount words, we need to + * break it down into smaller chunks of phrases that + * are less than or equal to $tuplecount words. We overlap + * the chunks so that we can ensure that a match is + * found for the complete phrase and not just portions + * of it. + */ + for ($i = 0, $c = count($parts); $i < $c; $i++) { + array_shift($chunk); + $chunk[] = array_shift($parts); + + // If the chunk is not empty, add it as a phrase. + if (count($chunk)) { + $phrases[] = implode(' ', $chunk); + $terms[] = implode(' ', $chunk); + } + } + } else { + // The phrase is <= $tuplecount words so we can use it as is. + $phrases[] = $match; + $terms[] = $match; + } + } + } + } + + // Add the remaining terms if present. + if ((bool) $input) { + $terms = array_merge($terms, explode(' ', $input)); + } + + // An array of our boolean operators. $operator => $translation + $operators = array( + 'AND' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_AND')), + 'OR' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_OR')), + 'NOT' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_NOT')), + ); + + // If language debugging is enabled you need to ignore the debug strings in matching. + if (JDEBUG) { + $debugStrings = array('**', '??'); + $operators = str_replace($debugStrings, '', $operators); + } + + /* + * Iterate through the terms and perform any sorting that needs to be + * done based on boolean search operators. Terms that are before an + * and/or/not modifier have to be handled in relation to their operator. + */ + for ($i = 0, $c = count($terms); $i < $c; $i++) { + // Check if the term is followed by an operator that we understand. + if (isset($terms[$i + 1]) && in_array($terms[$i + 1], $operators, true)) { + // Get the operator mode. + $op = array_search($terms[$i + 1], $operators, true); + + // Handle the AND operator. + if ($op === 'AND' && isset($terms[$i + 2])) { + // Tokenize the current term. + $token = Helper::tokenize($terms[$i], $lang, true); + + // @todo: The previous function call may return an array, which seems not to be handled by the next one, which expects an object + $token = $this->getTokenData(array_shift($token)); + + if ($params->get('filter_commonwords', 0) && $token->common) { + continue; + } + + if ($params->get('filter_numeric', 0) && $token->numeric) { + continue; + } + + // Set the required flag. + $token->required = true; + + // Add the current token to the stack. + $this->included[] = $token; + $this->highlight = array_merge($this->highlight, array_keys($token->matches)); + + // Skip the next token (the mode operator). + $this->operators[] = $terms[$i + 1]; + + // Tokenize the term after the next term (current plus two). + $other = Helper::tokenize($terms[$i + 2], $lang, true); + $other = $this->getTokenData(array_shift($other)); + + // Set the required flag. + $other->required = true; + + // Add the token after the next token to the stack. + $this->included[] = $other; + $this->highlight = array_merge($this->highlight, array_keys($other->matches)); + + // Remove the processed phrases if possible. + if (($pk = array_search($terms[$i], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + if (($pk = array_search($terms[$i + 2], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + // Remove the processed terms. + unset($terms[$i], $terms[$i + 1], $terms[$i + 2]); + + // Adjust the loop. + $i += 2; + } + // Handle the OR operator. + elseif ($op === 'OR' && isset($terms[$i + 2])) { + // Tokenize the current term. + $token = Helper::tokenize($terms[$i], $lang, true); + $token = $this->getTokenData(array_shift($token)); + + if ($params->get('filter_commonwords', 0) && $token->common) { + continue; + } + + if ($params->get('filter_numeric', 0) && $token->numeric) { + continue; + } + + // Set the required flag. + $token->required = false; + + // Add the current token to the stack. + if ((bool) $token->matches) { + $this->included[] = $token; + $this->highlight = array_merge($this->highlight, array_keys($token->matches)); + } else { + $this->ignored[] = $token; + } + + // Skip the next token (the mode operator). + $this->operators[] = $terms[$i + 1]; + + // Tokenize the term after the next term (current plus two). + $other = Helper::tokenize($terms[$i + 2], $lang, true); + $other = $this->getTokenData(array_shift($other)); + + // Set the required flag. + $other->required = false; + + // Add the token after the next token to the stack. + if ((bool) $other->matches) { + $this->included[] = $other; + $this->highlight = array_merge($this->highlight, array_keys($other->matches)); + } else { + $this->ignored[] = $other; + } + + // Remove the processed phrases if possible. + if (($pk = array_search($terms[$i], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + if (($pk = array_search($terms[$i + 2], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + // Remove the processed terms. + unset($terms[$i], $terms[$i + 1], $terms[$i + 2]); + + // Adjust the loop. + $i += 2; + } + } + // Handle an orphaned OR operator. + elseif (isset($terms[$i + 1]) && array_search($terms[$i], $operators, true) === 'OR') { + // Skip the next token (the mode operator). + $this->operators[] = $terms[$i]; + + // Tokenize the next term (current plus one). + $other = Helper::tokenize($terms[$i + 1], $lang, true); + $other = $this->getTokenData(array_shift($other)); + + if ($params->get('filter_commonwords', 0) && $other->common) { + continue; + } + + if ($params->get('filter_numeric', 0) && $other->numeric) { + continue; + } + + // Set the required flag. + $other->required = false; + + // Add the token after the next token to the stack. + if ((bool) $other->matches) { + $this->included[] = $other; + $this->highlight = array_merge($this->highlight, array_keys($other->matches)); + } else { + $this->ignored[] = $other; + } + + // Remove the processed phrase if possible. + if (($pk = array_search($terms[$i + 1], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + // Remove the processed terms. + unset($terms[$i], $terms[$i + 1]); + + // Adjust the loop. + $i++; + } + // Handle the NOT operator. + elseif (isset($terms[$i + 1]) && array_search($terms[$i], $operators, true) === 'NOT') { + // Skip the next token (the mode operator). + $this->operators[] = $terms[$i]; + + // Tokenize the next term (current plus one). + $other = Helper::tokenize($terms[$i + 1], $lang, true); + $other = $this->getTokenData(array_shift($other)); + + if ($params->get('filter_commonwords', 0) && $other->common) { + continue; + } + + if ($params->get('filter_numeric', 0) && $other->numeric) { + continue; + } + + // Set the required flag. + $other->required = false; + + // Add the next token to the stack. + if ((bool) $other->matches) { + $this->excluded[] = $other; + } else { + $this->ignored[] = $other; + } + + // Remove the processed phrase if possible. + if (($pk = array_search($terms[$i + 1], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + // Remove the processed terms. + unset($terms[$i], $terms[$i + 1]); + + // Adjust the loop. + $i++; + } + } + + /* + * Iterate through any search phrases and tokenize them. We handle + * phrases as autonomous units and do not break them down into two and + * three word combinations. + */ + for ($i = 0, $c = count($phrases); $i < $c; $i++) { + // Tokenize the phrase. + $token = Helper::tokenize($phrases[$i], $lang, true); + + if (!count($token)) { + continue; + } + + $token = $this->getTokenData(array_shift($token)); + + if ($params->get('filter_commonwords', 0) && $token->common) { + continue; + } + + if ($params->get('filter_numeric', 0) && $token->numeric) { + continue; + } + + // Set the required flag. + $token->required = true; + + // Add the current token to the stack. + $this->included[] = $token; + $this->highlight = array_merge($this->highlight, array_keys($token->matches)); + + // Remove the processed term if possible. + if (($pk = array_search($phrases[$i], $terms, true)) !== false) { + unset($terms[$pk]); + } + + // Remove the processed phrase. + unset($phrases[$i]); + } + + /* + * Handle any remaining tokens using the standard processing mechanism. + */ + if ((bool) $terms) { + // Tokenize the terms. + $terms = implode(' ', $terms); + $tokens = Helper::tokenize($terms, $lang, false); + + // Make sure we are working with an array. + $tokens = is_array($tokens) ? $tokens : array($tokens); + + // Get the token data and required state for all the tokens. + foreach ($tokens as $token) { + // Get the token data. + $token = $this->getTokenData($token); + + if ($params->get('filter_commonwords', 0) && $token->common) { + continue; + } + + if ($params->get('filter_numerics', 0) && $token->numeric) { + continue; + } + + // Set the required flag for the token. + $token->required = $mode === 'AND' ? (!$token->phrase) : false; + + // Add the token to the appropriate stack. + if ($token->required || (bool) $token->matches) { + $this->included[] = $token; + $this->highlight = array_merge($this->highlight, array_keys($token->matches)); + } else { + $this->ignored[] = $token; + } + } + } + + return true; + } + + /** + * Method to get the base and similar term ids and, if necessary, suggested + * term data from the database. The terms ids are identified based on a + * 'like' match in MySQL and/or a common stem. If no term ids could be + * found, then we know that we will not be able to return any results for + * that term and we should try to find a similar term to use that we can + * match so that we can suggest the alternative search query to the user. + * + * @param Token $token A Token object. + * + * @return Token A Token object. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getTokenData($token) + { + // Get the database object. + $db = $this->getDatabase(); + + // Create a database query to build match the token. + $query = $db->getQuery(true) + ->select('t.term, t.term_id') + ->from('#__finder_terms AS t'); + + if ($token->phrase) { + // Add the phrase to the query. + $query->where('t.term = ' . $db->quote($token->term)) + ->where('t.phrase = 1'); + } else { + // Add the term to the query. + + $searchTerm = $token->term; + $searchStem = $token->stem; + $term = $query->quoteName('t.term'); + $stem = $query->quoteName('t.stem'); + + if ($this->wordmode === 'begin') { + $searchTerm .= '%'; + $searchStem .= '%'; + $query->where('(' . $term . ' LIKE :searchTerm OR ' . $stem . ' LIKE :searchStem)'); + } elseif ($this->wordmode === 'fuzzy') { + $searchTerm = '%' . $searchTerm . '%'; + $searchStem = '%' . $searchStem . '%'; + $query->where('(' . $term . ' LIKE :searchTerm OR ' . $stem . ' LIKE :searchStem)'); + } else { + $query->where('(' . $term . ' = :searchTerm OR ' . $stem . ' = :searchStem)'); + } + + $query->bind(':searchTerm', $searchTerm, ParameterType::STRING) + ->bind(':searchStem', $searchStem, ParameterType::STRING); + + $query->where('t.phrase = 0') + ->where('t.language IN (\'*\',' . $db->quote($token->language) . ')'); + } + + // Get the terms. + $db->setQuery($query); + $matches = $db->loadObjectList(); + + // Check the matching terms. + if ((bool) $matches) { + // Add the matches to the token. + for ($i = 0, $c = count($matches); $i < $c; $i++) { + if (!isset($token->matches[$matches[$i]->term])) { + $token->matches[$matches[$i]->term] = array(); + } + + $token->matches[$matches[$i]->term][] = (int) $matches[$i]->term_id; + } + } + + // If no matches were found, try to find a similar but better token. + if (empty($token->matches)) { + // Create a database query to get the similar terms. + $query->clear() + ->select('DISTINCT t.term_id AS id, t.term AS term') + ->from('#__finder_terms AS t') + // ->where('t.soundex = ' . soundex($db->quote($token->term))) + ->where('t.soundex = SOUNDEX(' . $db->quote($token->term) . ')') + ->where('t.phrase = ' . (int) $token->phrase); + + // Get the terms. + $db->setQuery($query); + $results = $db->loadObjectList(); + + // Check if any similar terms were found. + if (empty($results)) { + return $token; + } + + // Stack for sorting the similar terms. + $suggestions = array(); + + // Get the levnshtein distance for all suggested terms. + foreach ($results as $sk => $st) { + // Get the levenshtein distance between terms. + $distance = levenshtein($st->term, $token->term); + + // Make sure the levenshtein distance isn't over 50. + if ($distance < 50) { + $suggestions[$sk] = $distance; + } + } + + // Sort the suggestions. + asort($suggestions, SORT_NUMERIC); + + // Get the closest match. + $keys = array_keys($suggestions); + $key = $keys[0]; + + // Add the suggested term. + $token->suggestion = $results[$key]->term; + } + + return $token; + } } diff --git a/administrator/components/com_finder/src/Indexer/Result.php b/administrator/components/com_finder/src/Indexer/Result.php index cef7d04f7753b..71a370c04125d 100644 --- a/administrator/components/com_finder/src/Indexer/Result.php +++ b/administrator/components/com_finder/src/Indexer/Result.php @@ -1,4 +1,5 @@ array('title', 'subtitle', 'id'), - Indexer::TEXT_CONTEXT => array('summary', 'body'), - Indexer::META_CONTEXT => array('meta', 'list_price', 'sale_price'), - Indexer::PATH_CONTEXT => array('path', 'alias'), - Indexer::MISC_CONTEXT => array('comments'), - ); - - /** - * The indexer will use this data to create taxonomy mapping entries for - * the item so that it can be filtered by type, label, category, - * or whatever. - * - * @var array - * @since 2.5 - */ - protected $taxonomy = array(); - - /** - * The content URL. - * - * @var string - * @since 2.5 - */ - public $url; - - /** - * The content route. - * - * @var string - * @since 2.5 - */ - public $route; - - /** - * The content title. - * - * @var string - * @since 2.5 - */ - public $title; - - /** - * The content description. - * - * @var string - * @since 2.5 - */ - public $description; - - /** - * The published state of the result. - * - * @var integer - * @since 2.5 - */ - public $published; - - /** - * The content published state. - * - * @var integer - * @since 2.5 - */ - public $state; - - /** - * The content access level. - * - * @var integer - * @since 2.5 - */ - public $access; - - /** - * The content language. - * - * @var string - * @since 2.5 - */ - public $language = '*'; - - /** - * The publishing start date. - * - * @var string - * @since 2.5 - */ - public $publish_start_date; - - /** - * The publishing end date. - * - * @var string - * @since 2.5 - */ - public $publish_end_date; - - /** - * The generic start date. - * - * @var string - * @since 2.5 - */ - public $start_date; - - /** - * The generic end date. - * - * @var string - * @since 2.5 - */ - public $end_date; - - /** - * The item list price. - * - * @var mixed - * @since 2.5 - */ - public $list_price; - - /** - * The item sale price. - * - * @var mixed - * @since 2.5 - */ - public $sale_price; - - /** - * The content type id. This is set by the adapter. - * - * @var integer - * @since 2.5 - */ - public $type_id; - - /** - * The default language for content. - * - * @var string - * @since 3.0.2 - */ - public $defaultLanguage; - - /** - * Constructor - * - * @since 3.0.3 - */ - public function __construct() - { - $this->defaultLanguage = ComponentHelper::getParams('com_languages')->get('site', 'en-GB'); - } - - /** - * The magic set method is used to push additional values into the elements - * array in order to preserve the cleanliness of the object. - * - * @param string $name The name of the element. - * @param mixed $value The value of the element. - * - * @return void - * - * @since 2.5 - */ - public function __set($name, $value) - { - $this->setElement($name, $value); - } - - /** - * The magic get method is used to retrieve additional element values from the elements array. - * - * @param string $name The name of the element. - * - * @return mixed The value of the element if set, null otherwise. - * - * @since 2.5 - */ - public function __get($name) - { - return $this->getElement($name); - } - - /** - * The magic isset method is used to check the state of additional element values in the elements array. - * - * @param string $name The name of the element. - * - * @return boolean True if set, false otherwise. - * - * @since 2.5 - */ - public function __isset($name) - { - return isset($this->elements[$name]); - } - - /** - * The magic unset method is used to unset additional element values in the elements array. - * - * @param string $name The name of the element. - * - * @return void - * - * @since 2.5 - */ - public function __unset($name) - { - unset($this->elements[$name]); - } - - /** - * Method to retrieve additional element values from the elements array. - * - * @param string $name The name of the element. - * - * @return mixed The value of the element if set, null otherwise. - * - * @since 2.5 - */ - public function getElement($name) - { - // Get the element value if set. - if (array_key_exists($name, $this->elements)) - { - return $this->elements[$name]; - } - - return null; - } - - /** - * Method to retrieve all elements. - * - * @return array The elements - * - * @since 3.8.3 - */ - public function getElements() - { - return $this->elements; - } - - /** - * Method to set additional element values in the elements array. - * - * @param string $name The name of the element. - * @param mixed $value The value of the element. - * - * @return void - * - * @since 2.5 - */ - public function setElement($name, $value) - { - $this->elements[$name] = $value; - } - - /** - * Method to get all processing instructions. - * - * @return array An array of processing instructions. - * - * @since 2.5 - */ - public function getInstructions() - { - return $this->instructions; - } - - /** - * Method to add a processing instruction for an item property. - * - * @param string $group The group to associate the property with. - * @param string $property The property to process. - * - * @return void - * - * @since 2.5 - */ - public function addInstruction($group, $property) - { - // Check if the group exists. We can't add instructions for unknown groups. - // Check if the property exists in the group. - if (array_key_exists($group, $this->instructions) && !in_array($property, $this->instructions[$group], true)) - { - // Add the property to the group. - $this->instructions[$group][] = $property; - } - } - - /** - * Method to remove a processing instruction for an item property. - * - * @param string $group The group to associate the property with. - * @param string $property The property to process. - * - * @return void - * - * @since 2.5 - */ - public function removeInstruction($group, $property) - { - // Check if the group exists. We can't remove instructions for unknown groups. - if (array_key_exists($group, $this->instructions)) - { - // Search for the property in the group. - $key = array_search($property, $this->instructions[$group]); - - // If the property was found, remove it. - if ($key !== false) - { - unset($this->instructions[$group][$key]); - } - } - } - - /** - * Method to get the taxonomy maps for an item. - * - * @param string $branch The taxonomy branch to get. [optional] - * - * @return array An array of taxonomy maps. - * - * @since 2.5 - */ - public function getTaxonomy($branch = null) - { - // Get the taxonomy branch if available. - if ($branch !== null && isset($this->taxonomy[$branch])) - { - return $this->taxonomy[$branch]; - } - - return $this->taxonomy; - } - - /** - * Method to add a taxonomy map for an item. - * - * @param string $branch The title of the taxonomy branch to add the node to. - * @param string $title The title of the taxonomy node. - * @param integer $state The published state of the taxonomy node. [optional] - * @param integer $access The access level of the taxonomy node. [optional] - * @param string $language The language of the taxonomy. [optional] - * - * @return void - * - * @since 2.5 - */ - public function addTaxonomy($branch, $title, $state = 1, $access = 1, $language = '') - { - // Filter the input. - $branch = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,_]+#mui', ' ', $branch); - - // Create the taxonomy node. - $node = new \stdClass; - $node->title = $title; - $node->state = (int) $state; - $node->access = (int) $access; - $node->language = $language; - $node->nested = false; - - // Add the node to the taxonomy branch. - $this->taxonomy[$branch][] = $node; - } - - /** - * Method to add a nested taxonomy map for an item. - * - * @param string $branch The title of the taxonomy branch to add the node to. - * @param ImmutableNodeInterface $contentNode The node object. - * @param integer $state The published state of the taxonomy node. [optional] - * @param integer $access The access level of the taxonomy node. [optional] - * @param string $language The language of the taxonomy. [optional] - * - * @return void - * - * @since 4.0.0 - */ - public function addNestedTaxonomy($branch, ImmutableNodeInterface $contentNode, $state = 1, $access = 1, $language = '') - { - // Filter the input. - $branch = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,_]+#mui', ' ', $branch); - - // Create the taxonomy node. - $node = new \stdClass; - $node->title = $contentNode->title; - $node->state = (int) $state; - $node->access = (int) $access; - $node->language = $language; - $node->nested = true; - $node->node = $contentNode; - - // Add the node to the taxonomy branch. - $this->taxonomy[$branch][] = $node; - } - - /** - * Method to set the item language - * - * @return void - * - * @since 3.0 - */ - public function setLanguage() - { - if ($this->language == '') - { - $this->language = $this->defaultLanguage; - } - } - - /** - * Helper function to serialise the data of a Result object - * - * @return string The serialised data - * - * @since 4.0.0 - */ - public function serialize() - { - return serialize($this->__serialize()); - } - - /** - * Helper function to unserialise the data for this object - * - * @param string $serialized Serialised data to unserialise - * - * @return void - * - * @since 4.0.0 - */ - public function unserialize($serialized): void - { - $this->__unserialize(unserialize($serialized)); - } - - /** - * Magic method used for serializing. - * - * @since 4.1.3 - */ - public function __serialize(): array - { - $taxonomy = []; - - foreach ($this->taxonomy as $branch => $nodes) - { - $taxonomy[$branch] = []; - - foreach ($nodes as $node) - { - if ($node->nested) - { - $n = clone $node; - unset($n->node); - $taxonomy[$branch][] = $n; - } - else - { - $taxonomy[$branch][] = $node; - } - } - } - - // This order must match EXACTLY the order of the $properties in the self::__unserialize method - return [ - $this->access, - $this->defaultLanguage, - $this->description, - $this->elements, - $this->end_date, - $this->instructions, - $this->language, - $this->list_price, - $this->publish_end_date, - $this->publish_start_date, - $this->published, - $this->route, - $this->sale_price, - $this->start_date, - $this->state, - $taxonomy, - $this->title, - $this->type_id, - $this->url - ]; - } - - /** - * Magic method used for unserializing. - * - * @since 4.1.3 - */ - public function __unserialize(array $serialized): void - { - // This order must match EXACTLY the order of the array in the self::__serialize method - $properties = [ - 'access', - 'defaultLanguage', - 'description', - 'elements', - 'end_date', - 'instructions', - 'language', - 'list_price', - 'publish_end_date', - 'publish_start_date', - 'published', - 'route', - 'sale_price', - 'start_date', - 'state', - 'taxonomy', - 'title', - 'type_id', - 'url', - ]; - - foreach ($properties as $k => $v) - { - $this->$v = $serialized[$k]; - } - - foreach ($this->taxonomy as $nodes) - { - foreach ($nodes as $node) - { - $curTaxonomy = Taxonomy::getTaxonomy($node->id); - $node->state = $curTaxonomy->state; - $node->access = $curTaxonomy->access; - } - } - } + /** + * An array of extra result properties. + * + * @var array + * @since 2.5 + */ + protected $elements = array(); + + /** + * This array tells the indexer which properties should be indexed and what + * weights to use for those properties. + * + * @var array + * @since 2.5 + */ + protected $instructions = array( + Indexer::TITLE_CONTEXT => array('title', 'subtitle', 'id'), + Indexer::TEXT_CONTEXT => array('summary', 'body'), + Indexer::META_CONTEXT => array('meta', 'list_price', 'sale_price'), + Indexer::PATH_CONTEXT => array('path', 'alias'), + Indexer::MISC_CONTEXT => array('comments'), + ); + + /** + * The indexer will use this data to create taxonomy mapping entries for + * the item so that it can be filtered by type, label, category, + * or whatever. + * + * @var array + * @since 2.5 + */ + protected $taxonomy = array(); + + /** + * The content URL. + * + * @var string + * @since 2.5 + */ + public $url; + + /** + * The content route. + * + * @var string + * @since 2.5 + */ + public $route; + + /** + * The content title. + * + * @var string + * @since 2.5 + */ + public $title; + + /** + * The content description. + * + * @var string + * @since 2.5 + */ + public $description; + + /** + * The published state of the result. + * + * @var integer + * @since 2.5 + */ + public $published; + + /** + * The content published state. + * + * @var integer + * @since 2.5 + */ + public $state; + + /** + * The content access level. + * + * @var integer + * @since 2.5 + */ + public $access; + + /** + * The content language. + * + * @var string + * @since 2.5 + */ + public $language = '*'; + + /** + * The publishing start date. + * + * @var string + * @since 2.5 + */ + public $publish_start_date; + + /** + * The publishing end date. + * + * @var string + * @since 2.5 + */ + public $publish_end_date; + + /** + * The generic start date. + * + * @var string + * @since 2.5 + */ + public $start_date; + + /** + * The generic end date. + * + * @var string + * @since 2.5 + */ + public $end_date; + + /** + * The item list price. + * + * @var mixed + * @since 2.5 + */ + public $list_price; + + /** + * The item sale price. + * + * @var mixed + * @since 2.5 + */ + public $sale_price; + + /** + * The content type id. This is set by the adapter. + * + * @var integer + * @since 2.5 + */ + public $type_id; + + /** + * The default language for content. + * + * @var string + * @since 3.0.2 + */ + public $defaultLanguage; + + /** + * Constructor + * + * @since 3.0.3 + */ + public function __construct() + { + $this->defaultLanguage = ComponentHelper::getParams('com_languages')->get('site', 'en-GB'); + } + + /** + * The magic set method is used to push additional values into the elements + * array in order to preserve the cleanliness of the object. + * + * @param string $name The name of the element. + * @param mixed $value The value of the element. + * + * @return void + * + * @since 2.5 + */ + public function __set($name, $value) + { + $this->setElement($name, $value); + } + + /** + * The magic get method is used to retrieve additional element values from the elements array. + * + * @param string $name The name of the element. + * + * @return mixed The value of the element if set, null otherwise. + * + * @since 2.5 + */ + public function __get($name) + { + return $this->getElement($name); + } + + /** + * The magic isset method is used to check the state of additional element values in the elements array. + * + * @param string $name The name of the element. + * + * @return boolean True if set, false otherwise. + * + * @since 2.5 + */ + public function __isset($name) + { + return isset($this->elements[$name]); + } + + /** + * The magic unset method is used to unset additional element values in the elements array. + * + * @param string $name The name of the element. + * + * @return void + * + * @since 2.5 + */ + public function __unset($name) + { + unset($this->elements[$name]); + } + + /** + * Method to retrieve additional element values from the elements array. + * + * @param string $name The name of the element. + * + * @return mixed The value of the element if set, null otherwise. + * + * @since 2.5 + */ + public function getElement($name) + { + // Get the element value if set. + if (array_key_exists($name, $this->elements)) { + return $this->elements[$name]; + } + + return null; + } + + /** + * Method to retrieve all elements. + * + * @return array The elements + * + * @since 3.8.3 + */ + public function getElements() + { + return $this->elements; + } + + /** + * Method to set additional element values in the elements array. + * + * @param string $name The name of the element. + * @param mixed $value The value of the element. + * + * @return void + * + * @since 2.5 + */ + public function setElement($name, $value) + { + $this->elements[$name] = $value; + } + + /** + * Method to get all processing instructions. + * + * @return array An array of processing instructions. + * + * @since 2.5 + */ + public function getInstructions() + { + return $this->instructions; + } + + /** + * Method to add a processing instruction for an item property. + * + * @param string $group The group to associate the property with. + * @param string $property The property to process. + * + * @return void + * + * @since 2.5 + */ + public function addInstruction($group, $property) + { + // Check if the group exists. We can't add instructions for unknown groups. + // Check if the property exists in the group. + if (array_key_exists($group, $this->instructions) && !in_array($property, $this->instructions[$group], true)) { + // Add the property to the group. + $this->instructions[$group][] = $property; + } + } + + /** + * Method to remove a processing instruction for an item property. + * + * @param string $group The group to associate the property with. + * @param string $property The property to process. + * + * @return void + * + * @since 2.5 + */ + public function removeInstruction($group, $property) + { + // Check if the group exists. We can't remove instructions for unknown groups. + if (array_key_exists($group, $this->instructions)) { + // Search for the property in the group. + $key = array_search($property, $this->instructions[$group]); + + // If the property was found, remove it. + if ($key !== false) { + unset($this->instructions[$group][$key]); + } + } + } + + /** + * Method to get the taxonomy maps for an item. + * + * @param string $branch The taxonomy branch to get. [optional] + * + * @return array An array of taxonomy maps. + * + * @since 2.5 + */ + public function getTaxonomy($branch = null) + { + // Get the taxonomy branch if available. + if ($branch !== null && isset($this->taxonomy[$branch])) { + return $this->taxonomy[$branch]; + } + + return $this->taxonomy; + } + + /** + * Method to add a taxonomy map for an item. + * + * @param string $branch The title of the taxonomy branch to add the node to. + * @param string $title The title of the taxonomy node. + * @param integer $state The published state of the taxonomy node. [optional] + * @param integer $access The access level of the taxonomy node. [optional] + * @param string $language The language of the taxonomy. [optional] + * + * @return void + * + * @since 2.5 + */ + public function addTaxonomy($branch, $title, $state = 1, $access = 1, $language = '') + { + // Filter the input. + $branch = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,_]+#mui', ' ', $branch); + + // Create the taxonomy node. + $node = new \stdClass(); + $node->title = $title; + $node->state = (int) $state; + $node->access = (int) $access; + $node->language = $language; + $node->nested = false; + + // Add the node to the taxonomy branch. + $this->taxonomy[$branch][] = $node; + } + + /** + * Method to add a nested taxonomy map for an item. + * + * @param string $branch The title of the taxonomy branch to add the node to. + * @param ImmutableNodeInterface $contentNode The node object. + * @param integer $state The published state of the taxonomy node. [optional] + * @param integer $access The access level of the taxonomy node. [optional] + * @param string $language The language of the taxonomy. [optional] + * + * @return void + * + * @since 4.0.0 + */ + public function addNestedTaxonomy($branch, ImmutableNodeInterface $contentNode, $state = 1, $access = 1, $language = '') + { + // Filter the input. + $branch = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,_]+#mui', ' ', $branch); + + // Create the taxonomy node. + $node = new \stdClass(); + $node->title = $contentNode->title; + $node->state = (int) $state; + $node->access = (int) $access; + $node->language = $language; + $node->nested = true; + $node->node = $contentNode; + + // Add the node to the taxonomy branch. + $this->taxonomy[$branch][] = $node; + } + + /** + * Method to set the item language + * + * @return void + * + * @since 3.0 + */ + public function setLanguage() + { + if ($this->language == '') { + $this->language = $this->defaultLanguage; + } + } + + /** + * Helper function to serialise the data of a Result object + * + * @return string The serialised data + * + * @since 4.0.0 + */ + public function serialize() + { + return serialize($this->__serialize()); + } + + /** + * Helper function to unserialise the data for this object + * + * @param string $serialized Serialised data to unserialise + * + * @return void + * + * @since 4.0.0 + */ + public function unserialize($serialized): void + { + $this->__unserialize(unserialize($serialized)); + } + + /** + * Magic method used for serializing. + * + * @since 4.1.3 + */ + public function __serialize(): array + { + $taxonomy = []; + + foreach ($this->taxonomy as $branch => $nodes) { + $taxonomy[$branch] = []; + + foreach ($nodes as $node) { + if ($node->nested) { + $n = clone $node; + unset($n->node); + $taxonomy[$branch][] = $n; + } else { + $taxonomy[$branch][] = $node; + } + } + } + + // This order must match EXACTLY the order of the $properties in the self::__unserialize method + return [ + $this->access, + $this->defaultLanguage, + $this->description, + $this->elements, + $this->end_date, + $this->instructions, + $this->language, + $this->list_price, + $this->publish_end_date, + $this->publish_start_date, + $this->published, + $this->route, + $this->sale_price, + $this->start_date, + $this->state, + $taxonomy, + $this->title, + $this->type_id, + $this->url + ]; + } + + /** + * Magic method used for unserializing. + * + * @since 4.1.3 + */ + public function __unserialize(array $serialized): void + { + // This order must match EXACTLY the order of the array in the self::__serialize method + $properties = [ + 'access', + 'defaultLanguage', + 'description', + 'elements', + 'end_date', + 'instructions', + 'language', + 'list_price', + 'publish_end_date', + 'publish_start_date', + 'published', + 'route', + 'sale_price', + 'start_date', + 'state', + 'taxonomy', + 'title', + 'type_id', + 'url', + ]; + + foreach ($properties as $k => $v) { + $this->$v = $serialized[$k]; + } + + foreach ($this->taxonomy as $nodes) { + foreach ($nodes as $node) { + $curTaxonomy = Taxonomy::getTaxonomy($node->id); + $node->state = $curTaxonomy->state; + $node->access = $curTaxonomy->access; + } + } + } } diff --git a/administrator/components/com_finder/src/Indexer/Taxonomy.php b/administrator/components/com_finder/src/Indexer/Taxonomy.php index bbc76c123b5bb..c50bf8a505bf7 100644 --- a/administrator/components/com_finder/src/Indexer/Taxonomy.php +++ b/administrator/components/com_finder/src/Indexer/Taxonomy.php @@ -1,4 +1,5 @@ title = $title; - $node->state = $state; - $node->access = $access; - $node->parent_id = 1; - $node->language = ''; - - return self::storeNode($node, 1); - } - - /** - * Method to add a node to the taxonomy tree. - * - * @param string $branch The title of the branch to store the node in. - * @param string $title The title of the node. - * @param integer $state The published state of the node. [optional] - * @param integer $access The access state of the node. [optional] - * @param string $language The language of the node. [optional] - * - * @return integer The id of the node. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function addNode($branch, $title, $state = 1, $access = 1, $language = '') - { - // Get the branch id, insert it if it does not exist. - $branchId = static::addBranch($branch); - - $node = new \stdClass; - $node->title = $title; - $node->state = $state; - $node->access = $access; - $node->parent_id = $branchId; - $node->language = $language; - - return self::storeNode($node, $branchId); - } - - /** - * Method to add a nested node to the taxonomy tree. - * - * @param string $branch The title of the branch to store the node in. - * @param NodeInterface $node The source-node of the taxonomy node. - * @param integer $state The published state of the node. [optional] - * @param integer $access The access state of the node. [optional] - * @param string $language The language of the node. [optional] - * @param integer $branchId ID of a branch if known. [optional] - * - * @return integer The id of the node. - * - * @since 4.0.0 - */ - public static function addNestedNode($branch, NodeInterface $node, $state = 1, $access = 1, $language = '', $branchId = null) - { - if (!$branchId) - { - // Get the branch id, insert it if it does not exist. - $branchId = static::addBranch($branch); - } - - $parent = $node->getParent(); - - if ($parent && $parent->title != 'ROOT') - { - $parentId = self::addNestedNode($branch, $parent, $state, $access, $language, $branchId); - } - else - { - $parentId = $branchId; - } - - $temp = new \stdClass; - $temp->title = $node->title; - $temp->state = $state; - $temp->access = $access; - $temp->parent_id = $parentId; - $temp->language = $language; - - return self::storeNode($temp, $parentId); - } - - /** - * A helper method to store a node in the taxonomy - * - * @param object $node The node data to include - * @param integer $parentId The parent id of the node to add. - * - * @return integer The id of the inserted node. - * - * @since 4.0.0 - * @throws \RuntimeException - */ - protected static function storeNode($node, $parentId) - { - // Check to see if the node is in the cache. - if (isset(static::$nodes[$parentId . ':' . $node->title])) - { - return static::$nodes[$parentId . ':' . $node->title]->id; - } - - // Check to see if the node is in the table. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' = ' . $db->quote($parentId)) - ->where($db->quoteName('title') . ' = ' . $db->quote($node->title)) - ->where($db->quoteName('language') . ' = ' . $db->quote($node->language)); - - $db->setQuery($query); - - // Get the result. - $result = $db->loadObject(); - - // Check if the database matches the input data. - if ((bool) $result && $result->state == $node->state && $result->access == $node->access) - { - // The data matches, add the item to the cache. - static::$nodes[$parentId . ':' . $node->title] = $result; - - return static::$nodes[$parentId . ':' . $node->title]->id; - } - - /* - * The database did not match the input. This could be because the - * state has changed or because the node does not exist. Let's figure - * out which case is true and deal with it. - * @todo: use factory? - */ - $nodeTable = new MapTable($db); - - if (empty($result)) - { - // Prepare the node object. - $nodeTable->title = $node->title; - $nodeTable->state = (int) $node->state; - $nodeTable->access = (int) $node->access; - $nodeTable->language = $node->language; - $nodeTable->setLocation((int) $parentId, 'last-child'); - } - else - { - // Prepare the node object. - $nodeTable->id = (int) $result->id; - $nodeTable->title = $result->title; - $nodeTable->state = (int) ($node->state > 0 ? $node->state : $result->state); - $nodeTable->access = (int) $result->access; - $nodeTable->language = $node->language; - $nodeTable->setLocation($result->parent_id, 'last-child'); - } - - // Check the data. - if (!$nodeTable->check()) - { - $error = $nodeTable->getError(); - - if ($error instanceof \Exception) - { - // \Joomla\CMS\Table\NestedTable sets errors of exceptions, so in this case we can pass on more - // information - throw new \RuntimeException( - $error->getMessage(), - $error->getCode(), - $error - ); - } - - // Standard string returned. Probably from the \Joomla\CMS\Table\Table class - throw new \RuntimeException($error, 500); - } - - // Store the data. - if (!$nodeTable->store()) - { - $error = $nodeTable->getError(); - - if ($error instanceof \Exception) - { - // \Joomla\CMS\Table\NestedTable sets errors of exceptions, so in this case we can pass on more - // information - throw new \RuntimeException( - $error->getMessage(), - $error->getCode(), - $error - ); - } - - // Standard string returned. Probably from the \Joomla\CMS\Table\Table class - throw new \RuntimeException($error, 500); - } - - $nodeTable->rebuildPath($nodeTable->id); - - // Add the node to the cache. - static::$nodes[$parentId . ':' . $nodeTable->title] = (object) $nodeTable->getProperties(); - - return static::$nodes[$parentId . ':' . $nodeTable->title]->id; - } - - /** - * Method to add a map entry between a link and a taxonomy node. - * - * @param integer $linkId The link to map to. - * @param integer $nodeId The node to map to. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function addMap($linkId, $nodeId) - { - // Insert the map. - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select($db->quoteName('link_id')) - ->from($db->quoteName('#__finder_taxonomy_map')) - ->where($db->quoteName('link_id') . ' = ' . (int) $linkId) - ->where($db->quoteName('node_id') . ' = ' . (int) $nodeId); - $db->setQuery($query); - $db->execute(); - $id = (int) $db->loadResult(); - - if (!$id) - { - $map = new \stdClass; - $map->link_id = (int) $linkId; - $map->node_id = (int) $nodeId; - $db->insertObject('#__finder_taxonomy_map', $map); - } - - return true; - } - - /** - * Method to get the title of all taxonomy branches. - * - * @return array An array of branch titles. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function getBranchTitles() - { - $db = Factory::getDbo(); - - // Set user variables - $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); - - // Create a query to get the taxonomy branch titles. - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' = 1') - ->where($db->quoteName('state') . ' = 1') - ->where($db->quoteName('access') . ' IN (' . $groups . ')'); - - // Get the branch titles. - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Method to find a taxonomy node in a branch. - * - * @param string $branch The branch to search. - * @param string $title The title of the node. - * - * @return mixed Integer id on success, null on no match. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function getNodeByTitle($branch, $title) - { - $db = Factory::getDbo(); - - // Set user variables - $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); - - // Create a query to get the node. - $query = $db->getQuery(true) - ->select('t1.*') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') - ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') - ->where('t1.access IN (' . $groups . ')') - ->where('t1.state = 1') - ->where('t1.title LIKE ' . $db->quote($db->escape($title) . '%')) - ->where('t2.access IN (' . $groups . ')') - ->where('t2.state = 1') - ->where('t2.title = ' . $db->quote($branch)); - - // Get the node. - $query->setLimit(1); - $db->setQuery($query); - - return $db->loadObject(); - } - - /** - * Method to remove map entries for a link. - * - * @param integer $linkId The link to remove. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function removeMaps($linkId) - { - // Delete the maps. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__finder_taxonomy_map')) - ->where($db->quoteName('link_id') . ' = ' . (int) $linkId); - $db->setQuery($query); - $db->execute(); - - return true; - } - - /** - * Method to remove orphaned taxonomy maps - * - * @return integer The number of deleted rows. - * - * @since 4.2.0 - * @throws \RuntimeException on database error. - */ - public static function removeOrphanMaps() - { - // Delete all orphaned maps - $db = Factory::getDbo(); - $query2 = $db->getQuery(true) - ->select($db->quoteName('link_id')) - ->from($db->quoteName('#__finder_links')); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__finder_taxonomy_map')) - ->where($db->quoteName('link_id') . ' NOT IN (' . $query2 . ')'); - $db->setQuery($query); - $db->execute(); - $count = $db->getAffectedRows(); - - return $count; - } - - /** - * Method to remove orphaned taxonomy nodes and branches. - * - * @return integer The number of deleted rows. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function removeOrphanNodes() - { - // Delete all orphaned nodes. - $affectedRows = 0; - $db = Factory::getDbo(); - $nodeTable = new MapTable($db); - $query = $db->getQuery(true); - - $query->select($db->quoteName('t.id')) - ->from($db->quoteName('#__finder_taxonomy', 't')) - ->join('LEFT', $db->quoteName('#__finder_taxonomy_map', 'm') . ' ON ' . $db->quoteName('m.node_id') . '=' . $db->quoteName('t.id')) - ->where($db->quoteName('t.parent_id') . ' > 1 ') - ->where('t.lft + 1 = t.rgt') - ->where($db->quoteName('m.link_id') . ' IS NULL'); - - do - { - $db->setQuery($query); - $nodes = $db->loadColumn(); - - foreach ($nodes as $node) - { - $nodeTable->delete($node); - $affectedRows++; - } - } - while ($nodes); - - return $affectedRows; - } - - /** - * Get a taxonomy based on its id or all taxonomies - * - * @param integer $id Id of the taxonomy - * - * @return object|array A taxonomy object or an array of all taxonomies - * - * @since 4.0.0 - */ - public static function getTaxonomy($id = 0) - { - if (!count(self::$taxonomies)) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - $query->select(array('id','parent_id','lft','rgt','level','path','title','alias','state','access','language')) - ->from($db->quoteName('#__finder_taxonomy')) - ->order($db->quoteName('lft')); - - $db->setQuery($query); - self::$taxonomies = $db->loadObjectList('id'); - } - - if ($id == 0) - { - return self::$taxonomies; - } - - if (isset(self::$taxonomies[$id])) - { - return self::$taxonomies[$id]; - } - - return false; - } - - /** - * Get a taxonomy branch object based on its title or all branches - * - * @param string $title Title of the branch - * - * @return object|array The object with the branch data or an array of all branches - * - * @since 4.0.0 - */ - public static function getBranch($title = '') - { - if (!count(self::$branches)) - { - $taxonomies = self::getTaxonomy(); - - foreach ($taxonomies as $t) - { - if ($t->level == 1) - { - self::$branches[$t->title] = $t; - } - } - } - - if ($title == '') - { - return self::$branches; - } - - if (isset(self::$branches[$title])) - { - return self::$branches[$title]; - } - - return false; - } + /** + * An internal cache of taxonomy data. + * + * @var object[] + * @since 4.0.0 + */ + public static $taxonomies = array(); + + /** + * An internal cache of branch data. + * + * @var object[] + * @since 4.0.0 + */ + public static $branches = array(); + + /** + * An internal cache of taxonomy node data for inserting it. + * + * @var object[] + * @since 2.5 + */ + public static $nodes = array(); + + /** + * Method to add a branch to the taxonomy tree. + * + * @param string $title The title of the branch. + * @param integer $state The published state of the branch. [optional] + * @param integer $access The access state of the branch. [optional] + * + * @return integer The id of the branch. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function addBranch($title, $state = 1, $access = 1) + { + $node = new \stdClass(); + $node->title = $title; + $node->state = $state; + $node->access = $access; + $node->parent_id = 1; + $node->language = ''; + + return self::storeNode($node, 1); + } + + /** + * Method to add a node to the taxonomy tree. + * + * @param string $branch The title of the branch to store the node in. + * @param string $title The title of the node. + * @param integer $state The published state of the node. [optional] + * @param integer $access The access state of the node. [optional] + * @param string $language The language of the node. [optional] + * + * @return integer The id of the node. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function addNode($branch, $title, $state = 1, $access = 1, $language = '') + { + // Get the branch id, insert it if it does not exist. + $branchId = static::addBranch($branch); + + $node = new \stdClass(); + $node->title = $title; + $node->state = $state; + $node->access = $access; + $node->parent_id = $branchId; + $node->language = $language; + + return self::storeNode($node, $branchId); + } + + /** + * Method to add a nested node to the taxonomy tree. + * + * @param string $branch The title of the branch to store the node in. + * @param NodeInterface $node The source-node of the taxonomy node. + * @param integer $state The published state of the node. [optional] + * @param integer $access The access state of the node. [optional] + * @param string $language The language of the node. [optional] + * @param integer $branchId ID of a branch if known. [optional] + * + * @return integer The id of the node. + * + * @since 4.0.0 + */ + public static function addNestedNode($branch, NodeInterface $node, $state = 1, $access = 1, $language = '', $branchId = null) + { + if (!$branchId) { + // Get the branch id, insert it if it does not exist. + $branchId = static::addBranch($branch); + } + + $parent = $node->getParent(); + + if ($parent && $parent->title != 'ROOT') { + $parentId = self::addNestedNode($branch, $parent, $state, $access, $language, $branchId); + } else { + $parentId = $branchId; + } + + $temp = new \stdClass(); + $temp->title = $node->title; + $temp->state = $state; + $temp->access = $access; + $temp->parent_id = $parentId; + $temp->language = $language; + + return self::storeNode($temp, $parentId); + } + + /** + * A helper method to store a node in the taxonomy + * + * @param object $node The node data to include + * @param integer $parentId The parent id of the node to add. + * + * @return integer The id of the inserted node. + * + * @since 4.0.0 + * @throws \RuntimeException + */ + protected static function storeNode($node, $parentId) + { + // Check to see if the node is in the cache. + if (isset(static::$nodes[$parentId . ':' . $node->title])) { + return static::$nodes[$parentId . ':' . $node->title]->id; + } + + // Check to see if the node is in the table. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' = ' . $db->quote($parentId)) + ->where($db->quoteName('title') . ' = ' . $db->quote($node->title)) + ->where($db->quoteName('language') . ' = ' . $db->quote($node->language)); + + $db->setQuery($query); + + // Get the result. + $result = $db->loadObject(); + + // Check if the database matches the input data. + if ((bool) $result && $result->state == $node->state && $result->access == $node->access) { + // The data matches, add the item to the cache. + static::$nodes[$parentId . ':' . $node->title] = $result; + + return static::$nodes[$parentId . ':' . $node->title]->id; + } + + /* + * The database did not match the input. This could be because the + * state has changed or because the node does not exist. Let's figure + * out which case is true and deal with it. + * @todo: use factory? + */ + $nodeTable = new MapTable($db); + + if (empty($result)) { + // Prepare the node object. + $nodeTable->title = $node->title; + $nodeTable->state = (int) $node->state; + $nodeTable->access = (int) $node->access; + $nodeTable->language = $node->language; + $nodeTable->setLocation((int) $parentId, 'last-child'); + } else { + // Prepare the node object. + $nodeTable->id = (int) $result->id; + $nodeTable->title = $result->title; + $nodeTable->state = (int) ($node->state > 0 ? $node->state : $result->state); + $nodeTable->access = (int) $result->access; + $nodeTable->language = $node->language; + $nodeTable->setLocation($result->parent_id, 'last-child'); + } + + // Check the data. + if (!$nodeTable->check()) { + $error = $nodeTable->getError(); + + if ($error instanceof \Exception) { + // \Joomla\CMS\Table\NestedTable sets errors of exceptions, so in this case we can pass on more + // information + throw new \RuntimeException( + $error->getMessage(), + $error->getCode(), + $error + ); + } + + // Standard string returned. Probably from the \Joomla\CMS\Table\Table class + throw new \RuntimeException($error, 500); + } + + // Store the data. + if (!$nodeTable->store()) { + $error = $nodeTable->getError(); + + if ($error instanceof \Exception) { + // \Joomla\CMS\Table\NestedTable sets errors of exceptions, so in this case we can pass on more + // information + throw new \RuntimeException( + $error->getMessage(), + $error->getCode(), + $error + ); + } + + // Standard string returned. Probably from the \Joomla\CMS\Table\Table class + throw new \RuntimeException($error, 500); + } + + $nodeTable->rebuildPath($nodeTable->id); + + // Add the node to the cache. + static::$nodes[$parentId . ':' . $nodeTable->title] = (object) $nodeTable->getProperties(); + + return static::$nodes[$parentId . ':' . $nodeTable->title]->id; + } + + /** + * Method to add a map entry between a link and a taxonomy node. + * + * @param integer $linkId The link to map to. + * @param integer $nodeId The node to map to. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function addMap($linkId, $nodeId) + { + // Insert the map. + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select($db->quoteName('link_id')) + ->from($db->quoteName('#__finder_taxonomy_map')) + ->where($db->quoteName('link_id') . ' = ' . (int) $linkId) + ->where($db->quoteName('node_id') . ' = ' . (int) $nodeId); + $db->setQuery($query); + $db->execute(); + $id = (int) $db->loadResult(); + + if (!$id) { + $map = new \stdClass(); + $map->link_id = (int) $linkId; + $map->node_id = (int) $nodeId; + $db->insertObject('#__finder_taxonomy_map', $map); + } + + return true; + } + + /** + * Method to get the title of all taxonomy branches. + * + * @return array An array of branch titles. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function getBranchTitles() + { + $db = Factory::getDbo(); + + // Set user variables + $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); + + // Create a query to get the taxonomy branch titles. + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' = 1') + ->where($db->quoteName('state') . ' = 1') + ->where($db->quoteName('access') . ' IN (' . $groups . ')'); + + // Get the branch titles. + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Method to find a taxonomy node in a branch. + * + * @param string $branch The branch to search. + * @param string $title The title of the node. + * + * @return mixed Integer id on success, null on no match. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function getNodeByTitle($branch, $title) + { + $db = Factory::getDbo(); + + // Set user variables + $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); + + // Create a query to get the node. + $query = $db->getQuery(true) + ->select('t1.*') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') + ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') + ->where('t1.access IN (' . $groups . ')') + ->where('t1.state = 1') + ->where('t1.title LIKE ' . $db->quote($db->escape($title) . '%')) + ->where('t2.access IN (' . $groups . ')') + ->where('t2.state = 1') + ->where('t2.title = ' . $db->quote($branch)); + + // Get the node. + $query->setLimit(1); + $db->setQuery($query); + + return $db->loadObject(); + } + + /** + * Method to remove map entries for a link. + * + * @param integer $linkId The link to remove. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function removeMaps($linkId) + { + // Delete the maps. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__finder_taxonomy_map')) + ->where($db->quoteName('link_id') . ' = ' . (int) $linkId); + $db->setQuery($query); + $db->execute(); + + return true; + } + + /** + * Method to remove orphaned taxonomy maps + * + * @return integer The number of deleted rows. + * + * @since 4.2.0 + * @throws \RuntimeException on database error. + */ + public static function removeOrphanMaps() + { + // Delete all orphaned maps + $db = Factory::getDbo(); + $query2 = $db->getQuery(true) + ->select($db->quoteName('link_id')) + ->from($db->quoteName('#__finder_links')); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__finder_taxonomy_map')) + ->where($db->quoteName('link_id') . ' NOT IN (' . $query2 . ')'); + $db->setQuery($query); + $db->execute(); + $count = $db->getAffectedRows(); + + return $count; + } + + /** + * Method to remove orphaned taxonomy nodes and branches. + * + * @return integer The number of deleted rows. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function removeOrphanNodes() + { + // Delete all orphaned nodes. + $affectedRows = 0; + $db = Factory::getDbo(); + $nodeTable = new MapTable($db); + $query = $db->getQuery(true); + + $query->select($db->quoteName('t.id')) + ->from($db->quoteName('#__finder_taxonomy', 't')) + ->join('LEFT', $db->quoteName('#__finder_taxonomy_map', 'm') . ' ON ' . $db->quoteName('m.node_id') . '=' . $db->quoteName('t.id')) + ->where($db->quoteName('t.parent_id') . ' > 1 ') + ->where('t.lft + 1 = t.rgt') + ->where($db->quoteName('m.link_id') . ' IS NULL'); + + do { + $db->setQuery($query); + $nodes = $db->loadColumn(); + + foreach ($nodes as $node) { + $nodeTable->delete($node); + $affectedRows++; + } + } while ($nodes); + + return $affectedRows; + } + + /** + * Get a taxonomy based on its id or all taxonomies + * + * @param integer $id Id of the taxonomy + * + * @return object|array A taxonomy object or an array of all taxonomies + * + * @since 4.0.0 + */ + public static function getTaxonomy($id = 0) + { + if (!count(self::$taxonomies)) { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->select(array('id','parent_id','lft','rgt','level','path','title','alias','state','access','language')) + ->from($db->quoteName('#__finder_taxonomy')) + ->order($db->quoteName('lft')); + + $db->setQuery($query); + self::$taxonomies = $db->loadObjectList('id'); + } + + if ($id == 0) { + return self::$taxonomies; + } + + if (isset(self::$taxonomies[$id])) { + return self::$taxonomies[$id]; + } + + return false; + } + + /** + * Get a taxonomy branch object based on its title or all branches + * + * @param string $title Title of the branch + * + * @return object|array The object with the branch data or an array of all branches + * + * @since 4.0.0 + */ + public static function getBranch($title = '') + { + if (!count(self::$branches)) { + $taxonomies = self::getTaxonomy(); + + foreach ($taxonomies as $t) { + if ($t->level == 1) { + self::$branches[$t->title] = $t; + } + } + } + + if ($title == '') { + return self::$branches; + } + + if (isset(self::$branches[$title])) { + return self::$branches[$title]; + } + + return false; + } } diff --git a/administrator/components/com_finder/src/Indexer/Token.php b/administrator/components/com_finder/src/Indexer/Token.php index d52d9d18d8f53..f769af45c4cfe 100644 --- a/administrator/components/com_finder/src/Indexer/Token.php +++ b/administrator/components/com_finder/src/Indexer/Token.php @@ -1,4 +1,5 @@ language = '*'; - } - else - { - $this->language = $lang; - } - - // Tokens can be a single word or an array of words representing a phrase. - if (is_array($term)) - { - // Populate the token instance. - $this->term = implode($spacer, $term); - $this->stem = implode($spacer, array_map(array(Helper::class, 'stem'), $term, array($lang))); - $this->numeric = false; - $this->common = false; - $this->phrase = true; - $this->length = StringHelper::strlen($this->term); - - /* - * Calculate the weight of the token. - * - * 1. Length of the token up to 30 and divide by 30, add 1. - * 2. Round weight to 4 decimal points. - */ - $this->weight = (($this->length >= 30 ? 30 : $this->length) / 30) + 1; - $this->weight = round($this->weight, 4); - } - else - { - // Populate the token instance. - $this->term = $term; - $this->stem = Helper::stem($this->term, $lang); - $this->numeric = (is_numeric($this->term) || (bool) preg_match('#^[0-9,.\-\+]+$#', $this->term)); - $this->common = $this->numeric ? false : Helper::isCommon($this->term, $lang); - $this->phrase = false; - $this->length = StringHelper::strlen($this->term); - - /* - * Calculate the weight of the token. - * - * 1. Length of the token up to 15 and divide by 15. - * 2. If common term, divide weight by 8. - * 3. If numeric, multiply weight by 1.5. - * 4. Round weight to 4 decimal points. - */ - $this->weight = ($this->length >= 15 ? 15 : $this->length) / 15; - $this->weight = $this->common === true ? $this->weight / 8 : $this->weight; - $this->weight = $this->numeric === true ? $this->weight * 1.5 : $this->weight; - $this->weight = round($this->weight, 4); - } - } + /** + * This is the term that will be referenced in the terms table and the + * mapping tables. + * + * @var string + * @since 2.5 + */ + public $term; + + /** + * The stem is used to match the root term and produce more potential + * matches when searching the index. + * + * @var string + * @since 2.5 + */ + public $stem; + + /** + * If the token is numeric, it is likely to be short and uncommon so the + * weight is adjusted to compensate for that situation. + * + * @var boolean + * @since 2.5 + */ + public $numeric; + + /** + * If the token is a common term, the weight is adjusted to compensate for + * the higher frequency of the term in relation to other terms. + * + * @var boolean + * @since 2.5 + */ + public $common; + + /** + * Flag for phrase tokens. + * + * @var boolean + * @since 2.5 + */ + public $phrase; + + /** + * The length is used to calculate the weight of the token. + * + * @var integer + * @since 2.5 + */ + public $length; + + /** + * The weight is calculated based on token size and whether the token is + * considered a common term. + * + * @var integer + * @since 2.5 + */ + public $weight; + + /** + * The simple language identifier for the token. + * + * @var string + * @since 2.5 + */ + public $language; + + /** + * The container for matches. + * + * @var array + * @since 3.8.12 + */ + public $matches = array(); + + /** + * Is derived token (from individual words) + * + * @var boolean + * @since 3.8.12 + */ + public $derived; + + /** + * The suggested term + * + * @var string + * @since 3.8.12 + */ + public $suggestion; + + /** + * Method to construct the token object. + * + * @param mixed $term The term as a string for words or an array for phrases. + * @param string $lang The simple language identifier. + * @param string $spacer The space separator for phrases. [optional] + * + * @since 2.5 + */ + public function __construct($term, $lang, $spacer = ' ') + { + if (!$lang) { + $this->language = '*'; + } else { + $this->language = $lang; + } + + // Tokens can be a single word or an array of words representing a phrase. + if (is_array($term)) { + // Populate the token instance. + $this->term = implode($spacer, $term); + $this->stem = implode($spacer, array_map(array(Helper::class, 'stem'), $term, array($lang))); + $this->numeric = false; + $this->common = false; + $this->phrase = true; + $this->length = StringHelper::strlen($this->term); + + /* + * Calculate the weight of the token. + * + * 1. Length of the token up to 30 and divide by 30, add 1. + * 2. Round weight to 4 decimal points. + */ + $this->weight = (($this->length >= 30 ? 30 : $this->length) / 30) + 1; + $this->weight = round($this->weight, 4); + } else { + // Populate the token instance. + $this->term = $term; + $this->stem = Helper::stem($this->term, $lang); + $this->numeric = (is_numeric($this->term) || (bool) preg_match('#^[0-9,.\-\+]+$#', $this->term)); + $this->common = $this->numeric ? false : Helper::isCommon($this->term, $lang); + $this->phrase = false; + $this->length = StringHelper::strlen($this->term); + + /* + * Calculate the weight of the token. + * + * 1. Length of the token up to 15 and divide by 15. + * 2. If common term, divide weight by 8. + * 3. If numeric, multiply weight by 1.5. + * 4. Round weight to 4 decimal points. + */ + $this->weight = ($this->length >= 15 ? 15 : $this->length) / 15; + $this->weight = $this->common === true ? $this->weight / 8 : $this->weight; + $this->weight = $this->numeric === true ? $this->weight * 1.5 : $this->weight; + $this->weight = round($this->weight, 4); + } + } } diff --git a/administrator/components/com_finder/src/Model/FilterModel.php b/administrator/components/com_finder/src/Model/FilterModel.php index 1c1111923b319..7db8dc06f8cbc 100644 --- a/administrator/components/com_finder/src/Model/FilterModel.php +++ b/administrator/components/com_finder/src/Model/FilterModel.php @@ -1,4 +1,5 @@ getState('filter.id'); - - // Get a FinderTableFilter instance. - $filter = $this->getTable(); - - // Attempt to load the row. - $return = $filter->load($filter_id); - - // Check for a database error. - if ($return === false && $filter->getError()) - { - $this->setError($filter->getError()); - - return false; - } - - // Process the filter data. - if (!empty($filter->data)) - { - $filter->data = explode(',', $filter->data); - } - elseif (empty($filter->data)) - { - $filter->data = array(); - } - - return $filter; - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. [optional] - * @param boolean $loadData True if the form is to load its own data (default case), false if not. [optional] - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 2.5 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_finder.filter', 'filter', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 2.5 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_finder.edit.filter.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_finder.filter', $data); - - return $data; - } - - /** - * Method to get the total indexed items - * - * @return number the number of indexed items - * - * @since 3.5 - */ - public function getTotal() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('MAX(link_id)') - ->from('#__finder_links'); - - return $db->setQuery($query)->loadResult(); - } + /** + * The prefix to use with controller messages. + * + * @var string + * @since 2.5 + */ + protected $text_prefix = 'COM_FINDER'; + + /** + * Model context string. + * + * @var string + * @since 2.5 + */ + protected $context = 'com_finder.filter'; + + /** + * Custom clean cache method. + * + * @param string $group The component name. [optional] + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 2.5 + */ + protected function cleanCache($group = 'com_finder', $clientId = 0) + { + parent::cleanCache($group); + } + + /** + * Method to get the filter data. + * + * @return FilterTable|boolean The filter data or false on a failure. + * + * @since 2.5 + */ + public function getFilter() + { + $filter_id = (int) $this->getState('filter.id'); + + // Get a FinderTableFilter instance. + $filter = $this->getTable(); + + // Attempt to load the row. + $return = $filter->load($filter_id); + + // Check for a database error. + if ($return === false && $filter->getError()) { + $this->setError($filter->getError()); + + return false; + } + + // Process the filter data. + if (!empty($filter->data)) { + $filter->data = explode(',', $filter->data); + } elseif (empty($filter->data)) { + $filter->data = array(); + } + + return $filter; + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. [optional] + * @param boolean $loadData True if the form is to load its own data (default case), false if not. [optional] + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 2.5 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_finder.filter', 'filter', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 2.5 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_finder.edit.filter.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_finder.filter', $data); + + return $data; + } + + /** + * Method to get the total indexed items + * + * @return number the number of indexed items + * + * @since 3.5 + */ + public function getTotal() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('MAX(link_id)') + ->from('#__finder_links'); + + return $db->setQuery($query)->loadResult(); + } } diff --git a/administrator/components/com_finder/src/Model/FiltersModel.php b/administrator/components/com_finder/src/Model/FiltersModel.php index 619910d57fd2d..3156e137a7926 100644 --- a/administrator/components/com_finder/src/Model/FiltersModel.php +++ b/administrator/components/com_finder/src/Model/FiltersModel.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true); - - // Select all fields from the table. - $query->select('a.*') - ->from($db->quoteName('#__finder_filters', 'a')); - - // Join over the users for the checked out user. - $query->select($db->quoteName('uc.name', 'editor')) - ->join('LEFT', $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Join over the users for the author. - $query->select($db->quoteName('ua.name', 'user_name')) - ->join('LEFT', $db->quoteName('#__users', 'ua') . ' ON ' . $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')); - - // Check for a search filter. - if ($search = $this->getState('filter.search')) - { - $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); - $query->where($db->quoteName('a.title') . ' LIKE ' . $search); - } - - // If the model is set to check item state, add to the query. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $query->where($db->quoteName('a.state') . ' = ' . (int) $state); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.title') . ' ' . $db->escape($this->getState('list.direction', 'ASC')))); - - return $query; - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. [optional] - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. [optional] - * @param string $direction An optional direction. [optional] - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = 'a.title', $direction = 'asc') - { - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_finder'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.7 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'filter_id', 'a.filter_id', + 'title', 'a.title', + 'state', 'a.state', + 'created_by_alias', 'a.created_by_alias', + 'created', 'a.created', + 'map_count', 'a.map_count' + ); + } + + parent::__construct($config, $factory); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 2.5 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select all fields from the table. + $query->select('a.*') + ->from($db->quoteName('#__finder_filters', 'a')); + + // Join over the users for the checked out user. + $query->select($db->quoteName('uc.name', 'editor')) + ->join('LEFT', $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Join over the users for the author. + $query->select($db->quoteName('ua.name', 'user_name')) + ->join('LEFT', $db->quoteName('#__users', 'ua') . ' ON ' . $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')); + + // Check for a search filter. + if ($search = $this->getState('filter.search')) { + $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); + $query->where($db->quoteName('a.title') . ' LIKE ' . $search); + } + + // If the model is set to check item state, add to the query. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $query->where($db->quoteName('a.state') . ' = ' . (int) $state); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.title') . ' ' . $db->escape($this->getState('list.direction', 'ASC')))); + + return $query; + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. [optional] + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. [optional] + * @param string $direction An optional direction. [optional] + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = 'a.title', $direction = 'asc') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_finder'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } } diff --git a/administrator/components/com_finder/src/Model/IndexModel.php b/administrator/components/com_finder/src/Model/IndexModel.php index 752bd982e7a1d..6f3b9b1c6ee9c 100644 --- a/administrator/components/com_finder/src/Model/IndexModel.php +++ b/administrator/components/com_finder/src/Model/IndexModel.php @@ -1,4 +1,5 @@ authorise('core.delete', $this->option); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. - * - * @since 2.5 - */ - protected function canEditState($record) - { - return Factory::getUser()->authorise('core.edit.state', $this->option); - } - - /** - * Method to delete one or more records. - * - * @param array $pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 2.5 - */ - public function delete(&$pks) - { - $pks = (array) $pks; - $table = $this->getTable(); - - // Include the content plugins for the on delete events. - PluginHelper::importPlugin('content'); - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if ($this->canDelete($table)) - { - $context = $this->option . '.' . $this->name; - - // Trigger the onContentBeforeDelete event. - $result = Factory::getApplication()->triggerEvent($this->event_before_delete, array($context, $table)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - if (!$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the onContentAfterDelete event. - Factory::getApplication()->triggerEvent($this->event_after_delete, array($context, $table)); - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - $error = $this->getError(); - - if ($error) - { - $this->setError($error); - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); - } - } - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 2.5 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('l.*') - ->select($db->quoteName('t.title', 't_title')) - ->from($db->quoteName('#__finder_links', 'l')) - ->join('INNER', $db->quoteName('#__finder_types', 't') . ' ON ' . $db->quoteName('t.id') . ' = ' . $db->quoteName('l.type_id')); - - // Check the type filter. - $type = $this->getState('filter.type'); - - // Join over the language - $query->select('la.title AS language_title, la.image AS language_image') - ->join('LEFT', $db->quoteName('#__languages') . ' AS la ON la.lang_code = l.language'); - - if (is_numeric($type)) - { - $query->where($db->quoteName('l.type_id') . ' = ' . (int) $type); - } - - // Check the map filter. - $contentMapId = $this->getState('filter.content_map'); - - if (is_numeric($contentMapId)) - { - $query->join('INNER', $db->quoteName('#__finder_taxonomy_map', 'm') . ' ON ' . $db->quoteName('m.link_id') . ' = ' . $db->quoteName('l.link_id')) - ->where($db->quoteName('m.node_id') . ' = ' . (int) $contentMapId); - } - - // Check for state filter. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $query->where($db->quoteName('l.published') . ' = ' . (int) $state); - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('l.language') . ' = ' . $db->quote($language)); - } - - // Check the search phrase. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); - $orSearchSql = $db->quoteName('l.title') . ' LIKE ' . $search . ' OR ' . $db->quoteName('l.url') . ' LIKE ' . $search; - - // Filter by indexdate only if $search doesn't contains non-ascii characters - if (!preg_match('/[^\x00-\x7F]/', $search)) - { - $orSearchSql .= ' OR ' . $query->castAsChar($db->quoteName('l.indexdate')) . ' LIKE ' . $search; - } - - $query->where('(' . $orSearchSql . ')'); - } - - // Handle the list ordering. - $listOrder = $this->getState('list.ordering', 'l.title'); - $listDir = $this->getState('list.direction', 'ASC'); - - if ($listOrder === 't.title') - { - $ordering = $db->quoteName('t.title') . ' ' . $db->escape($listDir) . ', ' . $db->quoteName('l.title') . ' ' . $db->escape($listDir); - } - else - { - $ordering = $db->escape($listOrder) . ' ' . $db->escape($listDir); - } - - $query->order($ordering); - - return $query; - } - - /** - * Method to get the state of the Smart Search Plugins. - * - * @return array Array of relevant plugins and whether they are enabled or not. - * - * @since 2.5 - */ - public function getPluginState() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('name, enabled') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' IN (' . $db->quote('system') . ',' . $db->quote('content') . ')') - ->where($db->quoteName('element') . ' = ' . $db->quote('finder')); - $db->setQuery($query); - - return $db->loadObjectList('name'); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. [optional] - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.type'); - $id .= ':' . $this->getState('filter.content_map'); - - return parent::getStoreId($id); - } - - /** - * Gets the total of indexed items. - * - * @return integer The total of indexed items. - * - * @since 3.6.0 - */ - public function getTotalIndexed() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('COUNT(link_id)') - ->from($db->quoteName('#__finder_links')); - $db->setQuery($query); - - return (int) $db->loadResult(); - } - - /** - * Returns a Table object, always creating it. - * - * @param string $type The table type to instantiate. [optional] - * @param string $prefix A prefix for the table class name. [optional] - * @param array $config Configuration array for model. [optional] - * - * @return \Joomla\CMS\Table\Table A database object - * - * @since 2.5 - */ - public function getTable($type = 'Link', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * Method to purge the index, deleting all links. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - * @throws \Exception on database error - */ - public function purge() - { - $db = $this->getDatabase(); - - // Truncate the links table. - $db->truncateTable('#__finder_links'); - - // Truncate the links terms tables. - $db->truncateTable('#__finder_links_terms'); - - // Truncate the terms table. - $db->truncateTable('#__finder_terms'); - - // Truncate the taxonomy map table. - $db->truncateTable('#__finder_taxonomy_map'); - - // Truncate the taxonomy table and insert the root node. - $db->truncateTable('#__finder_taxonomy'); - $root = (object) array( - 'id' => 1, - 'parent_id' => 0, - 'lft' => 0, - 'rgt' => 1, - 'level' => 0, - 'path' => '', - 'title' => 'ROOT', - 'alias' => 'root', - 'state' => 1, - 'access' => 1, - 'language' => '*' - ); - $db->insertObject('#__finder_taxonomy', $root); - - // Truncate the tokens tables. - $db->truncateTable('#__finder_tokens'); - - // Truncate the tokens aggregate table. - $db->truncateTable('#__finder_tokens_aggregate'); - - // Include the finder plugins for the on purge events. - PluginHelper::importPlugin('finder'); - Factory::getApplication()->triggerEvent($this->event_after_purge); - - return true; - } - - /** - * Method to auto-populate the model state. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. [optional] - * @param string $direction An optional direction. [optional] - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = 'l.title', $direction = 'asc') - { - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); - $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'cmd')); - $this->setState('filter.content_map', $this->getUserStateFromRequest($this->context . '.filter.content_map', 'filter_content_map', '', 'cmd')); - $this->setState('filter.language', $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', '')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_finder'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to change the published state of one or more records. - * - * @param array $pks A list of the primary keys to change. - * @param integer $value The value of the published state. [optional] - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function publish(&$pks, $value = 1) - { - $user = Factory::getUser(); - $table = $this->getTable(); - $pks = (array) $pks; - - // Include the content plugins for the change of state event. - PluginHelper::importPlugin('content'); - - // Access checks. - foreach ($pks as $i => $pk) - { - $table->reset(); - - if ($table->load($pk) && !$this->canEditState($table)) - { - // Prune items that you can't change. - unset($pks[$i]); - $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); - - return false; - } - } - - // Attempt to change the state of the records. - if (!$table->publish($pks, $value, $user->get('id'))) - { - $this->setError($table->getError()); - - return false; - } - - $context = $this->option . '.' . $this->name; - - // Trigger the onContentChangeState event. - $result = Factory::getApplication()->triggerEvent('onContentChangeState', array($context, $pks, $value)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } + /** + * The event to trigger after deleting the data. + * + * @var string + * @since 2.5 + */ + protected $event_after_delete = 'onContentAfterDelete'; + + /** + * The event to trigger before deleting the data. + * + * @var string + * @since 2.5 + */ + protected $event_before_delete = 'onContentBeforeDelete'; + + /** + * The event to trigger after purging the data. + * + * @var string + * @since 4.0.0 + */ + protected $event_after_purge = 'onFinderIndexAfterPurge'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.7 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'state', 'published', 'l.published', + 'title', 'l.title', + 'type', 'type_id', 'l.type_id', + 't.title', 't_title', + 'url', 'l.url', + 'language', 'l.language', + 'indexdate', 'l.indexdate', + 'content_map', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 2.5 + */ + protected function canDelete($record) + { + return Factory::getUser()->authorise('core.delete', $this->option); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. + * + * @since 2.5 + */ + protected function canEditState($record) + { + return Factory::getUser()->authorise('core.edit.state', $this->option); + } + + /** + * Method to delete one or more records. + * + * @param array $pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 2.5 + */ + public function delete(&$pks) + { + $pks = (array) $pks; + $table = $this->getTable(); + + // Include the content plugins for the on delete events. + PluginHelper::importPlugin('content'); + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if ($this->canDelete($table)) { + $context = $this->option . '.' . $this->name; + + // Trigger the onContentBeforeDelete event. + $result = Factory::getApplication()->triggerEvent($this->event_before_delete, array($context, $table)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + if (!$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the onContentAfterDelete event. + Factory::getApplication()->triggerEvent($this->event_after_delete, array($context, $table)); + } else { + // Prune items that you can't change. + unset($pks[$i]); + $error = $this->getError(); + + if ($error) { + $this->setError($error); + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); + } + } + } else { + $this->setError($table->getError()); + + return false; + } + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 2.5 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('l.*') + ->select($db->quoteName('t.title', 't_title')) + ->from($db->quoteName('#__finder_links', 'l')) + ->join('INNER', $db->quoteName('#__finder_types', 't') . ' ON ' . $db->quoteName('t.id') . ' = ' . $db->quoteName('l.type_id')); + + // Check the type filter. + $type = $this->getState('filter.type'); + + // Join over the language + $query->select('la.title AS language_title, la.image AS language_image') + ->join('LEFT', $db->quoteName('#__languages') . ' AS la ON la.lang_code = l.language'); + + if (is_numeric($type)) { + $query->where($db->quoteName('l.type_id') . ' = ' . (int) $type); + } + + // Check the map filter. + $contentMapId = $this->getState('filter.content_map'); + + if (is_numeric($contentMapId)) { + $query->join('INNER', $db->quoteName('#__finder_taxonomy_map', 'm') . ' ON ' . $db->quoteName('m.link_id') . ' = ' . $db->quoteName('l.link_id')) + ->where($db->quoteName('m.node_id') . ' = ' . (int) $contentMapId); + } + + // Check for state filter. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $query->where($db->quoteName('l.published') . ' = ' . (int) $state); + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('l.language') . ' = ' . $db->quote($language)); + } + + // Check the search phrase. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); + $orSearchSql = $db->quoteName('l.title') . ' LIKE ' . $search . ' OR ' . $db->quoteName('l.url') . ' LIKE ' . $search; + + // Filter by indexdate only if $search doesn't contains non-ascii characters + if (!preg_match('/[^\x00-\x7F]/', $search)) { + $orSearchSql .= ' OR ' . $query->castAsChar($db->quoteName('l.indexdate')) . ' LIKE ' . $search; + } + + $query->where('(' . $orSearchSql . ')'); + } + + // Handle the list ordering. + $listOrder = $this->getState('list.ordering', 'l.title'); + $listDir = $this->getState('list.direction', 'ASC'); + + if ($listOrder === 't.title') { + $ordering = $db->quoteName('t.title') . ' ' . $db->escape($listDir) . ', ' . $db->quoteName('l.title') . ' ' . $db->escape($listDir); + } else { + $ordering = $db->escape($listOrder) . ' ' . $db->escape($listDir); + } + + $query->order($ordering); + + return $query; + } + + /** + * Method to get the state of the Smart Search Plugins. + * + * @return array Array of relevant plugins and whether they are enabled or not. + * + * @since 2.5 + */ + public function getPluginState() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('name, enabled') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' IN (' . $db->quote('system') . ',' . $db->quote('content') . ')') + ->where($db->quoteName('element') . ' = ' . $db->quote('finder')); + $db->setQuery($query); + + return $db->loadObjectList('name'); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. [optional] + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.type'); + $id .= ':' . $this->getState('filter.content_map'); + + return parent::getStoreId($id); + } + + /** + * Gets the total of indexed items. + * + * @return integer The total of indexed items. + * + * @since 3.6.0 + */ + public function getTotalIndexed() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('COUNT(link_id)') + ->from($db->quoteName('#__finder_links')); + $db->setQuery($query); + + return (int) $db->loadResult(); + } + + /** + * Returns a Table object, always creating it. + * + * @param string $type The table type to instantiate. [optional] + * @param string $prefix A prefix for the table class name. [optional] + * @param array $config Configuration array for model. [optional] + * + * @return \Joomla\CMS\Table\Table A database object + * + * @since 2.5 + */ + public function getTable($type = 'Link', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * Method to purge the index, deleting all links. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + * @throws \Exception on database error + */ + public function purge() + { + $db = $this->getDatabase(); + + // Truncate the links table. + $db->truncateTable('#__finder_links'); + + // Truncate the links terms tables. + $db->truncateTable('#__finder_links_terms'); + + // Truncate the terms table. + $db->truncateTable('#__finder_terms'); + + // Truncate the taxonomy map table. + $db->truncateTable('#__finder_taxonomy_map'); + + // Truncate the taxonomy table and insert the root node. + $db->truncateTable('#__finder_taxonomy'); + $root = (object) array( + 'id' => 1, + 'parent_id' => 0, + 'lft' => 0, + 'rgt' => 1, + 'level' => 0, + 'path' => '', + 'title' => 'ROOT', + 'alias' => 'root', + 'state' => 1, + 'access' => 1, + 'language' => '*' + ); + $db->insertObject('#__finder_taxonomy', $root); + + // Truncate the tokens tables. + $db->truncateTable('#__finder_tokens'); + + // Truncate the tokens aggregate table. + $db->truncateTable('#__finder_tokens_aggregate'); + + // Include the finder plugins for the on purge events. + PluginHelper::importPlugin('finder'); + Factory::getApplication()->triggerEvent($this->event_after_purge); + + return true; + } + + /** + * Method to auto-populate the model state. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. [optional] + * @param string $direction An optional direction. [optional] + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = 'l.title', $direction = 'asc') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); + $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'cmd')); + $this->setState('filter.content_map', $this->getUserStateFromRequest($this->context . '.filter.content_map', 'filter_content_map', '', 'cmd')); + $this->setState('filter.language', $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', '')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_finder'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to change the published state of one or more records. + * + * @param array $pks A list of the primary keys to change. + * @param integer $value The value of the published state. [optional] + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function publish(&$pks, $value = 1) + { + $user = Factory::getUser(); + $table = $this->getTable(); + $pks = (array) $pks; + + // Include the content plugins for the change of state event. + PluginHelper::importPlugin('content'); + + // Access checks. + foreach ($pks as $i => $pk) { + $table->reset(); + + if ($table->load($pk) && !$this->canEditState($table)) { + // Prune items that you can't change. + unset($pks[$i]); + $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); + + return false; + } + } + + // Attempt to change the state of the records. + if (!$table->publish($pks, $value, $user->get('id'))) { + $this->setError($table->getError()); + + return false; + } + + $context = $this->option . '.' . $this->name; + + // Trigger the onContentChangeState event. + $result = Factory::getApplication()->triggerEvent('onContentChangeState', array($context, $pks, $value)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } } diff --git a/administrator/components/com_finder/src/Model/IndexerModel.php b/administrator/components/com_finder/src/Model/IndexerModel.php index 34a9f023259e5..bf5f6a474a76d 100644 --- a/administrator/components/com_finder/src/Model/IndexerModel.php +++ b/administrator/components/com_finder/src/Model/IndexerModel.php @@ -1,4 +1,5 @@ authorise('core.delete', $this->option); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. - * - * @since 2.5 - */ - protected function canEditState($record) - { - return Factory::getUser()->authorise('core.edit.state', $this->option); - } - - /** - * Method to delete one or more records. - * - * @param array $pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 2.5 - */ - public function delete(&$pks) - { - $pks = (array) $pks; - $table = $this->getTable(); - - // Include the content plugins for the on delete events. - PluginHelper::importPlugin('content'); - - // Iterate the items to check if all of them exist. - foreach ($pks as $i => $pk) - { - if (!$table->load($pk)) - { - // Item is not in the table. - $this->setError($table->getError()); - - return false; - } - } - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if ($this->canDelete($table)) - { - $context = $this->option . '.' . $this->name; - - // Trigger the onContentBeforeDelete event. - $result = Factory::getApplication()->triggerEvent('onContentBeforeDelete', array($context, $table)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - if (!$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the onContentAfterDelete event. - Factory::getApplication()->triggerEvent('onContentAfterDelete', array($context, $table)); - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - $error = $this->getError(); - - if ($error) - { - $this->setError($error); - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); - } - } - } - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 2.5 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - - // Select all fields from the table. - $query = $db->getQuery(true) - ->select('a.id, a.parent_id, a.lft, a.rgt, a.level, a.path, a.title, a.alias, a.state, a.access, a.language') - ->from($db->quoteName('#__finder_taxonomy', 'a')) - ->where('a.parent_id != 0'); - - // Join to get the branch title - $query->select([$db->quoteName('b.id', 'branch_id'), $db->quoteName('b.title', 'branch_title')]) - ->leftJoin($db->quoteName('#__finder_taxonomy', 'b') . ' ON b.level = 1 AND b.lft <= a.lft AND a.rgt <= b.rgt'); - - // Join to get the map links. - $stateQuery = $db->getQuery(true) - ->select('m.node_id') - ->select('COUNT(NULLIF(l.published, 0)) AS count_published') - ->select('COUNT(NULLIF(l.published, 1)) AS count_unpublished') - ->from($db->quoteName('#__finder_taxonomy_map', 'm')) - ->leftJoin($db->quoteName('#__finder_links', 'l') . ' ON l.link_id = m.link_id') - ->group('m.node_id'); - - $query->select('COALESCE(s.count_published, 0) AS count_published'); - $query->select('COALESCE(s.count_unpublished, 0) AS count_unpublished'); - $query->leftJoin('(' . $stateQuery . ') AS s ON s.node_id = a.id'); - - // If the model is set to check item state, add to the query. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $query->where('a.state = ' . (int) $state); - } - - // Filter over level. - $level = $this->getState('filter.level'); - - if (is_numeric($level) && (int) $level === 1) - { - $query->where('a.parent_id = 1'); - } - - // Filter the maps over the branch if set. - $branchId = $this->getState('filter.branch'); - - if (is_numeric($branchId)) - { - $query->where('a.parent_id = ' . (int) $branchId); - } - - // Filter the maps over the search string if set. - if ($search = $this->getState('filter.search')) - { - $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); - $query->where('a.title LIKE ' . $search); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'branch_title, a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Returns a record count for the query. - * - * @param \Joomla\Database\DatabaseQuery|string - * - * @return integer Number of rows for query. - * - * @since 3.0 - */ - protected function _getListCount($query) - { - $query = clone $query; - $query->clear('select')->clear('join')->clear('order')->clear('limit')->clear('offset')->select('COUNT(*)'); - - return (int) $this->getDatabase()->setQuery($query)->loadResult(); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. [optional] - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.branch'); - $id .= ':' . $this->getState('filter.level'); - - return parent::getStoreId($id); - } - - /** - * Returns a Table object, always creating it. - * - * @param string $type The table type to instantiate. [optional] - * @param string $prefix A prefix for the table class name. [optional] - * @param array $config Configuration array for model. [optional] - * - * @return \Joomla\CMS\Table\Table A database object - * - * @since 2.5 - */ - public function getTable($type = 'Map', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * Method to auto-populate the model state. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. [optional] - * @param string $direction An optional direction. [optional] - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = 'branch_title, a.lft', $direction = 'ASC') - { - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); - $this->setState('filter.branch', $this->getUserStateFromRequest($this->context . '.filter.branch', 'filter_branch', '', 'cmd')); - $this->setState('filter.level', $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level', '', 'cmd')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_finder'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to change the published state of one or more records. - * - * @param array $pks A list of the primary keys to change. - * @param integer $value The value of the published state. [optional] - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function publish(&$pks, $value = 1) - { - $user = Factory::getUser(); - $table = $this->getTable(); - $pks = (array) $pks; - - // Include the content plugins for the change of state event. - PluginHelper::importPlugin('content'); - - // Access checks. - foreach ($pks as $i => $pk) - { - $table->reset(); - - if ($table->load($pk) && !$this->canEditState($table)) - { - // Prune items that you can't change. - unset($pks[$i]); - $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); - - return false; - } - } - - // Attempt to change the state of the records. - if (!$table->publish($pks, $value, $user->get('id'))) - { - $this->setError($table->getError()); - - return false; - } - - $context = $this->option . '.' . $this->name; - - // Trigger the onContentChangeState event. - $result = Factory::getApplication()->triggerEvent('onContentChangeState', array($context, $pks, $value)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Method to purge all maps from the taxonomy. - * - * @return boolean Returns true on success, false on failure. - * - * @since 2.5 - */ - public function purge() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' > 1'); - $db->setQuery($query); - $db->execute(); - - $query->clear() - ->delete($db->quoteName('#__finder_taxonomy_map')); - $db->setQuery($query); - $db->execute(); - - return true; - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - $title = 'ROOT'; - - $query->where($this->getDatabase()->quoteName('title') . ' <> :title') - ->bind(':title', $title); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.7 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'state', 'a.state', + 'title', 'a.title', + 'branch', + 'branch_title', 'd.branch_title', + 'level', 'd.level', + 'language', 'a.language', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 2.5 + */ + protected function canDelete($record) + { + return Factory::getUser()->authorise('core.delete', $this->option); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. + * + * @since 2.5 + */ + protected function canEditState($record) + { + return Factory::getUser()->authorise('core.edit.state', $this->option); + } + + /** + * Method to delete one or more records. + * + * @param array $pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 2.5 + */ + public function delete(&$pks) + { + $pks = (array) $pks; + $table = $this->getTable(); + + // Include the content plugins for the on delete events. + PluginHelper::importPlugin('content'); + + // Iterate the items to check if all of them exist. + foreach ($pks as $i => $pk) { + if (!$table->load($pk)) { + // Item is not in the table. + $this->setError($table->getError()); + + return false; + } + } + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if ($this->canDelete($table)) { + $context = $this->option . '.' . $this->name; + + // Trigger the onContentBeforeDelete event. + $result = Factory::getApplication()->triggerEvent('onContentBeforeDelete', array($context, $table)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + if (!$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the onContentAfterDelete event. + Factory::getApplication()->triggerEvent('onContentAfterDelete', array($context, $table)); + } else { + // Prune items that you can't change. + unset($pks[$i]); + $error = $this->getError(); + + if ($error) { + $this->setError($error); + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); + } + } + } + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 2.5 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + + // Select all fields from the table. + $query = $db->getQuery(true) + ->select('a.id, a.parent_id, a.lft, a.rgt, a.level, a.path, a.title, a.alias, a.state, a.access, a.language') + ->from($db->quoteName('#__finder_taxonomy', 'a')) + ->where('a.parent_id != 0'); + + // Join to get the branch title + $query->select([$db->quoteName('b.id', 'branch_id'), $db->quoteName('b.title', 'branch_title')]) + ->leftJoin($db->quoteName('#__finder_taxonomy', 'b') . ' ON b.level = 1 AND b.lft <= a.lft AND a.rgt <= b.rgt'); + + // Join to get the map links. + $stateQuery = $db->getQuery(true) + ->select('m.node_id') + ->select('COUNT(NULLIF(l.published, 0)) AS count_published') + ->select('COUNT(NULLIF(l.published, 1)) AS count_unpublished') + ->from($db->quoteName('#__finder_taxonomy_map', 'm')) + ->leftJoin($db->quoteName('#__finder_links', 'l') . ' ON l.link_id = m.link_id') + ->group('m.node_id'); + + $query->select('COALESCE(s.count_published, 0) AS count_published'); + $query->select('COALESCE(s.count_unpublished, 0) AS count_unpublished'); + $query->leftJoin('(' . $stateQuery . ') AS s ON s.node_id = a.id'); + + // If the model is set to check item state, add to the query. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $query->where('a.state = ' . (int) $state); + } + + // Filter over level. + $level = $this->getState('filter.level'); + + if (is_numeric($level) && (int) $level === 1) { + $query->where('a.parent_id = 1'); + } + + // Filter the maps over the branch if set. + $branchId = $this->getState('filter.branch'); + + if (is_numeric($branchId)) { + $query->where('a.parent_id = ' . (int) $branchId); + } + + // Filter the maps over the search string if set. + if ($search = $this->getState('filter.search')) { + $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); + $query->where('a.title LIKE ' . $search); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'branch_title, a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Returns a record count for the query. + * + * @param \Joomla\Database\DatabaseQuery|string + * + * @return integer Number of rows for query. + * + * @since 3.0 + */ + protected function _getListCount($query) + { + $query = clone $query; + $query->clear('select')->clear('join')->clear('order')->clear('limit')->clear('offset')->select('COUNT(*)'); + + return (int) $this->getDatabase()->setQuery($query)->loadResult(); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. [optional] + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.branch'); + $id .= ':' . $this->getState('filter.level'); + + return parent::getStoreId($id); + } + + /** + * Returns a Table object, always creating it. + * + * @param string $type The table type to instantiate. [optional] + * @param string $prefix A prefix for the table class name. [optional] + * @param array $config Configuration array for model. [optional] + * + * @return \Joomla\CMS\Table\Table A database object + * + * @since 2.5 + */ + public function getTable($type = 'Map', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * Method to auto-populate the model state. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. [optional] + * @param string $direction An optional direction. [optional] + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = 'branch_title, a.lft', $direction = 'ASC') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); + $this->setState('filter.branch', $this->getUserStateFromRequest($this->context . '.filter.branch', 'filter_branch', '', 'cmd')); + $this->setState('filter.level', $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level', '', 'cmd')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_finder'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to change the published state of one or more records. + * + * @param array $pks A list of the primary keys to change. + * @param integer $value The value of the published state. [optional] + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function publish(&$pks, $value = 1) + { + $user = Factory::getUser(); + $table = $this->getTable(); + $pks = (array) $pks; + + // Include the content plugins for the change of state event. + PluginHelper::importPlugin('content'); + + // Access checks. + foreach ($pks as $i => $pk) { + $table->reset(); + + if ($table->load($pk) && !$this->canEditState($table)) { + // Prune items that you can't change. + unset($pks[$i]); + $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); + + return false; + } + } + + // Attempt to change the state of the records. + if (!$table->publish($pks, $value, $user->get('id'))) { + $this->setError($table->getError()); + + return false; + } + + $context = $this->option . '.' . $this->name; + + // Trigger the onContentChangeState event. + $result = Factory::getApplication()->triggerEvent('onContentChangeState', array($context, $pks, $value)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Method to purge all maps from the taxonomy. + * + * @return boolean Returns true on success, false on failure. + * + * @since 2.5 + */ + public function purge() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' > 1'); + $db->setQuery($query); + $db->execute(); + + $query->clear() + ->delete($db->quoteName('#__finder_taxonomy_map')); + $db->setQuery($query); + $db->execute(); + + return true; + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + $title = 'ROOT'; + + $query->where($this->getDatabase()->quoteName('title') . ' <> :title') + ->bind(':title', $title); + + return $query; + } } diff --git a/administrator/components/com_finder/src/Model/SearchesModel.php b/administrator/components/com_finder/src/Model/SearchesModel.php index e4027989971bc..34e1ba60bf19f 100644 --- a/administrator/components/com_finder/src/Model/SearchesModel.php +++ b/administrator/components/com_finder/src/Model/SearchesModel.php @@ -1,4 +1,5 @@ setState('show_results', $this->getUserStateFromRequest($this->context . '.show_results', 'show_results', 1, 'int')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_finder'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 4.0.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('show_results'); - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 4.0.0 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.*' - ) - ); - $query->from($db->quoteName('#__finder_logging', 'a')); - - // Filter by search in title - if ($search = $this->getState('filter.search')) - { - $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); - $query->where($db->quoteName('a.searchterm') . ' LIKE ' . $search); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.hits')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Override the parent getItems to inject optional data. - * - * @return mixed An array of objects on success, false on failure. - * - * @since 4.0.0 - */ - public function getItems() - { - $items = parent::getItems(); - - foreach ($items as $item) - { - if (is_resource($item->query)) - { - $item->query = unserialize(stream_get_contents($item->query)); - } - else - { - $item->query = unserialize($item->query); - } - } - - return $items; - } - - /** - * Method to reset the search log table. - * - * @return boolean - * - * @since 4.0.0 - */ - public function reset() - { - $db = $this->getDatabase(); - - try - { - $db->truncateTable('#__finder_logging'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return true; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 4.0.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'searchterm', 'a.searchterm', + 'hits', 'a.hits', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState($ordering = 'a.hits', $direction = 'asc') + { + // Special state for toggle results button. + $this->setState('show_results', $this->getUserStateFromRequest($this->context . '.show_results', 'show_results', 1, 'int')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_finder'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 4.0.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('show_results'); + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 4.0.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.*' + ) + ); + $query->from($db->quoteName('#__finder_logging', 'a')); + + // Filter by search in title + if ($search = $this->getState('filter.search')) { + $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); + $query->where($db->quoteName('a.searchterm') . ' LIKE ' . $search); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.hits')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Override the parent getItems to inject optional data. + * + * @return mixed An array of objects on success, false on failure. + * + * @since 4.0.0 + */ + public function getItems() + { + $items = parent::getItems(); + + foreach ($items as $item) { + if (is_resource($item->query)) { + $item->query = unserialize(stream_get_contents($item->query)); + } else { + $item->query = unserialize($item->query); + } + } + + return $items; + } + + /** + * Method to reset the search log table. + * + * @return boolean + * + * @since 4.0.0 + */ + public function reset() + { + $db = $this->getDatabase(); + + try { + $db->truncateTable('#__finder_logging'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return true; + } } diff --git a/administrator/components/com_finder/src/Model/StatisticsModel.php b/administrator/components/com_finder/src/Model/StatisticsModel.php index 7757ddd755d98..8d7b76880adf7 100644 --- a/administrator/components/com_finder/src/Model/StatisticsModel.php +++ b/administrator/components/com_finder/src/Model/StatisticsModel.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true); - $data = new CMSObject; + /** + * Method to get the component statistics + * + * @return CMSObject The component statistics + * + * @since 2.5 + */ + public function getData() + { + // Initialise + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $data = new CMSObject(); - $query->select('COUNT(term_id)') - ->from($db->quoteName('#__finder_terms')); - $db->setQuery($query); - $data->term_count = $db->loadResult(); + $query->select('COUNT(term_id)') + ->from($db->quoteName('#__finder_terms')); + $db->setQuery($query); + $data->term_count = $db->loadResult(); - $query->clear() - ->select('COUNT(link_id)') - ->from($db->quoteName('#__finder_links')); - $db->setQuery($query); - $data->link_count = $db->loadResult(); + $query->clear() + ->select('COUNT(link_id)') + ->from($db->quoteName('#__finder_links')); + $db->setQuery($query); + $data->link_count = $db->loadResult(); - $query->clear() - ->select('COUNT(id)') - ->from($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' = 1'); - $db->setQuery($query); - $data->taxonomy_branch_count = $db->loadResult(); + $query->clear() + ->select('COUNT(id)') + ->from($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' = 1'); + $db->setQuery($query); + $data->taxonomy_branch_count = $db->loadResult(); - $query->clear() - ->select('COUNT(id)') - ->from($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' > 1'); - $db->setQuery($query); - $data->taxonomy_node_count = $db->loadResult(); + $query->clear() + ->select('COUNT(id)') + ->from($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' > 1'); + $db->setQuery($query); + $data->taxonomy_node_count = $db->loadResult(); - $query->clear() - ->select('t.title AS type_title, COUNT(a.link_id) AS link_count') - ->from($db->quoteName('#__finder_links') . ' AS a') - ->join('INNER', $db->quoteName('#__finder_types') . ' AS t ON t.id = a.type_id') - ->group('a.type_id, t.title') - ->order($db->quoteName('type_title') . ' ASC'); - $db->setQuery($query); - $data->type_list = $db->loadObjectList(); + $query->clear() + ->select('t.title AS type_title, COUNT(a.link_id) AS link_count') + ->from($db->quoteName('#__finder_links') . ' AS a') + ->join('INNER', $db->quoteName('#__finder_types') . ' AS t ON t.id = a.type_id') + ->group('a.type_id, t.title') + ->order($db->quoteName('type_title') . ' ASC'); + $db->setQuery($query); + $data->type_list = $db->loadObjectList(); - $lang = Factory::getLanguage(); - $plugins = PluginHelper::getPlugin('finder'); + $lang = Factory::getLanguage(); + $plugins = PluginHelper::getPlugin('finder'); - foreach ($plugins as $plugin) - { - $lang->load('plg_finder_' . $plugin->name . '.sys', JPATH_ADMINISTRATOR) - || $lang->load('plg_finder_' . $plugin->name . '.sys', JPATH_PLUGINS . '/finder/' . $plugin->name); - } + foreach ($plugins as $plugin) { + $lang->load('plg_finder_' . $plugin->name . '.sys', JPATH_ADMINISTRATOR) + || $lang->load('plg_finder_' . $plugin->name . '.sys', JPATH_PLUGINS . '/finder/' . $plugin->name); + } - return $data; - } + return $data; + } } diff --git a/administrator/components/com_finder/src/Response/Response.php b/administrator/components/com_finder/src/Response/Response.php index d45a7f40f7710..faf1f6ececd6b 100644 --- a/administrator/components/com_finder/src/Response/Response.php +++ b/administrator/components/com_finder/src/Response/Response.php @@ -1,4 +1,5 @@ get('enable_logging', '0')) - { - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'indexer.php'; - Log::addLogger($options); - } + if ($params->get('enable_logging', '0')) { + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'indexer.php'; + Log::addLogger($options); + } - // Check if we are dealing with an error. - if ($state instanceof \Exception) - { - // Log the error - try - { - Log::add($state->getMessage(), Log::ERROR); - } - catch (\RuntimeException $exception) - { - // Informational log only - } + // Check if we are dealing with an error. + if ($state instanceof \Exception) { + // Log the error + try { + Log::add($state->getMessage(), Log::ERROR); + } catch (\RuntimeException $exception) { + // Informational log only + } - // Prepare the error response. - $this->error = true; - $this->header = Text::_('COM_FINDER_INDEXER_HEADER_ERROR'); - $this->message = $state->getMessage(); - } - else - { - // Prepare the response data. - $this->batchSize = (int) $state->batchSize; - $this->batchOffset = (int) $state->batchOffset; - $this->totalItems = (int) $state->totalItems; - $this->pluginState = $state->pluginState; + // Prepare the error response. + $this->error = true; + $this->header = Text::_('COM_FINDER_INDEXER_HEADER_ERROR'); + $this->message = $state->getMessage(); + } else { + // Prepare the response data. + $this->batchSize = (int) $state->batchSize; + $this->batchOffset = (int) $state->batchOffset; + $this->totalItems = (int) $state->totalItems; + $this->pluginState = $state->pluginState; - $this->startTime = $state->startTime; - $this->endTime = Factory::getDate()->toSql(); + $this->startTime = $state->startTime; + $this->endTime = Factory::getDate()->toSql(); - $this->start = !empty($state->start) ? (int) $state->start : 0; - $this->complete = !empty($state->complete) ? (int) $state->complete : 0; + $this->start = !empty($state->start) ? (int) $state->start : 0; + $this->complete = !empty($state->complete) ? (int) $state->complete : 0; - // Set the appropriate messages. - if ($this->totalItems <= 0 && $this->complete) - { - $this->header = Text::_('COM_FINDER_INDEXER_HEADER_COMPLETE'); - $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_COMPLETE'); - } - elseif ($this->totalItems <= 0) - { - $this->header = Text::_('COM_FINDER_INDEXER_HEADER_OPTIMIZE'); - $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_OPTIMIZE'); - } - else - { - $this->header = Text::_('COM_FINDER_INDEXER_HEADER_RUNNING'); - $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_RUNNING'); - } - } - } + // Set the appropriate messages. + if ($this->totalItems <= 0 && $this->complete) { + $this->header = Text::_('COM_FINDER_INDEXER_HEADER_COMPLETE'); + $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_COMPLETE'); + } elseif ($this->totalItems <= 0) { + $this->header = Text::_('COM_FINDER_INDEXER_HEADER_OPTIMIZE'); + $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_OPTIMIZE'); + } else { + $this->header = Text::_('COM_FINDER_INDEXER_HEADER_RUNNING'); + $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_RUNNING'); + } + } + } } diff --git a/administrator/components/com_finder/src/Service/HTML/Filter.php b/administrator/components/com_finder/src/Service/HTML/Filter.php index f81a1610a0e96..cbb666c5e6706 100644 --- a/administrator/components/com_finder/src/Service/HTML/Filter.php +++ b/administrator/components/com_finder/src/Service/HTML/Filter.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - $groups = implode(',', $user->getAuthorisedViewLevels()); - $html = ''; - $filter = null; - - // Get the configuration options. - $filterId = $options['filter_id'] ?? null; - $activeNodes = array_key_exists('selected_nodes', $options) ? $options['selected_nodes'] : array(); - $classSuffix = array_key_exists('class_suffix', $options) ? $options['class_suffix'] : ''; - - // Load the predefined filter if specified. - if (!empty($filterId)) - { - $query->select('f.data, f.params') - ->from($db->quoteName('#__finder_filters') . ' AS f') - ->where('f.filter_id = ' . (int) $filterId); - - // Load the filter data. - $db->setQuery($query); - - try - { - $filter = $db->loadObject(); - } - catch (\RuntimeException $e) - { - return null; - } - - // Initialize the filter parameters. - if ($filter) - { - $filter->params = new Registry($filter->params); - } - } - - // Build the query to get the branch data and the number of child nodes. - $query->clear() - ->select('t.*, count(c.id) AS children') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS c ON c.parent_id = t.id') - ->where('t.parent_id = 1') - ->where('t.state = 1') - ->where('t.access IN (' . $groups . ')') - ->group('t.id, t.parent_id, t.state, t.access, t.title, c.parent_id') - ->order('t.lft, t.title'); - - // Limit the branch children to a predefined filter. - if ($filter) - { - $query->where('c.id IN(' . $filter->data . ')'); - } - - // Load the branches. - $db->setQuery($query); - - try - { - $branches = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - return null; - } - - // Check that we have at least one branch. - if (count($branches) === 0) - { - return null; - } - - $branch_keys = array_keys($branches); - $html .= HTMLHelper::_('bootstrap.startAccordion', 'accordion', array('active' => 'accordion-' . $branch_keys[0]) - ); - - // Load plugin language files. - LanguageHelper::loadPluginLanguage(); - - // Iterate through the branches and build the branch groups. - foreach ($branches as $bk => $bv) - { - // If the multi-lang plugin is enabled then drop the language branch. - if ($bv->title === 'Language' && Multilanguage::isEnabled()) - { - continue; - } - - // Build the query to get the child nodes for this branch. - $query->clear() - ->select('t.*') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->where('t.lft > ' . (int) $bv->lft) - ->where('t.rgt < ' . (int) $bv->rgt) - ->where('t.state = 1') - ->where('t.access IN (' . $groups . ')') - ->order('t.lft, t.title'); - - // Self-join to get the parent title. - $query->select('e.title AS parent_title') - ->join('LEFT', $db->quoteName('#__finder_taxonomy', 'e') . ' ON ' . $db->quoteName('e.id') . ' = ' . $db->quoteName('t.parent_id')); - - // Load the branches. - $db->setQuery($query); - - try - { - $nodes = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - return null; - } - - // Translate node titles if possible. - $lang = Factory::getLanguage(); - - foreach ($nodes as $nk => $nv) - { - if (trim($nv->parent_title, '*') === 'Language') - { - $title = LanguageHelper::branchLanguageTitle($nv->title); - } - else - { - $key = LanguageHelper::branchPlural($nv->title); - $title = $lang->hasKey($key) ? Text::_($key) : $nv->title; - } - - $nodes[$nk]->title = $title; - } - - // Adding slides - $html .= HTMLHelper::_('bootstrap.addSlide', - 'accordion', - Text::sprintf('COM_FINDER_FILTER_BRANCH_LABEL', - Text::_(LanguageHelper::branchSingular($bv->title)) . ' - ' . count($nodes) - ), - 'accordion-' . $bk - ); - - // Populate the toggle button. - $html .= '
    '; - - // Populate the group with nodes. - foreach ($nodes as $nk => $nv) - { - // Determine if the node should be checked. - $checked = in_array($nk, $activeNodes) ? ' checked="checked"' : ''; - - // Build a node. - $html .= '
    '; - $html .= ''; - $html .= '
    '; - } - - $html .= HTMLHelper::_('bootstrap.endSlide'); - } - - $html .= HTMLHelper::_('bootstrap.endAccordion'); - - return $html; - } - - /** - * Method to generate filters using select box dropdown controls. - * - * @param Query $idxQuery A Query object. - * @param array $options An array of options. - * - * @return mixed A rendered HTML widget on success, null otherwise. - * - * @since 2.5 - */ - public function select($idxQuery, $options) - { - $user = Factory::getUser(); - $groups = implode(',', $user->getAuthorisedViewLevels()); - $filter = null; - - // Get the configuration options. - $classSuffix = $options->get('class_suffix', null); - $showDates = $options->get('show_date_filters', false); - - // Try to load the results from cache. - $cache = Factory::getCache('com_finder', ''); - $cacheId = 'filter_select_' . serialize(array($idxQuery->filter, $options, $groups, Factory::getLanguage()->getTag())); - - // Check the cached results. - if ($cache->contains($cacheId)) - { - $branches = $cache->get($cacheId); - } - else - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Load the predefined filter if specified. - if (!empty($idxQuery->filter)) - { - $query->select('f.data, ' . $db->quoteName('f.params')) - ->from($db->quoteName('#__finder_filters') . ' AS f') - ->where('f.filter_id = ' . (int) $idxQuery->filter); - - // Load the filter data. - $db->setQuery($query); - - try - { - $filter = $db->loadObject(); - } - catch (\RuntimeException $e) - { - return null; - } - - // Initialize the filter parameters. - if ($filter) - { - $filter->params = new Registry($filter->params); - } - } - - // Build the query to get the branch data and the number of child nodes. - $query->clear() - ->select('t.*, count(c.id) AS children') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS c ON c.parent_id = t.id') - ->where('t.parent_id = 1') - ->where('t.state = 1') - ->where('t.access IN (' . $groups . ')') - ->where('c.state = 1') - ->where('c.access IN (' . $groups . ')') - ->group($db->quoteName('t.id')) - ->group($db->quoteName('t.parent_id')) - ->group('t.title, t.state, t.access, t.lft') - ->order('t.lft, t.title'); - - // Limit the branch children to a predefined filter. - if (!empty($filter->data)) - { - $query->where('c.id IN(' . $filter->data . ')'); - } - - // Load the branches. - $db->setQuery($query); - - try - { - $branches = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - return null; - } - - // Check that we have at least one branch. - if (count($branches) === 0) - { - return null; - } - - // Iterate through the branches and build the branch groups. - foreach ($branches as $bk => $bv) - { - // If the multi-lang plugin is enabled then drop the language branch. - if ($bv->title === 'Language' && Multilanguage::isEnabled()) - { - continue; - } - - // Build the query to get the child nodes for this branch. - $query->clear() - ->select('t.*') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->where('t.lft > ' . (int) $bv->lft) - ->where('t.rgt < ' . (int) $bv->rgt) - ->where('t.state = 1') - ->where('t.access IN (' . $groups . ')') - ->order('t.title'); - - // Self-join to get the parent title. - $query->select('e.title AS parent_title') - ->join('LEFT', $db->quoteName('#__finder_taxonomy', 'e') . ' ON ' . $db->quoteName('e.id') . ' = ' . $db->quoteName('t.parent_id')); - - // Limit the nodes to a predefined filter. - if (!empty($filter->data)) - { - $query->where('t.id IN(' . $filter->data . ')'); - } - - // Load the branches. - $db->setQuery($query); - - try - { - $branches[$bk]->nodes = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - return null; - } - - // Translate branch nodes if possible. - $language = Factory::getLanguage(); - - foreach ($branches[$bk]->nodes as $node_id => $node) - { - if (trim($node->parent_title, '*') === 'Language') - { - $title = LanguageHelper::branchLanguageTitle($node->title); - } - else - { - $key = LanguageHelper::branchPlural($node->title); - $title = $language->hasKey($key) ? Text::_($key) : $node->title; - } - - if ($node->level > 2) - { - $branches[$bk]->nodes[$node_id]->title = str_repeat('-', $node->level - 2) . $title; - } - else - { - $branches[$bk]->nodes[$node_id]->title = $title; - } - } - - // Add the Search All option to the branch. - array_unshift($branches[$bk]->nodes, array('id' => null, 'title' => Text::_('COM_FINDER_FILTER_SELECT_ALL_LABEL'))); - } - - // Store the data in cache. - $cache->store($branches, $cacheId); - } - - $html = ''; - - // Add the dates if enabled. - if ($showDates) - { - $html .= HTMLHelper::_('filter.dates', $idxQuery, $options); - } - - $html .= '
    '; - - // Iterate through all branches and build code. - foreach ($branches as $bk => $bv) - { - // If the multi-lang plugin is enabled then drop the language branch. - if ($bv->title === 'Language' && Multilanguage::isEnabled()) - { - continue; - } - - $active = null; - - // Check if the branch is in the filter. - if (array_key_exists($bv->title, $idxQuery->filters)) - { - // Get the request filters. - $temp = Factory::getApplication()->input->request->get('t', array(), 'array'); - - // Search for active nodes in the branch and get the active node. - $active = array_intersect($temp, $idxQuery->filters[$bv->title]); - $active = count($active) === 1 ? array_shift($active) : null; - } - - // Build a node. - $html .= '
    '; - $html .= '
    '; - $html .= ''; - $html .= '
    '; - $html .= '
    '; - $html .= HTMLHelper::_( - 'select.genericlist', - $branches[$bk]->nodes, 't[]', 'class="form-select advancedSelect"', 'id', 'title', $active, - 'tax-' . OutputFilter::stringURLSafe($bv->title) - ); - $html .= '
    '; - $html .= '
    '; - } - - $html .= '
    '; - - return $html; - } - - /** - * Method to generate fields for filtering dates - * - * @param Query $idxQuery A Query object. - * @param array $options An array of options. - * - * @return mixed A rendered HTML widget on success, null otherwise. - * - * @since 2.5 - */ - public function dates($idxQuery, $options) - { - $html = ''; - - // Get the configuration options. - $classSuffix = $options->get('class_suffix', null); - $loadMedia = $options->get('load_media', true); - $showDates = $options->get('show_date_filters', false); - - if (!empty($showDates)) - { - // Build the date operators options. - $operators = array(); - $operators[] = HTMLHelper::_('select.option', 'before', Text::_('COM_FINDER_FILTER_DATE_BEFORE')); - $operators[] = HTMLHelper::_('select.option', 'exact', Text::_('COM_FINDER_FILTER_DATE_EXACTLY')); - $operators[] = HTMLHelper::_('select.option', 'after', Text::_('COM_FINDER_FILTER_DATE_AFTER')); - - // Load the CSS/JS resources. - if ($loadMedia) - { - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - $wa->useStyle('com_finder.dates'); - } - - // Open the widget. - $html .= '
      '; - - // Start date filter. - $attribs['class'] = 'input-medium'; - $html .= '
    • '; - $html .= ''; - $html .= '
      '; - $html .= HTMLHelper::_( - 'select.genericlist', - $operators, 'w1', 'class="inputbox filter-date-operator advancedSelect form-select w-auto mb-2"', 'value', 'text', $idxQuery->when1, 'finder-filter-w1' - ); - $html .= HTMLHelper::_('calendar', $idxQuery->date1, 'd1', 'filter_date1', '%Y-%m-%d', $attribs); - $html .= '
    • '; - - // End date filter. - $html .= '
    • '; - $html .= ''; - $html .= '
      '; - $html .= HTMLHelper::_( - 'select.genericlist', - $operators, 'w2', 'class="inputbox filter-date-operator advancedSelect form-select w-auto mb-2"', 'value', 'text', $idxQuery->when2, 'finder-filter-w2' - ); - $html .= HTMLHelper::_('calendar', $idxQuery->date2, 'd2', 'filter_date2', '%Y-%m-%d', $attribs); - $html .= '
    • '; - - // Close the widget. - $html .= '
    '; - } - - return $html; - } + use DatabaseAwareTrait; + + /** + * Method to generate filters using the slider widget and decorated + * with the FinderFilter JavaScript behaviors. + * + * @param array $options An array of configuration options. [optional] + * + * @return mixed A rendered HTML widget on success, null otherwise. + * + * @since 2.5 + */ + public function slider($options = array()) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + $groups = implode(',', $user->getAuthorisedViewLevels()); + $html = ''; + $filter = null; + + // Get the configuration options. + $filterId = $options['filter_id'] ?? null; + $activeNodes = array_key_exists('selected_nodes', $options) ? $options['selected_nodes'] : array(); + $classSuffix = array_key_exists('class_suffix', $options) ? $options['class_suffix'] : ''; + + // Load the predefined filter if specified. + if (!empty($filterId)) { + $query->select('f.data, f.params') + ->from($db->quoteName('#__finder_filters') . ' AS f') + ->where('f.filter_id = ' . (int) $filterId); + + // Load the filter data. + $db->setQuery($query); + + try { + $filter = $db->loadObject(); + } catch (\RuntimeException $e) { + return null; + } + + // Initialize the filter parameters. + if ($filter) { + $filter->params = new Registry($filter->params); + } + } + + // Build the query to get the branch data and the number of child nodes. + $query->clear() + ->select('t.*, count(c.id) AS children') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS c ON c.parent_id = t.id') + ->where('t.parent_id = 1') + ->where('t.state = 1') + ->where('t.access IN (' . $groups . ')') + ->group('t.id, t.parent_id, t.state, t.access, t.title, c.parent_id') + ->order('t.lft, t.title'); + + // Limit the branch children to a predefined filter. + if ($filter) { + $query->where('c.id IN(' . $filter->data . ')'); + } + + // Load the branches. + $db->setQuery($query); + + try { + $branches = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + return null; + } + + // Check that we have at least one branch. + if (count($branches) === 0) { + return null; + } + + $branch_keys = array_keys($branches); + $html .= HTMLHelper::_('bootstrap.startAccordion', 'accordion', array('active' => 'accordion-' . $branch_keys[0])); + + // Load plugin language files. + LanguageHelper::loadPluginLanguage(); + + // Iterate through the branches and build the branch groups. + foreach ($branches as $bk => $bv) { + // If the multi-lang plugin is enabled then drop the language branch. + if ($bv->title === 'Language' && Multilanguage::isEnabled()) { + continue; + } + + // Build the query to get the child nodes for this branch. + $query->clear() + ->select('t.*') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->where('t.lft > ' . (int) $bv->lft) + ->where('t.rgt < ' . (int) $bv->rgt) + ->where('t.state = 1') + ->where('t.access IN (' . $groups . ')') + ->order('t.lft, t.title'); + + // Self-join to get the parent title. + $query->select('e.title AS parent_title') + ->join('LEFT', $db->quoteName('#__finder_taxonomy', 'e') . ' ON ' . $db->quoteName('e.id') . ' = ' . $db->quoteName('t.parent_id')); + + // Load the branches. + $db->setQuery($query); + + try { + $nodes = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + return null; + } + + // Translate node titles if possible. + $lang = Factory::getLanguage(); + + foreach ($nodes as $nk => $nv) { + if (trim($nv->parent_title, '*') === 'Language') { + $title = LanguageHelper::branchLanguageTitle($nv->title); + } else { + $key = LanguageHelper::branchPlural($nv->title); + $title = $lang->hasKey($key) ? Text::_($key) : $nv->title; + } + + $nodes[$nk]->title = $title; + } + + // Adding slides + $html .= HTMLHelper::_( + 'bootstrap.addSlide', + 'accordion', + Text::sprintf( + 'COM_FINDER_FILTER_BRANCH_LABEL', + Text::_(LanguageHelper::branchSingular($bv->title)) . ' - ' . count($nodes) + ), + 'accordion-' . $bk + ); + + // Populate the toggle button. + $html .= '
    '; + + // Populate the group with nodes. + foreach ($nodes as $nk => $nv) { + // Determine if the node should be checked. + $checked = in_array($nk, $activeNodes) ? ' checked="checked"' : ''; + + // Build a node. + $html .= '
    '; + $html .= ''; + $html .= '
    '; + } + + $html .= HTMLHelper::_('bootstrap.endSlide'); + } + + $html .= HTMLHelper::_('bootstrap.endAccordion'); + + return $html; + } + + /** + * Method to generate filters using select box dropdown controls. + * + * @param Query $idxQuery A Query object. + * @param array $options An array of options. + * + * @return mixed A rendered HTML widget on success, null otherwise. + * + * @since 2.5 + */ + public function select($idxQuery, $options) + { + $user = Factory::getUser(); + $groups = implode(',', $user->getAuthorisedViewLevels()); + $filter = null; + + // Get the configuration options. + $classSuffix = $options->get('class_suffix', null); + $showDates = $options->get('show_date_filters', false); + + // Try to load the results from cache. + $cache = Factory::getCache('com_finder', ''); + $cacheId = 'filter_select_' . serialize(array($idxQuery->filter, $options, $groups, Factory::getLanguage()->getTag())); + + // Check the cached results. + if ($cache->contains($cacheId)) { + $branches = $cache->get($cacheId); + } else { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Load the predefined filter if specified. + if (!empty($idxQuery->filter)) { + $query->select('f.data, ' . $db->quoteName('f.params')) + ->from($db->quoteName('#__finder_filters') . ' AS f') + ->where('f.filter_id = ' . (int) $idxQuery->filter); + + // Load the filter data. + $db->setQuery($query); + + try { + $filter = $db->loadObject(); + } catch (\RuntimeException $e) { + return null; + } + + // Initialize the filter parameters. + if ($filter) { + $filter->params = new Registry($filter->params); + } + } + + // Build the query to get the branch data and the number of child nodes. + $query->clear() + ->select('t.*, count(c.id) AS children') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS c ON c.parent_id = t.id') + ->where('t.parent_id = 1') + ->where('t.state = 1') + ->where('t.access IN (' . $groups . ')') + ->where('c.state = 1') + ->where('c.access IN (' . $groups . ')') + ->group($db->quoteName('t.id')) + ->group($db->quoteName('t.parent_id')) + ->group('t.title, t.state, t.access, t.lft') + ->order('t.lft, t.title'); + + // Limit the branch children to a predefined filter. + if (!empty($filter->data)) { + $query->where('c.id IN(' . $filter->data . ')'); + } + + // Load the branches. + $db->setQuery($query); + + try { + $branches = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + return null; + } + + // Check that we have at least one branch. + if (count($branches) === 0) { + return null; + } + + // Iterate through the branches and build the branch groups. + foreach ($branches as $bk => $bv) { + // If the multi-lang plugin is enabled then drop the language branch. + if ($bv->title === 'Language' && Multilanguage::isEnabled()) { + continue; + } + + // Build the query to get the child nodes for this branch. + $query->clear() + ->select('t.*') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->where('t.lft > ' . (int) $bv->lft) + ->where('t.rgt < ' . (int) $bv->rgt) + ->where('t.state = 1') + ->where('t.access IN (' . $groups . ')') + ->order('t.title'); + + // Self-join to get the parent title. + $query->select('e.title AS parent_title') + ->join('LEFT', $db->quoteName('#__finder_taxonomy', 'e') . ' ON ' . $db->quoteName('e.id') . ' = ' . $db->quoteName('t.parent_id')); + + // Limit the nodes to a predefined filter. + if (!empty($filter->data)) { + $query->where('t.id IN(' . $filter->data . ')'); + } + + // Load the branches. + $db->setQuery($query); + + try { + $branches[$bk]->nodes = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + return null; + } + + // Translate branch nodes if possible. + $language = Factory::getLanguage(); + + foreach ($branches[$bk]->nodes as $node_id => $node) { + if (trim($node->parent_title, '*') === 'Language') { + $title = LanguageHelper::branchLanguageTitle($node->title); + } else { + $key = LanguageHelper::branchPlural($node->title); + $title = $language->hasKey($key) ? Text::_($key) : $node->title; + } + + if ($node->level > 2) { + $branches[$bk]->nodes[$node_id]->title = str_repeat('-', $node->level - 2) . $title; + } else { + $branches[$bk]->nodes[$node_id]->title = $title; + } + } + + // Add the Search All option to the branch. + array_unshift($branches[$bk]->nodes, array('id' => null, 'title' => Text::_('COM_FINDER_FILTER_SELECT_ALL_LABEL'))); + } + + // Store the data in cache. + $cache->store($branches, $cacheId); + } + + $html = ''; + + // Add the dates if enabled. + if ($showDates) { + $html .= HTMLHelper::_('filter.dates', $idxQuery, $options); + } + + $html .= '
    '; + + // Iterate through all branches and build code. + foreach ($branches as $bk => $bv) { + // If the multi-lang plugin is enabled then drop the language branch. + if ($bv->title === 'Language' && Multilanguage::isEnabled()) { + continue; + } + + $active = null; + + // Check if the branch is in the filter. + if (array_key_exists($bv->title, $idxQuery->filters)) { + // Get the request filters. + $temp = Factory::getApplication()->input->request->get('t', array(), 'array'); + + // Search for active nodes in the branch and get the active node. + $active = array_intersect($temp, $idxQuery->filters[$bv->title]); + $active = count($active) === 1 ? array_shift($active) : null; + } + + // Build a node. + $html .= '
    '; + $html .= '
    '; + $html .= ''; + $html .= '
    '; + $html .= '
    '; + $html .= HTMLHelper::_( + 'select.genericlist', + $branches[$bk]->nodes, + 't[]', + 'class="form-select advancedSelect"', + 'id', + 'title', + $active, + 'tax-' . OutputFilter::stringURLSafe($bv->title) + ); + $html .= '
    '; + $html .= '
    '; + } + + $html .= '
    '; + + return $html; + } + + /** + * Method to generate fields for filtering dates + * + * @param Query $idxQuery A Query object. + * @param array $options An array of options. + * + * @return mixed A rendered HTML widget on success, null otherwise. + * + * @since 2.5 + */ + public function dates($idxQuery, $options) + { + $html = ''; + + // Get the configuration options. + $classSuffix = $options->get('class_suffix', null); + $loadMedia = $options->get('load_media', true); + $showDates = $options->get('show_date_filters', false); + + if (!empty($showDates)) { + // Build the date operators options. + $operators = array(); + $operators[] = HTMLHelper::_('select.option', 'before', Text::_('COM_FINDER_FILTER_DATE_BEFORE')); + $operators[] = HTMLHelper::_('select.option', 'exact', Text::_('COM_FINDER_FILTER_DATE_EXACTLY')); + $operators[] = HTMLHelper::_('select.option', 'after', Text::_('COM_FINDER_FILTER_DATE_AFTER')); + + // Load the CSS/JS resources. + if ($loadMedia) { + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + $wa->useStyle('com_finder.dates'); + } + + // Open the widget. + $html .= '
      '; + + // Start date filter. + $attribs['class'] = 'input-medium'; + $html .= '
    • '; + $html .= ''; + $html .= '
      '; + $html .= HTMLHelper::_( + 'select.genericlist', + $operators, + 'w1', + 'class="inputbox filter-date-operator advancedSelect form-select w-auto mb-2"', + 'value', + 'text', + $idxQuery->when1, + 'finder-filter-w1' + ); + $html .= HTMLHelper::_('calendar', $idxQuery->date1, 'd1', 'filter_date1', '%Y-%m-%d', $attribs); + $html .= '
    • '; + + // End date filter. + $html .= '
    • '; + $html .= ''; + $html .= '
      '; + $html .= HTMLHelper::_( + 'select.genericlist', + $operators, + 'w2', + 'class="inputbox filter-date-operator advancedSelect form-select w-auto mb-2"', + 'value', + 'text', + $idxQuery->when2, + 'finder-filter-w2' + ); + $html .= HTMLHelper::_('calendar', $idxQuery->date2, 'd2', 'filter_date2', '%Y-%m-%d', $attribs); + $html .= '
    • '; + + // Close the widget. + $html .= '
    '; + } + + return $html; + } } diff --git a/administrator/components/com_finder/src/Service/HTML/Finder.php b/administrator/components/com_finder/src/Service/HTML/Finder.php index 0968df797eb34..c6993c70dacbd 100644 --- a/administrator/components/com_finder/src/Service/HTML/Finder.php +++ b/administrator/components/com_finder/src/Service/HTML/Finder.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select('DISTINCT t.title AS text, t.id AS value') - ->from($db->quoteName('#__finder_types') . ' AS t') - ->join('LEFT', $db->quoteName('#__finder_links') . ' AS l ON l.type_id = t.id') - ->order('t.title ASC'); - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - return array(); - } - - // Compile the options. - $options = array(); - - $lang = Factory::getLanguage(); - - foreach ($rows as $row) - { - $key = $lang->hasKey(LanguageHelper::branchPlural($row->text)) ? LanguageHelper::branchPlural($row->text) : $row->text; - $options[] = HTMLHelper::_('select.option', $row->value, Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_($key))); - } - - return $options; - } - - /** - * Creates a list of maps. - * - * @return array An array containing the maps that can be selected. - * - * @since 2.5 - */ - public function mapslist() - { - // Load the finder types. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('title', 'text')) - ->select($db->quoteName('id', 'value')) - ->from($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' = 1'); - $db->setQuery($query); - - try - { - $branches = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Translate. - $lang = Factory::getLanguage(); - - foreach ($branches as $branch) - { - $key = LanguageHelper::branchPlural($branch->text); - $branch->translatedText = $lang->hasKey($key) ? Text::_($key) : $branch->text; - } - - // Order by title. - $branches = ArrayHelper::sortObjects($branches, 'translatedText', 1, true, true); - - // Compile the options. - $options = array(); - $options[] = HTMLHelper::_('select.option', '', Text::_('COM_FINDER_MAPS_SELECT_BRANCH')); - - // Convert the values to options. - foreach ($branches as $branch) - { - $options[] = HTMLHelper::_('select.option', $branch->value, $branch->translatedText); - } - - return $options; - } - - /** - * Creates a list of published states. - * - * @return array An array containing the states that can be selected. - * - * @since 2.5 - */ - public static function statelist() - { - return array( - HTMLHelper::_('select.option', '1', Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_('JPUBLISHED'))), - HTMLHelper::_('select.option', '0', Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_('JUNPUBLISHED'))) - ); - } + use DatabaseAwareTrait; + + /** + * Creates a list of types to filter on. + * + * @return array An array containing the types that can be selected. + * + * @since 2.5 + */ + public function typeslist() + { + // Load the finder types. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('DISTINCT t.title AS text, t.id AS value') + ->from($db->quoteName('#__finder_types') . ' AS t') + ->join('LEFT', $db->quoteName('#__finder_links') . ' AS l ON l.type_id = t.id') + ->order('t.title ASC'); + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + return array(); + } + + // Compile the options. + $options = array(); + + $lang = Factory::getLanguage(); + + foreach ($rows as $row) { + $key = $lang->hasKey(LanguageHelper::branchPlural($row->text)) ? LanguageHelper::branchPlural($row->text) : $row->text; + $options[] = HTMLHelper::_('select.option', $row->value, Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_($key))); + } + + return $options; + } + + /** + * Creates a list of maps. + * + * @return array An array containing the maps that can be selected. + * + * @since 2.5 + */ + public function mapslist() + { + // Load the finder types. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title', 'text')) + ->select($db->quoteName('id', 'value')) + ->from($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' = 1'); + $db->setQuery($query); + + try { + $branches = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Translate. + $lang = Factory::getLanguage(); + + foreach ($branches as $branch) { + $key = LanguageHelper::branchPlural($branch->text); + $branch->translatedText = $lang->hasKey($key) ? Text::_($key) : $branch->text; + } + + // Order by title. + $branches = ArrayHelper::sortObjects($branches, 'translatedText', 1, true, true); + + // Compile the options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '', Text::_('COM_FINDER_MAPS_SELECT_BRANCH')); + + // Convert the values to options. + foreach ($branches as $branch) { + $options[] = HTMLHelper::_('select.option', $branch->value, $branch->translatedText); + } + + return $options; + } + + /** + * Creates a list of published states. + * + * @return array An array containing the states that can be selected. + * + * @since 2.5 + */ + public static function statelist() + { + return array( + HTMLHelper::_('select.option', '1', Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_('JPUBLISHED'))), + HTMLHelper::_('select.option', '0', Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_('JUNPUBLISHED'))) + ); + } } diff --git a/administrator/components/com_finder/src/Service/HTML/Query.php b/administrator/components/com_finder/src/Service/HTML/Query.php index 47540a83035c3..cbbff3ba7bf11 100644 --- a/administrator/components/com_finder/src/Service/HTML/Query.php +++ b/administrator/components/com_finder/src/Service/HTML/Query.php @@ -1,4 +1,5 @@ included as $token) - { - if ($token->required && (!isset($token->derived) || $token->derived == false)) - { - $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_REQUIRED', $token->term) . ''; - } - } - - // Process the optional tokens. - foreach ($query->included as $token) - { - if (!$token->required && (!isset($token->derived) || $token->derived == false)) - { - $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_OPTIONAL', $token->term) . ''; - } - } - - // Process the excluded tokens. - foreach ($query->excluded as $token) - { - if (!isset($token->derived) || $token->derived === false) - { - $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_EXCLUDED', $token->term) . ''; - } - } - - // Process the start date. - if ($query->date1) - { - $date = Factory::getDate($query->date1)->format(Text::_('DATE_FORMAT_LC')); - $datecondition = Text::_('COM_FINDER_QUERY_DATE_CONDITION_' . strtoupper($query->when1)); - $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_START_DATE', $datecondition, $date) . ''; - } - - // Process the end date. - if ($query->date2) - { - $date = Factory::getDate($query->date2)->format(Text::_('DATE_FORMAT_LC')); - $datecondition = Text::_('COM_FINDER_QUERY_DATE_CONDITION_' . strtoupper($query->when2)); - $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_END_DATE', $datecondition, $date) . ''; - } - - // Process the taxonomy filters. - if (!empty($query->filters)) - { - // Get the filters in the request. - $t = Factory::getApplication()->input->request->get('t', array(), 'array'); - - // Process the taxonomy branches. - foreach ($query->filters as $branch => $nodes) - { - // Process the taxonomy nodes. - $lang = Factory::getLanguage(); - - foreach ($nodes as $title => $id) - { - // Translate the title for Types - $key = LanguageHelper::branchPlural($title); - - if ($lang->hasKey($key)) - { - $title = Text::_($key); - } - - // Don't include the node if it is not in the request. - if (!in_array($id, $t)) - { - continue; - } - - // Add the node to the explanation. - $parts[] = '' - . Text::sprintf('COM_FINDER_QUERY_TAXONOMY_NODE', $title, Text::_(LanguageHelper::branchSingular($branch))) - . ''; - } - } - } - - // Build the interpreted query. - return count($parts) ? implode(Text::_('COM_FINDER_QUERY_TOKEN_GLUE'), $parts) : null; - } - - /** - * Method to get the suggested search query. - * - * @param IndexerQuery $query A IndexerQuery object. - * - * @return mixed String if there is a suggestion, false otherwise. - * - * @since 2.5 - */ - public static function suggested(IndexerQuery $query) - { - $suggested = false; - - // Check if the query input is empty. - if (empty($query->input)) - { - return $suggested; - } - - // Check if there were any ignored or included keywords. - if (count($query->ignored) || count($query->included)) - { - $suggested = $query->input; - - // Replace the ignored keyword suggestions. - foreach (array_reverse($query->ignored) as $token) - { - if (isset($token->suggestion)) - { - $suggested = str_ireplace($token->term, $token->suggestion, $suggested); - } - } - - // Replace the included keyword suggestions. - foreach (array_reverse($query->included) as $token) - { - if (isset($token->suggestion)) - { - $suggested = str_ireplace($token->term, $token->suggestion, $suggested); - } - } - - // Check if we made any changes. - if ($suggested == $query->input) - { - $suggested = false; - } - } - - return $suggested; - } + /** + * Method to get the explained (human-readable) search query. + * + * @param IndexerQuery $query A IndexerQuery object to explain. + * + * @return mixed String if there is data to explain, null otherwise. + * + * @since 2.5 + */ + public static function explained(IndexerQuery $query) + { + $parts = array(); + + // Process the required tokens. + foreach ($query->included as $token) { + if ($token->required && (!isset($token->derived) || $token->derived == false)) { + $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_REQUIRED', $token->term) . ''; + } + } + + // Process the optional tokens. + foreach ($query->included as $token) { + if (!$token->required && (!isset($token->derived) || $token->derived == false)) { + $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_OPTIONAL', $token->term) . ''; + } + } + + // Process the excluded tokens. + foreach ($query->excluded as $token) { + if (!isset($token->derived) || $token->derived === false) { + $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_EXCLUDED', $token->term) . ''; + } + } + + // Process the start date. + if ($query->date1) { + $date = Factory::getDate($query->date1)->format(Text::_('DATE_FORMAT_LC')); + $datecondition = Text::_('COM_FINDER_QUERY_DATE_CONDITION_' . strtoupper($query->when1)); + $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_START_DATE', $datecondition, $date) . ''; + } + + // Process the end date. + if ($query->date2) { + $date = Factory::getDate($query->date2)->format(Text::_('DATE_FORMAT_LC')); + $datecondition = Text::_('COM_FINDER_QUERY_DATE_CONDITION_' . strtoupper($query->when2)); + $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_END_DATE', $datecondition, $date) . ''; + } + + // Process the taxonomy filters. + if (!empty($query->filters)) { + // Get the filters in the request. + $t = Factory::getApplication()->input->request->get('t', array(), 'array'); + + // Process the taxonomy branches. + foreach ($query->filters as $branch => $nodes) { + // Process the taxonomy nodes. + $lang = Factory::getLanguage(); + + foreach ($nodes as $title => $id) { + // Translate the title for Types + $key = LanguageHelper::branchPlural($title); + + if ($lang->hasKey($key)) { + $title = Text::_($key); + } + + // Don't include the node if it is not in the request. + if (!in_array($id, $t)) { + continue; + } + + // Add the node to the explanation. + $parts[] = '' + . Text::sprintf('COM_FINDER_QUERY_TAXONOMY_NODE', $title, Text::_(LanguageHelper::branchSingular($branch))) + . ''; + } + } + } + + // Build the interpreted query. + return count($parts) ? implode(Text::_('COM_FINDER_QUERY_TOKEN_GLUE'), $parts) : null; + } + + /** + * Method to get the suggested search query. + * + * @param IndexerQuery $query A IndexerQuery object. + * + * @return mixed String if there is a suggestion, false otherwise. + * + * @since 2.5 + */ + public static function suggested(IndexerQuery $query) + { + $suggested = false; + + // Check if the query input is empty. + if (empty($query->input)) { + return $suggested; + } + + // Check if there were any ignored or included keywords. + if (count($query->ignored) || count($query->included)) { + $suggested = $query->input; + + // Replace the ignored keyword suggestions. + foreach (array_reverse($query->ignored) as $token) { + if (isset($token->suggestion)) { + $suggested = str_ireplace($token->term, $token->suggestion, $suggested); + } + } + + // Replace the included keyword suggestions. + foreach (array_reverse($query->included) as $token) { + if (isset($token->suggestion)) { + $suggested = str_ireplace($token->term, $token->suggestion, $suggested); + } + } + + // Check if we made any changes. + if ($suggested == $query->input) { + $suggested = false; + } + } + + return $suggested; + } } diff --git a/administrator/components/com_finder/src/Table/FilterTable.php b/administrator/components/com_finder/src/Table/FilterTable.php index 2e12e043829c5..12f103216686b 100644 --- a/administrator/components/com_finder/src/Table/FilterTable.php +++ b/administrator/components/com_finder/src/Table/FilterTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - } - - /** - * Method to perform sanity checks on the \JTable instance properties to ensure - * they are safe to store in the database. Child classes should override this - * method to make sure the data they are storing in the database is safe and - * as expected before storage. - * - * @return boolean True if the instance is sane and able to be stored in the database. - * - * @since 2.5 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (trim($this->alias) === '') - { - $this->alias = $this->title; - } - - $this->alias = ApplicationHelper::stringURLSafe($this->alias); - - if (trim(str_replace('-', '', $this->alias)) === '') - { - $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); - } - - $params = new Registry($this->params); - - $d1 = $params->get('d1', ''); - $d2 = $params->get('d2', ''); - - // Check the end date is not earlier than the start date. - if (!empty($d1) && !empty($d2) && $d2 < $d1) - { - // Swap the dates. - $params->set('d1', $d2); - $params->set('d2', $d1); - $this->params = (string) $params; - } - - return true; - } - - /** - * Method to store a row in the database from the \JTable instance properties. - * If a primary key value is set the row with that primary key value will be - * updated with the instance property values. If no primary key value is set - * a new row will be inserted into the database with the properties from the - * \JTable instance. - * - * @param boolean $updateNulls True to update fields even if they are null. [optional] - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate()->toSql(); - $userId = Factory::getUser()->id; - - // Set created date if not set. - if (!(int) $this->created) - { - $this->created = $date; - } - - if ($this->filter_id) - { - // Existing item - $this->modified_by = $userId; - $this->modified = $date; - } - else - { - if (empty($this->created_by)) - { - $this->created_by = $userId; - } - - if (!(int) $this->modified) - { - $this->modified = $this->created; - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - } - - if (is_array($this->data)) - { - $this->map_count = count($this->data); - $this->data = implode(',', $this->data); - } - else - { - $this->map_count = 0; - $this->data = implode(',', array()); - } - - // Verify that the alias is unique - $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')); - - return false; - } - - return parent::store($updateNulls); - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Ensure the params are json encoded in the bind method + * + * @var array + * @since 4.0.0 + */ + protected $_jsonEncode = array('params'); + + /** + * Constructor + * + * @param DatabaseDriver $db Database Driver connector object. + * + * @since 2.5 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__finder_filters', 'filter_id', $db); + + $this->setColumnAlias('published', 'state'); + } + + /** + * Method to perform sanity checks on the \JTable instance properties to ensure + * they are safe to store in the database. Child classes should override this + * method to make sure the data they are storing in the database is safe and + * as expected before storage. + * + * @return boolean True if the instance is sane and able to be stored in the database. + * + * @since 2.5 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (trim($this->alias) === '') { + $this->alias = $this->title; + } + + $this->alias = ApplicationHelper::stringURLSafe($this->alias); + + if (trim(str_replace('-', '', $this->alias)) === '') { + $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); + } + + $params = new Registry($this->params); + + $d1 = $params->get('d1', ''); + $d2 = $params->get('d2', ''); + + // Check the end date is not earlier than the start date. + if (!empty($d1) && !empty($d2) && $d2 < $d1) { + // Swap the dates. + $params->set('d1', $d2); + $params->set('d2', $d1); + $this->params = (string) $params; + } + + return true; + } + + /** + * Method to store a row in the database from the \JTable instance properties. + * If a primary key value is set the row with that primary key value will be + * updated with the instance property values. If no primary key value is set + * a new row will be inserted into the database with the properties from the + * \JTable instance. + * + * @param boolean $updateNulls True to update fields even if they are null. [optional] + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate()->toSql(); + $userId = Factory::getUser()->id; + + // Set created date if not set. + if (!(int) $this->created) { + $this->created = $date; + } + + if ($this->filter_id) { + // Existing item + $this->modified_by = $userId; + $this->modified = $date; + } else { + if (empty($this->created_by)) { + $this->created_by = $userId; + } + + if (!(int) $this->modified) { + $this->modified = $this->created; + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + } + + if (is_array($this->data)) { + $this->map_count = count($this->data); + $this->data = implode(',', $this->data); + } else { + $this->map_count = 0; + $this->data = implode(',', array()); + } + + // Verify that the alias is unique + $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')); + + return false; + } + + return parent::store($updateNulls); + } } diff --git a/administrator/components/com_finder/src/Table/LinkTable.php b/administrator/components/com_finder/src/Table/LinkTable.php index 62f3c0e11fb21..b732fd536c0ab 100644 --- a/administrator/components/com_finder/src/Table/LinkTable.php +++ b/administrator/components/com_finder/src/Table/LinkTable.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Finder\Administrator\Table; use Joomla\CMS\Table\Table; @@ -20,38 +22,38 @@ */ class LinkTable extends Table { - /** - * Indicates that columns fully support the NULL value in the database - * - * @var boolean - * @since 4.0.0 - */ - protected $_supportNullValue = true; + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; - /** - * Constructor - * - * @param DatabaseDriver $db Database Driver connector object. - * - * @since 2.5 - */ - public function __construct(DatabaseDriver $db) - { - parent::__construct('#__finder_links', 'link_id', $db); - } + /** + * Constructor + * + * @param DatabaseDriver $db Database Driver connector object. + * + * @since 2.5 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__finder_links', 'link_id', $db); + } - /** - * Overloaded store function - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return mixed False on failure, positive integer on success. - * - * @see Table::store() - * @since 4.0.0 - */ - public function store($updateNulls = true) - { - return parent::store($updateNulls); - } + /** + * Overloaded store function + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return mixed False on failure, positive integer on success. + * + * @see Table::store() + * @since 4.0.0 + */ + public function store($updateNulls = true) + { + return parent::store($updateNulls); + } } diff --git a/administrator/components/com_finder/src/Table/MapTable.php b/administrator/components/com_finder/src/Table/MapTable.php index 26bc302cb6b24..ad5b78da5e1ad 100644 --- a/administrator/components/com_finder/src/Table/MapTable.php +++ b/administrator/components/com_finder/src/Table/MapTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - $this->access = (int) Factory::getApplication()->get('access'); - } + $this->setColumnAlias('published', 'state'); + $this->access = (int) Factory::getApplication()->get('access'); + } - /** - * Override check function - * - * @return boolean - * - * @see Table::check() - * @since 4.0.0 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); + /** + * Override check function + * + * @return boolean + * + * @see Table::check() + * @since 4.0.0 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); - return false; - } + return false; + } - // Check for a title. - if (trim($this->title) == '') - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_CATEGORY')); + // Check for a title. + if (trim($this->title) == '') { + $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_CATEGORY')); - return false; - } + return false; + } - $this->alias = ApplicationHelper::stringURLSafe($this->title, $this->language); + $this->alias = ApplicationHelper::stringURLSafe($this->title, $this->language); - if (trim($this->alias) == '') - { - $this->alias = md5(serialize($this->getProperties())); - } + if (trim($this->alias) == '') { + $this->alias = md5(serialize($this->getProperties())); + } - return true; - } + return true; + } } diff --git a/administrator/components/com_finder/src/View/Filter/HtmlView.php b/administrator/components/com_finder/src/View/Filter/HtmlView.php index b51c2166d9a89..0650e6ab69293 100644 --- a/administrator/components/com_finder/src/View/Filter/HtmlView.php +++ b/administrator/components/com_finder/src/View/Filter/HtmlView.php @@ -1,4 +1,5 @@ filter = $this->get('Filter'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - $this->state = $this->get('State'); - $this->total = $this->get('Total'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Configure the toolbar. - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Method to configure the toolbar for this view. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $isNew = ($this->item->filter_id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $this->getCurrentUser()->id); - $canDo = ContentHelper::getActions('com_finder'); - - // Configure the toolbar. - ToolbarHelper::title( - $isNew ? Text::_('COM_FINDER_FILTER_NEW_TOOLBAR_TITLE') : Text::_('COM_FINDER_FILTER_EDIT_TOOLBAR_TITLE'), - 'zoom-in finder' - ); - - // Set the actions for new and existing records. - if ($isNew) - { - // For new records, check the create permission. - if ($canDo->get('core.create')) - { - ToolbarHelper::apply('filter.apply'); - - ToolbarHelper::saveGroup( - [ - ['save', 'filter.save'], - ['save2new', 'filter.save2new'] - ], - 'btn-success' - ); - } - - ToolbarHelper::cancel('filter.cancel'); - } - else - { - $toolbarButtons = []; - - // Can't save the record if it's checked out. - // Since it's an existing record, check the edit permission. - if (!$checkedOut && $canDo->get('core.edit')) - { - ToolbarHelper::apply('filter.apply'); - - $toolbarButtons[] = ['save', 'filter.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'filter.save2new']; - } - } - - // If an existing item, can save as a copy - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'filter.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('filter.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Smart_Search:_New_or_Edit_Filter'); - } + /** + * The filter object + * + * @var \Joomla\Component\Finder\Administrator\Table\FilterTable + * + * @since 3.6.2 + */ + protected $filter; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + * + * @since 3.6.2 + */ + protected $form; + + /** + * The active item + * + * @var CMSObject|boolean + * + * @since 3.6.2 + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + * + * @since 3.6.2 + */ + protected $state; + + /** + * The total indexed items + * + * @var integer + * + * @since 3.8.0 + */ + protected $total; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Load the view data. + $this->filter = $this->get('Filter'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->total = $this->get('Total'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Configure the toolbar. + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Method to configure the toolbar for this view. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $isNew = ($this->item->filter_id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $this->getCurrentUser()->id); + $canDo = ContentHelper::getActions('com_finder'); + + // Configure the toolbar. + ToolbarHelper::title( + $isNew ? Text::_('COM_FINDER_FILTER_NEW_TOOLBAR_TITLE') : Text::_('COM_FINDER_FILTER_EDIT_TOOLBAR_TITLE'), + 'zoom-in finder' + ); + + // Set the actions for new and existing records. + if ($isNew) { + // For new records, check the create permission. + if ($canDo->get('core.create')) { + ToolbarHelper::apply('filter.apply'); + + ToolbarHelper::saveGroup( + [ + ['save', 'filter.save'], + ['save2new', 'filter.save2new'] + ], + 'btn-success' + ); + } + + ToolbarHelper::cancel('filter.cancel'); + } else { + $toolbarButtons = []; + + // Can't save the record if it's checked out. + // Since it's an existing record, check the edit permission. + if (!$checkedOut && $canDo->get('core.edit')) { + ToolbarHelper::apply('filter.apply'); + + $toolbarButtons[] = ['save', 'filter.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'filter.save2new']; + } + } + + // If an existing item, can save as a copy + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'filter.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('filter.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Smart_Search:_New_or_Edit_Filter'); + } } diff --git a/administrator/components/com_finder/src/View/Filters/HtmlView.php b/administrator/components/com_finder/src/View/Filters/HtmlView.php index f935a72ed948b..bbcd5d15fb9e4 100644 --- a/administrator/components/com_finder/src/View/Filters/HtmlView.php +++ b/administrator/components/com_finder/src/View/Filters/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->total = $this->get('Total'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (\count($this->items) === 0 && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Configure the toolbar. - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Method to configure the toolbar for this view. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_finder'); - - ToolbarHelper::title(Text::_('COM_FINDER_FILTERS_TOOLBAR_TITLE'), 'search-plus finder'); - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.create')) - { - ToolbarHelper::addNew('filter.add'); - ToolbarHelper::divider(); - } - - if ($this->isEmptyState === false) - { - if ($canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('filters.publish')->listCheck(true); - $childBar->unpublish('filters.unpublish')->listCheck(true); - $childBar->checkin('filters.checkin')->listCheck(true); - } - - ToolbarHelper::divider(); - $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); - ToolbarHelper::divider(); - - if ($canDo->get('core.delete')) - { - ToolbarHelper::deleteList('', 'filters.delete'); - ToolbarHelper::divider(); - } - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_finder'); - } - - ToolbarHelper::help('Smart_Search:_Search_Filters'); - } + /** + * An array of items + * + * @var array + * + * @since 3.6.1 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.6.1 + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.6.1 + */ + protected $state; + + /** + * The total number of items + * + * @var integer + * + * @since 3.6.1 + */ + protected $total; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Load the view data. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->total = $this->get('Total'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (\count($this->items) === 0 && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Configure the toolbar. + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Method to configure the toolbar for this view. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_finder'); + + ToolbarHelper::title(Text::_('COM_FINDER_FILTERS_TOOLBAR_TITLE'), 'search-plus finder'); + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.create')) { + ToolbarHelper::addNew('filter.add'); + ToolbarHelper::divider(); + } + + if ($this->isEmptyState === false) { + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('filters.publish')->listCheck(true); + $childBar->unpublish('filters.unpublish')->listCheck(true); + $childBar->checkin('filters.checkin')->listCheck(true); + } + + ToolbarHelper::divider(); + $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); + ToolbarHelper::divider(); + + if ($canDo->get('core.delete')) { + ToolbarHelper::deleteList('', 'filters.delete'); + ToolbarHelper::divider(); + } + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_finder'); + } + + ToolbarHelper::help('Smart_Search:_Search_Filters'); + } } diff --git a/administrator/components/com_finder/src/View/Index/HtmlView.php b/administrator/components/com_finder/src/View/Index/HtmlView.php index 3bdfa77b23359..1265b959f5ce1 100644 --- a/administrator/components/com_finder/src/View/Index/HtmlView.php +++ b/administrator/components/com_finder/src/View/Index/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->total = $this->get('Total'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->pluginState = $this->get('pluginState'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if ($this->get('TotalIndexed') === 0 && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check that the content - finder plugin is enabled - if (!PluginHelper::isEnabled('content', 'finder')) - { - $this->finderPluginId = FinderHelper::getFinderPluginId(); - } - - // Configure the toolbar. - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Method to configure the toolbar for this view. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_finder'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_FINDER_INDEX_TOOLBAR_TITLE'), 'search-plus finder'); - - $toolbar->appendButton( - 'Popup', 'archive', 'COM_FINDER_INDEX', 'index.php?option=com_finder&view=indexer&tmpl=component', 500, 210, 0, 0, - 'window.parent.location.reload()', Text::_('COM_FINDER_HEADING_INDEXER') - ); - - if (!$this->isEmptyState) - { - if ($canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('index.publish')->listCheck(true); - $childBar->unpublish('index.unpublish')->listCheck(true); - } - - $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); - - if ($canDo->get('core.delete')) - { - $toolbar->confirmButton('', 'JTOOLBAR_DELETE', 'index.delete') - ->icon('icon-delete') - ->message('COM_FINDER_INDEX_CONFIRM_DELETE_PROMPT') - ->listCheck(true); - $toolbar->divider(); - } - - if ($canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('maintenance-group'); - $dropdown->text('COM_FINDER_INDEX_TOOLBAR_MAINTENANCE') - ->toggleSplit(false) - ->icon('icon-wrench') - ->buttonClass('btn btn-action'); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->standardButton('cog', 'COM_FINDER_INDEX_TOOLBAR_OPTIMISE', 'index.optimise', false); - $childBar->confirmButton('index.purge', 'COM_FINDER_INDEX_TOOLBAR_PURGE', 'index.purge') - ->icon('icon-trash') - ->message('COM_FINDER_INDEX_CONFIRM_PURGE_PROMPT'); - } - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_finder'); - } - - ToolbarHelper::help('Smart_Search:_Indexed_Content'); - } + /** + * An array of items + * + * @var array + * + * @since 3.6.1 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.6.1 + */ + protected $pagination; + + /** + * The state of core Smart Search plugins + * + * @var array + * + * @since 3.6.1 + */ + protected $pluginState; + + /** + * The id of the content - finder plugin in mysql + * + * @var integer + * + * @since 4.0.0 + */ + protected $finderPluginId = 0; + + /** + * The model state + * + * @var mixed + * + * @since 3.6.1 + */ + protected $state; + + /** + * The total number of items + * + * @var integer + * + * @since 3.6.1 + */ + protected $total; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * @var mixed + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Load plugin language files. + LanguageHelper::loadPluginLanguage(); + + $this->items = $this->get('Items'); + $this->total = $this->get('Total'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->pluginState = $this->get('pluginState'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if ($this->get('TotalIndexed') === 0 && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check that the content - finder plugin is enabled + if (!PluginHelper::isEnabled('content', 'finder')) { + $this->finderPluginId = FinderHelper::getFinderPluginId(); + } + + // Configure the toolbar. + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Method to configure the toolbar for this view. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_finder'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_FINDER_INDEX_TOOLBAR_TITLE'), 'search-plus finder'); + + $toolbar->appendButton( + 'Popup', + 'archive', + 'COM_FINDER_INDEX', + 'index.php?option=com_finder&view=indexer&tmpl=component', + 500, + 210, + 0, + 0, + 'window.parent.location.reload()', + Text::_('COM_FINDER_HEADING_INDEXER') + ); + + if (!$this->isEmptyState) { + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('index.publish')->listCheck(true); + $childBar->unpublish('index.unpublish')->listCheck(true); + } + + $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); + + if ($canDo->get('core.delete')) { + $toolbar->confirmButton('', 'JTOOLBAR_DELETE', 'index.delete') + ->icon('icon-delete') + ->message('COM_FINDER_INDEX_CONFIRM_DELETE_PROMPT') + ->listCheck(true); + $toolbar->divider(); + } + + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('maintenance-group'); + $dropdown->text('COM_FINDER_INDEX_TOOLBAR_MAINTENANCE') + ->toggleSplit(false) + ->icon('icon-wrench') + ->buttonClass('btn btn-action'); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->standardButton('cog', 'COM_FINDER_INDEX_TOOLBAR_OPTIMISE', 'index.optimise', false); + $childBar->confirmButton('index.purge', 'COM_FINDER_INDEX_TOOLBAR_PURGE', 'index.purge') + ->icon('icon-trash') + ->message('COM_FINDER_INDEX_CONFIRM_PURGE_PROMPT'); + } + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_finder'); + } + + ToolbarHelper::help('Smart_Search:_Indexed_Content'); + } } diff --git a/administrator/components/com_finder/src/View/Indexer/HtmlView.php b/administrator/components/com_finder/src/View/Indexer/HtmlView.php index 981ffbbf6b5a6..53f754f501e10 100644 --- a/administrator/components/com_finder/src/View/Indexer/HtmlView.php +++ b/administrator/components/com_finder/src/View/Indexer/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->total = $this->get('Total'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if ($this->total === 0 && $this->isEmptyState = $this->get('isEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Prepare the view. - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Method to configure the toolbar for this view. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_finder'); - - ToolbarHelper::title(Text::_('COM_FINDER_MAPS_TOOLBAR_TITLE'), 'search-plus finder'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if (!$this->isEmptyState) - { - if ($canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('maps.publish')->listCheck(true); - $childBar->unpublish('maps.unpublish')->listCheck(true); - } - - ToolbarHelper::divider(); - $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); - ToolbarHelper::divider(); - - if ($canDo->get('core.delete')) - { - ToolbarHelper::deleteList('', 'maps.delete'); - ToolbarHelper::divider(); - } - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_finder'); - } - - ToolbarHelper::help('Smart_Search:_Content_Maps'); - } + /** + * An array of items + * + * @var array + * + * @since 3.6.1 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.6.1 + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.6.1 + */ + protected $state; + + /** + * The total number of items + * + * @var integer + * + * @since 3.6.1 + */ + protected $total; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Load plugin language files. + LanguageHelper::loadPluginLanguage(); + + // Load the view data. + $this->items = $this->get('Items'); + $this->total = $this->get('Total'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if ($this->total === 0 && $this->isEmptyState = $this->get('isEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Prepare the view. + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Method to configure the toolbar for this view. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_finder'); + + ToolbarHelper::title(Text::_('COM_FINDER_MAPS_TOOLBAR_TITLE'), 'search-plus finder'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if (!$this->isEmptyState) { + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('maps.publish')->listCheck(true); + $childBar->unpublish('maps.unpublish')->listCheck(true); + } + + ToolbarHelper::divider(); + $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); + ToolbarHelper::divider(); + + if ($canDo->get('core.delete')) { + ToolbarHelper::deleteList('', 'maps.delete'); + ToolbarHelper::divider(); + } + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_finder'); + } + + ToolbarHelper::help('Smart_Search:_Content_Maps'); + } } diff --git a/administrator/components/com_finder/src/View/Searches/HtmlView.php b/administrator/components/com_finder/src/View/Searches/HtmlView.php index b8029582de735..7b1f22e73cc93 100644 --- a/administrator/components/com_finder/src/View/Searches/HtmlView.php +++ b/administrator/components/com_finder/src/View/Searches/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->enabled = $this->state->params->get('gather_search_statistics', 0); - $this->canDo = ContentHelper::getActions('com_finder'); - $uri = Uri::getInstance(); - $link = 'index.php?option=com_config&view=component&component=com_finder&return=' . base64_encode($uri); - $output = HTMLHelper::_('link', Route::_($link), Text::_('JOPTIONS')); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check if component is enabled - if (!$this->enabled) - { - $app->enqueueMessage(Text::sprintf('COM_FINDER_LOGGING_DISABLED', $output), 'warning'); - } - - // Prepare the view. - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = $this->canDo; - - ToolbarHelper::title(Text::_('COM_FINDER_MANAGER_SEARCHES'), 'search'); - - if (!$this->isEmptyState) - { - if ($canDo->get('core.edit.state')) - { - ToolbarHelper::custom('searches.reset', 'refresh', '', 'JSEARCH_RESET', false); - } - - ToolbarHelper::divider(); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_finder'); - } - - ToolbarHelper::help('Smart_Search:_Search_Term_Analysis'); - } + /** + * True if gathering search statistics is enabled + * + * @var boolean + */ + protected $enabled; + + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * The actions the user is authorised to perform + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 4.0.0 + */ + protected $canDo; + + /** + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->enabled = $this->state->params->get('gather_search_statistics', 0); + $this->canDo = ContentHelper::getActions('com_finder'); + $uri = Uri::getInstance(); + $link = 'index.php?option=com_config&view=component&component=com_finder&return=' . base64_encode($uri); + $output = HTMLHelper::_('link', Route::_($link), Text::_('JOPTIONS')); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check if component is enabled + if (!$this->enabled) { + $app->enqueueMessage(Text::sprintf('COM_FINDER_LOGGING_DISABLED', $output), 'warning'); + } + + // Prepare the view. + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = $this->canDo; + + ToolbarHelper::title(Text::_('COM_FINDER_MANAGER_SEARCHES'), 'search'); + + if (!$this->isEmptyState) { + if ($canDo->get('core.edit.state')) { + ToolbarHelper::custom('searches.reset', 'refresh', '', 'JSEARCH_RESET', false); + } + + ToolbarHelper::divider(); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_finder'); + } + + ToolbarHelper::help('Smart_Search:_Search_Term_Analysis'); + } } diff --git a/administrator/components/com_finder/src/View/Statistics/HtmlView.php b/administrator/components/com_finder/src/View/Statistics/HtmlView.php index 1ccaf2fce7191..cfdff67708921 100644 --- a/administrator/components/com_finder/src/View/Statistics/HtmlView.php +++ b/administrator/components/com_finder/src/View/Statistics/HtmlView.php @@ -1,4 +1,5 @@ data = $this->get('Data'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - parent::display($tpl); - } + /** + * The index statistics + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.6.1 + */ + protected $data; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Load the view data. + $this->data = $this->get('Data'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + parent::display($tpl); + } } diff --git a/administrator/components/com_finder/tmpl/filter/edit.php b/administrator/components/com_finder/tmpl/filter/edit.php index 7cec87a044de4..1dd6f47bee024 100644 --- a/administrator/components/com_finder/tmpl/filter/edit.php +++ b/administrator/components/com_finder/tmpl/filter/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_finder.finder-edit'); + ->useScript('form.validate') + ->useScript('com_finder.finder-edit'); ?>
    - - -
    - 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - -
    -
    - total > 0) : ?> -
    - form->renderField('map_count'); ?> -
    - - - -
    - - - $this->filter->data)); ?> -
    -
    - -
    -
    - - - -
    -
    -
    - -
    - -
    -
    -
    -
    -
    - -
    - form->renderFieldset('jbasic'); ?> -
    -
    -
    -
    - - - - - - - - - -
    + + +
    + 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + +
    +
    + total > 0) : ?> +
    + form->renderField('map_count'); ?> +
    + + + +
    + + + $this->filter->data)); ?> +
    +
    + +
    +
    + + + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + form->renderFieldset('jbasic'); ?> +
    +
    +
    +
    + + + + + + + + + +
    diff --git a/administrator/components/com_finder/tmpl/filters/default.php b/administrator/components/com_finder/tmpl/filters/default.php index 3b38e40eec239..3be3fc9feeef3 100644 --- a/administrator/components/com_finder/tmpl/filters/default.php +++ b/administrator/components/com_finder/tmpl/filters/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_finder.filters') - ->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('table.columns') + ->useScript('multiselect'); ?>
    -
    -
    -
    - $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - authorise('core.create', 'com_finder'); - $canEdit = $user->authorise('core.edit', 'com_finder'); - $userAuthoriseCoreManage = $user->authorise('core.manage', 'com_checkin'); - $userAuthoriseCoreEditState = $user->authorise('core.edit.state', 'com_finder'); - $userId = $user->id; - foreach ($this->items as $i => $item) : - $canCheckIn = $userAuthoriseCoreManage || $item->checked_out == $userId || is_null($item->checked_out); - $canChange = $userAuthoriseCoreEditState && $canCheckIn; - $escapedTitle = $this->escape($item->title); - ?> - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - -
    - filter_id, false, 'cid', 'cb', $item->title); ?> - - state, $i, 'filters.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'filters.', $canCheckIn); ?> - - - - - - - - - created_by_alias ?: $item->user_name; ?> - - created, Text::_('DATE_FORMAT_LC4')); ?> - - map_count; ?> - - filter_id; ?> -
    +
    +
    +
    + $this)); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + authorise('core.create', 'com_finder'); + $canEdit = $user->authorise('core.edit', 'com_finder'); + $userAuthoriseCoreManage = $user->authorise('core.manage', 'com_checkin'); + $userAuthoriseCoreEditState = $user->authorise('core.edit.state', 'com_finder'); + $userId = $user->id; + foreach ($this->items as $i => $item) : + $canCheckIn = $userAuthoriseCoreManage || $item->checked_out == $userId || is_null($item->checked_out); + $canChange = $userAuthoriseCoreEditState && $canCheckIn; + $escapedTitle = $this->escape($item->title); + ?> + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + +
    + filter_id, false, 'cid', 'cb', $item->title); ?> + + state, $i, 'filters.', $canChange); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'filters.', $canCheckIn); ?> + + + + + + + + + created_by_alias ?: $item->user_name; ?> + + created, Text::_('DATE_FORMAT_LC4')); ?> + + map_count; ?> + + filter_id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
    -
    -
    + + + + +
    +
    +
    diff --git a/administrator/components/com_finder/tmpl/filters/emptystate.php b/administrator/components/com_finder/tmpl/filters/emptystate.php index aaa26c47d062f..bea0eab762273 100644 --- a/administrator/components/com_finder/tmpl/filters/emptystate.php +++ b/administrator/components/com_finder/tmpl/filters/emptystate.php @@ -1,4 +1,5 @@ 'COM_FINDER', - 'formURL' => 'index.php?option=com_finder&view=filters', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Smart_Search_quickstart_guide', - 'icon' => 'icon-search-plus finder', - 'btnadd' => Text::_('COM_FINDER_FILTERS_EMPTYSTATE_BUTTON_ADD'), - 'content' => Text::_('COM_FINDER_FILTERS_EMPTYSTATE_CONTENT'), - 'title' => Text::_('COM_FINDER_FILTERS_TOOLBAR_TITLE'), + 'textPrefix' => 'COM_FINDER', + 'formURL' => 'index.php?option=com_finder&view=filters', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Smart_Search_quickstart_guide', + 'icon' => 'icon-search-plus finder', + 'btnadd' => Text::_('COM_FINDER_FILTERS_EMPTYSTATE_BUTTON_ADD'), + 'content' => Text::_('COM_FINDER_FILTERS_EMPTYSTATE_CONTENT'), + 'title' => Text::_('COM_FINDER_FILTERS_TOOLBAR_TITLE'), ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_finder')) -{ - $displayData['createURL'] = "index.php?option=com_finder&task=filter.add"; +if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_finder')) { + $displayData['createURL'] = "index.php?option=com_finder&task=filter.add"; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_finder/tmpl/index/default.php b/administrator/components/com_finder/tmpl/index/default.php index 80b33832daa21..bc5d1c1279214 100644 --- a/administrator/components/com_finder/tmpl/index/default.php +++ b/administrator/components/com_finder/tmpl/index/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('multiselect') - ->useScript('table.columns'); + ->useScript('table.columns'); ?>
    -
    -
    -
    - $this)); ?> - finderPluginId) : ?> - finderPluginId . '&tmpl=component&layout=modal'); ?> - finderPluginId . 'Modal', - array( - 'url' => $link, - 'title' => Text::_('COM_FINDER_EDIT_PLUGIN_SETTINGS'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'closeButton' => false, - 'backdrop' => 'static', - 'keyboard' => false, - 'footer' => '' - . '' - . '' - ) - ); ?> - - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - - authorise('core.manage', 'com_finder'); ?> - items as $i => $item) : ?> - - - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - - - -
    - link_id, false, 'cid', 'cb', $item->title); ?> - - published, $i, 'index.', $canChange, 'cb'); ?> - - escape($item->title); ?> - - t_title); - echo $lang->hasKey($key) ? Text::_($key) : $item->t_title; - ?> - - indexdate, Text::_('DATE_FORMAT_LC4')); ?> - - - - publish_start_date or (int) $item->publish_end_date or (int) $item->start_date or (int) $item->end_date) : ?> - - - - - - - - url) > 80) ? substr($item->url, 0, 70) . '...' : $item->url; ?> -
    +
    +
    +
    + $this)); ?> + finderPluginId) : ?> + finderPluginId . '&tmpl=component&layout=modal'); ?> + finderPluginId . 'Modal', + array( + 'url' => $link, + 'title' => Text::_('COM_FINDER_EDIT_PLUGIN_SETTINGS'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'closeButton' => false, + 'backdrop' => 'static', + 'keyboard' => false, + 'footer' => '' + . '' + . '' + ) + ); ?> + + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + + + authorise('core.manage', 'com_finder'); ?> + items as $i => $item) : ?> + + + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + + + +
    + link_id, false, 'cid', 'cb', $item->title); ?> + + published, $i, 'index.', $canChange, 'cb'); ?> + + escape($item->title); ?> + + t_title); + echo $lang->hasKey($key) ? Text::_($key) : $item->t_title; + ?> + + indexdate, Text::_('DATE_FORMAT_LC4')); ?> + + + + publish_start_date or (int) $item->publish_end_date or (int) $item->start_date or (int) $item->end_date) : ?> + + + + + + + + url) > 80) ? substr($item->url, 0, 70) . '...' : $item->url; ?> +
    - - pagination->getListFooter(); ?> - + + pagination->getListFooter(); ?> + - - - -
    -
    -
    + + + +
    +
    +
    diff --git a/administrator/components/com_finder/tmpl/index/emptystate.php b/administrator/components/com_finder/tmpl/index/emptystate.php index dea524c6f41c4..e6d54ce79733c 100644 --- a/administrator/components/com_finder/tmpl/index/emptystate.php +++ b/administrator/components/com_finder/tmpl/index/emptystate.php @@ -1,4 +1,5 @@ 'COM_FINDER', - 'formURL' => 'index.php?option=com_finder&view=index', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Smart_Search_quickstart_guide', - 'icon' => 'icon-search-plus finder', - 'content' => Text::_('COM_FINDER_INDEX_NO_DATA') . '
    ' . Text::_('COM_FINDER_INDEX_TIP'), - 'title' => Text::_('COM_FINDER_HEADING_INDEXER'), - 'createURL' => "javascript:document.getElementsByClassName('button-archive')[0].click();", + 'textPrefix' => 'COM_FINDER', + 'formURL' => 'index.php?option=com_finder&view=index', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Smart_Search_quickstart_guide', + 'icon' => 'icon-search-plus finder', + 'content' => Text::_('COM_FINDER_INDEX_NO_DATA') . '
    ' . Text::_('COM_FINDER_INDEX_TIP'), + 'title' => Text::_('COM_FINDER_HEADING_INDEXER'), + 'createURL' => "javascript:document.getElementsByClassName('button-archive')[0].click();", ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); if ($this->finderPluginId) : ?> - finderPluginId . '&tmpl=component&layout=modal'); ?> - finderPluginId . 'Modal', - array( - 'url' => $link, - 'title' => Text::_('COM_FINDER_EDIT_PLUGIN_SETTINGS'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'closeButton' => false, - 'backdrop' => 'static', - 'keyboard' => false, - 'footer' => '' - . '' - . '' - ) - ); ?> - + finderPluginId . '&tmpl=component&layout=modal'); ?> + finderPluginId . 'Modal', + array( + 'url' => $link, + 'title' => Text::_('COM_FINDER_EDIT_PLUGIN_SETTINGS'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'closeButton' => false, + 'backdrop' => 'static', + 'keyboard' => false, + 'footer' => '' + . '' + . '' + ) + ); ?> +document->getWebAssetManager(); $wa->useScript('keepalive') - ->useStyle('com_finder.indexer') - ->useScript('com_finder.indexer'); + ->useStyle('com_finder.indexer') + ->useScript('com_finder.indexer'); ?>
    -

    -

    -
    -
    -
    - -
    -
    - - +

    +

    +
    +
    +
    + +
    +
    + +
    diff --git a/administrator/components/com_finder/tmpl/maps/default.php b/administrator/components/com_finder/tmpl/maps/default.php index 305e92d67867f..9466adc8267aa 100644 --- a/administrator/components/com_finder/tmpl/maps/default.php +++ b/administrator/components/com_finder/tmpl/maps/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_finder.maps') - ->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('table.columns') + ->useScript('multiselect'); ?>
    -
    -
    -
    - $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - - - getIdentity()->authorise('core.manage', 'com_finder'); ?> - items as $i => $item) : ?> - - - - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - - - -
    - id, false, 'cid', 'cb', $item->title); ?> - - state, $i, 'maps.', $canChange, 'cb'); ?> - - branch_title, '*') === 'Language') - { - $title = LanguageHelper::branchLanguageTitle($item->title); - } - else - { - $key = LanguageHelper::branchSingular($item->title); - $title = $lang->hasKey($key) ? Text::_($key) : $item->title; - } - ?> - —', $item->level - 1); ?> - escape($title); ?> - escape(trim($title, '*')) === 'Language' && Multilanguage::isEnabled()) : ?> -
    - -
    - -
    - rgt - $item->lft > 1) : ?> - - rgt - $item->lft) / 2); ?> - - - - - - - - level > 1) : ?> - - count_published; ?> - - - - - - - - level > 1) : ?> - - count_unpublished; ?> - - - - - - - - language; ?> -
    +
    +
    +
    + $this)); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + + + + getIdentity()->authorise('core.manage', 'com_finder'); ?> + items as $i => $item) : ?> + + + + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + + + +
    + id, false, 'cid', 'cb', $item->title); ?> + + state, $i, 'maps.', $canChange, 'cb'); ?> + + branch_title, '*') === 'Language') { + $title = LanguageHelper::branchLanguageTitle($item->title); + } else { + $key = LanguageHelper::branchSingular($item->title); + $title = $lang->hasKey($key) ? Text::_($key) : $item->title; + } + ?> + —', $item->level - 1); ?> + escape($title); ?> + escape(trim($title, '*')) === 'Language' && Multilanguage::isEnabled()) : ?> +
    + +
    + +
    + rgt - $item->lft > 1) : ?> + + rgt - $item->lft) / 2); ?> + + + + - + + + level > 1) : ?> + + count_published; ?> + + + + - + + + level > 1) : ?> + + count_unpublished; ?> + + + + - + + + language; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - -
    + +
    - - - -
    -
    + + + +
    +
    diff --git a/administrator/components/com_finder/tmpl/maps/emptystate.php b/administrator/components/com_finder/tmpl/maps/emptystate.php index 3675b05b6677b..301df7df10f8a 100644 --- a/administrator/components/com_finder/tmpl/maps/emptystate.php +++ b/administrator/components/com_finder/tmpl/maps/emptystate.php @@ -1,4 +1,5 @@ 'COM_FINDER', - 'formURL' => 'index.php?option=com_finder&view=maps', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Smart_Search:_Content_Maps', - 'icon' => 'icon-search-plus finder', - 'title' => Text::_('COM_FINDER_MAPS_TOOLBAR_TITLE') + 'textPrefix' => 'COM_FINDER', + 'formURL' => 'index.php?option=com_finder&view=maps', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Smart_Search:_Content_Maps', + 'icon' => 'icon-search-plus finder', + 'title' => Text::_('COM_FINDER_MAPS_TOOLBAR_TITLE') ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_finder/tmpl/searches/default.php b/administrator/components/com_finder/tmpl/searches/default.php index b0557ece11f48..81dc67ee05789 100644 --- a/administrator/components/com_finder/tmpl/searches/default.php +++ b/administrator/components/com_finder/tmpl/searches/default.php @@ -1,4 +1,5 @@
    -
    -
    -
    - $this, 'options' => array('filterButton' => false))); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - items as $i => $item) : ?> - - - - - - - -
    - , - , - -
    - - - - - -
    - escape($item->searchterm); ?> - - hits; ?> - - results; ?> -
    +
    +
    +
    + $this, 'options' => array('filterButton' => false))); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + +
    + , + , + +
    + + + + + +
    + escape($item->searchterm); ?> + + hits; ?> + + results; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
    -
    -
    + + + + +
    +
    +
    diff --git a/administrator/components/com_finder/tmpl/searches/emptystate.php b/administrator/components/com_finder/tmpl/searches/emptystate.php index 27c523194d499..f43471bc5aede 100644 --- a/administrator/components/com_finder/tmpl/searches/emptystate.php +++ b/administrator/components/com_finder/tmpl/searches/emptystate.php @@ -1,4 +1,5 @@ 'COM_FINDER', - 'formURL' => 'index.php?option=com_finder&view=searches', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Smart_Search:_Search_Term_Analysis', - 'icon' => 'icon-search', - 'title' => Text::_('COM_FINDER_MANAGER_SEARCHES'), - 'content' => Text::_('COM_FINDER_EMPTYSTATE_SEARCHES_CONTENT'), + 'textPrefix' => 'COM_FINDER', + 'formURL' => 'index.php?option=com_finder&view=searches', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Smart_Search:_Search_Term_Analysis', + 'icon' => 'icon-search', + 'title' => Text::_('COM_FINDER_MANAGER_SEARCHES'), + 'content' => Text::_('COM_FINDER_EMPTYSTATE_SEARCHES_CONTENT'), ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_finder/tmpl/statistics/default.php b/administrator/components/com_finder/tmpl/statistics/default.php index 2726038bc966b..4c2e2444c3cff 100644 --- a/administrator/components/com_finder/tmpl/statistics/default.php +++ b/administrator/components/com_finder/tmpl/statistics/default.php @@ -1,4 +1,5 @@
    - - - - - - - - - - data->type_list as $type) : ?> - - - - - - - - - - -
    data->term_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->taxonomy_node_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->taxonomy_branch_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR'))); ?>
    - - - -
    - type_title); - $lang_string = Text::_($lang_key); - echo $lang_string === $lang_key ? $type->type_title : $lang_string; - ?> - - link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> -
    - - - data->link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> -
    + + + + + + + + + + data->type_list as $type) : ?> + + + + + + + + + + +
    data->term_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->taxonomy_node_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->taxonomy_branch_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR'))); ?>
    + + + +
    + type_title); + $lang_string = Text::_($lang_key); + echo $lang_string === $lang_key ? $type->type_title : $lang_string; + ?> + + link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> +
    + + + data->link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> +
    diff --git a/administrator/components/com_installer/helpers/installer.php b/administrator/components/com_installer/helpers/installer.php index 59ccf41a9a435..bc72931178d55 100644 --- a/administrator/components/com_installer/helpers/installer.php +++ b/administrator/components/com_installer/helpers/installer.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Installer')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Installer')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Installer')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Installer')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new InstallerComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new InstallerComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_installer/src/Controller/DatabaseController.php b/administrator/components/com_installer/src/Controller/DatabaseController.php index 8b54fc5efda46..00ae2524d9853 100644 --- a/administrator/components/com_installer/src/Controller/DatabaseController.php +++ b/administrator/components/com_installer/src/Controller/DatabaseController.php @@ -1,4 +1,5 @@ checkToken(); - - // Get items to fix the database. - $cid = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $cid = array_filter($cid); - - if (empty($cid)) - { - $this->app->getLogger()->warning( - Text::_( - 'COM_INSTALLER_ERROR_NO_EXTENSIONS_SELECTED' - ), array('category' => 'jerror') - ); - } - else - { - /** @var DatabaseModel $model */ - $model = $this->getModel('Database'); - $model->fix($cid); - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $updateModel */ - $updateModel = $this->app->bootComponent('com_joomlaupdate') - ->getMVCFactory()->createModel('Update', 'Administrator', ['ignore_request' => true]); - $updateModel->purge(); - - // Refresh versionable assets cache - $this->app->flushAssets(); - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=database', false)); - } - - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Database'); - - $changeSet = $model->getItems(); - - $changeSetCount = 0; - - foreach ($changeSet as $item) - { - $changeSetCount += $item['errorsCount']; - } - - echo new JsonResponse($changeSetCount); - } + /** + * Tries to fix missing database updates + * + * @return void + * + * @throws \Exception + * + * @since 2.5 + * @todo Purge updates has to be replaced with an events system + */ + public function fix() + { + // Check for request forgeries. + $this->checkToken(); + + // Get items to fix the database. + $cid = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $cid = array_filter($cid); + + if (empty($cid)) { + $this->app->getLogger()->warning( + Text::_( + 'COM_INSTALLER_ERROR_NO_EXTENSIONS_SELECTED' + ), + array('category' => 'jerror') + ); + } else { + /** @var DatabaseModel $model */ + $model = $this->getModel('Database'); + $model->fix($cid); + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $updateModel */ + $updateModel = $this->app->bootComponent('com_joomlaupdate') + ->getMVCFactory()->createModel('Update', 'Administrator', ['ignore_request' => true]); + $updateModel->purge(); + + // Refresh versionable assets cache + $this->app->flushAssets(); + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=database', false)); + } + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Database'); + + $changeSet = $model->getItems(); + + $changeSetCount = 0; + + foreach ($changeSet as $item) { + $changeSetCount += $item['errorsCount']; + } + + echo new JsonResponse($changeSetCount); + } } diff --git a/administrator/components/com_installer/src/Controller/DiscoverController.php b/administrator/components/com_installer/src/Controller/DiscoverController.php index 65a74580f8970..3c593f7351586 100644 --- a/administrator/components/com_installer/src/Controller/DiscoverController.php +++ b/administrator/components/com_installer/src/Controller/DiscoverController.php @@ -1,4 +1,5 @@ checkToken('request'); + /** + * Refreshes the cache of discovered extensions. + * + * @return void + * + * @since 1.6 + */ + public function refresh() + { + $this->checkToken('request'); - /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ - $model = $this->getModel('discover'); - $model->discover(); + /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ + $model = $this->getModel('discover'); + $model->discover(); - if (!$model->getTotal()) - { - $this->setMessage(Text::_('COM_INSTALLER_ERROR_NO_EXTENSIONS_DISCOVERED'), 'info'); - } + if (!$model->getTotal()) { + $this->setMessage(Text::_('COM_INSTALLER_ERROR_NO_EXTENSIONS_DISCOVERED'), 'info'); + } - $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false)); - } + $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false)); + } - /** - * Install a discovered extension. - * - * @return void - * - * @since 1.6 - */ - public function install() - { - $this->checkToken(); + /** + * Install a discovered extension. + * + * @return void + * + * @since 1.6 + */ + public function install() + { + $this->checkToken(); - /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ - $model = $this->getModel('discover'); - $model->discover_install(); - $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false)); - } + /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ + $model = $this->getModel('discover'); + $model->discover_install(); + $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false)); + } - /** - * Clean out the discovered extension cache. - * - * @return void - * - * @since 1.6 - */ - public function purge() - { - $this->checkToken('request'); + /** + * Clean out the discovered extension cache. + * + * @return void + * + * @since 1.6 + */ + public function purge() + { + $this->checkToken('request'); - /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ - $model = $this->getModel('discover'); - $model->purge(); - $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false), $model->_message); - } + /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ + $model = $this->getModel('discover'); + $model->purge(); + $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false), $model->_message); + } - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } - $model = $this->getModel('Discover'); + $model = $this->getModel('Discover'); - echo new JsonResponse($model->getTotal()); - } + echo new JsonResponse($model->getTotal()); + } } diff --git a/administrator/components/com_installer/src/Controller/DisplayController.php b/administrator/components/com_installer/src/Controller/DisplayController.php index 9584877bdb194..8430edb89c43d 100644 --- a/administrator/components/com_installer/src/Controller/DisplayController.php +++ b/administrator/components/com_installer/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'install'); - $vFormat = $document->getType(); - $lName = $this->input->get('layout', 'default', 'string'); - $id = $this->input->getInt('update_site_id'); - - // Check for edit form. - if ($vName === 'updatesite' && $lName === 'edit' && !$this->checkEditId('com_installer.edit.updatesite', $id)) - { - // 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', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); - - $this->redirect(); - } - - // Get and render the view. - if ($view = $this->getView($vName, $vFormat)) - { - // Get the model for the view. - $model = $this->getModel($vName); - - // Push the model into the view (as default). - $view->setModel($model, true); - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - - $view->display(); - } - - return $this; - } - - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Warnings'); - - echo new JsonResponse(count($model->getItems())); - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'install'); + $vFormat = $document->getType(); + $lName = $this->input->get('layout', 'default', 'string'); + $id = $this->input->getInt('update_site_id'); + + // Check for edit form. + if ($vName === 'updatesite' && $lName === 'edit' && !$this->checkEditId('com_installer.edit.updatesite', $id)) { + // 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', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); + + $this->redirect(); + } + + // Get and render the view. + if ($view = $this->getView($vName, $vFormat)) { + // Get the model for the view. + $model = $this->getModel($vName); + + // Push the model into the view (as default). + $view->setModel($model, true); + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + + $view->display(); + } + + return $this; + } + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Warnings'); + + echo new JsonResponse(count($model->getItems())); + } } diff --git a/administrator/components/com_installer/src/Controller/InstallController.php b/administrator/components/com_installer/src/Controller/InstallController.php index 3382e5e6d9746..de84dc7046039 100644 --- a/administrator/components/com_installer/src/Controller/InstallController.php +++ b/administrator/components/com_installer/src/Controller/InstallController.php @@ -1,4 +1,5 @@ checkToken(); - - if (!$this->app->getIdentity()->authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $model */ - $model = $this->getModel('install'); - - // @todo: Reset the users acl here as well to kill off any missing bits. - $result = $model->install(); - - $app = $this->app; - $redirect_url = $app->getUserState('com_installer.redirect_url'); - $return = $this->input->getBase64('return'); - - if (!$redirect_url && $return) - { - $redirect_url = base64_decode($return); - } - - // Don't redirect to an external URL. - if ($redirect_url && !Uri::isInternal($redirect_url)) - { - $redirect_url = ''; - } - - if (empty($redirect_url)) - { - $redirect_url = Route::_('index.php?option=com_installer&view=install', false); - } - else - { - // Wipe out the user state when we're going to redirect. - $app->setUserState('com_installer.redirect_url', ''); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - } - - $this->setRedirect($redirect_url); - - return $result; - } - - /** - * Install an extension from drag & drop ajax upload. - * - * @return void - * - * @since 3.7.0 - */ - public function ajax_upload() - { - // Check for request forgeries. - Session::checkToken() or jexit(Text::_('JINVALID_TOKEN')); - - if (!$this->app->getIdentity()->authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $message = $this->app->getUserState('com_installer.message'); - - // Do install - $result = $this->install(); - - // Get redirect URL - $redirect = $this->redirect; - - // Push message queue to session because we will redirect page by \Javascript, not $app->redirect(). - // The "application.queue" is only set in redirect() method, so we must manually store it. - $this->app->getSession()->set('application.queue', $this->app->getMessageQueue()); - - header('Content-Type: application/json'); - - echo new JsonResponse(array('redirect' => $redirect), $message, !$result); - - $this->app->close(); - } + /** + * Install an extension. + * + * @return mixed + * + * @since 1.5 + */ + public function install() + { + // Check for request forgeries. + $this->checkToken(); + + if (!$this->app->getIdentity()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $model */ + $model = $this->getModel('install'); + + // @todo: Reset the users acl here as well to kill off any missing bits. + $result = $model->install(); + + $app = $this->app; + $redirect_url = $app->getUserState('com_installer.redirect_url'); + $return = $this->input->getBase64('return'); + + if (!$redirect_url && $return) { + $redirect_url = base64_decode($return); + } + + // Don't redirect to an external URL. + if ($redirect_url && !Uri::isInternal($redirect_url)) { + $redirect_url = ''; + } + + if (empty($redirect_url)) { + $redirect_url = Route::_('index.php?option=com_installer&view=install', false); + } else { + // Wipe out the user state when we're going to redirect. + $app->setUserState('com_installer.redirect_url', ''); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + } + + $this->setRedirect($redirect_url); + + return $result; + } + + /** + * Install an extension from drag & drop ajax upload. + * + * @return void + * + * @since 3.7.0 + */ + public function ajax_upload() + { + // Check for request forgeries. + Session::checkToken() or jexit(Text::_('JINVALID_TOKEN')); + + if (!$this->app->getIdentity()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $message = $this->app->getUserState('com_installer.message'); + + // Do install + $result = $this->install(); + + // Get redirect URL + $redirect = $this->redirect; + + // Push message queue to session because we will redirect page by \Javascript, not $app->redirect(). + // The "application.queue" is only set in redirect() method, so we must manually store it. + $this->app->getSession()->set('application.queue', $this->app->getMessageQueue()); + + header('Content-Type: application/json'); + + echo new JsonResponse(array('redirect' => $redirect), $message, !$result); + + $this->app->close(); + } } diff --git a/administrator/components/com_installer/src/Controller/ManageController.php b/administrator/components/com_installer/src/Controller/ManageController.php index 3496706ad99d7..1b05af2cfb055 100644 --- a/administrator/components/com_installer/src/Controller/ManageController.php +++ b/administrator/components/com_installer/src/Controller/ManageController.php @@ -1,4 +1,5 @@ registerTask('unpublish', 'publish'); - $this->registerTask('publish', 'publish'); - } - - /** - * Enable/Disable an extension (if supported). - * - * @return void - * - * @throws \Exception - * - * @since 1.6 - */ - public function publish() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('publish' => 1, 'unpublish' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->setMessage(Text::_('COM_INSTALLER_ERROR_NO_EXTENSIONS_SELECTED'), 'warning'); - } - else - { - /** @var ManageModel $model */ - $model = $this->getModel('manage'); - - // Change the state of the records. - if (!$model->publish($ids, $value)) - { - $this->setMessage(implode('
    ', $model->getErrors()), 'warning'); - } - else - { - if ($value == 1) - { - $ntext = 'COM_INSTALLER_N_EXTENSIONS_PUBLISHED'; - } - else - { - $ntext = 'COM_INSTALLER_N_EXTENSIONS_UNPUBLISHED'; - } - - $this->setMessage(Text::plural($ntext, count($ids))); - } - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); - } - - /** - * Remove an extension (Uninstall). - * - * @return void - * - * @throws \Exception - * - * @since 1.5 - */ - public function remove() - { - // Check for request forgeries. - $this->checkToken(); - - $eid = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $eid = array_filter($eid); - - if (!empty($eid)) - { - /** @var ManageModel $model */ - $model = $this->getModel('manage'); - - $model->remove($eid); - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); - } - - /** - * Refreshes the cached metadata about an extension. - * - * Useful for debugging and testing purposes when the XML file might change. - * - * @return void - * - * @since 1.6 - */ - public function refresh() - { - // Check for request forgeries. - $this->checkToken(); - - $uid = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $uid = array_filter($uid); - - if (!empty($uid)) - { - /** @var ManageModel $model */ - $model = $this->getModel('manage'); - - $model->refresh($uid); - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); - } - - /** - * Load the changelog for a given extension. - * - * @return void - * - * @since 4.0.0 - */ - public function loadChangelog() - { - /** @var ManageModel $model */ - $model = $this->getModel('manage'); - - $eid = $this->input->get('eid', 0, 'int'); - $source = $this->input->get('source', 'manage', 'string'); - - if (!$eid) - { - return; - } - - $output = $model->loadChangelog($eid, $source); - - echo (new JsonResponse($output)); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('unpublish', 'publish'); + $this->registerTask('publish', 'publish'); + } + + /** + * Enable/Disable an extension (if supported). + * + * @return void + * + * @throws \Exception + * + * @since 1.6 + */ + public function publish() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('publish' => 1, 'unpublish' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->setMessage(Text::_('COM_INSTALLER_ERROR_NO_EXTENSIONS_SELECTED'), 'warning'); + } else { + /** @var ManageModel $model */ + $model = $this->getModel('manage'); + + // Change the state of the records. + if (!$model->publish($ids, $value)) { + $this->setMessage(implode('
    ', $model->getErrors()), 'warning'); + } else { + if ($value == 1) { + $ntext = 'COM_INSTALLER_N_EXTENSIONS_PUBLISHED'; + } else { + $ntext = 'COM_INSTALLER_N_EXTENSIONS_UNPUBLISHED'; + } + + $this->setMessage(Text::plural($ntext, count($ids))); + } + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); + } + + /** + * Remove an extension (Uninstall). + * + * @return void + * + * @throws \Exception + * + * @since 1.5 + */ + public function remove() + { + // Check for request forgeries. + $this->checkToken(); + + $eid = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $eid = array_filter($eid); + + if (!empty($eid)) { + /** @var ManageModel $model */ + $model = $this->getModel('manage'); + + $model->remove($eid); + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); + } + + /** + * Refreshes the cached metadata about an extension. + * + * Useful for debugging and testing purposes when the XML file might change. + * + * @return void + * + * @since 1.6 + */ + public function refresh() + { + // Check for request forgeries. + $this->checkToken(); + + $uid = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $uid = array_filter($uid); + + if (!empty($uid)) { + /** @var ManageModel $model */ + $model = $this->getModel('manage'); + + $model->refresh($uid); + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); + } + + /** + * Load the changelog for a given extension. + * + * @return void + * + * @since 4.0.0 + */ + public function loadChangelog() + { + /** @var ManageModel $model */ + $model = $this->getModel('manage'); + + $eid = $this->input->get('eid', 0, 'int'); + $source = $this->input->get('source', 'manage', 'string'); + + if (!$eid) { + return; + } + + $output = $model->loadChangelog($eid, $source); + + echo (new JsonResponse($output)); + } } diff --git a/administrator/components/com_installer/src/Controller/UpdateController.php b/administrator/components/com_installer/src/Controller/UpdateController.php index cc3a6e89f6372..8d200dd4fc08e 100644 --- a/administrator/components/com_installer/src/Controller/UpdateController.php +++ b/administrator/components/com_installer/src/Controller/UpdateController.php @@ -1,4 +1,5 @@ checkToken(); - - /** @var UpdateModel $model */ - $model = $this->getModel('update'); - - $uid = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $uid = array_filter($uid); - - // Get the minimum stability. - $params = ComponentHelper::getComponent('com_installer')->getParams(); - $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); - - $model->update($uid, $minimum_stability); - - $app = $this->app; - $redirect_url = $app->getUserState('com_installer.redirect_url'); - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect_url)) - { - $redirect_url = ''; - } - - if (empty($redirect_url)) - { - $redirect_url = Route::_('index.php?option=com_installer&view=update', false); - } - else - { - // Wipe out the user state when we're going to redirect. - $app->setUserState('com_installer.redirect_url', ''); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - } - - $this->setRedirect($redirect_url); - } - - /** - * Find new updates. - * - * @return void - * - * @since 1.6 - */ - public function find() - { - $this->checkToken('request'); - - // Get the caching duration. - $params = ComponentHelper::getComponent('com_installer')->getParams(); - $cache_timeout = (int) $params->get('cachetimeout', 6); - $cache_timeout = 3600 * $cache_timeout; - - // Get the minimum stability. - $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); - - // Find updates. - /** @var UpdateModel $model */ - $model = $this->getModel('update'); - - // Purge the table before checking again - $model->purge(); - - $disabledUpdateSites = $model->getDisabledUpdateSites(); - - if ($disabledUpdateSites) - { - $updateSitesUrl = Route::_('index.php?option=com_installer&view=updatesites'); - $this->app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATE_SITES_COUNT_CHECK', $updateSitesUrl), 'warning'); - } - - $model->findUpdates(0, $cache_timeout, $minimum_stability); - - if (0 === $model->getTotal()) - { - $this->app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATE_NOUPDATES'), 'info'); - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=update', false)); - } - - /** - * Fetch and report updates in \JSON format, for AJAX requests - * - * @return void - * - * @since 2.5 - */ - public function ajax() - { - $app = $this->app; - - if (!Session::checkToken('get')) - { - $app->setHeader('status', 403, true); - $app->sendHeaders(); - echo Text::_('JINVALID_TOKEN_NOTICE'); - $app->close(); - } - - // Close the session before we make a long running request - $app->getSession()->abort(); - - $eid = $this->input->getInt('eid', 0); - $skip = $this->input->get('skip', array(), 'array'); - $cache_timeout = $this->input->getInt('cache_timeout', 0); - $minimum_stability = $this->input->getInt('minimum_stability', -1); - - $params = ComponentHelper::getComponent('com_installer')->getParams(); - - if ($cache_timeout == 0) - { - $cache_timeout = (int) $params->get('cachetimeout', 6); - $cache_timeout = 3600 * $cache_timeout; - } - - if ($minimum_stability < 0) - { - $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); - } - - /** @var UpdateModel $model */ - $model = $this->getModel('update'); - $model->findUpdates($eid, $cache_timeout, $minimum_stability); - - $model->setState('list.start', 0); - $model->setState('list.limit', 0); - - if ($eid != 0) - { - $model->setState('filter.extension_id', $eid); - } - - $updates = $model->getItems(); - - if (!empty($skip)) - { - $unfiltered_updates = $updates; - $updates = array(); - - foreach ($unfiltered_updates as $update) - { - if (!in_array($update->extension_id, $skip)) - { - $updates[] = $update; - } - } - } - - echo json_encode($updates); - - $app->close(); - } - - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Update'); - - echo new JsonResponse($model->getTotal()); - } + /** + * Update a set of extensions. + * + * @return void + * + * @since 1.6 + */ + public function update() + { + // Check for request forgeries. + $this->checkToken(); + + /** @var UpdateModel $model */ + $model = $this->getModel('update'); + + $uid = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $uid = array_filter($uid); + + // Get the minimum stability. + $params = ComponentHelper::getComponent('com_installer')->getParams(); + $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); + + $model->update($uid, $minimum_stability); + + $app = $this->app; + $redirect_url = $app->getUserState('com_installer.redirect_url'); + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect_url)) { + $redirect_url = ''; + } + + if (empty($redirect_url)) { + $redirect_url = Route::_('index.php?option=com_installer&view=update', false); + } else { + // Wipe out the user state when we're going to redirect. + $app->setUserState('com_installer.redirect_url', ''); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + } + + $this->setRedirect($redirect_url); + } + + /** + * Find new updates. + * + * @return void + * + * @since 1.6 + */ + public function find() + { + $this->checkToken('request'); + + // Get the caching duration. + $params = ComponentHelper::getComponent('com_installer')->getParams(); + $cache_timeout = (int) $params->get('cachetimeout', 6); + $cache_timeout = 3600 * $cache_timeout; + + // Get the minimum stability. + $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); + + // Find updates. + /** @var UpdateModel $model */ + $model = $this->getModel('update'); + + // Purge the table before checking again + $model->purge(); + + $disabledUpdateSites = $model->getDisabledUpdateSites(); + + if ($disabledUpdateSites) { + $updateSitesUrl = Route::_('index.php?option=com_installer&view=updatesites'); + $this->app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATE_SITES_COUNT_CHECK', $updateSitesUrl), 'warning'); + } + + $model->findUpdates(0, $cache_timeout, $minimum_stability); + + if (0 === $model->getTotal()) { + $this->app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATE_NOUPDATES'), 'info'); + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=update', false)); + } + + /** + * Fetch and report updates in \JSON format, for AJAX requests + * + * @return void + * + * @since 2.5 + */ + public function ajax() + { + $app = $this->app; + + if (!Session::checkToken('get')) { + $app->setHeader('status', 403, true); + $app->sendHeaders(); + echo Text::_('JINVALID_TOKEN_NOTICE'); + $app->close(); + } + + // Close the session before we make a long running request + $app->getSession()->abort(); + + $eid = $this->input->getInt('eid', 0); + $skip = $this->input->get('skip', array(), 'array'); + $cache_timeout = $this->input->getInt('cache_timeout', 0); + $minimum_stability = $this->input->getInt('minimum_stability', -1); + + $params = ComponentHelper::getComponent('com_installer')->getParams(); + + if ($cache_timeout == 0) { + $cache_timeout = (int) $params->get('cachetimeout', 6); + $cache_timeout = 3600 * $cache_timeout; + } + + if ($minimum_stability < 0) { + $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); + } + + /** @var UpdateModel $model */ + $model = $this->getModel('update'); + $model->findUpdates($eid, $cache_timeout, $minimum_stability); + + $model->setState('list.start', 0); + $model->setState('list.limit', 0); + + if ($eid != 0) { + $model->setState('filter.extension_id', $eid); + } + + $updates = $model->getItems(); + + if (!empty($skip)) { + $unfiltered_updates = $updates; + $updates = array(); + + foreach ($unfiltered_updates as $update) { + if (!in_array($update->extension_id, $skip)) { + $updates[] = $update; + } + } + } + + echo json_encode($updates); + + $app->close(); + } + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Update'); + + echo new JsonResponse($model->getTotal()); + } } diff --git a/administrator/components/com_installer/src/Controller/UpdatesiteController.php b/administrator/components/com_installer/src/Controller/UpdatesiteController.php index 1ce791f760e02..f36ccd65dad8d 100644 --- a/administrator/components/com_installer/src/Controller/UpdatesiteController.php +++ b/administrator/components/com_installer/src/Controller/UpdatesiteController.php @@ -1,4 +1,5 @@ registerTask('unpublish', 'publish'); - $this->registerTask('publish', 'publish'); - $this->registerTask('delete', 'delete'); - $this->registerTask('rebuild', 'rebuild'); - } - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config The array of possible config values. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel - * - * @since 4.0.0 - */ - public function getModel($name = 'Updatesite', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Enable/Disable an extension (if supported). - * - * @return void - * - * @since 3.4 - * - * @throws \Exception on error - */ - public function publish() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('publish' => 1, 'unpublish' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - throw new \Exception(Text::_('COM_INSTALLER_ERROR_NO_UPDATESITES_SELECTED'), 500); - } - - // Get the model. - /** @var \Joomla\Component\Installer\Administrator\Model\UpdatesitesModel $model */ - $model = $this->getModel('Updatesites'); - - // Change the state of the records. - if (!$model->publish($ids, $value)) - { - throw new \Exception(implode('
    ', $model->getErrors()), 500); - } - - $ntext = ($value == 0) ? 'COM_INSTALLER_N_UPDATESITES_UNPUBLISHED' : 'COM_INSTALLER_N_UPDATESITES_PUBLISHED'; - - $this->setMessage(Text::plural($ntext, count($ids))); - - $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); - } - - /** - * Deletes an update site (if supported). - * - * @return void - * - * @since 3.6 - * - * @throws \Exception on error - */ - public function delete() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - throw new \Exception(Text::_('COM_INSTALLER_ERROR_NO_UPDATESITES_SELECTED'), 500); - } - - // Delete the records. - $this->getModel('Updatesites')->delete($ids); - - $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); - } - - /** - * Rebuild update sites tables. - * - * @return void - * - * @since 3.6 - */ - public function rebuild() - { - // Check for request forgeries. - $this->checkToken(); - - // Rebuild the update sites. - $this->getModel('Updatesites')->rebuild(); - - $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); - } + /** + * The prefix to use with controller messages. + * + * @var string + * @since 4.0.0 + */ + protected $text_prefix = 'COM_INSTALLER_UPDATESITES'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('unpublish', 'publish'); + $this->registerTask('publish', 'publish'); + $this->registerTask('delete', 'delete'); + $this->registerTask('rebuild', 'rebuild'); + } + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since 4.0.0 + */ + public function getModel($name = 'Updatesite', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Enable/Disable an extension (if supported). + * + * @return void + * + * @since 3.4 + * + * @throws \Exception on error + */ + public function publish() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('publish' => 1, 'unpublish' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + throw new \Exception(Text::_('COM_INSTALLER_ERROR_NO_UPDATESITES_SELECTED'), 500); + } + + // Get the model. + /** @var \Joomla\Component\Installer\Administrator\Model\UpdatesitesModel $model */ + $model = $this->getModel('Updatesites'); + + // Change the state of the records. + if (!$model->publish($ids, $value)) { + throw new \Exception(implode('
    ', $model->getErrors()), 500); + } + + $ntext = ($value == 0) ? 'COM_INSTALLER_N_UPDATESITES_UNPUBLISHED' : 'COM_INSTALLER_N_UPDATESITES_PUBLISHED'; + + $this->setMessage(Text::plural($ntext, count($ids))); + + $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); + } + + /** + * Deletes an update site (if supported). + * + * @return void + * + * @since 3.6 + * + * @throws \Exception on error + */ + public function delete() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + throw new \Exception(Text::_('COM_INSTALLER_ERROR_NO_UPDATESITES_SELECTED'), 500); + } + + // Delete the records. + $this->getModel('Updatesites')->delete($ids); + + $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); + } + + /** + * Rebuild update sites tables. + * + * @return void + * + * @since 3.6 + */ + public function rebuild() + { + // Check for request forgeries. + $this->checkToken(); + + // Rebuild the update sites. + $this->getModel('Updatesites')->rebuild(); + + $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); + } } diff --git a/administrator/components/com_installer/src/Extension/InstallerComponent.php b/administrator/components/com_installer/src/Extension/InstallerComponent.php index 2a48011a0fc4b..9b14efe4f5cf6 100644 --- a/administrator/components/com_installer/src/Extension/InstallerComponent.php +++ b/administrator/components/com_installer/src/Extension/InstallerComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('manage', new Manage); - $this->getRegistry()->register('updatesites', new Updatesites); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('manage', new Manage()); + $this->getRegistry()->register('updatesites', new Updatesites()); + } } diff --git a/administrator/components/com_installer/src/Field/ExtensionstatusField.php b/administrator/components/com_installer/src/Field/ExtensionstatusField.php index 5a418cb5f2494..01f6e2db8e0e9 100644 --- a/administrator/components/com_installer/src/Field/ExtensionstatusField.php +++ b/administrator/components/com_installer/src/Field/ExtensionstatusField.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('DISTINCT ' . $db->quoteName('type')) - ->from($db->quoteName('#__extensions')); - $db->setQuery($query); - $types = $db->loadColumn(); - - $options = array(); - - foreach ($types as $type) - { - $options[] = HTMLHelper::_('select.option', $type, Text::_('COM_INSTALLER_TYPE_' . strtoupper($type))); - } - - return $options; - } - - /** - * Get a list of filter options for the extension types. - * - * @return array An array of \stdClass objects. - * - * @since 3.0 - */ - public static function getExtensionGroups() - { - $nofolder = ''; - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('folder')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' != :folder') - ->bind(':folder', $nofolder) - ->order($db->quoteName('folder')); - $db->setQuery($query); - $folders = $db->loadColumn(); - - $options = array(); - - foreach ($folders as $folder) - { - $options[] = HTMLHelper::_('select.option', $folder, $folder); - } - - return $options; - } - - /** - * Get a list of filter options for the application clients. - * - * @return array An array of \JHtmlOption elements. - * - * @since 3.5 - */ - public static function getClientOptions() - { - // Build the filter options. - $options = array(); - $options[] = HTMLHelper::_('select.option', '0', Text::_('JSITE')); - $options[] = HTMLHelper::_('select.option', '1', Text::_('JADMINISTRATOR')); - $options[] = HTMLHelper::_('select.option', '3', Text::_('JAPI')); - - return $options; - } - - /** - * Get a list of filter options for the application statuses. - * - * @return array An array of \JHtmlOption elements. - * - * @since 3.5 - */ - public static function getStateOptions() - { - // Build the filter options. - $options = array(); - $options[] = HTMLHelper::_('select.option', '0', Text::_('JDISABLED')); - $options[] = HTMLHelper::_('select.option', '1', Text::_('JENABLED')); - $options[] = HTMLHelper::_('select.option', '2', Text::_('JPROTECTED')); - $options[] = HTMLHelper::_('select.option', '3', Text::_('JUNPROTECTED')); - - return $options; - } - - /** - * Get a list of filter options for extensions of the "package" type. - * - * @return array - * @since 4.2.0 - */ - public static function getPackageOptions(): array - { - $options = []; - - /** @var DatabaseDriver $db The application's database driver object */ - $db = Factory::getContainer()->get(DatabaseDriver::class); - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 'extension_id', - 'name', - 'element', - ] - ) - ) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('package')); - $extensions = $db->setQuery($query)->loadObjectList() ?: []; - - if (empty($extensions)) - { - return $options; - } - - $language = Factory::getApplication()->getLanguage(); - $arrayKeys = array_map( - function (object $entry) use ($language): string - { - $language->load($entry->element, JPATH_ADMINISTRATOR); - - return Text::_($entry->name); - }, - $extensions - ); - $arrayValues = array_map( - function (object $entry): int - { - return $entry->extension_id; - }, - $extensions - ); - - $extensions = array_combine($arrayKeys, $arrayValues); - ksort($extensions); - - foreach ($extensions as $label => $id) - { - $options[] = HTMLHelper::_('select.option', $id, $label); - } - - return $options; - } - - /** - * Get a list of filter options for the application statuses. - * - * @param string $element element of an extension - * @param string $type type of an extension - * @param integer $clientId client_id of an extension - * @param string $folder folder of an extension - * - * @return SimpleXMLElement - * - * @since 4.0.0 - */ - public static function getInstallationXML(string $element, string $type, int $clientId = 1, - ?string $folder = null - ): ?SimpleXMLElement - { - $path = [0 => JPATH_SITE, 1 => JPATH_ADMINISTRATOR, 3 => JPATH_API][$clientId] ?? JPATH_SITE; - - switch ($type) - { - case 'component': - $path .= '/components/' . $element . '/' . substr($element, 4) . '.xml'; - break; - case 'plugin': - $path .= '/plugins/' . $folder . '/' . $element . '/' . $element . '.xml'; - break; - case 'module': - $path .= '/modules/' . $element . '/' . $element . '.xml'; - break; - case 'template': - $path .= '/templates/' . $element . '/templateDetails.xml'; - break; - case 'library': - $path = JPATH_ADMINISTRATOR . '/manifests/libraries/' . $element . '.xml'; - break; - case 'file': - $path = JPATH_ADMINISTRATOR . '/manifests/files/' . $element . '.xml'; - break; - case 'package': - $path = JPATH_ADMINISTRATOR . '/manifests/packages/' . $element . '.xml'; - break; - case 'language': - $path .= '/language/' . $element . '/install.xml'; - } - - if (file_exists($path) === false) - { - return null; - } - - $xmlElement = simplexml_load_file($path); - - return ($xmlElement !== false) ? $xmlElement : null; - } - - /** - * Get the download key of an extension going through their installation xml - * - * @param CMSObject $extension element of an extension - * - * @return array An array with the prefix, suffix and value of the download key - * - * @since 4.0.0 - */ - public static function getDownloadKey(CMSObject $extension): array - { - $installXmlFile = self::getInstallationXML( - $extension->get('element'), - $extension->get('type'), - $extension->get('client_id'), - $extension->get('folder') - ); - - if (!$installXmlFile) - { - return [ - 'supported' => false, - 'valid' => false, - ]; - } - - if (!isset($installXmlFile->dlid)) - { - return [ - 'supported' => false, - 'valid' => false, - ]; - } - - $prefix = (string) $installXmlFile->dlid['prefix']; - $suffix = (string) $installXmlFile->dlid['suffix']; - $value = substr($extension->get('extra_query'), strlen($prefix)); - - if ($suffix) - { - $value = substr($value, 0, -strlen($suffix)); - } - - $downloadKey = [ - 'supported' => true, - 'valid' => $value ? true : false, - 'prefix' => $prefix, - 'suffix' => $suffix, - 'value' => $value - ]; - - return $downloadKey; - } - - /** - * Get the download key of an extension given enough information to locate it in the #__extensions table - * - * @param string $element Name of the extension, e.g. com_foo - * @param string $type The type of the extension, e.g. component - * @param int $clientId [optional] Joomla client for the extension, see the #__extensions table - * @param string|null $folder Extension folder, only applies for 'plugin' type - * - * @return array - * - * @since 4.0.0 - */ - public static function getExtensionDownloadKey(string $element, string $type, int $clientId = 1, - ?string $folder = null - ): array - { - // Get the database driver. If it fails we cannot report whether the extension supports download keys. - try - { - $db = Factory::getDbo(); - } - catch (Exception $e) - { - return [ - 'supported' => false, - 'valid' => false, - ]; - } - - // Try to retrieve the extension information as a CMSObject - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = :type') - ->where($db->quoteName('element') . ' = :element') - ->where($db->quoteName('folder') . ' = :folder') - ->where($db->quoteName('client_id') . ' = :client_id'); - $query->bind(':type', $type, ParameterType::STRING); - $query->bind(':element', $element, ParameterType::STRING); - $query->bind(':client_id', $clientId, ParameterType::INTEGER); - $query->bind(':folder', $folder, ParameterType::STRING); - - try - { - $extension = new CMSObject($db->setQuery($query)->loadAssoc()); - } - catch (Exception $e) - { - return [ - 'supported' => false, - 'valid' => false, - ]; - } - - // Use the getDownloadKey() method to return the download key information - return self::getDownloadKey($extension); - } - - /** - * Returns a list of update site IDs which support download keys. By default this returns all qualifying update - * sites, even if they are not enabled. - * - * - * @param bool $onlyEnabled [optional] Set true to only returned enabled update sites. - * - * @return int[] - * @since 4.0.0 - */ - public static function getDownloadKeySupportedSites($onlyEnabled = false): array - { - /** - * NOTE: The closures are not inlined because in this case the Joomla Code Style standard produces two mutually - * exclusive errors, making the file impossible to commit. Using closures in variables makes the code less - * readable but works around that issue. - */ - - $extensions = self::getUpdateSitesInformation($onlyEnabled); - - $filterClosure = function (CMSObject $extension) { - $dlidInfo = self::getDownloadKey($extension); - - return $dlidInfo['supported']; - }; - $extensions = array_filter($extensions, $filterClosure); - - $mapClosure = function (CMSObject $extension) { - return $extension->get('update_site_id'); - }; - - return array_map($mapClosure, $extensions); - } - - /** - * Returns a list of update site IDs which are missing download keys. By default this returns all qualifying update - * sites, even if they are not enabled. - * - * @param bool $exists [optional] If true, returns update sites with a valid download key. When false, - * returns update sites with an invalid / missing download key. - * @param bool $onlyEnabled [optional] Set true to only returned enabled update sites. - * - * @return int[] - * @since 4.0.0 - */ - public static function getDownloadKeyExistsSites(bool $exists = true, $onlyEnabled = false): array - { - /** - * NOTE: The closures are not inlined because in this case the Joomla Code Style standard produces two mutually - * exclusive errors, making the file impossible to commit. Using closures in variables makes the code less - * readable but works around that issue. - */ - - $extensions = self::getUpdateSitesInformation($onlyEnabled); - - // Filter the extensions by what supports Download Keys - $filterClosure = function (CMSObject $extension) use ($exists) { - $dlidInfo = self::getDownloadKey($extension); - - if (!$dlidInfo['supported']) - { - return false; - } - - return $exists ? $dlidInfo['valid'] : !$dlidInfo['valid']; - }; - $extensions = array_filter($extensions, $filterClosure); - - // Return only the update site IDs - $mapClosure = function (CMSObject $extension) { - return $extension->get('update_site_id'); - }; - - return array_map($mapClosure, $extensions); - } - - - /** - * Get information about the update sites - * - * @param bool $onlyEnabled Only return enabled update sites - * - * @return CMSObject[] List of update site and linked extension information - * @since 4.0.0 - */ - protected static function getUpdateSitesInformation(bool $onlyEnabled): array - { - try - { - $db = Factory::getDbo(); - } - catch (Exception $e) - { - return []; - } - - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 's.update_site_id', - 's.enabled', - 's.extra_query', - 'e.extension_id', - 'e.type', - 'e.element', - 'e.folder', - 'e.client_id', - 'e.manifest_cache', - ], - [ - 'update_site_id', - 'enabled', - 'extra_query', - 'extension_id', - 'type', - 'element', - 'folder', - 'client_id', - 'manifest_cache', - ] - ) - ) - ->from($db->quoteName('#__update_sites', 's')) - ->innerJoin( - $db->quoteName('#__update_sites_extensions', 'se'), - $db->quoteName('se.update_site_id') . ' = ' . $db->quoteName('s.update_site_id') - ) - ->innerJoin( - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('se.extension_id') - ) - ->where($db->quoteName('state') . ' = 0'); - - if ($onlyEnabled) - { - $enabled = 1; - $query->where($db->quoteName('s.enabled') . ' = :enabled') - ->bind(':enabled', $enabled, ParameterType::INTEGER); - } - - // Try to get all of the update sites, including related extension information - try - { - $items = []; - $db->setQuery($query); - - foreach ($db->getIterator() as $item) - { - $items[] = new CMSObject($item); - } - - return $items; - } - catch (Exception $e) - { - return []; - } - } + /** + * Get a list of filter options for the extension types. + * + * @return array An array of \stdClass objects. + * + * @since 3.0 + */ + public static function getExtensionTypes() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('type')) + ->from($db->quoteName('#__extensions')); + $db->setQuery($query); + $types = $db->loadColumn(); + + $options = array(); + + foreach ($types as $type) { + $options[] = HTMLHelper::_('select.option', $type, Text::_('COM_INSTALLER_TYPE_' . strtoupper($type))); + } + + return $options; + } + + /** + * Get a list of filter options for the extension types. + * + * @return array An array of \stdClass objects. + * + * @since 3.0 + */ + public static function getExtensionGroups() + { + $nofolder = ''; + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('folder')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' != :folder') + ->bind(':folder', $nofolder) + ->order($db->quoteName('folder')); + $db->setQuery($query); + $folders = $db->loadColumn(); + + $options = array(); + + foreach ($folders as $folder) { + $options[] = HTMLHelper::_('select.option', $folder, $folder); + } + + return $options; + } + + /** + * Get a list of filter options for the application clients. + * + * @return array An array of \JHtmlOption elements. + * + * @since 3.5 + */ + public static function getClientOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JSITE')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JADMINISTRATOR')); + $options[] = HTMLHelper::_('select.option', '3', Text::_('JAPI')); + + return $options; + } + + /** + * Get a list of filter options for the application statuses. + * + * @return array An array of \JHtmlOption elements. + * + * @since 3.5 + */ + public static function getStateOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JDISABLED')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JENABLED')); + $options[] = HTMLHelper::_('select.option', '2', Text::_('JPROTECTED')); + $options[] = HTMLHelper::_('select.option', '3', Text::_('JUNPROTECTED')); + + return $options; + } + + /** + * Get a list of filter options for extensions of the "package" type. + * + * @return array + * @since 4.2.0 + */ + public static function getPackageOptions(): array + { + $options = []; + + /** @var DatabaseDriver $db The application's database driver object */ + $db = Factory::getContainer()->get(DatabaseDriver::class); + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 'extension_id', + 'name', + 'element', + ] + ) + ) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('package')); + $extensions = $db->setQuery($query)->loadObjectList() ?: []; + + if (empty($extensions)) { + return $options; + } + + $language = Factory::getApplication()->getLanguage(); + $arrayKeys = array_map( + function (object $entry) use ($language): string { + $language->load($entry->element, JPATH_ADMINISTRATOR); + + return Text::_($entry->name); + }, + $extensions + ); + $arrayValues = array_map( + function (object $entry): int { + return $entry->extension_id; + }, + $extensions + ); + + $extensions = array_combine($arrayKeys, $arrayValues); + ksort($extensions); + + foreach ($extensions as $label => $id) { + $options[] = HTMLHelper::_('select.option', $id, $label); + } + + return $options; + } + + /** + * Get a list of filter options for the application statuses. + * + * @param string $element element of an extension + * @param string $type type of an extension + * @param integer $clientId client_id of an extension + * @param string $folder folder of an extension + * + * @return SimpleXMLElement + * + * @since 4.0.0 + */ + public static function getInstallationXML( + string $element, + string $type, + int $clientId = 1, + ?string $folder = null + ): ?SimpleXMLElement { + $path = [0 => JPATH_SITE, 1 => JPATH_ADMINISTRATOR, 3 => JPATH_API][$clientId] ?? JPATH_SITE; + + switch ($type) { + case 'component': + $path .= '/components/' . $element . '/' . substr($element, 4) . '.xml'; + break; + case 'plugin': + $path .= '/plugins/' . $folder . '/' . $element . '/' . $element . '.xml'; + break; + case 'module': + $path .= '/modules/' . $element . '/' . $element . '.xml'; + break; + case 'template': + $path .= '/templates/' . $element . '/templateDetails.xml'; + break; + case 'library': + $path = JPATH_ADMINISTRATOR . '/manifests/libraries/' . $element . '.xml'; + break; + case 'file': + $path = JPATH_ADMINISTRATOR . '/manifests/files/' . $element . '.xml'; + break; + case 'package': + $path = JPATH_ADMINISTRATOR . '/manifests/packages/' . $element . '.xml'; + break; + case 'language': + $path .= '/language/' . $element . '/install.xml'; + } + + if (file_exists($path) === false) { + return null; + } + + $xmlElement = simplexml_load_file($path); + + return ($xmlElement !== false) ? $xmlElement : null; + } + + /** + * Get the download key of an extension going through their installation xml + * + * @param CMSObject $extension element of an extension + * + * @return array An array with the prefix, suffix and value of the download key + * + * @since 4.0.0 + */ + public static function getDownloadKey(CMSObject $extension): array + { + $installXmlFile = self::getInstallationXML( + $extension->get('element'), + $extension->get('type'), + $extension->get('client_id'), + $extension->get('folder') + ); + + if (!$installXmlFile) { + return [ + 'supported' => false, + 'valid' => false, + ]; + } + + if (!isset($installXmlFile->dlid)) { + return [ + 'supported' => false, + 'valid' => false, + ]; + } + + $prefix = (string) $installXmlFile->dlid['prefix']; + $suffix = (string) $installXmlFile->dlid['suffix']; + $value = substr($extension->get('extra_query'), strlen($prefix)); + + if ($suffix) { + $value = substr($value, 0, -strlen($suffix)); + } + + $downloadKey = [ + 'supported' => true, + 'valid' => $value ? true : false, + 'prefix' => $prefix, + 'suffix' => $suffix, + 'value' => $value + ]; + + return $downloadKey; + } + + /** + * Get the download key of an extension given enough information to locate it in the #__extensions table + * + * @param string $element Name of the extension, e.g. com_foo + * @param string $type The type of the extension, e.g. component + * @param int $clientId [optional] Joomla client for the extension, see the #__extensions table + * @param string|null $folder Extension folder, only applies for 'plugin' type + * + * @return array + * + * @since 4.0.0 + */ + public static function getExtensionDownloadKey( + string $element, + string $type, + int $clientId = 1, + ?string $folder = null + ): array { + // Get the database driver. If it fails we cannot report whether the extension supports download keys. + try { + $db = Factory::getDbo(); + } catch (Exception $e) { + return [ + 'supported' => false, + 'valid' => false, + ]; + } + + // Try to retrieve the extension information as a CMSObject + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = :type') + ->where($db->quoteName('element') . ' = :element') + ->where($db->quoteName('folder') . ' = :folder') + ->where($db->quoteName('client_id') . ' = :client_id'); + $query->bind(':type', $type, ParameterType::STRING); + $query->bind(':element', $element, ParameterType::STRING); + $query->bind(':client_id', $clientId, ParameterType::INTEGER); + $query->bind(':folder', $folder, ParameterType::STRING); + + try { + $extension = new CMSObject($db->setQuery($query)->loadAssoc()); + } catch (Exception $e) { + return [ + 'supported' => false, + 'valid' => false, + ]; + } + + // Use the getDownloadKey() method to return the download key information + return self::getDownloadKey($extension); + } + + /** + * Returns a list of update site IDs which support download keys. By default this returns all qualifying update + * sites, even if they are not enabled. + * + * + * @param bool $onlyEnabled [optional] Set true to only returned enabled update sites. + * + * @return int[] + * @since 4.0.0 + */ + public static function getDownloadKeySupportedSites($onlyEnabled = false): array + { + /** + * NOTE: The closures are not inlined because in this case the Joomla Code Style standard produces two mutually + * exclusive errors, making the file impossible to commit. Using closures in variables makes the code less + * readable but works around that issue. + */ + + $extensions = self::getUpdateSitesInformation($onlyEnabled); + + $filterClosure = function (CMSObject $extension) { + $dlidInfo = self::getDownloadKey($extension); + + return $dlidInfo['supported']; + }; + $extensions = array_filter($extensions, $filterClosure); + + $mapClosure = function (CMSObject $extension) { + return $extension->get('update_site_id'); + }; + + return array_map($mapClosure, $extensions); + } + + /** + * Returns a list of update site IDs which are missing download keys. By default this returns all qualifying update + * sites, even if they are not enabled. + * + * @param bool $exists [optional] If true, returns update sites with a valid download key. When false, + * returns update sites with an invalid / missing download key. + * @param bool $onlyEnabled [optional] Set true to only returned enabled update sites. + * + * @return int[] + * @since 4.0.0 + */ + public static function getDownloadKeyExistsSites(bool $exists = true, $onlyEnabled = false): array + { + /** + * NOTE: The closures are not inlined because in this case the Joomla Code Style standard produces two mutually + * exclusive errors, making the file impossible to commit. Using closures in variables makes the code less + * readable but works around that issue. + */ + + $extensions = self::getUpdateSitesInformation($onlyEnabled); + + // Filter the extensions by what supports Download Keys + $filterClosure = function (CMSObject $extension) use ($exists) { + $dlidInfo = self::getDownloadKey($extension); + + if (!$dlidInfo['supported']) { + return false; + } + + return $exists ? $dlidInfo['valid'] : !$dlidInfo['valid']; + }; + $extensions = array_filter($extensions, $filterClosure); + + // Return only the update site IDs + $mapClosure = function (CMSObject $extension) { + return $extension->get('update_site_id'); + }; + + return array_map($mapClosure, $extensions); + } + + + /** + * Get information about the update sites + * + * @param bool $onlyEnabled Only return enabled update sites + * + * @return CMSObject[] List of update site and linked extension information + * @since 4.0.0 + */ + protected static function getUpdateSitesInformation(bool $onlyEnabled): array + { + try { + $db = Factory::getDbo(); + } catch (Exception $e) { + return []; + } + + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 's.update_site_id', + 's.enabled', + 's.extra_query', + 'e.extension_id', + 'e.type', + 'e.element', + 'e.folder', + 'e.client_id', + 'e.manifest_cache', + ], + [ + 'update_site_id', + 'enabled', + 'extra_query', + 'extension_id', + 'type', + 'element', + 'folder', + 'client_id', + 'manifest_cache', + ] + ) + ) + ->from($db->quoteName('#__update_sites', 's')) + ->innerJoin( + $db->quoteName('#__update_sites_extensions', 'se'), + $db->quoteName('se.update_site_id') . ' = ' . $db->quoteName('s.update_site_id') + ) + ->innerJoin( + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('se.extension_id') + ) + ->where($db->quoteName('state') . ' = 0'); + + if ($onlyEnabled) { + $enabled = 1; + $query->where($db->quoteName('s.enabled') . ' = :enabled') + ->bind(':enabled', $enabled, ParameterType::INTEGER); + } + + // Try to get all of the update sites, including related extension information + try { + $items = []; + $db->setQuery($query); + + foreach ($db->getIterator() as $item) { + $items[] = new CMSObject($item); + } + + return $items; + } catch (Exception $e) { + return []; + } + } } diff --git a/administrator/components/com_installer/src/Model/DatabaseModel.php b/administrator/components/com_installer/src/Model/DatabaseModel.php index 3e8b24400769f..a2d5d310a1026 100644 --- a/administrator/components/com_installer/src/Model/DatabaseModel.php +++ b/administrator/components/com_installer/src/Model/DatabaseModel.php @@ -1,4 +1,5 @@ errorCount; - } - - /** - * Method to populate the schema cache. - * - * @param integer $cid The extension ID to get the schema for - * - * @return void - * - * @throws \Exception - * - * @since 4.0.0 - */ - private function fetchSchemaCache($cid = 0) - { - // We already have it - if (array_key_exists($cid, $this->changeSetList)) - { - return; - } - - // Add the ID to the state so it can be used for filtering - if ($cid) - { - $this->setState('filter.extension_id', $cid); - } - - // With the parent::save it can get the limit and we need to make sure it gets all extensions - $results = $this->_getList($this->getListQuery()); - - foreach ($results as $result) - { - $errorMessages = array(); - $errorCount = 0; - - if (strcmp($result->element, 'joomla') === 0) - { - $result->element = 'com_admin'; - - if (!$this->getDefaultTextFilters()) - { - $errorMessages[] = Text::_('COM_INSTALLER_MSG_DATABASE_FILTER_ERROR'); - $errorCount++; - } - } - - $db = $this->getDatabase(); - - if ($result->type === 'component') - { - $basePath = JPATH_ADMINISTRATOR . '/components/' . $result->element; - } - elseif ($result->type === 'plugin') - { - $basePath = JPATH_PLUGINS . '/' . $result->folder . '/' . $result->element; - } - elseif ($result->type === 'module') - { - // Typehint to integer to normalise some DBs returning strings and others integers - if ((int) $result->client_id === 1) - { - $basePath = JPATH_ADMINISTRATOR . '/modules/' . $result->element; - } - elseif ((int) $result->client_id === 0) - { - $basePath = JPATH_SITE . '/modules/' . $result->element; - } - else - { - // Module with unknown client id!? - bail - continue; - } - } - // Specific bodge for the Joomla CMS special database check which points to com_admin - elseif ($result->type === 'file' && $result->element === 'com_admin') - { - $basePath = JPATH_ADMINISTRATOR . '/components/' . $result->element; - } - else - { - // Unknown extension type (library, files etc which don't have known SQL paths right now) - continue; - } - - // Search the standard SQL Path for the SQL Updates and then if not there check the configuration of the XML - // file. This just gives us a small performance win of not parsing the XML every time. - $folderTmp = $basePath . '/sql/updates/'; - - if (!file_exists($folderTmp)) - { - $installationXML = InstallerHelper::getInstallationXML( - $result->element, - $result->type, - $result->client_id, - $result->type === 'plugin' ? $result->folder : null - ); - - if ($installationXML !== null) - { - $folderTmp = (string) $installationXML->update->schemas->schemapath[0]; - $a = explode('/', $folderTmp); - array_pop($a); - $folderTmp = $basePath . '/' . implode('/', $a); - } - } - - // Can't find the folder still - give up now and move on. - if (!file_exists($folderTmp)) - { - continue; - } - - $changeSet = new ChangeSet($db, $folderTmp); - - // If the version in the #__schemas is different - // than the update files, add to problems message - $schema = $changeSet->getSchema(); - - // If the schema is empty we couldn't find any update files. Just ignore the extension. - if (empty($schema)) - { - continue; - } - - if ($result->version_id !== $schema) - { - $errorMessages[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_SCHEMA_ERROR', $result->version_id, $schema); - $errorCount++; - } - - // If the version in the manifest_cache is different than the - // version in the installation xml, add to problems message - $compareUpdateMessage = $this->compareUpdateVersion($result); - - if ($compareUpdateMessage) - { - $errorMessages[] = $compareUpdateMessage; - $errorCount++; - } - - // If there are errors in the database, add to the problems message - $errors = $changeSet->check(); - - $errorsMessage = $this->getErrorsMessage($errors); - - if ($errorsMessage) - { - $errorMessages = array_merge($errorMessages, $errorsMessage); - $errorCount++; - } - - // Number of database tables Checked and Skipped - $errorMessages = array_merge($errorMessages, $this->getOtherInformationMessage($changeSet->getStatus())); - - // Set the total number of errors - $this->errorCount += $errorCount; - - // Collect the extension details - $this->changeSetList[$result->extension_id] = array( - 'folderTmp' => $folderTmp, - 'errorsMessage' => $errorMessages, - 'errorsCount' => $errorCount, - 'results' => $changeSet->getStatus(), - 'schema' => $schema, - 'extension' => $result - ); - } - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'name', $direction = 'asc') - { - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); - $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); - $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); - - parent::populateState($ordering, $direction); - } - - /** - * Fixes database problems. - * - * @param array $cids List of the selected extensions to fix - * - * @return void|boolean - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function fix($cids = array()) - { - $db = $this->getDatabase(); - - foreach ($cids as $i => $cid) - { - // Load the database issues - $this->fetchSchemaCache($cid); - - $changeSet = $this->changeSetList[$cid]; - $changeSet['changeset'] = new ChangeSet($db, $changeSet['folderTmp']); - $changeSet['changeset']->fix(); - - $this->fixSchemaVersion($changeSet['changeset'], $changeSet['extension']->extension_id); - $this->fixUpdateVersion($changeSet['extension']->extension_id); - - if ($changeSet['extension']->element === 'com_admin') - { - $installer = new \JoomlaInstallerScript; - $installer->deleteUnexistingFiles(); - $this->fixDefaultTextFilters(); - - /* - * Finally, if the schema updates succeeded, make sure the database table is - * converted to utf8mb4 or, if not supported by the server, compatible to it. - */ - $statusArray = $changeSet['changeset']->getStatus(); - - if (count($statusArray['error']) == 0) - { - $installer->convertTablesToUtf8mb4(false); - } - } - } - } - - /** - * Gets the changeset array. - * - * @return array Array with the information of the versions problems, errors and the extensions itself - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function getItems() - { - $this->fetchSchemaCache(); - - $results = parent::getItems(); - $results = $this->mergeSchemaCache($results); - - return $results; - } - - /** - * Method to get the database query - * - * @return DatabaseQuery The database query - * - * @since 4.0.0 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 'extensions.client_id', - 'extensions.element', - 'extensions.extension_id', - 'extensions.folder', - 'extensions.manifest_cache', - 'extensions.name', - 'extensions.type', - 'schemas.version_id' - ] - ) - ) - ->from( - $db->quoteName( - '#__schemas', - 'schemas' - ) - ) - ->join( - 'INNER', - $db->quoteName('#__extensions', 'extensions'), - $db->quoteName('schemas.extension_id') . ' = ' . $db->quoteName('extensions.extension_id') - ); - - $type = $this->getState('filter.type'); - $clientId = $this->getState('filter.client_id'); - $extensionId = $this->getState('filter.extension_id'); - $folder = $this->getState('filter.folder'); - - if ($type) - { - $query->where($db->quoteName('extensions.type') . ' = :type') - ->bind(':type', $type); - } - - if ($clientId != '') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('extensions.client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - } - - if ($extensionId != '') - { - $extensionId = (int) $extensionId; - $query->where($db->quoteName('extensions.extension_id') . ' = :extensionid') - ->bind(':extensionid', $extensionId, ParameterType::INTEGER); - } - - if ($folder != '' && in_array($type, array('plugin', 'library', ''))) - { - $folder = $folder === '*' ? '' : $folder; - $query->where($db->quoteName('extensions.folder') . ' = :folder') - ->bind(':folder', $folder); - } - - // Process search filter (update site id). - $search = $this->getState('filter.search'); - - if (!empty($search) && stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('schemas.extension_id') . ' = :eid') - ->bind(':eid', $ids, ParameterType::INTEGER); - } - - return $query; - } - - /** - * Merge the items that will be visible with the changeSet information in cache - * - * @param array $results extensions returned from parent::getItems(). - * - * @return array the changeSetList of the merged items - * - * @since 4.0.0 - */ - protected function mergeSchemaCache($results) - { - $changeSetList = $this->changeSetList; - $finalResults = array(); - - foreach ($results as $result) - { - if (array_key_exists($result->extension_id, $changeSetList) && $changeSetList[$result->extension_id]) - { - $finalResults[] = $changeSetList[$result->extension_id]; - } - } - - return $finalResults; - } - - /** - * Get version from #__schemas table. - * - * @param integer $extensionId id of the extensions. - * - * @return mixed the return value from the query, or null if the query fails. - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function getSchemaVersion($extensionId) - { - $db = $this->getDatabase(); - $extensionId = (int) $extensionId; - $query = $db->getQuery(true) - ->select($db->quoteName('version_id')) - ->from($db->quoteName('#__schemas')) - ->where($db->quoteName('extension_id') . ' = :extensionid') - ->bind(':extensionid', $extensionId, ParameterType::INTEGER); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Fix schema version if wrong. - * - * @param ChangeSet $changeSet Schema change set. - * @param integer $extensionId ID of the extensions. - * - * @return mixed string schema version if success, false if fail. - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function fixSchemaVersion($changeSet, $extensionId) - { - // Get correct schema version -- last file in array. - $schema = $changeSet->getSchema(); - - // Check value. If ok, don't do update. - if ($schema == $this->getSchemaVersion($extensionId)) - { - return $schema; - } - - // Delete old row. - $extensionId = (int) $extensionId; - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__schemas')) - ->where($db->quoteName('extension_id') . ' = :extensionid') - ->bind(':extensionid', $extensionId, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - // Add new row. - $query->clear() - ->insert($db->quoteName('#__schemas')) - ->columns($db->quoteName('extension_id') . ',' . $db->quoteName('version_id')) - ->values(':extensionid, :schema') - ->bind(':extensionid', $extensionId, ParameterType::INTEGER) - ->bind(':schema', $schema); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - return false; - } - - return $schema; - } - - /** - * Get current version from #__extensions table. - * - * @param object $extension data from #__extensions of a single extension. - * - * @return mixed string message with the errors with the update version or null if none - * - * @since 4.0.0 - */ - public function compareUpdateVersion($extension) - { - $updateVersion = json_decode($extension->manifest_cache)->version; - - if ($extension->element === 'com_admin') - { - $extensionVersion = JVERSION; - } - else - { - $installationXML = InstallerHelper::getInstallationXML( - $extension->element, - $extension->type, - $extension->client_id, - $extension->type === 'plugin' ? $extension->folder : null - ); - - $extensionVersion = (string) $installationXML->version; - } - - if (version_compare($extensionVersion, $updateVersion) != 0) - { - return Text::sprintf('COM_INSTALLER_MSG_DATABASE_UPDATEVERSION_ERROR', $updateVersion, $extension->name, $extensionVersion); - } - - return null; - } - - /** - * Get a message of the tables skipped and checked - * - * @param array $status status of of the update files - * - * @return array Messages with the errors with the update version - * - * @since 4.0.0 - */ - private function getOtherInformationMessage($status) - { - $problemsMessage = array(); - $problemsMessage[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_CHECKED_OK', count($status['ok'])); - $problemsMessage[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_SKIPPED', count($status['skipped'])); - - return $problemsMessage; - } - - /** - * Get a message with all errors found in a given extension - * - * @param array $errors data from #__extensions of a single extension. - * - * @return array List of messages with the errors in the database - * - * @since 4.0.0 - */ - private function getErrorsMessage($errors) - { - $errorMessages = array(); - - foreach ($errors as $line => $error) - { - $key = 'COM_INSTALLER_MSG_DATABASE_' . $error->queryType; - $messages = $error->msgElements; - $file = basename($error->file); - $message0 = isset($messages[0]) ? $messages[0] : ' '; - $message1 = isset($messages[1]) ? $messages[1] : ' '; - $message2 = isset($messages[2]) ? $messages[2] : ' '; - $errorMessages[] = Text::sprintf($key, $file, $message0, $message1, $message2); - } - - return $errorMessages; - } - - /** - * Fix Joomla version in #__extensions table if wrong (doesn't equal \JVersion short version). - * - * @param integer $extensionId id of the extension - * - * @return mixed string update version if success, false if fail. - * - * @since 4.0.0 - */ - public function fixUpdateVersion($extensionId) - { - $table = new Extension($this->getDatabase()); - $table->load($extensionId); - $cache = new Registry($table->manifest_cache); - $updateVersion = $cache->get('version'); - - if ($table->get('type') === 'file' && $table->get('element') === 'joomla') - { - $extensionVersion = new Version; - $extensionVersion = $extensionVersion->getShortVersion(); - } - else - { - $installationXML = InstallerHelper::getInstallationXML( - $table->get('element'), - $table->get('type'), - $table->get('client_id'), - $table->get('type') === 'plugin' ? $table->get('folder') : null - ); - $extensionVersion = (string) $installationXML->version; - } - - if ($updateVersion === $extensionVersion) - { - return $updateVersion; - } - - $cache->set('version', $extensionVersion); - $table->set('manifest_cache', $cache->toString()); - - if ($table->store()) - { - return $extensionVersion; - } - - return false; - } - - /** - * For version 2.5.x only - * Check if com_config parameters are blank. - * - * @return string default text filters (if any). - * - * @since 4.0.0 - */ - public function getDefaultTextFilters() - { - $table = new Extension($this->getDatabase()); - $table->load($table->find(array('name' => 'com_config'))); - - return $table->params; - } - - /** - * For version 2.5.x only - * Check if com_config parameters are blank. If so, populate with com_content text filters. - * - * @return void - * - * @since 4.0.0 - */ - private function fixDefaultTextFilters() - { - $table = new Extension($this->getDatabase()); - $table->load($table->find(array('name' => 'com_config'))); - - // Check for empty $config and non-empty content filters. - if (!$table->params) - { - // Get filters from com_content and store if you find them. - $contentParams = ComponentHelper::getComponent('com_content')->getParams(); - - if ($contentParams->get('filters')) - { - $newParams = new Registry; - $newParams->set('filters', $contentParams->get('filters')); - $table->params = (string) $newParams; - $table->store(); - } - } - } + /** + * Set the model context + * + * @var string + * + * @since 4.0.0 + */ + protected $_context = 'com_installer.discover'; + + /** + * ChangeSet of all extensions + * + * @var array + * + * @since 4.0.0 + */ + private $changeSetList = array(); + + /** + * Total of errors + * + * @var integer + * + * @since 4.0.0 + */ + private $errorCount = 0; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see ListModel + * @since 4.0.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'update_site_name', + 'name', + 'client_id', + 'client', 'client_translated', + 'status', + 'type', 'type_translated', + 'folder', 'folder_translated', + 'extension_id' + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to return the total number of errors in all the extensions, saved in cache. + * + * @return integer + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function getErrorCount() + { + return $this->errorCount; + } + + /** + * Method to populate the schema cache. + * + * @param integer $cid The extension ID to get the schema for + * + * @return void + * + * @throws \Exception + * + * @since 4.0.0 + */ + private function fetchSchemaCache($cid = 0) + { + // We already have it + if (array_key_exists($cid, $this->changeSetList)) { + return; + } + + // Add the ID to the state so it can be used for filtering + if ($cid) { + $this->setState('filter.extension_id', $cid); + } + + // With the parent::save it can get the limit and we need to make sure it gets all extensions + $results = $this->_getList($this->getListQuery()); + + foreach ($results as $result) { + $errorMessages = array(); + $errorCount = 0; + + if (strcmp($result->element, 'joomla') === 0) { + $result->element = 'com_admin'; + + if (!$this->getDefaultTextFilters()) { + $errorMessages[] = Text::_('COM_INSTALLER_MSG_DATABASE_FILTER_ERROR'); + $errorCount++; + } + } + + $db = $this->getDatabase(); + + if ($result->type === 'component') { + $basePath = JPATH_ADMINISTRATOR . '/components/' . $result->element; + } elseif ($result->type === 'plugin') { + $basePath = JPATH_PLUGINS . '/' . $result->folder . '/' . $result->element; + } elseif ($result->type === 'module') { + // Typehint to integer to normalise some DBs returning strings and others integers + if ((int) $result->client_id === 1) { + $basePath = JPATH_ADMINISTRATOR . '/modules/' . $result->element; + } elseif ((int) $result->client_id === 0) { + $basePath = JPATH_SITE . '/modules/' . $result->element; + } else { + // Module with unknown client id!? - bail + continue; + } + } + // Specific bodge for the Joomla CMS special database check which points to com_admin + elseif ($result->type === 'file' && $result->element === 'com_admin') { + $basePath = JPATH_ADMINISTRATOR . '/components/' . $result->element; + } else { + // Unknown extension type (library, files etc which don't have known SQL paths right now) + continue; + } + + // Search the standard SQL Path for the SQL Updates and then if not there check the configuration of the XML + // file. This just gives us a small performance win of not parsing the XML every time. + $folderTmp = $basePath . '/sql/updates/'; + + if (!file_exists($folderTmp)) { + $installationXML = InstallerHelper::getInstallationXML( + $result->element, + $result->type, + $result->client_id, + $result->type === 'plugin' ? $result->folder : null + ); + + if ($installationXML !== null) { + $folderTmp = (string) $installationXML->update->schemas->schemapath[0]; + $a = explode('/', $folderTmp); + array_pop($a); + $folderTmp = $basePath . '/' . implode('/', $a); + } + } + + // Can't find the folder still - give up now and move on. + if (!file_exists($folderTmp)) { + continue; + } + + $changeSet = new ChangeSet($db, $folderTmp); + + // If the version in the #__schemas is different + // than the update files, add to problems message + $schema = $changeSet->getSchema(); + + // If the schema is empty we couldn't find any update files. Just ignore the extension. + if (empty($schema)) { + continue; + } + + if ($result->version_id !== $schema) { + $errorMessages[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_SCHEMA_ERROR', $result->version_id, $schema); + $errorCount++; + } + + // If the version in the manifest_cache is different than the + // version in the installation xml, add to problems message + $compareUpdateMessage = $this->compareUpdateVersion($result); + + if ($compareUpdateMessage) { + $errorMessages[] = $compareUpdateMessage; + $errorCount++; + } + + // If there are errors in the database, add to the problems message + $errors = $changeSet->check(); + + $errorsMessage = $this->getErrorsMessage($errors); + + if ($errorsMessage) { + $errorMessages = array_merge($errorMessages, $errorsMessage); + $errorCount++; + } + + // Number of database tables Checked and Skipped + $errorMessages = array_merge($errorMessages, $this->getOtherInformationMessage($changeSet->getStatus())); + + // Set the total number of errors + $this->errorCount += $errorCount; + + // Collect the extension details + $this->changeSetList[$result->extension_id] = array( + 'folderTmp' => $folderTmp, + 'errorsMessage' => $errorMessages, + 'errorsCount' => $errorCount, + 'results' => $changeSet->getStatus(), + 'schema' => $schema, + 'extension' => $result + ); + } + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); + $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); + $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); + + parent::populateState($ordering, $direction); + } + + /** + * Fixes database problems. + * + * @param array $cids List of the selected extensions to fix + * + * @return void|boolean + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function fix($cids = array()) + { + $db = $this->getDatabase(); + + foreach ($cids as $i => $cid) { + // Load the database issues + $this->fetchSchemaCache($cid); + + $changeSet = $this->changeSetList[$cid]; + $changeSet['changeset'] = new ChangeSet($db, $changeSet['folderTmp']); + $changeSet['changeset']->fix(); + + $this->fixSchemaVersion($changeSet['changeset'], $changeSet['extension']->extension_id); + $this->fixUpdateVersion($changeSet['extension']->extension_id); + + if ($changeSet['extension']->element === 'com_admin') { + $installer = new \JoomlaInstallerScript(); + $installer->deleteUnexistingFiles(); + $this->fixDefaultTextFilters(); + + /* + * Finally, if the schema updates succeeded, make sure the database table is + * converted to utf8mb4 or, if not supported by the server, compatible to it. + */ + $statusArray = $changeSet['changeset']->getStatus(); + + if (count($statusArray['error']) == 0) { + $installer->convertTablesToUtf8mb4(false); + } + } + } + } + + /** + * Gets the changeset array. + * + * @return array Array with the information of the versions problems, errors and the extensions itself + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function getItems() + { + $this->fetchSchemaCache(); + + $results = parent::getItems(); + $results = $this->mergeSchemaCache($results); + + return $results; + } + + /** + * Method to get the database query + * + * @return DatabaseQuery The database query + * + * @since 4.0.0 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 'extensions.client_id', + 'extensions.element', + 'extensions.extension_id', + 'extensions.folder', + 'extensions.manifest_cache', + 'extensions.name', + 'extensions.type', + 'schemas.version_id' + ] + ) + ) + ->from( + $db->quoteName( + '#__schemas', + 'schemas' + ) + ) + ->join( + 'INNER', + $db->quoteName('#__extensions', 'extensions'), + $db->quoteName('schemas.extension_id') . ' = ' . $db->quoteName('extensions.extension_id') + ); + + $type = $this->getState('filter.type'); + $clientId = $this->getState('filter.client_id'); + $extensionId = $this->getState('filter.extension_id'); + $folder = $this->getState('filter.folder'); + + if ($type) { + $query->where($db->quoteName('extensions.type') . ' = :type') + ->bind(':type', $type); + } + + if ($clientId != '') { + $clientId = (int) $clientId; + $query->where($db->quoteName('extensions.client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + } + + if ($extensionId != '') { + $extensionId = (int) $extensionId; + $query->where($db->quoteName('extensions.extension_id') . ' = :extensionid') + ->bind(':extensionid', $extensionId, ParameterType::INTEGER); + } + + if ($folder != '' && in_array($type, array('plugin', 'library', ''))) { + $folder = $folder === '*' ? '' : $folder; + $query->where($db->quoteName('extensions.folder') . ' = :folder') + ->bind(':folder', $folder); + } + + // Process search filter (update site id). + $search = $this->getState('filter.search'); + + if (!empty($search) && stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('schemas.extension_id') . ' = :eid') + ->bind(':eid', $ids, ParameterType::INTEGER); + } + + return $query; + } + + /** + * Merge the items that will be visible with the changeSet information in cache + * + * @param array $results extensions returned from parent::getItems(). + * + * @return array the changeSetList of the merged items + * + * @since 4.0.0 + */ + protected function mergeSchemaCache($results) + { + $changeSetList = $this->changeSetList; + $finalResults = array(); + + foreach ($results as $result) { + if (array_key_exists($result->extension_id, $changeSetList) && $changeSetList[$result->extension_id]) { + $finalResults[] = $changeSetList[$result->extension_id]; + } + } + + return $finalResults; + } + + /** + * Get version from #__schemas table. + * + * @param integer $extensionId id of the extensions. + * + * @return mixed the return value from the query, or null if the query fails. + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function getSchemaVersion($extensionId) + { + $db = $this->getDatabase(); + $extensionId = (int) $extensionId; + $query = $db->getQuery(true) + ->select($db->quoteName('version_id')) + ->from($db->quoteName('#__schemas')) + ->where($db->quoteName('extension_id') . ' = :extensionid') + ->bind(':extensionid', $extensionId, ParameterType::INTEGER); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Fix schema version if wrong. + * + * @param ChangeSet $changeSet Schema change set. + * @param integer $extensionId ID of the extensions. + * + * @return mixed string schema version if success, false if fail. + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function fixSchemaVersion($changeSet, $extensionId) + { + // Get correct schema version -- last file in array. + $schema = $changeSet->getSchema(); + + // Check value. If ok, don't do update. + if ($schema == $this->getSchemaVersion($extensionId)) { + return $schema; + } + + // Delete old row. + $extensionId = (int) $extensionId; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__schemas')) + ->where($db->quoteName('extension_id') . ' = :extensionid') + ->bind(':extensionid', $extensionId, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + // Add new row. + $query->clear() + ->insert($db->quoteName('#__schemas')) + ->columns($db->quoteName('extension_id') . ',' . $db->quoteName('version_id')) + ->values(':extensionid, :schema') + ->bind(':extensionid', $extensionId, ParameterType::INTEGER) + ->bind(':schema', $schema); + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + return false; + } + + return $schema; + } + + /** + * Get current version from #__extensions table. + * + * @param object $extension data from #__extensions of a single extension. + * + * @return mixed string message with the errors with the update version or null if none + * + * @since 4.0.0 + */ + public function compareUpdateVersion($extension) + { + $updateVersion = json_decode($extension->manifest_cache)->version; + + if ($extension->element === 'com_admin') { + $extensionVersion = JVERSION; + } else { + $installationXML = InstallerHelper::getInstallationXML( + $extension->element, + $extension->type, + $extension->client_id, + $extension->type === 'plugin' ? $extension->folder : null + ); + + $extensionVersion = (string) $installationXML->version; + } + + if (version_compare($extensionVersion, $updateVersion) != 0) { + return Text::sprintf('COM_INSTALLER_MSG_DATABASE_UPDATEVERSION_ERROR', $updateVersion, $extension->name, $extensionVersion); + } + + return null; + } + + /** + * Get a message of the tables skipped and checked + * + * @param array $status status of of the update files + * + * @return array Messages with the errors with the update version + * + * @since 4.0.0 + */ + private function getOtherInformationMessage($status) + { + $problemsMessage = array(); + $problemsMessage[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_CHECKED_OK', count($status['ok'])); + $problemsMessage[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_SKIPPED', count($status['skipped'])); + + return $problemsMessage; + } + + /** + * Get a message with all errors found in a given extension + * + * @param array $errors data from #__extensions of a single extension. + * + * @return array List of messages with the errors in the database + * + * @since 4.0.0 + */ + private function getErrorsMessage($errors) + { + $errorMessages = array(); + + foreach ($errors as $line => $error) { + $key = 'COM_INSTALLER_MSG_DATABASE_' . $error->queryType; + $messages = $error->msgElements; + $file = basename($error->file); + $message0 = isset($messages[0]) ? $messages[0] : ' '; + $message1 = isset($messages[1]) ? $messages[1] : ' '; + $message2 = isset($messages[2]) ? $messages[2] : ' '; + $errorMessages[] = Text::sprintf($key, $file, $message0, $message1, $message2); + } + + return $errorMessages; + } + + /** + * Fix Joomla version in #__extensions table if wrong (doesn't equal \JVersion short version). + * + * @param integer $extensionId id of the extension + * + * @return mixed string update version if success, false if fail. + * + * @since 4.0.0 + */ + public function fixUpdateVersion($extensionId) + { + $table = new Extension($this->getDatabase()); + $table->load($extensionId); + $cache = new Registry($table->manifest_cache); + $updateVersion = $cache->get('version'); + + if ($table->get('type') === 'file' && $table->get('element') === 'joomla') { + $extensionVersion = new Version(); + $extensionVersion = $extensionVersion->getShortVersion(); + } else { + $installationXML = InstallerHelper::getInstallationXML( + $table->get('element'), + $table->get('type'), + $table->get('client_id'), + $table->get('type') === 'plugin' ? $table->get('folder') : null + ); + $extensionVersion = (string) $installationXML->version; + } + + if ($updateVersion === $extensionVersion) { + return $updateVersion; + } + + $cache->set('version', $extensionVersion); + $table->set('manifest_cache', $cache->toString()); + + if ($table->store()) { + return $extensionVersion; + } + + return false; + } + + /** + * For version 2.5.x only + * Check if com_config parameters are blank. + * + * @return string default text filters (if any). + * + * @since 4.0.0 + */ + public function getDefaultTextFilters() + { + $table = new Extension($this->getDatabase()); + $table->load($table->find(array('name' => 'com_config'))); + + return $table->params; + } + + /** + * For version 2.5.x only + * Check if com_config parameters are blank. If so, populate with com_content text filters. + * + * @return void + * + * @since 4.0.0 + */ + private function fixDefaultTextFilters() + { + $table = new Extension($this->getDatabase()); + $table->load($table->find(array('name' => 'com_config'))); + + // Check for empty $config and non-empty content filters. + if (!$table->params) { + // Get filters from com_content and store if you find them. + $contentParams = ComponentHelper::getComponent('com_content')->getParams(); + + if ($contentParams->get('filters')) { + $newParams = new Registry(); + $newParams->set('filters', $contentParams->get('filters')); + $table->params = (string) $newParams; + $table->store(); + } + } + } } diff --git a/administrator/components/com_installer/src/Model/DiscoverModel.php b/administrator/components/com_installer/src/Model/DiscoverModel.php index 425cfca7588b3..a35e34e43f8a8 100644 --- a/administrator/components/com_installer/src/Model/DiscoverModel.php +++ b/administrator/components/com_installer/src/Model/DiscoverModel.php @@ -1,4 +1,5 @@ setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); - $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); - $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); - - $this->setState('message', $app->getUserState('com_installer.message')); - $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); - - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - - parent::populateState($ordering, $direction); - } - - /** - * Method to get the database query. - * - * @return DatabaseQuery The database query - * - * @since 3.1 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('state') . ' = -1'); - - // Process select filters. - $type = $this->getState('filter.type'); - $clientId = $this->getState('filter.client_id'); - $folder = $this->getState('filter.folder'); - - if ($type) - { - $query->where($db->quoteName('type') . ' = :type') - ->bind(':type', $type); - } - - if ($clientId != '') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - } - - if ($folder != '' && in_array($type, array('plugin', 'library', ''))) - { - $folder = $folder === '*' ? '' : $folder; - $query->where($db->quoteName('folder') . ' = :folder') - ->bind(':folder', $folder); - } - - // Process search filter. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $ids, ParameterType::INTEGER); - } - } - - // Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in extension.php). - - return $query; - } - - /** - * Discover extensions. - * - * Finds uninstalled extensions - * - * @return int The count of discovered extensions - * - * @since 1.6 - */ - public function discover() - { - // Purge the list of discovered extensions and fetch them again. - $this->purge(); - $results = Installer::getInstance()->discover(); - - // Get all templates, including discovered ones - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['extension_id', 'element', 'folder', 'client_id', 'type'])) - ->from($db->quoteName('#__extensions')); - $db->setQuery($query); - $installedtmp = $db->loadObjectList(); - - $extensions = array(); - - foreach ($installedtmp as $install) - { - $key = implode(':', - [ - $install->type, - str_replace('\\', '/', $install->element), - $install->folder, - $install->client_id - ] - ); - $extensions[$key] = $install; - } - - $count = 0; - - foreach ($results as $result) - { - // Check if we have a match on the element - $key = implode(':', - [ - $result->type, - str_replace('\\', '/', $result->element), - $result->folder, - $result->client_id - ] - ); - - if (!array_key_exists($key, $extensions)) - { - // Put it into the table - $result->check(); - $result->store(); - $count++; - } - } - - return $count; - } - - /** - * Installs a discovered extension. - * - * @return void - * - * @since 1.6 - */ - public function discover_install() - { - $app = Factory::getApplication(); - $input = $app->input; - $eid = $input->get('cid', 0, 'array'); - - if (is_array($eid) || $eid) - { - if (!is_array($eid)) - { - $eid = array($eid); - } - - $eid = ArrayHelper::toInteger($eid); - $failed = false; - - foreach ($eid as $id) - { - $installer = new Installer; - $installer->setDatabase($this->getDatabase()); - - $result = $installer->discover_install($id); - - if (!$result) - { - $failed = true; - $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_INSTALLFAILED') . ': ' . $id); - } - } - - // @todo - We are only receiving the message for the last Installer instance - $this->setState('action', 'remove'); - $this->setState('name', $installer->get('name')); - $app->setUserState('com_installer.message', $installer->message); - $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); - - if (!$failed) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_INSTALLSUCCESSFUL')); - } - } - else - { - $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_NOEXTENSIONSELECTED')); - } - } - - /** - * Cleans out the list of discovered extensions. - * - * @return boolean True on success - * - * @since 1.6 - */ - public function purge() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__extensions')) - ->where($db->quoteName('state') . ' = -1'); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - $this->_message = Text::_('COM_INSTALLER_MSG_DISCOVER_FAILEDTOPURGEEXTENSIONS'); - - return false; - } - - $this->_message = Text::_('COM_INSTALLER_MSG_DISCOVER_PURGEDDISCOVEREDEXTENSIONS'); - - return true; - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - $query->where($this->getDatabase()->quoteName('state') . ' = -1'); - - return $query; - } - - /** - * Checks for not installed extensions in extensions table. - * - * @return boolean True if there are discovered extensions in the database. - * - * @since 4.2.0 - */ - public function checkExtensions() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('state') . ' = -1'); - $db->setQuery($query); - $discoveredExtensions = $db->loadObjectList(); - - return count($discoveredExtensions) > 0; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\ListModel + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'name', + 'client_id', + 'client', 'client_translated', + 'type', 'type_translated', + 'folder', 'folder_translated', + 'extension_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.1 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + $app = Factory::getApplication(); + + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); + $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); + $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); + + $this->setState('message', $app->getUserState('com_installer.message')); + $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); + + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + + parent::populateState($ordering, $direction); + } + + /** + * Method to get the database query. + * + * @return DatabaseQuery The database query + * + * @since 3.1 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('state') . ' = -1'); + + // Process select filters. + $type = $this->getState('filter.type'); + $clientId = $this->getState('filter.client_id'); + $folder = $this->getState('filter.folder'); + + if ($type) { + $query->where($db->quoteName('type') . ' = :type') + ->bind(':type', $type); + } + + if ($clientId != '') { + $clientId = (int) $clientId; + $query->where($db->quoteName('client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + } + + if ($folder != '' && in_array($type, array('plugin', 'library', ''))) { + $folder = $folder === '*' ? '' : $folder; + $query->where($db->quoteName('folder') . ' = :folder') + ->bind(':folder', $folder); + } + + // Process search filter. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $ids, ParameterType::INTEGER); + } + } + + // Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in extension.php). + + return $query; + } + + /** + * Discover extensions. + * + * Finds uninstalled extensions + * + * @return int The count of discovered extensions + * + * @since 1.6 + */ + public function discover() + { + // Purge the list of discovered extensions and fetch them again. + $this->purge(); + $results = Installer::getInstance()->discover(); + + // Get all templates, including discovered ones + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['extension_id', 'element', 'folder', 'client_id', 'type'])) + ->from($db->quoteName('#__extensions')); + $db->setQuery($query); + $installedtmp = $db->loadObjectList(); + + $extensions = array(); + + foreach ($installedtmp as $install) { + $key = implode( + ':', + [ + $install->type, + str_replace('\\', '/', $install->element), + $install->folder, + $install->client_id + ] + ); + $extensions[$key] = $install; + } + + $count = 0; + + foreach ($results as $result) { + // Check if we have a match on the element + $key = implode( + ':', + [ + $result->type, + str_replace('\\', '/', $result->element), + $result->folder, + $result->client_id + ] + ); + + if (!array_key_exists($key, $extensions)) { + // Put it into the table + $result->check(); + $result->store(); + $count++; + } + } + + return $count; + } + + /** + * Installs a discovered extension. + * + * @return void + * + * @since 1.6 + */ + public function discover_install() + { + $app = Factory::getApplication(); + $input = $app->input; + $eid = $input->get('cid', 0, 'array'); + + if (is_array($eid) || $eid) { + if (!is_array($eid)) { + $eid = array($eid); + } + + $eid = ArrayHelper::toInteger($eid); + $failed = false; + + foreach ($eid as $id) { + $installer = new Installer(); + $installer->setDatabase($this->getDatabase()); + + $result = $installer->discover_install($id); + + if (!$result) { + $failed = true; + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_INSTALLFAILED') . ': ' . $id); + } + } + + // @todo - We are only receiving the message for the last Installer instance + $this->setState('action', 'remove'); + $this->setState('name', $installer->get('name')); + $app->setUserState('com_installer.message', $installer->message); + $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); + + if (!$failed) { + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_INSTALLSUCCESSFUL')); + } + } else { + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_NOEXTENSIONSELECTED')); + } + } + + /** + * Cleans out the list of discovered extensions. + * + * @return boolean True on success + * + * @since 1.6 + */ + public function purge() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__extensions')) + ->where($db->quoteName('state') . ' = -1'); + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + $this->_message = Text::_('COM_INSTALLER_MSG_DISCOVER_FAILEDTOPURGEEXTENSIONS'); + + return false; + } + + $this->_message = Text::_('COM_INSTALLER_MSG_DISCOVER_PURGEDDISCOVEREDEXTENSIONS'); + + return true; + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + $query->where($this->getDatabase()->quoteName('state') . ' = -1'); + + return $query; + } + + /** + * Checks for not installed extensions in extensions table. + * + * @return boolean True if there are discovered extensions in the database. + * + * @since 4.2.0 + */ + public function checkExtensions() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('state') . ' = -1'); + $db->setQuery($query); + $discoveredExtensions = $db->loadObjectList(); + + return count($discoveredExtensions) > 0; + } } diff --git a/administrator/components/com_installer/src/Model/InstallModel.php b/administrator/components/com_installer/src/Model/InstallModel.php index e3ea81ebc0b08..aed3fc051f927 100644 --- a/administrator/components/com_installer/src/Model/InstallModel.php +++ b/administrator/components/com_installer/src/Model/InstallModel.php @@ -1,4 +1,5 @@ setState('message', $app->getUserState('com_installer.message')); - $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - - parent::populateState(); - } - - /** - * Install an extension from either folder, URL or upload. - * - * @return boolean - * - * @since 1.5 - */ - public function install() - { - $this->setState('action', 'install'); - - $app = Factory::getApplication(); - - // Load installer plugins for assistance if required: - PluginHelper::importPlugin('installer'); - - $package = null; - - // This event allows an input pre-treatment, a custom pre-packing or custom installation. - // (e.g. from a \JSON description). - $results = $app->triggerEvent('onInstallerBeforeInstallation', array($this, &$package)); - - if (in_array(true, $results, true)) - { - return true; - } - - if (in_array(false, $results, true)) - { - return false; - } - - $installType = $app->input->getWord('installtype'); - $installLang = $app->input->getWord('package'); - - if ($package === null) - { - switch ($installType) - { - case 'folder': - // Remember the 'Install from Directory' path. - $app->getUserStateFromRequest($this->_context . '.install_directory', 'install_directory'); - $package = $this->_getPackageFromFolder(); - break; - - case 'upload': - $package = $this->_getPackageFromUpload(); - break; - - case 'url': - $package = $this->_getPackageFromUrl(); - break; - - default: - $app->setUserState('com_installer.message', Text::_('COM_INSTALLER_NO_INSTALL_TYPE_FOUND')); - - return false; - } - } - - // This event allows a custom installation of the package or a customization of the package: - $results = $app->triggerEvent('onInstallerBeforeInstaller', array($this, &$package)); - - if (in_array(true, $results, true)) - { - return true; - } - - if (in_array(false, $results, true)) - { - if (in_array($installType, array('upload', 'url'))) - { - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - } - - return false; - } - - // Check if package was uploaded successfully. - if (!\is_array($package)) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_UNABLE_TO_FIND_INSTALL_PACKAGE'), 'error'); - - return false; - } - - // Get an installer instance. - $installer = Installer::getInstance(); - - /* - * Check for a Joomla core package. - * To do this we need to set the source path to find the manifest (the same first step as Installer::install()) - * - * This must be done before the unpacked check because InstallerHelper::detectType() returns a boolean false since the manifest - * can't be found in the expected location. - */ - if (isset($package['dir']) && is_dir($package['dir'])) - { - $installer->setPath('source', $package['dir']); - - if (!$installer->findManifest()) - { - // If a manifest isn't found at the source, this may be a Joomla package; check the package directory for the Joomla manifest - if (file_exists($package['dir'] . '/administrator/manifests/files/joomla.xml')) - { - // We have a Joomla package - if (in_array($installType, array('upload', 'url'))) - { - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - } - - $app->enqueueMessage( - Text::sprintf('COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE', Route::_('index.php?option=com_joomlaupdate')), - 'warning' - ); - - return false; - } - } - } - - // Was the package unpacked? - if (empty($package['type'])) - { - if (in_array($installType, array('upload', 'url'))) - { - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - } - - $app->enqueueMessage(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST'), 'error'); - - return false; - } - - // Install the package. - if (!$installer->install($package['dir'])) - { - // There was an error installing the package. - $msg = Text::sprintf('COM_INSTALLER_INSTALL_ERROR', Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type']))); - $result = false; - $msgType = 'error'; - } - else - { - // Package installed successfully. - $msg = Text::sprintf('COM_INSTALLER_INSTALL_SUCCESS', Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($installLang . $package['type']))); - $result = true; - $msgType = 'message'; - } - - // This event allows a custom a post-flight: - $app->triggerEvent('onInstallerAfterInstaller', array($this, &$package, $installer, &$result, &$msg)); - - // Set some model state values. - $app->enqueueMessage($msg, $msgType); - $this->setState('name', $installer->get('name')); - $this->setState('result', $result); - $app->setUserState('com_installer.message', $installer->message); - $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); - $app->setUserState('com_installer.redirect_url', $installer->get('redirect_url')); - - // Cleanup the install files. - if (!is_file($package['packagefile'])) - { - $package['packagefile'] = $app->get('tmp_path') . '/' . $package['packagefile']; - } - - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - - // Clear the cached extension data and menu cache - $this->cleanCache('_system'); - $this->cleanCache('com_modules'); - $this->cleanCache('com_plugins'); - $this->cleanCache('mod_menu'); - - return $result; - } - - /** - * Works out an installation package from a HTTP upload. - * - * @return mixed Package definition or false on failure. - */ - protected function _getPackageFromUpload() - { - // Get the uploaded file information. - $input = Factory::getApplication()->input; - - // Do not change the filter type 'raw'. We need this to let files containing PHP code to upload. See \JInputFiles::get. - $userfile = $input->files->get('install_package', null, 'raw'); - - // Make sure that file uploads are enabled in php. - if (!(bool) ini_get('file_uploads')) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLFILE'), 'error'); - - return false; - } - - // Make sure that zlib is loaded so that the package can be unpacked. - if (!extension_loaded('zlib')) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLZLIB'), 'error'); - - return false; - } - - // If there is no uploaded file, we have a problem... - if (!is_array($userfile)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_NO_FILE_SELECTED'), 'error'); - - return false; - } - - // Is the PHP tmp directory missing? - if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_NO_TMP_DIR)) - { - Factory::getApplication()->enqueueMessage( - Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
    ' . Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), - 'error' - ); - - return false; - } - - // Is the max upload size too small in php.ini? - if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_INI_SIZE)) - { - Factory::getApplication()->enqueueMessage( - Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
    ' . Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), - 'error' - ); - - return false; - } - - // Check if there was a different problem uploading the file. - if ($userfile['error'] || $userfile['size'] < 1) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 'error'); - - return false; - } - - // Build the appropriate paths. - $config = Factory::getApplication()->getConfig(); - $tmp_dest = $config->get('tmp_path') . '/' . $userfile['name']; - $tmp_src = $userfile['tmp_name']; - - // Move uploaded file. - File::upload($tmp_src, $tmp_dest, false, true); - - // Unpack the downloaded package file. - $package = InstallerHelper::unpack($tmp_dest, true); - - return $package; - } - - /** - * Install an extension from a directory - * - * @return array Package details or false on failure - * - * @since 1.5 - */ - protected function _getPackageFromFolder() - { - $input = Factory::getApplication()->input; - - // Get the path to the package to install. - $p_dir = $input->getString('install_directory'); - $p_dir = Path::clean($p_dir); - - // Did you give us a valid directory? - if (!is_dir($p_dir)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_PLEASE_ENTER_A_PACKAGE_DIRECTORY'), 'error'); - - return false; - } - - // Detect the package type - $type = InstallerHelper::detectType($p_dir); - - // Did you give us a valid package? - if (!$type) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_PATH_DOES_NOT_HAVE_A_VALID_PACKAGE'), 'error'); - } - - $package['packagefile'] = null; - $package['extractdir'] = null; - $package['dir'] = $p_dir; - $package['type'] = $type; - - return $package; - } - - /** - * Install an extension from a URL. - * - * @return bool|array Package details or false on failure. - * - * @since 1.5 - */ - protected function _getPackageFromUrl() - { - $input = Factory::getApplication()->input; - - // Get the URL of the package to install. - $url = $input->getString('install_url'); - - // Did you give us a URL? - if (!$url) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_ENTER_A_URL'), 'error'); - - return false; - } - - // We only allow http & https here - $uri = new Uri($url); - - if (!in_array($uri->getScheme(), ['http', 'https'])) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_INVALID_URL_SCHEME'), 'error'); - - return false; - } - - // Handle updater XML file case: - if (preg_match('/\.xml\s*$/', $url)) - { - $update = new Update; - $update->loadFromXml($url); - $package_url = trim($update->get('downloadurl', false)->_data); - - if ($package_url) - { - $url = $package_url; - } - - unset($update); - } - - // Download the package at the URL given. - $p_file = InstallerHelper::downloadPackage($url); - - // Was the package downloaded? - if (!$p_file) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_INVALID_URL'), 'error'); - - return false; - } - - $tmp_dest = Factory::getApplication()->get('tmp_path'); - - // Unpack the downloaded package file. - $package = InstallerHelper::unpack($tmp_dest . '/' . $p_file, true); - - return $package; - } + /** + * @var \Joomla\CMS\Table\Table Table object + */ + protected $_table = null; + + /** + * @var string URL + */ + protected $_url = null; + + /** + * Model context string. + * + * @var string + */ + protected $_context = 'com_installer.install'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + $this->setState('message', $app->getUserState('com_installer.message')); + $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + + parent::populateState(); + } + + /** + * Install an extension from either folder, URL or upload. + * + * @return boolean + * + * @since 1.5 + */ + public function install() + { + $this->setState('action', 'install'); + + $app = Factory::getApplication(); + + // Load installer plugins for assistance if required: + PluginHelper::importPlugin('installer'); + + $package = null; + + // This event allows an input pre-treatment, a custom pre-packing or custom installation. + // (e.g. from a \JSON description). + $results = $app->triggerEvent('onInstallerBeforeInstallation', array($this, &$package)); + + if (in_array(true, $results, true)) { + return true; + } + + if (in_array(false, $results, true)) { + return false; + } + + $installType = $app->input->getWord('installtype'); + $installLang = $app->input->getWord('package'); + + if ($package === null) { + switch ($installType) { + case 'folder': + // Remember the 'Install from Directory' path. + $app->getUserStateFromRequest($this->_context . '.install_directory', 'install_directory'); + $package = $this->_getPackageFromFolder(); + break; + + case 'upload': + $package = $this->_getPackageFromUpload(); + break; + + case 'url': + $package = $this->_getPackageFromUrl(); + break; + + default: + $app->setUserState('com_installer.message', Text::_('COM_INSTALLER_NO_INSTALL_TYPE_FOUND')); + + return false; + } + } + + // This event allows a custom installation of the package or a customization of the package: + $results = $app->triggerEvent('onInstallerBeforeInstaller', array($this, &$package)); + + if (in_array(true, $results, true)) { + return true; + } + + if (in_array(false, $results, true)) { + if (in_array($installType, array('upload', 'url'))) { + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + } + + return false; + } + + // Check if package was uploaded successfully. + if (!\is_array($package)) { + $app->enqueueMessage(Text::_('COM_INSTALLER_UNABLE_TO_FIND_INSTALL_PACKAGE'), 'error'); + + return false; + } + + // Get an installer instance. + $installer = Installer::getInstance(); + + /* + * Check for a Joomla core package. + * To do this we need to set the source path to find the manifest (the same first step as Installer::install()) + * + * This must be done before the unpacked check because InstallerHelper::detectType() returns a boolean false since the manifest + * can't be found in the expected location. + */ + if (isset($package['dir']) && is_dir($package['dir'])) { + $installer->setPath('source', $package['dir']); + + if (!$installer->findManifest()) { + // If a manifest isn't found at the source, this may be a Joomla package; check the package directory for the Joomla manifest + if (file_exists($package['dir'] . '/administrator/manifests/files/joomla.xml')) { + // We have a Joomla package + if (in_array($installType, array('upload', 'url'))) { + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + } + + $app->enqueueMessage( + Text::sprintf('COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE', Route::_('index.php?option=com_joomlaupdate')), + 'warning' + ); + + return false; + } + } + } + + // Was the package unpacked? + if (empty($package['type'])) { + if (in_array($installType, array('upload', 'url'))) { + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + } + + $app->enqueueMessage(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST'), 'error'); + + return false; + } + + // Install the package. + if (!$installer->install($package['dir'])) { + // There was an error installing the package. + $msg = Text::sprintf('COM_INSTALLER_INSTALL_ERROR', Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type']))); + $result = false; + $msgType = 'error'; + } else { + // Package installed successfully. + $msg = Text::sprintf('COM_INSTALLER_INSTALL_SUCCESS', Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($installLang . $package['type']))); + $result = true; + $msgType = 'message'; + } + + // This event allows a custom a post-flight: + $app->triggerEvent('onInstallerAfterInstaller', array($this, &$package, $installer, &$result, &$msg)); + + // Set some model state values. + $app->enqueueMessage($msg, $msgType); + $this->setState('name', $installer->get('name')); + $this->setState('result', $result); + $app->setUserState('com_installer.message', $installer->message); + $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); + $app->setUserState('com_installer.redirect_url', $installer->get('redirect_url')); + + // Cleanup the install files. + if (!is_file($package['packagefile'])) { + $package['packagefile'] = $app->get('tmp_path') . '/' . $package['packagefile']; + } + + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + + // Clear the cached extension data and menu cache + $this->cleanCache('_system'); + $this->cleanCache('com_modules'); + $this->cleanCache('com_plugins'); + $this->cleanCache('mod_menu'); + + return $result; + } + + /** + * Works out an installation package from a HTTP upload. + * + * @return mixed Package definition or false on failure. + */ + protected function _getPackageFromUpload() + { + // Get the uploaded file information. + $input = Factory::getApplication()->input; + + // Do not change the filter type 'raw'. We need this to let files containing PHP code to upload. See \JInputFiles::get. + $userfile = $input->files->get('install_package', null, 'raw'); + + // Make sure that file uploads are enabled in php. + if (!(bool) ini_get('file_uploads')) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLFILE'), 'error'); + + return false; + } + + // Make sure that zlib is loaded so that the package can be unpacked. + if (!extension_loaded('zlib')) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLZLIB'), 'error'); + + return false; + } + + // If there is no uploaded file, we have a problem... + if (!is_array($userfile)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_NO_FILE_SELECTED'), 'error'); + + return false; + } + + // Is the PHP tmp directory missing? + if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_NO_TMP_DIR)) { + Factory::getApplication()->enqueueMessage( + Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
    ' . Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), + 'error' + ); + + return false; + } + + // Is the max upload size too small in php.ini? + if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_INI_SIZE)) { + Factory::getApplication()->enqueueMessage( + Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
    ' . Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), + 'error' + ); + + return false; + } + + // Check if there was a different problem uploading the file. + if ($userfile['error'] || $userfile['size'] < 1) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 'error'); + + return false; + } + + // Build the appropriate paths. + $config = Factory::getApplication()->getConfig(); + $tmp_dest = $config->get('tmp_path') . '/' . $userfile['name']; + $tmp_src = $userfile['tmp_name']; + + // Move uploaded file. + File::upload($tmp_src, $tmp_dest, false, true); + + // Unpack the downloaded package file. + $package = InstallerHelper::unpack($tmp_dest, true); + + return $package; + } + + /** + * Install an extension from a directory + * + * @return array Package details or false on failure + * + * @since 1.5 + */ + protected function _getPackageFromFolder() + { + $input = Factory::getApplication()->input; + + // Get the path to the package to install. + $p_dir = $input->getString('install_directory'); + $p_dir = Path::clean($p_dir); + + // Did you give us a valid directory? + if (!is_dir($p_dir)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_PLEASE_ENTER_A_PACKAGE_DIRECTORY'), 'error'); + + return false; + } + + // Detect the package type + $type = InstallerHelper::detectType($p_dir); + + // Did you give us a valid package? + if (!$type) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_PATH_DOES_NOT_HAVE_A_VALID_PACKAGE'), 'error'); + } + + $package['packagefile'] = null; + $package['extractdir'] = null; + $package['dir'] = $p_dir; + $package['type'] = $type; + + return $package; + } + + /** + * Install an extension from a URL. + * + * @return bool|array Package details or false on failure. + * + * @since 1.5 + */ + protected function _getPackageFromUrl() + { + $input = Factory::getApplication()->input; + + // Get the URL of the package to install. + $url = $input->getString('install_url'); + + // Did you give us a URL? + if (!$url) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_ENTER_A_URL'), 'error'); + + return false; + } + + // We only allow http & https here + $uri = new Uri($url); + + if (!in_array($uri->getScheme(), ['http', 'https'])) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_INVALID_URL_SCHEME'), 'error'); + + return false; + } + + // Handle updater XML file case: + if (preg_match('/\.xml\s*$/', $url)) { + $update = new Update(); + $update->loadFromXml($url); + $package_url = trim($update->get('downloadurl', false)->_data); + + if ($package_url) { + $url = $package_url; + } + + unset($update); + } + + // Download the package at the URL given. + $p_file = InstallerHelper::downloadPackage($url); + + // Was the package downloaded? + if (!$p_file) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_INVALID_URL'), 'error'); + + return false; + } + + $tmp_dest = Factory::getApplication()->get('tmp_path'); + + // Unpack the downloaded package file. + $package = InstallerHelper::unpack($tmp_dest . '/' . $p_file, true); + + return $package; + } } diff --git a/administrator/components/com_installer/src/Model/InstallerModel.php b/administrator/components/com_installer/src/Model/InstallerModel.php index cf1a350f7d16b..65b7579fb1331 100644 --- a/administrator/components/com_installer/src/Model/InstallerModel.php +++ b/administrator/components/com_installer/src/Model/InstallerModel.php @@ -1,4 +1,5 @@ getState('list.ordering', 'name'); - $listDirn = $this->getState('list.direction', 'asc'); - - // Replace slashes so preg_match will work - $search = $this->getState('filter.search'); - $search = str_replace('/', ' ', $search); - $db = $this->getDatabase(); - - // Define which fields have to be processed in a custom way because of translation. - $customOrderFields = array('name', 'client_translated', 'type_translated', 'folder_translated', 'creationDate'); - - // Process searching, ordering and pagination for fields that need to be translated. - if (in_array($listOrder, $customOrderFields) || (!empty($search) && stripos($search, 'id:') !== 0)) - { - // Get results from database and translate them. - $db->setQuery($query); - $result = $db->loadObjectList(); - $this->translate($result); - - // Process searching. - if (!empty($search) && stripos($search, 'id:') !== 0) - { - $escapedSearchString = $this->refineSearchStringToRegex($search, '/'); - - // By default search only the extension name field. - $searchFields = array('name'); - - // If in update sites view search also in the update site name field. - if ($this instanceof UpdatesitesModel) - { - $searchFields[] = 'update_site_name'; - } - - foreach ($result as $i => $item) - { - // Check if search string exists in any of the fields to be searched. - $found = 0; - - foreach ($searchFields as $key => $field) - { - if (!$found && preg_match('/' . $escapedSearchString . '/i', $item->{$field})) - { - $found = 1; - } - } - - // If search string was not found in any of the fields searched remove it from results array. - if (!$found) - { - unset($result[$i]); - } - } - } - - // Process ordering. - // Sort array object by selected ordering and selected direction. Sort is case insensitive and using locale sorting. - $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) == 'desc' ? -1 : 1, false, true); - - // Process pagination. - $total = count($result); - $this->cache[$this->getStoreId('getTotal')] = $total; - - if ($total <= $limitstart) - { - $limitstart = 0; - $this->setState('list.limitstart', 0); - } - - return array_slice($result, $limitstart, $limit ?: null); - } - - // Process searching, ordering and pagination for regular database fields. - $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); - $result = parent::_getList($query, $limitstart, $limit); - $this->translate($result); - - return $result; - } - - /** - * Translate a list of objects - * - * @param array $items The array of objects - * - * @return array The array of translated objects - */ - protected function translate(&$items) - { - $lang = Factory::getLanguage(); - - foreach ($items as &$item) - { - if (strlen($item->manifest_cache) && $data = json_decode($item->manifest_cache)) - { - foreach ($data as $key => $value) - { - if ($key == 'type') - { - // Ignore the type field - continue; - } - - $item->$key = $value; - } - } - - $item->author_info = @$item->authorEmail . '
    ' . @$item->authorUrl; - $item->client = Text::_([0 => 'JSITE', 1 => 'JADMINISTRATOR', 3 => 'JAPI'][$item->client_id] ?? 'JSITE'); - $item->client_translated = $item->client; - $item->type_translated = Text::_('COM_INSTALLER_TYPE_' . strtoupper($item->type)); - $item->folder_translated = @$item->folder ? $item->folder : Text::_('COM_INSTALLER_TYPE_NONAPPLICABLE'); - - $path = $item->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE; - - switch ($item->type) - { - case 'component': - $extension = $item->element; - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) || $lang->load("$extension.sys", $source); - break; - case 'file': - $extension = 'files_' . $item->element; - $lang->load("$extension.sys", JPATH_SITE); - break; - case 'library': - $parts = explode('/', $item->element); - $vendor = (isset($parts[1]) ? $parts[0] : null); - $extension = 'lib_' . ($vendor ? implode('_', $parts) : $item->element); - - if (!$lang->load("$extension.sys", $path)) - { - $source = $path . '/libraries/' . ($vendor ? $vendor . '/' . $parts[1] : $item->element); - $lang->load("$extension.sys", $source); - } - break; - case 'module': - $extension = $item->element; - $source = $path . '/modules/' . $extension; - $lang->load("$extension.sys", $path) || $lang->load("$extension.sys", $source); - break; - case 'plugin': - $extension = 'plg_' . $item->folder . '_' . $item->element; - $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) || $lang->load("$extension.sys", $source); - break; - case 'template': - $extension = 'tpl_' . $item->element; - $source = $path . '/templates/' . $item->element; - $lang->load("$extension.sys", $path) || $lang->load("$extension.sys", $source); - break; - case 'package': - default: - $extension = $item->element; - $lang->load("$extension.sys", JPATH_SITE); - break; - } - - // Translate the extension name if possible - $item->name = Text::_($item->name); - - settype($item->description, 'string'); - - if (!in_array($item->type, array('language'))) - { - $item->description = Text::_($item->description); - } - } - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\ListModel + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'name', + 'client_id', + 'client', 'client_translated', + 'enabled', + 'type', 'type_translated', + 'folder', 'folder_translated', + 'extension_id', + 'creationDate', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Returns an object list + * + * @param DatabaseQuery $query The query + * @param int $limitstart Offset + * @param int $limit The number of records + * + * @return array + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $listOrder = $this->getState('list.ordering', 'name'); + $listDirn = $this->getState('list.direction', 'asc'); + + // Replace slashes so preg_match will work + $search = $this->getState('filter.search'); + $search = str_replace('/', ' ', $search); + $db = $this->getDatabase(); + + // Define which fields have to be processed in a custom way because of translation. + $customOrderFields = array('name', 'client_translated', 'type_translated', 'folder_translated', 'creationDate'); + + // Process searching, ordering and pagination for fields that need to be translated. + if (in_array($listOrder, $customOrderFields) || (!empty($search) && stripos($search, 'id:') !== 0)) { + // Get results from database and translate them. + $db->setQuery($query); + $result = $db->loadObjectList(); + $this->translate($result); + + // Process searching. + if (!empty($search) && stripos($search, 'id:') !== 0) { + $escapedSearchString = $this->refineSearchStringToRegex($search, '/'); + + // By default search only the extension name field. + $searchFields = array('name'); + + // If in update sites view search also in the update site name field. + if ($this instanceof UpdatesitesModel) { + $searchFields[] = 'update_site_name'; + } + + foreach ($result as $i => $item) { + // Check if search string exists in any of the fields to be searched. + $found = 0; + + foreach ($searchFields as $key => $field) { + if (!$found && preg_match('/' . $escapedSearchString . '/i', $item->{$field})) { + $found = 1; + } + } + + // If search string was not found in any of the fields searched remove it from results array. + if (!$found) { + unset($result[$i]); + } + } + } + + // Process ordering. + // Sort array object by selected ordering and selected direction. Sort is case insensitive and using locale sorting. + $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) == 'desc' ? -1 : 1, false, true); + + // Process pagination. + $total = count($result); + $this->cache[$this->getStoreId('getTotal')] = $total; + + if ($total <= $limitstart) { + $limitstart = 0; + $this->setState('list.limitstart', 0); + } + + return array_slice($result, $limitstart, $limit ?: null); + } + + // Process searching, ordering and pagination for regular database fields. + $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); + $result = parent::_getList($query, $limitstart, $limit); + $this->translate($result); + + return $result; + } + + /** + * Translate a list of objects + * + * @param array $items The array of objects + * + * @return array The array of translated objects + */ + protected function translate(&$items) + { + $lang = Factory::getLanguage(); + + foreach ($items as &$item) { + if (strlen($item->manifest_cache) && $data = json_decode($item->manifest_cache)) { + foreach ($data as $key => $value) { + if ($key == 'type') { + // Ignore the type field + continue; + } + + $item->$key = $value; + } + } + + $item->author_info = @$item->authorEmail . '
    ' . @$item->authorUrl; + $item->client = Text::_([0 => 'JSITE', 1 => 'JADMINISTRATOR', 3 => 'JAPI'][$item->client_id] ?? 'JSITE'); + $item->client_translated = $item->client; + $item->type_translated = Text::_('COM_INSTALLER_TYPE_' . strtoupper($item->type)); + $item->folder_translated = @$item->folder ? $item->folder : Text::_('COM_INSTALLER_TYPE_NONAPPLICABLE'); + + $path = $item->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE; + + switch ($item->type) { + case 'component': + $extension = $item->element; + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) || $lang->load("$extension.sys", $source); + break; + case 'file': + $extension = 'files_' . $item->element; + $lang->load("$extension.sys", JPATH_SITE); + break; + case 'library': + $parts = explode('/', $item->element); + $vendor = (isset($parts[1]) ? $parts[0] : null); + $extension = 'lib_' . ($vendor ? implode('_', $parts) : $item->element); + + if (!$lang->load("$extension.sys", $path)) { + $source = $path . '/libraries/' . ($vendor ? $vendor . '/' . $parts[1] : $item->element); + $lang->load("$extension.sys", $source); + } + break; + case 'module': + $extension = $item->element; + $source = $path . '/modules/' . $extension; + $lang->load("$extension.sys", $path) || $lang->load("$extension.sys", $source); + break; + case 'plugin': + $extension = 'plg_' . $item->folder . '_' . $item->element; + $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) || $lang->load("$extension.sys", $source); + break; + case 'template': + $extension = 'tpl_' . $item->element; + $source = $path . '/templates/' . $item->element; + $lang->load("$extension.sys", $path) || $lang->load("$extension.sys", $source); + break; + case 'package': + default: + $extension = $item->element; + $lang->load("$extension.sys", JPATH_SITE); + break; + } + + // Translate the extension name if possible + $item->name = Text::_($item->name); + + settype($item->description, 'string'); + + if (!in_array($item->type, array('language'))) { + $item->description = Text::_($item->description); + } + } + } } diff --git a/administrator/components/com_installer/src/Model/LanguagesModel.php b/administrator/components/com_installer/src/Model/LanguagesModel.php index 12bdde329517f..aad35c9b1f29c 100644 --- a/administrator/components/com_installer/src/Model/LanguagesModel.php +++ b/administrator/components/com_installer/src/Model/LanguagesModel.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('us.location')) - ->from($db->quoteName('#__extensions', 'e')) - ->where($db->quoteName('e.type') . ' = ' . $db->quote('package')) - ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_en-GB')) - ->where($db->quoteName('e.client_id') . ' = 0') - ->join( - 'LEFT', $db->quoteName('#__update_sites_extensions', 'use') - . ' ON ' . $db->quoteName('use.extension_id') . ' = ' . $db->quoteName('e.extension_id') - ) - ->join( - 'LEFT', $db->quoteName('#__update_sites', 'us') - . ' ON ' . $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('use.update_site_id') - ); - - return $db->setQuery($query)->loadResult(); - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.7.0 - */ - public function getItems() - { - // Get a storage key. - $store = $this->getStoreId(); - - // Try to load the data from internal storage. - if (isset($this->cache[$store])) - { - return $this->cache[$store]; - } - - try - { - // Load the list items and add the items to the internal cache. - $this->cache[$store] = $this->getLanguages(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $this->cache[$store]; - } - - /** - * Gets an array of objects from the updatesite. - * - * @return object[] An array of results. - * - * @since 3.0 - * @throws \RuntimeException - */ - protected function getLanguages() - { - $updateSite = $this->getUpdateSite(); - - // Check whether the updateserver is found - if (empty($updateSite)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_WARNING_NO_LANGUAGES_UPDATESERVER'), 'warning'); - - return; - } - - try - { - $response = HttpFactory::getHttp()->get($updateSite); - } - catch (\RuntimeException $e) - { - $response = null; - } - - if ($response === null || $response->code !== 200) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_ERROR_CANT_CONNECT_TO_UPDATESERVER', $updateSite), 'error'); - - return; - } - - $updateSiteXML = simplexml_load_string($response->body); - - if (!$updateSiteXML) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_ERROR_CANT_RETRIEVE_XML', $updateSite), 'error'); - - return; - } - - $languages = array(); - $search = strtolower($this->getState('filter.search')); - - foreach ($updateSiteXML->extension as $extension) - { - $language = new \stdClass; - - foreach ($extension->attributes() as $key => $value) - { - $language->$key = (string) $value; - } - - if ($search) - { - if (strpos(strtolower($language->name), $search) === false - && strpos(strtolower($language->element), $search) === false) - { - continue; - } - } - - $languages[$language->name] = $language; - } - - // Workaround for php 5.3 - $that = $this; - - // Sort the array by value of subarray - usort( - $languages, - function ($a, $b) use ($that) - { - $ordering = $that->getState('list.ordering'); - - if (strtolower($that->getState('list.direction')) === 'asc') - { - return StringHelper::strcmp($a->$ordering, $b->$ordering); - } - else - { - return StringHelper::strcmp($b->$ordering, $a->$ordering); - } - } - ); - - // Count the non-paginated list - $this->languageCount = count($languages); - $limit = ($this->getState('list.limit') > 0) ? $this->getState('list.limit') : $this->languageCount; - - return array_slice($languages, $this->getStart(), $limit); - } - - /** - * Returns a record count for the updatesite. - * - * @param \Joomla\Database\DatabaseQuery|string $query The query. - * - * @return integer Number of rows for query. - * - * @since 3.7.0 - */ - protected function _getListCount($query) - { - return $this->languageCount; - } - - /** - * Method to get a store id based on model configuration state. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 2.5.7 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering list order - * @param string $direction direction in the list - * - * @return void - * - * @since 2.5.7 - */ - protected function populateState($ordering = 'name', $direction = 'asc') - { - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - - $this->setState('extension_message', Factory::getApplication()->getUserState('com_installer.extension_message')); - - parent::populateState($ordering, $direction); - } - - /** - * Method to compare two languages in order to sort them. - * - * @param object $lang1 The first language. - * @param object $lang2 The second language. - * - * @return integer - * - * @since 3.7.0 - */ - protected function compareLanguages($lang1, $lang2) - { - return strcmp($lang1->name, $lang2->name); - } + /** + * Language count + * + * @var integer + * @since 3.7.0 + */ + private $languageCount; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\ListModel + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'name', + 'element', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Get the Update Site + * + * @since 3.7.0 + * + * @return string The URL of the Accredited Languagepack Updatesite XML + */ + private function getUpdateSite() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('us.location')) + ->from($db->quoteName('#__extensions', 'e')) + ->where($db->quoteName('e.type') . ' = ' . $db->quote('package')) + ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_en-GB')) + ->where($db->quoteName('e.client_id') . ' = 0') + ->join( + 'LEFT', + $db->quoteName('#__update_sites_extensions', 'use') + . ' ON ' . $db->quoteName('use.extension_id') . ' = ' . $db->quoteName('e.extension_id') + ) + ->join( + 'LEFT', + $db->quoteName('#__update_sites', 'us') + . ' ON ' . $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('use.update_site_id') + ); + + return $db->setQuery($query)->loadResult(); + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.7.0 + */ + public function getItems() + { + // Get a storage key. + $store = $this->getStoreId(); + + // Try to load the data from internal storage. + if (isset($this->cache[$store])) { + return $this->cache[$store]; + } + + try { + // Load the list items and add the items to the internal cache. + $this->cache[$store] = $this->getLanguages(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $this->cache[$store]; + } + + /** + * Gets an array of objects from the updatesite. + * + * @return object[] An array of results. + * + * @since 3.0 + * @throws \RuntimeException + */ + protected function getLanguages() + { + $updateSite = $this->getUpdateSite(); + + // Check whether the updateserver is found + if (empty($updateSite)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_WARNING_NO_LANGUAGES_UPDATESERVER'), 'warning'); + + return; + } + + try { + $response = HttpFactory::getHttp()->get($updateSite); + } catch (\RuntimeException $e) { + $response = null; + } + + if ($response === null || $response->code !== 200) { + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_ERROR_CANT_CONNECT_TO_UPDATESERVER', $updateSite), 'error'); + + return; + } + + $updateSiteXML = simplexml_load_string($response->body); + + if (!$updateSiteXML) { + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_ERROR_CANT_RETRIEVE_XML', $updateSite), 'error'); + + return; + } + + $languages = array(); + $search = strtolower($this->getState('filter.search')); + + foreach ($updateSiteXML->extension as $extension) { + $language = new \stdClass(); + + foreach ($extension->attributes() as $key => $value) { + $language->$key = (string) $value; + } + + if ($search) { + if ( + strpos(strtolower($language->name), $search) === false + && strpos(strtolower($language->element), $search) === false + ) { + continue; + } + } + + $languages[$language->name] = $language; + } + + // Workaround for php 5.3 + $that = $this; + + // Sort the array by value of subarray + usort( + $languages, + function ($a, $b) use ($that) { + $ordering = $that->getState('list.ordering'); + + if (strtolower($that->getState('list.direction')) === 'asc') { + return StringHelper::strcmp($a->$ordering, $b->$ordering); + } else { + return StringHelper::strcmp($b->$ordering, $a->$ordering); + } + } + ); + + // Count the non-paginated list + $this->languageCount = count($languages); + $limit = ($this->getState('list.limit') > 0) ? $this->getState('list.limit') : $this->languageCount; + + return array_slice($languages, $this->getStart(), $limit); + } + + /** + * Returns a record count for the updatesite. + * + * @param \Joomla\Database\DatabaseQuery|string $query The query. + * + * @return integer Number of rows for query. + * + * @since 3.7.0 + */ + protected function _getListCount($query) + { + return $this->languageCount; + } + + /** + * Method to get a store id based on model configuration state. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 2.5.7 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering list order + * @param string $direction direction in the list + * + * @return void + * + * @since 2.5.7 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + + $this->setState('extension_message', Factory::getApplication()->getUserState('com_installer.extension_message')); + + parent::populateState($ordering, $direction); + } + + /** + * Method to compare two languages in order to sort them. + * + * @param object $lang1 The first language. + * @param object $lang2 The second language. + * + * @return integer + * + * @since 3.7.0 + */ + protected function compareLanguages($lang1, $lang2) + { + return strcmp($lang1->name, $lang2->name); + } } diff --git a/administrator/components/com_installer/src/Model/ManageModel.php b/administrator/components/com_installer/src/Model/ManageModel.php index 6832a5e01bd14..ddb467e86306a 100644 --- a/administrator/components/com_installer/src/Model/ManageModel.php +++ b/administrator/components/com_installer/src/Model/ManageModel.php @@ -1,4 +1,5 @@ setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); - $this->setState('filter.package_id', $this->getUserStateFromRequest($this->context . '.filter.package_id', 'filter_package_id', null, 'int')); - $this->setState('filter.status', $this->getUserStateFromRequest($this->context . '.filter.status', 'filter_status', '', 'string')); - $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); - $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); - $this->setState('filter.core', $this->getUserStateFromRequest($this->context . '.filter.core', 'filter_core', '', 'string')); - - $this->setState('message', $app->getUserState('com_installer.message')); - $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - - parent::populateState($ordering, $direction); - } - - /** - * Enable/Disable an extension. - * - * @param array $eid Extension ids to un/publish - * @param int $value Publish value - * - * @return boolean True on success - * - * @throws \Exception - * - * @since 1.5 - */ - public function publish(&$eid = array(), $value = 1) - { - if (!Factory::getUser()->authorise('core.edit.state', 'com_installer')) - { - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - - return false; - } - - $result = true; - - /* - * Ensure eid is an array of extension ids - * @todo: If it isn't an array do we want to set an error and fail? - */ - if (!is_array($eid)) - { - $eid = array($eid); - } - - // Get a table object for the extension type - $table = new Extension($this->getDatabase()); - - // Enable the extension in the table and store it in the database - foreach ($eid as $i => $id) - { - $table->load($id); - - if ($table->type == 'template') - { - $style = new StyleTable($this->getDatabase()); - - if ($style->load(array('template' => $table->element, 'client_id' => $table->client_id, 'home' => 1))) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_ERROR_DISABLE_DEFAULT_TEMPLATE_NOT_PERMITTED'), 'notice'); - unset($eid[$i]); - continue; - } - - // Parent template cannot be disabled if there are children - if ($style->load(['parent' => $table->element, 'client_id' => $table->client_id])) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_ERROR_DISABLE_PARENT_TEMPLATE_NOT_PERMITTED'), 'notice'); - unset($eid[$i]); - continue; - } - } - - if ($table->protected == 1) - { - $result = false; - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - } - else - { - $table->enabled = $value; - } - - $context = $this->option . '.' . $this->name; - - PluginHelper::importPlugin('extension'); - Factory::getApplication()->triggerEvent('onExtensionChangeState', array($context, $eid, $value)); - - if (!$table->store()) - { - $this->setError($table->getError()); - $result = false; - } - } - - // Clear the cached extension data and menu cache - $this->cleanCache('_system'); - $this->cleanCache('com_modules'); - $this->cleanCache('mod_menu'); - - return $result; - } - - /** - * Refreshes the cached manifest information for an extension. - * - * @param int|int[] $eid extension identifier (key in #__extensions) - * - * @return boolean result of refresh - * - * @since 1.6 - */ - public function refresh($eid) - { - if (!is_array($eid)) - { - $eid = array($eid => 0); - } - - // Get an installer object for the extension type - $installer = Installer::getInstance(); - $result = 0; - - // Uninstall the chosen extensions - foreach ($eid as $id) - { - $result |= $installer->refreshManifestCache($id); - } - - return $result; - } - - /** - * Remove (uninstall) an extension - * - * @param array $eid An array of identifiers - * - * @return boolean True on success - * - * @throws \Exception - * - * @since 1.5 - */ - public function remove($eid = array()) - { - if (!Factory::getUser()->authorise('core.delete', 'com_installer')) - { - Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); - - return false; - } - - /* - * Ensure eid is an array of extension ids in the form id => client_id - * @todo: If it isn't an array do we want to set an error and fail? - */ - if (!is_array($eid)) - { - $eid = array($eid => 0); - } - - // Get an installer object for the extension type - $installer = Installer::getInstance(); - $row = new \Joomla\CMS\Table\Extension($this->getDatabase()); - - // Uninstall the chosen extensions - $msgs = array(); - $result = false; - - foreach ($eid as $id) - { - $id = trim($id); - $row->load($id); - $result = false; - - // Do not allow to uninstall locked extensions. - if ((int) $row->locked === 1) - { - $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR_LOCKED_EXTENSION', $row->name, $id); - - continue; - } - - $langstring = 'COM_INSTALLER_TYPE_TYPE_' . strtoupper($row->type); - $rowtype = Text::_($langstring); - - if (strpos($rowtype, $langstring) !== false) - { - $rowtype = $row->type; - } - - if ($row->type) - { - $result = $installer->uninstall($row->type, $id); - - // Build an array of extensions that failed to uninstall - if ($result === false) - { - // There was an error in uninstalling the package - $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR', $rowtype); - - continue; - } - - // Package uninstalled successfully - $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_SUCCESS', $rowtype); - $result = true; - - continue; - } - - // There was an error in uninstalling the package - $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR', $rowtype); - } - - $msg = implode('
    ', $msgs); - $app = Factory::getApplication(); - $app->enqueueMessage($msg); - $this->setState('action', 'remove'); - $this->setState('name', $installer->get('name')); - $app->setUserState('com_installer.message', $installer->message); - $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); - - // Clear the cached extension data and menu cache - $this->cleanCache('_system'); - $this->cleanCache('com_modules'); - $this->cleanCache('com_plugins'); - $this->cleanCache('mod_menu'); - - return $result; - } - - /** - * Method to get the database query - * - * @return DatabaseQuery The database query - * - * @since 1.6 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('*') - ->select('2*protected+(1-protected)*enabled AS status') - ->from('#__extensions') - ->where('state = 0'); - - // Process select filters. - $status = $this->getState('filter.status', ''); - $type = $this->getState('filter.type'); - $clientId = $this->getState('filter.client_id', ''); - $folder = $this->getState('filter.folder'); - $core = $this->getState('filter.core', ''); - $packageId = $this->getState('filter.package_id', ''); - - if ($status !== '') - { - if ($status === '2') - { - $query->where('protected = 1'); - } - elseif ($status === '3') - { - $query->where('protected = 0'); - } - else - { - $status = (int) $status; - $query->where($db->quoteName('protected') . ' = 0') - ->where($db->quoteName('enabled') . ' = :status') - ->bind(':status', $status, ParameterType::INTEGER); - } - } - - if ($type) - { - $query->where($db->quoteName('type') . ' = :type') - ->bind(':type', $type); - } - - if ($clientId !== '') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - } - - if ($packageId !== '') - { - $packageId = (int) $packageId; - $query->where( - '((' . $db->quoteName('package_id') . ' = :packageId1) OR ' - . '(' . $db->quoteName('extension_id') . ' = :packageId2))' - ) - ->bind([':packageId1',':packageId2'], $packageId, ParameterType::INTEGER); - } - - if ($folder) - { - $folder = $folder === '*' ? '' : $folder; - $query->where($db->quoteName('folder') . ' = :folder') - ->bind(':folder', $folder); - } - - // Filter by core extensions. - if ($core === '1' || $core === '0') - { - $coreExtensionIds = ExtensionHelper::getCoreExtensionIds(); - $method = $core === '1' ? 'whereIn' : 'whereNotIn'; - $query->$method($db->quoteName('extension_id'), $coreExtensionIds); - } - - // Process search filter (extension id). - $search = $this->getState('filter.search'); - - if (!empty($search) && stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $ids, ParameterType::INTEGER); - } - - // Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in extension.php). - - return $query; - } - - /** - * Load the changelog details for a given extension. - * - * @param integer $eid The extension ID - * @param string $source The view the changelog is for, this is used to determine which version number to show - * - * @return string The output to show in the modal. - * - * @since 4.0.0 - */ - public function loadChangelog($eid, $source) - { - // Get the changelog URL - $eid = (int) $eid; - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 'extensions.element', - 'extensions.type', - 'extensions.folder', - 'extensions.changelogurl', - 'extensions.manifest_cache', - 'extensions.client_id' - ] - ) - ) - ->select($db->quoteName('updates.version', 'updateVersion')) - ->from($db->quoteName('#__extensions', 'extensions')) - ->join( - 'LEFT', - $db->quoteName('#__updates', 'updates'), - $db->quoteName('updates.extension_id') . ' = ' . $db->quoteName('extensions.extension_id') - ) - ->where($db->quoteName('extensions.extension_id') . ' = :eid') - ->bind(':eid', $eid, ParameterType::INTEGER); - $db->setQuery($query); - - $extensions = $db->loadObjectList(); - $this->translate($extensions); - $extension = array_shift($extensions); - - if (!$extension->changelogurl) - { - return ''; - } - - $changelog = new Changelog; - $changelog->setVersion($source === 'manage' ? $extension->version : $extension->updateVersion); - $changelog->loadFromXml($extension->changelogurl); - - // Read all the entries - $entries = array( - 'security' => array(), - 'fix' => array(), - 'addition' => array(), - 'change' => array(), - 'remove' => array(), - 'language' => array(), - 'note' => array() - ); - - array_walk( - $entries, - function (&$value, $name) use ($changelog) { - if ($field = $changelog->get($name)) - { - $value = $changelog->get($name)->data; - } - } - ); - - $layout = new FileLayout('joomla.installer.changelog'); - $output = $layout->render($entries); - - return $output; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\ListModel + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'status', + 'name', + 'client_id', + 'client', 'client_translated', + 'type', 'type_translated', + 'folder', 'folder_translated', + 'package_id', + 'extension_id', + 'creationDate', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @throws \Exception + * + * @since 1.6 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + $app = Factory::getApplication(); + + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); + $this->setState('filter.package_id', $this->getUserStateFromRequest($this->context . '.filter.package_id', 'filter_package_id', null, 'int')); + $this->setState('filter.status', $this->getUserStateFromRequest($this->context . '.filter.status', 'filter_status', '', 'string')); + $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); + $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); + $this->setState('filter.core', $this->getUserStateFromRequest($this->context . '.filter.core', 'filter_core', '', 'string')); + + $this->setState('message', $app->getUserState('com_installer.message')); + $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + + parent::populateState($ordering, $direction); + } + + /** + * Enable/Disable an extension. + * + * @param array $eid Extension ids to un/publish + * @param int $value Publish value + * + * @return boolean True on success + * + * @throws \Exception + * + * @since 1.5 + */ + public function publish(&$eid = array(), $value = 1) + { + if (!Factory::getUser()->authorise('core.edit.state', 'com_installer')) { + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + + return false; + } + + $result = true; + + /* + * Ensure eid is an array of extension ids + * @todo: If it isn't an array do we want to set an error and fail? + */ + if (!is_array($eid)) { + $eid = array($eid); + } + + // Get a table object for the extension type + $table = new Extension($this->getDatabase()); + + // Enable the extension in the table and store it in the database + foreach ($eid as $i => $id) { + $table->load($id); + + if ($table->type == 'template') { + $style = new StyleTable($this->getDatabase()); + + if ($style->load(array('template' => $table->element, 'client_id' => $table->client_id, 'home' => 1))) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_ERROR_DISABLE_DEFAULT_TEMPLATE_NOT_PERMITTED'), 'notice'); + unset($eid[$i]); + continue; + } + + // Parent template cannot be disabled if there are children + if ($style->load(['parent' => $table->element, 'client_id' => $table->client_id])) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_ERROR_DISABLE_PARENT_TEMPLATE_NOT_PERMITTED'), 'notice'); + unset($eid[$i]); + continue; + } + } + + if ($table->protected == 1) { + $result = false; + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + } else { + $table->enabled = $value; + } + + $context = $this->option . '.' . $this->name; + + PluginHelper::importPlugin('extension'); + Factory::getApplication()->triggerEvent('onExtensionChangeState', array($context, $eid, $value)); + + if (!$table->store()) { + $this->setError($table->getError()); + $result = false; + } + } + + // Clear the cached extension data and menu cache + $this->cleanCache('_system'); + $this->cleanCache('com_modules'); + $this->cleanCache('mod_menu'); + + return $result; + } + + /** + * Refreshes the cached manifest information for an extension. + * + * @param int|int[] $eid extension identifier (key in #__extensions) + * + * @return boolean result of refresh + * + * @since 1.6 + */ + public function refresh($eid) + { + if (!is_array($eid)) { + $eid = array($eid => 0); + } + + // Get an installer object for the extension type + $installer = Installer::getInstance(); + $result = 0; + + // Uninstall the chosen extensions + foreach ($eid as $id) { + $result |= $installer->refreshManifestCache($id); + } + + return $result; + } + + /** + * Remove (uninstall) an extension + * + * @param array $eid An array of identifiers + * + * @return boolean True on success + * + * @throws \Exception + * + * @since 1.5 + */ + public function remove($eid = array()) + { + if (!Factory::getUser()->authorise('core.delete', 'com_installer')) { + Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); + + return false; + } + + /* + * Ensure eid is an array of extension ids in the form id => client_id + * @todo: If it isn't an array do we want to set an error and fail? + */ + if (!is_array($eid)) { + $eid = array($eid => 0); + } + + // Get an installer object for the extension type + $installer = Installer::getInstance(); + $row = new \Joomla\CMS\Table\Extension($this->getDatabase()); + + // Uninstall the chosen extensions + $msgs = array(); + $result = false; + + foreach ($eid as $id) { + $id = trim($id); + $row->load($id); + $result = false; + + // Do not allow to uninstall locked extensions. + if ((int) $row->locked === 1) { + $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR_LOCKED_EXTENSION', $row->name, $id); + + continue; + } + + $langstring = 'COM_INSTALLER_TYPE_TYPE_' . strtoupper($row->type); + $rowtype = Text::_($langstring); + + if (strpos($rowtype, $langstring) !== false) { + $rowtype = $row->type; + } + + if ($row->type) { + $result = $installer->uninstall($row->type, $id); + + // Build an array of extensions that failed to uninstall + if ($result === false) { + // There was an error in uninstalling the package + $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR', $rowtype); + + continue; + } + + // Package uninstalled successfully + $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_SUCCESS', $rowtype); + $result = true; + + continue; + } + + // There was an error in uninstalling the package + $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR', $rowtype); + } + + $msg = implode('
    ', $msgs); + $app = Factory::getApplication(); + $app->enqueueMessage($msg); + $this->setState('action', 'remove'); + $this->setState('name', $installer->get('name')); + $app->setUserState('com_installer.message', $installer->message); + $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); + + // Clear the cached extension data and menu cache + $this->cleanCache('_system'); + $this->cleanCache('com_modules'); + $this->cleanCache('com_plugins'); + $this->cleanCache('mod_menu'); + + return $result; + } + + /** + * Method to get the database query + * + * @return DatabaseQuery The database query + * + * @since 1.6 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->select('2*protected+(1-protected)*enabled AS status') + ->from('#__extensions') + ->where('state = 0'); + + // Process select filters. + $status = $this->getState('filter.status', ''); + $type = $this->getState('filter.type'); + $clientId = $this->getState('filter.client_id', ''); + $folder = $this->getState('filter.folder'); + $core = $this->getState('filter.core', ''); + $packageId = $this->getState('filter.package_id', ''); + + if ($status !== '') { + if ($status === '2') { + $query->where('protected = 1'); + } elseif ($status === '3') { + $query->where('protected = 0'); + } else { + $status = (int) $status; + $query->where($db->quoteName('protected') . ' = 0') + ->where($db->quoteName('enabled') . ' = :status') + ->bind(':status', $status, ParameterType::INTEGER); + } + } + + if ($type) { + $query->where($db->quoteName('type') . ' = :type') + ->bind(':type', $type); + } + + if ($clientId !== '') { + $clientId = (int) $clientId; + $query->where($db->quoteName('client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + } + + if ($packageId !== '') { + $packageId = (int) $packageId; + $query->where( + '((' . $db->quoteName('package_id') . ' = :packageId1) OR ' + . '(' . $db->quoteName('extension_id') . ' = :packageId2))' + ) + ->bind([':packageId1',':packageId2'], $packageId, ParameterType::INTEGER); + } + + if ($folder) { + $folder = $folder === '*' ? '' : $folder; + $query->where($db->quoteName('folder') . ' = :folder') + ->bind(':folder', $folder); + } + + // Filter by core extensions. + if ($core === '1' || $core === '0') { + $coreExtensionIds = ExtensionHelper::getCoreExtensionIds(); + $method = $core === '1' ? 'whereIn' : 'whereNotIn'; + $query->$method($db->quoteName('extension_id'), $coreExtensionIds); + } + + // Process search filter (extension id). + $search = $this->getState('filter.search'); + + if (!empty($search) && stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $ids, ParameterType::INTEGER); + } + + // Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in extension.php). + + return $query; + } + + /** + * Load the changelog details for a given extension. + * + * @param integer $eid The extension ID + * @param string $source The view the changelog is for, this is used to determine which version number to show + * + * @return string The output to show in the modal. + * + * @since 4.0.0 + */ + public function loadChangelog($eid, $source) + { + // Get the changelog URL + $eid = (int) $eid; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 'extensions.element', + 'extensions.type', + 'extensions.folder', + 'extensions.changelogurl', + 'extensions.manifest_cache', + 'extensions.client_id' + ] + ) + ) + ->select($db->quoteName('updates.version', 'updateVersion')) + ->from($db->quoteName('#__extensions', 'extensions')) + ->join( + 'LEFT', + $db->quoteName('#__updates', 'updates'), + $db->quoteName('updates.extension_id') . ' = ' . $db->quoteName('extensions.extension_id') + ) + ->where($db->quoteName('extensions.extension_id') . ' = :eid') + ->bind(':eid', $eid, ParameterType::INTEGER); + $db->setQuery($query); + + $extensions = $db->loadObjectList(); + $this->translate($extensions); + $extension = array_shift($extensions); + + if (!$extension->changelogurl) { + return ''; + } + + $changelog = new Changelog(); + $changelog->setVersion($source === 'manage' ? $extension->version : $extension->updateVersion); + $changelog->loadFromXml($extension->changelogurl); + + // Read all the entries + $entries = array( + 'security' => array(), + 'fix' => array(), + 'addition' => array(), + 'change' => array(), + 'remove' => array(), + 'language' => array(), + 'note' => array() + ); + + array_walk( + $entries, + function (&$value, $name) use ($changelog) { + if ($field = $changelog->get($name)) { + $value = $changelog->get($name)->data; + } + } + ); + + $layout = new FileLayout('joomla.installer.changelog'); + $output = $layout->render($entries); + + return $output; + } } diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index 85d1ae353738d..64a9ee428f2de 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -1,4 +1,5 @@ setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); - $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); - $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); - - $app = Factory::getApplication(); - $this->setState('message', $app->getUserState('com_installer.message')); - $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - - parent::populateState($ordering, $direction); - } - - /** - * Method to get the database query - * - * @return \Joomla\Database\DatabaseQuery The database query - * - * @since 1.6 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - - // Grab updates ignoring new installs - $query = $db->getQuery(true) - ->select('u.*') - ->select($db->quoteName('e.manifest_cache')) - ->from($db->quoteName('#__updates', 'u')) - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('u.extension_id') - ) - ->where($db->quoteName('u.extension_id') . ' != 0'); - - // Process select filters. - $clientId = $this->getState('filter.client_id'); - $type = $this->getState('filter.type'); - $folder = $this->getState('filter.folder'); - $extensionId = $this->getState('filter.extension_id'); - - if ($type) - { - $query->where($db->quoteName('u.type') . ' = :type') - ->bind(':type', $type); - } - - if ($clientId != '') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('u.client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - } - - if ($folder != '' && in_array($type, array('plugin', 'library', ''))) - { - $folder = $folder === '*' ? '' : $folder; - $query->where($db->quoteName('u.folder') . ' = :folder') - ->bind(':folder', $folder); - } - - if ($extensionId) - { - $extensionId = (int) $extensionId; - $query->where($db->quoteName('u.extension_id') . ' = :extensionid') - ->bind(':extensionid', $extensionId, ParameterType::INTEGER); - } - else - { - $eid = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - $query->where($db->quoteName('u.extension_id') . ' != 0') - ->where($db->quoteName('u.extension_id') . ' != :eid') - ->bind(':eid', $eid, ParameterType::INTEGER); - } - - // Process search filter. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'eid:') !== false) - { - $sid = (int) substr($search, 4); - $query->where($db->quoteName('u.extension_id') . ' = :sid') - ->bind(':sid', $sid, ParameterType::INTEGER); - } - else - { - if (stripos($search, 'uid:') !== false) - { - $suid = (int) substr($search, 4); - $query->where($db->quoteName('u.update_site_id') . ' = :suid') - ->bind(':suid', $suid, ParameterType::INTEGER); - } - elseif (stripos($search, 'id:') !== false) - { - $uid = (int) substr($search, 3); - $query->where($db->quoteName('u.update_id') . ' = :uid') - ->bind(':uid', $uid, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where($db->quoteName('u.name') . ' LIKE :search') - ->bind(':search', $search); - } - } - } - - return $query; - } - - /** - * Translate a list of objects - * - * @param array $items The array of objects - * - * @return array The array of translated objects - * - * @since 3.5 - */ - protected function translate(&$items) - { - foreach ($items as &$item) - { - $item->client_translated = Text::_([0 => 'JSITE', 1 => 'JADMINISTRATOR', 3 => 'JAPI'][$item->client_id] ?? 'JSITE'); - $manifest = json_decode($item->manifest_cache); - $item->current_version = $manifest->version ?? Text::_('JLIB_UNKNOWN'); - $item->description = $item->description !== '' ? $item->description : Text::_('COM_INSTALLER_MSG_UPDATE_NODESC'); - $item->type_translated = Text::_('COM_INSTALLER_TYPE_' . strtoupper($item->type)); - $item->folder_translated = $item->folder ?: Text::_('COM_INSTALLER_TYPE_NONAPPLICABLE'); - $item->install_type = $item->extension_id ? Text::_('COM_INSTALLER_MSG_UPDATE_UPDATE') : Text::_('COM_INSTALLER_NEW_INSTALL'); - } - - return $items; - } - - /** - * Returns an object list - * - * @param DatabaseQuery $query The query - * @param int $limitstart Offset - * @param int $limit The number of records - * - * @return array - * - * @since 3.5 - */ - protected function _getList($query, $limitstart = 0, $limit = 0) - { - $db = $this->getDatabase(); - $listOrder = $this->getState('list.ordering', 'u.name'); - $listDirn = $this->getState('list.direction', 'asc'); - - // Process ordering. - if (in_array($listOrder, array('client_translated', 'folder_translated', 'type_translated'))) - { - $db->setQuery($query); - $result = $db->loadObjectList(); - $this->translate($result); - $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); - $total = count($result); - - if ($total < $limitstart) - { - $limitstart = 0; - $this->setState('list.start', 0); - } - - return array_slice($result, $limitstart, $limit ?: null); - } - else - { - $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); - - $result = parent::_getList($query, $limitstart, $limit); - $this->translate($result); - - return $result; - } - } - - /** - * Get the count of disabled update sites - * - * @return integer - * - * @since 3.4 - */ - public function getDisabledUpdateSites() - { - $db = $this->getDatabase(); - - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__update_sites')) - ->where($db->quoteName('enabled') . ' = 0'); - - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Finds updates for an extension. - * - * @param int $eid Extension identifier to look for - * @param int $cacheTimeout Cache timeout - * @param int $minimumStability Minimum stability for updates {@see Updater} (0=dev, 1=alpha, 2=beta, 3=rc, 4=stable) - * - * @return boolean Result - * - * @since 1.6 - */ - public function findUpdates($eid = 0, $cacheTimeout = 0, $minimumStability = Updater::STABILITY_STABLE) - { - Updater::getInstance()->findUpdates($eid, $cacheTimeout, $minimumStability); - - return true; - } - - /** - * Removes all of the updates from the table. - * - * @return boolean result of operation - * - * @since 1.6 - */ - public function purge() - { - $db = $this->getDatabase(); - - try - { - $db->truncateTable('#__updates'); - } - catch (ExecutionFailureException $e) - { - $this->_message = Text::_('JLIB_INSTALLER_FAILED_TO_PURGE_UPDATES'); - - return false; - } - - // Reset the last update check timestamp - $query = $db->getQuery(true) - ->update($db->quoteName('#__update_sites')) - ->set($db->quoteName('last_check_timestamp') . ' = ' . $db->quote(0)); - $db->setQuery($query); - $db->execute(); - - // Clear the administrator cache - $this->cleanCache('_system'); - - $this->_message = Text::_('JLIB_INSTALLER_PURGED_UPDATES'); - - return true; - } - - /** - * Update function. - * - * Sets the "result" state with the result of the operation. - * - * @param int[] $uids List of updates to apply - * @param int $minimumStability The minimum allowed stability for installed updates {@see Updater} - * - * @return void - * - * @since 1.6 - */ - public function update($uids, $minimumStability = Updater::STABILITY_STABLE) - { - $result = true; - - foreach ($uids as $uid) - { - $update = new Update; - $instance = new \Joomla\CMS\Table\Update($this->getDatabase()); - - if (!$instance->load($uid)) - { - // Update no longer available, maybe already updated by a package. - continue; - } - - $update->loadFromXml($instance->detailsurl, $minimumStability); - - // Find and use extra_query from update_site if available - $updateSiteInstance = new \Joomla\CMS\Table\UpdateSite($this->getDatabase()); - $updateSiteInstance->load($instance->update_site_id); - - if ($updateSiteInstance->extra_query) - { - $update->set('extra_query', $updateSiteInstance->extra_query); - } - - $this->preparePreUpdate($update, $instance); - - // Install sets state and enqueues messages - $res = $this->install($update); - - if ($res) - { - $instance->delete($uid); - } - - $result = $res & $result; - } - - // Clear the cached extension data and menu cache - $this->cleanCache('_system'); - $this->cleanCache('com_modules'); - $this->cleanCache('com_plugins'); - $this->cleanCache('mod_menu'); - - // Set the final state - $this->setState('result', $result); - } - - /** - * Handles the actual update installation. - * - * @param Update $update An update definition - * - * @return boolean Result of install - * - * @since 1.6 - */ - private function install($update) - { - // Load overrides plugin. - PluginHelper::importPlugin('installer'); - - $app = Factory::getApplication(); - - if (!isset($update->get('downloadurl')->_data)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_INVALID_EXTENSION_UPDATE'), 'error'); - - return false; - } - - $url = trim($update->downloadurl->_data); - $sources = $update->get('downloadSources', array()); - - if ($extra_query = $update->get('extra_query')) - { - $url .= (strpos($url, '?') === false) ? '?' : '&'; - $url .= $extra_query; - } - - $mirror = 0; - - while (!($p_file = InstallerHelper::downloadPackage($url)) && isset($sources[$mirror])) - { - $name = $sources[$mirror]; - $url = trim($name->url); - - if ($extra_query) - { - $url .= (strpos($url, '?') === false) ? '?' : '&'; - $url .= $extra_query; - } - - $mirror++; - } - - // Was the package downloaded? - if (!$p_file) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED', $url), 'error'); - - return false; - } - - $config = $app->getConfig(); - $tmp_dest = $config->get('tmp_path'); - - // Unpack the downloaded package file - $package = InstallerHelper::unpack($tmp_dest . '/' . $p_file); - - if (empty($package)) - { - $app->enqueueMessage(Text::sprintf('COM_INSTALLER_UNPACK_ERROR', $p_file), 'error'); - - return false; - } - - // Get an installer instance - $installer = Installer::getInstance(); - $update->set('type', $package['type']); - - // Check the package - $check = InstallerHelper::isChecksumValid($package['packagefile'], $update); - - if ($check === InstallerHelper::HASH_NOT_VALIDATED) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WRONG'), 'error'); - - return false; - } - - if ($check === InstallerHelper::HASH_NOT_PROVIDED) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WARNING'), 'warning'); - } - - // Install the package - if (!$installer->update($package['dir'])) - { - // There was an error updating the package - $app->enqueueMessage( - Text::sprintf('COM_INSTALLER_MSG_UPDATE_ERROR', - Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])) - ), 'error' - ); - $result = false; - } - else - { - // Package updated successfully - $app->enqueueMessage( - Text::sprintf('COM_INSTALLER_MSG_UPDATE_SUCCESS', - Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])) - ), 'success' - ); - $result = true; - } - - // Quick change - $this->type = $package['type']; - - // @todo: Reconfigure this code when you have more battery life left - $this->setState('name', $installer->get('name')); - $this->setState('result', $result); - $app->setUserState('com_installer.message', $installer->message); - $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); - - // Cleanup the install files - if (!is_file($package['packagefile'])) - { - $package['packagefile'] = $config->get('tmp_path') . '/' . $package['packagefile']; - } - - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - - return $result; - } - - /** - * Method to get the row form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 2.5.2 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - Form::addFormPath(JPATH_COMPONENT . '/models/forms'); - Form::addFieldPath(JPATH_COMPONENT . '/models/fields'); - $form = Form::getInstance('com_installer.update', 'update', array('load_data' => $loadData)); - - // Check for an error. - if ($form == false) - { - $this->setError($form->getMessage()); - - return false; - } - - // Check the session for previously entered form data. - $data = $this->loadFormData(); - - // Bind the form data if present. - if (!empty($data)) - { - $form->bind($data); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 2.5.2 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState($this->context, array()); - - return $data; - } - - /** - * Method to add parameters to the update - * - * @param Update $update An update definition - * @param \Joomla\CMS\Table\Update $table The update instance from the database - * - * @return void - * - * @since 3.7.0 - */ - protected function preparePreUpdate($update, $table) - { - switch ($table->type) - { - // Components could have a helper which adds additional data - case 'component': - $ename = str_replace('com_', '', $table->element); - $fname = $ename . '.php'; - $cname = ucfirst($ename) . 'Helper'; - - $path = JPATH_ADMINISTRATOR . '/components/' . $table->element . '/helpers/' . $fname; - - if (File::exists($path)) - { - require_once $path; - - if (class_exists($cname) && is_callable(array($cname, 'prepareUpdate'))) - { - call_user_func_array(array($cname, 'prepareUpdate'), array(&$update, &$table)); - } - } - - break; - - // Modules could have a helper which adds additional data - case 'module': - $cname = str_replace('_', '', $table->element) . 'Helper'; - $path = ($table->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE) . '/modules/' . $table->element . '/helper.php'; - - if (File::exists($path)) - { - require_once $path; - - if (class_exists($cname) && is_callable(array($cname, 'prepareUpdate'))) - { - call_user_func_array(array($cname, 'prepareUpdate'), array(&$update, &$table)); - } - } - - break; - - // If we have a plugin, we can use the plugin trigger "onInstallerBeforePackageDownload" - // But we should make sure, that our plugin is loaded, so we don't need a second "installer" plugin - case 'plugin': - $cname = str_replace('plg_', '', $table->element); - PluginHelper::importPlugin($table->folder, $cname); - break; - } - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - $query->where($this->getDatabase()->quoteName('extension_id') . ' != 0'); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\ListModel + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'name', 'u.name', + 'client_id', 'u.client_id', 'client_translated', + 'type', 'u.type', 'type_translated', + 'folder', 'u.folder', 'folder_translated', + 'extension_id', 'u.extension_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'u.name', $direction = 'asc') + { + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); + $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); + $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); + + $app = Factory::getApplication(); + $this->setState('message', $app->getUserState('com_installer.message')); + $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + + parent::populateState($ordering, $direction); + } + + /** + * Method to get the database query + * + * @return \Joomla\Database\DatabaseQuery The database query + * + * @since 1.6 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + + // Grab updates ignoring new installs + $query = $db->getQuery(true) + ->select('u.*') + ->select($db->quoteName('e.manifest_cache')) + ->from($db->quoteName('#__updates', 'u')) + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('u.extension_id') + ) + ->where($db->quoteName('u.extension_id') . ' != 0'); + + // Process select filters. + $clientId = $this->getState('filter.client_id'); + $type = $this->getState('filter.type'); + $folder = $this->getState('filter.folder'); + $extensionId = $this->getState('filter.extension_id'); + + if ($type) { + $query->where($db->quoteName('u.type') . ' = :type') + ->bind(':type', $type); + } + + if ($clientId != '') { + $clientId = (int) $clientId; + $query->where($db->quoteName('u.client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + } + + if ($folder != '' && in_array($type, array('plugin', 'library', ''))) { + $folder = $folder === '*' ? '' : $folder; + $query->where($db->quoteName('u.folder') . ' = :folder') + ->bind(':folder', $folder); + } + + if ($extensionId) { + $extensionId = (int) $extensionId; + $query->where($db->quoteName('u.extension_id') . ' = :extensionid') + ->bind(':extensionid', $extensionId, ParameterType::INTEGER); + } else { + $eid = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; + $query->where($db->quoteName('u.extension_id') . ' != 0') + ->where($db->quoteName('u.extension_id') . ' != :eid') + ->bind(':eid', $eid, ParameterType::INTEGER); + } + + // Process search filter. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'eid:') !== false) { + $sid = (int) substr($search, 4); + $query->where($db->quoteName('u.extension_id') . ' = :sid') + ->bind(':sid', $sid, ParameterType::INTEGER); + } else { + if (stripos($search, 'uid:') !== false) { + $suid = (int) substr($search, 4); + $query->where($db->quoteName('u.update_site_id') . ' = :suid') + ->bind(':suid', $suid, ParameterType::INTEGER); + } elseif (stripos($search, 'id:') !== false) { + $uid = (int) substr($search, 3); + $query->where($db->quoteName('u.update_id') . ' = :uid') + ->bind(':uid', $uid, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where($db->quoteName('u.name') . ' LIKE :search') + ->bind(':search', $search); + } + } + } + + return $query; + } + + /** + * Translate a list of objects + * + * @param array $items The array of objects + * + * @return array The array of translated objects + * + * @since 3.5 + */ + protected function translate(&$items) + { + foreach ($items as &$item) { + $item->client_translated = Text::_([0 => 'JSITE', 1 => 'JADMINISTRATOR', 3 => 'JAPI'][$item->client_id] ?? 'JSITE'); + $manifest = json_decode($item->manifest_cache); + $item->current_version = $manifest->version ?? Text::_('JLIB_UNKNOWN'); + $item->description = $item->description !== '' ? $item->description : Text::_('COM_INSTALLER_MSG_UPDATE_NODESC'); + $item->type_translated = Text::_('COM_INSTALLER_TYPE_' . strtoupper($item->type)); + $item->folder_translated = $item->folder ?: Text::_('COM_INSTALLER_TYPE_NONAPPLICABLE'); + $item->install_type = $item->extension_id ? Text::_('COM_INSTALLER_MSG_UPDATE_UPDATE') : Text::_('COM_INSTALLER_NEW_INSTALL'); + } + + return $items; + } + + /** + * Returns an object list + * + * @param DatabaseQuery $query The query + * @param int $limitstart Offset + * @param int $limit The number of records + * + * @return array + * + * @since 3.5 + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $db = $this->getDatabase(); + $listOrder = $this->getState('list.ordering', 'u.name'); + $listDirn = $this->getState('list.direction', 'asc'); + + // Process ordering. + if (in_array($listOrder, array('client_translated', 'folder_translated', 'type_translated'))) { + $db->setQuery($query); + $result = $db->loadObjectList(); + $this->translate($result); + $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); + $total = count($result); + + if ($total < $limitstart) { + $limitstart = 0; + $this->setState('list.start', 0); + } + + return array_slice($result, $limitstart, $limit ?: null); + } else { + $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); + + $result = parent::_getList($query, $limitstart, $limit); + $this->translate($result); + + return $result; + } + } + + /** + * Get the count of disabled update sites + * + * @return integer + * + * @since 3.4 + */ + public function getDisabledUpdateSites() + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__update_sites')) + ->where($db->quoteName('enabled') . ' = 0'); + + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Finds updates for an extension. + * + * @param int $eid Extension identifier to look for + * @param int $cacheTimeout Cache timeout + * @param int $minimumStability Minimum stability for updates {@see Updater} (0=dev, 1=alpha, 2=beta, 3=rc, 4=stable) + * + * @return boolean Result + * + * @since 1.6 + */ + public function findUpdates($eid = 0, $cacheTimeout = 0, $minimumStability = Updater::STABILITY_STABLE) + { + Updater::getInstance()->findUpdates($eid, $cacheTimeout, $minimumStability); + + return true; + } + + /** + * Removes all of the updates from the table. + * + * @return boolean result of operation + * + * @since 1.6 + */ + public function purge() + { + $db = $this->getDatabase(); + + try { + $db->truncateTable('#__updates'); + } catch (ExecutionFailureException $e) { + $this->_message = Text::_('JLIB_INSTALLER_FAILED_TO_PURGE_UPDATES'); + + return false; + } + + // Reset the last update check timestamp + $query = $db->getQuery(true) + ->update($db->quoteName('#__update_sites')) + ->set($db->quoteName('last_check_timestamp') . ' = ' . $db->quote(0)); + $db->setQuery($query); + $db->execute(); + + // Clear the administrator cache + $this->cleanCache('_system'); + + $this->_message = Text::_('JLIB_INSTALLER_PURGED_UPDATES'); + + return true; + } + + /** + * Update function. + * + * Sets the "result" state with the result of the operation. + * + * @param int[] $uids List of updates to apply + * @param int $minimumStability The minimum allowed stability for installed updates {@see Updater} + * + * @return void + * + * @since 1.6 + */ + public function update($uids, $minimumStability = Updater::STABILITY_STABLE) + { + $result = true; + + foreach ($uids as $uid) { + $update = new Update(); + $instance = new \Joomla\CMS\Table\Update($this->getDatabase()); + + if (!$instance->load($uid)) { + // Update no longer available, maybe already updated by a package. + continue; + } + + $update->loadFromXml($instance->detailsurl, $minimumStability); + + // Find and use extra_query from update_site if available + $updateSiteInstance = new \Joomla\CMS\Table\UpdateSite($this->getDatabase()); + $updateSiteInstance->load($instance->update_site_id); + + if ($updateSiteInstance->extra_query) { + $update->set('extra_query', $updateSiteInstance->extra_query); + } + + $this->preparePreUpdate($update, $instance); + + // Install sets state and enqueues messages + $res = $this->install($update); + + if ($res) { + $instance->delete($uid); + } + + $result = $res & $result; + } + + // Clear the cached extension data and menu cache + $this->cleanCache('_system'); + $this->cleanCache('com_modules'); + $this->cleanCache('com_plugins'); + $this->cleanCache('mod_menu'); + + // Set the final state + $this->setState('result', $result); + } + + /** + * Handles the actual update installation. + * + * @param Update $update An update definition + * + * @return boolean Result of install + * + * @since 1.6 + */ + private function install($update) + { + // Load overrides plugin. + PluginHelper::importPlugin('installer'); + + $app = Factory::getApplication(); + + if (!isset($update->get('downloadurl')->_data)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_INVALID_EXTENSION_UPDATE'), 'error'); + + return false; + } + + $url = trim($update->downloadurl->_data); + $sources = $update->get('downloadSources', array()); + + if ($extra_query = $update->get('extra_query')) { + $url .= (strpos($url, '?') === false) ? '?' : '&'; + $url .= $extra_query; + } + + $mirror = 0; + + while (!($p_file = InstallerHelper::downloadPackage($url)) && isset($sources[$mirror])) { + $name = $sources[$mirror]; + $url = trim($name->url); + + if ($extra_query) { + $url .= (strpos($url, '?') === false) ? '?' : '&'; + $url .= $extra_query; + } + + $mirror++; + } + + // Was the package downloaded? + if (!$p_file) { + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED', $url), 'error'); + + return false; + } + + $config = $app->getConfig(); + $tmp_dest = $config->get('tmp_path'); + + // Unpack the downloaded package file + $package = InstallerHelper::unpack($tmp_dest . '/' . $p_file); + + if (empty($package)) { + $app->enqueueMessage(Text::sprintf('COM_INSTALLER_UNPACK_ERROR', $p_file), 'error'); + + return false; + } + + // Get an installer instance + $installer = Installer::getInstance(); + $update->set('type', $package['type']); + + // Check the package + $check = InstallerHelper::isChecksumValid($package['packagefile'], $update); + + if ($check === InstallerHelper::HASH_NOT_VALIDATED) { + $app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WRONG'), 'error'); + + return false; + } + + if ($check === InstallerHelper::HASH_NOT_PROVIDED) { + $app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WARNING'), 'warning'); + } + + // Install the package + if (!$installer->update($package['dir'])) { + // There was an error updating the package + $app->enqueueMessage( + Text::sprintf( + 'COM_INSTALLER_MSG_UPDATE_ERROR', + Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])) + ), + 'error' + ); + $result = false; + } else { + // Package updated successfully + $app->enqueueMessage( + Text::sprintf( + 'COM_INSTALLER_MSG_UPDATE_SUCCESS', + Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])) + ), + 'success' + ); + $result = true; + } + + // Quick change + $this->type = $package['type']; + + // @todo: Reconfigure this code when you have more battery life left + $this->setState('name', $installer->get('name')); + $this->setState('result', $result); + $app->setUserState('com_installer.message', $installer->message); + $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); + + // Cleanup the install files + if (!is_file($package['packagefile'])) { + $package['packagefile'] = $config->get('tmp_path') . '/' . $package['packagefile']; + } + + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + + return $result; + } + + /** + * Method to get the row form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 2.5.2 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + Form::addFormPath(JPATH_COMPONENT . '/models/forms'); + Form::addFieldPath(JPATH_COMPONENT . '/models/fields'); + $form = Form::getInstance('com_installer.update', 'update', array('load_data' => $loadData)); + + // Check for an error. + if ($form == false) { + $this->setError($form->getMessage()); + + return false; + } + + // Check the session for previously entered form data. + $data = $this->loadFormData(); + + // Bind the form data if present. + if (!empty($data)) { + $form->bind($data); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 2.5.2 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState($this->context, array()); + + return $data; + } + + /** + * Method to add parameters to the update + * + * @param Update $update An update definition + * @param \Joomla\CMS\Table\Update $table The update instance from the database + * + * @return void + * + * @since 3.7.0 + */ + protected function preparePreUpdate($update, $table) + { + switch ($table->type) { + // Components could have a helper which adds additional data + case 'component': + $ename = str_replace('com_', '', $table->element); + $fname = $ename . '.php'; + $cname = ucfirst($ename) . 'Helper'; + + $path = JPATH_ADMINISTRATOR . '/components/' . $table->element . '/helpers/' . $fname; + + if (File::exists($path)) { + require_once $path; + + if (class_exists($cname) && is_callable(array($cname, 'prepareUpdate'))) { + call_user_func_array(array($cname, 'prepareUpdate'), array(&$update, &$table)); + } + } + + break; + + // Modules could have a helper which adds additional data + case 'module': + $cname = str_replace('_', '', $table->element) . 'Helper'; + $path = ($table->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE) . '/modules/' . $table->element . '/helper.php'; + + if (File::exists($path)) { + require_once $path; + + if (class_exists($cname) && is_callable(array($cname, 'prepareUpdate'))) { + call_user_func_array(array($cname, 'prepareUpdate'), array(&$update, &$table)); + } + } + + break; + + // If we have a plugin, we can use the plugin trigger "onInstallerBeforePackageDownload" + // But we should make sure, that our plugin is loaded, so we don't need a second "installer" plugin + case 'plugin': + $cname = str_replace('plg_', '', $table->element); + PluginHelper::importPlugin($table->folder, $cname); + break; + } + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + $query->where($this->getDatabase()->quoteName('extension_id') . ' != 0'); + + return $query; + } } diff --git a/administrator/components/com_installer/src/Model/UpdatesiteModel.php b/administrator/components/com_installer/src/Model/UpdatesiteModel.php index e9def0c2f6c68..ad31ff50dc922 100644 --- a/administrator/components/com_installer/src/Model/UpdatesiteModel.php +++ b/administrator/components/com_installer/src/Model/UpdatesiteModel.php @@ -1,4 +1,5 @@ loadForm('com_installer.updatesite', 'updatesite', ['control' => 'jform', 'load_data' => $loadData]); - - if (empty($form)) - { - return false; - } - - return $form; - } - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - $data = $this->getItem(); - $this->preprocessData('com_installer.updatesite', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return CMSObject|boolean Object on success, false on failure. - * - * @since 4.0.0 - */ - public function getItem($pk = null) - { - $item = parent::getItem($pk); - - $db = $this->getDatabase(); - $updateSiteId = (int) $item->get('update_site_id'); - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 'update_sites.extra_query', - 'extensions.type', - 'extensions.element', - 'extensions.folder', - 'extensions.client_id', - 'extensions.checked_out' - ] - ) - ) - ->from($db->quoteName('#__update_sites', 'update_sites')) - ->join( - 'INNER', - $db->quoteName('#__update_sites_extensions', 'update_sites_extensions'), - $db->quoteName('update_sites_extensions.update_site_id') . ' = ' . $db->quoteName('update_sites.update_site_id') - ) - ->join( - 'INNER', - $db->quoteName('#__extensions', 'extensions'), - $db->quoteName('extensions.extension_id') . ' = ' . $db->quoteName('update_sites_extensions.extension_id') - ) - ->where($db->quoteName('update_sites.update_site_id') . ' = :updatesiteid') - ->bind(':updatesiteid', $updateSiteId, ParameterType::INTEGER); - - $db->setQuery($query); - $extension = new CMSObject($db->loadAssoc()); - - $downloadKey = InstallerHelper::getDownloadKey($extension); - - $item->set('extra_query', $downloadKey['value'] ?? ''); - $item->set('downloadIdPrefix', $downloadKey['prefix'] ?? ''); - $item->set('downloadIdSuffix', $downloadKey['suffix'] ?? ''); - - return $item; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success, False on error. - * - * @since 4.0.0 - */ - public function save($data): bool - { - // Apply the extra_query. Always empty when saving a free extension's update site. - if (isset($data['extra_query'])) - { - $data['extra_query'] = $data['downloadIdPrefix'] . $data['extra_query'] . $data['downloadIdSuffix']; - } - - // Force Joomla to recheck for updates - $data['last_check_timestamp'] = 0; - - $result = parent::save($data); - - if (!$result) - { - return $result; - } - - // Delete update records forcing Joomla to fetch them again, applying the new extra_query. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__updates')) - ->where($db->quoteName('update_site_id') . ' = :updateSiteId'); - $query->bind(':updateSiteId', $data['update_site_id'], ParameterType::INTEGER); - - try - { - $db->setQuery($query)->execute(); - } - catch (Exception $e) - { - // No problem if this fails for any reason. - } - - return true; - } + /** + * The type alias for this content type. + * + * @var string + * @since 4.0.0 + */ + public $typeAlias = 'com_installer.updatesite'; + + /** + * Method to get the row form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @throws Exception + * + * @since 4.0.0 + */ + public function getForm($data = [], $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_installer.updatesite', 'updatesite', ['control' => 'jform', 'load_data' => $loadData]); + + if (empty($form)) { + return false; + } + + return $form; + } + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + $data = $this->getItem(); + $this->preprocessData('com_installer.updatesite', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return CMSObject|boolean Object on success, false on failure. + * + * @since 4.0.0 + */ + public function getItem($pk = null) + { + $item = parent::getItem($pk); + + $db = $this->getDatabase(); + $updateSiteId = (int) $item->get('update_site_id'); + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 'update_sites.extra_query', + 'extensions.type', + 'extensions.element', + 'extensions.folder', + 'extensions.client_id', + 'extensions.checked_out' + ] + ) + ) + ->from($db->quoteName('#__update_sites', 'update_sites')) + ->join( + 'INNER', + $db->quoteName('#__update_sites_extensions', 'update_sites_extensions'), + $db->quoteName('update_sites_extensions.update_site_id') . ' = ' . $db->quoteName('update_sites.update_site_id') + ) + ->join( + 'INNER', + $db->quoteName('#__extensions', 'extensions'), + $db->quoteName('extensions.extension_id') . ' = ' . $db->quoteName('update_sites_extensions.extension_id') + ) + ->where($db->quoteName('update_sites.update_site_id') . ' = :updatesiteid') + ->bind(':updatesiteid', $updateSiteId, ParameterType::INTEGER); + + $db->setQuery($query); + $extension = new CMSObject($db->loadAssoc()); + + $downloadKey = InstallerHelper::getDownloadKey($extension); + + $item->set('extra_query', $downloadKey['value'] ?? ''); + $item->set('downloadIdPrefix', $downloadKey['prefix'] ?? ''); + $item->set('downloadIdSuffix', $downloadKey['suffix'] ?? ''); + + return $item; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success, False on error. + * + * @since 4.0.0 + */ + public function save($data): bool + { + // Apply the extra_query. Always empty when saving a free extension's update site. + if (isset($data['extra_query'])) { + $data['extra_query'] = $data['downloadIdPrefix'] . $data['extra_query'] . $data['downloadIdSuffix']; + } + + // Force Joomla to recheck for updates + $data['last_check_timestamp'] = 0; + + $result = parent::save($data); + + if (!$result) { + return $result; + } + + // Delete update records forcing Joomla to fetch them again, applying the new extra_query. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__updates')) + ->where($db->quoteName('update_site_id') . ' = :updateSiteId'); + $query->bind(':updateSiteId', $data['update_site_id'], ParameterType::INTEGER); + + try { + $db->setQuery($query)->execute(); + } catch (Exception $e) { + // No problem if this fails for any reason. + } + + return true; + } } diff --git a/administrator/components/com_installer/src/Model/UpdatesitesModel.php b/administrator/components/com_installer/src/Model/UpdatesitesModel.php index 4be1b8c62079d..a33d9542e815e 100644 --- a/administrator/components/com_installer/src/Model/UpdatesitesModel.php +++ b/administrator/components/com_installer/src/Model/UpdatesitesModel.php @@ -1,4 +1,5 @@ authorise('core.edit.state', 'com_installer')) - { - throw new Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 403); - } - - $result = true; - - // Ensure eid is an array of extension ids - if (!is_array($eid)) - { - $eid = [$eid]; - } - - // Get a table object for the extension type - $table = new UpdateSiteTable($this->getDatabase()); - - // Enable the update site in the table and store it in the database - foreach ($eid as $i => $id) - { - $table->load($id); - $table->enabled = $value; - - if (!$table->store()) - { - $this->setError($table->getError()); - $result = false; - } - } - - return $result; - } - - /** - * Deletes an update site. - * - * @param array $ids Extension ids to delete. - * - * @return void - * - * @throws Exception on ACL error - * @since 3.6 - * - */ - public function delete($ids = []) - { - if (!Factory::getUser()->authorise('core.delete', 'com_installer')) - { - throw new Exception(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403); - } - - // Ensure eid is an array of extension ids - if (!is_array($ids)) - { - $ids = [$ids]; - } - - $db = $this->getDatabase(); - $app = Factory::getApplication(); - - $count = 0; - - // Gets the update site names. - $query = $db->getQuery(true) - ->select($db->quoteName(['update_site_id', 'name'])) - ->from($db->quoteName('#__update_sites')) - ->whereIn($db->quoteName('update_site_id'), $ids); - $db->setQuery($query); - $updateSitesNames = $db->loadObjectList('update_site_id'); - - // Gets Joomla core update sites Ids. - $joomlaUpdateSitesIds = $this->getJoomlaUpdateSitesIds(0); - - // Enable the update site in the table and store it in the database - foreach ($ids as $i => $id) - { - // Don't allow to delete Joomla Core update sites. - if (in_array((int) $id, $joomlaUpdateSitesIds)) - { - $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATESITES_DELETE_CANNOT_DELETE', $updateSitesNames[$id]->name), 'error'); - continue; - } - - // Delete the update site from all tables. - try - { - $id = (int) $id; - $query = $db->getQuery(true) - ->delete($db->quoteName('#__update_sites')) - ->where($db->quoteName('update_site_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__update_sites_extensions')) - ->where($db->quoteName('update_site_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__updates')) - ->where($db->quoteName('update_site_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - $count++; - } - catch (RuntimeException $e) - { - $app->enqueueMessage( - Text::sprintf( - 'COM_INSTALLER_MSG_UPDATESITES_DELETE_ERROR', - $updateSitesNames[$id]->name, $e->getMessage() - ), 'error' - ); - } - } - - if ($count > 0) - { - $app->enqueueMessage(Text::plural('COM_INSTALLER_MSG_UPDATESITES_N_DELETE_UPDATESITES_DELETED', $count), 'message'); - } - } - - /** - * Fetch the Joomla update sites ids. - * - * @param integer $column Column to return. 0 for update site ids, 1 for extension ids. - * - * @return array Array with joomla core update site ids. - * - * @since 3.6.0 - */ - protected function getJoomlaUpdateSitesIds($column = 0) - { - $db = $this->getDatabase(); - - // Fetch the Joomla core update sites ids and their extension ids. We search for all except the core joomla extension with update sites. - $query = $db->getQuery(true) - ->select($db->quoteName(['use.update_site_id', 'e.extension_id'])) - ->from($db->quoteName('#__update_sites_extensions', 'use')) - ->join( - 'LEFT', - $db->quoteName('#__update_sites', 'us'), - $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('use.update_site_id') - ) - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id') - ) - ->where('(' - . '(' . $db->quoteName('e.type') . ' = ' . $db->quote('file') . - ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quote('joomla') . ')' - . ' OR (' . $db->quoteName('e.type') . ' = ' . $db->quote('package') . ' AND ' . $db->quoteName('e.element') - . ' = ' . $db->quote('pkg_en-GB') . ') OR (' . $db->quoteName('e.type') . ' = ' . $db->quote('component') - . ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quote('com_joomlaupdate') . ')' - . ')' - ); - - $db->setQuery($query); - - return $db->loadColumn($column); - } - - /** - * Rebuild update sites tables. - * - * @return void - * - * @throws Exception on ACL error - * @since 3.6 - * - */ - public function rebuild(): void - { - if (!Factory::getUser()->authorise('core.admin', 'com_installer')) - { - throw new Exception(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_NOT_PERMITTED'), 403); - } - - $db = $this->getDatabase(); - $app = Factory::getApplication(); - - // Check if Joomla Extension plugin is enabled. - if (!PluginHelper::isEnabled('extension', 'joomla')) - { - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('extension')); - $db->setQuery($query); - - $pluginId = (int) $db->loadResult(); - - $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $pluginId); - $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATESITES_REBUILD_EXTENSION_PLUGIN_NOT_ENABLED', $link), 'error'); - - return; - } - - $clients = [JPATH_SITE, JPATH_ADMINISTRATOR, JPATH_API]; - $extensionGroupFolders = ['components', 'modules', 'plugins', 'templates', 'language', 'manifests']; - - $pathsToSearch = []; - - // Identifies which folders to search for manifest files. - foreach ($clients as $clientPath) - { - foreach ($extensionGroupFolders as $extensionGroupFolderName) - { - // Components, modules, plugins, templates, languages and manifest (files, libraries, etc) - if ($extensionGroupFolderName !== 'plugins') - { - foreach (glob($clientPath . '/' . $extensionGroupFolderName . '/*', GLOB_NOSORT | GLOB_ONLYDIR) as $extensionFolderPath) - { - $pathsToSearch[] = $extensionFolderPath; - } - } - else - { - // Plugins (another directory level is needed) - foreach (glob($clientPath . '/' . $extensionGroupFolderName . '/*', - GLOB_NOSORT | GLOB_ONLYDIR - ) as $pluginGroupFolderPath) - { - foreach (glob($pluginGroupFolderPath . '/*', GLOB_NOSORT | GLOB_ONLYDIR) as $extensionFolderPath) - { - $pathsToSearch[] = $extensionFolderPath; - } - } - } - } - } - - // Gets Joomla core update sites Ids. - $joomlaUpdateSitesIds = $this->getJoomlaUpdateSitesIds(0); - - // First backup any custom extra_query for the sites - $query = $db->getQuery(true) - ->select('TRIM(' . $db->quoteName('location') . ') AS ' . $db->quoteName('location') . ', ' . $db->quoteName('extra_query')) - ->from($db->quoteName('#__update_sites')); - $db->setQuery($query); - $backupExtraQuerys = $db->loadAssocList('location'); - - // Delete from all tables (except joomla core update sites). - $query = $db->getQuery(true) - ->delete($db->quoteName('#__update_sites')) - ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); - $db->setQuery($query); - $db->execute(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__update_sites_extensions')) - ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); - $db->setQuery($query); - $db->execute(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__updates')) - ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); - $db->setQuery($query); - $db->execute(); - - $count = 0; - - // Gets Joomla core extension Ids. - $joomlaCoreExtensionIds = $this->getJoomlaUpdateSitesIds(1); - - // Search for updateservers in manifest files inside the folders to search. - foreach ($pathsToSearch as $extensionFolderPath) - { - $tmpInstaller = new Installer; - $tmpInstaller->setDatabase($this->getDatabase()); - - $tmpInstaller->setPath('source', $extensionFolderPath); - - // Main folder manifests (higher priority) - $parentXmlfiles = Folder::files($tmpInstaller->getPath('source'), '.xml$', false, true); - - // Search for children manifests (lower priority) - $allXmlFiles = Folder::files($tmpInstaller->getPath('source'), '.xml$', 1, true); - - // Create a unique array of files ordered by priority - $xmlfiles = array_unique(array_merge($parentXmlfiles, $allXmlFiles)); - - if (!empty($xmlfiles)) - { - foreach ($xmlfiles as $file) - { - // Is it a valid Joomla installation manifest file? - $manifest = $tmpInstaller->isManifest($file); - - if ($manifest !== null) - { - /** - * Search if the extension exists in the extensions table. Excluding Joomla - * core extensions and discovered but not yet installed extensions. - */ - - $name = (string) $manifest->name; - $pkgName = (string) $manifest->packagename; - $type = (string) $manifest['type']; - - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where( - [ - $db->quoteName('type') . ' = :type', - $db->quoteName('state') . ' != -1', - ] - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('name') . ' = :name', - $db->quoteName('name') . ' = :pkgname', - ], - 'OR' - ) - ->whereNotIn($db->quoteName('extension_id'), $joomlaCoreExtensionIds) - ->bind(':name', $name) - ->bind(':pkgname', $pkgName) - ->bind(':type', $type); - $db->setQuery($query); - - $eid = (int) $db->loadResult(); - - if ($eid && $manifest->updateservers) - { - // Set the manifest object and path - $tmpInstaller->manifest = $manifest; - $tmpInstaller->setPath('manifest', $file); - - // Remove last extra_query as we are in a foreach - $tmpInstaller->extraQuery = ''; - - if ($tmpInstaller->manifest->updateservers - && $tmpInstaller->manifest->updateservers->server - && isset($backupExtraQuerys[trim((string) $tmpInstaller->manifest->updateservers->server)])) - { - $tmpInstaller->extraQuery = $backupExtraQuerys[trim((string) $tmpInstaller->manifest->updateservers->server)]['extra_query']; - } - - // Load the extension plugin (if not loaded yet). - PluginHelper::importPlugin('extension', 'joomla'); - - // Fire the onExtensionAfterUpdate - $app->triggerEvent('onExtensionAfterUpdate', ['installer' => $tmpInstaller, 'eid' => $eid]); - - $count++; - } - } - } - } - } - - if ($count > 0) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_SUCCESS'), 'message'); - } - else - { - $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_MESSAGE'), 'message'); - } - - // Flush the system cache to ensure extra_query is correctly loaded next time. - $this->cleanCache('_system'); - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 4.0.0 - */ - public function getItems() - { - $items = parent::getItems(); - - array_walk($items, - static function ($item) { - $data = new CMSObject($item); - $item->downloadKey = InstallerHelper::getDownloadKey($data); - } - ); - - return $items; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 3.4 - */ - protected function populateState($ordering = 'name', $direction = 'asc') - { - // Load the filter state. - $stateKeys = [ - 'search' => 'string', - 'client_id' => 'int', - 'enabled' => 'string', - 'type' => 'string', - 'folder' => 'string', - 'supported' => 'int', - ]; - - foreach ($stateKeys as $key => $filterType) - { - $stateKey = 'filter.' . $key; - - switch ($filterType) - { - case 'int': - case 'bool': - $default = null; - break; - - default: - $default = ''; - break; - } - - $stateValue = $this->getUserStateFromRequest( - $this->context . '.' . $stateKey, 'filter_' . $key, $default, $filterType - ); - - $this->setState($stateKey, $stateValue); - } - - parent::populateState($ordering, $direction); - } - - protected function getStoreId($id = '') - { - $id .= ':' . $this->getState('search'); - $id .= ':' . $this->getState('client_id'); - $id .= ':' . $this->getState('enabled'); - $id .= ':' . $this->getState('type'); - $id .= ':' . $this->getState('folder'); - $id .= ':' . $this->getState('supported'); - - return parent::getStoreId($id); - } - - /** - * Method to get the database query - * - * @return \Joomla\Database\DatabaseQuery The database query - * - * @since 3.4 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 's.update_site_id', - 's.name', - 's.type', - 's.location', - 's.enabled', - 's.checked_out', - 's.checked_out_time', - 's.extra_query', - 'e.extension_id', - 'e.name', - 'e.type', - 'e.element', - 'e.folder', - 'e.client_id', - 'e.state', - 'e.manifest_cache', - 'u.name' - ], - [ - 'update_site_id', - 'update_site_name', - 'update_site_type', - 'location', - 'enabled', - 'checked_out', - 'checked_out_time', - 'extra_query', - 'extension_id', - 'name', - 'type', - 'element', - 'folder', - 'client_id', - 'state', - 'manifest_cache', - 'editor' - ] - ) - ) - ->from($db->quoteName('#__update_sites', 's')) - ->join( - 'INNER', - $db->quoteName('#__update_sites_extensions', 'se'), - $db->quoteName('se.update_site_id') . ' = ' . $db->quoteName('s.update_site_id') - ) - ->join( - 'INNER', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('se.extension_id') - ) - ->join( - 'LEFT', - $db->quoteName('#__users', 'u'), - $db->quoteName('s.checked_out') . ' = ' . $db->quoteName('u.id') - ) - ->where($db->quoteName('state') . ' = 0'); - - // Process select filters. - $supported = $this->getState('filter.supported'); - $enabled = $this->getState('filter.enabled'); - $type = $this->getState('filter.type'); - $clientId = $this->getState('filter.client_id'); - $folder = $this->getState('filter.folder'); - - if ($enabled !== '') - { - $enabled = (int) $enabled; - $query->where($db->quoteName('s.enabled') . ' = :enabled') - ->bind(':enabled', $enabled, ParameterType::INTEGER); - } - - if ($type) - { - $query->where($db->quoteName('e.type') . ' = :type') - ->bind(':type', $type); - } - - if ($clientId !== null && $clientId !== '') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('e.client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - if ($folder !== '' && in_array($type, ['plugin', 'library', ''], true)) - { - $folderForBinding = $folder === '*' ? '' : $folder; - $query->where($db->quoteName('e.folder') . ' = :folder') - ->bind(':folder', $folderForBinding); - } - - // Process search filter (update site id). - $search = $this->getState('filter.search'); - - if (!empty($search) && stripos($search, 'id:') === 0) - { - $uid = (int) substr($search, 3); - $query->where($db->quoteName('s.update_site_id') . ' = :siteId') - ->bind(':siteId', $uid, ParameterType::INTEGER); - } - - if (is_numeric($supported)) - { - switch ($supported) - { - // Show Update Sites which support Download Keys - case 1: - $supportedIDs = InstallerHelper::getDownloadKeySupportedSites($enabled); - break; - - // Show Update Sites which are missing Download Keys - case -1: - $supportedIDs = InstallerHelper::getDownloadKeyExistsSites(false, $enabled); - break; - - // Show Update Sites which have valid Download Keys - case 2: - $supportedIDs = InstallerHelper::getDownloadKeyExistsSites(true, $enabled); - break; - } - - if (!empty($supportedIDs)) - { - // Don't remove array_values(). whereIn expect a zero-based array. - $query->whereIn($db->qn('s.update_site_id'), array_values($supportedIDs)); - } - else - { - // In case of an empty list of IDs we apply a fake filter to effectively return no data - $query->where($db->qn('s.update_site_id') . ' = 0'); - } - } - - /** - * Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in - * extension.php). - */ - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @since 1.6 + * @see \Joomla\CMS\MVC\Model\ListModel + */ + public function __construct($config = [], MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = [ + 'update_site_name', + 'name', + 'client_id', + 'client', + 'client_translated', + 'status', + 'type', + 'type_translated', + 'folder', + 'folder_translated', + 'update_site_id', + 'enabled', + 'supported' + ]; + } + + parent::__construct($config, $factory); + } + + /** + * Enable/Disable an extension. + * + * @param array $eid Extension ids to un/publish + * @param int $value Publish value + * + * @return boolean True on success + * + * @throws Exception on ACL error + * @since 3.4 + * + */ + public function publish(&$eid = [], $value = 1) + { + if (!Factory::getUser()->authorise('core.edit.state', 'com_installer')) { + throw new Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 403); + } + + $result = true; + + // Ensure eid is an array of extension ids + if (!is_array($eid)) { + $eid = [$eid]; + } + + // Get a table object for the extension type + $table = new UpdateSiteTable($this->getDatabase()); + + // Enable the update site in the table and store it in the database + foreach ($eid as $i => $id) { + $table->load($id); + $table->enabled = $value; + + if (!$table->store()) { + $this->setError($table->getError()); + $result = false; + } + } + + return $result; + } + + /** + * Deletes an update site. + * + * @param array $ids Extension ids to delete. + * + * @return void + * + * @throws Exception on ACL error + * @since 3.6 + * + */ + public function delete($ids = []) + { + if (!Factory::getUser()->authorise('core.delete', 'com_installer')) { + throw new Exception(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403); + } + + // Ensure eid is an array of extension ids + if (!is_array($ids)) { + $ids = [$ids]; + } + + $db = $this->getDatabase(); + $app = Factory::getApplication(); + + $count = 0; + + // Gets the update site names. + $query = $db->getQuery(true) + ->select($db->quoteName(['update_site_id', 'name'])) + ->from($db->quoteName('#__update_sites')) + ->whereIn($db->quoteName('update_site_id'), $ids); + $db->setQuery($query); + $updateSitesNames = $db->loadObjectList('update_site_id'); + + // Gets Joomla core update sites Ids. + $joomlaUpdateSitesIds = $this->getJoomlaUpdateSitesIds(0); + + // Enable the update site in the table and store it in the database + foreach ($ids as $i => $id) { + // Don't allow to delete Joomla Core update sites. + if (in_array((int) $id, $joomlaUpdateSitesIds)) { + $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATESITES_DELETE_CANNOT_DELETE', $updateSitesNames[$id]->name), 'error'); + continue; + } + + // Delete the update site from all tables. + try { + $id = (int) $id; + $query = $db->getQuery(true) + ->delete($db->quoteName('#__update_sites')) + ->where($db->quoteName('update_site_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__update_sites_extensions')) + ->where($db->quoteName('update_site_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__updates')) + ->where($db->quoteName('update_site_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + $count++; + } catch (RuntimeException $e) { + $app->enqueueMessage( + Text::sprintf( + 'COM_INSTALLER_MSG_UPDATESITES_DELETE_ERROR', + $updateSitesNames[$id]->name, + $e->getMessage() + ), + 'error' + ); + } + } + + if ($count > 0) { + $app->enqueueMessage(Text::plural('COM_INSTALLER_MSG_UPDATESITES_N_DELETE_UPDATESITES_DELETED', $count), 'message'); + } + } + + /** + * Fetch the Joomla update sites ids. + * + * @param integer $column Column to return. 0 for update site ids, 1 for extension ids. + * + * @return array Array with joomla core update site ids. + * + * @since 3.6.0 + */ + protected function getJoomlaUpdateSitesIds($column = 0) + { + $db = $this->getDatabase(); + + // Fetch the Joomla core update sites ids and their extension ids. We search for all except the core joomla extension with update sites. + $query = $db->getQuery(true) + ->select($db->quoteName(['use.update_site_id', 'e.extension_id'])) + ->from($db->quoteName('#__update_sites_extensions', 'use')) + ->join( + 'LEFT', + $db->quoteName('#__update_sites', 'us'), + $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('use.update_site_id') + ) + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id') + ) + ->where('(' + . '(' . $db->quoteName('e.type') . ' = ' . $db->quote('file') . + ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quote('joomla') . ')' + . ' OR (' . $db->quoteName('e.type') . ' = ' . $db->quote('package') . ' AND ' . $db->quoteName('e.element') + . ' = ' . $db->quote('pkg_en-GB') . ') OR (' . $db->quoteName('e.type') . ' = ' . $db->quote('component') + . ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quote('com_joomlaupdate') . ')' + . ')'); + + $db->setQuery($query); + + return $db->loadColumn($column); + } + + /** + * Rebuild update sites tables. + * + * @return void + * + * @throws Exception on ACL error + * @since 3.6 + * + */ + public function rebuild(): void + { + if (!Factory::getUser()->authorise('core.admin', 'com_installer')) { + throw new Exception(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_NOT_PERMITTED'), 403); + } + + $db = $this->getDatabase(); + $app = Factory::getApplication(); + + // Check if Joomla Extension plugin is enabled. + if (!PluginHelper::isEnabled('extension', 'joomla')) { + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('extension')); + $db->setQuery($query); + + $pluginId = (int) $db->loadResult(); + + $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $pluginId); + $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATESITES_REBUILD_EXTENSION_PLUGIN_NOT_ENABLED', $link), 'error'); + + return; + } + + $clients = [JPATH_SITE, JPATH_ADMINISTRATOR, JPATH_API]; + $extensionGroupFolders = ['components', 'modules', 'plugins', 'templates', 'language', 'manifests']; + + $pathsToSearch = []; + + // Identifies which folders to search for manifest files. + foreach ($clients as $clientPath) { + foreach ($extensionGroupFolders as $extensionGroupFolderName) { + // Components, modules, plugins, templates, languages and manifest (files, libraries, etc) + if ($extensionGroupFolderName !== 'plugins') { + foreach (glob($clientPath . '/' . $extensionGroupFolderName . '/*', GLOB_NOSORT | GLOB_ONLYDIR) as $extensionFolderPath) { + $pathsToSearch[] = $extensionFolderPath; + } + } else { + // Plugins (another directory level is needed) + foreach ( + glob( + $clientPath . '/' . $extensionGroupFolderName . '/*', + GLOB_NOSORT | GLOB_ONLYDIR + ) as $pluginGroupFolderPath + ) { + foreach (glob($pluginGroupFolderPath . '/*', GLOB_NOSORT | GLOB_ONLYDIR) as $extensionFolderPath) { + $pathsToSearch[] = $extensionFolderPath; + } + } + } + } + } + + // Gets Joomla core update sites Ids. + $joomlaUpdateSitesIds = $this->getJoomlaUpdateSitesIds(0); + + // First backup any custom extra_query for the sites + $query = $db->getQuery(true) + ->select('TRIM(' . $db->quoteName('location') . ') AS ' . $db->quoteName('location') . ', ' . $db->quoteName('extra_query')) + ->from($db->quoteName('#__update_sites')); + $db->setQuery($query); + $backupExtraQuerys = $db->loadAssocList('location'); + + // Delete from all tables (except joomla core update sites). + $query = $db->getQuery(true) + ->delete($db->quoteName('#__update_sites')) + ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); + $db->setQuery($query); + $db->execute(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__update_sites_extensions')) + ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); + $db->setQuery($query); + $db->execute(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__updates')) + ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); + $db->setQuery($query); + $db->execute(); + + $count = 0; + + // Gets Joomla core extension Ids. + $joomlaCoreExtensionIds = $this->getJoomlaUpdateSitesIds(1); + + // Search for updateservers in manifest files inside the folders to search. + foreach ($pathsToSearch as $extensionFolderPath) { + $tmpInstaller = new Installer(); + $tmpInstaller->setDatabase($this->getDatabase()); + + $tmpInstaller->setPath('source', $extensionFolderPath); + + // Main folder manifests (higher priority) + $parentXmlfiles = Folder::files($tmpInstaller->getPath('source'), '.xml$', false, true); + + // Search for children manifests (lower priority) + $allXmlFiles = Folder::files($tmpInstaller->getPath('source'), '.xml$', 1, true); + + // Create a unique array of files ordered by priority + $xmlfiles = array_unique(array_merge($parentXmlfiles, $allXmlFiles)); + + if (!empty($xmlfiles)) { + foreach ($xmlfiles as $file) { + // Is it a valid Joomla installation manifest file? + $manifest = $tmpInstaller->isManifest($file); + + if ($manifest !== null) { + /** + * Search if the extension exists in the extensions table. Excluding Joomla + * core extensions and discovered but not yet installed extensions. + */ + + $name = (string) $manifest->name; + $pkgName = (string) $manifest->packagename; + $type = (string) $manifest['type']; + + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where( + [ + $db->quoteName('type') . ' = :type', + $db->quoteName('state') . ' != -1', + ] + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('name') . ' = :name', + $db->quoteName('name') . ' = :pkgname', + ], + 'OR' + ) + ->whereNotIn($db->quoteName('extension_id'), $joomlaCoreExtensionIds) + ->bind(':name', $name) + ->bind(':pkgname', $pkgName) + ->bind(':type', $type); + $db->setQuery($query); + + $eid = (int) $db->loadResult(); + + if ($eid && $manifest->updateservers) { + // Set the manifest object and path + $tmpInstaller->manifest = $manifest; + $tmpInstaller->setPath('manifest', $file); + + // Remove last extra_query as we are in a foreach + $tmpInstaller->extraQuery = ''; + + if ( + $tmpInstaller->manifest->updateservers + && $tmpInstaller->manifest->updateservers->server + && isset($backupExtraQuerys[trim((string) $tmpInstaller->manifest->updateservers->server)]) + ) { + $tmpInstaller->extraQuery = $backupExtraQuerys[trim((string) $tmpInstaller->manifest->updateservers->server)]['extra_query']; + } + + // Load the extension plugin (if not loaded yet). + PluginHelper::importPlugin('extension', 'joomla'); + + // Fire the onExtensionAfterUpdate + $app->triggerEvent('onExtensionAfterUpdate', ['installer' => $tmpInstaller, 'eid' => $eid]); + + $count++; + } + } + } + } + } + + if ($count > 0) { + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_SUCCESS'), 'message'); + } else { + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_MESSAGE'), 'message'); + } + + // Flush the system cache to ensure extra_query is correctly loaded next time. + $this->cleanCache('_system'); + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 4.0.0 + */ + public function getItems() + { + $items = parent::getItems(); + + array_walk( + $items, + static function ($item) { + $data = new CMSObject($item); + $item->downloadKey = InstallerHelper::getDownloadKey($data); + } + ); + + return $items; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.4 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + // Load the filter state. + $stateKeys = [ + 'search' => 'string', + 'client_id' => 'int', + 'enabled' => 'string', + 'type' => 'string', + 'folder' => 'string', + 'supported' => 'int', + ]; + + foreach ($stateKeys as $key => $filterType) { + $stateKey = 'filter.' . $key; + + switch ($filterType) { + case 'int': + case 'bool': + $default = null; + break; + + default: + $default = ''; + break; + } + + $stateValue = $this->getUserStateFromRequest( + $this->context . '.' . $stateKey, + 'filter_' . $key, + $default, + $filterType + ); + + $this->setState($stateKey, $stateValue); + } + + parent::populateState($ordering, $direction); + } + + protected function getStoreId($id = '') + { + $id .= ':' . $this->getState('search'); + $id .= ':' . $this->getState('client_id'); + $id .= ':' . $this->getState('enabled'); + $id .= ':' . $this->getState('type'); + $id .= ':' . $this->getState('folder'); + $id .= ':' . $this->getState('supported'); + + return parent::getStoreId($id); + } + + /** + * Method to get the database query + * + * @return \Joomla\Database\DatabaseQuery The database query + * + * @since 3.4 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 's.update_site_id', + 's.name', + 's.type', + 's.location', + 's.enabled', + 's.checked_out', + 's.checked_out_time', + 's.extra_query', + 'e.extension_id', + 'e.name', + 'e.type', + 'e.element', + 'e.folder', + 'e.client_id', + 'e.state', + 'e.manifest_cache', + 'u.name' + ], + [ + 'update_site_id', + 'update_site_name', + 'update_site_type', + 'location', + 'enabled', + 'checked_out', + 'checked_out_time', + 'extra_query', + 'extension_id', + 'name', + 'type', + 'element', + 'folder', + 'client_id', + 'state', + 'manifest_cache', + 'editor' + ] + ) + ) + ->from($db->quoteName('#__update_sites', 's')) + ->join( + 'INNER', + $db->quoteName('#__update_sites_extensions', 'se'), + $db->quoteName('se.update_site_id') . ' = ' . $db->quoteName('s.update_site_id') + ) + ->join( + 'INNER', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('se.extension_id') + ) + ->join( + 'LEFT', + $db->quoteName('#__users', 'u'), + $db->quoteName('s.checked_out') . ' = ' . $db->quoteName('u.id') + ) + ->where($db->quoteName('state') . ' = 0'); + + // Process select filters. + $supported = $this->getState('filter.supported'); + $enabled = $this->getState('filter.enabled'); + $type = $this->getState('filter.type'); + $clientId = $this->getState('filter.client_id'); + $folder = $this->getState('filter.folder'); + + if ($enabled !== '') { + $enabled = (int) $enabled; + $query->where($db->quoteName('s.enabled') . ' = :enabled') + ->bind(':enabled', $enabled, ParameterType::INTEGER); + } + + if ($type) { + $query->where($db->quoteName('e.type') . ' = :type') + ->bind(':type', $type); + } + + if ($clientId !== null && $clientId !== '') { + $clientId = (int) $clientId; + $query->where($db->quoteName('e.client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + if ($folder !== '' && in_array($type, ['plugin', 'library', ''], true)) { + $folderForBinding = $folder === '*' ? '' : $folder; + $query->where($db->quoteName('e.folder') . ' = :folder') + ->bind(':folder', $folderForBinding); + } + + // Process search filter (update site id). + $search = $this->getState('filter.search'); + + if (!empty($search) && stripos($search, 'id:') === 0) { + $uid = (int) substr($search, 3); + $query->where($db->quoteName('s.update_site_id') . ' = :siteId') + ->bind(':siteId', $uid, ParameterType::INTEGER); + } + + if (is_numeric($supported)) { + switch ($supported) { + // Show Update Sites which support Download Keys + case 1: + $supportedIDs = InstallerHelper::getDownloadKeySupportedSites($enabled); + break; + + // Show Update Sites which are missing Download Keys + case -1: + $supportedIDs = InstallerHelper::getDownloadKeyExistsSites(false, $enabled); + break; + + // Show Update Sites which have valid Download Keys + case 2: + $supportedIDs = InstallerHelper::getDownloadKeyExistsSites(true, $enabled); + break; + } + + if (!empty($supportedIDs)) { + // Don't remove array_values(). whereIn expect a zero-based array. + $query->whereIn($db->qn('s.update_site_id'), array_values($supportedIDs)); + } else { + // In case of an empty list of IDs we apply a fake filter to effectively return no data + $query->where($db->qn('s.update_site_id') . ' = 0'); + } + } + + /** + * Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in + * extension.php). + */ + + return $query; + } } diff --git a/administrator/components/com_installer/src/Model/WarningsModel.php b/administrator/components/com_installer/src/Model/WarningsModel.php index ff3d20e5d1ce5..729c5c8a0e0d7 100644 --- a/administrator/components/com_installer/src/Model/WarningsModel.php +++ b/administrator/components/com_installer/src/Model/WarningsModel.php @@ -1,4 +1,5 @@ Text::_('COM_INSTALLER_MSG_WARNINGS_FILEUPLOADSDISABLED'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_FILEUPLOADISDISABLEDDESC'), - ]; - } - - $upload_dir = ini_get('upload_tmp_dir'); - - if (!$upload_dir) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSETDESC'), - ]; - } - elseif (!is_writable($upload_dir)) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTWRITEABLE'), - 'description' => Text::sprintf('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTWRITEABLEDESC', $upload_dir), - ]; - } - - $tmp_path = Factory::getApplication()->get('tmp_path'); - - if (!$tmp_path) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSETDESC'), - ]; - } - elseif (!is_writable($tmp_path)) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE'), - 'description' => Text::sprintf('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLEDESC', $tmp_path), - ]; - } - - $memory_limit = $this->return_bytes(ini_get('memory_limit')); - - if ($memory_limit > -1) - { - if ($memory_limit < $minLimit) - { - // 16MB - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_LOWMEMORYWARN'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_LOWMEMORYDESC'), - ]; - } - elseif ($memory_limit < ($minLimit * 1.5)) - { - // 24MB - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_MEDMEMORYWARN'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_MEDMEMORYDESC'), - ]; - } - } - - $post_max_size = $this->return_bytes(ini_get('post_max_size')); - $upload_max_filesize = $this->return_bytes(ini_get('upload_max_filesize')); - - if ($post_max_size > 0 && $post_max_size < $upload_max_filesize) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_UPLOADBIGGERTHANPOST'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_UPLOADBIGGERTHANPOSTDESC'), - ]; - } - - if ($post_max_size > 0 && $post_max_size < $minLimit) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLPOSTSIZE'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLPOSTSIZEDESC'), - ]; - } - - if ($upload_max_filesize > 0 && $upload_max_filesize < $minLimit) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZEDESC'), - ]; - } - - return $messages; - } + /** + * Extension Type + * @var string + */ + public $type = 'warnings'; + + /** + * Return the byte value of a particular string. + * + * @param string $val String optionally with G, M or K suffix + * + * @return integer size in bytes + * + * @since 1.6 + */ + public function return_bytes($val) + { + if (empty($val)) { + return 0; + } + + $val = trim($val); + + preg_match('#([0-9]+)[\s]*([a-z]+)#i', $val, $matches); + + $last = ''; + + if (isset($matches[2])) { + $last = $matches[2]; + } + + if (isset($matches[1])) { + $val = (int) $matches[1]; + } + + switch (strtolower($last)) { + case 'g': + case 'gb': + $val *= (1024 * 1024 * 1024); + break; + case 'm': + case 'mb': + $val *= (1024 * 1024); + break; + case 'k': + case 'kb': + $val *= 1024; + break; + } + + return (int) $val; + } + + /** + * Load the data. + * + * @return array Messages + * + * @since 1.6 + */ + public function getItems() + { + static $messages; + + if ($messages) { + return $messages; + } + + $messages = []; + + // 16MB + $minLimit = 16 * 1024 * 1024; + + $file_uploads = ini_get('file_uploads'); + + if (!$file_uploads) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_FILEUPLOADSDISABLED'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_FILEUPLOADISDISABLEDDESC'), + ]; + } + + $upload_dir = ini_get('upload_tmp_dir'); + + if (!$upload_dir) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSETDESC'), + ]; + } elseif (!is_writable($upload_dir)) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTWRITEABLE'), + 'description' => Text::sprintf('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTWRITEABLEDESC', $upload_dir), + ]; + } + + $tmp_path = Factory::getApplication()->get('tmp_path'); + + if (!$tmp_path) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSETDESC'), + ]; + } elseif (!is_writable($tmp_path)) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE'), + 'description' => Text::sprintf('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLEDESC', $tmp_path), + ]; + } + + $memory_limit = $this->return_bytes(ini_get('memory_limit')); + + if ($memory_limit > -1) { + if ($memory_limit < $minLimit) { + // 16MB + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_LOWMEMORYWARN'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_LOWMEMORYDESC'), + ]; + } elseif ($memory_limit < ($minLimit * 1.5)) { + // 24MB + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_MEDMEMORYWARN'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_MEDMEMORYDESC'), + ]; + } + } + + $post_max_size = $this->return_bytes(ini_get('post_max_size')); + $upload_max_filesize = $this->return_bytes(ini_get('upload_max_filesize')); + + if ($post_max_size > 0 && $post_max_size < $upload_max_filesize) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_UPLOADBIGGERTHANPOST'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_UPLOADBIGGERTHANPOSTDESC'), + ]; + } + + if ($post_max_size > 0 && $post_max_size < $minLimit) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLPOSTSIZE'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLPOSTSIZEDESC'), + ]; + } + + if ($upload_max_filesize > 0 && $upload_max_filesize < $minLimit) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZEDESC'), + ]; + } + + return $messages; + } } diff --git a/administrator/components/com_installer/src/Service/HTML/Manage.php b/administrator/components/com_installer/src/Service/HTML/Manage.php index 35a10d69450d1..0c34f64df3faa 100644 --- a/administrator/components/com_installer/src/Service/HTML/Manage.php +++ b/administrator/components/com_installer/src/Service/HTML/Manage.php @@ -1,4 +1,5 @@ array( - '', - 'COM_INSTALLER_EXTENSION_PROTECTED', - '', - 'COM_INSTALLER_EXTENSION_PROTECTED', - true, - 'protected', - 'protected', - ), - 1 => array( - 'unpublish', - 'COM_INSTALLER_EXTENSION_ENABLED', - 'COM_INSTALLER_EXTENSION_DISABLE', - 'COM_INSTALLER_EXTENSION_ENABLED', - true, - 'publish', - 'publish', - ), - 0 => array( - 'publish', - 'COM_INSTALLER_EXTENSION_DISABLED', - 'COM_INSTALLER_EXTENSION_ENABLE', - 'COM_INSTALLER_EXTENSION_DISABLED', - true, - 'unpublish', - 'unpublish', - ), - ); + /** + * Returns a published state on a grid. + * + * @param integer $value The state value. + * @param integer $i The row index. + * @param boolean $enabled An optional setting for access control on the action. + * @param string $checkbox An optional prefix for checkboxes. + * + * @return string The Html code + * + * @see JHtmlJGrid::state + * + * @since 2.5 + */ + public function state($value, $i, $enabled = true, $checkbox = 'cb') + { + $states = array( + 2 => array( + '', + 'COM_INSTALLER_EXTENSION_PROTECTED', + '', + 'COM_INSTALLER_EXTENSION_PROTECTED', + true, + 'protected', + 'protected', + ), + 1 => array( + 'unpublish', + 'COM_INSTALLER_EXTENSION_ENABLED', + 'COM_INSTALLER_EXTENSION_DISABLE', + 'COM_INSTALLER_EXTENSION_ENABLED', + true, + 'publish', + 'publish', + ), + 0 => array( + 'publish', + 'COM_INSTALLER_EXTENSION_DISABLED', + 'COM_INSTALLER_EXTENSION_ENABLE', + 'COM_INSTALLER_EXTENSION_DISABLED', + true, + 'unpublish', + 'unpublish', + ), + ); - return HTMLHelper::_('jgrid.state', $states, $value, $i, 'manage.', $enabled, true, $checkbox); - } + return HTMLHelper::_('jgrid.state', $states, $value, $i, 'manage.', $enabled, true, $checkbox); + } } diff --git a/administrator/components/com_installer/src/Service/HTML/Updatesites.php b/administrator/components/com_installer/src/Service/HTML/Updatesites.php index 8f917c2c9841f..a5afda5c27e93 100644 --- a/administrator/components/com_installer/src/Service/HTML/Updatesites.php +++ b/administrator/components/com_installer/src/Service/HTML/Updatesites.php @@ -1,4 +1,5 @@ array( - 'unpublish', - 'COM_INSTALLER_UPDATESITE_ENABLED', - 'COM_INSTALLER_UPDATESITE_DISABLE', - 'COM_INSTALLER_UPDATESITE_ENABLED', - true, - 'publish', - 'publish', - ), - 0 => array( - 'publish', - 'COM_INSTALLER_UPDATESITE_DISABLED', - 'COM_INSTALLER_UPDATESITE_ENABLE', - 'COM_INSTALLER_UPDATESITE_DISABLED', - true, - 'unpublish', - 'unpublish', - ), - ); + /** + * Returns a published state on a grid. + * + * @param integer $value The state value. + * @param integer $i The row index. + * @param boolean $enabled An optional setting for access control on the action. + * @param string $checkbox An optional prefix for checkboxes. + * + * @return string The HTML code + * + * @see JHtmlJGrid::state() + * @since 3.5 + */ + public function state($value, $i, $enabled = true, $checkbox = 'cb') + { + $states = array( + 1 => array( + 'unpublish', + 'COM_INSTALLER_UPDATESITE_ENABLED', + 'COM_INSTALLER_UPDATESITE_DISABLE', + 'COM_INSTALLER_UPDATESITE_ENABLED', + true, + 'publish', + 'publish', + ), + 0 => array( + 'publish', + 'COM_INSTALLER_UPDATESITE_DISABLED', + 'COM_INSTALLER_UPDATESITE_ENABLE', + 'COM_INSTALLER_UPDATESITE_DISABLED', + true, + 'unpublish', + 'unpublish', + ), + ); - return HTMLHelper::_('jgrid.state', $states, $value, $i, 'updatesites.', $enabled, true, $checkbox); - } + return HTMLHelper::_('jgrid.state', $states, $value, $i, 'updatesites.', $enabled, true, $checkbox); + } } diff --git a/administrator/components/com_installer/src/Table/UpdatesiteTable.php b/administrator/components/com_installer/src/Table/UpdatesiteTable.php index 57a5f8784afa6..061f7618ac134 100644 --- a/administrator/components/com_installer/src/Table/UpdatesiteTable.php +++ b/administrator/components/com_installer/src/Table/UpdatesiteTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_installer.downloadkey'; + /** + * Constructor + * + * @param DatabaseDriver $db Database connector object + * + * @since 4.0.0 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_installer.downloadkey'; - parent::__construct('#__update_sites', 'update_site_id', $db); - } + parent::__construct('#__update_sites', 'update_site_id', $db); + } } diff --git a/administrator/components/com_installer/src/View/Database/HtmlView.php b/administrator/components/com_installer/src/View/Database/HtmlView.php index 772e86d40b6d2..5a6c27029bc9d 100644 --- a/administrator/components/com_installer/src/View/Database/HtmlView.php +++ b/administrator/components/com_installer/src/View/Database/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - - try - { - $this->changeSet = $model->getItems(); - } - catch (\Exception $exception) - { - $app->enqueueMessage($exception->getMessage(), 'error'); - } - - $this->errorCount = $model->getErrorCount(); - $this->pagination = $model->getPagination(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - if ($this->changeSet) - { - ($this->errorCount === 0) - ? $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DATABASE_CORE_OK'), 'info') - : $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DATABASE_CORE_ERRORS'), 'warning'); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - /* - * Set toolbar items for the page. - */ - ToolbarHelper::custom('database.fix', 'refresh', '', 'COM_INSTALLER_TOOLBAR_DATABASE_FIX', true); - ToolbarHelper::divider(); - parent::addToolbar(); - ToolbarHelper::help('Information:_Database'); - } + /** + * List of change sets + * + * @var array + * @since 4.0.0 + */ + protected $changeSet = array(); + + /** + * The number of errors found + * + * @var integer + * @since 4.0.0 + */ + protected $errorCount = 0; + + /** + * List pagination. + * + * @var Pagination + * @since 4.0.0 + */ + protected $pagination; + + /** + * The filter form + * + * @var Form + * @since 4.0.0 + */ + public $filterForm; + + /** + * A list of form filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters = array(); + + /** + * Display the view. + * + * @param string $tpl Template + * + * @return void + * + * @throws \Exception + * + * @since 1.6 + */ + public function display($tpl = null) + { + // Get the application + $app = Factory::getApplication(); + + // Get data from the model. + /** @var DatabaseModel $model */ + $model = $this->getModel(); + + try { + $this->changeSet = $model->getItems(); + } catch (\Exception $exception) { + $app->enqueueMessage($exception->getMessage(), 'error'); + } + + $this->errorCount = $model->getErrorCount(); + $this->pagination = $model->getPagination(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + if ($this->changeSet) { + ($this->errorCount === 0) + ? $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DATABASE_CORE_OK'), 'info') + : $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DATABASE_CORE_ERRORS'), 'warning'); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + /* + * Set toolbar items for the page. + */ + ToolbarHelper::custom('database.fix', 'refresh', '', 'COM_INSTALLER_TOOLBAR_DATABASE_FIX', true); + ToolbarHelper::divider(); + parent::addToolbar(); + ToolbarHelper::help('Information:_Database'); + } } diff --git a/administrator/components/com_installer/src/View/Discover/HtmlView.php b/administrator/components/com_installer/src/View/Discover/HtmlView.php index c923170296983..f259ba2a8274d 100644 --- a/administrator/components/com_installer/src/View/Discover/HtmlView.php +++ b/administrator/components/com_installer/src/View/Discover/HtmlView.php @@ -1,4 +1,5 @@ getModel()->checkExtensions()) - { - $this->getModel()->discover(); - } + /** + * Display the view. + * + * @param string $tpl Template + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + // Run discover from the model. + if (!$this->getModel()->checkExtensions()) { + $this->getModel()->discover(); + } - // Get data from the model. - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); + // Get data from the model. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); - if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } + if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.1 - */ - protected function addToolbar() - { - /* - * Set toolbar items for the page. - */ - if (!$this->isEmptyState) - { - ToolbarHelper::custom('discover.install', 'upload', '', 'JTOOLBAR_INSTALL', true); - } + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.1 + */ + protected function addToolbar() + { + /* + * Set toolbar items for the page. + */ + if (!$this->isEmptyState) { + ToolbarHelper::custom('discover.install', 'upload', '', 'JTOOLBAR_INSTALL', true); + } - ToolbarHelper::custom('discover.refresh', 'refresh', '', 'COM_INSTALLER_TOOLBAR_DISCOVER', false); - ToolbarHelper::divider(); + ToolbarHelper::custom('discover.refresh', 'refresh', '', 'COM_INSTALLER_TOOLBAR_DISCOVER', false); + ToolbarHelper::divider(); - parent::addToolbar(); + parent::addToolbar(); - ToolbarHelper::help('Extensions:_Discover'); - } + ToolbarHelper::help('Extensions:_Discover'); + } } diff --git a/administrator/components/com_installer/src/View/Install/HtmlView.php b/administrator/components/com_installer/src/View/Install/HtmlView.php index acd570b4c3d78..c4370d011de21 100644 --- a/administrator/components/com_installer/src/View/Install/HtmlView.php +++ b/administrator/components/com_installer/src/View/Install/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser()->authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Display the view + * + * @param string $tpl Template + * + * @return void + * + * @since 1.5 + */ + public function display($tpl = null) + { + if (!$this->getCurrentUser()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - $paths = new \stdClass; - $paths->first = ''; + $paths = new \stdClass(); + $paths->first = ''; - $this->paths = &$paths; + $this->paths = &$paths; - PluginHelper::importPlugin('installer'); + PluginHelper::importPlugin('installer'); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - if (ContentHelper::getActions('com_installer')->get('core.manage')) - { - ToolbarHelper::link('index.php?option=com_installer&view=manage', 'COM_INSTALLER_TOOLBAR_MANAGE', 'list'); - ToolbarHelper::divider(); - } + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + if (ContentHelper::getActions('com_installer')->get('core.manage')) { + ToolbarHelper::link('index.php?option=com_installer&view=manage', 'COM_INSTALLER_TOOLBAR_MANAGE', 'list'); + ToolbarHelper::divider(); + } - parent::addToolbar(); + parent::addToolbar(); - ToolbarHelper::help('Extensions:_Install'); - } + ToolbarHelper::help('Extensions:_Install'); + } } diff --git a/administrator/components/com_installer/src/View/Installer/HtmlView.php b/administrator/components/com_installer/src/View/Installer/HtmlView.php index bf3cbb9b938a8..9dfea1a312dd1 100644 --- a/administrator/components/com_installer/src/View/Installer/HtmlView.php +++ b/administrator/components/com_installer/src/View/Installer/HtmlView.php @@ -1,4 +1,5 @@ _addPath('template', $this->_basePath . '/tmpl/installer'); - $this->_addPath('template', JPATH_THEMES . '/' . Factory::getApplication()->getTemplate() . '/html/com_installer/installer'); - } + $this->_addPath('template', $this->_basePath . '/tmpl/installer'); + $this->_addPath('template', JPATH_THEMES . '/' . Factory::getApplication()->getTemplate() . '/html/com_installer/installer'); + } - /** - * Display the view. - * - * @param string $tpl Template - * - * @return void - * - * @since 1.5 - */ - public function display($tpl = null) - { - // Get data from the model. - $state = $this->get('State'); + /** + * Display the view. + * + * @param string $tpl Template + * + * @return void + * + * @since 1.5 + */ + public function display($tpl = null) + { + // Get data from the model. + $state = $this->get('State'); - // Are there messages to display? - $showMessage = false; + // Are there messages to display? + $showMessage = false; - if (is_object($state)) - { - $message1 = $state->get('message'); - $message2 = $state->get('extension_message'); - $showMessage = ($message1 || $message2); - } + if (is_object($state)) { + $message1 = $state->get('message'); + $message2 = $state->get('extension_message'); + $showMessage = ($message1 || $message2); + } - $this->showMessage = $showMessage; - $this->state = &$state; + $this->showMessage = $showMessage; + $this->state = &$state; - $this->addToolbar(); - parent::display($tpl); - } + $this->addToolbar(); + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_installer'); - ToolbarHelper::title(Text::_('COM_INSTALLER_HEADER_' . strtoupper($this->getName())), 'puzzle-piece install'); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_installer'); + ToolbarHelper::title(Text::_('COM_INSTALLER_HEADER_' . strtoupper($this->getName())), 'puzzle-piece install'); - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_installer'); - ToolbarHelper::divider(); - } - } + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_installer'); + ToolbarHelper::divider(); + } + } } diff --git a/administrator/components/com_installer/src/View/Languages/HtmlView.php b/administrator/components/com_installer/src/View/Languages/HtmlView.php index 41766b6330663..c2031d85bd32c 100644 --- a/administrator/components/com_installer/src/View/Languages/HtmlView.php +++ b/administrator/components/com_installer/src/View/Languages/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser()->authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Display the view. + * + * @param null $tpl template to display + * + * @return mixed|void + */ + public function display($tpl = null) + { + if (!$this->getCurrentUser()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - // Get data from the model. - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->installedLang = LanguageHelper::getInstalledLanguages(); + // Get data from the model. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->installedLang = LanguageHelper::getInstalledLanguages(); - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_installer'); - ToolbarHelper::title(Text::_('COM_INSTALLER_HEADER_' . $this->getName()), 'puzzle-piece install'); + /** + * Add the page title and toolbar. + * + * @return void + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_installer'); + ToolbarHelper::title(Text::_('COM_INSTALLER_HEADER_' . $this->getName()), 'puzzle-piece install'); - if ($canDo->get('core.admin')) - { - parent::addToolbar(); + if ($canDo->get('core.admin')) { + parent::addToolbar(); - ToolbarHelper::help('Extensions:_Languages'); - } - } + ToolbarHelper::help('Extensions:_Languages'); + } + } } diff --git a/administrator/components/com_installer/src/View/Manage/HtmlView.php b/administrator/components/com_installer/src/View/Manage/HtmlView.php index 3262bae2834ae..25a5222f08bfa 100644 --- a/administrator/components/com_installer/src/View/Manage/HtmlView.php +++ b/administrator/components/com_installer/src/View/Manage/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Display the view. - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $toolbar = Toolbar::getInstance('toolbar'); - $canDo = ContentHelper::getActions('com_installer'); - - if ($canDo->get('core.edit.state')) - { - $toolbar->publish('manage.publish') - ->text('JTOOLBAR_ENABLE') - ->listCheck(true); - $toolbar->unpublish('manage.unpublish') - ->text('JTOOLBAR_DISABLE') - ->listCheck(true); - $toolbar->divider(); - } - - $toolbar->standardButton('refresh') - ->text('JTOOLBAR_REFRESH_CACHE') - ->task('manage.refresh') - ->listCheck(true); - $toolbar->divider(); - - if ($canDo->get('core.delete')) - { - $toolbar->delete('manage.remove') - ->text('JTOOLBAR_UNINSTALL') - ->message('COM_INSTALLER_CONFIRM_UNINSTALL') - ->listCheck(true); - $toolbar->divider(); - } - - if ($canDo->get('core.manage')) - { - ToolbarHelper::link('index.php?option=com_installer&view=install', 'COM_INSTALLER_TOOLBAR_INSTALL_EXTENSIONS', 'upload'); - $toolbar->divider(); - } - - parent::addToolbar(); - $toolbar->help('Extensions:_Manage'); - } + /** + * List of updatesites + * + * @var \stdClass[] + */ + protected $items; + + /** + * Pagination object + * + * @var Pagination + */ + protected $pagination; + + /** + * Form object + * + * @var Form + */ + protected $form; + + /** + * Display the view. + * + * @param string $tpl Template + * + * @return mixed|void + * + * @since 1.6 + */ + public function display($tpl = null) + { + // Get data from the model. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Display the view. + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $toolbar = Toolbar::getInstance('toolbar'); + $canDo = ContentHelper::getActions('com_installer'); + + if ($canDo->get('core.edit.state')) { + $toolbar->publish('manage.publish') + ->text('JTOOLBAR_ENABLE') + ->listCheck(true); + $toolbar->unpublish('manage.unpublish') + ->text('JTOOLBAR_DISABLE') + ->listCheck(true); + $toolbar->divider(); + } + + $toolbar->standardButton('refresh') + ->text('JTOOLBAR_REFRESH_CACHE') + ->task('manage.refresh') + ->listCheck(true); + $toolbar->divider(); + + if ($canDo->get('core.delete')) { + $toolbar->delete('manage.remove') + ->text('JTOOLBAR_UNINSTALL') + ->message('COM_INSTALLER_CONFIRM_UNINSTALL') + ->listCheck(true); + $toolbar->divider(); + } + + if ($canDo->get('core.manage')) { + ToolbarHelper::link('index.php?option=com_installer&view=install', 'COM_INSTALLER_TOOLBAR_INSTALL_EXTENSIONS', 'upload'); + $toolbar->divider(); + } + + parent::addToolbar(); + $toolbar->help('Extensions:_Manage'); + } } diff --git a/administrator/components/com_installer/src/View/Update/HtmlView.php b/administrator/components/com_installer/src/View/Update/HtmlView.php index 2f1b991527d43..e831474ffb459 100644 --- a/administrator/components/com_installer/src/View/Update/HtmlView.php +++ b/administrator/components/com_installer/src/View/Update/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - $paths = new \stdClass; - $paths->first = ''; - - $this->paths = &$paths; - - if (count($this->items) === 0 && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - else - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE'), 'warning'); - } - - // Find if there are any updates which require but are missing a Download Key - if (!class_exists('Joomla\Component\Installer\Administrator\Helper\InstallerHelper')) - { - require_once JPATH_COMPONENT_ADMINISTRATOR . '/Helper/InstallerHelper.php'; - } - - $mappingCallback = function ($item) { - $dlkeyInfo = CmsInstallerHelper::getDownloadKey(new CMSObject($item)); - $item->isMissingDownloadKey = $dlkeyInfo['supported'] && !$dlkeyInfo['valid']; - - if ($item->isMissingDownloadKey) - { - $this->missingDownloadKeys++; - } - - return $item; - }; - $this->items = array_map($mappingCallback, $this->items); - - if ($this->missingDownloadKeys) - { - $url = 'index.php?option=com_installer&view=updatesites&filter[supported]=-1'; - $msg = Text::plural('COM_INSTALLER_UPDATE_MISSING_DOWNLOADKEY_LABEL_N', $this->missingDownloadKeys, $url); - Factory::getApplication()->enqueueMessage($msg, CMSApplication::MSG_WARNING); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - if (false === $this->isEmptyState) - { - ToolbarHelper::custom('update.update', 'upload', '', 'COM_INSTALLER_TOOLBAR_UPDATE', true); - } - - ToolbarHelper::custom('update.find', 'refresh', '', 'COM_INSTALLER_TOOLBAR_FIND_UPDATES', false); - ToolbarHelper::divider(); - - parent::addToolbar(); - ToolbarHelper::help('Extensions:_Update'); - } + /** + * List of update items. + * + * @var array + */ + protected $items; + + /** + * List pagination. + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * How many updates require but are missing Download Keys + * + * @var integer + * @since 4.0.0 + */ + protected $missingDownloadKeys = 0; + + /** + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view. + * + * @param string $tpl Template + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + // Get data from the model. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + $paths = new \stdClass(); + $paths->first = ''; + + $this->paths = &$paths; + + if (count($this->items) === 0 && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } else { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE'), 'warning'); + } + + // Find if there are any updates which require but are missing a Download Key + if (!class_exists('Joomla\Component\Installer\Administrator\Helper\InstallerHelper')) { + require_once JPATH_COMPONENT_ADMINISTRATOR . '/Helper/InstallerHelper.php'; + } + + $mappingCallback = function ($item) { + $dlkeyInfo = CmsInstallerHelper::getDownloadKey(new CMSObject($item)); + $item->isMissingDownloadKey = $dlkeyInfo['supported'] && !$dlkeyInfo['valid']; + + if ($item->isMissingDownloadKey) { + $this->missingDownloadKeys++; + } + + return $item; + }; + $this->items = array_map($mappingCallback, $this->items); + + if ($this->missingDownloadKeys) { + $url = 'index.php?option=com_installer&view=updatesites&filter[supported]=-1'; + $msg = Text::plural('COM_INSTALLER_UPDATE_MISSING_DOWNLOADKEY_LABEL_N', $this->missingDownloadKeys, $url); + Factory::getApplication()->enqueueMessage($msg, CMSApplication::MSG_WARNING); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + if (false === $this->isEmptyState) { + ToolbarHelper::custom('update.update', 'upload', '', 'COM_INSTALLER_TOOLBAR_UPDATE', true); + } + + ToolbarHelper::custom('update.find', 'refresh', '', 'COM_INSTALLER_TOOLBAR_FIND_UPDATES', false); + ToolbarHelper::divider(); + + parent::addToolbar(); + ToolbarHelper::help('Extensions:_Update'); + } } diff --git a/administrator/components/com_installer/src/View/Updatesite/HtmlView.php b/administrator/components/com_installer/src/View/Updatesite/HtmlView.php index eff7ee4eeff4d..2cde0cb0c9a2a 100644 --- a/administrator/components/com_installer/src/View/Updatesite/HtmlView.php +++ b/administrator/components/com_installer/src/View/Updatesite/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->form = $model->getForm(); - $this->item = $model->getItem(); - - // Remove the extra_query field if it's a free download extension - $dlidSupportingSites = InstallerHelper::getDownloadKeySupportedSites(false); - $update_site_id = $this->item->get('update_site_id'); - - if (!in_array($update_site_id, $dlidSupportingSites)) - { - $this->form->removeField('extra_query'); - } - - // Check for errors. - if (count($errors = $model->getErrors())) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - * - * @throws \Exception - */ - protected function addToolbar(): void - { - $app = Factory::getApplication(); - $app->input->set('hidemainmenu', true); - - $user = $app->getIdentity(); - $userId = $user->id; - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out === $userId); - - // Since we don't track these assets at the item level, use the category id. - $canDo = ContentHelper::getActions('com_installer', 'updatesite'); - - ToolbarHelper::title(Text::_('COM_INSTALLER_UPDATESITE_EDIT_TITLE'), 'address contact'); - - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit'); - $toolbarButtons = []; - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable && $this->form->getField('extra_query')) - { - $toolbarButtons[] = ['apply', 'updatesite.apply']; - $toolbarButtons[] = ['save', 'updatesite.save']; - } - - ToolbarHelper::saveGroup($toolbarButtons); - - ToolbarHelper::cancel('updatesite.cancel', 'JTOOLBAR_CLOSE'); - - ToolbarHelper::help('Edit_Update_Site'); - } + /** + * The Form object + * + * @var Form + * + * @since 4.0.0 + */ + protected $form; + + /** + * The active item + * + * @var object + * + * @since 4.0.0 + */ + protected $item; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + * + * @throws \Exception + */ + public function display($tpl = null): void + { + /** @var UpdatesiteModel $model */ + $model = $this->getModel(); + $this->form = $model->getForm(); + $this->item = $model->getItem(); + + // Remove the extra_query field if it's a free download extension + $dlidSupportingSites = InstallerHelper::getDownloadKeySupportedSites(false); + $update_site_id = $this->item->get('update_site_id'); + + if (!in_array($update_site_id, $dlidSupportingSites)) { + $this->form->removeField('extra_query'); + } + + // Check for errors. + if (count($errors = $model->getErrors())) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + * + * @throws \Exception + */ + protected function addToolbar(): void + { + $app = Factory::getApplication(); + $app->input->set('hidemainmenu', true); + + $user = $app->getIdentity(); + $userId = $user->id; + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out === $userId); + + // Since we don't track these assets at the item level, use the category id. + $canDo = ContentHelper::getActions('com_installer', 'updatesite'); + + ToolbarHelper::title(Text::_('COM_INSTALLER_UPDATESITE_EDIT_TITLE'), 'address contact'); + + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit'); + $toolbarButtons = []; + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable && $this->form->getField('extra_query')) { + $toolbarButtons[] = ['apply', 'updatesite.apply']; + $toolbarButtons[] = ['save', 'updatesite.save']; + } + + ToolbarHelper::saveGroup($toolbarButtons); + + ToolbarHelper::cancel('updatesite.cancel', 'JTOOLBAR_CLOSE'); + + ToolbarHelper::help('Edit_Update_Site'); + } } diff --git a/administrator/components/com_installer/src/View/Updatesites/HtmlView.php b/administrator/components/com_installer/src/View/Updatesites/HtmlView.php index 3041f780a7dba..f1a3298d309de 100644 --- a/administrator/components/com_installer/src/View/Updatesites/HtmlView.php +++ b/administrator/components/com_installer/src/View/Updatesites/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - // Check for errors. - if (count($errors = $model->getErrors())) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Display the view - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.4 - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_installer'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('updatesites.publish', 'JTOOLBAR_ENABLE')->listCheck(true); - $childBar->unpublish('updatesites.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); - - if ($canDo->get('core.delete')) - { - $childBar->delete('updatesites.delete')->listCheck(true); - } - - $childBar->checkin('updatesites.checkin')->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::custom('updatesites.rebuild', 'refresh', '', 'JTOOLBAR_REBUILD', false); - } - - parent::addToolbar(); - - ToolbarHelper::help('Extensions:_Update_Sites'); - } + /** + * The search tools form + * + * @var Form + * @since 3.4 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 3.4 + */ + public $activeFilters = []; + + /** + * List of updatesites + * + * @var \stdClass[] + * @since 3.4 + */ + protected $items; + + /** + * Pagination object + * + * @var Pagination + * @since 3.4 + */ + protected $pagination; + + /** + * Display the view + * + * @param string $tpl Template + * + * @return mixed|void + * + * @since 3.4 + * + * @throws \Exception on errors + */ + public function display($tpl = null): void + { + /** @var UpdatesitesModel $model */ + $model = $this->getModel(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + // Check for errors. + if (count($errors = $model->getErrors())) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Display the view + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.4 + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_installer'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('updatesites.publish', 'JTOOLBAR_ENABLE')->listCheck(true); + $childBar->unpublish('updatesites.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); + + if ($canDo->get('core.delete')) { + $childBar->delete('updatesites.delete')->listCheck(true); + } + + $childBar->checkin('updatesites.checkin')->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::custom('updatesites.rebuild', 'refresh', '', 'JTOOLBAR_REBUILD', false); + } + + parent::addToolbar(); + + ToolbarHelper::help('Extensions:_Update_Sites'); + } } diff --git a/administrator/components/com_installer/src/View/Warnings/HtmlView.php b/administrator/components/com_installer/src/View/Warnings/HtmlView.php index 6452040816bc1..17cce256dbef8 100644 --- a/administrator/components/com_installer/src/View/Warnings/HtmlView.php +++ b/administrator/components/com_installer/src/View/Warnings/HtmlView.php @@ -1,4 +1,5 @@ messages = $this->get('Items'); - - if (!\count($this->messages)) - { - $this->setLayout('emptystate'); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - parent::addToolbar(); - - ToolbarHelper::help('Information:_Warnings'); - } + /** + * Display the view + * + * @param string $tpl Template + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->messages = $this->get('Items'); + + if (!\count($this->messages)) { + $this->setLayout('emptystate'); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + parent::addToolbar(); + + ToolbarHelper::help('Information:_Warnings'); + } } diff --git a/administrator/components/com_installer/tmpl/database/default.php b/administrator/components/com_installer/tmpl/database/default.php index ff27f5666d25c..ac7d87af469eb 100644 --- a/administrator/components/com_installer/tmpl/database/default.php +++ b/administrator/components/com_installer/tmpl/database/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirection = $this->escape($this->state->get('list.direction')); ?>
    -
    -
    -
    -
    - $this)); ?> - changeSet)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - changeSet as $i => $item) : ?> - - manifest_cache); ?> + +
    +
    +
    + $this)); ?> + changeSet)) : ?> +
    + + +
    + +
    - , - , - -
    - - - - - - - - - - - - - - - - - -
    + + + + + + + + + + + + + + + + changeSet as $i => $item) : ?> + + manifest_cache); ?> - - - - - - - - - - - - - -
    + , + , + +
    + + + + + + + + + + + + + + + + + +
    - extension_id, false, 'cid', 'cb', $extension->name); ?> - - name; ?> -
    - description); ?> -
    -
    - client_translated; ?> - - type_translated; ?> - - - - - - - version_id; ?> - - version; ?> - - folder_translated; ?> - - extension_id; ?> -
    + + + extension_id, false, 'cid', 'cb', $extension->name); ?> + + + name; ?> +
    + description); ?> +
    + + + client_translated; ?> + + + type_translated; ?> + + + + + + + + + version_id; ?> + + + version; ?> + + + folder_translated; ?> + + + extension_id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
    -
    -
    -
    + + + + +
    + + + diff --git a/administrator/components/com_installer/tmpl/discover/default.php b/administrator/components/com_installer/tmpl/discover/default.php index c01ea5f9324fd..c2b850b8a35a4 100644 --- a/administrator/components/com_installer/tmpl/discover/default.php +++ b/administrator/components/com_installer/tmpl/discover/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
    -
    -
    -
    -
    - showMessage) : ?> - loadTemplate('message'); ?> - - $this)); ?> - items)) : ?> -
    - - -
    -
    - - -
    - - - - - - - - - - - - - - - - - - items as $i => $item) : ?> - - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - - - - - -
    - extension_id, false, 'cid', 'cb', $item->name); ?> - - name; ?> -
    description; ?>
    -
    - client_translated; ?> - - type_translated; ?> - - version) ? $item->version : ' '; ?> - - creationDate) ? $item->creationDate : ' '; ?> - - author) ? $item->author : ' '; ?> - - folder_translated; ?> - - extension_id; ?> -
    + +
    +
    +
    + showMessage) : ?> + loadTemplate('message'); ?> + + $this)); ?> + items)) : ?> +
    + + +
    +
    + + +
    + + + + + + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + + + + + +
    + extension_id, false, 'cid', 'cb', $item->name); ?> + + name; ?> +
    description; ?>
    +
    + client_translated; ?> + + type_translated; ?> + + version) ? $item->version : ' '; ?> + + creationDate) ? $item->creationDate : ' '; ?> + + author) ? $item->author : ' '; ?> + + folder_translated; ?> + + extension_id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
    -
    -
    - + + + + +
    +
    +
    +
    diff --git a/administrator/components/com_installer/tmpl/discover/emptystate.php b/administrator/components/com_installer/tmpl/discover/emptystate.php index 547c2b77acfb1..0d56305b7ea34 100644 --- a/administrator/components/com_installer/tmpl/discover/emptystate.php +++ b/administrator/components/com_installer/tmpl/discover/emptystate.php @@ -1,4 +1,5 @@ 'COM_INSTALLER', - 'formURL' => 'index.php?option=com_installer&task=discover.refresh', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Extensions:_Discover', - 'icon' => 'icon-puzzle-piece install', - 'createURL' => 'index.php?option=com_installer&task=discover.refresh&' . Session::getFormToken() . '=1', - 'content' => Text::_('COM_INSTALLER_MSG_DISCOVER_DESCRIPTION'), - 'title' => Text::_('COM_INSTALLER_EMPTYSTATE_DISCOVER_TITLE'), - 'btnadd' => Text::_('COM_INSTALLER_EMPTYSTATE_DISCOVER_BUTTON_ADD'), + 'textPrefix' => 'COM_INSTALLER', + 'formURL' => 'index.php?option=com_installer&task=discover.refresh', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Extensions:_Discover', + 'icon' => 'icon-puzzle-piece install', + 'createURL' => 'index.php?option=com_installer&task=discover.refresh&' . Session::getFormToken() . '=1', + 'content' => Text::_('COM_INSTALLER_MSG_DISCOVER_DESCRIPTION'), + 'title' => Text::_('COM_INSTALLER_EMPTYSTATE_DISCOVER_TITLE'), + 'btnadd' => Text::_('COM_INSTALLER_EMPTYSTATE_DISCOVER_BUTTON_ADD'), ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_installer/tmpl/install/default.php b/administrator/components/com_installer/tmpl/install/default.php index 151bf9fa384c0..1c2b115aa7477 100644 --- a/administrator/components/com_installer/tmpl/install/default.php +++ b/administrator/components/com_installer/tmpl/install/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->usePreset('com_installer.installer') - ->useScript('webcomponent.core-loader'); + ->usePreset('com_installer.installer') + ->useScript('webcomponent.core-loader'); $app = Factory::getApplication(); $tabs = $app->triggerEvent('onInstallerAddInstallationTab', []); @@ -34,42 +35,42 @@ ?>
    -
    - - showMessage) : ?> - loadTemplate('message'); ?> - + + + showMessage) : ?> + loadTemplate('message'); ?> + -
    -
    -
    - -
    - - -
    - +
    +
    +
    + +
    + + +
    + - - $tabs[0]['name'] ?? '', 'recall' => true, 'breakpoint' => 768]); ?> - - - -
    - -
    - - + + $tabs[0]['name'] ?? '', 'recall' => true, 'breakpoint' => 768]); ?> + + + +
    + +
    + + - - + + - - - -
    -
    -
    - + + + +
    +
    +
    +
    diff --git a/administrator/components/com_installer/tmpl/installer/default_message.php b/administrator/components/com_installer/tmpl/installer/default_message.php index b0e8170be42d1..3d1c78d6aa87b 100644 --- a/administrator/components/com_installer/tmpl/installer/default_message.php +++ b/administrator/components/com_installer/tmpl/installer/default_message.php @@ -1,4 +1,5 @@ -
    - -
    +
    + +
    -
    - -
    +
    + +
    diff --git a/administrator/components/com_installer/tmpl/languages/default.php b/administrator/components/com_installer/tmpl/languages/default.php index e14f69e07d226..e17347af3646b 100644 --- a/administrator/components/com_installer/tmpl/languages/default.php +++ b/administrator/components/com_installer/tmpl/languages/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?>
    -
    -
    -
    -
    - $this, 'options' => array('filterButton' => false))); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - getShortVersion()); - $i = 0; - foreach ($this->items as $language) : - preg_match('#^pkg_([a-z]{2,3}-[A-Z]{2})$#', $language->element, $element); - $language->code = $element[1]; - ?> - - - - - - - - - - -
    - , - , - -
    - - - - - - - -
    - installedLang[0][$language->code]) || isset($this->installedLang[1][$language->code])) ? 'REINSTALL' : 'INSTALL'; ?> - installedLang[0][$language->code]) || isset($this->installedLang[1][$language->code])) ? 'btn btn-success btn-sm' : 'btn btn-primary btn-sm'; ?> - detailsurl . '\'; Joomla.submitbutton(\'install.install\');'; ?> - - - name; ?> - - code; ?> - - - - version, $minorVersion) !== 0 || strpos($language->version, $currentShortVersion) !== 0) : ?> - version; ?> - - - - version; ?> - - - detailsurl; ?> -
    + +
    +
    +
    + $this, 'options' => array('filterButton' => false))); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + getShortVersion()); + $i = 0; + foreach ($this->items as $language) : + preg_match('#^pkg_([a-z]{2,3}-[A-Z]{2})$#', $language->element, $element); + $language->code = $element[1]; + ?> + + + + + + + + + + +
    + , + , + +
    + + + + + + + +
    + installedLang[0][$language->code]) || isset($this->installedLang[1][$language->code])) ? 'REINSTALL' : 'INSTALL'; ?> + installedLang[0][$language->code]) || isset($this->installedLang[1][$language->code])) ? 'btn btn-success btn-sm' : 'btn btn-primary btn-sm'; ?> + detailsurl . '\'; Joomla.submitbutton(\'install.install\');'; ?> + + + name; ?> + + code; ?> + + + + version, $minorVersion) !== 0 || strpos($language->version, $currentShortVersion) !== 0) : ?> + version; ?> + + + + version; ?> + + + detailsurl; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - - - - - -
    -
    -
    - + + + + + + + + +
    +
    +
    +
    diff --git a/administrator/components/com_installer/tmpl/manage/default.php b/administrator/components/com_installer/tmpl/manage/default.php index 90a939b7fbd13..52fb80188299d 100644 --- a/administrator/components/com_installer/tmpl/manage/default.php +++ b/administrator/components/com_installer/tmpl/manage/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_installer.changelog') - ->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('table.columns') + ->useScript('multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?>
    -
    -
    -
    -
    - showMessage) : ?> - loadTemplate('message'); ?> - - $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - - - - - items as $i => $item) : ?> - - - - - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - - - - - - - - - - - -
    - extension_id, false, 'cid', 'cb', $item->name); ?> - - element) : ?> - X - - status, $i, $item->status < 2, 'cb'); ?> - - - name; ?> - - - client_translated; ?> - - type_translated; ?> - - version)) : ?> - changelogurl)) : ?> - - version?> - - extension_id, - array( - 'title' => Text::sprintf('COM_INSTALLER_CHANGELOG_TITLE', $item->name, $item->version), - ), - '' - ); - ?> - - version; ?> - - - - creationDate)) : ?> - creationDate, $createdDateFormat); - } - catch (Exception $e) { - echo $item->creationDate; - }?> - - - - - author) ? $item->author : ' '; ?> - - folder_translated; ?> - - locked ? Text::_('JYES') : Text::_('JNO'); ?> - - package_id ?: ' '; ?> - - extension_id; ?> -
    + +
    +
    +
    + showMessage) : ?> + loadTemplate('message'); ?> + + $this)); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + extension_id, false, 'cid', 'cb', $item->name); ?> + + element) : ?> + X + + status, $i, $item->status < 2, 'cb'); ?> + + + name; ?> + + + client_translated; ?> + + type_translated; ?> + + version)) : ?> + changelogurl)) : ?> + + version?> + + extension_id, + array( + 'title' => Text::sprintf('COM_INSTALLER_CHANGELOG_TITLE', $item->name, $item->version), + ), + '' + ); + ?> + + version; ?> + + + + creationDate)) : ?> + creationDate, $createdDateFormat); + } catch (Exception $e) { + echo $item->creationDate; + }?> + + + + + author) ? $item->author : ' '; ?> + + folder_translated; ?> + + locked ? Text::_('JYES') : Text::_('JNO'); ?> + + package_id ?: ' '; ?> + + extension_id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
    -
    -
    - + + + + +
    +
    +
    +
    diff --git a/administrator/components/com_installer/tmpl/update/default.php b/administrator/components/com_installer/tmpl/update/default.php index 9122e871137d0..ea0fe5681717a 100644 --- a/administrator/components/com_installer/tmpl/update/default.php +++ b/administrator/components/com_installer/tmpl/update/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('multiselect') - ->useScript('com_installer.changelog'); + ->useScript('com_installer.changelog'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?>
    -
    -
    -
    -
    - showMessage) : ?> - loadTemplate('message'); ?> - - $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - items as $i => $item): ?> - - - - - - - - + + + + + +
    - , - , - -
    - - - - - - - - - - - - - - - - - -
    - isMissingDownloadKey): ?> - - - update_id, false, 'cid', 'cb', $item->name); ?> - - - escape($item->name); ?> - -
    - detailsurl; ?> - infourl)) : ?> -
    - escape(trim($item->infourl)); ?> - -
    - isMissingDownloadKey): ?> - update_site_id; ?> - - -
    - client_translated; ?> - - type_translated; ?> - - current_version; ?> - - version; ?> - - changelogurl)) : ?> - - - - extension_id, - array( - 'title' => Text::sprintf('COM_INSTALLER_CHANGELOG_TITLE', $item->name, $item->version), - ), - '' - ); - ?> - - - - + +
    +
    +
    + showMessage) : ?> + loadTemplate('message'); ?> + + $this)); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + - - - - - -
    + , + , + +
    + + + + + + + + + + + + + + + + + +
    + isMissingDownloadKey) : ?> + + + update_id, false, 'cid', 'cb', $item->name); ?> + + + escape($item->name); ?> + +
    + detailsurl; ?> + infourl)) : ?> +
    + escape(trim($item->infourl)); ?> + +
    + isMissingDownloadKey) : ?> + update_site_id; ?> + + +
    + client_translated; ?> + + type_translated; ?> + + current_version; ?> + + version; ?> + + changelogurl)) : ?> + + + + extension_id, + array( + 'title' => Text::sprintf('COM_INSTALLER_CHANGELOG_TITLE', $item->name, $item->version), + ), + '' + ); + ?> + + + + - - - folder_translated; ?> - - install_type; ?> -
    + +
    + folder_translated; ?> + + install_type; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
    -
    -
    -
    + + + + +
    + + + diff --git a/administrator/components/com_installer/tmpl/update/emptystate.php b/administrator/components/com_installer/tmpl/update/emptystate.php index 923e561332e76..172cfd663eaad 100644 --- a/administrator/components/com_installer/tmpl/update/emptystate.php +++ b/administrator/components/com_installer/tmpl/update/emptystate.php @@ -1,4 +1,5 @@ 'COM_INSTALLER', - 'formURL' => 'index.php?option=com_installer&view=update', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Extensions:_Update', - 'icon' => 'icon-puzzle-piece install', + 'textPrefix' => 'COM_INSTALLER', + 'formURL' => 'index.php?option=com_installer&view=update', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Extensions:_Update', + 'icon' => 'icon-puzzle-piece install', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_installer&task=update.find&' . Session::getFormToken() . '=1'; +if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_installer&task=update.find&' . Session::getFormToken() . '=1'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_installer/tmpl/updatesite/edit.php b/administrator/components/com_installer/tmpl/updatesite/edit.php index 25bce98728730..0c252686195b4 100644 --- a/administrator/components/com_installer/tmpl/updatesite/edit.php +++ b/administrator/components/com_installer/tmpl/updatesite/edit.php @@ -1,4 +1,5 @@

    item->name; ?>

    - form->renderFieldset('updateSite'); ?> - - + form->renderFieldset('updateSite'); ?> + +
    diff --git a/administrator/components/com_installer/tmpl/updatesites/default.php b/administrator/components/com_installer/tmpl/updatesites/default.php index ec4d156125b72..79e90252f8130 100644 --- a/administrator/components/com_installer/tmpl/updatesites/default.php +++ b/administrator/components/com_installer/tmpl/updatesites/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getApplication()->getIdentity(); $userId = $user->get('id'); @@ -26,136 +27,139 @@ $listDirn = $this->escape($this->state->get('list.direction')); ?>
    -
    -
    -
    -
    - $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - items as $i => $item) : - $canCheckin = $user->authorise('core.manage', 'com_checkin') - || $item->checked_out === $userId - || is_null($item->checked_out); - $canEdit = $user->authorise('core.edit', 'com_installer'); - ?> - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - - - -
    - update_site_id, false, 'cid', 'cb', $item->update_site_name); ?> - - element) : ?> - X - - enabled, $i, $item->enabled < 2, 'cb'); ?> - - - checked_out) : ?> - editor, $item->checked_out_time, 'updatesites.', $canCheckin); ?> - - - - update_site_name); ?> - - - update_site_name); ?> - - -
    - downloadKey['valid']) : ?> - - - - downloadKey['value']; ?> - downloadKey['supported']) : ?> - - - - - -
    -
    - - name; ?> - - - - client_translated; ?> - - type_translated; ?> - - folder_translated; ?> - - update_site_id; ?> -
    + +
    +
    +
    + $this)); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + items as $i => $item) : + $canCheckin = $user->authorise('core.manage', 'com_checkin') + || $item->checked_out === $userId + || is_null($item->checked_out); + $canEdit = $user->authorise('core.edit', 'com_installer'); + ?> + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + + + +
    + update_site_id, false, 'cid', 'cb', $item->update_site_name); ?> + + element) : ?> + X + + enabled, $i, $item->enabled < 2, 'cb'); ?> + + + checked_out) : ?> + editor, $item->checked_out_time, 'updatesites.', $canCheckin); ?> + + + + update_site_name); ?> + + + update_site_name); ?> + + +
    + downloadKey['valid']) : ?> + + + + downloadKey['value']; ?> + downloadKey['supported']) : ?> + + + + + +
    +
    + + name; ?> + + + + client_translated; ?> + + type_translated; ?> + + folder_translated; ?> + + update_site_id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
    -
    -
    - + + + + +
    +
    +
    +
    diff --git a/administrator/components/com_installer/tmpl/warnings/default.php b/administrator/components/com_installer/tmpl/warnings/default.php index bcf4c662ac9ab..8bd5bc8a8d52f 100644 --- a/administrator/components/com_installer/tmpl/warnings/default.php +++ b/administrator/components/com_installer/tmpl/warnings/default.php @@ -1,4 +1,5 @@
    -
    -
    -
    -
    - messages)) : ?> - messages as $message) : ?> -
    -

    - - - -

    -

    -
    - -
    -

    - - - -

    -

    -
    - -
    - - - -
    - -
    - - -
    -
    -
    -
    -
    +
    +
    +
    +
    + messages)) : ?> + messages as $message) : ?> +
    +

    + + + +

    +

    +
    + +
    +

    + + + +

    +

    +
    + +
    + + + +
    + +
    + + +
    +
    +
    +
    +
    diff --git a/administrator/components/com_installer/tmpl/warnings/emptystate.php b/administrator/components/com_installer/tmpl/warnings/emptystate.php index 03ebbd1bc86b2..82397e9b1b6a7 100644 --- a/administrator/components/com_installer/tmpl/warnings/emptystate.php +++ b/administrator/components/com_installer/tmpl/warnings/emptystate.php @@ -1,4 +1,5 @@ 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Information:_Warnings', - 'icon' => 'icon-puzzle-piece install', - 'title' => Text::_('COM_INSTALLER_MSG_WARNINGS_NONE'), - 'content' => '', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Information:_Warnings', + 'icon' => 'icon-puzzle-piece install', + 'title' => Text::_('COM_INSTALLER_MSG_WARNINGS_NONE'), + 'content' => '', ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php index 1ea4d93c8a799..38aa03f2e5cd0 100644 --- a/administrator/components/com_joomlaupdate/extract.php +++ b/administrator/components/com_joomlaupdate/extract.php @@ -1,4 +1,5 @@ setupMaxExecTime(); - - // Initialize start time - $this->startTime = microtime(true); - } - - /** - * Singleton implementation. - * - * @return static - * @since 4.0.4 - */ - public static function getInstance(): self - { - if (is_null(self::$instance)) - { - self::$instance = new self; - } - - return self::$instance; - } - - /** - * Returns a serialised copy of the object. - * - * This is different to calling serialise() directly. This operates on a copy of the object which undergoes a - * call to shutdown() first so any open files are closed first. - * - * @return string The serialised data, potentially base64 encoded. - * @since 4.0.4 - */ - public static function getSerialised(): string - { - $clone = clone self::getInstance(); - $clone->shutdown(); - $serialized = serialize($clone); - - return (function_exists('base64_encode') && function_exists('base64_decode')) ? base64_encode($serialized) : $serialized; - } - - /** - * Restores a serialised instance into the singleton implementation and returns it. - * - * If the serialised data is corrupt it will return null. - * - * @param string $serialised The serialised data, potentially base64 encoded, to deserialize. - * - * @return static|null The instance of the object, NULL if it cannot be deserialised. - * @since 4.0.4 - */ - public static function unserialiseInstance(string $serialised): ?self - { - if (function_exists('base64_encode') && function_exists('base64_decode')) - { - $serialised = base64_decode($serialised); - } - - $instance = @unserialize($serialised, [ - 'allowed_classes' => [ - self::class, - stdClass::class, - ], - ] - ); - - if (($instance === false) || !is_object($instance) || !($instance instanceof self)) - { - return null; - } - - self::$instance = $instance; - - return self::$instance; - } - - /** - * Wakeup function, called whenever the class is deserialized. - * - * This method does the following: - * - Restart the timer. - * - Reopen the archive file, if one is defined. - * - Seek to the correct offset of the file. - * - * @return void - * @since 4.0.4 - * @internal - */ - public function __wakeup(): void - { - // Reset the timer when deserializing the object. - $this->startTime = microtime(true); - - if (!$this->archiveFileIsBeingRead) - { - return; - } - - $this->fp = @fopen($this->filename, 'rb'); - - if ((is_resource($this->fp)) && ($this->currentOffset > 0)) - { - @fseek($this->fp, $this->currentOffset); - } - } - - /** - * Enforce the minimum execution time. - * - * @return void - * @since 4.0.4 - */ - public function enforceMinimumExecutionTime() - { - $elapsed = $this->getRunningTime() * 1000; - $minExecTime = 1000.0 * min(1, (min(self::MIN_EXEC_TIME, $this->getPhpMaxExecTime()) - 1)); - - // Only run a sleep delay if we haven't reached the minimum execution time - if (($minExecTime <= $elapsed) || ($elapsed <= 0)) - { - return; - } - - $sleepMillisec = intval($minExecTime - $elapsed); - - /** - * If we need to sleep for more than 1 second we should be using sleep() or time_sleep_until() to prevent high - * CPU usage, also because some OS might not support sleeping for over 1 second using these functions. In all - * other cases we will try to use usleep or time_nanosleep instead. - */ - $longSleep = $sleepMillisec > 1000; - $miniSleepSupported = function_exists('usleep') || function_exists('time_nanosleep'); - - if (!$longSleep && $miniSleepSupported) - { - if (function_exists('usleep') && ($sleepMillisec < 1000)) - { - usleep(1000 * $sleepMillisec); - - return; - } - - if (function_exists('time_nanosleep') && ($sleepMillisec < 1000)) - { - time_nanosleep(0, 1000000 * $sleepMillisec); - - return; - } - } - - if (function_exists('sleep')) - { - sleep(ceil($sleepMillisec / 1000)); - - return; - } - - if (function_exists('time_sleep_until')) - { - time_sleep_until(time() + ceil($sleepMillisec / 1000)); - } - } - - /** - * Set the filepath to the ZIP archive which will be extracted. - * - * @param string $value The filepath to the archive. Only LOCAL files are allowed! - * - * @return void - * @since 4.0.4 - */ - public function setFilename(string $value) - { - // Security check: disallow remote filenames - if (!empty($value) && strpos($value, '://') !== false) - { - $this->setError('Invalid archive location'); - - return; - } - - $this->filename = $value; - $this->initializeLog(dirname($this->filename)); - } - - /** - * Sets the path to prefix all extracted files with. Essentially, where the archive will be extracted to. - * - * @param string $addPath The path where the archive will be extracted. - * - * @return void - * @since 4.0.4 - */ - public function setAddPath(string $addPath): void - { - $this->addPath = $addPath; - $this->addPath = str_replace('\\', '/', $this->addPath); - $this->addPath = rtrim($this->addPath, '/'); - - if (!empty($this->addPath)) - { - $this->addPath .= '/'; - } - } - - /** - * Set the list of files to skip when extracting the ZIP file. - * - * @param array $skipFiles A list of files to skip when extracting the ZIP archive - * - * @return void - * @since 4.0.4 - */ - public function setSkipFiles(array $skipFiles): void - { - $this->skipFiles = array_values($skipFiles); - } - - /** - * Set the directories to skip over when extracting the ZIP archive - * - * @param array $ignoreDirectories The list of directories to ignore. - * - * @return void - * @since 4.0.4 - */ - public function setIgnoreDirectories(array $ignoreDirectories): void - { - $this->ignoreDirectories = array_values($ignoreDirectories); - } - - /** - * Prepares for the archive extraction - * - * @return void - * @since 4.0.4 - */ - public function initialize(): void - { - $this->debugMsg(sprintf('Initializing extraction. Filepath: %s', $this->filename)); - $this->totalSize = @filesize($this->filename) ?: 0; - $this->archiveFileIsBeingRead = false; - $this->currentOffset = 0; - $this->runState = self::AK_STATE_NOFILE; - - $this->readArchiveHeader(); - - if (!empty($this->getError())) - { - $this->debugMsg(sprintf('Error: %s', $this->getError()), self::LOG_ERROR); - - return; - } - - $this->archiveFileIsBeingRead = true; - $this->runState = self::AK_STATE_NOFILE; - - $this->debugMsg('Setting state to NOFILE', self::LOG_DEBUG); - } - - /** - * Executes a step of the archive extraction - * - * @return boolean True if we are done extracting or an error occurred - * @since 4.0.4 - */ - public function step(): bool - { - $status = true; - - $this->debugMsg('Starting a new step', self::LOG_INFO); - - while ($status && ($this->getTimeLeft() > 0)) - { - switch ($this->runState) - { - case self::AK_STATE_INITIALIZE: - $this->debugMsg('Current run state: INITIALIZE', self::LOG_DEBUG); - $this->initialize(); - break; - - case self::AK_STATE_NOFILE: - $this->debugMsg('Current run state: NOFILE', self::LOG_DEBUG); - $status = $this->readFileHeader(); - - if ($status) - { - $this->debugMsg('Found file header; updating number of files processed and bytes in/out', self::LOG_DEBUG); - - // Update running tallies when we start extracting a file - $this->filesProcessed++; - $this->compressedTotal += array_key_exists('compressed', get_object_vars($this->fileHeader)) - ? $this->fileHeader->compressed : 0; - $this->uncompressedTotal += $this->fileHeader->uncompressed; - } - - break; - - case self::AK_STATE_HEADER: - case self::AK_STATE_DATA: - $runStateHuman = $this->runState === self::AK_STATE_HEADER ? 'HEADER' : 'DATA'; - $this->debugMsg(sprintf('Current run state: %s', $runStateHuman), self::LOG_DEBUG); - - $status = $this->processFileData(); - break; - - case self::AK_STATE_DATAREAD: - case self::AK_STATE_POSTPROC: - $runStateHuman = $this->runState === self::AK_STATE_DATAREAD ? 'DATAREAD' : 'POSTPROC'; - $this->debugMsg(sprintf('Current run state: %s', $runStateHuman), self::LOG_DEBUG); - - $this->setLastExtractedFileTimestamp($this->fileHeader->timestamp); - $this->processLastExtractedFile(); - - $status = true; - $this->runState = self::AK_STATE_DONE; - break; - - case self::AK_STATE_DONE: - default: - $this->debugMsg('Current run state: DONE', self::LOG_DEBUG); - $this->runState = self::AK_STATE_NOFILE; - - break; - - case self::AK_STATE_FINISHED: - $this->debugMsg('Current run state: FINISHED', self::LOG_DEBUG); - $status = false; - break; - } - - if ($this->getTimeLeft() <= 0) - { - $this->debugMsg('Ran out of time; the step will break.'); - } - elseif (!$status) - { - $this->debugMsg('Last step status is false; the step will break.'); - } - } - - $error = $this->getError() ?? null; - - if (!empty($error)) - { - $this->debugMsg(sprintf('Step failed with error: %s', $error), self::LOG_ERROR); - } - - // Did we just finish or run into an error? - if (!empty($error) || $this->runState === self::AK_STATE_FINISHED) - { - $this->debugMsg('Returning true (must stop running) from step()', self::LOG_DEBUG); - - // Reset internal state, prevents __wakeup from trying to open a non-existent file - $this->archiveFileIsBeingRead = false; - - return true; - } - - $this->debugMsg('Returning false (must continue running) from step()', self::LOG_DEBUG); - - return false; - } - - /** - * Get the most recent error message - * - * @return string|null The message string, null if there's no error - * @since 4.0.4 - */ - public function getError(): ?string - { - return $this->lastErrorMessage; - } - - /** - * Gets the number of seconds left, before we hit the "must break" threshold - * - * @return float - * @since 4.0.4 - */ - private function getTimeLeft(): float - { - return $this->maxExecTime - $this->getRunningTime(); - } - - /** - * Gets the time elapsed since object creation/unserialization, effectively how - * long Akeeba Engine has been processing data - * - * @return float - * @since 4.0.4 - */ - private function getRunningTime(): float - { - return microtime(true) - $this->startTime; - } - - /** - * Process the last extracted file or directory - * - * This invalidates OPcache for .php files. Also applies the correct permissions and timestamp. - * - * @return void - * @since 4.0.4 - */ - private function processLastExtractedFile(): void - { - $this->debugMsg(sprintf('Processing last extracted entity: %s', $this->lastExtractedFilename), self::LOG_DEBUG); - - if (@is_file($this->lastExtractedFilename)) - { - @chmod($this->lastExtractedFilename, 0644); - - clearFileInOPCache($this->lastExtractedFilename); - } - else - { - @chmod($this->lastExtractedFilename, 0755); - } - - if ($this->lastExtractedFileTimestamp > 0) - { - @touch($this->lastExtractedFilename, $this->lastExtractedFileTimestamp); - } - } - - /** - * Set the last extracted filename - * - * @param string|null $lastExtractedFilename The last extracted filename - * - * @return void - * @since 4.0.4 - */ - private function setLastExtractedFilename(?string $lastExtractedFilename): void - { - $this->lastExtractedFilename = $lastExtractedFilename; - } - - /** - * Set the last modification UNIX timestamp for the last extracted file - * - * @param int $lastExtractedFileTimestamp The timestamp - * - * @return void - * @since 4.0.4 - */ - private function setLastExtractedFileTimestamp(int $lastExtractedFileTimestamp): void - { - $this->lastExtractedFileTimestamp = $lastExtractedFileTimestamp; - } - - /** - * Sleep function, called whenever the class is serialized - * - * @return void - * @since 4.0.4 - * @internal - */ - private function shutdown(): void - { - if (is_resource(self::$logFP)) - { - @fclose(self::$logFP); - } - - if (!is_resource($this->fp)) - { - return; - } - - $this->currentOffset = @ftell($this->fp); - - @fclose($this->fp); - } - - /** - * Unicode-safe binary data length - * - * @param string|null $string The binary data to get the length for - * - * @return integer - * @since 4.0.4 - */ - private function binStringLength(?string $string): int - { - if (is_null($string)) - { - return 0; - } - - if (function_exists('mb_strlen')) - { - return mb_strlen($string, '8bit') ?: 0; - } - - return strlen($string) ?: 0; - } - - /** - * Add an error message - * - * @param string $error Error message - * - * @return void - * @since 4.0.4 - */ - private function setError(string $error): void - { - $this->lastErrorMessage = $error; - } - - /** - * Reads data from the archive. - * - * @param resource $fp The file pointer to read data from - * @param int|null $length The volume of data to read, in bytes - * - * @return string The data read from the file - * @since 4.0.4 - */ - private function fread($fp, ?int $length = null): string - { - $readLength = (is_numeric($length) && ($length > 0)) ? $length : PHP_INT_MAX; - $data = fread($fp, $readLength); - - if ($data === false) - { - $this->debugMsg('No more data could be read from the file', self::LOG_WARNING); - - $data = ''; - } - - return $data; - } - - /** - * Read the header of the archive, making sure it's a valid ZIP file. - * - * @return void - * @since 4.0.4 - */ - private function readArchiveHeader(): void - { - $this->debugMsg('Reading the archive header.', self::LOG_DEBUG); - - // Open the first part - $this->openArchiveFile(); - - // Fail for unreadable files - if ($this->fp === false) - { - return; - } - - // Read the header data. - $sigBinary = fread($this->fp, 4); - $headerData = unpack('Vsig', $sigBinary); - - // We only support single part ZIP files - if ($headerData['sig'] != 0x04034b50) - { - $this->setError('The archive file is corrupt: bad header'); - - return; - } - - // Roll back the file pointer - fseek($this->fp, -4, SEEK_CUR); - - $this->currentOffset = @ftell($this->fp); - $this->dataReadLength = 0; - - } - - /** - * Concrete classes must use this method to read the file header - * - * @return boolean True if reading the file was successful, false if an error occurred or we - * reached end of archive. - * @since 4.0.4 - */ - private function readFileHeader(): bool - { - $this->debugMsg('Reading the file entry header.', self::LOG_DEBUG); - - if (!is_resource($this->fp)) - { - $this->setError('The archive is not open for reading.'); - - return false; - } - - // Unexpected end of file - if ($this->isEOF()) - { - $this->debugMsg('EOF when reading file header data', self::LOG_WARNING); - $this->setError('The archive is corrupt or truncated'); - - return false; - } - - $this->currentOffset = ftell($this->fp); - - if ($this->expectDataDescriptor) - { - $this->debugMsg('Expecting data descriptor (bit 3 of general purpose flag was set).', self::LOG_DEBUG); - - /** - * The last file had bit 3 of the general purpose bit flag set. This means that we have a 12 byte data - * descriptor we need to skip. To make things worse, there might also be a 4 byte optional data descriptor - * header (0x08074b50). - */ - $junk = @fread($this->fp, 4); - $junk = unpack('Vsig', $junk); - $readLength = ($junk['sig'] == 0x08074b50) ? 12 : 8; - $junk = @fread($this->fp, $readLength); - - // And check for EOF, too - if ($this->isEOF()) - { - $this->debugMsg('EOF when reading data descriptor', self::LOG_WARNING); - $this->setError('The archive is corrupt or truncated'); - - return false; - } - } - - // Get and decode Local File Header - $headerBinary = fread($this->fp, 30); - $format = 'Vsig/C2ver/vbitflag/vcompmethod/vlastmodtime/vlastmoddate/Vcrc/Vcompsize/' - . 'Vuncomp/vfnamelen/veflen'; - $headerData = unpack($format, $headerBinary); - - // Check signature - if (!($headerData['sig'] == 0x04034b50)) - { - // The signature is not the one used for files. Is this a central directory record (i.e. we're done)? - if ($headerData['sig'] == 0x02014b50) - { - $this->debugMsg('Found Central Directory header; the extraction is complete', self::LOG_DEBUG); - - // End of ZIP file detected. We'll just skip to the end of file... - @fseek($this->fp, 0, SEEK_END); - $this->runState = self::AK_STATE_FINISHED; - - return false; - } - - $this->setError('The archive file is corrupt or truncated'); - - return false; - } - - // If bit 3 of the bitflag is set, expectDataDescriptor is true - $this->expectDataDescriptor = ($headerData['bitflag'] & 4) == 4; - $this->fileHeader = new stdClass; - $this->fileHeader->timestamp = 0; - - // Read the last modified date and time - $lastmodtime = $headerData['lastmodtime']; - $lastmoddate = $headerData['lastmoddate']; - - if ($lastmoddate && $lastmodtime) - { - $vHour = ($lastmodtime & 0xF800) >> 11; - $vMInute = ($lastmodtime & 0x07E0) >> 5; - $vSeconds = ($lastmodtime & 0x001F) * 2; - $vYear = (($lastmoddate & 0xFE00) >> 9) + 1980; - $vMonth = ($lastmoddate & 0x01E0) >> 5; - $vDay = $lastmoddate & 0x001F; - - $this->fileHeader->timestamp = @mktime($vHour, $vMInute, $vSeconds, $vMonth, $vDay, $vYear); - } - - $isBannedFile = false; - - $this->fileHeader->compressed = $headerData['compsize']; - $this->fileHeader->uncompressed = $headerData['uncomp']; - $nameFieldLength = $headerData['fnamelen']; - $extraFieldLength = $headerData['eflen']; - - // Read filename field - $this->fileHeader->file = fread($this->fp, $nameFieldLength); - - // Read extra field if present - if ($extraFieldLength > 0) - { - $extrafield = fread($this->fp, $extraFieldLength); - } - - // Decide filetype -- Check for directories - $this->fileHeader->type = 'file'; - - if (strrpos($this->fileHeader->file, '/') == strlen($this->fileHeader->file) - 1) - { - $this->fileHeader->type = 'dir'; - } - - // Decide filetype -- Check for symbolic links - if (($headerData['ver1'] == 10) && ($headerData['ver2'] == 3)) - { - $this->fileHeader->type = 'link'; - } - - switch ($headerData['compmethod']) - { - case 0: - $this->fileHeader->compression = 'none'; - break; - case 8: - $this->fileHeader->compression = 'gzip'; - break; - default: - $messageTemplate = 'This script cannot handle ZIP compression method %d. ' - . 'Only 0 (no compression) and 8 (DEFLATE, gzip) can be handled.'; - $actualMessage = sprintf($messageTemplate, $headerData['compmethod']); - $this->setError($actualMessage); - - return false; - break; - } - - // Find hard-coded banned files - if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) - { - $isBannedFile = true; - } - - // Also try to find banned files passed in class configuration - if ((count($this->skipFiles) > 0) && in_array($this->fileHeader->file, $this->skipFiles)) - { - $isBannedFile = true; - } - - // If we have a banned file, let's skip it - if ($isBannedFile) - { - $debugMessage = sprintf('Current entity (%s) is banned from extraction and will be skipped over.', $this->fileHeader->file); - $this->debugMsg($debugMessage, self::LOG_DEBUG); - - // Advance the file pointer, skipping exactly the size of the compressed data - $seekleft = $this->fileHeader->compressed; - - while ($seekleft > 0) - { - // Ensure that we can seek past archive part boundaries - $curSize = @filesize($this->filename); - $curPos = @ftell($this->fp); - $canSeek = $curSize - $curPos; - $canSeek = ($canSeek > $seekleft) ? $seekleft : $canSeek; - @fseek($this->fp, $canSeek, SEEK_CUR); - $seekleft -= $canSeek; - - if ($seekleft) - { - $this->setError('The archive is corrupt or truncated'); - - return false; - } - } - - $this->currentOffset = @ftell($this->fp); - $this->runState = self::AK_STATE_DONE; - - return true; - } - - // Last chance to prepend a path to the filename - if (!empty($this->addPath)) - { - $this->fileHeader->file = $this->addPath . $this->fileHeader->file; - } - - // Get the translated path name - if ($this->fileHeader->type == 'file') - { - $this->fileHeader->realFile = $this->fileHeader->file; - $this->setLastExtractedFilename($this->fileHeader->file); - } - elseif ($this->fileHeader->type == 'dir') - { - $this->fileHeader->timestamp = 0; - - $dir = $this->fileHeader->file; - - if (!@is_dir($dir)) - { - mkdir($dir, 0755, true); - } - - $this->setLastExtractedFilename(null); - } - else - { - // Symlink; do not post-process - $this->fileHeader->timestamp = 0; - $this->setLastExtractedFilename(null); - } - - $this->createDirectory(); - - // Header is read - $this->runState = self::AK_STATE_HEADER; - - return true; - } - - /** - * Creates the directory this file points to - * - * @return void - * @since 4.0.4 - */ - private function createDirectory(): void - { - // Do we need to create a directory? - if (empty($this->fileHeader->realFile)) - { - $this->fileHeader->realFile = $this->fileHeader->file; - } - - $lastSlash = strrpos($this->fileHeader->realFile, '/'); - $dirName = substr($this->fileHeader->realFile, 0, $lastSlash); - $perms = 0755; - $ignore = $this->isIgnoredDirectory($dirName); - - if (@is_dir($dirName)) - { - return; - } - - if ((@mkdir($dirName, $perms, true) === false) && (!$ignore)) - { - $this->setError(sprintf('Could not create %s folder', $dirName)); - } - - } - - /** - * Concrete classes must use this method to process file data. It must set $runState to self::AK_STATE_DATAREAD when - * it's finished processing the file data. - * - * @return boolean True if processing the file data was successful, false if an error occurred - * @since 4.0.4 - */ - private function processFileData(): bool - { - switch ($this->fileHeader->type) - { - case 'dir': - $this->debugMsg('Extracting entity of type Directory', self::LOG_DEBUG); - - return $this->processTypeDir(); - break; - - case 'link': - $this->debugMsg('Extracting entity of type Symbolic Link', self::LOG_DEBUG); - - return $this->processTypeLink(); - break; - - case 'file': - switch ($this->fileHeader->compression) - { - case 'none': - $this->debugMsg('Extracting entity of type File (Stored)', self::LOG_DEBUG); - - return $this->processTypeFileUncompressed(); - break; - - case 'gzip': - case 'bzip2': - $this->debugMsg('Extracting entity of type File (Compressed)', self::LOG_DEBUG); - - return $this->processTypeFileCompressed(); - break; - - case 'default': - $this->setError(sprintf('Unknown compression type %s.', $this->fileHeader->compression)); - - return false; - break; - } - break; - } - - $this->setError(sprintf('Unknown entry type %s.', $this->fileHeader->type)); - - return false; - } - - /** - * Opens the next part file for reading - * - * @return void - * @since 4.0.4 - */ - private function openArchiveFile(): void - { - $this->debugMsg('Opening archive file for reading', self::LOG_DEBUG); - - if ($this->archiveFileIsBeingRead) - { - return; - } - - if (is_resource($this->fp)) - { - @fclose($this->fp); - } - - $this->fp = @fopen($this->filename, 'rb'); - - if ($this->fp === false) - { - $message = 'Could not open archive for reading. Check that the file exists, is ' - . 'readable by the web server and is not in a directory made out of reach by chroot, ' - . 'open_basedir restrictions or any other restriction put in place by your host.'; - $this->setError($message); - - return; - } - - fseek($this->fp, 0); - $this->currentOffset = 0; - - } - - /** - * Returns true if we have reached the end of file - * - * @return boolean True if we have reached End Of File - * @since 4.0.4 - */ - private function isEOF(): bool - { - /** - * feof() will return false if the file pointer is exactly at the last byte of the file. However, this is a - * condition we want to treat as a proper EOF for the purpose of extracting a ZIP file. Hence the second part - * after the logical OR. - */ - return @feof($this->fp) || (@ftell($this->fp) > @filesize($this->filename)); - } - - /** - * Handles the permissions of the parent directory to a file and the file itself to make it writeable. - * - * @param string $path A path to a file - * - * @return void - * @since 4.0.4 - */ - private function setCorrectPermissions(string $path): void - { - static $rootDir = null; - - if (is_null($rootDir)) - { - $rootDir = rtrim($this->addPath, '/\\'); - } - - $directory = rtrim(dirname($path), '/\\'); - - // Is this an unwritable directory? - if (($directory != $rootDir) && !is_writeable($directory)) - { - @chmod($directory, 0755); - } - - @chmod($path, 0644); - } - - /** - * Is this file or directory contained in a directory we've decided to ignore - * write errors for? This is useful to let the extraction work despite write - * errors in the log, logs and tmp directories which MIGHT be used by the system - * on some low quality hosts and Plesk-powered hosts. - * - * @param string $shortFilename The relative path of the file/directory in the package - * - * @return boolean True if it belongs in an ignored directory - * @since 4.0.4 - */ - private function isIgnoredDirectory(string $shortFilename): bool - { - $check = substr($shortFilename, -1) == '/' ? rtrim($shortFilename, '/') : dirname($shortFilename); - - return in_array($check, $this->ignoreDirectories); - } - - /** - * Process the file data of a directory entry - * - * @return boolean - * @since 4.0.4 - */ - private function processTypeDir(): bool - { - // Directory entries do not have file data, therefore we're done processing the entry. - $this->runState = self::AK_STATE_DATAREAD; - - return true; - } - - /** - * Process the file data of a link entry - * - * @return boolean - * @since 4.0.4 - */ - private function processTypeLink(): bool - { - $toReadBytes = 0; - $leftBytes = $this->fileHeader->compressed; - $data = ''; - - while ($leftBytes > 0) - { - $toReadBytes = min($leftBytes, self::CHUNK_SIZE); - $mydata = $this->fread($this->fp, $toReadBytes); - $reallyReadBytes = $this->binStringLength($mydata); - $data .= $mydata; - $leftBytes -= $reallyReadBytes; - - if ($reallyReadBytes < $toReadBytes) - { - // We read less than requested! - if ($this->isEOF()) - { - $this->debugMsg('EOF when reading symlink data', self::LOG_WARNING); - $this->setError('The archive file is corrupt or truncated'); - - return false; - } - } - } - - $filename = $this->fileHeader->realFile ?? $this->fileHeader->file; - - // Try to remove an existing file or directory by the same name - if (file_exists($filename)) - { - clearFileInOPCache($filename); - @unlink($filename); - @rmdir($filename); - } - - // Remove any trailing slash - if (substr($filename, -1) == '/') - { - $filename = substr($filename, 0, -1); - } - - // Create the symlink - @symlink($data, $filename); - - $this->runState = self::AK_STATE_DATAREAD; - - // No matter if the link was created! - return true; - } - - /** - * Processes an uncompressed (stored) file - * - * @return boolean - * @since 4.0.4 - */ - private function processTypeFileUncompressed(): bool - { - // Uncompressed files are being processed in small chunks, to avoid timeouts - if ($this->dataReadLength == 0) - { - // Before processing file data, ensure permissions are adequate - $this->setCorrectPermissions($this->fileHeader->file); - } - - // Open the output file - $ignore = $this->isIgnoredDirectory($this->fileHeader->file); - - $writeMode = ($this->dataReadLength == 0) ? 'wb' : 'ab'; - $outfp = @fopen($this->fileHeader->realFile, $writeMode); - - // Can we write to the file? - if (($outfp === false) && (!$ignore)) - { - // An error occurred - $this->setError(sprintf('Could not open %s for writing.', $this->fileHeader->realFile)); - - return false; - } - - // Does the file have any data, at all? - if ($this->fileHeader->compressed == 0) - { - // No file data! - if (is_resource($outfp)) - { - @fclose($outfp); - } - - $this->debugMsg('Zero byte Stored file; no data will be read', self::LOG_DEBUG); - - $this->runState = self::AK_STATE_DATAREAD; - - return true; - } - - $leftBytes = $this->fileHeader->compressed - $this->dataReadLength; - - // Loop while there's data to read and enough time to do it - while (($leftBytes > 0) && ($this->getTimeLeft() > 0)) - { - $toReadBytes = min($leftBytes, self::CHUNK_SIZE); - $data = $this->fread($this->fp, $toReadBytes); - $reallyReadBytes = $this->binStringLength($data); - $leftBytes -= $reallyReadBytes; - $this->dataReadLength += $reallyReadBytes; - - if ($reallyReadBytes < $toReadBytes) - { - // We read less than requested! Why? Did we hit local EOF? - if ($this->isEOF()) - { - // Nope. The archive is corrupt - $this->debugMsg('EOF when reading stored file data', self::LOG_WARNING); - $this->setError('The archive file is corrupt or truncated'); - - return false; - } - } - - if (is_resource($outfp)) - { - @fwrite($outfp, $data); - } - - if ($this->getTimeLeft()) - { - $this->debugMsg('Out of time; will resume extraction in the next step', self::LOG_DEBUG); - } - } - - // Close the file pointer - if (is_resource($outfp)) - { - @fclose($outfp); - } - - // Was this a pre-timeout bail out? - if ($leftBytes > 0) - { - $this->debugMsg(sprintf('We have %d bytes left to extract in the next step', $leftBytes), self::LOG_DEBUG); - $this->runState = self::AK_STATE_DATA; - - return true; - } - - // Oh! We just finished! - $this->runState = self::AK_STATE_DATAREAD; - $this->dataReadLength = 0; - - return true; - } - - /** - * Processes a compressed file - * - * @return boolean - * @since 4.0.4 - */ - private function processTypeFileCompressed(): bool - { - // Before processing file data, ensure permissions are adequate - $this->setCorrectPermissions($this->fileHeader->file); - - // Open the output file - $outfp = @fopen($this->fileHeader->realFile, 'wb'); - - // Can we write to the file? - $ignore = $this->isIgnoredDirectory($this->fileHeader->file); - - if (($outfp === false) && (!$ignore)) - { - // An error occurred - $this->setError(sprintf('Could not open %s for writing.', $this->fileHeader->realFile)); - - return false; - } - - // Does the file have any data, at all? - if ($this->fileHeader->compressed == 0) - { - $this->debugMsg('Zero byte Compressed file; no data will be read', self::LOG_DEBUG); - - // No file data! - if (is_resource($outfp)) - { - @fclose($outfp); - } - - $this->runState = self::AK_STATE_DATAREAD; - - return true; - } - - // Simple compressed files are processed as a whole; we can't do chunk processing - $zipData = $this->fread($this->fp, $this->fileHeader->compressed); - - while ($this->binStringLength($zipData) < $this->fileHeader->compressed) - { - // End of local file before reading all data? - if ($this->isEOF()) - { - $this->debugMsg('EOF reading compressed data', self::LOG_WARNING); - $this->setError('The archive file is corrupt or truncated'); - - return false; - } - } - - switch ($this->fileHeader->compression) - { - case 'gzip': - /** @noinspection PhpComposerExtensionStubsInspection */ - $unzipData = gzinflate($zipData); - break; - - case 'bzip2': - /** @noinspection PhpComposerExtensionStubsInspection */ - $unzipData = bzdecompress($zipData); - break; - - default: - $this->setError(sprintf('Unknown compression method %s', $this->fileHeader->compression)); - - return false; - break; - } - - unset($zipData); - - // Write to the file. - if (is_resource($outfp)) - { - @fwrite($outfp, $unzipData, $this->fileHeader->uncompressed); - @fclose($outfp); - } - - unset($unzipData); - - $this->runState = self::AK_STATE_DATAREAD; - - return true; - } - - /** - * Set up the maximum execution time - * - * @return void - * @since 4.0.4 - */ - private function setupMaxExecTime(): void - { - $configMaxTime = self::MAX_EXEC_TIME; - $bias = self::RUNTIME_BIAS / 100; - $this->maxExecTime = min($this->getPhpMaxExecTime(), $configMaxTime) * $bias; - } - - /** - * Get the PHP maximum execution time. - * - * If it's not defined or it's zero (infinite) we use a fake value of 10 seconds. - * - * @return integer - * @since 4.0.4 - */ - private function getPhpMaxExecTime(): int - { - if (!@function_exists('ini_get')) - { - return 10; - } - - $phpMaxTime = @ini_get("maximum_execution_time"); - $phpMaxTime = (!is_numeric($phpMaxTime) ? 10 : @intval($phpMaxTime)) ?: 10; - - return max(1, $phpMaxTime); - } - - /** - * Write a message to the debug error log - * - * @param string $message The message to log - * @param int $priority The message's log priority - * - * @return void - * @since 4.0.4 - */ - private function debugMsg(string $message, int $priority = self::LOG_INFO): void - { - if (!defined('_JOOMLA_UPDATE_DEBUG')) - { - return; - } - - if (!is_resource(self::$logFP) && !is_bool(self::$logFP)) - { - self::$logFP = @fopen(self::$logFilePath, 'at'); - } - - if (!is_resource(self::$logFP)) - { - return; - } - - switch ($priority) - { - case self::LOG_DEBUG: - $priorityString = 'DEBUG'; - break; - - case self::LOG_INFO: - $priorityString = 'INFO'; - break; - - case self::LOG_WARNING: - $priorityString = 'WARNING'; - break; - - case self::LOG_ERROR: - $priorityString = 'ERROR'; - break; - } - - fputs(self::$logFP, sprintf('%s | %7s | %s' . "\r\n", gmdate('Y-m-d H:i:s'), $priorityString, $message)); - } - - /** - * Initialise the debug log file - * - * @param string $logPath The path where the log file will be written to - * - * @return void - * @since 4.0.4 - */ - private function initializeLog(string $logPath): void - { - if (!defined('_JOOMLA_UPDATE_DEBUG')) - { - return; - } - - $logPath = $logPath ?: dirname($this->filename); - $logFile = rtrim($logPath, '/' . DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'joomla_update.txt'; - - self::$logFilePath = $logFile; - } + /** + * How much data to read at once when processing files + * + * @var int + * @since 4.0.4 + */ + private const CHUNK_SIZE = 524288; + + /** + * Maximum execution time (seconds). + * + * Each page load will take at most this much time. Please note that if the ZIP archive contains fairly large, + * compressed files we may overshoot this time since we can't interrupt the decompression. This should not be an + * issue in the context of updating Joomla as the ZIP archive contains fairly small files. + * + * If this is too low it will cause too many requests to hit the server, potentially triggering a DoS protection and + * causing the extraction to fail. If this is too big the extraction will not be as verbose and the user might think + * something is broken. A value between 3 and 7 seconds is, therefore, recommended. + * + * @var int + * @since 4.0.4 + */ + private const MAX_EXEC_TIME = 4; + + /** + * Run-time execution bias (percentage points). + * + * We evaluate the time remaining on the timer before processing each file on the ZIP archive. If we have already + * consumed at least this much percentage of the MAX_EXEC_TIME we will stop processing the archive in this page + * load, return the result to the client and wait for it to call us again so we can resume the extraction. + * + * This becomes important when the MAX_EXEC_TIME is close to the PHP, PHP-FPM or Apache timeout on the server + * (whichever is lowest) and there are fairly large files in the backup archive. If we start extracting a large, + * compressed file close to a hard server timeout it's possible that we will overshoot that hard timeout and see the + * extraction failing. + * + * Since Joomla Update is used to extract a ZIP archive with many small files we can keep at a fairly high 90% + * without much fear that something will break. + * + * Example: if MAX_EXEC_TIME is 10 seconds and RUNTIME_BIAS is 80 each page load will take between 80% and 100% of + * the MAX_EXEC_TIME, i.e. anywhere between 8 and 10 seconds. + * + * Lower values make it less likely to overshoot MAX_EXEC_TIME when extracting large files. + * + * @var int + * @since 4.0.4 + */ + private const RUNTIME_BIAS = 90; + + /** + * Minimum execution time (seconds). + * + * A request cannot take less than this many seconds. If it does, we add “dead time” (sleep) where the script does + * nothing except wait. This is essentially a rate limiting feature to avoid hitting a server-side DoS protection + * which could be triggered if we ended up sending too many requests in a limited amount of time. + * + * This should normally be less than MAX_EXEC * (RUNTIME_BIAS / 100). Values between that and MAX_EXEC_TIME have the + * effect of almost always adding dead time in each request, unless a really large file is being extracted from the + * ZIP archive. Values larger than MAX_EXEC will always add dead time to the request. This can be useful to + * artificially reduce the CPU usage limit. Some servers might kill the request if they see a sustained CPU usage + * spike over a short period of time. + * + * The chosen value of 3 seconds belongs to the first category, essentially making sure that we have a decent rate + * limiting without annoying the user too much but also catering for the most badly configured of shared + * hosting. It's a happy medium which works for the majority (~90%) of commercial servers out there. + * + * @var int + * @since 4.0.4 + */ + private const MIN_EXEC_TIME = 3; + + /** + * Internal state when extracting files: we need to be initialised + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_INITIALIZE = -1; + + /** + * Internal state when extracting files: no file currently being extracted + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_NOFILE = 0; + + /** + * Internal state when extracting files: reading the file header + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_HEADER = 1; + + /** + * Internal state when extracting files: reading file data + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_DATA = 2; + + /** + * Internal state when extracting files: file data has been read thoroughly + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_DATAREAD = 3; + + /** + * Internal state when extracting files: post-processing the file + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_POSTPROC = 4; + + /** + * Internal state when extracting files: done with this file + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_DONE = 5; + + /** + * Internal state when extracting files: finished extracting the ZIP file + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_FINISHED = 999; + + /** + * Internal logging level: debug + * + * @var int + * @since 4.0.4 + */ + private const LOG_DEBUG = 1; + + /** + * Internal logging level: information + * + * @var int + * @since 4.0.4 + */ + private const LOG_INFO = 10; + + /** + * Internal logging level: warning + * + * @var int + * @since 4.0.4 + */ + private const LOG_WARNING = 50; + + /** + * Internal logging level: error + * + * @var int + * @since 4.0.4 + */ + private const LOG_ERROR = 90; + + /** + * Singleton instance + * + * @var null|self + * @since 4.0.4 + */ + private static $instance = null; + + /** + * Debug log file pointer resource + * + * @var null|resource|boolean + * @since 4.0.4 + */ + private static $logFP = null; + + /** + * Debug log filename + * + * @var null|string + * @since 4.0.4 + */ + private static $logFilePath = null; + + /** + * The total size of the ZIP archive + * + * @var integer + * @since 4.0.4 + */ + public $totalSize = 0; + + /** + * Which files to skip + * + * @var array + * @since 4.0.4 + */ + public $skipFiles = []; + + /** + * Current tally of compressed size read + * + * @var integer + * @since 4.0.4 + */ + public $compressedTotal = 0; + + /** + * Current tally of bytes written to disk + * + * @var integer + * @since 4.0.4 + */ + public $uncompressedTotal = 0; + + /** + * Current tally of files extracted + * + * @var integer + * @since 4.0.4 + */ + public $filesProcessed = 0; + + /** + * Maximum execution time allowance per step + * + * @var integer + * @since 4.0.4 + */ + private $maxExecTime = null; + + /** + * Timestamp of execution start + * + * @var integer + * @since 4.0.4 + */ + private $startTime; + + /** + * The last error message + * + * @var string|null + * @since 4.0.4 + */ + private $lastErrorMessage = null; + + /** + * Archive filename + * + * @var string + * @since 4.0.4 + */ + private $filename = null; + + /** + * Current archive part number + * + * @var boolean + * @since 4.0.4 + */ + private $archiveFileIsBeingRead = false; + + /** + * The offset inside the current part + * + * @var integer + * @since 4.0.4 + */ + private $currentOffset = 0; + + /** + * Absolute path to prepend to extracted files + * + * @var string + * @since 4.0.4 + */ + private $addPath = ''; + + /** + * File pointer to the current archive part file + * + * @var resource|null + * @since 4.0.4 + */ + private $fp = null; + + /** + * Run state when processing the current archive file + * + * @var integer + * @since 4.0.4 + */ + private $runState = self::AK_STATE_INITIALIZE; + + /** + * File header data, as read by the readFileHeader() method + * + * @var stdClass + * @since 4.0.4 + */ + private $fileHeader = null; + + /** + * How much of the uncompressed data we've read so far + * + * @var integer + * @since 4.0.4 + */ + private $dataReadLength = 0; + + /** + * Unwritable files in these directories are always ignored and do not cause errors when not + * extracted. + * + * @var array + * @since 4.0.4 + */ + private $ignoreDirectories = []; + + /** + * Internal flag, set when the ZIP file has a data descriptor (which we will be ignoring) + * + * @var boolean + * @since 4.0.4 + */ + private $expectDataDescriptor = false; + + /** + * The UNIX last modification timestamp of the file last extracted + * + * @var integer + * @since 4.0.4 + */ + private $lastExtractedFileTimestamp = 0; + + /** + * The file path of the file last extracted + * + * @var string + * @since 4.0.4 + */ + private $lastExtractedFilename = null; + + /** + * Public constructor. + * + * Sets up the internal timer. + * + * @since 4.0.4 + */ + public function __construct() + { + $this->setupMaxExecTime(); + + // Initialize start time + $this->startTime = microtime(true); + } + + /** + * Singleton implementation. + * + * @return static + * @since 4.0.4 + */ + public static function getInstance(): self + { + if (is_null(self::$instance)) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Returns a serialised copy of the object. + * + * This is different to calling serialise() directly. This operates on a copy of the object which undergoes a + * call to shutdown() first so any open files are closed first. + * + * @return string The serialised data, potentially base64 encoded. + * @since 4.0.4 + */ + public static function getSerialised(): string + { + $clone = clone self::getInstance(); + $clone->shutdown(); + $serialized = serialize($clone); + + return (function_exists('base64_encode') && function_exists('base64_decode')) ? base64_encode($serialized) : $serialized; + } + + /** + * Restores a serialised instance into the singleton implementation and returns it. + * + * If the serialised data is corrupt it will return null. + * + * @param string $serialised The serialised data, potentially base64 encoded, to deserialize. + * + * @return static|null The instance of the object, NULL if it cannot be deserialised. + * @since 4.0.4 + */ + public static function unserialiseInstance(string $serialised): ?self + { + if (function_exists('base64_encode') && function_exists('base64_decode')) { + $serialised = base64_decode($serialised); + } + + $instance = @unserialize($serialised, [ + 'allowed_classes' => [ + self::class, + stdClass::class, + ], + ]); + + if (($instance === false) || !is_object($instance) || !($instance instanceof self)) { + return null; + } + + self::$instance = $instance; + + return self::$instance; + } + + /** + * Wakeup function, called whenever the class is deserialized. + * + * This method does the following: + * - Restart the timer. + * - Reopen the archive file, if one is defined. + * - Seek to the correct offset of the file. + * + * @return void + * @since 4.0.4 + * @internal + */ + public function __wakeup(): void + { + // Reset the timer when deserializing the object. + $this->startTime = microtime(true); + + if (!$this->archiveFileIsBeingRead) { + return; + } + + $this->fp = @fopen($this->filename, 'rb'); + + if ((is_resource($this->fp)) && ($this->currentOffset > 0)) { + @fseek($this->fp, $this->currentOffset); + } + } + + /** + * Enforce the minimum execution time. + * + * @return void + * @since 4.0.4 + */ + public function enforceMinimumExecutionTime() + { + $elapsed = $this->getRunningTime() * 1000; + $minExecTime = 1000.0 * min(1, (min(self::MIN_EXEC_TIME, $this->getPhpMaxExecTime()) - 1)); + + // Only run a sleep delay if we haven't reached the minimum execution time + if (($minExecTime <= $elapsed) || ($elapsed <= 0)) { + return; + } + + $sleepMillisec = intval($minExecTime - $elapsed); + + /** + * If we need to sleep for more than 1 second we should be using sleep() or time_sleep_until() to prevent high + * CPU usage, also because some OS might not support sleeping for over 1 second using these functions. In all + * other cases we will try to use usleep or time_nanosleep instead. + */ + $longSleep = $sleepMillisec > 1000; + $miniSleepSupported = function_exists('usleep') || function_exists('time_nanosleep'); + + if (!$longSleep && $miniSleepSupported) { + if (function_exists('usleep') && ($sleepMillisec < 1000)) { + usleep(1000 * $sleepMillisec); + + return; + } + + if (function_exists('time_nanosleep') && ($sleepMillisec < 1000)) { + time_nanosleep(0, 1000000 * $sleepMillisec); + + return; + } + } + + if (function_exists('sleep')) { + sleep(ceil($sleepMillisec / 1000)); + + return; + } + + if (function_exists('time_sleep_until')) { + time_sleep_until(time() + ceil($sleepMillisec / 1000)); + } + } + + /** + * Set the filepath to the ZIP archive which will be extracted. + * + * @param string $value The filepath to the archive. Only LOCAL files are allowed! + * + * @return void + * @since 4.0.4 + */ + public function setFilename(string $value) + { + // Security check: disallow remote filenames + if (!empty($value) && strpos($value, '://') !== false) { + $this->setError('Invalid archive location'); + + return; + } + + $this->filename = $value; + $this->initializeLog(dirname($this->filename)); + } + + /** + * Sets the path to prefix all extracted files with. Essentially, where the archive will be extracted to. + * + * @param string $addPath The path where the archive will be extracted. + * + * @return void + * @since 4.0.4 + */ + public function setAddPath(string $addPath): void + { + $this->addPath = $addPath; + $this->addPath = str_replace('\\', '/', $this->addPath); + $this->addPath = rtrim($this->addPath, '/'); + + if (!empty($this->addPath)) { + $this->addPath .= '/'; + } + } + + /** + * Set the list of files to skip when extracting the ZIP file. + * + * @param array $skipFiles A list of files to skip when extracting the ZIP archive + * + * @return void + * @since 4.0.4 + */ + public function setSkipFiles(array $skipFiles): void + { + $this->skipFiles = array_values($skipFiles); + } + + /** + * Set the directories to skip over when extracting the ZIP archive + * + * @param array $ignoreDirectories The list of directories to ignore. + * + * @return void + * @since 4.0.4 + */ + public function setIgnoreDirectories(array $ignoreDirectories): void + { + $this->ignoreDirectories = array_values($ignoreDirectories); + } + + /** + * Prepares for the archive extraction + * + * @return void + * @since 4.0.4 + */ + public function initialize(): void + { + $this->debugMsg(sprintf('Initializing extraction. Filepath: %s', $this->filename)); + $this->totalSize = @filesize($this->filename) ?: 0; + $this->archiveFileIsBeingRead = false; + $this->currentOffset = 0; + $this->runState = self::AK_STATE_NOFILE; + + $this->readArchiveHeader(); + + if (!empty($this->getError())) { + $this->debugMsg(sprintf('Error: %s', $this->getError()), self::LOG_ERROR); + + return; + } + + $this->archiveFileIsBeingRead = true; + $this->runState = self::AK_STATE_NOFILE; + + $this->debugMsg('Setting state to NOFILE', self::LOG_DEBUG); + } + + /** + * Executes a step of the archive extraction + * + * @return boolean True if we are done extracting or an error occurred + * @since 4.0.4 + */ + public function step(): bool + { + $status = true; + + $this->debugMsg('Starting a new step', self::LOG_INFO); + + while ($status && ($this->getTimeLeft() > 0)) { + switch ($this->runState) { + case self::AK_STATE_INITIALIZE: + $this->debugMsg('Current run state: INITIALIZE', self::LOG_DEBUG); + $this->initialize(); + break; + + case self::AK_STATE_NOFILE: + $this->debugMsg('Current run state: NOFILE', self::LOG_DEBUG); + $status = $this->readFileHeader(); + + if ($status) { + $this->debugMsg('Found file header; updating number of files processed and bytes in/out', self::LOG_DEBUG); + + // Update running tallies when we start extracting a file + $this->filesProcessed++; + $this->compressedTotal += array_key_exists('compressed', get_object_vars($this->fileHeader)) + ? $this->fileHeader->compressed : 0; + $this->uncompressedTotal += $this->fileHeader->uncompressed; + } + + break; + + case self::AK_STATE_HEADER: + case self::AK_STATE_DATA: + $runStateHuman = $this->runState === self::AK_STATE_HEADER ? 'HEADER' : 'DATA'; + $this->debugMsg(sprintf('Current run state: %s', $runStateHuman), self::LOG_DEBUG); + + $status = $this->processFileData(); + break; + + case self::AK_STATE_DATAREAD: + case self::AK_STATE_POSTPROC: + $runStateHuman = $this->runState === self::AK_STATE_DATAREAD ? 'DATAREAD' : 'POSTPROC'; + $this->debugMsg(sprintf('Current run state: %s', $runStateHuman), self::LOG_DEBUG); + + $this->setLastExtractedFileTimestamp($this->fileHeader->timestamp); + $this->processLastExtractedFile(); + + $status = true; + $this->runState = self::AK_STATE_DONE; + break; + + case self::AK_STATE_DONE: + default: + $this->debugMsg('Current run state: DONE', self::LOG_DEBUG); + $this->runState = self::AK_STATE_NOFILE; + + break; + + case self::AK_STATE_FINISHED: + $this->debugMsg('Current run state: FINISHED', self::LOG_DEBUG); + $status = false; + break; + } + + if ($this->getTimeLeft() <= 0) { + $this->debugMsg('Ran out of time; the step will break.'); + } elseif (!$status) { + $this->debugMsg('Last step status is false; the step will break.'); + } + } + + $error = $this->getError() ?? null; + + if (!empty($error)) { + $this->debugMsg(sprintf('Step failed with error: %s', $error), self::LOG_ERROR); + } + + // Did we just finish or run into an error? + if (!empty($error) || $this->runState === self::AK_STATE_FINISHED) { + $this->debugMsg('Returning true (must stop running) from step()', self::LOG_DEBUG); + + // Reset internal state, prevents __wakeup from trying to open a non-existent file + $this->archiveFileIsBeingRead = false; + + return true; + } + + $this->debugMsg('Returning false (must continue running) from step()', self::LOG_DEBUG); + + return false; + } + + /** + * Get the most recent error message + * + * @return string|null The message string, null if there's no error + * @since 4.0.4 + */ + public function getError(): ?string + { + return $this->lastErrorMessage; + } + + /** + * Gets the number of seconds left, before we hit the "must break" threshold + * + * @return float + * @since 4.0.4 + */ + private function getTimeLeft(): float + { + return $this->maxExecTime - $this->getRunningTime(); + } + + /** + * Gets the time elapsed since object creation/unserialization, effectively how + * long Akeeba Engine has been processing data + * + * @return float + * @since 4.0.4 + */ + private function getRunningTime(): float + { + return microtime(true) - $this->startTime; + } + + /** + * Process the last extracted file or directory + * + * This invalidates OPcache for .php files. Also applies the correct permissions and timestamp. + * + * @return void + * @since 4.0.4 + */ + private function processLastExtractedFile(): void + { + $this->debugMsg(sprintf('Processing last extracted entity: %s', $this->lastExtractedFilename), self::LOG_DEBUG); + + if (@is_file($this->lastExtractedFilename)) { + @chmod($this->lastExtractedFilename, 0644); + + clearFileInOPCache($this->lastExtractedFilename); + } else { + @chmod($this->lastExtractedFilename, 0755); + } + + if ($this->lastExtractedFileTimestamp > 0) { + @touch($this->lastExtractedFilename, $this->lastExtractedFileTimestamp); + } + } + + /** + * Set the last extracted filename + * + * @param string|null $lastExtractedFilename The last extracted filename + * + * @return void + * @since 4.0.4 + */ + private function setLastExtractedFilename(?string $lastExtractedFilename): void + { + $this->lastExtractedFilename = $lastExtractedFilename; + } + + /** + * Set the last modification UNIX timestamp for the last extracted file + * + * @param int $lastExtractedFileTimestamp The timestamp + * + * @return void + * @since 4.0.4 + */ + private function setLastExtractedFileTimestamp(int $lastExtractedFileTimestamp): void + { + $this->lastExtractedFileTimestamp = $lastExtractedFileTimestamp; + } + + /** + * Sleep function, called whenever the class is serialized + * + * @return void + * @since 4.0.4 + * @internal + */ + private function shutdown(): void + { + if (is_resource(self::$logFP)) { + @fclose(self::$logFP); + } + + if (!is_resource($this->fp)) { + return; + } + + $this->currentOffset = @ftell($this->fp); + + @fclose($this->fp); + } + + /** + * Unicode-safe binary data length + * + * @param string|null $string The binary data to get the length for + * + * @return integer + * @since 4.0.4 + */ + private function binStringLength(?string $string): int + { + if (is_null($string)) { + return 0; + } + + if (function_exists('mb_strlen')) { + return mb_strlen($string, '8bit') ?: 0; + } + + return strlen($string) ?: 0; + } + + /** + * Add an error message + * + * @param string $error Error message + * + * @return void + * @since 4.0.4 + */ + private function setError(string $error): void + { + $this->lastErrorMessage = $error; + } + + /** + * Reads data from the archive. + * + * @param resource $fp The file pointer to read data from + * @param int|null $length The volume of data to read, in bytes + * + * @return string The data read from the file + * @since 4.0.4 + */ + private function fread($fp, ?int $length = null): string + { + $readLength = (is_numeric($length) && ($length > 0)) ? $length : PHP_INT_MAX; + $data = fread($fp, $readLength); + + if ($data === false) { + $this->debugMsg('No more data could be read from the file', self::LOG_WARNING); + + $data = ''; + } + + return $data; + } + + /** + * Read the header of the archive, making sure it's a valid ZIP file. + * + * @return void + * @since 4.0.4 + */ + private function readArchiveHeader(): void + { + $this->debugMsg('Reading the archive header.', self::LOG_DEBUG); + + // Open the first part + $this->openArchiveFile(); + + // Fail for unreadable files + if ($this->fp === false) { + return; + } + + // Read the header data. + $sigBinary = fread($this->fp, 4); + $headerData = unpack('Vsig', $sigBinary); + + // We only support single part ZIP files + if ($headerData['sig'] != 0x04034b50) { + $this->setError('The archive file is corrupt: bad header'); + + return; + } + + // Roll back the file pointer + fseek($this->fp, -4, SEEK_CUR); + + $this->currentOffset = @ftell($this->fp); + $this->dataReadLength = 0; + } + + /** + * Concrete classes must use this method to read the file header + * + * @return boolean True if reading the file was successful, false if an error occurred or we + * reached end of archive. + * @since 4.0.4 + */ + private function readFileHeader(): bool + { + $this->debugMsg('Reading the file entry header.', self::LOG_DEBUG); + + if (!is_resource($this->fp)) { + $this->setError('The archive is not open for reading.'); + + return false; + } + + // Unexpected end of file + if ($this->isEOF()) { + $this->debugMsg('EOF when reading file header data', self::LOG_WARNING); + $this->setError('The archive is corrupt or truncated'); + + return false; + } + + $this->currentOffset = ftell($this->fp); + + if ($this->expectDataDescriptor) { + $this->debugMsg('Expecting data descriptor (bit 3 of general purpose flag was set).', self::LOG_DEBUG); + + /** + * The last file had bit 3 of the general purpose bit flag set. This means that we have a 12 byte data + * descriptor we need to skip. To make things worse, there might also be a 4 byte optional data descriptor + * header (0x08074b50). + */ + $junk = @fread($this->fp, 4); + $junk = unpack('Vsig', $junk); + $readLength = ($junk['sig'] == 0x08074b50) ? 12 : 8; + $junk = @fread($this->fp, $readLength); + + // And check for EOF, too + if ($this->isEOF()) { + $this->debugMsg('EOF when reading data descriptor', self::LOG_WARNING); + $this->setError('The archive is corrupt or truncated'); + + return false; + } + } + + // Get and decode Local File Header + $headerBinary = fread($this->fp, 30); + $format = 'Vsig/C2ver/vbitflag/vcompmethod/vlastmodtime/vlastmoddate/Vcrc/Vcompsize/' + . 'Vuncomp/vfnamelen/veflen'; + $headerData = unpack($format, $headerBinary); + + // Check signature + if (!($headerData['sig'] == 0x04034b50)) { + // The signature is not the one used for files. Is this a central directory record (i.e. we're done)? + if ($headerData['sig'] == 0x02014b50) { + $this->debugMsg('Found Central Directory header; the extraction is complete', self::LOG_DEBUG); + + // End of ZIP file detected. We'll just skip to the end of file... + @fseek($this->fp, 0, SEEK_END); + $this->runState = self::AK_STATE_FINISHED; + + return false; + } + + $this->setError('The archive file is corrupt or truncated'); + + return false; + } + + // If bit 3 of the bitflag is set, expectDataDescriptor is true + $this->expectDataDescriptor = ($headerData['bitflag'] & 4) == 4; + $this->fileHeader = new stdClass(); + $this->fileHeader->timestamp = 0; + + // Read the last modified date and time + $lastmodtime = $headerData['lastmodtime']; + $lastmoddate = $headerData['lastmoddate']; + + if ($lastmoddate && $lastmodtime) { + $vHour = ($lastmodtime & 0xF800) >> 11; + $vMInute = ($lastmodtime & 0x07E0) >> 5; + $vSeconds = ($lastmodtime & 0x001F) * 2; + $vYear = (($lastmoddate & 0xFE00) >> 9) + 1980; + $vMonth = ($lastmoddate & 0x01E0) >> 5; + $vDay = $lastmoddate & 0x001F; + + $this->fileHeader->timestamp = @mktime($vHour, $vMInute, $vSeconds, $vMonth, $vDay, $vYear); + } + + $isBannedFile = false; + + $this->fileHeader->compressed = $headerData['compsize']; + $this->fileHeader->uncompressed = $headerData['uncomp']; + $nameFieldLength = $headerData['fnamelen']; + $extraFieldLength = $headerData['eflen']; + + // Read filename field + $this->fileHeader->file = fread($this->fp, $nameFieldLength); + + // Read extra field if present + if ($extraFieldLength > 0) { + $extrafield = fread($this->fp, $extraFieldLength); + } + + // Decide filetype -- Check for directories + $this->fileHeader->type = 'file'; + + if (strrpos($this->fileHeader->file, '/') == strlen($this->fileHeader->file) - 1) { + $this->fileHeader->type = 'dir'; + } + + // Decide filetype -- Check for symbolic links + if (($headerData['ver1'] == 10) && ($headerData['ver2'] == 3)) { + $this->fileHeader->type = 'link'; + } + + switch ($headerData['compmethod']) { + case 0: + $this->fileHeader->compression = 'none'; + break; + case 8: + $this->fileHeader->compression = 'gzip'; + break; + default: + $messageTemplate = 'This script cannot handle ZIP compression method %d. ' + . 'Only 0 (no compression) and 8 (DEFLATE, gzip) can be handled.'; + $actualMessage = sprintf($messageTemplate, $headerData['compmethod']); + $this->setError($actualMessage); + + return false; + break; + } + + // Find hard-coded banned files + if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) { + $isBannedFile = true; + } + + // Also try to find banned files passed in class configuration + if ((count($this->skipFiles) > 0) && in_array($this->fileHeader->file, $this->skipFiles)) { + $isBannedFile = true; + } + + // If we have a banned file, let's skip it + if ($isBannedFile) { + $debugMessage = sprintf('Current entity (%s) is banned from extraction and will be skipped over.', $this->fileHeader->file); + $this->debugMsg($debugMessage, self::LOG_DEBUG); + + // Advance the file pointer, skipping exactly the size of the compressed data + $seekleft = $this->fileHeader->compressed; + + while ($seekleft > 0) { + // Ensure that we can seek past archive part boundaries + $curSize = @filesize($this->filename); + $curPos = @ftell($this->fp); + $canSeek = $curSize - $curPos; + $canSeek = ($canSeek > $seekleft) ? $seekleft : $canSeek; + @fseek($this->fp, $canSeek, SEEK_CUR); + $seekleft -= $canSeek; + + if ($seekleft) { + $this->setError('The archive is corrupt or truncated'); + + return false; + } + } + + $this->currentOffset = @ftell($this->fp); + $this->runState = self::AK_STATE_DONE; + + return true; + } + + // Last chance to prepend a path to the filename + if (!empty($this->addPath)) { + $this->fileHeader->file = $this->addPath . $this->fileHeader->file; + } + + // Get the translated path name + if ($this->fileHeader->type == 'file') { + $this->fileHeader->realFile = $this->fileHeader->file; + $this->setLastExtractedFilename($this->fileHeader->file); + } elseif ($this->fileHeader->type == 'dir') { + $this->fileHeader->timestamp = 0; + + $dir = $this->fileHeader->file; + + if (!@is_dir($dir)) { + mkdir($dir, 0755, true); + } + + $this->setLastExtractedFilename(null); + } else { + // Symlink; do not post-process + $this->fileHeader->timestamp = 0; + $this->setLastExtractedFilename(null); + } + + $this->createDirectory(); + + // Header is read + $this->runState = self::AK_STATE_HEADER; + + return true; + } + + /** + * Creates the directory this file points to + * + * @return void + * @since 4.0.4 + */ + private function createDirectory(): void + { + // Do we need to create a directory? + if (empty($this->fileHeader->realFile)) { + $this->fileHeader->realFile = $this->fileHeader->file; + } + + $lastSlash = strrpos($this->fileHeader->realFile, '/'); + $dirName = substr($this->fileHeader->realFile, 0, $lastSlash); + $perms = 0755; + $ignore = $this->isIgnoredDirectory($dirName); + + if (@is_dir($dirName)) { + return; + } + + if ((@mkdir($dirName, $perms, true) === false) && (!$ignore)) { + $this->setError(sprintf('Could not create %s folder', $dirName)); + } + } + + /** + * Concrete classes must use this method to process file data. It must set $runState to self::AK_STATE_DATAREAD when + * it's finished processing the file data. + * + * @return boolean True if processing the file data was successful, false if an error occurred + * @since 4.0.4 + */ + private function processFileData(): bool + { + switch ($this->fileHeader->type) { + case 'dir': + $this->debugMsg('Extracting entity of type Directory', self::LOG_DEBUG); + + return $this->processTypeDir(); + break; + + case 'link': + $this->debugMsg('Extracting entity of type Symbolic Link', self::LOG_DEBUG); + + return $this->processTypeLink(); + break; + + case 'file': + switch ($this->fileHeader->compression) { + case 'none': + $this->debugMsg('Extracting entity of type File (Stored)', self::LOG_DEBUG); + + return $this->processTypeFileUncompressed(); + break; + + case 'gzip': + case 'bzip2': + $this->debugMsg('Extracting entity of type File (Compressed)', self::LOG_DEBUG); + + return $this->processTypeFileCompressed(); + break; + + case 'default': + $this->setError(sprintf('Unknown compression type %s.', $this->fileHeader->compression)); + + return false; + break; + } + break; + } + + $this->setError(sprintf('Unknown entry type %s.', $this->fileHeader->type)); + + return false; + } + + /** + * Opens the next part file for reading + * + * @return void + * @since 4.0.4 + */ + private function openArchiveFile(): void + { + $this->debugMsg('Opening archive file for reading', self::LOG_DEBUG); + + if ($this->archiveFileIsBeingRead) { + return; + } + + if (is_resource($this->fp)) { + @fclose($this->fp); + } + + $this->fp = @fopen($this->filename, 'rb'); + + if ($this->fp === false) { + $message = 'Could not open archive for reading. Check that the file exists, is ' + . 'readable by the web server and is not in a directory made out of reach by chroot, ' + . 'open_basedir restrictions or any other restriction put in place by your host.'; + $this->setError($message); + + return; + } + + fseek($this->fp, 0); + $this->currentOffset = 0; + } + + /** + * Returns true if we have reached the end of file + * + * @return boolean True if we have reached End Of File + * @since 4.0.4 + */ + private function isEOF(): bool + { + /** + * feof() will return false if the file pointer is exactly at the last byte of the file. However, this is a + * condition we want to treat as a proper EOF for the purpose of extracting a ZIP file. Hence the second part + * after the logical OR. + */ + return @feof($this->fp) || (@ftell($this->fp) > @filesize($this->filename)); + } + + /** + * Handles the permissions of the parent directory to a file and the file itself to make it writeable. + * + * @param string $path A path to a file + * + * @return void + * @since 4.0.4 + */ + private function setCorrectPermissions(string $path): void + { + static $rootDir = null; + + if (is_null($rootDir)) { + $rootDir = rtrim($this->addPath, '/\\'); + } + + $directory = rtrim(dirname($path), '/\\'); + + // Is this an unwritable directory? + if (($directory != $rootDir) && !is_writeable($directory)) { + @chmod($directory, 0755); + } + + @chmod($path, 0644); + } + + /** + * Is this file or directory contained in a directory we've decided to ignore + * write errors for? This is useful to let the extraction work despite write + * errors in the log, logs and tmp directories which MIGHT be used by the system + * on some low quality hosts and Plesk-powered hosts. + * + * @param string $shortFilename The relative path of the file/directory in the package + * + * @return boolean True if it belongs in an ignored directory + * @since 4.0.4 + */ + private function isIgnoredDirectory(string $shortFilename): bool + { + $check = substr($shortFilename, -1) == '/' ? rtrim($shortFilename, '/') : dirname($shortFilename); + + return in_array($check, $this->ignoreDirectories); + } + + /** + * Process the file data of a directory entry + * + * @return boolean + * @since 4.0.4 + */ + private function processTypeDir(): bool + { + // Directory entries do not have file data, therefore we're done processing the entry. + $this->runState = self::AK_STATE_DATAREAD; + + return true; + } + + /** + * Process the file data of a link entry + * + * @return boolean + * @since 4.0.4 + */ + private function processTypeLink(): bool + { + $toReadBytes = 0; + $leftBytes = $this->fileHeader->compressed; + $data = ''; + + while ($leftBytes > 0) { + $toReadBytes = min($leftBytes, self::CHUNK_SIZE); + $mydata = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = $this->binStringLength($mydata); + $data .= $mydata; + $leftBytes -= $reallyReadBytes; + + if ($reallyReadBytes < $toReadBytes) { + // We read less than requested! + if ($this->isEOF()) { + $this->debugMsg('EOF when reading symlink data', self::LOG_WARNING); + $this->setError('The archive file is corrupt or truncated'); + + return false; + } + } + } + + $filename = $this->fileHeader->realFile ?? $this->fileHeader->file; + + // Try to remove an existing file or directory by the same name + if (file_exists($filename)) { + clearFileInOPCache($filename); + @unlink($filename); + @rmdir($filename); + } + + // Remove any trailing slash + if (substr($filename, -1) == '/') { + $filename = substr($filename, 0, -1); + } + + // Create the symlink + @symlink($data, $filename); + + $this->runState = self::AK_STATE_DATAREAD; + + // No matter if the link was created! + return true; + } + + /** + * Processes an uncompressed (stored) file + * + * @return boolean + * @since 4.0.4 + */ + private function processTypeFileUncompressed(): bool + { + // Uncompressed files are being processed in small chunks, to avoid timeouts + if ($this->dataReadLength == 0) { + // Before processing file data, ensure permissions are adequate + $this->setCorrectPermissions($this->fileHeader->file); + } + + // Open the output file + $ignore = $this->isIgnoredDirectory($this->fileHeader->file); + + $writeMode = ($this->dataReadLength == 0) ? 'wb' : 'ab'; + $outfp = @fopen($this->fileHeader->realFile, $writeMode); + + // Can we write to the file? + if (($outfp === false) && (!$ignore)) { + // An error occurred + $this->setError(sprintf('Could not open %s for writing.', $this->fileHeader->realFile)); + + return false; + } + + // Does the file have any data, at all? + if ($this->fileHeader->compressed == 0) { + // No file data! + if (is_resource($outfp)) { + @fclose($outfp); + } + + $this->debugMsg('Zero byte Stored file; no data will be read', self::LOG_DEBUG); + + $this->runState = self::AK_STATE_DATAREAD; + + return true; + } + + $leftBytes = $this->fileHeader->compressed - $this->dataReadLength; + + // Loop while there's data to read and enough time to do it + while (($leftBytes > 0) && ($this->getTimeLeft() > 0)) { + $toReadBytes = min($leftBytes, self::CHUNK_SIZE); + $data = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = $this->binStringLength($data); + $leftBytes -= $reallyReadBytes; + $this->dataReadLength += $reallyReadBytes; + + if ($reallyReadBytes < $toReadBytes) { + // We read less than requested! Why? Did we hit local EOF? + if ($this->isEOF()) { + // Nope. The archive is corrupt + $this->debugMsg('EOF when reading stored file data', self::LOG_WARNING); + $this->setError('The archive file is corrupt or truncated'); + + return false; + } + } + + if (is_resource($outfp)) { + @fwrite($outfp, $data); + } + + if ($this->getTimeLeft()) { + $this->debugMsg('Out of time; will resume extraction in the next step', self::LOG_DEBUG); + } + } + + // Close the file pointer + if (is_resource($outfp)) { + @fclose($outfp); + } + + // Was this a pre-timeout bail out? + if ($leftBytes > 0) { + $this->debugMsg(sprintf('We have %d bytes left to extract in the next step', $leftBytes), self::LOG_DEBUG); + $this->runState = self::AK_STATE_DATA; + + return true; + } + + // Oh! We just finished! + $this->runState = self::AK_STATE_DATAREAD; + $this->dataReadLength = 0; + + return true; + } + + /** + * Processes a compressed file + * + * @return boolean + * @since 4.0.4 + */ + private function processTypeFileCompressed(): bool + { + // Before processing file data, ensure permissions are adequate + $this->setCorrectPermissions($this->fileHeader->file); + + // Open the output file + $outfp = @fopen($this->fileHeader->realFile, 'wb'); + + // Can we write to the file? + $ignore = $this->isIgnoredDirectory($this->fileHeader->file); + + if (($outfp === false) && (!$ignore)) { + // An error occurred + $this->setError(sprintf('Could not open %s for writing.', $this->fileHeader->realFile)); + + return false; + } + + // Does the file have any data, at all? + if ($this->fileHeader->compressed == 0) { + $this->debugMsg('Zero byte Compressed file; no data will be read', self::LOG_DEBUG); + + // No file data! + if (is_resource($outfp)) { + @fclose($outfp); + } + + $this->runState = self::AK_STATE_DATAREAD; + + return true; + } + + // Simple compressed files are processed as a whole; we can't do chunk processing + $zipData = $this->fread($this->fp, $this->fileHeader->compressed); + + while ($this->binStringLength($zipData) < $this->fileHeader->compressed) { + // End of local file before reading all data? + if ($this->isEOF()) { + $this->debugMsg('EOF reading compressed data', self::LOG_WARNING); + $this->setError('The archive file is corrupt or truncated'); + + return false; + } + } + + switch ($this->fileHeader->compression) { + case 'gzip': + /** @noinspection PhpComposerExtensionStubsInspection */ + $unzipData = gzinflate($zipData); + break; + + case 'bzip2': + /** @noinspection PhpComposerExtensionStubsInspection */ + $unzipData = bzdecompress($zipData); + break; + + default: + $this->setError(sprintf('Unknown compression method %s', $this->fileHeader->compression)); + + return false; + break; + } + + unset($zipData); + + // Write to the file. + if (is_resource($outfp)) { + @fwrite($outfp, $unzipData, $this->fileHeader->uncompressed); + @fclose($outfp); + } + + unset($unzipData); + + $this->runState = self::AK_STATE_DATAREAD; + + return true; + } + + /** + * Set up the maximum execution time + * + * @return void + * @since 4.0.4 + */ + private function setupMaxExecTime(): void + { + $configMaxTime = self::MAX_EXEC_TIME; + $bias = self::RUNTIME_BIAS / 100; + $this->maxExecTime = min($this->getPhpMaxExecTime(), $configMaxTime) * $bias; + } + + /** + * Get the PHP maximum execution time. + * + * If it's not defined or it's zero (infinite) we use a fake value of 10 seconds. + * + * @return integer + * @since 4.0.4 + */ + private function getPhpMaxExecTime(): int + { + if (!@function_exists('ini_get')) { + return 10; + } + + $phpMaxTime = @ini_get("maximum_execution_time"); + $phpMaxTime = (!is_numeric($phpMaxTime) ? 10 : @intval($phpMaxTime)) ?: 10; + + return max(1, $phpMaxTime); + } + + /** + * Write a message to the debug error log + * + * @param string $message The message to log + * @param int $priority The message's log priority + * + * @return void + * @since 4.0.4 + */ + private function debugMsg(string $message, int $priority = self::LOG_INFO): void + { + if (!defined('_JOOMLA_UPDATE_DEBUG')) { + return; + } + + if (!is_resource(self::$logFP) && !is_bool(self::$logFP)) { + self::$logFP = @fopen(self::$logFilePath, 'at'); + } + + if (!is_resource(self::$logFP)) { + return; + } + + switch ($priority) { + case self::LOG_DEBUG: + $priorityString = 'DEBUG'; + break; + + case self::LOG_INFO: + $priorityString = 'INFO'; + break; + + case self::LOG_WARNING: + $priorityString = 'WARNING'; + break; + + case self::LOG_ERROR: + $priorityString = 'ERROR'; + break; + } + + fputs(self::$logFP, sprintf('%s | %7s | %s' . "\r\n", gmdate('Y-m-d H:i:s'), $priorityString, $message)); + } + + /** + * Initialise the debug log file + * + * @param string $logPath The path where the log file will be written to + * + * @return void + * @since 4.0.4 + */ + private function initializeLog(string $logPath): void + { + if (!defined('_JOOMLA_UPDATE_DEBUG')) { + return; + } + + $logPath = $logPath ?: dirname($this->filename); + $logFile = rtrim($logPath, '/' . DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'joomla_update.txt'; + + self::$logFilePath = $logFile; + } } // Skip over the mini-controller for testing purposes -if (defined('_JOOMLA_UPDATE_TESTING')) -{ - return; +if (defined('_JOOMLA_UPDATE_TESTING')) { + return; } /** @@ -1782,21 +1683,19 @@ private function initializeLog(string $logPath): void */ function clearFileInOPCache(string $file): bool { - static $hasOpCache = null; + static $hasOpCache = null; - if (is_null($hasOpCache)) - { - $hasOpCache = ini_get('opcache.enable') - && function_exists('opcache_invalidate') - && (!ini_get('opcache.restrict_api') || stripos(realpath($_SERVER['SCRIPT_FILENAME']), ini_get('opcache.restrict_api')) === 0); - } + if (is_null($hasOpCache)) { + $hasOpCache = ini_get('opcache.enable') + && function_exists('opcache_invalidate') + && (!ini_get('opcache.restrict_api') || stripos(realpath($_SERVER['SCRIPT_FILENAME']), ini_get('opcache.restrict_api')) === 0); + } - if ($hasOpCache && (strtolower(substr($file, -4)) === '.php')) - { - return opcache_invalidate($file, true); - } + if ($hasOpCache && (strtolower(substr($file, -4)) === '.php')) { + return opcache_invalidate($file, true); + } - return false; + return false; } /** @@ -1817,28 +1716,25 @@ function clearFileInOPCache(string $file): bool */ function timingSafeEquals($known, $user) { - if (function_exists('hash_equals')) - { - return hash_equals($known, $user); - } + if (function_exists('hash_equals')) { + return hash_equals($known, $user); + } - $safeLen = strlen($known); - $userLen = strlen($user); + $safeLen = strlen($known); + $userLen = strlen($user); - if ($userLen != $safeLen) - { - return false; - } + if ($userLen != $safeLen) { + return false; + } - $result = 0; + $result = 0; - for ($i = 0; $i < $userLen; $i++) - { - $result |= (ord($known[$i]) ^ ord($user[$i])); - } + for ($i = 0; $i < $userLen; $i++) { + $result |= (ord($known[$i]) ^ ord($user[$i])); + } - // They are only identical strings if $result is exactly 0... - return $result === 0; + // They are only identical strings if $result is exactly 0... + return $result === 0; } /** @@ -1850,93 +1746,86 @@ function timingSafeEquals($known, $user) */ function getConfiguration(): ?array { - // Make sure the locale is correct for basename() to work - if (function_exists('setlocale')) - { - @setlocale(LC_ALL, 'en_US.UTF8'); - } - - // Require update.php or fail - $setupFile = __DIR__ . '/update.php'; - - if (!file_exists($setupFile)) - { - return null; - } - - /** - * If the setup file was created more than 1.5 hours ago we can assume that it's stale and someone forgot to - * remove it from the server. - * - * This prevents brute force attacks against the randomly generated password. Even a simple 8 character simple - * alphanum (a-z, 0-9) password yields over 2.8e12 permutation. Assuming a very fast server which can - * serve 100 requests to extract.php per second and an easy to attack password requiring going over just 1% of - * the search space it'd still take over 282 million seconds to brute force it. Our limit is more than 4 orders - * of magnitude lower than this best practical case scenario, giving us adequate protection against all but the - * luckiest attacker (spoiler alert: the mathematics of probabilities say you're not gonna get too lucky). - * - * It is still advisable to remove the update.php file once you are done with the extraction. This check - * here is only meant as a failsafe in case of a server error during the extraction and subsequent lack of user - * action to remove the update.php file from their server. - */ - clearstatcache(true); - $setupFileCreationTime = filectime($setupFile); - - if (abs(time() - $setupFileCreationTime) > 5400) - { - return null; - } - - // Load update.php. It pulls a variable named $restoration_setup into the local scope. - clearFileInOPCache($setupFile); - - require_once $setupFile; - - /** @var array $extractionSetup */ - - // The file exists but no configuration is present? - if (empty($extractionSetup ?? null) || !is_array($extractionSetup)) - { - return null; - } - - /** - * Immediately reject any attempt to run extract.php without a password. - * - * Doing that is a GRAVE SECURITY RISK. It makes it trivial to hack a site. Therefore we are preventing this script - * to run without a password. - */ - $password = $extractionSetup['security.password'] ?? null; - $userPassword = $_REQUEST['password'] ?? ''; - $userPassword = !is_string($userPassword) ? '' : trim($userPassword); - - if (empty($password) || !is_string($password) || (trim($password) == '') || (strlen(trim($password)) < 32)) - { - return null; - } - - // Timing-safe password comparison. See http://blog.ircmaxell.com/2014/11/its-all-about-time.html - if (!timingSafeEquals($password, $userPassword)) - { - return null; - } - - // An "instance" variable will resume the engine from the serialised instance - $serialized = $_REQUEST['instance'] ?? null; - - if (!is_null($serialized) && empty(ZIPExtraction::unserialiseInstance($serialized))) - { - // The serialised instance is corrupt or someone tries to trick us. YOU SHALL NOT PASS! - return null; - } - - return $extractionSetup; + // Make sure the locale is correct for basename() to work + if (function_exists('setlocale')) { + @setlocale(LC_ALL, 'en_US.UTF8'); + } + + // Require update.php or fail + $setupFile = __DIR__ . '/update.php'; + + if (!file_exists($setupFile)) { + return null; + } + + /** + * If the setup file was created more than 1.5 hours ago we can assume that it's stale and someone forgot to + * remove it from the server. + * + * This prevents brute force attacks against the randomly generated password. Even a simple 8 character simple + * alphanum (a-z, 0-9) password yields over 2.8e12 permutation. Assuming a very fast server which can + * serve 100 requests to extract.php per second and an easy to attack password requiring going over just 1% of + * the search space it'd still take over 282 million seconds to brute force it. Our limit is more than 4 orders + * of magnitude lower than this best practical case scenario, giving us adequate protection against all but the + * luckiest attacker (spoiler alert: the mathematics of probabilities say you're not gonna get too lucky). + * + * It is still advisable to remove the update.php file once you are done with the extraction. This check + * here is only meant as a failsafe in case of a server error during the extraction and subsequent lack of user + * action to remove the update.php file from their server. + */ + clearstatcache(true); + $setupFileCreationTime = filectime($setupFile); + + if (abs(time() - $setupFileCreationTime) > 5400) { + return null; + } + + // Load update.php. It pulls a variable named $restoration_setup into the local scope. + clearFileInOPCache($setupFile); + + require_once $setupFile; + + /** @var array $extractionSetup */ + + // The file exists but no configuration is present? + if (empty($extractionSetup ?? null) || !is_array($extractionSetup)) { + return null; + } + + /** + * Immediately reject any attempt to run extract.php without a password. + * + * Doing that is a GRAVE SECURITY RISK. It makes it trivial to hack a site. Therefore we are preventing this script + * to run without a password. + */ + $password = $extractionSetup['security.password'] ?? null; + $userPassword = $_REQUEST['password'] ?? ''; + $userPassword = !is_string($userPassword) ? '' : trim($userPassword); + + if (empty($password) || !is_string($password) || (trim($password) == '') || (strlen(trim($password)) < 32)) { + return null; + } + + // Timing-safe password comparison. See http://blog.ircmaxell.com/2014/11/its-all-about-time.html + if (!timingSafeEquals($password, $userPassword)) { + return null; + } + + // An "instance" variable will resume the engine from the serialised instance + $serialized = $_REQUEST['instance'] ?? null; + + if (!is_null($serialized) && empty(ZIPExtraction::unserialiseInstance($serialized))) { + // The serialised instance is corrupt or someone tries to trick us. YOU SHALL NOT PASS! + return null; + } + + return $extractionSetup; } // Import configuration $retArray = [ - 'status' => true, - 'message' => null, + 'status' => true, + 'message' => null, ]; $configuration = getConfiguration(); @@ -1950,12 +1839,11 @@ function getConfiguration(): ?array */ function setLongTimeout() { - if (!function_exists('ini_set')) - { - return; - } + if (!function_exists('ini_set')) { + return; + } - ini_set('max_execution_time', 3600); + ini_set('max_execution_time', 3600); } /** @@ -1966,128 +1854,114 @@ function setLongTimeout() */ function setHugeMemoryLimit() { - if (!function_exists('ini_set')) - { - return; - } + if (!function_exists('ini_set')) { + return; + } - ini_set('memory_limit', 1073741824); + ini_set('memory_limit', 1073741824); } -if ($enabled) -{ - // Try to set a very large memory and timeout limit - setLongTimeout(); - setHugeMemoryLimit(); - - $sourcePath = $configuration['setup.sourcepath'] ?? ''; - $sourceFile = $configuration['setup.sourcefile'] ?? ''; - $destDir = ($configuration['setup.destdir'] ?? null) ?: __DIR__; - $basePath = rtrim(str_replace('\\', '/', __DIR__), '/'); - $basePath = empty($basePath) ? $basePath : ($basePath . '/'); - $sourceFile = (empty($sourcePath) ? '' : (rtrim($sourcePath, '/\\') . '/')) . $sourceFile; - $engine = ZIPExtraction::getInstance(); - - $engine->setFilename($sourceFile); - $engine->setAddPath($destDir); - $skipFiles = [ - 'administrator/components/com_joomlaupdate/restoration.php', - 'administrator/components/com_joomlaupdate/update.php', - ]; - - if (defined('_JOOMLA_UPDATE_DEBUG')) - { - $skipFiles[] = 'administrator/components/com_joomlaupdate/extract.php'; - } - - $engine->setSkipFiles($skipFiles - ); - $engine->setIgnoreDirectories([ - 'tmp', 'administrator/logs', - ] - ); - - $task = $_REQUEST['task'] ?? null; - - switch ($task) - { - case 'startExtract': - case 'stepExtract': - $done = $engine->step(); - $error = $engine->getError(); - - if ($error != '') - { - $retArray['status'] = false; - $retArray['done'] = true; - $retArray['message'] = $error; - } - elseif ($done) - { - $retArray['files'] = $engine->filesProcessed; - $retArray['bytesIn'] = $engine->compressedTotal; - $retArray['bytesOut'] = $engine->uncompressedTotal; - $retArray['percent'] = 100; - $retArray['status'] = true; - $retArray['done'] = true; - - $retArray['percent'] = min($retArray['percent'], 100); - } - else - { - $retArray['files'] = $engine->filesProcessed; - $retArray['bytesIn'] = $engine->compressedTotal; - $retArray['bytesOut'] = $engine->uncompressedTotal; - $retArray['percent'] = ($engine->totalSize > 0) ? (100 * $engine->compressedTotal / $engine->totalSize) : 0; - $retArray['status'] = true; - $retArray['done'] = false; - $retArray['instance'] = ZIPExtraction::getSerialised(); - } - - $engine->enforceMinimumExecutionTime(); - - break; - - case 'finalizeUpdate': - $root = $configuration['setup.destdir'] ?? ''; - - // Remove update.php - clearFileInOPCache($basePath . 'update.php'); - @unlink($basePath . 'update.php'); - - // Import a custom finalisation file - $filename = dirname(__FILE__) . '/finalisation.php'; - - if (file_exists($filename)) - { - clearFileInOPCache($filename); - - include_once $filename; - } - - // Run a custom finalisation script - if (function_exists('finalizeUpdate')) - { - finalizeUpdate($root, $basePath); - } - - $engine->enforceMinimumExecutionTime(); - - break; - - default: - // Invalid task! - $enabled = false; - break; - } +if ($enabled) { + // Try to set a very large memory and timeout limit + setLongTimeout(); + setHugeMemoryLimit(); + + $sourcePath = $configuration['setup.sourcepath'] ?? ''; + $sourceFile = $configuration['setup.sourcefile'] ?? ''; + $destDir = ($configuration['setup.destdir'] ?? null) ?: __DIR__; + $basePath = rtrim(str_replace('\\', '/', __DIR__), '/'); + $basePath = empty($basePath) ? $basePath : ($basePath . '/'); + $sourceFile = (empty($sourcePath) ? '' : (rtrim($sourcePath, '/\\') . '/')) . $sourceFile; + $engine = ZIPExtraction::getInstance(); + + $engine->setFilename($sourceFile); + $engine->setAddPath($destDir); + $skipFiles = [ + 'administrator/components/com_joomlaupdate/restoration.php', + 'administrator/components/com_joomlaupdate/update.php', + ]; + + if (defined('_JOOMLA_UPDATE_DEBUG')) { + $skipFiles[] = 'administrator/components/com_joomlaupdate/extract.php'; + } + + $engine->setSkipFiles($skipFiles); + $engine->setIgnoreDirectories([ + 'tmp', 'administrator/logs', + ]); + + $task = $_REQUEST['task'] ?? null; + + switch ($task) { + case 'startExtract': + case 'stepExtract': + $done = $engine->step(); + $error = $engine->getError(); + + if ($error != '') { + $retArray['status'] = false; + $retArray['done'] = true; + $retArray['message'] = $error; + } elseif ($done) { + $retArray['files'] = $engine->filesProcessed; + $retArray['bytesIn'] = $engine->compressedTotal; + $retArray['bytesOut'] = $engine->uncompressedTotal; + $retArray['percent'] = 100; + $retArray['status'] = true; + $retArray['done'] = true; + + $retArray['percent'] = min($retArray['percent'], 100); + } else { + $retArray['files'] = $engine->filesProcessed; + $retArray['bytesIn'] = $engine->compressedTotal; + $retArray['bytesOut'] = $engine->uncompressedTotal; + $retArray['percent'] = ($engine->totalSize > 0) ? (100 * $engine->compressedTotal / $engine->totalSize) : 0; + $retArray['status'] = true; + $retArray['done'] = false; + $retArray['instance'] = ZIPExtraction::getSerialised(); + } + + $engine->enforceMinimumExecutionTime(); + + break; + + case 'finalizeUpdate': + $root = $configuration['setup.destdir'] ?? ''; + + // Remove update.php + clearFileInOPCache($basePath . 'update.php'); + @unlink($basePath . 'update.php'); + + // Import a custom finalisation file + $filename = dirname(__FILE__) . '/finalisation.php'; + + if (file_exists($filename)) { + clearFileInOPCache($filename); + + include_once $filename; + } + + // Run a custom finalisation script + if (function_exists('finalizeUpdate')) { + finalizeUpdate($root, $basePath); + } + + $engine->enforceMinimumExecutionTime(); + + break; + + default: + // Invalid task! + $enabled = false; + break; + } } // This could happen even if $enabled was true, e.g. if we were asked for an invalid task. -if (!$enabled) -{ - // Maybe we weren't authorized or the task was invalid? - $retArray['status'] = false; - $retArray['message'] = 'Invalid login'; +if (!$enabled) { + // Maybe we weren't authorized or the task was invalid? + $retArray['status'] = false; + $retArray['message'] = 'Invalid login'; } // JSON encode the message diff --git a/administrator/components/com_joomlaupdate/finalisation.php b/administrator/components/com_joomlaupdate/finalisation.php index 8319203fca08e..697eacff3c585 100644 --- a/administrator/components/com_joomlaupdate/finalisation.php +++ b/administrator/components/com_joomlaupdate/finalisation.php @@ -1,4 +1,5 @@ deleteUnexistingFiles(); - } + // Remove obsolete files - prevents errors occurring in some system plugins + if (class_exists('JoomlaInstallerScript')) { + (new JoomlaInstallerScript())->deleteUnexistingFiles(); + } - /** - * Remove autoload_psr4.php so that namespace map is re-generated on the next request. This is needed - * when there are new classes added to extensions on new Joomla! release. - */ - $namespaceMapFile = JPATH_ROOT . '/administrator/cache/autoload_psr4.php'; + /** + * Remove autoload_psr4.php so that namespace map is re-generated on the next request. This is needed + * when there are new classes added to extensions on new Joomla! release. + */ + $namespaceMapFile = JPATH_ROOT . '/administrator/cache/autoload_psr4.php'; - if (\Joomla\CMS\Filesystem\File::exists($namespaceMapFile)) - { - \Joomla\CMS\Filesystem\File::delete($namespaceMapFile); - } - } - } + if (\Joomla\CMS\Filesystem\File::exists($namespaceMapFile)) { + \Joomla\CMS\Filesystem\File::delete($namespaceMapFile); + } + } + } } namespace Joomla\CMS\Filesystem { - // Fake the File class - if (!class_exists('\Joomla\CMS\Filesystem\File')) - { - /** - * File mock class - * - * @since 3.5.1 - */ - abstract class File - { - /** - * Proxies checking a file exists to the native php version - * - * @param string $fileName The path to the file to be checked - * - * @return boolean - * - * @since 3.5.1 - */ - public static function exists(string $fileName): bool - { - return @file_exists($fileName); - } - - /** - * Delete a file and invalidate the PHP OPcache - * - * @param string $fileName The path to the file to be deleted - * - * @return boolean - * - * @since 3.5.1 - */ - public static function delete(string $fileName): bool - { - self::invalidateFileCache($fileName); + // Fake the File class + if (!class_exists('\Joomla\CMS\Filesystem\File')) { + /** + * File mock class + * + * @since 3.5.1 + */ + abstract class File + { + /** + * Proxies checking a file exists to the native php version + * + * @param string $fileName The path to the file to be checked + * + * @return boolean + * + * @since 3.5.1 + */ + public static function exists(string $fileName): bool + { + return @file_exists($fileName); + } - return @unlink($fileName); - } + /** + * Delete a file and invalidate the PHP OPcache + * + * @param string $fileName The path to the file to be deleted + * + * @return boolean + * + * @since 3.5.1 + */ + public static function delete(string $fileName): bool + { + self::invalidateFileCache($fileName); - /** - * Rename a file and invalidate the PHP OPcache - * - * @param string $src The path to the source file - * @param string $dest The path to the destination file - * - * @return boolean True on success - * - * @since 4.0.1 - */ - public static function move(string $src, string $dest): bool - { - self::invalidateFileCache($src); + return @unlink($fileName); + } - $result = @rename($src, $dest); + /** + * Rename a file and invalidate the PHP OPcache + * + * @param string $src The path to the source file + * @param string $dest The path to the destination file + * + * @return boolean True on success + * + * @since 4.0.1 + */ + public static function move(string $src, string $dest): bool + { + self::invalidateFileCache($src); - if ($result) - { - self::invalidateFileCache($dest); - } + $result = @rename($src, $dest); - return $result; - } + if ($result) { + self::invalidateFileCache($dest); + } - /** - * Invalidate opcache for a newly written/deleted file immediately, if opcache* functions exist and if this was a PHP file. - * - * @param string $filepath The path to the file just written to, to flush from opcache - * @param boolean $force If set to true, the script will be invalidated regardless of whether invalidation is necessary - * - * @return boolean TRUE if the opcode cache for script was invalidated/nothing to invalidate, - * or FALSE if the opcode cache is disabled or other conditions returning - * FALSE from opcache_invalidate (like file not found). - * - * @since 4.0.2 - */ - public static function invalidateFileCache($filepath, $force = true) - { - return \clearFileInOPCache($filepath); - } + return $result; + } - } - } + /** + * Invalidate opcache for a newly written/deleted file immediately, if opcache* functions exist and if this was a PHP file. + * + * @param string $filepath The path to the file just written to, to flush from opcache + * @param boolean $force If set to true, the script will be invalidated regardless of whether invalidation is necessary + * + * @return boolean TRUE if the opcode cache for script was invalidated/nothing to invalidate, + * or FALSE if the opcode cache is disabled or other conditions returning + * FALSE from opcache_invalidate (like file not found). + * + * @since 4.0.2 + */ + public static function invalidateFileCache($filepath, $force = true) + { + return \clearFileInOPCache($filepath); + } + } + } - // Fake the Folder class, mapping it to Restore's post-processing class - if (!class_exists('\Joomla\CMS\Filesystem\Folder')) - { - /** - * Folder mock class - * - * @since 3.5.1 - */ - abstract class Folder - { - /** - * Proxies checking a folder exists to the native php version - * - * @param string $folderName The path to the folder to be checked - * - * @return boolean - * - * @since 3.5.1 - */ - public static function exists(string $folderName): bool - { - return @is_dir($folderName); - } + // Fake the Folder class, mapping it to Restore's post-processing class + if (!class_exists('\Joomla\CMS\Filesystem\Folder')) { + /** + * Folder mock class + * + * @since 3.5.1 + */ + abstract class Folder + { + /** + * Proxies checking a folder exists to the native php version + * + * @param string $folderName The path to the folder to be checked + * + * @return boolean + * + * @since 3.5.1 + */ + public static function exists(string $folderName): bool + { + return @is_dir($folderName); + } - /** - * Delete a folder recursively and invalidate the PHP OPcache - * - * @param string $folderName The path to the folder to be deleted - * - * @return boolean - * - * @since 3.5.1 - */ - public static function delete(string $folderName): bool - { - if (substr($folderName, -1) == '/') - { - $folderName = substr($folderName, 0, -1); - } + /** + * Delete a folder recursively and invalidate the PHP OPcache + * + * @param string $folderName The path to the folder to be deleted + * + * @return boolean + * + * @since 3.5.1 + */ + public static function delete(string $folderName): bool + { + if (substr($folderName, -1) == '/') { + $folderName = substr($folderName, 0, -1); + } - if (!@file_exists($folderName) || !@is_dir($folderName) || !is_readable($folderName)) - { - return false; - } + if (!@file_exists($folderName) || !@is_dir($folderName) || !is_readable($folderName)) { + return false; + } - $di = new \DirectoryIterator($folderName); + $di = new \DirectoryIterator($folderName); - /** @var \DirectoryIterator $item */ - foreach ($di as $item) - { - if ($item->isDot()) - { - continue; - } + /** @var \DirectoryIterator $item */ + foreach ($di as $item) { + if ($item->isDot()) { + continue; + } - if ($item->isDir()) - { - $status = self::delete($item->getPathname()); + if ($item->isDir()) { + $status = self::delete($item->getPathname()); - if (!$status) - { - return false; - } + if (!$status) { + return false; + } - continue; - } + continue; + } - \clearFileInOPCache($item->getPathname()); + \clearFileInOPCache($item->getPathname()); - @unlink($item->getPathname()); - } + @unlink($item->getPathname()); + } - return @rmdir($folderName); - } - } - } + return @rmdir($folderName); + } + } + } } namespace Joomla\CMS\Language { - // Fake the Text class - we aren't going to show errors to people anyhow - if (!class_exists('\Joomla\CMS\Language\Text')) - { - /** - * Text mock class - * - * @since 3.5.1 - */ - abstract class Text - { - /** - * No need for translations in a non-interactive script, so always return an empty string here - * - * @param string $text A language constant - * - * @return string - * - * @since 3.5.1 - */ - public static function sprintf(string $text): string - { - return ''; - } - } - } + // Fake the Text class - we aren't going to show errors to people anyhow + if (!class_exists('\Joomla\CMS\Language\Text')) { + /** + * Text mock class + * + * @since 3.5.1 + */ + abstract class Text + { + /** + * No need for translations in a non-interactive script, so always return an empty string here + * + * @param string $text A language constant + * + * @return string + * + * @since 3.5.1 + */ + public static function sprintf(string $text): string + { + return ''; + } + } + } } diff --git a/administrator/components/com_joomlaupdate/restore_finalisation.php b/administrator/components/com_joomlaupdate/restore_finalisation.php index f2cdb41545e29..ffdaf06fed873 100644 --- a/administrator/components/com_joomlaupdate/restore_finalisation.php +++ b/administrator/components/com_joomlaupdate/restore_finalisation.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Joomlaupdate')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Joomlaupdate')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Joomlaupdate')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Joomlaupdate')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_joomlaupdate/src/Controller/DisplayController.php b/administrator/components/com_joomlaupdate/src/Controller/DisplayController.php index 75534fc13bc21..c6e1676b9503f 100644 --- a/administrator/components/com_joomlaupdate/src/Controller/DisplayController.php +++ b/administrator/components/com_joomlaupdate/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'Joomlaupdate'); - $vFormat = $document->getType(); - $lName = $this->input->get('layout', 'default', 'string'); - - // Get and render the view. - if ($view = $this->getView($vName, $vFormat)) - { - // Only super user can access file upload - if ($view == 'upload' && !$this->app->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) - { - $this->app->redirect(Route::_('index.php?option=com_joomlaupdate', true)); - } - - // Get the model for the view. - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - /** @var ?\Joomla\Component\Installer\Administrator\Model\WarningsModel $warningsModel */ - $warningsModel = $this->app->bootComponent('com_installer') - ->getMVCFactory()->createModel('Warnings', 'Administrator', ['ignore_request' => true]); - - if ($warningsModel !== null) - { - $view->setModel($warningsModel, false); - } - - // Perform update source preference check and refresh update information. - $model->applyUpdateSite(); - $model->refreshUpdates(); - - // Push the model into the view (as default). - $view->setModel($model, true); - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - $view->display(); - } - - return $this; - } - - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_joomlaupdate')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Update'); - - $model->refreshUpdates(); - - $joomlaUpdate = $model->getUpdateInformation(); - - $hasUpdate = $joomlaUpdate['hasUpdate'] ? $joomlaUpdate['latest'] : ''; - - echo new JsonResponse($hasUpdate); - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached. + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 2.5.4 + */ + public function display($cachable = false, $urlparams = false) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'Joomlaupdate'); + $vFormat = $document->getType(); + $lName = $this->input->get('layout', 'default', 'string'); + + // Get and render the view. + if ($view = $this->getView($vName, $vFormat)) { + // Only super user can access file upload + if ($view == 'upload' && !$this->app->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) { + $this->app->redirect(Route::_('index.php?option=com_joomlaupdate', true)); + } + + // Get the model for the view. + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + /** @var ?\Joomla\Component\Installer\Administrator\Model\WarningsModel $warningsModel */ + $warningsModel = $this->app->bootComponent('com_installer') + ->getMVCFactory()->createModel('Warnings', 'Administrator', ['ignore_request' => true]); + + if ($warningsModel !== null) { + $view->setModel($warningsModel, false); + } + + // Perform update source preference check and refresh update information. + $model->applyUpdateSite(); + $model->refreshUpdates(); + + // Push the model into the view (as default). + $view->setModel($model, true); + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + $view->display(); + } + + return $this; + } + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_joomlaupdate')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Update'); + + $model->refreshUpdates(); + + $joomlaUpdate = $model->getUpdateInformation(); + + $hasUpdate = $joomlaUpdate['hasUpdate'] ? $joomlaUpdate['latest'] : ''; + + echo new JsonResponse($hasUpdate); + } } diff --git a/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php b/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php index 5aa701148261d..650194bc8cef8 100644 --- a/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php +++ b/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php @@ -1,4 +1,5 @@ checkToken(); - - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'joomla_update.php'; - Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); - $user = $this->app->getIdentity(); - - try - { - Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_START', $user->id, $user->name, \JVERSION), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - $result = $model->download(); - $file = $result['basename']; - - $message = null; - $messageType = null; - - // The validation was not successful so abort. - if ($result['check'] === false) - { - $message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_CHECKSUM_WRONG'); - $messageType = 'error'; - $url = 'index.php?option=com_joomlaupdate'; - - $this->app->setUserState('com_joomlaupdate.file', null); - $this->setRedirect($url, $message, $messageType); - - try - { - Log::add($message, Log::ERROR, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - return; - } - - if ($file) - { - $this->app->setUserState('com_joomlaupdate.file', $file); - $url = 'index.php?option=com_joomlaupdate&task=update.install&' . $this->app->getSession()->getFormToken() . '=1'; - - try - { - Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_FILE', $file), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - } - else - { - $this->app->setUserState('com_joomlaupdate.file', null); - $url = 'index.php?option=com_joomlaupdate'; - $message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_DOWNLOADFAILED'); - $messageType = 'error'; - } - - $this->setRedirect($url, $message, $messageType); - } - - /** - * Start the installation of the new Joomla! version - * - * @return void - * - * @since 2.5.4 - */ - public function install() - { - $this->checkToken('get'); - $this->app->setUserState('com_joomlaupdate.oldversion', JVERSION); - - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'joomla_update.php'; - Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); - - try - { - Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_INSTALL'), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - $file = $this->app->getUserState('com_joomlaupdate.file', null); - $model->createRestorationFile($file); - - $this->display(); - } - - /** - * Finalise the upgrade by running the necessary scripts - * - * @return void - * - * @since 2.5.4 - */ - public function finalise() - { - /* - * Finalize with login page. Used for pre-token check versions - * to allow updates without problems but with a maximum of security. - */ - if (!Session::checkToken('get')) - { - $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); - - return; - } - - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'joomla_update.php'; - Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); - - try - { - Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_FINALISE'), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - $model->finaliseUpgrade(); - - $url = 'index.php?option=com_joomlaupdate&task=update.cleanup&' . Session::getFormToken() . '=1'; - $this->setRedirect($url); - } - - /** - * Clean up after ourselves - * - * @return void - * - * @since 2.5.4 - */ - public function cleanup() - { - /* - * Cleanup with login page. Used for pre-token check versions to be able to update - * from =< 3.2.7 to allow updates without problems but with a maximum of security. - */ - if (!Session::checkToken('get')) - { - $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); - - return; - } - - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'joomla_update.php'; - Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); - - try - { - Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_CLEANUP'), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - $model->cleanUp(); - - $url = 'index.php?option=com_joomlaupdate&view=joomlaupdate&layout=complete'; - $this->setRedirect($url); - - try - { - Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_COMPLETE', \JVERSION), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - } - - /** - * Purges updates. - * - * @return void - * - * @since 3.0 - */ - public function purge() - { - // Check for request forgeries - $this->checkToken('request'); - - // Purge updates - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - $model->purge(); - - $url = 'index.php?option=com_joomlaupdate'; - $this->setRedirect($url, $model->_message); - } - - /** - * Uploads an update package to the temporary directory, under a random name - * - * @return void - * - * @since 3.6.0 - */ - public function upload() - { - // Check for request forgeries - $this->checkToken(); - - // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? - $this->app->getIdentity()->authorise('core.admin') or jexit(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')); - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - try - { - $model->upload(); - } - catch (\RuntimeException $e) - { - $url = 'index.php?option=com_joomlaupdate'; - $this->setRedirect($url, $e->getMessage(), 'error'); - - return; - } - - $token = Session::getFormToken(); - $url = 'index.php?option=com_joomlaupdate&task=update.captive&' . $token . '=1'; - $this->setRedirect($url); - } - - /** - * Checks there is a valid update package and redirects to the captive view for super admin authentication. - * - * @return void - * - * @since 3.6.0 - */ - public function captive() - { - // Check for request forgeries - $this->checkToken('get'); - - // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? - if (!$this->app->getIdentity()->authorise('core.admin')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - // Do I really have an update package? - $tempFile = $this->app->getUserState('com_joomlaupdate.temp_file', null); - - if (empty($tempFile) || !File::exists($tempFile)) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - $this->input->set('view', 'upload'); - $this->input->set('layout', 'captive'); - - $this->display(); - } - - /** - * Checks the admin has super administrator privileges and then proceeds with the update. - * - * @return void - * - * @since 3.6.0 - */ - public function confirm() - { - // Check for request forgeries - $this->checkToken(); - - // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? - if (!$this->app->getIdentity()->authorise('core.admin')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - // Get the captive file before the session resets - $tempFile = $this->app->getUserState('com_joomlaupdate.temp_file', null); - - // Do I really have an update package? - if (!$model->captiveFileExists()) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - // Try to log in - $credentials = array( - 'username' => $this->input->post->get('username', '', 'username'), - 'password' => $this->input->post->get('passwd', '', 'raw'), - 'secretkey' => $this->input->post->get('secretkey', '', 'raw'), - ); - - $result = $model->captiveLogin($credentials); - - if (!$result) - { - $model->removePackageFiles(); - - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - // Set the update source in the session - $this->app->setUserState('com_joomlaupdate.file', basename($tempFile)); - - try - { - Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_FILE', $tempFile), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - // Redirect to the actual update page - $url = 'index.php?option=com_joomlaupdate&task=update.install&' . Session::getFormToken() . '=1'; - $this->setRedirect($url); - } - - /** - * Method to display a view. - * - * @param boolean $cachable If true, the view output will be cached - * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. - * - * @return static This object to support chaining. - * - * @since 2.5.4 - */ - public function display($cachable = false, $urlparams = array()) - { - // Get the document object. - $document = $this->app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'update'); - $vFormat = $document->getType(); - $lName = $this->input->get('layout', 'default', 'string'); - - // Get and render the view. - if ($view = $this->getView($vName, $vFormat)) - { - // Get the model for the view. - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - // Push the model into the view (as default). - $view->setModel($model, true); - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - $view->display(); - } - - return $this; - } - - /** - * Checks the admin has super administrator privileges and then proceeds with the final & cleanup steps. - * - * @return void - * - * @since 3.6.3 - */ - public function finaliseconfirm() - { - // Check for request forgeries - $this->checkToken(); - - // Did a non Super User try do this? - if (!$this->app->getIdentity()->authorise('core.admin')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - // Get the model - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - // Try to log in - $credentials = array( - 'username' => $this->input->post->get('username', '', 'username'), - 'password' => $this->input->post->get('passwd', '', 'raw'), - 'secretkey' => $this->input->post->get('secretkey', '', 'raw'), - ); - - $result = $model->captiveLogin($credentials); - - // The login fails? - if (!$result) - { - $this->setMessage(Text::_('JGLOBAL_AUTH_INVALID_PASS'), 'warning'); - $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); - - return; - } - - // Redirect back to the actual finalise page - $this->setRedirect('index.php?option=com_joomlaupdate&task=update.finalise&' . Session::getFormToken() . '=1'); - } - - /** - * Fetch Extension update XML proxy. Used to prevent Access-Control-Allow-Origin errors. - * Prints a JSON string. - * Called from JS. - * - * @since 3.10.0 - * @deprecated 5.0 Use batchextensioncompatibility instead. - * - * @return void - */ - public function fetchExtensionCompatibility() - { - $extensionID = $this->input->get('extension-id', '', 'DEFAULT'); - $joomlaTargetVersion = $this->input->get('joomla-target-version', '', 'DEFAULT'); - $joomlaCurrentVersion = $this->input->get('joomla-current-version', '', JVERSION); - $extensionVersion = $this->input->get('extension-version', '', 'DEFAULT'); - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - $upgradeCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaTargetVersion); - $currentCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaCurrentVersion); - $upgradeUpdateVersion = false; - $currentUpdateVersion = false; - - $upgradeWarning = 0; - - if ($upgradeCompatibilityStatus->state == 1 && !empty($upgradeCompatibilityStatus->compatibleVersions)) - { - $upgradeUpdateVersion = end($upgradeCompatibilityStatus->compatibleVersions); - } - - if ($currentCompatibilityStatus->state == 1 && !empty($currentCompatibilityStatus->compatibleVersions)) - { - $currentUpdateVersion = end($currentCompatibilityStatus->compatibleVersions); - } - - if ($upgradeUpdateVersion !== false) - { - $upgradeOldestVersion = $upgradeCompatibilityStatus->compatibleVersions[0]; - - if ($currentUpdateVersion !== false) - { - // If there are updates compatible with both CMS versions use these - $bothCompatibleVersions = array_values( - array_intersect($upgradeCompatibilityStatus->compatibleVersions, $currentCompatibilityStatus->compatibleVersions) - ); - - if (!empty($bothCompatibleVersions)) - { - $upgradeOldestVersion = $bothCompatibleVersions[0]; - $upgradeUpdateVersion = end($bothCompatibleVersions); - } - } - - if (version_compare($upgradeOldestVersion, $extensionVersion, '>')) - { - // Installed version is empty or older than the oldest compatible update: Update required - $resultGroup = 2; - } - else - { - // Current version is compatible - $resultGroup = 3; - } - - if ($currentUpdateVersion !== false && version_compare($upgradeUpdateVersion, $currentUpdateVersion, '<')) - { - // Special case warning when version compatible with target is lower than current - $upgradeWarning = 2; - } - } - elseif ($currentUpdateVersion !== false) - { - // No compatible version for target version but there is a compatible version for current version - $resultGroup = 1; - } - else - { - // No update server available - $resultGroup = 1; - } - - // Do we need to capture - $combinedCompatibilityStatus = array( - 'upgradeCompatibilityStatus' => (object) array( - 'state' => $upgradeCompatibilityStatus->state, - 'compatibleVersion' => $upgradeUpdateVersion - ), - 'currentCompatibilityStatus' => (object) array( - 'state' => $currentCompatibilityStatus->state, - 'compatibleVersion' => $currentUpdateVersion - ), - 'resultGroup' => $resultGroup, - 'upgradeWarning' => $upgradeWarning, - ); - - $this->app = Factory::getApplication(); - $this->app->mimeType = 'application/json'; - $this->app->charSet = 'utf-8'; - $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); - $this->app->sendHeaders(); - - try - { - echo new JsonResponse($combinedCompatibilityStatus); - } - catch (\Exception $e) - { - echo $e; - } - - $this->app->close(); - } - - /** - * Determines the compatibility information for a number of extensions. - * - * Called by the Joomla Update JavaScript (PreUpdateChecker.checkNextChunk). - * - * @return void - * @since 4.2.0 - * - */ - public function batchextensioncompatibility() - { - $joomlaTargetVersion = $this->input->post->get('joomla-target-version', '', 'DEFAULT'); - $joomlaCurrentVersion = $this->input->post->get('joomla-current-version', JVERSION); - $extensionInformation = $this->input->post->get('extensions', []); - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - $extensionResults = []; - $leftover = []; - $startTime = microtime(true); - - foreach ($extensionInformation as $information) - { - // Only process an extension if we have spent less than 5 seconds already - $currentTime = microtime(true); - - if ($currentTime - $startTime > 5.0) - { - $leftover[] = $information; - - continue; - } - - // Get the extension information and fetch its compatibility information - $extensionID = $information['eid'] ?: ''; - $extensionVersion = $information['version'] ?: ''; - $upgradeCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaTargetVersion); - $currentCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaCurrentVersion); - $upgradeUpdateVersion = false; - $currentUpdateVersion = false; - $upgradeWarning = 0; - - if ($upgradeCompatibilityStatus->state == 1 && !empty($upgradeCompatibilityStatus->compatibleVersions)) - { - $upgradeUpdateVersion = end($upgradeCompatibilityStatus->compatibleVersions); - } - - if ($currentCompatibilityStatus->state == 1 && !empty($currentCompatibilityStatus->compatibleVersions)) - { - $currentUpdateVersion = end($currentCompatibilityStatus->compatibleVersions); - } - - if ($upgradeUpdateVersion !== false) - { - $upgradeOldestVersion = $upgradeCompatibilityStatus->compatibleVersions[0]; - - if ($currentUpdateVersion !== false) - { - // If there are updates compatible with both CMS versions use these - $bothCompatibleVersions = array_values( - array_intersect($upgradeCompatibilityStatus->compatibleVersions, $currentCompatibilityStatus->compatibleVersions) - ); - - if (!empty($bothCompatibleVersions)) - { - $upgradeOldestVersion = $bothCompatibleVersions[0]; - $upgradeUpdateVersion = end($bothCompatibleVersions); - } - } - - if (version_compare($upgradeOldestVersion, $extensionVersion, '>')) - { - // Installed version is empty or older than the oldest compatible update: Update required - $resultGroup = 2; - } - else - { - // Current version is compatible - $resultGroup = 3; - } - - if ($currentUpdateVersion !== false && version_compare($upgradeUpdateVersion, $currentUpdateVersion, '<')) - { - // Special case warning when version compatible with target is lower than current - $upgradeWarning = 2; - } - } - elseif ($currentUpdateVersion !== false) - { - // No compatible version for target version but there is a compatible version for current version - $resultGroup = 1; - } - else - { - // No update server available - $resultGroup = 1; - } - - // Do we need to capture - $extensionResults[] = [ - 'id' => $extensionID, - 'upgradeCompatibilityStatus' => (object) [ - 'state' => $upgradeCompatibilityStatus->state, - 'compatibleVersion' => $upgradeUpdateVersion - ], - 'currentCompatibilityStatus' => (object) [ - 'state' => $currentCompatibilityStatus->state, - 'compatibleVersion' => $currentUpdateVersion - ], - 'resultGroup' => $resultGroup, - 'upgradeWarning' => $upgradeWarning, - ]; - } - - $this->app->mimeType = 'application/json'; - $this->app->charSet = 'utf-8'; - $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); - $this->app->sendHeaders(); - - try - { - $return = [ - 'compatibility' => $extensionResults, - 'extensions' => $leftover, - ]; - - echo new JsonResponse($return); - } - catch (\Exception $e) - { - echo $e; - } - - $this->app->close(); - } - - /** - * Fetch and report updates in \JSON format, for AJAX requests - * - * @return void - * - * @since 3.10.10 - */ - public function ajax() - { - if (!Session::checkToken('get')) - { - $this->app->setHeader('status', 403, true); - $this->app->sendHeaders(); - echo Text::_('JINVALID_TOKEN_NOTICE'); - $this->app->close(); - } - - /** @var UpdateModel $model */ - $model = $this->getModel('Update'); - $updateInfo = $model->getUpdateInformation(); - - $update = []; - $update[] = ['version' => $updateInfo['latest']]; - - echo json_encode($update); - - $this->app->close(); - } + /** + * Performs the download of the update package + * + * @return void + * + * @since 2.5.4 + */ + public function download() + { + $this->checkToken(); + + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'joomla_update.php'; + Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); + $user = $this->app->getIdentity(); + + try { + Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_START', $user->id, $user->name, \JVERSION), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + $result = $model->download(); + $file = $result['basename']; + + $message = null; + $messageType = null; + + // The validation was not successful so abort. + if ($result['check'] === false) { + $message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_CHECKSUM_WRONG'); + $messageType = 'error'; + $url = 'index.php?option=com_joomlaupdate'; + + $this->app->setUserState('com_joomlaupdate.file', null); + $this->setRedirect($url, $message, $messageType); + + try { + Log::add($message, Log::ERROR, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + return; + } + + if ($file) { + $this->app->setUserState('com_joomlaupdate.file', $file); + $url = 'index.php?option=com_joomlaupdate&task=update.install&' . $this->app->getSession()->getFormToken() . '=1'; + + try { + Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_FILE', $file), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + } else { + $this->app->setUserState('com_joomlaupdate.file', null); + $url = 'index.php?option=com_joomlaupdate'; + $message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_DOWNLOADFAILED'); + $messageType = 'error'; + } + + $this->setRedirect($url, $message, $messageType); + } + + /** + * Start the installation of the new Joomla! version + * + * @return void + * + * @since 2.5.4 + */ + public function install() + { + $this->checkToken('get'); + $this->app->setUserState('com_joomlaupdate.oldversion', JVERSION); + + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'joomla_update.php'; + Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); + + try { + Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_INSTALL'), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + $file = $this->app->getUserState('com_joomlaupdate.file', null); + $model->createRestorationFile($file); + + $this->display(); + } + + /** + * Finalise the upgrade by running the necessary scripts + * + * @return void + * + * @since 2.5.4 + */ + public function finalise() + { + /* + * Finalize with login page. Used for pre-token check versions + * to allow updates without problems but with a maximum of security. + */ + if (!Session::checkToken('get')) { + $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); + + return; + } + + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'joomla_update.php'; + Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); + + try { + Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_FINALISE'), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + $model->finaliseUpgrade(); + + $url = 'index.php?option=com_joomlaupdate&task=update.cleanup&' . Session::getFormToken() . '=1'; + $this->setRedirect($url); + } + + /** + * Clean up after ourselves + * + * @return void + * + * @since 2.5.4 + */ + public function cleanup() + { + /* + * Cleanup with login page. Used for pre-token check versions to be able to update + * from =< 3.2.7 to allow updates without problems but with a maximum of security. + */ + if (!Session::checkToken('get')) { + $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); + + return; + } + + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'joomla_update.php'; + Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); + + try { + Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_CLEANUP'), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + $model->cleanUp(); + + $url = 'index.php?option=com_joomlaupdate&view=joomlaupdate&layout=complete'; + $this->setRedirect($url); + + try { + Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_COMPLETE', \JVERSION), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + } + + /** + * Purges updates. + * + * @return void + * + * @since 3.0 + */ + public function purge() + { + // Check for request forgeries + $this->checkToken('request'); + + // Purge updates + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + $model->purge(); + + $url = 'index.php?option=com_joomlaupdate'; + $this->setRedirect($url, $model->_message); + } + + /** + * Uploads an update package to the temporary directory, under a random name + * + * @return void + * + * @since 3.6.0 + */ + public function upload() + { + // Check for request forgeries + $this->checkToken(); + + // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? + $this->app->getIdentity()->authorise('core.admin') or jexit(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')); + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + try { + $model->upload(); + } catch (\RuntimeException $e) { + $url = 'index.php?option=com_joomlaupdate'; + $this->setRedirect($url, $e->getMessage(), 'error'); + + return; + } + + $token = Session::getFormToken(); + $url = 'index.php?option=com_joomlaupdate&task=update.captive&' . $token . '=1'; + $this->setRedirect($url); + } + + /** + * Checks there is a valid update package and redirects to the captive view for super admin authentication. + * + * @return void + * + * @since 3.6.0 + */ + public function captive() + { + // Check for request forgeries + $this->checkToken('get'); + + // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? + if (!$this->app->getIdentity()->authorise('core.admin')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + // Do I really have an update package? + $tempFile = $this->app->getUserState('com_joomlaupdate.temp_file', null); + + if (empty($tempFile) || !File::exists($tempFile)) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + $this->input->set('view', 'upload'); + $this->input->set('layout', 'captive'); + + $this->display(); + } + + /** + * Checks the admin has super administrator privileges and then proceeds with the update. + * + * @return void + * + * @since 3.6.0 + */ + public function confirm() + { + // Check for request forgeries + $this->checkToken(); + + // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? + if (!$this->app->getIdentity()->authorise('core.admin')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + // Get the captive file before the session resets + $tempFile = $this->app->getUserState('com_joomlaupdate.temp_file', null); + + // Do I really have an update package? + if (!$model->captiveFileExists()) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + // Try to log in + $credentials = array( + 'username' => $this->input->post->get('username', '', 'username'), + 'password' => $this->input->post->get('passwd', '', 'raw'), + 'secretkey' => $this->input->post->get('secretkey', '', 'raw'), + ); + + $result = $model->captiveLogin($credentials); + + if (!$result) { + $model->removePackageFiles(); + + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + // Set the update source in the session + $this->app->setUserState('com_joomlaupdate.file', basename($tempFile)); + + try { + Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_FILE', $tempFile), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + // Redirect to the actual update page + $url = 'index.php?option=com_joomlaupdate&task=update.install&' . Session::getFormToken() . '=1'; + $this->setRedirect($url); + } + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 2.5.4 + */ + public function display($cachable = false, $urlparams = array()) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'update'); + $vFormat = $document->getType(); + $lName = $this->input->get('layout', 'default', 'string'); + + // Get and render the view. + if ($view = $this->getView($vName, $vFormat)) { + // Get the model for the view. + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + // Push the model into the view (as default). + $view->setModel($model, true); + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + $view->display(); + } + + return $this; + } + + /** + * Checks the admin has super administrator privileges and then proceeds with the final & cleanup steps. + * + * @return void + * + * @since 3.6.3 + */ + public function finaliseconfirm() + { + // Check for request forgeries + $this->checkToken(); + + // Did a non Super User try do this? + if (!$this->app->getIdentity()->authorise('core.admin')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + // Get the model + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + // Try to log in + $credentials = array( + 'username' => $this->input->post->get('username', '', 'username'), + 'password' => $this->input->post->get('passwd', '', 'raw'), + 'secretkey' => $this->input->post->get('secretkey', '', 'raw'), + ); + + $result = $model->captiveLogin($credentials); + + // The login fails? + if (!$result) { + $this->setMessage(Text::_('JGLOBAL_AUTH_INVALID_PASS'), 'warning'); + $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); + + return; + } + + // Redirect back to the actual finalise page + $this->setRedirect('index.php?option=com_joomlaupdate&task=update.finalise&' . Session::getFormToken() . '=1'); + } + + /** + * Fetch Extension update XML proxy. Used to prevent Access-Control-Allow-Origin errors. + * Prints a JSON string. + * Called from JS. + * + * @since 3.10.0 + * @deprecated 5.0 Use batchextensioncompatibility instead. + * + * @return void + */ + public function fetchExtensionCompatibility() + { + $extensionID = $this->input->get('extension-id', '', 'DEFAULT'); + $joomlaTargetVersion = $this->input->get('joomla-target-version', '', 'DEFAULT'); + $joomlaCurrentVersion = $this->input->get('joomla-current-version', '', JVERSION); + $extensionVersion = $this->input->get('extension-version', '', 'DEFAULT'); + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + $upgradeCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaTargetVersion); + $currentCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaCurrentVersion); + $upgradeUpdateVersion = false; + $currentUpdateVersion = false; + + $upgradeWarning = 0; + + if ($upgradeCompatibilityStatus->state == 1 && !empty($upgradeCompatibilityStatus->compatibleVersions)) { + $upgradeUpdateVersion = end($upgradeCompatibilityStatus->compatibleVersions); + } + + if ($currentCompatibilityStatus->state == 1 && !empty($currentCompatibilityStatus->compatibleVersions)) { + $currentUpdateVersion = end($currentCompatibilityStatus->compatibleVersions); + } + + if ($upgradeUpdateVersion !== false) { + $upgradeOldestVersion = $upgradeCompatibilityStatus->compatibleVersions[0]; + + if ($currentUpdateVersion !== false) { + // If there are updates compatible with both CMS versions use these + $bothCompatibleVersions = array_values( + array_intersect($upgradeCompatibilityStatus->compatibleVersions, $currentCompatibilityStatus->compatibleVersions) + ); + + if (!empty($bothCompatibleVersions)) { + $upgradeOldestVersion = $bothCompatibleVersions[0]; + $upgradeUpdateVersion = end($bothCompatibleVersions); + } + } + + if (version_compare($upgradeOldestVersion, $extensionVersion, '>')) { + // Installed version is empty or older than the oldest compatible update: Update required + $resultGroup = 2; + } else { + // Current version is compatible + $resultGroup = 3; + } + + if ($currentUpdateVersion !== false && version_compare($upgradeUpdateVersion, $currentUpdateVersion, '<')) { + // Special case warning when version compatible with target is lower than current + $upgradeWarning = 2; + } + } elseif ($currentUpdateVersion !== false) { + // No compatible version for target version but there is a compatible version for current version + $resultGroup = 1; + } else { + // No update server available + $resultGroup = 1; + } + + // Do we need to capture + $combinedCompatibilityStatus = array( + 'upgradeCompatibilityStatus' => (object) array( + 'state' => $upgradeCompatibilityStatus->state, + 'compatibleVersion' => $upgradeUpdateVersion + ), + 'currentCompatibilityStatus' => (object) array( + 'state' => $currentCompatibilityStatus->state, + 'compatibleVersion' => $currentUpdateVersion + ), + 'resultGroup' => $resultGroup, + 'upgradeWarning' => $upgradeWarning, + ); + + $this->app = Factory::getApplication(); + $this->app->mimeType = 'application/json'; + $this->app->charSet = 'utf-8'; + $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); + $this->app->sendHeaders(); + + try { + echo new JsonResponse($combinedCompatibilityStatus); + } catch (\Exception $e) { + echo $e; + } + + $this->app->close(); + } + + /** + * Determines the compatibility information for a number of extensions. + * + * Called by the Joomla Update JavaScript (PreUpdateChecker.checkNextChunk). + * + * @return void + * @since 4.2.0 + * + */ + public function batchextensioncompatibility() + { + $joomlaTargetVersion = $this->input->post->get('joomla-target-version', '', 'DEFAULT'); + $joomlaCurrentVersion = $this->input->post->get('joomla-current-version', JVERSION); + $extensionInformation = $this->input->post->get('extensions', []); + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + $extensionResults = []; + $leftover = []; + $startTime = microtime(true); + + foreach ($extensionInformation as $information) { + // Only process an extension if we have spent less than 5 seconds already + $currentTime = microtime(true); + + if ($currentTime - $startTime > 5.0) { + $leftover[] = $information; + + continue; + } + + // Get the extension information and fetch its compatibility information + $extensionID = $information['eid'] ?: ''; + $extensionVersion = $information['version'] ?: ''; + $upgradeCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaTargetVersion); + $currentCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaCurrentVersion); + $upgradeUpdateVersion = false; + $currentUpdateVersion = false; + $upgradeWarning = 0; + + if ($upgradeCompatibilityStatus->state == 1 && !empty($upgradeCompatibilityStatus->compatibleVersions)) { + $upgradeUpdateVersion = end($upgradeCompatibilityStatus->compatibleVersions); + } + + if ($currentCompatibilityStatus->state == 1 && !empty($currentCompatibilityStatus->compatibleVersions)) { + $currentUpdateVersion = end($currentCompatibilityStatus->compatibleVersions); + } + + if ($upgradeUpdateVersion !== false) { + $upgradeOldestVersion = $upgradeCompatibilityStatus->compatibleVersions[0]; + + if ($currentUpdateVersion !== false) { + // If there are updates compatible with both CMS versions use these + $bothCompatibleVersions = array_values( + array_intersect($upgradeCompatibilityStatus->compatibleVersions, $currentCompatibilityStatus->compatibleVersions) + ); + + if (!empty($bothCompatibleVersions)) { + $upgradeOldestVersion = $bothCompatibleVersions[0]; + $upgradeUpdateVersion = end($bothCompatibleVersions); + } + } + + if (version_compare($upgradeOldestVersion, $extensionVersion, '>')) { + // Installed version is empty or older than the oldest compatible update: Update required + $resultGroup = 2; + } else { + // Current version is compatible + $resultGroup = 3; + } + + if ($currentUpdateVersion !== false && version_compare($upgradeUpdateVersion, $currentUpdateVersion, '<')) { + // Special case warning when version compatible with target is lower than current + $upgradeWarning = 2; + } + } elseif ($currentUpdateVersion !== false) { + // No compatible version for target version but there is a compatible version for current version + $resultGroup = 1; + } else { + // No update server available + $resultGroup = 1; + } + + // Do we need to capture + $extensionResults[] = [ + 'id' => $extensionID, + 'upgradeCompatibilityStatus' => (object) [ + 'state' => $upgradeCompatibilityStatus->state, + 'compatibleVersion' => $upgradeUpdateVersion + ], + 'currentCompatibilityStatus' => (object) [ + 'state' => $currentCompatibilityStatus->state, + 'compatibleVersion' => $currentUpdateVersion + ], + 'resultGroup' => $resultGroup, + 'upgradeWarning' => $upgradeWarning, + ]; + } + + $this->app->mimeType = 'application/json'; + $this->app->charSet = 'utf-8'; + $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); + $this->app->sendHeaders(); + + try { + $return = [ + 'compatibility' => $extensionResults, + 'extensions' => $leftover, + ]; + + echo new JsonResponse($return); + } catch (\Exception $e) { + echo $e; + } + + $this->app->close(); + } + + /** + * Fetch and report updates in \JSON format, for AJAX requests + * + * @return void + * + * @since 3.10.10 + */ + public function ajax() + { + if (!Session::checkToken('get')) { + $this->app->setHeader('status', 403, true); + $this->app->sendHeaders(); + echo Text::_('JINVALID_TOKEN_NOTICE'); + $this->app->close(); + } + + /** @var UpdateModel $model */ + $model = $this->getModel('Update'); + $updateInfo = $model->getUpdateInformation(); + + $update = []; + $update[] = ['version' => $updateInfo['latest']]; + + echo json_encode($update); + + $this->app->close(); + } } diff --git a/administrator/components/com_joomlaupdate/src/Dispatcher/Dispatcher.php b/administrator/components/com_joomlaupdate/src/Dispatcher/Dispatcher.php index 5857cd7c68861..4b45ce9bee05f 100644 --- a/administrator/components/com_joomlaupdate/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_joomlaupdate/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.admin')) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + /** + * Joomla Update is checked for global core.admin rights - not the usual core.manage for the component + * + * @return void + */ + protected function checkAccess() + { + // Check the user has permission to access this component if in the backend + if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.admin')) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index a1a271503e761..e2610d265fb99 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -1,4 +1,5 @@ get('updatesource', 'nochange')) - { - // "Minor & Patch Release for Current version AND Next Major Release". - case 'next': - $updateURL = 'https://update.joomla.org/core/sts/list_sts.xml'; - break; - - // "Testing" - case 'testing': - $updateURL = 'https://update.joomla.org/core/test/list_test.xml'; - break; - - // "Custom" - // @todo: check if the customurl is valid and not just "not empty". - case 'custom': - if (trim($params->get('customurl', '')) != '') - { - $updateURL = trim($params->get('customurl', '')); - } - else - { - Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM_ERROR'), 'error'); - - return; - } - break; - - /** - * "Minor & Patch Release for Current version (recommended and default)". - * The commented "case" below are for documenting where 'default' and legacy options falls - * case 'default': - * case 'lts': - * case 'sts': (It's shown as "Default" because that option does not exist any more) - * case 'nochange': - */ - default: - $updateURL = 'https://update.joomla.org/core/list.xml'; - } - - $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('us') . '.*') - ->from($db->quoteName('#__update_sites_extensions', 'map')) - ->join( - 'INNER', - $db->quoteName('#__update_sites', 'us'), - $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('map.update_site_id') - ) - ->where($db->quoteName('map.extension_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $update_site = $db->loadObject(); - - if ($update_site->location != $updateURL) - { - // Modify the database record. - $update_site->last_check_timestamp = 0; - $update_site->location = $updateURL; - $db->updateObject('#__update_sites', $update_site, 'update_site_id'); - - // Remove cached updates. - $query->clear() - ->delete($db->quoteName('#__updates')) - ->where($db->quoteName('extension_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - } - } - - /** - * Makes sure that the Joomla! update cache is up-to-date. - * - * @param boolean $force Force reload, ignoring the cache timeout. - * - * @return void - * - * @since 2.5.4 - */ - public function refreshUpdates($force = false) - { - if ($force) - { - $cache_timeout = 0; - } - else - { - $update_params = ComponentHelper::getParams('com_installer'); - $cache_timeout = (int) $update_params->get('cachetimeout', 6); - $cache_timeout = 3600 * $cache_timeout; - } - - $updater = Updater::getInstance(); - $minimumStability = Updater::STABILITY_STABLE; - $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); - - if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), array('testing', 'custom'))) - { - $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); - } - - $reflection = new \ReflectionObject($updater); - $reflectionMethod = $reflection->getMethod('findUpdates'); - $methodParameters = $reflectionMethod->getParameters(); - - if (count($methodParameters) >= 4) - { - // Reinstall support is available in Updater - $updater->findUpdates(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id, $cache_timeout, $minimumStability, true); - } - else - { - $updater->findUpdates(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id, $cache_timeout, $minimumStability); - } - } - - /** - * Makes sure that the Joomla! Update Component Update is in the database and check if there is a new version. - * - * @return boolean True if there is an update else false - * - * @since 4.0.0 - */ - public function getCheckForSelfUpdate() - { - $db = $this->getDatabase(); - - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('element') . ' = ' . $db->quote('com_joomlaupdate')); - $db->setQuery($query); - - try - { - // Get the component extension ID - $joomlaUpdateComponentId = $db->loadResult(); - } - catch (\RuntimeException $e) - { - // Something is wrong here! - $joomlaUpdateComponentId = 0; - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Try the update only if we have an extension id - if ($joomlaUpdateComponentId != 0) - { - // Always force to check for an update! - $cache_timeout = 0; - - $updater = Updater::getInstance(); - $updater->findUpdates($joomlaUpdateComponentId, $cache_timeout, Updater::STABILITY_STABLE); - - // Fetch the update information from the database. - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__updates')) - ->where($db->quoteName('extension_id') . ' = :id') - ->bind(':id', $joomlaUpdateComponentId, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $joomlaUpdateComponentObject = $db->loadObject(); - } - catch (\RuntimeException $e) - { - // Something is wrong here! - $joomlaUpdateComponentObject = null; - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - return !empty($joomlaUpdateComponentObject); - } - - return false; - } - - /** - * Returns an array with the Joomla! update information. - * - * @return array - * - * @since 2.5.4 - */ - public function getUpdateInformation() - { - if ($this->updateInformation) - { - return $this->updateInformation; - } - - // Initialise the return array. - $this->updateInformation = array( - 'installed' => \JVERSION, - 'latest' => null, - 'object' => null, - 'hasUpdate' => false, - 'current' => JVERSION // This is deprecated please use 'installed' or JVERSION directly - ); - - // Fetch the update information from the database. - $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__updates')) - ->where($db->quoteName('extension_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $updateObject = $db->loadObject(); - - if (is_null($updateObject)) - { - // We have not found any update in the database - we seem to be running the latest version. - $this->updateInformation['latest'] = \JVERSION; - - return $this->updateInformation; - } - - // Check whether this is a valid update or not - if (version_compare($updateObject->version, JVERSION, '<')) - { - // This update points to an outdated version. We should not offer to update to this. - $this->updateInformation['latest'] = JVERSION; - - return $this->updateInformation; - } - - $minimumStability = Updater::STABILITY_STABLE; - $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); - - if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), array('testing', 'custom'))) - { - $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); - } - - // Fetch the full update details from the update details URL. - $update = new Update; - $update->loadFromXml($updateObject->detailsurl, $minimumStability); - - // Make sure we use the current information we got from the detailsurl - $this->updateInformation['object'] = $update; - $this->updateInformation['latest'] = $updateObject->version; - - // Check whether this is an update or not. - if (version_compare($this->updateInformation['latest'], JVERSION, '>')) - { - $this->updateInformation['hasUpdate'] = true; - } - - return $this->updateInformation; - } - - /** - * Removes all of the updates from the table and enable all update streams. - * - * @return boolean Result of operation. - * - * @since 3.0 - */ - public function purge() - { - $db = $this->getDatabase(); - - // Modify the database record - $update_site = new \stdClass; - $update_site->last_check_timestamp = 0; - $update_site->enabled = 1; - $update_site->update_site_id = 1; - $db->updateObject('#__update_sites', $update_site, 'update_site_id'); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__updates')) - ->where($db->quoteName('update_site_id') . ' = 1'); - $db->setQuery($query); - - if ($db->execute()) - { - $this->_message = Text::_('COM_JOOMLAUPDATE_CHECKED_UPDATES'); - - return true; - } - else - { - $this->_message = Text::_('COM_JOOMLAUPDATE_FAILED_TO_CHECK_UPDATES'); - - return false; - } - } - - /** - * Downloads the update package to the site. - * - * @return array - * - * @since 2.5.4 - */ - public function download() - { - $updateInfo = $this->getUpdateInformation(); - $packageURL = trim($updateInfo['object']->downloadurl->_data); - $sources = $updateInfo['object']->get('downloadSources', array()); - - // We have to manually follow the redirects here so we set the option to false. - $httpOptions = new Registry; - $httpOptions->set('follow_location', false); - - try - { - $head = HttpFactory::getHttp($httpOptions)->head($packageURL); - } - catch (\RuntimeException $e) - { - // Passing false here -> download failed message - $response['basename'] = false; - - return $response; - } - - // Follow the Location headers until the actual download URL is known - while (isset($head->headers['location'])) - { - $packageURL = (string) $head->headers['location'][0]; - - try - { - $head = HttpFactory::getHttp($httpOptions)->head($packageURL); - } - catch (\RuntimeException $e) - { - // Passing false here -> download failed message - $response['basename'] = false; - - return $response; - } - } - - // Remove protocol, path and query string from URL - $basename = basename($packageURL); - - if (strpos($basename, '?') !== false) - { - $basename = substr($basename, 0, strpos($basename, '?')); - } - - // Find the path to the temp directory and the local package. - $tempdir = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(Factory::getApplication()->get('tmp_path'), 'path'); - $target = $tempdir . '/' . $basename; - $response = []; - - // Do we have a cached file? - $exists = File::exists($target); - - if (!$exists) - { - // Not there, let's fetch it. - $mirror = 0; - - while (!($download = $this->downloadPackage($packageURL, $target)) && isset($sources[$mirror])) - { - $name = $sources[$mirror]; - $packageURL = trim($name->url); - $mirror++; - } - - $response['basename'] = $download; - } - else - { - // Is it a 0-byte file? If so, re-download please. - $filesize = @filesize($target); - - if (empty($filesize)) - { - $mirror = 0; - - while (!($download = $this->downloadPackage($packageURL, $target)) && isset($sources[$mirror])) - { - $name = $sources[$mirror]; - $packageURL = trim($name->url); - $mirror++; - } - - $response['basename'] = $download; - } - - // Yes, it's there, skip downloading. - $response['basename'] = $basename; - } - - $response['check'] = $this->isChecksumValid($target, $updateInfo['object']); - - return $response; - } - - /** - * Return the result of the checksum of a package with the SHA256/SHA384/SHA512 tags in the update server manifest - * - * @param string $packagefile Location of the package to be installed - * @param Update $updateObject The Update Object - * - * @return boolean False in case the validation did not work; true in any other case. - * - * @note This method has been forked from (JInstallerHelper::isChecksumValid) so it - * does not depend on an up-to-date InstallerHelper at the update time - * - * @since 3.9.0 - */ - private function isChecksumValid($packagefile, $updateObject) - { - $hashes = array('sha256', 'sha384', 'sha512'); - - foreach ($hashes as $hash) - { - if ($updateObject->get($hash, false)) - { - $hashPackage = hash_file($hash, $packagefile); - $hashRemote = $updateObject->$hash->_data; - - if ($hashPackage !== $hashRemote) - { - // Return false in case the hash did not match - return false; - } - } - } - - // Well nothing was provided or all worked - return true; - } - - /** - * Downloads a package file to a specific directory - * - * @param string $url The URL to download from - * @param string $target The directory to store the file - * - * @return boolean True on success - * - * @since 2.5.4 - */ - protected function downloadPackage($url, $target) - { - try - { - Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_URL', $url), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - // Make sure the target does not exist. - File::delete($target); - - // Download the package - try - { - $result = HttpFactory::getHttp([], ['curl', 'stream'])->get($url); - } - catch (\RuntimeException $e) - { - return false; - } - - if (!$result || ($result->code != 200 && $result->code != 310)) - { - return false; - } - - // Write the file to disk - File::write($target, $result->body); - - return basename($target); - } - - /** - * Backwards compatibility. Use createUpdateFile() instead. - * - * @param null $basename The basename of the file to create - * - * @return boolean - * @since 2.5.1 - * @deprecated 5.0 - */ - public function createRestorationFile($basename = null): bool - { - return $this->createUpdateFile($basename); - } - - /** - * Create the update.php file and trigger onJoomlaBeforeUpdate event. - * - * The onJoomlaBeforeUpdate event stores the core files for which overrides have been defined. - * This will be compared in the onJoomlaAfterUpdate event with the current filesystem state, - * thereby determining how many and which overrides need to be checked and possibly updated - * after Joomla installed an update. - * - * @param string $basename Optional base path to the file. - * - * @return boolean True if successful; false otherwise. - * - * @since 2.5.4 - */ - public function createUpdateFile($basename = null): bool - { - // Load overrides plugin. - PluginHelper::importPlugin('installer'); - - // Get a password - $password = UserHelper::genRandomPassword(32); - $app = Factory::getApplication(); - - // Trigger event before joomla update. - $app->triggerEvent('onJoomlaBeforeUpdate'); - - // Get the absolute path to site's root. - $siteroot = JPATH_SITE; - - // If the package name is not specified, get it from the update info. - if (empty($basename)) - { - $updateInfo = $this->getUpdateInformation(); - $packageURL = $updateInfo['object']->downloadurl->_data; - $basename = basename($packageURL); - } - - // Get the package name. - $config = $app->getConfig(); - $tempdir = $config->get('tmp_path'); - $file = $tempdir . '/' . $basename; - - $filesize = @filesize($file); - $app->setUserState('com_joomlaupdate.password', $password); - $app->setUserState('com_joomlaupdate.filesize', $filesize); - - $data = "get('updatesource', 'nochange')) { + // "Minor & Patch Release for Current version AND Next Major Release". + case 'next': + $updateURL = 'https://update.joomla.org/core/sts/list_sts.xml'; + break; + + // "Testing" + case 'testing': + $updateURL = 'https://update.joomla.org/core/test/list_test.xml'; + break; + + // "Custom" + // @todo: check if the customurl is valid and not just "not empty". + case 'custom': + if (trim($params->get('customurl', '')) != '') { + $updateURL = trim($params->get('customurl', '')); + } else { + Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM_ERROR'), 'error'); + + return; + } + break; + + /** + * "Minor & Patch Release for Current version (recommended and default)". + * The commented "case" below are for documenting where 'default' and legacy options falls + * case 'default': + * case 'lts': + * case 'sts': (It's shown as "Default" because that option does not exist any more) + * case 'nochange': + */ + default: + $updateURL = 'https://update.joomla.org/core/list.xml'; + } + + $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('us') . '.*') + ->from($db->quoteName('#__update_sites_extensions', 'map')) + ->join( + 'INNER', + $db->quoteName('#__update_sites', 'us'), + $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('map.update_site_id') + ) + ->where($db->quoteName('map.extension_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $update_site = $db->loadObject(); + + if ($update_site->location != $updateURL) { + // Modify the database record. + $update_site->last_check_timestamp = 0; + $update_site->location = $updateURL; + $db->updateObject('#__update_sites', $update_site, 'update_site_id'); + + // Remove cached updates. + $query->clear() + ->delete($db->quoteName('#__updates')) + ->where($db->quoteName('extension_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + } + } + + /** + * Makes sure that the Joomla! update cache is up-to-date. + * + * @param boolean $force Force reload, ignoring the cache timeout. + * + * @return void + * + * @since 2.5.4 + */ + public function refreshUpdates($force = false) + { + if ($force) { + $cache_timeout = 0; + } else { + $update_params = ComponentHelper::getParams('com_installer'); + $cache_timeout = (int) $update_params->get('cachetimeout', 6); + $cache_timeout = 3600 * $cache_timeout; + } + + $updater = Updater::getInstance(); + $minimumStability = Updater::STABILITY_STABLE; + $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); + + if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), array('testing', 'custom'))) { + $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); + } + + $reflection = new \ReflectionObject($updater); + $reflectionMethod = $reflection->getMethod('findUpdates'); + $methodParameters = $reflectionMethod->getParameters(); + + if (count($methodParameters) >= 4) { + // Reinstall support is available in Updater + $updater->findUpdates(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id, $cache_timeout, $minimumStability, true); + } else { + $updater->findUpdates(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id, $cache_timeout, $minimumStability); + } + } + + /** + * Makes sure that the Joomla! Update Component Update is in the database and check if there is a new version. + * + * @return boolean True if there is an update else false + * + * @since 4.0.0 + */ + public function getCheckForSelfUpdate() + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_joomlaupdate')); + $db->setQuery($query); + + try { + // Get the component extension ID + $joomlaUpdateComponentId = $db->loadResult(); + } catch (\RuntimeException $e) { + // Something is wrong here! + $joomlaUpdateComponentId = 0; + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Try the update only if we have an extension id + if ($joomlaUpdateComponentId != 0) { + // Always force to check for an update! + $cache_timeout = 0; + + $updater = Updater::getInstance(); + $updater->findUpdates($joomlaUpdateComponentId, $cache_timeout, Updater::STABILITY_STABLE); + + // Fetch the update information from the database. + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__updates')) + ->where($db->quoteName('extension_id') . ' = :id') + ->bind(':id', $joomlaUpdateComponentId, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $joomlaUpdateComponentObject = $db->loadObject(); + } catch (\RuntimeException $e) { + // Something is wrong here! + $joomlaUpdateComponentObject = null; + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + return !empty($joomlaUpdateComponentObject); + } + + return false; + } + + /** + * Returns an array with the Joomla! update information. + * + * @return array + * + * @since 2.5.4 + */ + public function getUpdateInformation() + { + if ($this->updateInformation) { + return $this->updateInformation; + } + + // Initialise the return array. + $this->updateInformation = array( + 'installed' => \JVERSION, + 'latest' => null, + 'object' => null, + 'hasUpdate' => false, + 'current' => JVERSION // This is deprecated please use 'installed' or JVERSION directly + ); + + // Fetch the update information from the database. + $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__updates')) + ->where($db->quoteName('extension_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $updateObject = $db->loadObject(); + + if (is_null($updateObject)) { + // We have not found any update in the database - we seem to be running the latest version. + $this->updateInformation['latest'] = \JVERSION; + + return $this->updateInformation; + } + + // Check whether this is a valid update or not + if (version_compare($updateObject->version, JVERSION, '<')) { + // This update points to an outdated version. We should not offer to update to this. + $this->updateInformation['latest'] = JVERSION; + + return $this->updateInformation; + } + + $minimumStability = Updater::STABILITY_STABLE; + $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); + + if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), array('testing', 'custom'))) { + $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); + } + + // Fetch the full update details from the update details URL. + $update = new Update(); + $update->loadFromXml($updateObject->detailsurl, $minimumStability); + + // Make sure we use the current information we got from the detailsurl + $this->updateInformation['object'] = $update; + $this->updateInformation['latest'] = $updateObject->version; + + // Check whether this is an update or not. + if (version_compare($this->updateInformation['latest'], JVERSION, '>')) { + $this->updateInformation['hasUpdate'] = true; + } + + return $this->updateInformation; + } + + /** + * Removes all of the updates from the table and enable all update streams. + * + * @return boolean Result of operation. + * + * @since 3.0 + */ + public function purge() + { + $db = $this->getDatabase(); + + // Modify the database record + $update_site = new \stdClass(); + $update_site->last_check_timestamp = 0; + $update_site->enabled = 1; + $update_site->update_site_id = 1; + $db->updateObject('#__update_sites', $update_site, 'update_site_id'); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__updates')) + ->where($db->quoteName('update_site_id') . ' = 1'); + $db->setQuery($query); + + if ($db->execute()) { + $this->_message = Text::_('COM_JOOMLAUPDATE_CHECKED_UPDATES'); + + return true; + } else { + $this->_message = Text::_('COM_JOOMLAUPDATE_FAILED_TO_CHECK_UPDATES'); + + return false; + } + } + + /** + * Downloads the update package to the site. + * + * @return array + * + * @since 2.5.4 + */ + public function download() + { + $updateInfo = $this->getUpdateInformation(); + $packageURL = trim($updateInfo['object']->downloadurl->_data); + $sources = $updateInfo['object']->get('downloadSources', array()); + + // We have to manually follow the redirects here so we set the option to false. + $httpOptions = new Registry(); + $httpOptions->set('follow_location', false); + + try { + $head = HttpFactory::getHttp($httpOptions)->head($packageURL); + } catch (\RuntimeException $e) { + // Passing false here -> download failed message + $response['basename'] = false; + + return $response; + } + + // Follow the Location headers until the actual download URL is known + while (isset($head->headers['location'])) { + $packageURL = (string) $head->headers['location'][0]; + + try { + $head = HttpFactory::getHttp($httpOptions)->head($packageURL); + } catch (\RuntimeException $e) { + // Passing false here -> download failed message + $response['basename'] = false; + + return $response; + } + } + + // Remove protocol, path and query string from URL + $basename = basename($packageURL); + + if (strpos($basename, '?') !== false) { + $basename = substr($basename, 0, strpos($basename, '?')); + } + + // Find the path to the temp directory and the local package. + $tempdir = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(Factory::getApplication()->get('tmp_path'), 'path'); + $target = $tempdir . '/' . $basename; + $response = []; + + // Do we have a cached file? + $exists = File::exists($target); + + if (!$exists) { + // Not there, let's fetch it. + $mirror = 0; + + while (!($download = $this->downloadPackage($packageURL, $target)) && isset($sources[$mirror])) { + $name = $sources[$mirror]; + $packageURL = trim($name->url); + $mirror++; + } + + $response['basename'] = $download; + } else { + // Is it a 0-byte file? If so, re-download please. + $filesize = @filesize($target); + + if (empty($filesize)) { + $mirror = 0; + + while (!($download = $this->downloadPackage($packageURL, $target)) && isset($sources[$mirror])) { + $name = $sources[$mirror]; + $packageURL = trim($name->url); + $mirror++; + } + + $response['basename'] = $download; + } + + // Yes, it's there, skip downloading. + $response['basename'] = $basename; + } + + $response['check'] = $this->isChecksumValid($target, $updateInfo['object']); + + return $response; + } + + /** + * Return the result of the checksum of a package with the SHA256/SHA384/SHA512 tags in the update server manifest + * + * @param string $packagefile Location of the package to be installed + * @param Update $updateObject The Update Object + * + * @return boolean False in case the validation did not work; true in any other case. + * + * @note This method has been forked from (JInstallerHelper::isChecksumValid) so it + * does not depend on an up-to-date InstallerHelper at the update time + * + * @since 3.9.0 + */ + private function isChecksumValid($packagefile, $updateObject) + { + $hashes = array('sha256', 'sha384', 'sha512'); + + foreach ($hashes as $hash) { + if ($updateObject->get($hash, false)) { + $hashPackage = hash_file($hash, $packagefile); + $hashRemote = $updateObject->$hash->_data; + + if ($hashPackage !== $hashRemote) { + // Return false in case the hash did not match + return false; + } + } + } + + // Well nothing was provided or all worked + return true; + } + + /** + * Downloads a package file to a specific directory + * + * @param string $url The URL to download from + * @param string $target The directory to store the file + * + * @return boolean True on success + * + * @since 2.5.4 + */ + protected function downloadPackage($url, $target) + { + try { + Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_URL', $url), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + // Make sure the target does not exist. + File::delete($target); + + // Download the package + try { + $result = HttpFactory::getHttp([], ['curl', 'stream'])->get($url); + } catch (\RuntimeException $e) { + return false; + } + + if (!$result || ($result->code != 200 && $result->code != 310)) { + return false; + } + + // Write the file to disk + File::write($target, $result->body); + + return basename($target); + } + + /** + * Backwards compatibility. Use createUpdateFile() instead. + * + * @param null $basename The basename of the file to create + * + * @return boolean + * @since 2.5.1 + * @deprecated 5.0 + */ + public function createRestorationFile($basename = null): bool + { + return $this->createUpdateFile($basename); + } + + /** + * Create the update.php file and trigger onJoomlaBeforeUpdate event. + * + * The onJoomlaBeforeUpdate event stores the core files for which overrides have been defined. + * This will be compared in the onJoomlaAfterUpdate event with the current filesystem state, + * thereby determining how many and which overrides need to be checked and possibly updated + * after Joomla installed an update. + * + * @param string $basename Optional base path to the file. + * + * @return boolean True if successful; false otherwise. + * + * @since 2.5.4 + */ + public function createUpdateFile($basename = null): bool + { + // Load overrides plugin. + PluginHelper::importPlugin('installer'); + + // Get a password + $password = UserHelper::genRandomPassword(32); + $app = Factory::getApplication(); + + // Trigger event before joomla update. + $app->triggerEvent('onJoomlaBeforeUpdate'); + + // Get the absolute path to site's root. + $siteroot = JPATH_SITE; + + // If the package name is not specified, get it from the update info. + if (empty($basename)) { + $updateInfo = $this->getUpdateInformation(); + $packageURL = $updateInfo['object']->downloadurl->_data; + $basename = basename($packageURL); + } + + // Get the package name. + $config = $app->getConfig(); + $tempdir = $config->get('tmp_path'); + $file = $tempdir . '/' . $basename; + + $filesize = @filesize($file); + $app->setUserState('com_joomlaupdate.password', $password); + $app->setUserState('com_joomlaupdate.filesize', $filesize); + + $data = " '$password', 'setup.sourcefile' => '$file', 'setup.destdir' => '$siteroot', ENDDATA; - $data .= '];'; - - // Remove the old file, if it's there... - $configpath = JPATH_COMPONENT_ADMINISTRATOR . '/update.php'; - - if (File::exists($configpath)) - { - if (!File::delete($configpath)) - { - File::invalidateFileCache($configpath); - @unlink($configpath); - } - } - - // Write new file. First try with File. - $result = File::write($configpath, $data); - - // In case File used FTP but direct access could help. - if (!$result) - { - if (function_exists('file_put_contents')) - { - $result = @file_put_contents($configpath, $data); - - if ($result !== false) - { - $result = true; - } - } - else - { - $fp = @fopen($configpath, 'wt'); - - if ($fp !== false) - { - $result = @fwrite($fp, $data); - - if ($result !== false) - { - $result = true; - } - - @fclose($fp); - } - } - } - - return $result; - } - - /** - * Finalise the upgrade. - * - * This method will do the following: - * * Run the schema update SQL files. - * * Run the Joomla post-update script. - * * Update the manifest cache and #__extensions entry for Joomla itself. - * - * It performs essentially the same function as InstallerFile::install() without the file copy. - * - * @return boolean True on success. - * - * @since 2.5.4 - */ - public function finaliseUpgrade() - { - $installer = Installer::getInstance(); - - $manifest = $installer->isManifest(JPATH_MANIFESTS . '/files/joomla.xml'); - - if ($manifest === false) - { - $installer->abort(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST')); - - return false; - } - - $installer->manifest = $manifest; - - $installer->setUpgrade(true); - $installer->setOverwrite(true); - - $installer->extension = new \Joomla\CMS\Table\Extension($this->getDatabase()); - $installer->extension->load(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id); - - $installer->setAdapter($installer->extension->type); - - $installer->setPath('manifest', JPATH_MANIFESTS . '/files/joomla.xml'); - $installer->setPath('source', JPATH_MANIFESTS . '/files'); - $installer->setPath('extension_root', JPATH_ROOT); - - // Run the script file. - \JLoader::register('JoomlaInstallerScript', JPATH_ADMINISTRATOR . '/components/com_admin/script.php'); - - $manifestClass = new \JoomlaInstallerScript; - - ob_start(); - ob_implicit_flush(false); - - if ($manifestClass && method_exists($manifestClass, 'preflight')) - { - if ($manifestClass->preflight('update', $installer) === false) - { - $installer->abort( - Text::sprintf( - 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', - Text::_('JLIB_INSTALLER_INSTALL') - ) - ); - - return false; - } - } - - // Create msg object; first use here. - $msg = ob_get_contents(); - ob_end_clean(); - - // Get a database connector object. - $db = $this->getDatabase(); - - /* - * Check to see if a file extension by the same name is already installed. - * If it is, then update the table because if the files aren't there - * we can assume that it was (badly) uninstalled. - * If it isn't, add an entry to extensions. - */ - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('file')) - ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - // Install failed, roll back changes. - $installer->abort( - Text::sprintf('JLIB_INSTALLER_ABORT_FILE_ROLLBACK', Text::_('JLIB_INSTALLER_UPDATE'), $e->getMessage()) - ); - - return false; - } - - $id = $db->loadResult(); - $row = new \Joomla\CMS\Table\Extension($this->getDatabase()); - - if ($id) - { - // Load the entry and update the manifest_cache. - $row->load($id); - - // Update name. - $row->set('name', 'files_joomla'); - - // Update manifest. - $row->manifest_cache = $installer->generateManifestCache(); - - if (!$row->store()) - { - // Install failed, roll back changes. - $installer->abort( - Text::sprintf('JLIB_INSTALLER_ABORT_FILE_ROLLBACK', Text::_('JLIB_INSTALLER_UPDATE'), $row->getError()) - ); - - return false; - } - } - else - { - // Add an entry to the extension table with a whole heap of defaults. - $row->set('name', 'files_joomla'); - $row->set('type', 'file'); - $row->set('element', 'joomla'); - - // There is no folder for files so leave it blank. - $row->set('folder', ''); - $row->set('enabled', 1); - $row->set('protected', 0); - $row->set('access', 0); - $row->set('client_id', 0); - $row->set('params', ''); - $row->set('manifest_cache', $installer->generateManifestCache()); - - if (!$row->store()) - { - // Install failed, roll back changes. - $installer->abort(Text::sprintf('JLIB_INSTALLER_ABORT_FILE_INSTALL_ROLLBACK', $row->getError())); - - return false; - } - - // Set the insert id. - $row->set('extension_id', $db->insertid()); - - // Since we have created a module item, we add it to the installation step stack - // so that if we have to rollback the changes we can undo it. - $installer->pushStep(array('type' => 'extension', 'extension_id' => $row->extension_id)); - } - - $result = $installer->parseSchemaUpdates($manifest->update->schemas, $row->extension_id); - - if ($result === false) - { - // Install failed, rollback changes (message already logged by the installer). - $installer->abort(); - - return false; - } - - // Reinitialise the installer's extensions table's properties. - $installer->extension->getFields(true); - - // Start Joomla! 1.6. - ob_start(); - ob_implicit_flush(false); - - if ($manifestClass && method_exists($manifestClass, 'update')) - { - if ($manifestClass->update($installer) === false) - { - // Install failed, rollback changes. - $installer->abort( - Text::sprintf( - 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', - Text::_('JLIB_INSTALLER_INSTALL') - ) - ); - - return false; - } - } - - // Append messages. - $msg .= ob_get_contents(); - ob_end_clean(); - - // Clobber any possible pending updates. - $update = new \Joomla\CMS\Table\Update($this->getDatabase()); - $uid = $update->find( - array('element' => 'joomla', 'type' => 'file', 'client_id' => '0', 'folder' => '') - ); - - if ($uid) - { - $update->delete($uid); - } - - // And now we run the postflight. - ob_start(); - ob_implicit_flush(false); - - if ($manifestClass && method_exists($manifestClass, 'postflight')) - { - $manifestClass->postflight('update', $installer); - } - - // Append messages. - $msg .= ob_get_contents(); - ob_end_clean(); - - if ($msg != '') - { - $installer->set('extension_message', $msg); - } - - // Refresh versionable assets cache. - Factory::getApplication()->flushAssets(); - - return true; - } - - /** - * Removes the extracted package file and trigger onJoomlaAfterUpdate event. - * - * The onJoomlaAfterUpdate event compares the stored list of files previously overridden with - * the updated core files, finding out which files have changed during the update, thereby - * determining how many and which override files need to be checked and possibly updated after - * the Joomla update. - * - * @return void - * - * @since 2.5.4 - */ - public function cleanUp() - { - // Load overrides plugin. - PluginHelper::importPlugin('installer'); - - $app = Factory::getApplication(); - - // Trigger event after joomla update. - $app->triggerEvent('onJoomlaAfterUpdate'); - - // Remove the update package. - $tempdir = $app->get('tmp_path'); - - $file = $app->getUserState('com_joomlaupdate.file', null); - File::delete($tempdir . '/' . $file); - - // Remove the update.php file used in Joomla 4.0.3 and later. - if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/update.php')) - { - File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/update.php'); - } - - // Remove the legacy restoration.php file (when updating from Joomla 4.0.2 and earlier). - if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php')) - { - File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php'); - } - - // Remove the legacy restore_finalisation.php file used in Joomla 4.0.2 and earlier. - if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/restore_finalisation.php')) - { - File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restore_finalisation.php'); - } - - // Remove joomla.xml from the site's root. - if (File::exists(JPATH_ROOT . '/joomla.xml')) - { - File::delete(JPATH_ROOT . '/joomla.xml'); - } - - // Unset the update filename from the session. - $app = Factory::getApplication(); - $app->setUserState('com_joomlaupdate.file', null); - $oldVersion = $app->getUserState('com_joomlaupdate.oldversion'); - - // Trigger event after joomla update. - $app->triggerEvent('onJoomlaAfterUpdate', array($oldVersion)); - $app->setUserState('com_joomlaupdate.oldversion', null); - } - - /** - * Uploads what is presumably an update ZIP file under a mangled name in the temporary directory. - * - * @return void - * - * @since 3.6.0 - */ - public function upload() - { - // Get the uploaded file information. - $input = Factory::getApplication()->input; - - // Do not change the filter type 'raw'. We need this to let files containing PHP code to upload. See \JInputFiles::get. - $userfile = $input->files->get('install_package', null, 'raw'); - - // Make sure that file uploads are enabled in php. - if (!(bool) ini_get('file_uploads')) - { - throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLFILE'), 500); - } - - // Make sure that zlib is loaded so that the package can be unpacked. - if (!extension_loaded('zlib')) - { - throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLZLIB'), 500); - } - - // If there is no uploaded file, we have a problem... - if (!is_array($userfile)) - { - throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_NO_FILE_SELECTED'), 500); - } - - // Is the PHP tmp directory missing? - if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_NO_TMP_DIR)) - { - throw new \RuntimeException( - Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
    ' . - Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), - 500 - ); - } - - // Is the max upload size too small in php.ini? - if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_INI_SIZE)) - { - throw new \RuntimeException( - Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
    ' . Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), - 500 - ); - } - - // Check if there was a different problem uploading the file. - if ($userfile['error'] || $userfile['size'] < 1) - { - throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 500); - } - - // Build the appropriate paths. - $tmp_dest = tempnam(Factory::getApplication()->get('tmp_path'), 'ju'); - $tmp_src = $userfile['tmp_name']; - - // Move uploaded file. - $result = File::upload($tmp_src, $tmp_dest, false, true); - - if (!$result) - { - throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 500); - } - - Factory::getApplication()->setUserState('com_joomlaupdate.temp_file', $tmp_dest); - } - - /** - * Checks the super admin credentials are valid for the currently logged in users - * - * @param array $credentials The credentials to authenticate the user with - * - * @return boolean - * - * @since 3.6.0 - */ - public function captiveLogin($credentials) - { - // Make sure the username matches - $username = $credentials['username'] ?? null; - $user = Factory::getUser(); - - if (strtolower($user->username) != strtolower($username)) - { - return false; - } - - // Make sure the user is authorised - if (!$user->authorise('core.admin')) - { - return false; - } - - // Get the global Authentication object. - $authenticate = Authentication::getInstance(); - $response = $authenticate->authenticate($credentials); - - if ($response->status !== Authentication::STATUS_SUCCESS) - { - return false; - } - - return true; - } - - /** - * Does the captive (temporary) file we uploaded before still exist? - * - * @return boolean - * - * @since 3.6.0 - */ - public function captiveFileExists() - { - $file = Factory::getApplication()->getUserState('com_joomlaupdate.temp_file', null); - - if (empty($file) || !File::exists($file)) - { - return false; - } - - return true; - } - - /** - * Remove the captive (temporary) file we uploaded before and the . - * - * @return void - * - * @since 3.6.0 - */ - public function removePackageFiles() - { - $files = array( - Factory::getApplication()->getUserState('com_joomlaupdate.temp_file', null), - Factory::getApplication()->getUserState('com_joomlaupdate.file', null), - ); - - foreach ($files as $file) - { - if ($file !== null && File::exists($file)) - { - File::delete($file); - } - } - } - - /** - * Gets PHP options. - * @todo: Outsource, build common code base for pre install and pre update check - * - * @return array Array of PHP config options - * - * @since 3.10.0 - */ - public function getPhpOptions() - { - $options = array(); - - /* - * Check the PHP Version. It is already checked in Update. - * A Joomla! Update which is not supported by current PHP - * version is not shown. So this check is actually unnecessary. - */ - $option = new \stdClass; - $option->label = Text::sprintf('INSTL_PHP_VERSION_NEWER', $this->getTargetMinimumPHPVersion()); - $option->state = $this->isPhpVersionSupported(); - $option->notice = null; - $options[] = $option; - - // Check for zlib support. - $option = new \stdClass; - $option->label = Text::_('INSTL_ZLIB_COMPRESSION_SUPPORT'); - $option->state = extension_loaded('zlib'); - $option->notice = null; - $options[] = $option; - - // Check for XML support. - $option = new \stdClass; - $option->label = Text::_('INSTL_XML_SUPPORT'); - $option->state = extension_loaded('xml'); - $option->notice = null; - $options[] = $option; - - // Check for mbstring options. - if (extension_loaded('mbstring')) - { - // Check for default MB language. - $option = new \stdClass; - $option->label = Text::_('INSTL_MB_LANGUAGE_IS_DEFAULT'); - $option->state = strtolower(ini_get('mbstring.language')) === 'neutral'; - $option->notice = $option->state ? null : Text::_('INSTL_NOTICEMBLANGNOTDEFAULT'); - $options[] = $option; - - // Check for MB function overload. - $option = new \stdClass; - $option->label = Text::_('INSTL_MB_STRING_OVERLOAD_OFF'); - $option->state = ini_get('mbstring.func_overload') == 0; - $option->notice = $option->state ? null : Text::_('INSTL_NOTICEMBSTRINGOVERLOAD'); - $options[] = $option; - } - - // Check for a missing native parse_ini_file implementation. - $option = new \stdClass; - $option->label = Text::_('INSTL_PARSE_INI_FILE_AVAILABLE'); - $option->state = $this->getIniParserAvailability(); - $option->notice = null; - $options[] = $option; - - // Check for missing native json_encode / json_decode support. - $option = new \stdClass; - $option->label = Text::_('INSTL_JSON_SUPPORT_AVAILABLE'); - $option->state = function_exists('json_encode') && function_exists('json_decode'); - $option->notice = null; - $options[] = $option; - $updateInformation = $this->getUpdateInformation(); - - // Check if configured database is compatible with the next major version of Joomla - $nextMajorVersion = Version::MAJOR_VERSION + 1; - - if (version_compare($updateInformation['latest'], (string) $nextMajorVersion, '>=')) - { - $option = new \stdClass; - $option->label = Text::sprintf('INSTL_DATABASE_SUPPORTED', $this->getConfiguredDatabaseType()); - $option->state = $this->isDatabaseTypeSupported(); - $option->notice = null; - $options[] = $option; - } - - // Check if database structure is up to date - $option = new \stdClass; - $option->label = Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_TITLE'); - $option->state = $this->getDatabaseSchemaCheck(); - $option->notice = $option->state ? null : Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_NOTICE'); - $options[] = $option; - - return $options; - } - - /** - * Gets PHP Settings. - * @todo: Outsource, build common code base for pre install and pre update check - * - * @return array - * - * @since 3.10.0 - */ - public function getPhpSettings() - { - $settings = array(); - - // Check for display errors. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_DISPLAY_ERRORS'); - $setting->state = (bool) ini_get('display_errors'); - $setting->recommended = false; - $settings[] = $setting; - - // Check for file uploads. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_FILE_UPLOADS'); - $setting->state = (bool) ini_get('file_uploads'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for output buffering. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_OUTPUT_BUFFERING'); - $setting->state = (int) ini_get('output_buffering') !== 0; - $setting->recommended = false; - $settings[] = $setting; - - // Check for session auto-start. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_SESSION_AUTO_START'); - $setting->state = (bool) ini_get('session.auto_start'); - $setting->recommended = false; - $settings[] = $setting; - - // Check for native ZIP support. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_ZIP_SUPPORT_AVAILABLE'); - $setting->state = function_exists('zip_open') && function_exists('zip_read'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for GD support - $setting = new \stdClass; - $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'GD'); - $setting->state = extension_loaded('gd'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for iconv support - $setting = new \stdClass; - $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'iconv'); - $setting->state = function_exists('iconv'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for intl support - $setting = new \stdClass; - $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'intl'); - $setting->state = function_exists('transliterator_transliterate'); - $setting->recommended = true; - $settings[] = $setting; - - return $settings; - } - - /** - * Returns the configured database type id (mysqli or sqlsrv or ...) - * - * @return string - * - * @since 3.10.0 - */ - private function getConfiguredDatabaseType() - { - return Factory::getApplication()->get('dbtype'); - } - - /** - * Returns true, if J! version is < 4 or current configured - * database type is compatible with the update. - * - * @return boolean - * - * @since 3.10.0 - */ - public function isDatabaseTypeSupported() - { - $updateInformation = $this->getUpdateInformation(); - $nextMajorVersion = Version::MAJOR_VERSION + 1; - - // Check if configured database is compatible with Joomla 4 - if (version_compare($updateInformation['latest'], (string) $nextMajorVersion, '>=')) - { - $unsupportedDatabaseTypes = array('sqlsrv', 'sqlazure'); - $currentDatabaseType = $this->getConfiguredDatabaseType(); - - return !in_array($currentDatabaseType, $unsupportedDatabaseTypes); - } - - return true; - } - - - /** - * Returns true, if current installed php version is compatible with the update. - * - * @return boolean - * - * @since 3.10.0 - */ - public function isPhpVersionSupported() - { - return version_compare(PHP_VERSION, $this->getTargetMinimumPHPVersion(), '>='); - } - - /** - * Returns the PHP minimum version for the update. - * Returns JOOMLA_MINIMUM_PHP, if there is no information given. - * - * @return string - * - * @since 3.10.0 - */ - private function getTargetMinimumPHPVersion() - { - $updateInformation = $this->getUpdateInformation(); - - return isset($updateInformation['object']->php_minimum) ? - $updateInformation['object']->php_minimum->_data : - JOOMLA_MINIMUM_PHP; - } - - /** - * Checks the availability of the parse_ini_file and parse_ini_string functions. - * @todo: Outsource, build common code base for pre install and pre update check - * - * @return boolean True if the method exists. - * - * @since 3.10.0 - */ - public function getIniParserAvailability() - { - $disabledFunctions = ini_get('disable_functions'); - - if (!empty($disabledFunctions)) - { - // Attempt to detect them in the PHP INI disable_functions variable. - $disabledFunctions = explode(',', trim($disabledFunctions)); - $numberOfDisabledFunctions = count($disabledFunctions); - - for ($i = 0; $i < $numberOfDisabledFunctions; $i++) - { - $disabledFunctions[$i] = trim($disabledFunctions[$i]); - } - - $result = !in_array('parse_ini_string', $disabledFunctions); - } - else - { - // Attempt to detect their existence; even pure PHP implementations of them will trigger a positive response, though. - $result = function_exists('parse_ini_string'); - } - - return $result; - } - - - /** - * Check if database structure is up to date - * - * @return boolean True if ok, false if not. - * - * @since 3.10.0 - */ - private function getDatabaseSchemaCheck(): bool - { - $mvcFactory = $this->bootComponent('com_installer')->getMVCFactory(); - - /** @var \Joomla\Component\Installer\Administrator\Model\DatabaseModel $model */ - $model = $mvcFactory->createModel('Database', 'Administrator'); - - // Check if no default text filters found - if (!$model->getDefaultTextFilters()) - { - return false; - } - - $coreExtensionInfo = \Joomla\CMS\Extension\ExtensionHelper::getExtensionRecord('joomla', 'file'); - $cache = new \Joomla\Registry\Registry($coreExtensionInfo->manifest_cache); - - $updateVersion = $cache->get('version'); - - // Check if database update version does not match CMS version - if (version_compare($updateVersion, JVERSION) != 0) - { - return false; - } - - // Ensure we only get information for core - $model->setState('filter.extension_id', $coreExtensionInfo->extension_id); - - // We're filtering by a single extension which must always exist - so can safely access this through - // element 0 of the array - $changeInformation = $model->getItems()[0]; - - // Check if schema errors found - if ($changeInformation['errorsCount'] !== 0) - { - return false; - } - - // Check if database schema version does not match CMS version - if ($model->getSchemaVersion($coreExtensionInfo->extension_id) != $changeInformation['schema']) - { - return false; - } - - // No database problems found - return true; - } - - /** - * Gets an array containing all installed extensions, that are not core extensions. - * - * @return array name,version,updateserver - * - * @since 3.10.0 - */ - public function getNonCoreExtensions() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select( - [ - $db->quoteName('ex.name'), - $db->quoteName('ex.extension_id'), - $db->quoteName('ex.manifest_cache'), - $db->quoteName('ex.type'), - $db->quoteName('ex.folder'), - $db->quoteName('ex.element'), - $db->quoteName('ex.client_id'), - ] - ) - ->from($db->quoteName('#__extensions', 'ex')) - ->where($db->quoteName('ex.package_id') . ' = 0') - ->whereNotIn($db->quoteName('ex.extension_id'), ExtensionHelper::getCoreExtensionIds()); - - $db->setQuery($query); - $rows = $db->loadObjectList(); - - foreach ($rows as $extension) - { - $decode = json_decode($extension->manifest_cache); - - // Remove unused fields so they do not cause javascript errors during pre-update check - unset($decode->description); - unset($decode->copyright); - unset($decode->creationDate); - - $this->translateExtensionName($extension); - $extension->version - = isset($decode->version) ? $decode->version : Text::_('COM_JOOMLAUPDATE_PREUPDATE_UNKNOWN_EXTENSION_MANIFESTCACHE_VERSION'); - unset($extension->manifest_cache); - $extension->manifest_cache = $decode; - } - - return $rows; - } - - /** - * Gets an array containing all installed and enabled plugins, that are not core plugins. - * - * @param array $folderFilter Limit the list of plugins to a specific set of folder values - * - * @return array name,version,updateserver - * - * @since 3.10.0 - */ - public function getNonCorePlugins($folderFilter = ['system','user','authentication','actionlog','multifactorauth']) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select( - $db->qn('ex.name') . ', ' . - $db->qn('ex.extension_id') . ', ' . - $db->qn('ex.manifest_cache') . ', ' . - $db->qn('ex.type') . ', ' . - $db->qn('ex.folder') . ', ' . - $db->qn('ex.element') . ', ' . - $db->qn('ex.client_id') . ', ' . - $db->qn('ex.package_id') - )->from( - $db->qn('#__extensions', 'ex') - )->where( - $db->qn('ex.type') . ' = ' . $db->quote('plugin') - )->where( - $db->qn('ex.enabled') . ' = 1' - )->whereNotIn( - $db->quoteName('ex.extension_id'), ExtensionHelper::getCoreExtensionIds() - ); - - if (count($folderFilter) > 0) - { - $folderFilter = array_map(array($db, 'quote'), $folderFilter); - - $query->where($db->qn('folder') . ' IN (' . implode(',', $folderFilter) . ')'); - } - - $db->setQuery($query); - $rows = $db->loadObjectList(); - - foreach ($rows as $plugin) - { - $decode = json_decode($plugin->manifest_cache); - - // Remove unused fields so they do not cause javascript errors during pre-update check - unset($decode->description); - unset($decode->copyright); - unset($decode->creationDate); - - $this->translateExtensionName($plugin); - $plugin->version = $decode->version ?? Text::_('COM_JOOMLAUPDATE_PREUPDATE_UNKNOWN_EXTENSION_MANIFESTCACHE_VERSION'); - unset($plugin->manifest_cache); - $plugin->manifest_cache = $decode; - } - - return $rows; - } - - /** - * Called by controller's fetchExtensionCompatibility, which is called via AJAX. - * - * @param string $extensionID The ID of the checked extension - * @param string $joomlaTargetVersion Target version of Joomla - * - * @return object - * - * @since 3.10.0 - */ - public function fetchCompatibility($extensionID, $joomlaTargetVersion) - { - $updateSites = $this->getUpdateSitesInfo($extensionID); - - if (empty($updateSites)) - { - return (object) array('state' => 2); - } - - foreach ($updateSites as $updateSite) - { - if ($updateSite['type'] === 'collection') - { - $updateFileUrls = $this->getCollectionDetailsUrls($updateSite, $joomlaTargetVersion); - - foreach ($updateFileUrls as $updateFileUrl) - { - $compatibleVersions = $this->checkCompatibility($updateFileUrl, $joomlaTargetVersion); - - // Return the compatible versions - return (object) array('state' => 1, 'compatibleVersions' => $compatibleVersions); - } - } - else - { - $compatibleVersions = $this->checkCompatibility($updateSite['location'], $joomlaTargetVersion); - - // Return the compatible versions - return (object) array('state' => 1, 'compatibleVersions' => $compatibleVersions); - } - } - - // In any other case we mark this extension as not compatible - return (object) array('state' => 0); - } - - /** - * Returns records with update sites and extension information for a given extension ID. - * - * @param int $extensionID The extension ID - * - * @return array - * - * @since 3.10.0 - */ - private function getUpdateSitesInfo($extensionID) - { - $id = (int) $extensionID; - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select( - $db->qn('us.type') . ', ' . - $db->qn('us.location') . ', ' . - $db->qn('e.element') . ' AS ' . $db->qn('ext_element') . ', ' . - $db->qn('e.type') . ' AS ' . $db->qn('ext_type') . ', ' . - $db->qn('e.folder') . ' AS ' . $db->qn('ext_folder') - ) - ->from($db->quoteName('#__update_sites', 'us')) - ->join( - 'LEFT', - $db->quoteName('#__update_sites_extensions', 'ue'), - $db->quoteName('ue.update_site_id') . ' = ' . $db->quoteName('us.update_site_id') - ) - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('ue.extension_id') - ) - ->where($db->quoteName('e.extension_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - $result = $db->loadAssocList(); - - if (!is_array($result)) - { - return array(); - } - - return $result; - } - - /** - * Method to get details URLs from a collection update site for given extension and Joomla target version. - * - * @param array $updateSiteInfo The update site and extension information record to process - * @param string $joomlaTargetVersion The Joomla! version to test against, - * - * @return array An array of URLs. - * - * @since 3.10.0 - */ - private function getCollectionDetailsUrls($updateSiteInfo, $joomlaTargetVersion) - { - $return = array(); - - $http = new Http; - - try - { - $response = $http->get($updateSiteInfo['location']); - } - catch (\RuntimeException $e) - { - $response = null; - } - - if ($response === null || $response->code !== 200) - { - return $return; - } - - $updateSiteXML = simplexml_load_string($response->body); - - foreach ($updateSiteXML->extension as $extension) - { - $attribs = new \stdClass; - - $attribs->element = ''; - $attribs->type = ''; - $attribs->folder = ''; - $attribs->targetplatformversion = ''; - - foreach ($extension->attributes() as $key => $value) - { - $attribs->$key = (string) $value; - } - - if ($attribs->element === $updateSiteInfo['ext_element'] - && $attribs->type === $updateSiteInfo['ext_type'] - && $attribs->folder === $updateSiteInfo['ext_folder'] - && preg_match('/^' . $attribs->targetplatformversion . '/', $joomlaTargetVersion)) - { - $return[] = (string) $extension['detailsurl']; - } - } - - return $return; - } - - /** - * Method to check non core extensions for compatibility. - * - * @param string $updateFileUrl The items update XML url. - * @param string $joomlaTargetVersion The Joomla! version to test against - * - * @return array An array of strings with compatible version numbers - * - * @since 3.10.0 - */ - private function checkCompatibility($updateFileUrl, $joomlaTargetVersion) - { - $minimumStability = ComponentHelper::getParams('com_installer')->get('minimum_stability', Updater::STABILITY_STABLE); - - $update = new Update; - $update->set('jversion.full', $joomlaTargetVersion); - $update->loadFromXml($updateFileUrl, $minimumStability); - - $compatibleVersions = $update->get('compatibleVersions'); - - // Check if old version of the updater library - if (!isset($compatibleVersions)) - { - $downloadUrl = $update->get('downloadurl'); - $updateVersion = $update->get('version'); - - return empty($downloadUrl) || empty($downloadUrl->_data) || empty($updateVersion) ? array() : array($updateVersion->_data); - } - - usort($compatibleVersions, 'version_compare'); - - return $compatibleVersions; - } - - /** - * Translates an extension name - * - * @param object &$item The extension of which the name needs to be translated - * - * @return void - * - * @since 3.10.0 - */ - protected function translateExtensionName(&$item) - { - // @todo: Cleanup duplicated code. from com_installer/models/extension.php - $lang = Factory::getLanguage(); - $path = $item->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE; - - $extension = $item->element; - $source = JPATH_SITE; - - switch ($item->type) - { - case 'component': - $extension = $item->element; - $source = $path . '/components/' . $extension; - break; - case 'module': - $extension = $item->element; - $source = $path . '/modules/' . $extension; - break; - case 'file': - $extension = 'files_' . $item->element; - break; - case 'library': - $extension = 'lib_' . $item->element; - break; - case 'plugin': - $extension = 'plg_' . $item->folder . '_' . $item->element; - $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; - break; - case 'template': - $extension = 'tpl_' . $item->element; - $source = $path . '/templates/' . $item->element; - } - - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) - || $lang->load("$extension.sys", $source); - $lang->load($extension, JPATH_ADMINISTRATOR) - || $lang->load($extension, $source); - - // Translate the extension name if possible - $item->name = strip_tags(Text::_($item->name)); - } - - /** - * Checks whether a given template is active - * - * @param string $template The template name to be checked - * - * @return boolean - * - * @since 3.10.4 - */ - public function isTemplateActive($template) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select( - $db->qn( - array( - 'id', - 'home' - ) - ) - )->from( - $db->qn('#__template_styles') - )->where( - $db->qn('template') . ' = :template' - )->bind(':template', $template, ParameterType::STRING); - - $templates = $db->setQuery($query)->loadObjectList(); - - $home = array_filter( - $templates, - function ($value) - { - return $value->home > 0; - } - ); - - $ids = ArrayHelper::getColumn($templates, 'id'); - - $menu = false; - - if (count($ids)) - { - $query = $db->getQuery(true); - - $query->select( - 'COUNT(*)' - )->from( - $db->qn('#__menu') - )->whereIn( - $db->qn('template_style_id'), $ids - ); - - $menu = $db->setQuery($query)->loadResult() > 0; - } - - return $home || $menu; - } + $data .= '];'; + + // Remove the old file, if it's there... + $configpath = JPATH_COMPONENT_ADMINISTRATOR . '/update.php'; + + if (File::exists($configpath)) { + if (!File::delete($configpath)) { + File::invalidateFileCache($configpath); + @unlink($configpath); + } + } + + // Write new file. First try with File. + $result = File::write($configpath, $data); + + // In case File used FTP but direct access could help. + if (!$result) { + if (function_exists('file_put_contents')) { + $result = @file_put_contents($configpath, $data); + + if ($result !== false) { + $result = true; + } + } else { + $fp = @fopen($configpath, 'wt'); + + if ($fp !== false) { + $result = @fwrite($fp, $data); + + if ($result !== false) { + $result = true; + } + + @fclose($fp); + } + } + } + + return $result; + } + + /** + * Finalise the upgrade. + * + * This method will do the following: + * * Run the schema update SQL files. + * * Run the Joomla post-update script. + * * Update the manifest cache and #__extensions entry for Joomla itself. + * + * It performs essentially the same function as InstallerFile::install() without the file copy. + * + * @return boolean True on success. + * + * @since 2.5.4 + */ + public function finaliseUpgrade() + { + $installer = Installer::getInstance(); + + $manifest = $installer->isManifest(JPATH_MANIFESTS . '/files/joomla.xml'); + + if ($manifest === false) { + $installer->abort(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST')); + + return false; + } + + $installer->manifest = $manifest; + + $installer->setUpgrade(true); + $installer->setOverwrite(true); + + $installer->extension = new \Joomla\CMS\Table\Extension($this->getDatabase()); + $installer->extension->load(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id); + + $installer->setAdapter($installer->extension->type); + + $installer->setPath('manifest', JPATH_MANIFESTS . '/files/joomla.xml'); + $installer->setPath('source', JPATH_MANIFESTS . '/files'); + $installer->setPath('extension_root', JPATH_ROOT); + + // Run the script file. + \JLoader::register('JoomlaInstallerScript', JPATH_ADMINISTRATOR . '/components/com_admin/script.php'); + + $manifestClass = new \JoomlaInstallerScript(); + + ob_start(); + ob_implicit_flush(false); + + if ($manifestClass && method_exists($manifestClass, 'preflight')) { + if ($manifestClass->preflight('update', $installer) === false) { + $installer->abort( + Text::sprintf( + 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', + Text::_('JLIB_INSTALLER_INSTALL') + ) + ); + + return false; + } + } + + // Create msg object; first use here. + $msg = ob_get_contents(); + ob_end_clean(); + + // Get a database connector object. + $db = $this->getDatabase(); + + /* + * Check to see if a file extension by the same name is already installed. + * If it is, then update the table because if the files aren't there + * we can assume that it was (badly) uninstalled. + * If it isn't, add an entry to extensions. + */ + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('file')) + ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + // Install failed, roll back changes. + $installer->abort( + Text::sprintf('JLIB_INSTALLER_ABORT_FILE_ROLLBACK', Text::_('JLIB_INSTALLER_UPDATE'), $e->getMessage()) + ); + + return false; + } + + $id = $db->loadResult(); + $row = new \Joomla\CMS\Table\Extension($this->getDatabase()); + + if ($id) { + // Load the entry and update the manifest_cache. + $row->load($id); + + // Update name. + $row->set('name', 'files_joomla'); + + // Update manifest. + $row->manifest_cache = $installer->generateManifestCache(); + + if (!$row->store()) { + // Install failed, roll back changes. + $installer->abort( + Text::sprintf('JLIB_INSTALLER_ABORT_FILE_ROLLBACK', Text::_('JLIB_INSTALLER_UPDATE'), $row->getError()) + ); + + return false; + } + } else { + // Add an entry to the extension table with a whole heap of defaults. + $row->set('name', 'files_joomla'); + $row->set('type', 'file'); + $row->set('element', 'joomla'); + + // There is no folder for files so leave it blank. + $row->set('folder', ''); + $row->set('enabled', 1); + $row->set('protected', 0); + $row->set('access', 0); + $row->set('client_id', 0); + $row->set('params', ''); + $row->set('manifest_cache', $installer->generateManifestCache()); + + if (!$row->store()) { + // Install failed, roll back changes. + $installer->abort(Text::sprintf('JLIB_INSTALLER_ABORT_FILE_INSTALL_ROLLBACK', $row->getError())); + + return false; + } + + // Set the insert id. + $row->set('extension_id', $db->insertid()); + + // Since we have created a module item, we add it to the installation step stack + // so that if we have to rollback the changes we can undo it. + $installer->pushStep(array('type' => 'extension', 'extension_id' => $row->extension_id)); + } + + $result = $installer->parseSchemaUpdates($manifest->update->schemas, $row->extension_id); + + if ($result === false) { + // Install failed, rollback changes (message already logged by the installer). + $installer->abort(); + + return false; + } + + // Reinitialise the installer's extensions table's properties. + $installer->extension->getFields(true); + + // Start Joomla! 1.6. + ob_start(); + ob_implicit_flush(false); + + if ($manifestClass && method_exists($manifestClass, 'update')) { + if ($manifestClass->update($installer) === false) { + // Install failed, rollback changes. + $installer->abort( + Text::sprintf( + 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', + Text::_('JLIB_INSTALLER_INSTALL') + ) + ); + + return false; + } + } + + // Append messages. + $msg .= ob_get_contents(); + ob_end_clean(); + + // Clobber any possible pending updates. + $update = new \Joomla\CMS\Table\Update($this->getDatabase()); + $uid = $update->find( + array('element' => 'joomla', 'type' => 'file', 'client_id' => '0', 'folder' => '') + ); + + if ($uid) { + $update->delete($uid); + } + + // And now we run the postflight. + ob_start(); + ob_implicit_flush(false); + + if ($manifestClass && method_exists($manifestClass, 'postflight')) { + $manifestClass->postflight('update', $installer); + } + + // Append messages. + $msg .= ob_get_contents(); + ob_end_clean(); + + if ($msg != '') { + $installer->set('extension_message', $msg); + } + + // Refresh versionable assets cache. + Factory::getApplication()->flushAssets(); + + return true; + } + + /** + * Removes the extracted package file and trigger onJoomlaAfterUpdate event. + * + * The onJoomlaAfterUpdate event compares the stored list of files previously overridden with + * the updated core files, finding out which files have changed during the update, thereby + * determining how many and which override files need to be checked and possibly updated after + * the Joomla update. + * + * @return void + * + * @since 2.5.4 + */ + public function cleanUp() + { + // Load overrides plugin. + PluginHelper::importPlugin('installer'); + + $app = Factory::getApplication(); + + // Trigger event after joomla update. + $app->triggerEvent('onJoomlaAfterUpdate'); + + // Remove the update package. + $tempdir = $app->get('tmp_path'); + + $file = $app->getUserState('com_joomlaupdate.file', null); + File::delete($tempdir . '/' . $file); + + // Remove the update.php file used in Joomla 4.0.3 and later. + if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/update.php')) { + File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/update.php'); + } + + // Remove the legacy restoration.php file (when updating from Joomla 4.0.2 and earlier). + if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php')) { + File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php'); + } + + // Remove the legacy restore_finalisation.php file used in Joomla 4.0.2 and earlier. + if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/restore_finalisation.php')) { + File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restore_finalisation.php'); + } + + // Remove joomla.xml from the site's root. + if (File::exists(JPATH_ROOT . '/joomla.xml')) { + File::delete(JPATH_ROOT . '/joomla.xml'); + } + + // Unset the update filename from the session. + $app = Factory::getApplication(); + $app->setUserState('com_joomlaupdate.file', null); + $oldVersion = $app->getUserState('com_joomlaupdate.oldversion'); + + // Trigger event after joomla update. + $app->triggerEvent('onJoomlaAfterUpdate', array($oldVersion)); + $app->setUserState('com_joomlaupdate.oldversion', null); + } + + /** + * Uploads what is presumably an update ZIP file under a mangled name in the temporary directory. + * + * @return void + * + * @since 3.6.0 + */ + public function upload() + { + // Get the uploaded file information. + $input = Factory::getApplication()->input; + + // Do not change the filter type 'raw'. We need this to let files containing PHP code to upload. See \JInputFiles::get. + $userfile = $input->files->get('install_package', null, 'raw'); + + // Make sure that file uploads are enabled in php. + if (!(bool) ini_get('file_uploads')) { + throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLFILE'), 500); + } + + // Make sure that zlib is loaded so that the package can be unpacked. + if (!extension_loaded('zlib')) { + throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLZLIB'), 500); + } + + // If there is no uploaded file, we have a problem... + if (!is_array($userfile)) { + throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_NO_FILE_SELECTED'), 500); + } + + // Is the PHP tmp directory missing? + if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_NO_TMP_DIR)) { + throw new \RuntimeException( + Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
    ' . + Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), + 500 + ); + } + + // Is the max upload size too small in php.ini? + if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_INI_SIZE)) { + throw new \RuntimeException( + Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
    ' . Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), + 500 + ); + } + + // Check if there was a different problem uploading the file. + if ($userfile['error'] || $userfile['size'] < 1) { + throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 500); + } + + // Build the appropriate paths. + $tmp_dest = tempnam(Factory::getApplication()->get('tmp_path'), 'ju'); + $tmp_src = $userfile['tmp_name']; + + // Move uploaded file. + $result = File::upload($tmp_src, $tmp_dest, false, true); + + if (!$result) { + throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 500); + } + + Factory::getApplication()->setUserState('com_joomlaupdate.temp_file', $tmp_dest); + } + + /** + * Checks the super admin credentials are valid for the currently logged in users + * + * @param array $credentials The credentials to authenticate the user with + * + * @return boolean + * + * @since 3.6.0 + */ + public function captiveLogin($credentials) + { + // Make sure the username matches + $username = $credentials['username'] ?? null; + $user = Factory::getUser(); + + if (strtolower($user->username) != strtolower($username)) { + return false; + } + + // Make sure the user is authorised + if (!$user->authorise('core.admin')) { + return false; + } + + // Get the global Authentication object. + $authenticate = Authentication::getInstance(); + $response = $authenticate->authenticate($credentials); + + if ($response->status !== Authentication::STATUS_SUCCESS) { + return false; + } + + return true; + } + + /** + * Does the captive (temporary) file we uploaded before still exist? + * + * @return boolean + * + * @since 3.6.0 + */ + public function captiveFileExists() + { + $file = Factory::getApplication()->getUserState('com_joomlaupdate.temp_file', null); + + if (empty($file) || !File::exists($file)) { + return false; + } + + return true; + } + + /** + * Remove the captive (temporary) file we uploaded before and the . + * + * @return void + * + * @since 3.6.0 + */ + public function removePackageFiles() + { + $files = array( + Factory::getApplication()->getUserState('com_joomlaupdate.temp_file', null), + Factory::getApplication()->getUserState('com_joomlaupdate.file', null), + ); + + foreach ($files as $file) { + if ($file !== null && File::exists($file)) { + File::delete($file); + } + } + } + + /** + * Gets PHP options. + * @todo: Outsource, build common code base for pre install and pre update check + * + * @return array Array of PHP config options + * + * @since 3.10.0 + */ + public function getPhpOptions() + { + $options = array(); + + /* + * Check the PHP Version. It is already checked in Update. + * A Joomla! Update which is not supported by current PHP + * version is not shown. So this check is actually unnecessary. + */ + $option = new \stdClass(); + $option->label = Text::sprintf('INSTL_PHP_VERSION_NEWER', $this->getTargetMinimumPHPVersion()); + $option->state = $this->isPhpVersionSupported(); + $option->notice = null; + $options[] = $option; + + // Check for zlib support. + $option = new \stdClass(); + $option->label = Text::_('INSTL_ZLIB_COMPRESSION_SUPPORT'); + $option->state = extension_loaded('zlib'); + $option->notice = null; + $options[] = $option; + + // Check for XML support. + $option = new \stdClass(); + $option->label = Text::_('INSTL_XML_SUPPORT'); + $option->state = extension_loaded('xml'); + $option->notice = null; + $options[] = $option; + + // Check for mbstring options. + if (extension_loaded('mbstring')) { + // Check for default MB language. + $option = new \stdClass(); + $option->label = Text::_('INSTL_MB_LANGUAGE_IS_DEFAULT'); + $option->state = strtolower(ini_get('mbstring.language')) === 'neutral'; + $option->notice = $option->state ? null : Text::_('INSTL_NOTICEMBLANGNOTDEFAULT'); + $options[] = $option; + + // Check for MB function overload. + $option = new \stdClass(); + $option->label = Text::_('INSTL_MB_STRING_OVERLOAD_OFF'); + $option->state = ini_get('mbstring.func_overload') == 0; + $option->notice = $option->state ? null : Text::_('INSTL_NOTICEMBSTRINGOVERLOAD'); + $options[] = $option; + } + + // Check for a missing native parse_ini_file implementation. + $option = new \stdClass(); + $option->label = Text::_('INSTL_PARSE_INI_FILE_AVAILABLE'); + $option->state = $this->getIniParserAvailability(); + $option->notice = null; + $options[] = $option; + + // Check for missing native json_encode / json_decode support. + $option = new \stdClass(); + $option->label = Text::_('INSTL_JSON_SUPPORT_AVAILABLE'); + $option->state = function_exists('json_encode') && function_exists('json_decode'); + $option->notice = null; + $options[] = $option; + $updateInformation = $this->getUpdateInformation(); + + // Check if configured database is compatible with the next major version of Joomla + $nextMajorVersion = Version::MAJOR_VERSION + 1; + + if (version_compare($updateInformation['latest'], (string) $nextMajorVersion, '>=')) { + $option = new \stdClass(); + $option->label = Text::sprintf('INSTL_DATABASE_SUPPORTED', $this->getConfiguredDatabaseType()); + $option->state = $this->isDatabaseTypeSupported(); + $option->notice = null; + $options[] = $option; + } + + // Check if database structure is up to date + $option = new \stdClass(); + $option->label = Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_TITLE'); + $option->state = $this->getDatabaseSchemaCheck(); + $option->notice = $option->state ? null : Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_NOTICE'); + $options[] = $option; + + return $options; + } + + /** + * Gets PHP Settings. + * @todo: Outsource, build common code base for pre install and pre update check + * + * @return array + * + * @since 3.10.0 + */ + public function getPhpSettings() + { + $settings = array(); + + // Check for display errors. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_DISPLAY_ERRORS'); + $setting->state = (bool) ini_get('display_errors'); + $setting->recommended = false; + $settings[] = $setting; + + // Check for file uploads. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_FILE_UPLOADS'); + $setting->state = (bool) ini_get('file_uploads'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for output buffering. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_OUTPUT_BUFFERING'); + $setting->state = (int) ini_get('output_buffering') !== 0; + $setting->recommended = false; + $settings[] = $setting; + + // Check for session auto-start. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_SESSION_AUTO_START'); + $setting->state = (bool) ini_get('session.auto_start'); + $setting->recommended = false; + $settings[] = $setting; + + // Check for native ZIP support. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_ZIP_SUPPORT_AVAILABLE'); + $setting->state = function_exists('zip_open') && function_exists('zip_read'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for GD support + $setting = new \stdClass(); + $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'GD'); + $setting->state = extension_loaded('gd'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for iconv support + $setting = new \stdClass(); + $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'iconv'); + $setting->state = function_exists('iconv'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for intl support + $setting = new \stdClass(); + $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'intl'); + $setting->state = function_exists('transliterator_transliterate'); + $setting->recommended = true; + $settings[] = $setting; + + return $settings; + } + + /** + * Returns the configured database type id (mysqli or sqlsrv or ...) + * + * @return string + * + * @since 3.10.0 + */ + private function getConfiguredDatabaseType() + { + return Factory::getApplication()->get('dbtype'); + } + + /** + * Returns true, if J! version is < 4 or current configured + * database type is compatible with the update. + * + * @return boolean + * + * @since 3.10.0 + */ + public function isDatabaseTypeSupported() + { + $updateInformation = $this->getUpdateInformation(); + $nextMajorVersion = Version::MAJOR_VERSION + 1; + + // Check if configured database is compatible with Joomla 4 + if (version_compare($updateInformation['latest'], (string) $nextMajorVersion, '>=')) { + $unsupportedDatabaseTypes = array('sqlsrv', 'sqlazure'); + $currentDatabaseType = $this->getConfiguredDatabaseType(); + + return !in_array($currentDatabaseType, $unsupportedDatabaseTypes); + } + + return true; + } + + + /** + * Returns true, if current installed php version is compatible with the update. + * + * @return boolean + * + * @since 3.10.0 + */ + public function isPhpVersionSupported() + { + return version_compare(PHP_VERSION, $this->getTargetMinimumPHPVersion(), '>='); + } + + /** + * Returns the PHP minimum version for the update. + * Returns JOOMLA_MINIMUM_PHP, if there is no information given. + * + * @return string + * + * @since 3.10.0 + */ + private function getTargetMinimumPHPVersion() + { + $updateInformation = $this->getUpdateInformation(); + + return isset($updateInformation['object']->php_minimum) ? + $updateInformation['object']->php_minimum->_data : + JOOMLA_MINIMUM_PHP; + } + + /** + * Checks the availability of the parse_ini_file and parse_ini_string functions. + * @todo: Outsource, build common code base for pre install and pre update check + * + * @return boolean True if the method exists. + * + * @since 3.10.0 + */ + public function getIniParserAvailability() + { + $disabledFunctions = ini_get('disable_functions'); + + if (!empty($disabledFunctions)) { + // Attempt to detect them in the PHP INI disable_functions variable. + $disabledFunctions = explode(',', trim($disabledFunctions)); + $numberOfDisabledFunctions = count($disabledFunctions); + + for ($i = 0; $i < $numberOfDisabledFunctions; $i++) { + $disabledFunctions[$i] = trim($disabledFunctions[$i]); + } + + $result = !in_array('parse_ini_string', $disabledFunctions); + } else { + // Attempt to detect their existence; even pure PHP implementations of them will trigger a positive response, though. + $result = function_exists('parse_ini_string'); + } + + return $result; + } + + + /** + * Check if database structure is up to date + * + * @return boolean True if ok, false if not. + * + * @since 3.10.0 + */ + private function getDatabaseSchemaCheck(): bool + { + $mvcFactory = $this->bootComponent('com_installer')->getMVCFactory(); + + /** @var \Joomla\Component\Installer\Administrator\Model\DatabaseModel $model */ + $model = $mvcFactory->createModel('Database', 'Administrator'); + + // Check if no default text filters found + if (!$model->getDefaultTextFilters()) { + return false; + } + + $coreExtensionInfo = \Joomla\CMS\Extension\ExtensionHelper::getExtensionRecord('joomla', 'file'); + $cache = new \Joomla\Registry\Registry($coreExtensionInfo->manifest_cache); + + $updateVersion = $cache->get('version'); + + // Check if database update version does not match CMS version + if (version_compare($updateVersion, JVERSION) != 0) { + return false; + } + + // Ensure we only get information for core + $model->setState('filter.extension_id', $coreExtensionInfo->extension_id); + + // We're filtering by a single extension which must always exist - so can safely access this through + // element 0 of the array + $changeInformation = $model->getItems()[0]; + + // Check if schema errors found + if ($changeInformation['errorsCount'] !== 0) { + return false; + } + + // Check if database schema version does not match CMS version + if ($model->getSchemaVersion($coreExtensionInfo->extension_id) != $changeInformation['schema']) { + return false; + } + + // No database problems found + return true; + } + + /** + * Gets an array containing all installed extensions, that are not core extensions. + * + * @return array name,version,updateserver + * + * @since 3.10.0 + */ + public function getNonCoreExtensions() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + [ + $db->quoteName('ex.name'), + $db->quoteName('ex.extension_id'), + $db->quoteName('ex.manifest_cache'), + $db->quoteName('ex.type'), + $db->quoteName('ex.folder'), + $db->quoteName('ex.element'), + $db->quoteName('ex.client_id'), + ] + ) + ->from($db->quoteName('#__extensions', 'ex')) + ->where($db->quoteName('ex.package_id') . ' = 0') + ->whereNotIn($db->quoteName('ex.extension_id'), ExtensionHelper::getCoreExtensionIds()); + + $db->setQuery($query); + $rows = $db->loadObjectList(); + + foreach ($rows as $extension) { + $decode = json_decode($extension->manifest_cache); + + // Remove unused fields so they do not cause javascript errors during pre-update check + unset($decode->description); + unset($decode->copyright); + unset($decode->creationDate); + + $this->translateExtensionName($extension); + $extension->version + = isset($decode->version) ? $decode->version : Text::_('COM_JOOMLAUPDATE_PREUPDATE_UNKNOWN_EXTENSION_MANIFESTCACHE_VERSION'); + unset($extension->manifest_cache); + $extension->manifest_cache = $decode; + } + + return $rows; + } + + /** + * Gets an array containing all installed and enabled plugins, that are not core plugins. + * + * @param array $folderFilter Limit the list of plugins to a specific set of folder values + * + * @return array name,version,updateserver + * + * @since 3.10.0 + */ + public function getNonCorePlugins($folderFilter = ['system','user','authentication','actionlog','multifactorauth']) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + $db->qn('ex.name') . ', ' . + $db->qn('ex.extension_id') . ', ' . + $db->qn('ex.manifest_cache') . ', ' . + $db->qn('ex.type') . ', ' . + $db->qn('ex.folder') . ', ' . + $db->qn('ex.element') . ', ' . + $db->qn('ex.client_id') . ', ' . + $db->qn('ex.package_id') + )->from( + $db->qn('#__extensions', 'ex') + )->where( + $db->qn('ex.type') . ' = ' . $db->quote('plugin') + )->where( + $db->qn('ex.enabled') . ' = 1' + )->whereNotIn( + $db->quoteName('ex.extension_id'), + ExtensionHelper::getCoreExtensionIds() + ); + + if (count($folderFilter) > 0) { + $folderFilter = array_map(array($db, 'quote'), $folderFilter); + + $query->where($db->qn('folder') . ' IN (' . implode(',', $folderFilter) . ')'); + } + + $db->setQuery($query); + $rows = $db->loadObjectList(); + + foreach ($rows as $plugin) { + $decode = json_decode($plugin->manifest_cache); + + // Remove unused fields so they do not cause javascript errors during pre-update check + unset($decode->description); + unset($decode->copyright); + unset($decode->creationDate); + + $this->translateExtensionName($plugin); + $plugin->version = $decode->version ?? Text::_('COM_JOOMLAUPDATE_PREUPDATE_UNKNOWN_EXTENSION_MANIFESTCACHE_VERSION'); + unset($plugin->manifest_cache); + $plugin->manifest_cache = $decode; + } + + return $rows; + } + + /** + * Called by controller's fetchExtensionCompatibility, which is called via AJAX. + * + * @param string $extensionID The ID of the checked extension + * @param string $joomlaTargetVersion Target version of Joomla + * + * @return object + * + * @since 3.10.0 + */ + public function fetchCompatibility($extensionID, $joomlaTargetVersion) + { + $updateSites = $this->getUpdateSitesInfo($extensionID); + + if (empty($updateSites)) { + return (object) array('state' => 2); + } + + foreach ($updateSites as $updateSite) { + if ($updateSite['type'] === 'collection') { + $updateFileUrls = $this->getCollectionDetailsUrls($updateSite, $joomlaTargetVersion); + + foreach ($updateFileUrls as $updateFileUrl) { + $compatibleVersions = $this->checkCompatibility($updateFileUrl, $joomlaTargetVersion); + + // Return the compatible versions + return (object) array('state' => 1, 'compatibleVersions' => $compatibleVersions); + } + } else { + $compatibleVersions = $this->checkCompatibility($updateSite['location'], $joomlaTargetVersion); + + // Return the compatible versions + return (object) array('state' => 1, 'compatibleVersions' => $compatibleVersions); + } + } + + // In any other case we mark this extension as not compatible + return (object) array('state' => 0); + } + + /** + * Returns records with update sites and extension information for a given extension ID. + * + * @param int $extensionID The extension ID + * + * @return array + * + * @since 3.10.0 + */ + private function getUpdateSitesInfo($extensionID) + { + $id = (int) $extensionID; + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + $db->qn('us.type') . ', ' . + $db->qn('us.location') . ', ' . + $db->qn('e.element') . ' AS ' . $db->qn('ext_element') . ', ' . + $db->qn('e.type') . ' AS ' . $db->qn('ext_type') . ', ' . + $db->qn('e.folder') . ' AS ' . $db->qn('ext_folder') + ) + ->from($db->quoteName('#__update_sites', 'us')) + ->join( + 'LEFT', + $db->quoteName('#__update_sites_extensions', 'ue'), + $db->quoteName('ue.update_site_id') . ' = ' . $db->quoteName('us.update_site_id') + ) + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('ue.extension_id') + ) + ->where($db->quoteName('e.extension_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + $result = $db->loadAssocList(); + + if (!is_array($result)) { + return array(); + } + + return $result; + } + + /** + * Method to get details URLs from a collection update site for given extension and Joomla target version. + * + * @param array $updateSiteInfo The update site and extension information record to process + * @param string $joomlaTargetVersion The Joomla! version to test against, + * + * @return array An array of URLs. + * + * @since 3.10.0 + */ + private function getCollectionDetailsUrls($updateSiteInfo, $joomlaTargetVersion) + { + $return = array(); + + $http = new Http(); + + try { + $response = $http->get($updateSiteInfo['location']); + } catch (\RuntimeException $e) { + $response = null; + } + + if ($response === null || $response->code !== 200) { + return $return; + } + + $updateSiteXML = simplexml_load_string($response->body); + + foreach ($updateSiteXML->extension as $extension) { + $attribs = new \stdClass(); + + $attribs->element = ''; + $attribs->type = ''; + $attribs->folder = ''; + $attribs->targetplatformversion = ''; + + foreach ($extension->attributes() as $key => $value) { + $attribs->$key = (string) $value; + } + + if ( + $attribs->element === $updateSiteInfo['ext_element'] + && $attribs->type === $updateSiteInfo['ext_type'] + && $attribs->folder === $updateSiteInfo['ext_folder'] + && preg_match('/^' . $attribs->targetplatformversion . '/', $joomlaTargetVersion) + ) { + $return[] = (string) $extension['detailsurl']; + } + } + + return $return; + } + + /** + * Method to check non core extensions for compatibility. + * + * @param string $updateFileUrl The items update XML url. + * @param string $joomlaTargetVersion The Joomla! version to test against + * + * @return array An array of strings with compatible version numbers + * + * @since 3.10.0 + */ + private function checkCompatibility($updateFileUrl, $joomlaTargetVersion) + { + $minimumStability = ComponentHelper::getParams('com_installer')->get('minimum_stability', Updater::STABILITY_STABLE); + + $update = new Update(); + $update->set('jversion.full', $joomlaTargetVersion); + $update->loadFromXml($updateFileUrl, $minimumStability); + + $compatibleVersions = $update->get('compatibleVersions'); + + // Check if old version of the updater library + if (!isset($compatibleVersions)) { + $downloadUrl = $update->get('downloadurl'); + $updateVersion = $update->get('version'); + + return empty($downloadUrl) || empty($downloadUrl->_data) || empty($updateVersion) ? array() : array($updateVersion->_data); + } + + usort($compatibleVersions, 'version_compare'); + + return $compatibleVersions; + } + + /** + * Translates an extension name + * + * @param object &$item The extension of which the name needs to be translated + * + * @return void + * + * @since 3.10.0 + */ + protected function translateExtensionName(&$item) + { + // @todo: Cleanup duplicated code. from com_installer/models/extension.php + $lang = Factory::getLanguage(); + $path = $item->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE; + + $extension = $item->element; + $source = JPATH_SITE; + + switch ($item->type) { + case 'component': + $extension = $item->element; + $source = $path . '/components/' . $extension; + break; + case 'module': + $extension = $item->element; + $source = $path . '/modules/' . $extension; + break; + case 'file': + $extension = 'files_' . $item->element; + break; + case 'library': + $extension = 'lib_' . $item->element; + break; + case 'plugin': + $extension = 'plg_' . $item->folder . '_' . $item->element; + $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; + break; + case 'template': + $extension = 'tpl_' . $item->element; + $source = $path . '/templates/' . $item->element; + } + + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) + || $lang->load("$extension.sys", $source); + $lang->load($extension, JPATH_ADMINISTRATOR) + || $lang->load($extension, $source); + + // Translate the extension name if possible + $item->name = strip_tags(Text::_($item->name)); + } + + /** + * Checks whether a given template is active + * + * @param string $template The template name to be checked + * + * @return boolean + * + * @since 3.10.4 + */ + public function isTemplateActive($template) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + $db->qn( + array( + 'id', + 'home' + ) + ) + )->from( + $db->qn('#__template_styles') + )->where( + $db->qn('template') . ' = :template' + )->bind(':template', $template, ParameterType::STRING); + + $templates = $db->setQuery($query)->loadObjectList(); + + $home = array_filter( + $templates, + function ($value) { + return $value->home > 0; + } + ); + + $ids = ArrayHelper::getColumn($templates, 'id'); + + $menu = false; + + if (count($ids)) { + $query = $db->getQuery(true); + + $query->select( + 'COUNT(*)' + )->from( + $db->qn('#__menu') + )->whereIn( + $db->qn('template_style_id'), + $ids + ); + + $menu = $db->setQuery($query)->loadResult() > 0; + } + + return $home || $menu; + } } diff --git a/administrator/components/com_joomlaupdate/src/View/Joomlaupdate/HtmlView.php b/administrator/components/com_joomlaupdate/src/View/Joomlaupdate/HtmlView.php index 554c8c01968fc..d9ad3325fef2e 100644 --- a/administrator/components/com_joomlaupdate/src/View/Joomlaupdate/HtmlView.php +++ b/administrator/components/com_joomlaupdate/src/View/Joomlaupdate/HtmlView.php @@ -1,4 +1,5 @@ updateInfo = $this->get('UpdateInformation'); - $this->selfUpdateAvailable = $this->get('CheckForSelfUpdate'); - - // Get results of pre update check evaluations - $model = $this->getModel(); - $this->phpOptions = $this->get('PhpOptions'); - $this->phpSettings = $this->get('PhpSettings'); - $this->nonCoreExtensions = $this->get('NonCoreExtensions'); - $this->isDefaultBackendTemplate = (bool) $model->isTemplateActive($this->defaultBackendTemplate); - $nextMajorVersion = Version::MAJOR_VERSION + 1; - - // The critical plugins check is only available for major updates. - if (version_compare($this->updateInfo['latest'], (string) $nextMajorVersion, '>=')) - { - $this->nonCoreCriticalPlugins = $this->get('NonCorePlugins'); - } - - // Set to true if a required PHP option is not ok - $isCritical = false; - - foreach ($this->phpOptions as $option) - { - if (!$option->state) - { - $isCritical = true; - break; - } - } - - $this->state = $this->get('State'); - - $hasUpdate = !empty($this->updateInfo['hasUpdate']); - $hasDownload = isset($this->updateInfo['object']->downloadurl->_data); - - // Fresh update, show it - if ($this->getLayout() == 'complete') - { - // Complete message, nothing to do here - } - // There is an update for the updater itself. So we have to update it first - elseif ($this->selfUpdateAvailable) - { - $this->setLayout('selfupdate'); - } - elseif (!$hasDownload || !$hasUpdate) - { - // Could be that we have a download file but no update, so we offer a re-install - if ($hasDownload) - { - // We can reinstall if we have a URL but no update - $this->setLayout('reinstall'); - } - // No download available - else - { - if ($hasUpdate) - { - $this->messagePrefix = '_NODOWNLOAD'; - } - - $this->setLayout('noupdate'); - } - } - // Here we have now two options: preupdatecheck or update - elseif ($this->getLayout() != 'update' && ($isCritical || $this->shouldDisplayPreUpdateCheck())) - { - $this->setLayout('preupdatecheck'); - } - else - { - $this->setLayout('update'); - } - - if (in_array($this->getLayout(), ['preupdatecheck', 'update', 'upload'])) - { - $language = Factory::getLanguage(); - $language->load('com_installer', JPATH_ADMINISTRATOR, 'en-GB', false, true); - $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); - - Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATE_NOTICE'), 'notice'); - } - - $params = ComponentHelper::getParams('com_joomlaupdate'); - - switch ($params->get('updatesource', 'default')) - { - // "Minor & Patch Release for Current version AND Next Major Release". - case 'next': - $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_NEXT'; - $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT'); - break; - - // "Testing" - case 'testing': - $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_TESTING'; - $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_TESTING'); - break; - - // "Custom" - case 'custom': - $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_CUSTOM'; - $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM'); - break; - - /** - * "Minor & Patch Release for Current version (recommended and default)". - * The commented "case" below are for documenting where 'default' and legacy options falls - * case 'default': - * case 'sts': - * case 'lts': - * case 'nochange': - */ - default: - $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_DEFAULT'; - $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_DEFAULT'); - } - - $this->noVersionCheck = $params->get('versioncheck', 1) == 0; - $this->noBackupCheck = $params->get('backupcheck', 1) == 0; - - // Remove temporary files - $this->getModel()->removePackageFiles(); - - $this->addToolbar(); - - // Render the view. - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - // Set the toolbar information. - ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'joomla install'); - - if (in_array($this->getLayout(), ['update', 'complete'])) - { - $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; - - ToolbarHelper::link('index.php?option=com_joomlaupdate', 'JTOOLBAR_BACK', $arrow); - - ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_TAB_UPLOAD'), 'joomla install'); - } - elseif (!$this->selfUpdateAvailable) - { - ToolbarHelper::custom('update.purge', 'loop', '', 'COM_JOOMLAUPDATE_TOOLBAR_CHECK', false); - } - - // Add toolbar buttons. - if ($this->getCurrentUser()->authorise('core.admin')) - { - ToolbarHelper::preferences('com_joomlaupdate'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Joomla_Update'); - } - - /** - * Returns true, if the pre update check should be displayed. - * - * @return boolean - * - * @since 3.10.0 - */ - public function shouldDisplayPreUpdateCheck() - { - // When the download URL is not found there is no core upgrade path - if (!isset($this->updateInfo['object']->downloadurl->_data)) - { - return false; - } - - $nextMinor = Version::MAJOR_VERSION . '.' . (Version::MINOR_VERSION + 1); - - // Show only when we found a download URL, we have an update and when we update to the next minor or greater. - return $this->updateInfo['hasUpdate'] - && version_compare($this->updateInfo['latest'], $nextMinor, '>='); - } + /** + * An array with the Joomla! update information. + * + * @var array + * + * @since 3.6.0 + */ + protected $updateInfo = null; + + /** + * PHP options. + * + * @var array Array of PHP config options + * + * @since 3.10.0 + */ + protected $phpOptions = null; + + /** + * PHP settings. + * + * @var array Array of PHP settings + * + * @since 3.10.0 + */ + protected $phpSettings = null; + + /** + * Non Core Extensions. + * + * @var array Array of Non-Core-Extensions + * + * @since 3.10.0 + */ + protected $nonCoreExtensions = null; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 4.0.0 + */ + protected $state; + + /** + * Flag if the update component itself has to be updated + * + * @var boolean True when update is available otherwise false + * + * @since 4.0.0 + */ + protected $selfUpdateAvailable = false; + + /** + * The default admin template for the major version of Joomla that should be used when + * upgrading to the next major version of Joomla + * + * @var string + * + * @since 4.0.0 + */ + protected $defaultBackendTemplate = 'atum'; + + /** + * Flag if default backend template is being used + * + * @var boolean True when default backend template is being used + * + * @since 4.0.0 + */ + protected $isDefaultBackendTemplate = false; + + /** + * A special prefix used for the emptystate layout variable + * + * @var string The prefix + * + * @since 4.0.0 + */ + protected $messagePrefix = ''; + + /** + * List of non core critical plugins + * + * @var \stdClass[] + * @since 4.0.0 + */ + protected $nonCoreCriticalPlugins = []; + + /** + * Should I disable the confirmation checkbox for pre-update extension version checks? + * + * @var boolean + * @since 4.2.0 + */ + protected $noVersionCheck = false; + + /** + * Should I disable the confirmation checkbox for taking a backup before updating? + * + * @var boolean + * @since 4.2.0 + */ + protected $noBackupCheck = false; + + /** + * Renders the view + * + * @param string $tpl Template name + * + * @return void + * + * @since 2.5.4 + */ + public function display($tpl = null) + { + $this->updateInfo = $this->get('UpdateInformation'); + $this->selfUpdateAvailable = $this->get('CheckForSelfUpdate'); + + // Get results of pre update check evaluations + $model = $this->getModel(); + $this->phpOptions = $this->get('PhpOptions'); + $this->phpSettings = $this->get('PhpSettings'); + $this->nonCoreExtensions = $this->get('NonCoreExtensions'); + $this->isDefaultBackendTemplate = (bool) $model->isTemplateActive($this->defaultBackendTemplate); + $nextMajorVersion = Version::MAJOR_VERSION + 1; + + // The critical plugins check is only available for major updates. + if (version_compare($this->updateInfo['latest'], (string) $nextMajorVersion, '>=')) { + $this->nonCoreCriticalPlugins = $this->get('NonCorePlugins'); + } + + // Set to true if a required PHP option is not ok + $isCritical = false; + + foreach ($this->phpOptions as $option) { + if (!$option->state) { + $isCritical = true; + break; + } + } + + $this->state = $this->get('State'); + + $hasUpdate = !empty($this->updateInfo['hasUpdate']); + $hasDownload = isset($this->updateInfo['object']->downloadurl->_data); + + // Fresh update, show it + if ($this->getLayout() == 'complete') { + // Complete message, nothing to do here + } + // There is an update for the updater itself. So we have to update it first + elseif ($this->selfUpdateAvailable) { + $this->setLayout('selfupdate'); + } elseif (!$hasDownload || !$hasUpdate) { + // Could be that we have a download file but no update, so we offer a re-install + if ($hasDownload) { + // We can reinstall if we have a URL but no update + $this->setLayout('reinstall'); + } + // No download available + else { + if ($hasUpdate) { + $this->messagePrefix = '_NODOWNLOAD'; + } + + $this->setLayout('noupdate'); + } + } + // Here we have now two options: preupdatecheck or update + elseif ($this->getLayout() != 'update' && ($isCritical || $this->shouldDisplayPreUpdateCheck())) { + $this->setLayout('preupdatecheck'); + } else { + $this->setLayout('update'); + } + + if (in_array($this->getLayout(), ['preupdatecheck', 'update', 'upload'])) { + $language = Factory::getLanguage(); + $language->load('com_installer', JPATH_ADMINISTRATOR, 'en-GB', false, true); + $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); + + Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATE_NOTICE'), 'notice'); + } + + $params = ComponentHelper::getParams('com_joomlaupdate'); + + switch ($params->get('updatesource', 'default')) { + // "Minor & Patch Release for Current version AND Next Major Release". + case 'next': + $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_NEXT'; + $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT'); + break; + + // "Testing" + case 'testing': + $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_TESTING'; + $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_TESTING'); + break; + + // "Custom" + case 'custom': + $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_CUSTOM'; + $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM'); + break; + + /** + * "Minor & Patch Release for Current version (recommended and default)". + * The commented "case" below are for documenting where 'default' and legacy options falls + * case 'default': + * case 'sts': + * case 'lts': + * case 'nochange': + */ + default: + $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_DEFAULT'; + $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_DEFAULT'); + } + + $this->noVersionCheck = $params->get('versioncheck', 1) == 0; + $this->noBackupCheck = $params->get('backupcheck', 1) == 0; + + // Remove temporary files + $this->getModel()->removePackageFiles(); + + $this->addToolbar(); + + // Render the view. + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + // Set the toolbar information. + ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'joomla install'); + + if (in_array($this->getLayout(), ['update', 'complete'])) { + $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + + ToolbarHelper::link('index.php?option=com_joomlaupdate', 'JTOOLBAR_BACK', $arrow); + + ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_TAB_UPLOAD'), 'joomla install'); + } elseif (!$this->selfUpdateAvailable) { + ToolbarHelper::custom('update.purge', 'loop', '', 'COM_JOOMLAUPDATE_TOOLBAR_CHECK', false); + } + + // Add toolbar buttons. + if ($this->getCurrentUser()->authorise('core.admin')) { + ToolbarHelper::preferences('com_joomlaupdate'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Joomla_Update'); + } + + /** + * Returns true, if the pre update check should be displayed. + * + * @return boolean + * + * @since 3.10.0 + */ + public function shouldDisplayPreUpdateCheck() + { + // When the download URL is not found there is no core upgrade path + if (!isset($this->updateInfo['object']->downloadurl->_data)) { + return false; + } + + $nextMinor = Version::MAJOR_VERSION . '.' . (Version::MINOR_VERSION + 1); + + // Show only when we found a download URL, we have an update and when we update to the next minor or greater. + return $this->updateInfo['hasUpdate'] + && version_compare($this->updateInfo['latest'], $nextMinor, '>='); + } } diff --git a/administrator/components/com_joomlaupdate/src/View/Update/HtmlView.php b/administrator/components/com_joomlaupdate/src/View/Update/HtmlView.php index 1d8471e4a825d..d61222fae74de 100644 --- a/administrator/components/com_joomlaupdate/src/View/Update/HtmlView.php +++ b/administrator/components/com_joomlaupdate/src/View/Update/HtmlView.php @@ -1,4 +1,5 @@ input->set('hidemainmenu', true); + /** + * Renders the view. + * + * @param string $tpl Template name. + * + * @return void + */ + public function display($tpl = null) + { + Factory::getApplication()->input->set('hidemainmenu', true); - // Set the toolbar information. - ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'sync install'); + // Set the toolbar information. + ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'sync install'); - // Render the view. - parent::display($tpl); - } + // Render the view. + parent::display($tpl); + } } diff --git a/administrator/components/com_joomlaupdate/src/View/Upload/HtmlView.php b/administrator/components/com_joomlaupdate/src/View/Upload/HtmlView.php index f172fc7900273..9263ef937a246 100644 --- a/administrator/components/com_joomlaupdate/src/View/Upload/HtmlView.php +++ b/administrator/components/com_joomlaupdate/src/View/Upload/HtmlView.php @@ -1,4 +1,5 @@ load('com_installer', JPATH_ADMINISTRATOR, 'en-GB', false, true); - $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); - - $this->updateInfo = $this->get('UpdateInformation'); - $this->selfUpdateAvailable = $this->get('CheckForSelfUpdate'); - - if ($this->getLayout() !== 'captive') - { - $this->warnings = $this->get('Items', 'warnings'); - } - - $params = ComponentHelper::getParams('com_joomlaupdate'); - $this->noBackupCheck = $params->get('backupcheck', 1) == 0; - - $this->addToolbar(); - - // Render the view. - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - // Set the toolbar information. - ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'sync install'); - - $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; - ToolbarHelper::link('index.php?option=com_joomlaupdate&' . ($this->getLayout() == 'captive' ? 'view=upload' : ''), 'JTOOLBAR_BACK', $arrow); - ToolbarHelper::divider(); - ToolbarHelper::help('Joomla_Update'); - } + /** + * An array with the Joomla! update information. + * + * @var array + * + * @since 4.0.0 + */ + protected $updateInfo = null; + + /** + * Flag if the update component itself has to be updated + * + * @var boolean True when update is available otherwise false + * + * @since 4.0.0 + */ + protected $selfUpdateAvailable = false; + + /** + * Warnings for the upload update + * + * @var array An array of warnings which could prevent the upload update + * + * @since 4.0.0 + */ + protected $warnings = []; + + /** + * Should I disable the confirmation checkbox for taking a backup before updating? + * + * @var boolean + * @since 4.2.0 + */ + protected $noBackupCheck = false; + + /** + * Renders the view. + * + * @param string $tpl Template name. + * + * @return void + * + * @since 3.6.0 + */ + public function display($tpl = null) + { + // Load com_installer's language + $language = Factory::getLanguage(); + $language->load('com_installer', JPATH_ADMINISTRATOR, 'en-GB', false, true); + $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); + + $this->updateInfo = $this->get('UpdateInformation'); + $this->selfUpdateAvailable = $this->get('CheckForSelfUpdate'); + + if ($this->getLayout() !== 'captive') { + $this->warnings = $this->get('Items', 'warnings'); + } + + $params = ComponentHelper::getParams('com_joomlaupdate'); + $this->noBackupCheck = $params->get('backupcheck', 1) == 0; + + $this->addToolbar(); + + // Render the view. + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + // Set the toolbar information. + ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'sync install'); + + $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + ToolbarHelper::link('index.php?option=com_joomlaupdate&' . ($this->getLayout() == 'captive' ? 'view=upload' : ''), 'JTOOLBAR_BACK', $arrow); + ToolbarHelper::divider(); + ToolbarHelper::help('Joomla_Update'); + } } diff --git a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/complete.php b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/complete.php index 01a8d4eb7fe1e..e168c2697e00c 100644 --- a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/complete.php +++ b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/complete.php @@ -1,4 +1,5 @@
    -

    -
    -
    - - -
    -
    +

    +
    +
    + + +
    +
    - - + +
    diff --git a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/noupdate.php b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/noupdate.php index ec72b53384e73..8a16e3ca46d86 100644 --- a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/noupdate.php +++ b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/noupdate.php @@ -1,4 +1,5 @@ 'COM_JOOMLAUPDATE' . $this->messagePrefix, - 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), - 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', - 'icon' => 'icon-loop joomlaupdate', - 'createURL' => 'index.php?option=com_joomlaupdate&task=update.purge&' . Session::getFormToken() . '=1' + 'textPrefix' => 'COM_JOOMLAUPDATE' . $this->messagePrefix, + 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), + 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', + 'icon' => 'icon-loop joomlaupdate', + 'createURL' => 'index.php?option=com_joomlaupdate&task=update.purge&' . Session::getFormToken() . '=1' ]; -if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) -{ - $displayData['formAppend'] = ''; +if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) { + $displayData['formAppend'] = ''; } if (isset($this->updateInfo['object']) && isset($this->updateInfo['object']->get('infourl')->_data)) : - $displayData['content'] .= '
    ' . HTMLHelper::_('link', - $this->updateInfo['object']->get('infourl')->_data, - Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), - [ - 'target' => '_blank', - 'rel' => 'noopener noreferrer', - 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' - ] - ); + $displayData['content'] .= '
    ' . HTMLHelper::_( + 'link', + $this->updateInfo['object']->get('infourl')->_data, + Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), + [ + 'target' => '_blank', + 'rel' => 'noopener noreferrer', + 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' + ] + ); endif; $content = LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/preupdatecheck.php b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/preupdatecheck.php index c64638bf382a7..58da0e0224a2c 100644 --- a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/preupdatecheck.php +++ b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/preupdatecheck.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('com_joomlaupdate.default') - ->useScript('bootstrap.popover') - ->useScript('bootstrap.tab'); + ->useScript('com_joomlaupdate.default') + ->useScript('bootstrap.popover') + ->useScript('bootstrap.tab'); // Text::script doesn't have a sprintf equivalent so work around this $this->document->addScriptOptions('nonCoreCriticalPlugins', $this->nonCoreCriticalPlugins); @@ -50,36 +51,36 @@ Text::script('JLIB_JS_AJAX_ERROR_TIMEOUT'); $compatibilityTypes = array( - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_RUNNING_PRE_UPDATE_CHECKS' => array( - 'class' => 'info', - 'icon' => 'hourglass fa-spin', - 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_RUNNING_PRE_UPDATE_CHECKS_NOTES', - 'group' => 0, - ), - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_REQUIRING_UPDATES_TO_BE_COMPATIBLE' => array( - 'class' => 'danger', - 'icon' => 'times', - 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_REQUIRING_UPDATES_TO_BE_COMPATIBLE_NOTES', - 'group' => 2, - ), - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PRE_UPDATE_CHECKS_FAILED' => array( - 'class' => 'warning', - 'icon' => 'exclamation-triangle', - 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PRE_UPDATE_CHECKS_FAILED_NOTES', - 'group' => 4, - ), - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION' => array( - 'class' => 'warning', - 'icon' => 'exclamation-triangle', - 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION_NOTES', - 'group' => 1, - ), - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PROBABLY_COMPATIBLE' => array( - 'class' => 'success', - 'icon' => 'check', - 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PROBABLY_COMPATIBLE_NOTES', - 'group' => 3, - ), + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_RUNNING_PRE_UPDATE_CHECKS' => array( + 'class' => 'info', + 'icon' => 'hourglass fa-spin', + 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_RUNNING_PRE_UPDATE_CHECKS_NOTES', + 'group' => 0, + ), + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_REQUIRING_UPDATES_TO_BE_COMPATIBLE' => array( + 'class' => 'danger', + 'icon' => 'times', + 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_REQUIRING_UPDATES_TO_BE_COMPATIBLE_NOTES', + 'group' => 2, + ), + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PRE_UPDATE_CHECKS_FAILED' => array( + 'class' => 'warning', + 'icon' => 'exclamation-triangle', + 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PRE_UPDATE_CHECKS_FAILED_NOTES', + 'group' => 4, + ), + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION' => array( + 'class' => 'warning', + 'icon' => 'exclamation-triangle', + 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION_NOTES', + 'group' => 1, + ), + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PROBABLY_COMPATIBLE' => array( + 'class' => 'success', + 'icon' => 'check', + 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PROBABLY_COMPATIBLE_NOTES', + 'group' => 3, + ), ); $latestJoomlaVersion = $this->updateInfo['latest']; @@ -87,286 +88,284 @@ $updatePossible = true; -if (version_compare($this->updateInfo['latest'], Version::MAJOR_VERSION + 1, '>=') && $this->isDefaultBackendTemplate === false) -{ - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_NON_CORE_BACKEND_TEMPLATE_USED_NOTICE', - ucfirst($this->defaultBackendTemplate) - ), - 'info' - ); +if (version_compare($this->updateInfo['latest'], Version::MAJOR_VERSION + 1, '>=') && $this->isDefaultBackendTemplate === false) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_NON_CORE_BACKEND_TEMPLATE_USED_NOTICE', + ucfirst($this->defaultBackendTemplate) + ), + 'info' + ); } ?>
    -

    - updateInfo['latest']); ?> -

    -

    - -

    - -
    - +

    + updateInfo['latest']); ?> +

    +

    + +

    -
    -
    -

    - -

    -
    - - - - - - - - - - phpOptions as $option) : ?> - - - - - - -
    - -
    - - - -
    - label; ?> - notice) : ?> -
    - notice; ?> -
    - -
    - - state ? 'JYES' : 'JNO'); ?> - -
    -
    -
    - -
    -

    - -

    -
    -
    -

    - -

    -
    -
    - -
    -
    -
    -
    +
    + - +
    +
    +

    + +

    +
    + + + + + + + + + + phpOptions as $option) : ?> + + + + + + +
    + +
    + + + +
    + label; ?> + notice) : ?> +
    + notice; ?> +
    + +
    + + state ? 'JYES' : 'JNO'); ?> + +
    +
    +
    + +
    +

    + +

    +
    +
    +

    + +

    +
    +
    + +
    +
    +
    +
    - nonCoreExtensions)) : ?> -
    - $data) : ?> -
    -

    - - - 0) : ?> - - -

    + -
    - - - - - - - - - - - - - - - nonCoreExtensions as $extension) : ?> - - - - - - - - - - -
    - -
    - - - -
    - name; ?> - - type)); ?> -
    -
    -
    - -
    - -
    - - -
    - -
    -
    -
    + nonCoreExtensions)) : ?> +
    + $data) : ?> +
    +

    + + + 0) : ?> + + +

    - +
    + + + + + + + + + + + + + + + nonCoreExtensions as $extension) : ?> + + + + + + + + + + +
    + +
    + + + +
    + name; ?> + + type)); ?> +
    +
    +
    + +
    + +
    + + +
    + +
    +
    +
    -
    + + - noVersionCheck): ?> -
    -
    - - -
    -
    - + noVersionCheck) : ?> +
    +
    + + +
    +
    + - -
    - + + + -
    - - -
    +
    + + +
    - authorise('core.admin')) : ?> -
    - - - -
    - + authorise('core.admin')) : ?> +
    + + + +
    +
    diff --git a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/reinstall.php b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/reinstall.php index e6e93402d4ec2..57a5ed36dc833 100644 --- a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/reinstall.php +++ b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/reinstall.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('com_joomlaupdate.default') - ->useScript('bootstrap.popover'); + ->useScript('com_joomlaupdate.default') + ->useScript('bootstrap.popover'); $uploadLink = 'index.php?option=com_joomlaupdate&view=upload'; $displayData = [ - 'textPrefix' => 'COM_JOOMLAUPDATE_REINSTALL', - 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), - 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', - 'icon' => 'icon-loop joomlaupdate', - 'createURL' => '#' + 'textPrefix' => 'COM_JOOMLAUPDATE_REINSTALL', + 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), + 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', + 'icon' => 'icon-loop joomlaupdate', + 'createURL' => '#' ]; if (isset($this->updateInfo['object']) && isset($this->updateInfo['object']->get('infourl')->_data)) : - $displayData['content'] .= '
    ' . HTMLHelper::_('link', - $this->updateInfo['object']->get('infourl')->_data, - Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), - [ - 'target' => '_blank', - 'rel' => 'noopener noreferrer', - 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' - ] - ); + $displayData['content'] .= '
    ' . HTMLHelper::_( + 'link', + $this->updateInfo['object']->get('infourl')->_data, + Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), + [ + 'target' => '_blank', + 'rel' => 'noopener noreferrer', + 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' + ] + ); endif; if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) : - $displayData['formAppend'] = ''; + $displayData['formAppend'] = ''; endif; echo '
    '; diff --git a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/selfupdate.php b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/selfupdate.php index 0ca24eb8efa62..52044361cb0b2 100644 --- a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/selfupdate.php +++ b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/selfupdate.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Layout\LayoutHelper; $displayData = [ - 'textPrefix' => 'COM_JOOMLAUPDATE_SELF', - 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', - 'icon' => 'icon-loop joomlaupdate', - 'createURL' => 'index.php?option=com_installer&view=update' + 'textPrefix' => 'COM_JOOMLAUPDATE_SELF', + 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', + 'icon' => 'icon-loop joomlaupdate', + 'createURL' => 'index.php?option=com_installer&view=update' ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php index 258fdc97cc655..1ee49824663b7 100644 --- a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php +++ b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('com_joomlaupdate.default') - ->useScript('bootstrap.popover'); + ->useScript('com_joomlaupdate.default') + ->useScript('bootstrap.popover'); $uploadLink = 'index.php?option=com_joomlaupdate&view=upload'; $displayData = [ - 'textPrefix' => 'COM_JOOMLAUPDATE_UPDATE', - 'title' => Text::sprintf('COM_JOOMLAUPDATE_UPDATE_EMPTYSTATE_TITLE', $this->escape($this->updateInfo['latest'])), - 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), - 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', - 'icon' => 'icon-loop joomlaupdate', - 'createURL' => '#' + 'textPrefix' => 'COM_JOOMLAUPDATE_UPDATE', + 'title' => Text::sprintf('COM_JOOMLAUPDATE_UPDATE_EMPTYSTATE_TITLE', $this->escape($this->updateInfo['latest'])), + 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), + 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', + 'icon' => 'icon-loop joomlaupdate', + 'createURL' => '#' ]; if (isset($this->updateInfo['object']) && isset($this->updateInfo['object']->get('infourl')->_data)) : - $displayData['content'] .= '
    ' . HTMLHelper::_('link', - $this->updateInfo['object']->get('infourl')->_data, - Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), - [ - 'target' => '_blank', - 'rel' => 'noopener noreferrer', - 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' - ] - ); + $displayData['content'] .= '
    ' . HTMLHelper::_( + 'link', + $this->updateInfo['object']->get('infourl')->_data, + Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), + [ + 'target' => '_blank', + 'rel' => 'noopener noreferrer', + 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' + ] + ); endif; // Confirm backup and check @@ -57,7 +59,7 @@
    '; if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) : - $displayData['formAppend'] = ''; + $displayData['formAppend'] = ''; endif; echo '
    '; diff --git a/administrator/components/com_joomlaupdate/tmpl/update/default.php b/administrator/components/com_joomlaupdate/tmpl/update/default.php index ff76166341672..0375bd95a1da6 100644 --- a/administrator/components/com_joomlaupdate/tmpl/update/default.php +++ b/administrator/components/com_joomlaupdate/tmpl/update/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('com_joomlaupdate.admin-update') - ->useScript('bootstrap.modal'); + ->useScript('com_joomlaupdate.admin-update') + ->useScript('bootstrap.modal'); Text::script('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_FORBIDDEN'); Text::script('COM_JOOMLAUPDATE_ERRORMODAL_BODY_FORBIDDEN'); @@ -45,90 +46,90 @@ $returnUrl = 'index.php?option=com_joomlaupdate&task=update.finalise&' . Factory::getSession()->getFormToken() . '=1'; $this->document->addScriptOptions( - 'joomlaupdate', - [ - 'password' => $password, - 'totalsize' => $filesize, - 'ajax_url' => $ajaxUrl, - 'return_url' => $returnUrl, - ] + 'joomlaupdate', + [ + 'password' => $password, + 'totalsize' => $filesize, + 'ajax_url' => $ajaxUrl, + 'return_url' => $returnUrl, + ] ); $helpUrl = Help::createUrl('JHELP_COMPONENTS_JOOMLA_UPDATE', false); ?>
    - -

    -
    -

    - -

    -
    -
    -
    -
    -
    -
    -
    - - - -
    -
    - - - -
    -
    - - - -
    -
    -
    -
    + +

    +
    +

    + +

    +
    +
    +
    +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    +
    +
    -
    -

    -
    -
    -
    - -
    +
    +

    +
    +
    +
    + +
    diff --git a/administrator/components/com_joomlaupdate/tmpl/update/finaliseconfirm.php b/administrator/components/com_joomlaupdate/tmpl/update/finaliseconfirm.php index 32cf3dde1cb68..261863f052dbc 100644 --- a/administrator/components/com_joomlaupdate/tmpl/update/finaliseconfirm.php +++ b/administrator/components/com_joomlaupdate/tmpl/update/finaliseconfirm.php @@ -1,4 +1,5 @@
    -

    - -

    -

    - get('sitename')); ?> -

    +

    + +

    +

    + get('sitename')); ?> +


    -
    - -
    -
    -
    - - - - - -
    -
    -
    -
    -
    -
    - - - - - -
    -
    -
    -
    -
    -
    - - - - -
    -
    -
    +
    + +
    +
    +
    + + + + + +
    +
    +
    +
    +
    +
    + + + + + +
    +
    +
    +
    +
    +
    + + + + +
    +
    +
    - - - -
    + + + +
    diff --git a/administrator/components/com_joomlaupdate/tmpl/upload/captive.php b/administrator/components/com_joomlaupdate/tmpl/upload/captive.php index b3c030d177478..15de15ee2f445 100644 --- a/administrator/components/com_joomlaupdate/tmpl/upload/captive.php +++ b/administrator/components/com_joomlaupdate/tmpl/upload/captive.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('jquery') - ->useScript('form.validate') - ->useScript('keepalive') - ->useScript('field.passwordview'); + ->useScript('jquery') + ->useScript('form.validate') + ->useScript('keepalive') + ->useScript('field.passwordview'); Text::script('JSHOWPASSWORD'); Text::script('JHIDEPASSWORD'); ?>
    -

    - -

    -

    - get('sitename')); ?> -

    +

    + +

    +

    + get('sitename')); ?> +


    -
    - -
    -
    -
    - - - - - -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    -
    - - - - -
    -
    +
    + +
    +
    +
    + + + + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + + + + +
    +
    - - - -
    + + + +
    diff --git a/administrator/components/com_joomlaupdate/tmpl/upload/default.php b/administrator/components/com_joomlaupdate/tmpl/upload/default.php index c0f7837f3ffb2..319c22d2a7c6e 100644 --- a/administrator/components/com_joomlaupdate/tmpl/upload/default.php +++ b/administrator/components/com_joomlaupdate/tmpl/upload/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('com_joomlaupdate.default') - ->useScript('bootstrap.popover'); + ->useScript('com_joomlaupdate.default') + ->useScript('bootstrap.popover'); Text::script('COM_INSTALLER_MSG_INSTALL_PLEASE_SELECT_A_PACKAGE', true); Text::script('COM_INSTALLER_MSG_WARNINGS_UPLOADFILETOOBIG', true); @@ -33,69 +34,69 @@
    - - - updateInfo['object']) && ($this->updateInfo['object'] instanceof Update)) : ?> -

    - - updateInfo['object']->downloadurl->_data); ?> - + + + updateInfo['object']) && ($this->updateInfo['object'] instanceof Update)) : ?> +

    + + updateInfo['object']->downloadurl->_data); ?> +
    warnings)) : ?> -

    - warnings as $warning) : ?> -
    -

    - - - -

    -

    -
    - -
    -

    - - - -

    -

    -
    +

    + warnings as $warning) : ?> +
    +

    + + + +

    +

    +
    + +
    +

    + + + +

    +

    +
    -
    - - - - - - -
    - - -
    - -
    - noBackupCheck ? 'checked' : '' ?>> - -
    - - - - - - +
    + + + + + + +
    + + +
    + +
    + noBackupCheck ? 'checked' : '' ?>> + +
    + + + + + +
    diff --git a/administrator/components/com_languages/services/provider.php b/administrator/components/com_languages/services/provider.php index c405c69c847df..b47d560cb2285 100644 --- a/administrator/components/com_languages/services/provider.php +++ b/administrator/components/com_languages/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Languages')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Languages')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Languages')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Languages')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new LanguagesComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new LanguagesComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_languages/src/Controller/DisplayController.php b/administrator/components/com_languages/src/Controller/DisplayController.php index 87d538a3ad59d..f5d118436fe77 100644 --- a/administrator/components/com_languages/src/Controller/DisplayController.php +++ b/administrator/components/com_languages/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', $this->default_view); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); - - // Check for edit form. - if ($view == 'language' && $layout == 'edit' && !$this->checkEditId('com_languages.edit.language', $id)) - { - // 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', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_languages&view=languages', false)); - - return false; - } - - return parent::display(); - } + /** + * @var string The default view. + * @since 1.6 + */ + protected $default_view = 'installed'; + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached. + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', $this->default_view); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); + + // Check for edit form. + if ($view == 'language' && $layout == 'edit' && !$this->checkEditId('com_languages.edit.language', $id)) { + // 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', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_languages&view=languages', false)); + + return false; + } + + return parent::display(); + } } diff --git a/administrator/components/com_languages/src/Controller/InstalledController.php b/administrator/components/com_languages/src/Controller/InstalledController.php index d565f0e89dcf2..fe229ad557182 100644 --- a/administrator/components/com_languages/src/Controller/InstalledController.php +++ b/administrator/components/com_languages/src/Controller/InstalledController.php @@ -1,4 +1,5 @@ checkToken(); - - $cid = (string) $this->input->get('cid', '', 'string'); - $model = $this->getModel('installed'); - - if ($model->publish($cid)) - { - // Switching to the new administrator language for the message - if ($model->getState('client_id') == 1) - { - $language = Factory::getLanguage(); - $newLang = Language::getInstance($cid); - Factory::$language = $newLang; - $this->app->loadLanguage($language = $newLang); - $newLang->load('com_languages', JPATH_ADMINISTRATOR); - } - - if (Multilanguage::isEnabled() && $model->getState('client_id') == 0) - { - $msg = Text::_('COM_LANGUAGES_MSG_DEFAULT_MULTILANG_SAVED'); - $type = 'message'; - } - else - { - $msg = Text::_('COM_LANGUAGES_MSG_DEFAULT_LANGUAGE_SAVED'); - $type = 'message'; - } - } - else - { - $msg = $model->getError(); - $type = 'error'; - } - - $clientId = $model->getState('client_id'); - $this->setRedirect('index.php?option=com_languages&view=installed&client=' . $clientId, $msg, $type); - } - - /** - * Task to switch the administrator language. - * - * @return void - */ - public function switchAdminLanguage() - { - // Check for request forgeries. - $this->checkToken(); - - $cid = (string) $this->input->get('cid', '', 'string'); - $model = $this->getModel('installed'); - - // Fetching the language name from the langmetadata.xml or xx-XX.xml respectively. - $file = JPATH_ADMINISTRATOR . '/language/' . $cid . '/langmetadata.xml'; - - if (!is_file($file)) - { - $file = JPATH_ADMINISTRATOR . '/language/' . $cid . '/' . $cid . '.xml'; - } - - $info = LanguageHelper::parseXMLLanguageFile($file); - - if ($model->switchAdminLanguage($cid)) - { - // Switching to the new language for the message - $languageName = $info['nativeName']; - $language = Factory::getLanguage(); - $newLang = Language::getInstance($cid); - Factory::$language = $newLang; - $this->app->loadLanguage($language = $newLang); - $newLang->load('com_languages', JPATH_ADMINISTRATOR); - - $msg = Text::sprintf('COM_LANGUAGES_MSG_SWITCH_ADMIN_LANGUAGE_SUCCESS', $languageName); - $type = 'message'; - } - else - { - $msg = $model->getError(); - $type = 'error'; - } - - $this->setRedirect('index.php?option=com_languages&view=installed', $msg, $type); - } + /** + * Task to set the default language. + * + * @return void + */ + public function setDefault() + { + // Check for request forgeries. + $this->checkToken(); + + $cid = (string) $this->input->get('cid', '', 'string'); + $model = $this->getModel('installed'); + + if ($model->publish($cid)) { + // Switching to the new administrator language for the message + if ($model->getState('client_id') == 1) { + $language = Factory::getLanguage(); + $newLang = Language::getInstance($cid); + Factory::$language = $newLang; + $this->app->loadLanguage($language = $newLang); + $newLang->load('com_languages', JPATH_ADMINISTRATOR); + } + + if (Multilanguage::isEnabled() && $model->getState('client_id') == 0) { + $msg = Text::_('COM_LANGUAGES_MSG_DEFAULT_MULTILANG_SAVED'); + $type = 'message'; + } else { + $msg = Text::_('COM_LANGUAGES_MSG_DEFAULT_LANGUAGE_SAVED'); + $type = 'message'; + } + } else { + $msg = $model->getError(); + $type = 'error'; + } + + $clientId = $model->getState('client_id'); + $this->setRedirect('index.php?option=com_languages&view=installed&client=' . $clientId, $msg, $type); + } + + /** + * Task to switch the administrator language. + * + * @return void + */ + public function switchAdminLanguage() + { + // Check for request forgeries. + $this->checkToken(); + + $cid = (string) $this->input->get('cid', '', 'string'); + $model = $this->getModel('installed'); + + // Fetching the language name from the langmetadata.xml or xx-XX.xml respectively. + $file = JPATH_ADMINISTRATOR . '/language/' . $cid . '/langmetadata.xml'; + + if (!is_file($file)) { + $file = JPATH_ADMINISTRATOR . '/language/' . $cid . '/' . $cid . '.xml'; + } + + $info = LanguageHelper::parseXMLLanguageFile($file); + + if ($model->switchAdminLanguage($cid)) { + // Switching to the new language for the message + $languageName = $info['nativeName']; + $language = Factory::getLanguage(); + $newLang = Language::getInstance($cid); + Factory::$language = $newLang; + $this->app->loadLanguage($language = $newLang); + $newLang->load('com_languages', JPATH_ADMINISTRATOR); + + $msg = Text::sprintf('COM_LANGUAGES_MSG_SWITCH_ADMIN_LANGUAGE_SUCCESS', $languageName); + $type = 'message'; + } else { + $msg = $model->getError(); + $type = 'error'; + } + + $this->setRedirect('index.php?option=com_languages&view=installed', $msg, $type); + } } diff --git a/administrator/components/com_languages/src/Controller/LanguageController.php b/administrator/components/com_languages/src/Controller/LanguageController.php index fadee0f8b483d..68fcbe8d812ae 100644 --- a/administrator/components/com_languages/src/Controller/LanguageController.php +++ b/administrator/components/com_languages/src/Controller/LanguageController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Language', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_languages/src/Controller/OverrideController.php b/administrator/components/com_languages/src/Controller/OverrideController.php index 7f29c944d53f9..7a31d281fe615 100644 --- a/administrator/components/com_languages/src/Controller/OverrideController.php +++ b/administrator/components/com_languages/src/Controller/OverrideController.php @@ -1,4 +1,5 @@ app->allowCache(false); - - $cid = (array) $this->input->post->get('cid', array(), 'string'); - $context = "$this->option.edit.$this->context"; - - // Get the constant name. - $recordId = (count($cid) ? $cid[0] : $this->input->get('id')); - - // Access check. - if (!$this->allowEdit()) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); - - return; - } - - $this->app->setUserState($context . '.data', null); - $this->setRedirect('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, 'id')); - } - - /** - * Method to save an override. - * - * @param string $key The name of the primary key of the URL variable (not used here). - * @param string $urlVar The name of the URL variable if different from the primary key (not used here). - * - * @return void - * - * @since 2.5 - */ - public function save($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - $app = $this->app; - $model = $this->getModel(); - $data = $this->input->post->get('jform', array(), 'array'); - $context = "$this->option.edit.$this->context"; - $task = $this->getTask(); - - $recordId = $this->input->get('id'); - $data['id'] = $recordId; - - // Access check. - if (!$this->allowSave($data, 'id')) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); - - return; - } - - // Validate the posted data. - $form = $model->getForm($data, false); - - if (!$form) - { - $app->enqueueMessage($model->getError(), 'error'); - - return; - } - - // Test whether the data is valid. - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $app->setUserState($context . '.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, 'id'), false) - ); - - return; - } - - // Attempt to save the data. - if (!$model->save($validData)) - { - // Save the data in the session. - $app->setUserState($context . '.data', $validData); - - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, 'id'), false) - ); - - return; - } - - // Add message of success. - $this->setMessage(Text::_('COM_LANGUAGES_VIEW_OVERRIDE_SAVE_SUCCESS')); - - // Redirect the user and adjust session state based on the chosen task. - switch ($task) - { - case 'apply': - // Set the record data in the session. - $app->setUserState($context . '.data', null); - - // Redirect back to the edit screen - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($validData['key'], 'id'), false) - ); - break; - - case 'save2new': - // Clear the record id and data from the session. - $app->setUserState($context . '.data', null); - - // Redirect back to the edit screen - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend(null, 'id'), false) - ); - break; - - default: - // Clear the record id and data from the session. - $app->setUserState($context . '.data', null); - - // Redirect to the list screen. - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); - break; - } - } - - /** - * Method to cancel an edit. - * - * @param string $key The name of the primary key of the URL variable (not used here). - * - * @return void - * - * @since 2.5 - */ - public function cancel($key = null) - { - $this->checkToken(); - - $context = "$this->option.edit.$this->context"; - - $this->app->setUserState($context . '.data', null); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); - } + /** + * Method to edit an existing override. + * + * @param string $key The name of the primary key of the URL variable (not used here). + * @param string $urlVar The name of the URL variable if different from the primary key (not used here). + * + * @return void + * + * @since 2.5 + */ + public function edit($key = null, $urlVar = null) + { + // Do not cache the response to this, its a redirect + $this->app->allowCache(false); + + $cid = (array) $this->input->post->get('cid', array(), 'string'); + $context = "$this->option.edit.$this->context"; + + // Get the constant name. + $recordId = (count($cid) ? $cid[0] : $this->input->get('id')); + + // Access check. + if (!$this->allowEdit()) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); + + return; + } + + $this->app->setUserState($context . '.data', null); + $this->setRedirect('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, 'id')); + } + + /** + * Method to save an override. + * + * @param string $key The name of the primary key of the URL variable (not used here). + * @param string $urlVar The name of the URL variable if different from the primary key (not used here). + * + * @return void + * + * @since 2.5 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + $app = $this->app; + $model = $this->getModel(); + $data = $this->input->post->get('jform', array(), 'array'); + $context = "$this->option.edit.$this->context"; + $task = $this->getTask(); + + $recordId = $this->input->get('id'); + $data['id'] = $recordId; + + // Access check. + if (!$this->allowSave($data, 'id')) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); + + return; + } + + // Validate the posted data. + $form = $model->getForm($data, false); + + if (!$form) { + $app->enqueueMessage($model->getError(), 'error'); + + return; + } + + // Test whether the data is valid. + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $app->setUserState($context . '.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, 'id'), false) + ); + + return; + } + + // Attempt to save the data. + if (!$model->save($validData)) { + // Save the data in the session. + $app->setUserState($context . '.data', $validData); + + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, 'id'), false) + ); + + return; + } + + // Add message of success. + $this->setMessage(Text::_('COM_LANGUAGES_VIEW_OVERRIDE_SAVE_SUCCESS')); + + // Redirect the user and adjust session state based on the chosen task. + switch ($task) { + case 'apply': + // Set the record data in the session. + $app->setUserState($context . '.data', null); + + // Redirect back to the edit screen + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($validData['key'], 'id'), false) + ); + break; + + case 'save2new': + // Clear the record id and data from the session. + $app->setUserState($context . '.data', null); + + // Redirect back to the edit screen + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend(null, 'id'), false) + ); + break; + + default: + // Clear the record id and data from the session. + $app->setUserState($context . '.data', null); + + // Redirect to the list screen. + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); + break; + } + } + + /** + * Method to cancel an edit. + * + * @param string $key The name of the primary key of the URL variable (not used here). + * + * @return void + * + * @since 2.5 + */ + public function cancel($key = null) + { + $this->checkToken(); + + $context = "$this->option.edit.$this->context"; + + $this->app->setUserState($context . '.data', null); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); + } } diff --git a/administrator/components/com_languages/src/Controller/OverridesController.php b/administrator/components/com_languages/src/Controller/OverridesController.php index a3edb544ffc74..e201a428f2f8b 100644 --- a/administrator/components/com_languages/src/Controller/OverridesController.php +++ b/administrator/components/com_languages/src/Controller/OverridesController.php @@ -1,4 +1,5 @@ checkToken(); + /** + * Method for deleting one or more overrides. + * + * @return void + * + * @since 2.5 + */ + public function delete() + { + // Check for request forgeries. + $this->checkToken(); - // Get items to delete from the request. - $cid = (array) $this->input->get('cid', array(), 'string'); + // Get items to delete from the request. + $cid = (array) $this->input->get('cid', array(), 'string'); - // Remove zero values resulting from input filter - $cid = array_filter($cid); + // Remove zero values resulting from input filter + $cid = array_filter($cid); - if (empty($cid)) - { - $this->setMessage(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), 'warning'); - } - else - { - // Get the model. - $model = $this->getModel('overrides'); + if (empty($cid)) { + $this->setMessage(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), 'warning'); + } else { + // Get the model. + $model = $this->getModel('overrides'); - // Remove the items. - if ($model->delete($cid)) - { - $this->setMessage(Text::plural($this->text_prefix . '_N_ITEMS_DELETED', count($cid))); - } - else - { - $this->setMessage($model->getError(), 'error'); - } - } + // Remove the items. + if ($model->delete($cid)) { + $this->setMessage(Text::plural($this->text_prefix . '_N_ITEMS_DELETED', count($cid))); + } else { + $this->setMessage($model->getError(), 'error'); + } + } - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); - } + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); + } - /** - * Method to purge the overrider table. - * - * @return void - * - * @since 3.4.2 - */ - public function purge() - { - // Check for request forgeries. - $this->checkToken(); + /** + * Method to purge the overrider table. + * + * @return void + * + * @since 3.4.2 + */ + public function purge() + { + // Check for request forgeries. + $this->checkToken(); - /** @var \Joomla\Component\Languages\Administrator\Model\OverridesModel $model */ - $model = $this->getModel('overrides'); - $model->purge(); - $this->setRedirect(Route::_('index.php?option=com_languages&view=overrides', false)); - } + /** @var \Joomla\Component\Languages\Administrator\Model\OverridesModel $model */ + $model = $this->getModel('overrides'); + $model->purge(); + $this->setRedirect(Route::_('index.php?option=com_languages&view=overrides', false)); + } } diff --git a/administrator/components/com_languages/src/Controller/StringsController.php b/administrator/components/com_languages/src/Controller/StringsController.php index 36de315c64218..48709d0bad69d 100644 --- a/administrator/components/com_languages/src/Controller/StringsController.php +++ b/administrator/components/com_languages/src/Controller/StringsController.php @@ -1,4 +1,5 @@ getModel('strings')->refresh()); - } + /** + * Method for refreshing the cache in the database with the known language strings + * + * @return void + * + * @since 2.5 + */ + public function refresh() + { + echo new JsonResponse($this->getModel('strings')->refresh()); + } - /** - * Method for searching language strings - * - * @return void - * - * @since 2.5 - */ - public function search() - { - echo new JsonResponse($this->getModel('strings')->search()); - } + /** + * Method for searching language strings + * + * @return void + * + * @since 2.5 + */ + public function search() + { + echo new JsonResponse($this->getModel('strings')->search()); + } } diff --git a/administrator/components/com_languages/src/Extension/LanguagesComponent.php b/administrator/components/com_languages/src/Extension/LanguagesComponent.php index 0621719ae1307..ba65a42a7c9b4 100644 --- a/administrator/components/com_languages/src/Extension/LanguagesComponent.php +++ b/administrator/components/com_languages/src/Extension/LanguagesComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('languages', new Languages); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('languages', new Languages()); + } } diff --git a/administrator/components/com_languages/src/Field/LanguageclientField.php b/administrator/components/com_languages/src/Field/LanguageclientField.php index 612f49be02413..e54bd26472d35 100644 --- a/administrator/components/com_languages/src/Field/LanguageclientField.php +++ b/administrator/components/com_languages/src/Field/LanguageclientField.php @@ -1,4 +1,5 @@ cache)) - { - return $this->cache; - } + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.9.0 + */ + protected function getOptions() + { + // Try to load the data from our mini-cache. + if (!empty($this->cache)) { + return $this->cache; + } - // Get all languages of frontend and backend. - $languages = array(); - $site_languages = LanguageHelper::getKnownLanguages(JPATH_SITE); - $admin_languages = LanguageHelper::getKnownLanguages(JPATH_ADMINISTRATOR); + // Get all languages of frontend and backend. + $languages = array(); + $site_languages = LanguageHelper::getKnownLanguages(JPATH_SITE); + $admin_languages = LanguageHelper::getKnownLanguages(JPATH_ADMINISTRATOR); - // Create a single array of them. - foreach ($site_languages as $tag => $language) - { - $languages[$tag . '0'] = Text::sprintf('COM_LANGUAGES_VIEW_OVERRIDES_LANGUAGES_BOX_ITEM', $language['name'], Text::_('JSITE')); - } + // Create a single array of them. + foreach ($site_languages as $tag => $language) { + $languages[$tag . '0'] = Text::sprintf('COM_LANGUAGES_VIEW_OVERRIDES_LANGUAGES_BOX_ITEM', $language['name'], Text::_('JSITE')); + } - foreach ($admin_languages as $tag => $language) - { - $languages[$tag . '1'] = Text::sprintf('COM_LANGUAGES_VIEW_OVERRIDES_LANGUAGES_BOX_ITEM', $language['name'], Text::_('JADMINISTRATOR')); - } + foreach ($admin_languages as $tag => $language) { + $languages[$tag . '1'] = Text::sprintf('COM_LANGUAGES_VIEW_OVERRIDES_LANGUAGES_BOX_ITEM', $language['name'], Text::_('JADMINISTRATOR')); + } - // Sort it by language tag and by client after that. - ksort($languages); + // Sort it by language tag and by client after that. + ksort($languages); - // Add the languages to the internal cache. - $this->cache = array_merge(parent::getOptions(), $languages); + // Add the languages to the internal cache. + $this->cache = array_merge(parent::getOptions(), $languages); - return $this->cache; - } + return $this->cache; + } } diff --git a/administrator/components/com_languages/src/Helper/LanguagesHelper.php b/administrator/components/com_languages/src/Helper/LanguagesHelper.php index acf03db636b8e..f812b92e06e80 100644 --- a/administrator/components/com_languages/src/Helper/LanguagesHelper.php +++ b/administrator/components/com_languages/src/Helper/LanguagesHelper.php @@ -1,4 +1,5 @@ clean($value, 'cmd')); - } - - /** - * Filter method for language strings. - * This method will be called by \JForm while filtering the form data. - * - * @param string $value The language string to filter. - * - * @return string The filtered language string. - * - * @since 2.5 - */ - public static function filterText($value) - { - $filter = InputFilter::getInstance([], [], InputFilter::ONLY_BLOCK_DEFINED_TAGS, InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES); - - return $filter->clean($value); - } + /** + * Filter method for language keys. + * This method will be called by \JForm while filtering the form data. + * + * @param string $value The language key to filter. + * + * @return string The filtered language key. + * + * @since 2.5 + */ + public static function filterKey($value) + { + $filter = InputFilter::getInstance([], [], InputFilter::ONLY_BLOCK_DEFINED_TAGS, InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES); + + return strtoupper($filter->clean($value, 'cmd')); + } + + /** + * Filter method for language strings. + * This method will be called by \JForm while filtering the form data. + * + * @param string $value The language string to filter. + * + * @return string The filtered language string. + * + * @since 2.5 + */ + public static function filterText($value) + { + $filter = InputFilter::getInstance([], [], InputFilter::ONLY_BLOCK_DEFINED_TAGS, InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES); + + return $filter->clean($value); + } } diff --git a/administrator/components/com_languages/src/Helper/MultilangstatusHelper.php b/administrator/components/com_languages/src/Helper/MultilangstatusHelper.php index ba2e534f7d088..7a11535cdb4db 100644 --- a/administrator/components/com_languages/src/Helper/MultilangstatusHelper.php +++ b/administrator/components/com_languages/src/Helper/MultilangstatusHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__menu')) - ->where( - [ - $db->quoteName('home') . ' = 1', - $db->quoteName('published') . ' = 1', - $db->quoteName('client_id') . ' = 0', - ] - ); - - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Method to get the number of published language switcher modules. - * - * @return integer - */ - public static function getLangswitchers() - { - // Check if switcher is published. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__modules')) - ->where( - [ - $db->quoteName('module') . ' = ' . $db->quote('mod_languages'), - $db->quoteName('published') . ' = 1', - $db->quoteName('client_id') . ' = 0', - ] - ); - - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Method to return a list of published content languages. - * - * @return array of language objects. - */ - public static function getContentlangs() - { - // Check for published Content Languages. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('lang_code'), - $db->quoteName('published'), - $db->quoteName('sef'), - ] - ) - ->from($db->quoteName('#__languages')); - - $db->setQuery($query); - - return $db->loadObjectList(); - } - - /** - * Method to return combined language status. - * - * @return array of language objects. - */ - public static function getStatus() - { - // Check for combined status. - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - // Select all fields from the languages table. - $query->select( - [ - $db->quoteName('a') . '.*', - $db->quoteName('a.published'), - $db->quoteName('a.lang_code'), - $db->quoteName('e.enabled'), - $db->quoteName('e.element'), - $db->quoteName('l.home'), - $db->quoteName('l.published', 'home_published'), - ] - ) - ->from($db->quoteName('#__languages', 'a')) - ->join( - 'LEFT', - $db->quoteName('#__menu', 'l'), - $db->quoteName('l.language') . ' = ' . $db->quoteName('a.lang_code') - . ' AND ' . $db->quoteName('l.home') . ' = 1 AND ' . $db->quoteName('l.language') . ' <> ' . $db->quote('*') - ) - ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.element') . ' = ' . $db->quoteName('a.lang_code')) - ->where( - [ - $db->quoteName('e.client_id') . ' = 0', - $db->quoteName('e.enabled') . ' = 1', - $db->quoteName('e.state') . ' = 0', - ] - ); - - $db->setQuery($query); - - return $db->loadObjectList(); - } - - /** - * Method to return a list of contact objects. - * - * @return array of contact objects. - */ - public static function getContacts() - { - $db = Factory::getDbo(); - $languages = count(LanguageHelper::getLanguages()); - - // Get the number of contact with all as language - $alang = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__contact_details', 'cd')) - ->where( - [ - $db->quoteName('cd.user_id') . ' = ' . $db->quoteName('u.id'), - $db->quoteName('cd.published') . ' = 1', - $db->quoteName('cd.language') . ' = ' . $db->quote('*'), - ] - ); - - // Get the number of languages for the contact - $slang = $db->getQuery(true) - ->select('COUNT(DISTINCT ' . $db->quoteName('l.lang_code') . ')') - ->from($db->quoteName('#__languages', 'l')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd'), $db->quoteName('cd.language') . ' = ' . $db->quoteName('l.lang_code')) - ->where( - [ - $db->quoteName('cd.user_id') . ' = ' . $db->quoteName('u.id'), - $db->quoteName('cd.published') . ' = 1', - $db->quoteName('l.published') . ' = 1', - ] - ); - - // Get the number of multiple contact/language - $mlang = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__languages', 'l')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd'), $db->quoteName('cd.language') . ' = ' . $db->quoteName('l.lang_code')) - ->where( - [ - $db->quoteName('cd.user_id') . ' = ' . $db->quoteName('u.id'), - $db->quoteName('cd.published') . ' = 1', - $db->quoteName('l.published') . ' = 1', - ] - ) - ->group($db->quoteName('l.lang_code')) - ->having('COUNT(*) > 1'); - - // Get the contacts - $subQuery = $db->getQuery(true) - ->select('1') - ->from($db->quoteName('#__content', 'c')) - ->where($db->quoteName('c.created_by') . ' = ' . $db->quoteName('u.id')); - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('u.name'), - '(' . $alang . ') AS ' . $db->quoteName('alang'), - '(' . $slang . ') AS ' . $db->quoteName('slang'), - '(' . $mlang . ') AS ' . $db->quoteName('mlang'), - ] - ) - ->from($db->quoteName('#__users', 'u')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd'), $db->quoteName('cd.user_id') . ' = ' . $db->quoteName('u.id')) - ->where('EXISTS (' . $subQuery . ')') - ->group( - [ - $db->quoteName('u.id'), - $db->quoteName('u.name'), - ] - ); - - $db->setQuery($query); - $warnings = $db->loadObjectList(); - - foreach ($warnings as $index => $warn) - { - if ($warn->alang == 1 && $warn->slang == 0) - { - unset($warnings[$index]); - } - - if ($warn->alang == 0 && $warn->slang == 0 && empty($warn->mlang)) - { - unset($warnings[$index]); - } - - if ($warn->alang == 0 && $warn->slang == $languages && empty($warn->mlang)) - { - unset($warnings[$index]); - } - } - - return $warnings; - } - - /** - * Method to get the status of the module displaying the menutype of the default Home page set to All languages. - * - * @return boolean True if the module is published, false otherwise. - * - * @since 3.7.0 - */ - public static function getDefaultHomeModule() - { - // Find Default Home menutype. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('menutype')) - ->from($db->quoteName('#__menu')) - ->where( - [ - $db->quoteName('home') . ' = 1', - $db->quoteName('published') . ' = 1', - $db->quoteName('client_id') . ' = 0', - $db->quoteName('language') . ' = ' . $db->quote('*'), - ] - ); - - $db->setQuery($query); - - $menutype = $db->loadResult(); - - // Get published site menu modules titles. - $query->clear() - ->select($db->quoteName('title')) - ->from($db->quoteName('#__modules')) - ->where( - [ - $db->quoteName('module') . ' = ' . $db->quote('mod_menu'), - $db->quoteName('published') . ' = 1', - $db->quoteName('client_id') . ' = 0', - ] - ); - - $db->setQuery($query); - - $menutitles = $db->loadColumn(); - - // Do we have a published menu module displaying the default Home menu item set to all languages? - foreach ($menutitles as $menutitle) - { - $module = self::getModule('mod_menu', $menutitle); - $moduleParams = new Registry($module->params); - $param = $moduleParams->get('menutype', ''); - - if ($param && $param != $menutype) - { - continue; - } - - return true; - } - } - - /** - * Get module by name - * - * @param string $moduleName The name of the module - * @param string $instanceTitle The title of the module, optional - * - * @return \stdClass The Module object - * - * @since 3.7.0 - */ - public static function getModule($moduleName, $instanceTitle = null) - { - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('id'), - $db->quoteName('title'), - $db->quoteName('module'), - $db->quoteName('position'), - $db->quoteName('content'), - $db->quoteName('showtitle'), - $db->quoteName('params'), - ] - ) - ->from($db->quoteName('#__modules')) - ->where( - [ - $db->quoteName('module') . ' = :module', - $db->quoteName('published') . ' = 1', - $db->quoteName('client_id') . ' = 0', - ] - ) - ->bind(':module', $moduleName); - - if ($instanceTitle) - { - $query->where($db->quoteName('title') . ' = :title') - ->bind(':title', $instanceTitle); - } - - $db->setQuery($query); - - try - { - $modules = $db->loadObject(); - } - catch (\RuntimeException $e) - { - Log::add(Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), Log::WARNING, 'jerror'); - } - - return $modules; - } + /** + * Method to get the number of published home pages. + * + * @return integer + */ + public static function getHomes() + { + // Check for multiple Home pages. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__menu')) + ->where( + [ + $db->quoteName('home') . ' = 1', + $db->quoteName('published') . ' = 1', + $db->quoteName('client_id') . ' = 0', + ] + ); + + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Method to get the number of published language switcher modules. + * + * @return integer + */ + public static function getLangswitchers() + { + // Check if switcher is published. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__modules')) + ->where( + [ + $db->quoteName('module') . ' = ' . $db->quote('mod_languages'), + $db->quoteName('published') . ' = 1', + $db->quoteName('client_id') . ' = 0', + ] + ); + + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Method to return a list of published content languages. + * + * @return array of language objects. + */ + public static function getContentlangs() + { + // Check for published Content Languages. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('lang_code'), + $db->quoteName('published'), + $db->quoteName('sef'), + ] + ) + ->from($db->quoteName('#__languages')); + + $db->setQuery($query); + + return $db->loadObjectList(); + } + + /** + * Method to return combined language status. + * + * @return array of language objects. + */ + public static function getStatus() + { + // Check for combined status. + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + // Select all fields from the languages table. + $query->select( + [ + $db->quoteName('a') . '.*', + $db->quoteName('a.published'), + $db->quoteName('a.lang_code'), + $db->quoteName('e.enabled'), + $db->quoteName('e.element'), + $db->quoteName('l.home'), + $db->quoteName('l.published', 'home_published'), + ] + ) + ->from($db->quoteName('#__languages', 'a')) + ->join( + 'LEFT', + $db->quoteName('#__menu', 'l'), + $db->quoteName('l.language') . ' = ' . $db->quoteName('a.lang_code') + . ' AND ' . $db->quoteName('l.home') . ' = 1 AND ' . $db->quoteName('l.language') . ' <> ' . $db->quote('*') + ) + ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.element') . ' = ' . $db->quoteName('a.lang_code')) + ->where( + [ + $db->quoteName('e.client_id') . ' = 0', + $db->quoteName('e.enabled') . ' = 1', + $db->quoteName('e.state') . ' = 0', + ] + ); + + $db->setQuery($query); + + return $db->loadObjectList(); + } + + /** + * Method to return a list of contact objects. + * + * @return array of contact objects. + */ + public static function getContacts() + { + $db = Factory::getDbo(); + $languages = count(LanguageHelper::getLanguages()); + + // Get the number of contact with all as language + $alang = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__contact_details', 'cd')) + ->where( + [ + $db->quoteName('cd.user_id') . ' = ' . $db->quoteName('u.id'), + $db->quoteName('cd.published') . ' = 1', + $db->quoteName('cd.language') . ' = ' . $db->quote('*'), + ] + ); + + // Get the number of languages for the contact + $slang = $db->getQuery(true) + ->select('COUNT(DISTINCT ' . $db->quoteName('l.lang_code') . ')') + ->from($db->quoteName('#__languages', 'l')) + ->join('LEFT', $db->quoteName('#__contact_details', 'cd'), $db->quoteName('cd.language') . ' = ' . $db->quoteName('l.lang_code')) + ->where( + [ + $db->quoteName('cd.user_id') . ' = ' . $db->quoteName('u.id'), + $db->quoteName('cd.published') . ' = 1', + $db->quoteName('l.published') . ' = 1', + ] + ); + + // Get the number of multiple contact/language + $mlang = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__languages', 'l')) + ->join('LEFT', $db->quoteName('#__contact_details', 'cd'), $db->quoteName('cd.language') . ' = ' . $db->quoteName('l.lang_code')) + ->where( + [ + $db->quoteName('cd.user_id') . ' = ' . $db->quoteName('u.id'), + $db->quoteName('cd.published') . ' = 1', + $db->quoteName('l.published') . ' = 1', + ] + ) + ->group($db->quoteName('l.lang_code')) + ->having('COUNT(*) > 1'); + + // Get the contacts + $subQuery = $db->getQuery(true) + ->select('1') + ->from($db->quoteName('#__content', 'c')) + ->where($db->quoteName('c.created_by') . ' = ' . $db->quoteName('u.id')); + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('u.name'), + '(' . $alang . ') AS ' . $db->quoteName('alang'), + '(' . $slang . ') AS ' . $db->quoteName('slang'), + '(' . $mlang . ') AS ' . $db->quoteName('mlang'), + ] + ) + ->from($db->quoteName('#__users', 'u')) + ->join('LEFT', $db->quoteName('#__contact_details', 'cd'), $db->quoteName('cd.user_id') . ' = ' . $db->quoteName('u.id')) + ->where('EXISTS (' . $subQuery . ')') + ->group( + [ + $db->quoteName('u.id'), + $db->quoteName('u.name'), + ] + ); + + $db->setQuery($query); + $warnings = $db->loadObjectList(); + + foreach ($warnings as $index => $warn) { + if ($warn->alang == 1 && $warn->slang == 0) { + unset($warnings[$index]); + } + + if ($warn->alang == 0 && $warn->slang == 0 && empty($warn->mlang)) { + unset($warnings[$index]); + } + + if ($warn->alang == 0 && $warn->slang == $languages && empty($warn->mlang)) { + unset($warnings[$index]); + } + } + + return $warnings; + } + + /** + * Method to get the status of the module displaying the menutype of the default Home page set to All languages. + * + * @return boolean True if the module is published, false otherwise. + * + * @since 3.7.0 + */ + public static function getDefaultHomeModule() + { + // Find Default Home menutype. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('menutype')) + ->from($db->quoteName('#__menu')) + ->where( + [ + $db->quoteName('home') . ' = 1', + $db->quoteName('published') . ' = 1', + $db->quoteName('client_id') . ' = 0', + $db->quoteName('language') . ' = ' . $db->quote('*'), + ] + ); + + $db->setQuery($query); + + $menutype = $db->loadResult(); + + // Get published site menu modules titles. + $query->clear() + ->select($db->quoteName('title')) + ->from($db->quoteName('#__modules')) + ->where( + [ + $db->quoteName('module') . ' = ' . $db->quote('mod_menu'), + $db->quoteName('published') . ' = 1', + $db->quoteName('client_id') . ' = 0', + ] + ); + + $db->setQuery($query); + + $menutitles = $db->loadColumn(); + + // Do we have a published menu module displaying the default Home menu item set to all languages? + foreach ($menutitles as $menutitle) { + $module = self::getModule('mod_menu', $menutitle); + $moduleParams = new Registry($module->params); + $param = $moduleParams->get('menutype', ''); + + if ($param && $param != $menutype) { + continue; + } + + return true; + } + } + + /** + * Get module by name + * + * @param string $moduleName The name of the module + * @param string $instanceTitle The title of the module, optional + * + * @return \stdClass The Module object + * + * @since 3.7.0 + */ + public static function getModule($moduleName, $instanceTitle = null) + { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('id'), + $db->quoteName('title'), + $db->quoteName('module'), + $db->quoteName('position'), + $db->quoteName('content'), + $db->quoteName('showtitle'), + $db->quoteName('params'), + ] + ) + ->from($db->quoteName('#__modules')) + ->where( + [ + $db->quoteName('module') . ' = :module', + $db->quoteName('published') . ' = 1', + $db->quoteName('client_id') . ' = 0', + ] + ) + ->bind(':module', $moduleName); + + if ($instanceTitle) { + $query->where($db->quoteName('title') . ' = :title') + ->bind(':title', $instanceTitle); + } + + $db->setQuery($query); + + try { + $modules = $db->loadObject(); + } catch (\RuntimeException $e) { + Log::add(Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), Log::WARNING, 'jerror'); + } + + return $modules; + } } diff --git a/administrator/components/com_languages/src/Model/InstalledModel.php b/administrator/components/com_languages/src/Model/InstalledModel.php index 0660c31dba961..4c32177c4b3dd 100644 --- a/administrator/components/com_languages/src/Model/InstalledModel.php +++ b/administrator/components/com_languages/src/Model/InstalledModel.php @@ -1,4 +1,5 @@ setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - - // Special case for client id. - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $clientId = (!in_array($clientId, array (0, 1))) ? 0 : $clientId; - $this->setState('client_id', $clientId); - - // Load the parameters. - $params = ComponentHelper::getParams('com_languages'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('client_id'); - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Method to get the client object. - * - * @return object - * - * @since 1.6 - */ - public function getClient() - { - return ApplicationHelper::getClientInfo($this->getState('client_id', 0)); - } - - /** - * Method to get the option. - * - * @return object - * - * @since 1.6 - */ - public function getOption() - { - $option = $this->getState('option'); - - return $option; - } - - /** - * Method to get Languages item data. - * - * @return array - * - * @since 1.6 - */ - public function getData() - { - // Fetch language data if not fetched yet. - if (is_null($this->data)) - { - $this->data = array(); - - $isCurrentLanguageRtl = Factory::getLanguage()->isRtl(); - $params = ComponentHelper::getParams('com_languages'); - $installedLanguages = LanguageHelper::getInstalledLanguages(null, true, true, null, null, null); - - // Compute all the languages. - foreach ($installedLanguages as $clientId => $languages) - { - $defaultLanguage = $params->get(ApplicationHelper::getClientInfo($clientId)->name, 'en-GB'); - - foreach ($languages as $lang) - { - $row = new \stdClass; - $row->language = $lang->element; - $row->name = $lang->metadata['name']; - $row->nativeName = $lang->metadata['nativeName'] ?? '-'; - $row->client_id = (int) $lang->client_id; - $row->extension_id = (int) $lang->extension_id; - $row->author = $lang->manifest['author']; - $row->creationDate = $lang->manifest['creationDate']; - $row->authorEmail = $lang->manifest['authorEmail']; - $row->version = $lang->manifest['version']; - $row->published = $defaultLanguage === $row->language ? 1 : 0; - $row->checked_out = null; - - // Fix wrongly set parentheses in RTL languages - if ($isCurrentLanguageRtl) - { - $row->name = html_entity_decode($row->name . '‎', ENT_QUOTES, 'UTF-8'); - $row->nativeName = html_entity_decode($row->nativeName . '‎', ENT_QUOTES, 'UTF-8'); - } - - $this->data[] = $row; - } - } - } - - $installedLanguages = array_merge($this->data); - - // Process filters. - $clientId = (int) $this->getState('client_id'); - $search = $this->getState('filter.search'); - - foreach ($installedLanguages as $key => $installedLanguage) - { - // Filter by client id. - if (in_array($clientId, array(0, 1))) - { - if ($installedLanguage->client_id !== $clientId) - { - unset($installedLanguages[$key]); - continue; - } - } - - // Filter by search term. - if (!empty($search)) - { - if (stripos($installedLanguage->name, $search) === false - && stripos($installedLanguage->nativeName, $search) === false - && stripos($installedLanguage->language, $search) === false) - { - unset($installedLanguages[$key]); - } - } - } - - // Process ordering. - $listOrder = $this->getState('list.ordering', 'name'); - $listDirn = $this->getState('list.direction', 'ASC'); - $installedLanguages = ArrayHelper::sortObjects($installedLanguages, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); - - // Process pagination. - $limit = (int) $this->getState('list.limit', 25); - - // Sets the total for pagination. - $this->total = count($installedLanguages); - - if ($limit !== 0) - { - $start = (int) $this->getState('list.start', 0); - - return array_slice($installedLanguages, $start, $limit); - } - - return $installedLanguages; - } - - /** - * Method to get the total number of Languages items. - * - * @return integer - * - * @since 1.6 - */ - public function getTotal() - { - if (is_null($this->total)) - { - $this->getData(); - } - - return $this->total; - } - - /** - * Method to set the default language. - * - * @param integer $cid Id of the language to publish. - * - * @return boolean - * - * @since 1.6 - */ - public function publish($cid) - { - if ($cid) - { - $client = $this->getClient(); - - $params = ComponentHelper::getParams('com_languages'); - $params->set($client->name, $cid); - - $table = Table::getInstance('extension', 'Joomla\\CMS\\Table\\'); - $id = $table->find(array('element' => 'com_languages')); - - // Load. - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - $table->params = (string) $params; - - // Pre-save checks. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Save the changes. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - else - { - $this->setError(Text::_('COM_LANGUAGES_ERR_NO_LANGUAGE_SELECTED')); - - return false; - } - - // Clean the cache of com_languages and component cache. - $this->cleanCache(); - $this->cleanCache('_system'); - - return true; - } - - /** - * Method to get the folders. - * - * @return array Languages folders. - * - * @since 1.6 - */ - protected function getFolders() - { - if (is_null($this->folders)) - { - $path = $this->getPath(); - $this->folders = Folder::folders($path, '.', false, false, array('.svn', 'CVS', '.DS_Store', '__MACOSX', 'pdf_fonts', 'overrides')); - } - - return $this->folders; - } - - /** - * Method to get the path. - * - * @return string The path to the languages folders. - * - * @since 1.6 - */ - protected function getPath() - { - if (is_null($this->path)) - { - $client = $this->getClient(); - $this->path = LanguageHelper::getLanguagePath($client->path); - } - - return $this->path; - } - - /** - * Method to switch the administrator language. - * - * @param string $cid The language tag. - * - * @return boolean - * - * @since 3.5 - */ - public function switchAdminLanguage($cid) - { - if ($cid) - { - $client = $this->getClient(); - - if ($client->name == 'administrator') - { - Factory::getApplication()->setUserState('application.lang', $cid); - } - } - else - { - Factory::getApplication()->enqueueMessage(Text::_('COM_LANGUAGES_ERR_NO_LANGUAGE_SELECTED'), 'error'); - - return false; - } - - return true; - } + /** + * @var object user object + */ + protected $user = null; + + /** + * @var string option name + */ + protected $option = null; + + /** + * @var array languages description + */ + protected $data = null; + + /** + * @var integer total number of languages + */ + protected $total = null; + + /** + * @var string language path + */ + protected $path = null; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'name', + 'nativeName', + 'language', + 'author', + 'published', + 'version', + 'creationDate', + 'author', + 'authorEmail', + 'extension_id', + 'client_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + + // Special case for client id. + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $clientId = (!in_array($clientId, array (0, 1))) ? 0 : $clientId; + $this->setState('client_id', $clientId); + + // Load the parameters. + $params = ComponentHelper::getParams('com_languages'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('client_id'); + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Method to get the client object. + * + * @return object + * + * @since 1.6 + */ + public function getClient() + { + return ApplicationHelper::getClientInfo($this->getState('client_id', 0)); + } + + /** + * Method to get the option. + * + * @return object + * + * @since 1.6 + */ + public function getOption() + { + $option = $this->getState('option'); + + return $option; + } + + /** + * Method to get Languages item data. + * + * @return array + * + * @since 1.6 + */ + public function getData() + { + // Fetch language data if not fetched yet. + if (is_null($this->data)) { + $this->data = array(); + + $isCurrentLanguageRtl = Factory::getLanguage()->isRtl(); + $params = ComponentHelper::getParams('com_languages'); + $installedLanguages = LanguageHelper::getInstalledLanguages(null, true, true, null, null, null); + + // Compute all the languages. + foreach ($installedLanguages as $clientId => $languages) { + $defaultLanguage = $params->get(ApplicationHelper::getClientInfo($clientId)->name, 'en-GB'); + + foreach ($languages as $lang) { + $row = new \stdClass(); + $row->language = $lang->element; + $row->name = $lang->metadata['name']; + $row->nativeName = $lang->metadata['nativeName'] ?? '-'; + $row->client_id = (int) $lang->client_id; + $row->extension_id = (int) $lang->extension_id; + $row->author = $lang->manifest['author']; + $row->creationDate = $lang->manifest['creationDate']; + $row->authorEmail = $lang->manifest['authorEmail']; + $row->version = $lang->manifest['version']; + $row->published = $defaultLanguage === $row->language ? 1 : 0; + $row->checked_out = null; + + // Fix wrongly set parentheses in RTL languages + if ($isCurrentLanguageRtl) { + $row->name = html_entity_decode($row->name . '‎', ENT_QUOTES, 'UTF-8'); + $row->nativeName = html_entity_decode($row->nativeName . '‎', ENT_QUOTES, 'UTF-8'); + } + + $this->data[] = $row; + } + } + } + + $installedLanguages = array_merge($this->data); + + // Process filters. + $clientId = (int) $this->getState('client_id'); + $search = $this->getState('filter.search'); + + foreach ($installedLanguages as $key => $installedLanguage) { + // Filter by client id. + if (in_array($clientId, array(0, 1))) { + if ($installedLanguage->client_id !== $clientId) { + unset($installedLanguages[$key]); + continue; + } + } + + // Filter by search term. + if (!empty($search)) { + if ( + stripos($installedLanguage->name, $search) === false + && stripos($installedLanguage->nativeName, $search) === false + && stripos($installedLanguage->language, $search) === false + ) { + unset($installedLanguages[$key]); + } + } + } + + // Process ordering. + $listOrder = $this->getState('list.ordering', 'name'); + $listDirn = $this->getState('list.direction', 'ASC'); + $installedLanguages = ArrayHelper::sortObjects($installedLanguages, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); + + // Process pagination. + $limit = (int) $this->getState('list.limit', 25); + + // Sets the total for pagination. + $this->total = count($installedLanguages); + + if ($limit !== 0) { + $start = (int) $this->getState('list.start', 0); + + return array_slice($installedLanguages, $start, $limit); + } + + return $installedLanguages; + } + + /** + * Method to get the total number of Languages items. + * + * @return integer + * + * @since 1.6 + */ + public function getTotal() + { + if (is_null($this->total)) { + $this->getData(); + } + + return $this->total; + } + + /** + * Method to set the default language. + * + * @param integer $cid Id of the language to publish. + * + * @return boolean + * + * @since 1.6 + */ + public function publish($cid) + { + if ($cid) { + $client = $this->getClient(); + + $params = ComponentHelper::getParams('com_languages'); + $params->set($client->name, $cid); + + $table = Table::getInstance('extension', 'Joomla\\CMS\\Table\\'); + $id = $table->find(array('element' => 'com_languages')); + + // Load. + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + $table->params = (string) $params; + + // Pre-save checks. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Save the changes. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } else { + $this->setError(Text::_('COM_LANGUAGES_ERR_NO_LANGUAGE_SELECTED')); + + return false; + } + + // Clean the cache of com_languages and component cache. + $this->cleanCache(); + $this->cleanCache('_system'); + + return true; + } + + /** + * Method to get the folders. + * + * @return array Languages folders. + * + * @since 1.6 + */ + protected function getFolders() + { + if (is_null($this->folders)) { + $path = $this->getPath(); + $this->folders = Folder::folders($path, '.', false, false, array('.svn', 'CVS', '.DS_Store', '__MACOSX', 'pdf_fonts', 'overrides')); + } + + return $this->folders; + } + + /** + * Method to get the path. + * + * @return string The path to the languages folders. + * + * @since 1.6 + */ + protected function getPath() + { + if (is_null($this->path)) { + $client = $this->getClient(); + $this->path = LanguageHelper::getLanguagePath($client->path); + } + + return $this->path; + } + + /** + * Method to switch the administrator language. + * + * @param string $cid The language tag. + * + * @return boolean + * + * @since 3.5 + */ + public function switchAdminLanguage($cid) + { + if ($cid) { + $client = $this->getClient(); + + if ($client->name == 'administrator') { + Factory::getApplication()->setUserState('application.lang', $cid); + } + } else { + Factory::getApplication()->enqueueMessage(Text::_('COM_LANGUAGES_ERR_NO_LANGUAGE_SELECTED'), 'error'); + + return false; + } + + return true; + } } diff --git a/administrator/components/com_languages/src/Model/LanguageModel.php b/administrator/components/com_languages/src/Model/LanguageModel.php index c9b953e22afbf..568ea95391d5d 100644 --- a/administrator/components/com_languages/src/Model/LanguageModel.php +++ b/administrator/components/com_languages/src/Model/LanguageModel.php @@ -1,4 +1,5 @@ 'onExtensionAfterSave', - 'event_before_save' => 'onExtensionBeforeSave', - 'events_map' => array( - 'save' => 'extension' - ) - ), $config - ); - - parent::__construct($config, $factory); - } - - /** - * Override to get the table. - * - * @param string $name Name of the table. - * @param string $prefix Table name prefix. - * @param array $options Array of options. - * - * @return Table - * - * @since 1.6 - */ - public function getTable($name = '', $prefix = '', $options = array()) - { - return Table::getInstance('Language', 'Joomla\\CMS\\Table\\'); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - $params = ComponentHelper::getParams('com_languages'); - - // Load the User state. - $langId = $app->input->getInt('lang_id'); - $this->setState('language.id', $langId); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to get a member item. - * - * @param integer $langId The id of the member to get. - * - * @return mixed User data object on success, false on failure. - * - * @since 1.0 - */ - public function getItem($langId = null) - { - $langId = (!empty($langId)) ? $langId : (int) $this->getState('language.id'); - - // Get a member row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load($langId); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - - // Set a valid accesslevel in case '0' is stored due to a bug in the installation SQL (was fixed with PR 2714). - if ($table->access == '0') - { - $table->access = (int) Factory::getApplication()->get('access'); - } - - $properties = $table->getProperties(1); - $value = ArrayHelper::toObject($properties, CMSObject::class); - - return $value; - } - - /** - * Method to get the group form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return \Joomla\CMS\Form\Form|bool A Form object on success, false on failure. - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_languages.language', 'language', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_languages.edit.language.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_languages.language', $data); - - return $data; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $langId = (!empty($data['lang_id'])) ? $data['lang_id'] : (int) $this->getState('language.id'); - $isNew = true; - - PluginHelper::importPlugin($this->events_map['save']); - - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - - // Load the row if saving an existing item. - if ($langId > 0) - { - $table->load($langId); - $isNew = false; - } - - // Prevent white spaces, including East Asian double bytes. - $spaces = array('/\xE3\x80\x80/', ' '); - - $data['lang_code'] = str_replace($spaces, '', $data['lang_code']); - - // Prevent saving an incorrect language tag - if (!preg_match('#\b([a-z]{2,3})[-]([A-Z]{2})\b#', $data['lang_code'])) - { - $this->setError(Text::_('COM_LANGUAGES_ERROR_LANG_TAG')); - - return false; - } - - $data['sef'] = str_replace($spaces, '', $data['sef']); - $data['sef'] = ApplicationHelper::stringURLSafe($data['sef']); - - // Prevent saving an empty url language code - if ($data['sef'] === '') - { - $this->setError(Text::_('COM_LANGUAGES_ERROR_SEF')); - - return false; - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew)); - - // Check the event responses. - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew)); - - $this->setState('language.id', $table->lang_id); - - // Clean the cache. - $this->cleanCache(); - - return true; - } - - /** - * Custom clean cache method. - * - * @param string $group Optional cache group name. - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('_system'); - parent::cleanCache('com_languages'); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $config = array_merge( + array( + 'event_after_save' => 'onExtensionAfterSave', + 'event_before_save' => 'onExtensionBeforeSave', + 'events_map' => array( + 'save' => 'extension' + ) + ), + $config + ); + + parent::__construct($config, $factory); + } + + /** + * Override to get the table. + * + * @param string $name Name of the table. + * @param string $prefix Table name prefix. + * @param array $options Array of options. + * + * @return Table + * + * @since 1.6 + */ + public function getTable($name = '', $prefix = '', $options = array()) + { + return Table::getInstance('Language', 'Joomla\\CMS\\Table\\'); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + $params = ComponentHelper::getParams('com_languages'); + + // Load the User state. + $langId = $app->input->getInt('lang_id'); + $this->setState('language.id', $langId); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to get a member item. + * + * @param integer $langId The id of the member to get. + * + * @return mixed User data object on success, false on failure. + * + * @since 1.0 + */ + public function getItem($langId = null) + { + $langId = (!empty($langId)) ? $langId : (int) $this->getState('language.id'); + + // Get a member row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load($langId); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + + // Set a valid accesslevel in case '0' is stored due to a bug in the installation SQL (was fixed with PR 2714). + if ($table->access == '0') { + $table->access = (int) Factory::getApplication()->get('access'); + } + + $properties = $table->getProperties(1); + $value = ArrayHelper::toObject($properties, CMSObject::class); + + return $value; + } + + /** + * Method to get the group form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|bool A Form object on success, false on failure. + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_languages.language', 'language', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_languages.edit.language.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_languages.language', $data); + + return $data; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $langId = (!empty($data['lang_id'])) ? $data['lang_id'] : (int) $this->getState('language.id'); + $isNew = true; + + PluginHelper::importPlugin($this->events_map['save']); + + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + + // Load the row if saving an existing item. + if ($langId > 0) { + $table->load($langId); + $isNew = false; + } + + // Prevent white spaces, including East Asian double bytes. + $spaces = array('/\xE3\x80\x80/', ' '); + + $data['lang_code'] = str_replace($spaces, '', $data['lang_code']); + + // Prevent saving an incorrect language tag + if (!preg_match('#\b([a-z]{2,3})[-]([A-Z]{2})\b#', $data['lang_code'])) { + $this->setError(Text::_('COM_LANGUAGES_ERROR_LANG_TAG')); + + return false; + } + + $data['sef'] = str_replace($spaces, '', $data['sef']); + $data['sef'] = ApplicationHelper::stringURLSafe($data['sef']); + + // Prevent saving an empty url language code + if ($data['sef'] === '') { + $this->setError(Text::_('COM_LANGUAGES_ERROR_SEF')); + + return false; + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew)); + + // Check the event responses. + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew)); + + $this->setState('language.id', $table->lang_id); + + // Clean the cache. + $this->cleanCache(); + + return true; + } + + /** + * Custom clean cache method. + * + * @param string $group Optional cache group name. + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('_system'); + parent::cleanCache('com_languages'); + } } diff --git a/administrator/components/com_languages/src/Model/LanguagesModel.php b/administrator/components/com_languages/src/Model/LanguagesModel.php index d6b415f5ab0ae..c6eda83d990fc 100644 --- a/administrator/components/com_languages/src/Model/LanguagesModel.php +++ b/administrator/components/com_languages/src/Model/LanguagesModel.php @@ -1,4 +1,5 @@ setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.published'); - - return parent::getStoreId($id); - } - - /** - * Method to build an SQL query to load the list data. - * - * @return string An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select all fields from the languages table. - $query->select( - $this->getState('list.select', - [ - $db->quoteName('a') . '.*', - ] - ) - ) - ->select( - [ - $db->quoteName('l.home'), - $db->quoteName('ag.title', 'access_level'), - ] - ) - ->from($db->quoteName('#__languages', 'a')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) - ->join( - 'LEFT', - $db->quoteName('#__menu', 'l'), - $db->quoteName('l.language') . ' = ' . $db->quoteName('a.lang_code') - . ' AND ' . $db->quoteName('l.home') . ' = 1 AND ' . $db->quoteName('l.language') . ' <> ' . $db->quote('*') - ); - - // Filter on the published state. - $published = (string) $this->getState('filter.published'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where($db->quoteName('a.published') . ' IN (0, 1)'); - } - - // Filter by search in title. - if ($search = $this->getState('filter.search')) - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where($db->quoteName('a.title') . ' LIKE :search') - ->bind(':search', $search); - } - - // Filter by access level. - if ($access = (int) $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $query->order($db->quoteName($db->escape($this->getState('list.ordering', 'a.ordering'))) - . ' ' . $db->escape($this->getState('list.direction', 'ASC')) - ); - - return $query; - } - - /** - * Set the published language(s). - * - * @param array $cid An array of language IDs. - * @param integer $value The value of the published state. - * - * @return boolean True on success, false otherwise. - * - * @since 1.6 - */ - public function setPublished($cid, $value = 0) - { - return Table::getInstance('Language', 'Joomla\\CMS\\Table\\')->publish($cid, $value); - } - - /** - * Method to delete records. - * - * @param array $pks An array of item primary keys. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - */ - public function delete($pks) - { - // Sanitize the array. - $pks = (array) $pks; - - // Get a row instance. - $table = Table::getInstance('Language', 'Joomla\\CMS\\Table\\'); - - // Iterate the items to delete each one. - foreach ($pks as $itemId) - { - if (!$table->delete((int) $itemId)) - { - $this->setError($table->getError()); - - return false; - } - } - - // Clean the cache. - $this->cleanCache(); - - return true; - } - - /** - * Custom clean cache method, 2 places for 2 clients. - * - * @param string $group Optional cache group name. - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('_system'); - parent::cleanCache('com_languages'); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'lang_id', 'a.lang_id', + 'lang_code', 'a.lang_code', + 'title', 'a.title', + 'title_native', 'a.title_native', + 'sef', 'a.sef', + 'image', 'a.image', + 'published', 'a.published', + 'ordering', 'a.ordering', + 'access', 'a.access', 'access_level', + 'home', 'l.home', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.ordering', $direction = 'asc') + { + // Load the parameters. + $params = ComponentHelper::getParams('com_languages'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.published'); + + return parent::getStoreId($id); + } + + /** + * Method to build an SQL query to load the list data. + * + * @return string An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select all fields from the languages table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a') . '.*', + ] + ) + ) + ->select( + [ + $db->quoteName('l.home'), + $db->quoteName('ag.title', 'access_level'), + ] + ) + ->from($db->quoteName('#__languages', 'a')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) + ->join( + 'LEFT', + $db->quoteName('#__menu', 'l'), + $db->quoteName('l.language') . ' = ' . $db->quoteName('a.lang_code') + . ' AND ' . $db->quoteName('l.home') . ' = 1 AND ' . $db->quoteName('l.language') . ' <> ' . $db->quote('*') + ); + + // Filter on the published state. + $published = (string) $this->getState('filter.published'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where($db->quoteName('a.published') . ' IN (0, 1)'); + } + + // Filter by search in title. + if ($search = $this->getState('filter.search')) { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where($db->quoteName('a.title') . ' LIKE :search') + ->bind(':search', $search); + } + + // Filter by access level. + if ($access = (int) $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $query->order($db->quoteName($db->escape($this->getState('list.ordering', 'a.ordering'))) + . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Set the published language(s). + * + * @param array $cid An array of language IDs. + * @param integer $value The value of the published state. + * + * @return boolean True on success, false otherwise. + * + * @since 1.6 + */ + public function setPublished($cid, $value = 0) + { + return Table::getInstance('Language', 'Joomla\\CMS\\Table\\')->publish($cid, $value); + } + + /** + * Method to delete records. + * + * @param array $pks An array of item primary keys. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + */ + public function delete($pks) + { + // Sanitize the array. + $pks = (array) $pks; + + // Get a row instance. + $table = Table::getInstance('Language', 'Joomla\\CMS\\Table\\'); + + // Iterate the items to delete each one. + foreach ($pks as $itemId) { + if (!$table->delete((int) $itemId)) { + $this->setError($table->getError()); + + return false; + } + } + + // Clean the cache. + $this->cleanCache(); + + return true; + } + + /** + * Custom clean cache method, 2 places for 2 clients. + * + * @param string $group Optional cache group name. + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('_system'); + parent::cleanCache('com_languages'); + } } diff --git a/administrator/components/com_languages/src/Model/OverrideModel.php b/administrator/components/com_languages/src/Model/OverrideModel.php index 7fc3ab3427f1e..695d5f1354e14 100644 --- a/administrator/components/com_languages/src/Model/OverrideModel.php +++ b/administrator/components/com_languages/src/Model/OverrideModel.php @@ -1,4 +1,5 @@ loadForm('com_languages.override', 'override', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - $client = $this->getState('filter.client', 'site'); - $language = $this->getState('filter.language', 'en-GB'); - $langName = Language::getInstance($language)->getName(); - - if (!$langName) - { - // If a language only exists in frontend, its metadata cannot be - // loaded in backend at the moment, so fall back to the language tag. - $langName = $language; - } - - $form->setValue('client', null, Text::_('COM_LANGUAGES_VIEW_OVERRIDE_CLIENT_' . strtoupper($client))); - $form->setValue('language', null, Text::sprintf('COM_LANGUAGES_VIEW_OVERRIDE_LANGUAGE', $langName, $language)); - $form->setValue('file', null, Path::clean(constant('JPATH_' . strtoupper($client)) . '/language/overrides/' . $language . '.override.ini')); - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 2.5 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_languages.edit.override.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_languages.override', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param string $pk The key name. - * - * @return mixed Object on success, false otherwise. - * - * @since 2.5 - */ - public function getItem($pk = null) - { - $input = Factory::getApplication()->input; - $pk = !empty($pk) ? $pk : $input->get('id'); - $fileName = constant('JPATH_' . strtoupper($this->getState('filter.client'))) - . '/language/overrides/' . $this->getState('filter.language', 'en-GB') . '.override.ini'; - $strings = LanguageHelper::parseIniFile($fileName); - - $result = new \stdClass; - $result->key = ''; - $result->override = ''; - - if (isset($strings[$pk])) - { - $result->key = $pk; - $result->override = $strings[$pk]; - } - - $oppositeFileName = constant('JPATH_' . strtoupper($this->getState('filter.client') == 'site' ? 'administrator' : 'site')) - . '/language/overrides/' . $this->getState('filter.language', 'en-GB') . '.override.ini'; - $oppositeStrings = LanguageHelper::parseIniFile($oppositeFileName); - $result->both = isset($oppositeStrings[$pk]) && ($oppositeStrings[$pk] == $strings[$pk]); - - return $result; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * @param boolean $oppositeClient Indicates whether the override should not be created for the current client. - * - * @return boolean True on success, false otherwise. - * - * @since 2.5 - */ - public function save($data, $oppositeClient = false) - { - $app = Factory::getApplication(); - - if ($app->isClient('api')) - { - $client = $this->getState('filter.client'); - $language = $this->getState('filter.language'); - } - else - { - $client = $app->getUserState('com_languages.overrides.filter.client', 0); - $language = $app->getUserState('com_languages.overrides.filter.language', 'en-GB'); - } - - // If the override should be created for both. - if ($oppositeClient) - { - $client = 1 - $client; - } - - // Return false if the constant is a reserved word, i.e. YES, NO, NULL, FALSE, ON, OFF, NONE, TRUE - $reservedWords = array('YES', 'NO', 'NULL', 'FALSE', 'ON', 'OFF', 'NONE', 'TRUE'); - - if (in_array($data['key'], $reservedWords)) - { - $this->setError(Text::_('COM_LANGUAGES_OVERRIDE_ERROR_RESERVED_WORDS')); - - return false; - } - - $client = $client ? 'administrator' : 'site'; - - // Parse the override.ini file in order to get the keys and strings. - $fileName = constant('JPATH_' . strtoupper($client)) . '/language/overrides/' . $language . '.override.ini'; - $strings = LanguageHelper::parseIniFile($fileName); - - if (isset($strings[$data['id']])) - { - // If an existent string was edited check whether - // the name of the constant is still the same. - if ($data['key'] == $data['id']) - { - // If yes, simply override it. - $strings[$data['key']] = $data['override']; - } - else - { - // If no, delete the old string and prepend the new one. - unset($strings[$data['id']]); - $strings = array($data['key'] => $data['override']) + $strings; - } - } - else - { - // If it is a new override simply prepend it. - $strings = array($data['key'] => $data['override']) + $strings; - } - - // Write override.ini file with the strings. - if (LanguageHelper::saveToIniFile($fileName, $strings) === false) - { - return false; - } - - // If the override should be stored for both clients save - // it also for the other one and prevent endless recursion. - if (isset($data['both']) && $data['both'] && !$oppositeClient) - { - return $this->save($data, true); - } - - return true; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 2.5 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - if ($app->isClient('api')) - { - return; - } - - $client = $app->getUserStateFromRequest('com_languages.overrides.filter.client', 'filter_client', 0, 'int') ? 'administrator' : 'site'; - $this->setState('filter.client', $client); - - $language = $app->getUserStateFromRequest('com_languages.overrides.filter.language', 'filter_language', 'en-GB', 'cmd'); - $this->setState('filter.language', $language); - } + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|bool A Form object on success, false on failure. + * + * @since 2.5 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_languages.override', 'override', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + $client = $this->getState('filter.client', 'site'); + $language = $this->getState('filter.language', 'en-GB'); + $langName = Language::getInstance($language)->getName(); + + if (!$langName) { + // If a language only exists in frontend, its metadata cannot be + // loaded in backend at the moment, so fall back to the language tag. + $langName = $language; + } + + $form->setValue('client', null, Text::_('COM_LANGUAGES_VIEW_OVERRIDE_CLIENT_' . strtoupper($client))); + $form->setValue('language', null, Text::sprintf('COM_LANGUAGES_VIEW_OVERRIDE_LANGUAGE', $langName, $language)); + $form->setValue('file', null, Path::clean(constant('JPATH_' . strtoupper($client)) . '/language/overrides/' . $language . '.override.ini')); + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 2.5 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_languages.edit.override.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_languages.override', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param string $pk The key name. + * + * @return mixed Object on success, false otherwise. + * + * @since 2.5 + */ + public function getItem($pk = null) + { + $input = Factory::getApplication()->input; + $pk = !empty($pk) ? $pk : $input->get('id'); + $fileName = constant('JPATH_' . strtoupper($this->getState('filter.client'))) + . '/language/overrides/' . $this->getState('filter.language', 'en-GB') . '.override.ini'; + $strings = LanguageHelper::parseIniFile($fileName); + + $result = new \stdClass(); + $result->key = ''; + $result->override = ''; + + if (isset($strings[$pk])) { + $result->key = $pk; + $result->override = $strings[$pk]; + } + + $oppositeFileName = constant('JPATH_' . strtoupper($this->getState('filter.client') == 'site' ? 'administrator' : 'site')) + . '/language/overrides/' . $this->getState('filter.language', 'en-GB') . '.override.ini'; + $oppositeStrings = LanguageHelper::parseIniFile($oppositeFileName); + $result->both = isset($oppositeStrings[$pk]) && ($oppositeStrings[$pk] == $strings[$pk]); + + return $result; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * @param boolean $oppositeClient Indicates whether the override should not be created for the current client. + * + * @return boolean True on success, false otherwise. + * + * @since 2.5 + */ + public function save($data, $oppositeClient = false) + { + $app = Factory::getApplication(); + + if ($app->isClient('api')) { + $client = $this->getState('filter.client'); + $language = $this->getState('filter.language'); + } else { + $client = $app->getUserState('com_languages.overrides.filter.client', 0); + $language = $app->getUserState('com_languages.overrides.filter.language', 'en-GB'); + } + + // If the override should be created for both. + if ($oppositeClient) { + $client = 1 - $client; + } + + // Return false if the constant is a reserved word, i.e. YES, NO, NULL, FALSE, ON, OFF, NONE, TRUE + $reservedWords = array('YES', 'NO', 'NULL', 'FALSE', 'ON', 'OFF', 'NONE', 'TRUE'); + + if (in_array($data['key'], $reservedWords)) { + $this->setError(Text::_('COM_LANGUAGES_OVERRIDE_ERROR_RESERVED_WORDS')); + + return false; + } + + $client = $client ? 'administrator' : 'site'; + + // Parse the override.ini file in order to get the keys and strings. + $fileName = constant('JPATH_' . strtoupper($client)) . '/language/overrides/' . $language . '.override.ini'; + $strings = LanguageHelper::parseIniFile($fileName); + + if (isset($strings[$data['id']])) { + // If an existent string was edited check whether + // the name of the constant is still the same. + if ($data['key'] == $data['id']) { + // If yes, simply override it. + $strings[$data['key']] = $data['override']; + } else { + // If no, delete the old string and prepend the new one. + unset($strings[$data['id']]); + $strings = array($data['key'] => $data['override']) + $strings; + } + } else { + // If it is a new override simply prepend it. + $strings = array($data['key'] => $data['override']) + $strings; + } + + // Write override.ini file with the strings. + if (LanguageHelper::saveToIniFile($fileName, $strings) === false) { + return false; + } + + // If the override should be stored for both clients save + // it also for the other one and prevent endless recursion. + if (isset($data['both']) && $data['both'] && !$oppositeClient) { + return $this->save($data, true); + } + + return true; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 2.5 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + if ($app->isClient('api')) { + return; + } + + $client = $app->getUserStateFromRequest('com_languages.overrides.filter.client', 'filter_client', 0, 'int') ? 'administrator' : 'site'; + $this->setState('filter.client', $client); + + $language = $app->getUserStateFromRequest('com_languages.overrides.filter.language', 'filter_language', 'en-GB', 'cmd'); + $this->setState('filter.language', $language); + } } diff --git a/administrator/components/com_languages/src/Model/OverridesModel.php b/administrator/components/com_languages/src/Model/OverridesModel.php index fbb95e7d7a055..68aed2f7fa0f7 100644 --- a/administrator/components/com_languages/src/Model/OverridesModel.php +++ b/administrator/components/com_languages/src/Model/OverridesModel.php @@ -1,4 +1,5 @@ getStoreId(); - - // Try to load the data from internal storage. - if (!empty($this->cache[$store])) - { - return $this->cache[$store]; - } - - $client = strtoupper($this->getState('filter.client')); - - // Parse the override.ini file in order to get the keys and strings. - $fileName = constant('JPATH_' . $client) . '/language/overrides/' . $this->getState('filter.language') . '.override.ini'; - $strings = LanguageHelper::parseIniFile($fileName); - - // Delete the override.ini file if empty. - if (file_exists($fileName) && $strings === array()) - { - File::delete($fileName); - } - - // Filter the loaded strings according to the search box. - $search = $this->getState('filter.search'); - - if ($search != '') - { - $search = preg_quote($search, '~'); - $matchvals = preg_grep('~' . $search . '~i', $strings); - $matchkeys = array_intersect_key($strings, array_flip(preg_grep('~' . $search . '~i', array_keys($strings)))); - $strings = array_merge($matchvals, $matchkeys); - } - - // Consider the ordering - if ($this->getState('list.ordering') == 'text') - { - if (strtoupper($this->getState('list.direction')) == 'DESC') - { - arsort($strings); - } - else - { - asort($strings); - } - } - else - { - if (strtoupper($this->getState('list.direction')) == 'DESC') - { - krsort($strings); - } - else - { - ksort($strings); - } - } - - // Consider the pagination. - if (!$all && $this->getState('list.limit') && $this->getTotal() > $this->getState('list.limit')) - { - $strings = array_slice($strings, $this->getStart(), $this->getState('list.limit'), true); - } - - // Add the items to the internal cache. - $this->cache[$store] = $strings; - - return $this->cache[$store]; - } - - /** - * Method to get the total number of overrides. - * - * @return integer The total number of overrides. - * - * @since 2.5 - */ - public function getTotal() - { - // Get a storage key. - $store = $this->getStoreId('getTotal'); - - // Try to load the data from internal storage - if (!empty($this->cache[$store])) - { - return $this->cache[$store]; - } - - // Add the total to the internal cache. - $this->cache[$store] = count($this->getOverrides(true)); - - return $this->cache[$store]; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = 'key', $direction = 'asc') - { - // We call populate state first so that we can then set the filter.client and filter.language properties in afterwards - parent::populateState($ordering, $direction); - - $app = Factory::getApplication(); - - if ($app->isClient('api')) - { - return; - } - - $language_client = $this->getUserStateFromRequest('com_languages.overrides.language_client', 'language_client', '', 'cmd'); - $client = substr($language_client, -1); - $language = substr($language_client, 0, -1); - - // Sets the search filter. - $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); - $this->setState('filter.search', $search); - - $this->setState('language_client', $language . $client); - $this->setState('filter.client', $client ? 'administrator' : 'site'); - $this->setState('filter.language', $language); - - // Add the 'language_client' value to the session to display a message if none selected - $app->setUserState('com_languages.overrides.language_client', $language . $client); - - // Add filters to the session because they won't be stored there by 'getUserStateFromRequest' if they aren't in the current request. - $app->setUserState('com_languages.overrides.filter.client', $client); - $app->setUserState('com_languages.overrides.filter.language', $language); - } - - /** - * Method to delete one or more overrides. - * - * @param array $cids Array of keys to delete. - * - * @return integer Number of successfully deleted overrides, boolean false if an error occurred. - * - * @since 2.5 - */ - public function delete($cids) - { - // Check permissions first. - if (!Factory::getUser()->authorise('core.delete', 'com_languages')) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); - - return false; - } - - $app = Factory::getApplication(); - - if ($app->isClient('api')) - { - $cids = (array) $cids; - $client = $this->getState('filter.client'); - } - else - { - $filterclient = Factory::getApplication()->getUserState('com_languages.overrides.filter.client'); - $client = $filterclient == 0 ? 'site' : 'administrator'; - } - - // Parse the override.ini file in order to get the keys and strings. - $fileName = constant('JPATH_' . strtoupper($client)) . '/language/overrides/' . $this->getState('filter.language') . '.override.ini'; - $strings = LanguageHelper::parseIniFile($fileName); - - // Unset strings that shall be deleted - foreach ($cids as $key) - { - if (isset($strings[$key])) - { - unset($strings[$key]); - } - } - - // Write override.ini file with the strings. - if (LanguageHelper::saveToIniFile($fileName, $strings) === false) - { - return false; - } - - $this->cleanCache(); - - return count($cids); - } - - /** - * Removes all of the cached strings from the table. - * - * @return boolean result of operation - * - * @since 3.4.2 - */ - public function purge() - { - $db = $this->getDatabase(); - - // Note: TRUNCATE is a DDL operation - // This may or may not mean depending on your database - try - { - $db->truncateTable('#__overrider'); - } - catch (\RuntimeException $e) - { - return $e; - } - - Factory::getApplication()->enqueueMessage(Text::_('COM_LANGUAGES_VIEW_OVERRIDES_PURGE_SUCCESS')); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 2.5 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'key', + 'text', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Retrieves the overrides data + * + * @param boolean $all True if all overrides shall be returned without considering pagination, defaults to false + * + * @return array Array of objects containing the overrides of the override.ini file + * + * @since 2.5 + */ + public function getOverrides($all = false) + { + // Get a storage key. + $store = $this->getStoreId(); + + // Try to load the data from internal storage. + if (!empty($this->cache[$store])) { + return $this->cache[$store]; + } + + $client = strtoupper($this->getState('filter.client')); + + // Parse the override.ini file in order to get the keys and strings. + $fileName = constant('JPATH_' . $client) . '/language/overrides/' . $this->getState('filter.language') . '.override.ini'; + $strings = LanguageHelper::parseIniFile($fileName); + + // Delete the override.ini file if empty. + if (file_exists($fileName) && $strings === array()) { + File::delete($fileName); + } + + // Filter the loaded strings according to the search box. + $search = $this->getState('filter.search'); + + if ($search != '') { + $search = preg_quote($search, '~'); + $matchvals = preg_grep('~' . $search . '~i', $strings); + $matchkeys = array_intersect_key($strings, array_flip(preg_grep('~' . $search . '~i', array_keys($strings)))); + $strings = array_merge($matchvals, $matchkeys); + } + + // Consider the ordering + if ($this->getState('list.ordering') == 'text') { + if (strtoupper($this->getState('list.direction')) == 'DESC') { + arsort($strings); + } else { + asort($strings); + } + } else { + if (strtoupper($this->getState('list.direction')) == 'DESC') { + krsort($strings); + } else { + ksort($strings); + } + } + + // Consider the pagination. + if (!$all && $this->getState('list.limit') && $this->getTotal() > $this->getState('list.limit')) { + $strings = array_slice($strings, $this->getStart(), $this->getState('list.limit'), true); + } + + // Add the items to the internal cache. + $this->cache[$store] = $strings; + + return $this->cache[$store]; + } + + /** + * Method to get the total number of overrides. + * + * @return integer The total number of overrides. + * + * @since 2.5 + */ + public function getTotal() + { + // Get a storage key. + $store = $this->getStoreId('getTotal'); + + // Try to load the data from internal storage + if (!empty($this->cache[$store])) { + return $this->cache[$store]; + } + + // Add the total to the internal cache. + $this->cache[$store] = count($this->getOverrides(true)); + + return $this->cache[$store]; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = 'key', $direction = 'asc') + { + // We call populate state first so that we can then set the filter.client and filter.language properties in afterwards + parent::populateState($ordering, $direction); + + $app = Factory::getApplication(); + + if ($app->isClient('api')) { + return; + } + + $language_client = $this->getUserStateFromRequest('com_languages.overrides.language_client', 'language_client', '', 'cmd'); + $client = substr($language_client, -1); + $language = substr($language_client, 0, -1); + + // Sets the search filter. + $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); + $this->setState('filter.search', $search); + + $this->setState('language_client', $language . $client); + $this->setState('filter.client', $client ? 'administrator' : 'site'); + $this->setState('filter.language', $language); + + // Add the 'language_client' value to the session to display a message if none selected + $app->setUserState('com_languages.overrides.language_client', $language . $client); + + // Add filters to the session because they won't be stored there by 'getUserStateFromRequest' if they aren't in the current request. + $app->setUserState('com_languages.overrides.filter.client', $client); + $app->setUserState('com_languages.overrides.filter.language', $language); + } + + /** + * Method to delete one or more overrides. + * + * @param array $cids Array of keys to delete. + * + * @return integer Number of successfully deleted overrides, boolean false if an error occurred. + * + * @since 2.5 + */ + public function delete($cids) + { + // Check permissions first. + if (!Factory::getUser()->authorise('core.delete', 'com_languages')) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); + + return false; + } + + $app = Factory::getApplication(); + + if ($app->isClient('api')) { + $cids = (array) $cids; + $client = $this->getState('filter.client'); + } else { + $filterclient = Factory::getApplication()->getUserState('com_languages.overrides.filter.client'); + $client = $filterclient == 0 ? 'site' : 'administrator'; + } + + // Parse the override.ini file in order to get the keys and strings. + $fileName = constant('JPATH_' . strtoupper($client)) . '/language/overrides/' . $this->getState('filter.language') . '.override.ini'; + $strings = LanguageHelper::parseIniFile($fileName); + + // Unset strings that shall be deleted + foreach ($cids as $key) { + if (isset($strings[$key])) { + unset($strings[$key]); + } + } + + // Write override.ini file with the strings. + if (LanguageHelper::saveToIniFile($fileName, $strings) === false) { + return false; + } + + $this->cleanCache(); + + return count($cids); + } + + /** + * Removes all of the cached strings from the table. + * + * @return boolean result of operation + * + * @since 3.4.2 + */ + public function purge() + { + $db = $this->getDatabase(); + + // Note: TRUNCATE is a DDL operation + // This may or may not mean depending on your database + try { + $db->truncateTable('#__overrider'); + } catch (\RuntimeException $e) { + return $e; + } + + Factory::getApplication()->enqueueMessage(Text::_('COM_LANGUAGES_VIEW_OVERRIDES_PURGE_SUCCESS')); + } } diff --git a/administrator/components/com_languages/src/Model/StringsModel.php b/administrator/components/com_languages/src/Model/StringsModel.php index fe1e991ddc82c..e02802830da3c 100644 --- a/administrator/components/com_languages/src/Model/StringsModel.php +++ b/administrator/components/com_languages/src/Model/StringsModel.php @@ -1,4 +1,5 @@ getDatabase(); - - $app->setUserState('com_languages.overrides.cachedtime', null); - - // Empty the database cache first. - try - { - $db->truncateTable('#__overrider'); - } - catch (\RuntimeException $e) - { - return $e; - } - - // Create the insert query. - $query = $db->getQuery(true) - ->insert($db->quoteName('#__overrider')) - ->columns( - [ - $db->quoteName('constant'), - $db->quoteName('string'), - $db->quoteName('file'), - ] - ); - - // Initialize some variables. - $client = $app->getUserState('com_languages.overrides.filter.client', 'site') ? 'administrator' : 'site'; - $language = $app->getUserState('com_languages.overrides.filter.language', 'en-GB'); - - $base = constant('JPATH_' . strtoupper($client)); - $path = $base . '/language/' . $language; - - $files = array(); - - // Parse common language directory. - if (is_dir($path)) - { - $files = Folder::files($path, '.*ini$', false, true); - } - - // Parse language directories of components. - $files = array_merge($files, Folder::files($base . '/components', '.*ini$', 3, true)); - - // Parse language directories of modules. - $files = array_merge($files, Folder::files($base . '/modules', '.*ini$', 3, true)); - - // Parse language directories of templates. - $files = array_merge($files, Folder::files($base . '/templates', '.*ini$', 3, true)); - - // Parse language directories of plugins. - $files = array_merge($files, Folder::files(JPATH_PLUGINS, '.*ini$', 4, true)); - - // Parse all found ini files and add the strings to the database cache. - foreach ($files as $file) - { - // Only process if language file is for selected language - if (strpos($file, $language, strlen($base)) === false) - { - continue; - } - - $strings = LanguageHelper::parseIniFile($file); - - if ($strings) - { - $file = Path::clean($file); - - $query->clear('values') - ->clear('bounded'); - - foreach ($strings as $key => $string) - { - $query->values(implode(',', $query->bindArray([$key, $string, $file], ParameterType::STRING))); - } - - try - { - $db->setQuery($query); - $db->execute(); - } - catch (\RuntimeException $e) - { - return $e; - } - } - } - - // Update the cached time. - $app->setUserState('com_languages.overrides.cachedtime.' . $client . '.' . $language, time()); - - return true; - } - - /** - * Method for searching language strings. - * - * @return array|\Exception Array of results on success, \Exception object otherwise. - * - * @since 2.5 - */ - public function search() - { - $results = array(); - $input = Factory::getApplication()->input; - $filter = InputFilter::getInstance(); - $db = $this->getDatabase(); - $searchTerm = $input->getString('searchstring'); - - $limitstart = $input->getInt('more'); - - try - { - $searchstring = '%' . $filter->clean($searchTerm, 'TRIM') . '%'; - - // Create the search query. - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('constant'), - $db->quoteName('string'), - $db->quoteName('file'), - ] - ) - ->from($db->quoteName('#__overrider')); - - if ($input->get('searchtype') === 'constant') - { - $query->where($db->quoteName('constant') . ' LIKE :search'); - } - else - { - $query->where($db->quoteName('string') . ' LIKE :search'); - } - - $query->bind(':search', $searchstring); - - // Consider the limitstart according to the 'more' parameter and load the results. - $query->setLimit(10, $limitstart); - $db->setQuery($query); - $results['results'] = $db->loadObjectList(); - - // Check whether there are more results than already loaded. - $query->clear('select') - ->clear('limit') - ->select('COUNT(' . $db->quoteName('id') . ')'); - $db->setQuery($query); - - if ($db->loadResult() > $limitstart + 10) - { - // If this is set a 'More Results' link will be displayed in the view. - $results['more'] = $limitstart + 10; - } - } - catch (\RuntimeException $e) - { - return $e; - } - - return $results; - } + /** + * Method for refreshing the cache in the database with the known language strings. + * + * @return boolean|\Exception True on success, \Exception object otherwise. + * + * @since 2.5 + */ + public function refresh() + { + $app = Factory::getApplication(); + $db = $this->getDatabase(); + + $app->setUserState('com_languages.overrides.cachedtime', null); + + // Empty the database cache first. + try { + $db->truncateTable('#__overrider'); + } catch (\RuntimeException $e) { + return $e; + } + + // Create the insert query. + $query = $db->getQuery(true) + ->insert($db->quoteName('#__overrider')) + ->columns( + [ + $db->quoteName('constant'), + $db->quoteName('string'), + $db->quoteName('file'), + ] + ); + + // Initialize some variables. + $client = $app->getUserState('com_languages.overrides.filter.client', 'site') ? 'administrator' : 'site'; + $language = $app->getUserState('com_languages.overrides.filter.language', 'en-GB'); + + $base = constant('JPATH_' . strtoupper($client)); + $path = $base . '/language/' . $language; + + $files = array(); + + // Parse common language directory. + if (is_dir($path)) { + $files = Folder::files($path, '.*ini$', false, true); + } + + // Parse language directories of components. + $files = array_merge($files, Folder::files($base . '/components', '.*ini$', 3, true)); + + // Parse language directories of modules. + $files = array_merge($files, Folder::files($base . '/modules', '.*ini$', 3, true)); + + // Parse language directories of templates. + $files = array_merge($files, Folder::files($base . '/templates', '.*ini$', 3, true)); + + // Parse language directories of plugins. + $files = array_merge($files, Folder::files(JPATH_PLUGINS, '.*ini$', 4, true)); + + // Parse all found ini files and add the strings to the database cache. + foreach ($files as $file) { + // Only process if language file is for selected language + if (strpos($file, $language, strlen($base)) === false) { + continue; + } + + $strings = LanguageHelper::parseIniFile($file); + + if ($strings) { + $file = Path::clean($file); + + $query->clear('values') + ->clear('bounded'); + + foreach ($strings as $key => $string) { + $query->values(implode(',', $query->bindArray([$key, $string, $file], ParameterType::STRING))); + } + + try { + $db->setQuery($query); + $db->execute(); + } catch (\RuntimeException $e) { + return $e; + } + } + } + + // Update the cached time. + $app->setUserState('com_languages.overrides.cachedtime.' . $client . '.' . $language, time()); + + return true; + } + + /** + * Method for searching language strings. + * + * @return array|\Exception Array of results on success, \Exception object otherwise. + * + * @since 2.5 + */ + public function search() + { + $results = array(); + $input = Factory::getApplication()->input; + $filter = InputFilter::getInstance(); + $db = $this->getDatabase(); + $searchTerm = $input->getString('searchstring'); + + $limitstart = $input->getInt('more'); + + try { + $searchstring = '%' . $filter->clean($searchTerm, 'TRIM') . '%'; + + // Create the search query. + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('constant'), + $db->quoteName('string'), + $db->quoteName('file'), + ] + ) + ->from($db->quoteName('#__overrider')); + + if ($input->get('searchtype') === 'constant') { + $query->where($db->quoteName('constant') . ' LIKE :search'); + } else { + $query->where($db->quoteName('string') . ' LIKE :search'); + } + + $query->bind(':search', $searchstring); + + // Consider the limitstart according to the 'more' parameter and load the results. + $query->setLimit(10, $limitstart); + $db->setQuery($query); + $results['results'] = $db->loadObjectList(); + + // Check whether there are more results than already loaded. + $query->clear('select') + ->clear('limit') + ->select('COUNT(' . $db->quoteName('id') . ')'); + $db->setQuery($query); + + if ($db->loadResult() > $limitstart + 10) { + // If this is set a 'More Results' link will be displayed in the view. + $results['more'] = $limitstart + 10; + } + } catch (\RuntimeException $e) { + return $e; + } + + return $results; + } } diff --git a/administrator/components/com_languages/src/Service/HTML/Languages.php b/administrator/components/com_languages/src/Service/HTML/Languages.php index b4b4e423ebda1..5b7e2d84c8234 100644 --- a/administrator/components/com_languages/src/Service/HTML/Languages.php +++ b/administrator/components/com_languages/src/Service/HTML/Languages.php @@ -1,4 +1,5 @@ '; - } + /** + * Method to generate an input radio button. + * + * @param integer $rowNum The row number. + * @param string $language Language tag. + * + * @return string HTML code. + */ + public function id($rowNum, $language) + { + return ''; + } - /** - * Method to generate an array of clients. - * - * @return array of client objects. - */ - public function clients() - { - return array( - HTMLHelper::_('select.option', 0, Text::_('JSITE')), - HTMLHelper::_('select.option', 1, Text::_('JADMINISTRATOR')) - ); - } + /** + * Method to generate an array of clients. + * + * @return array of client objects. + */ + public function clients() + { + return array( + HTMLHelper::_('select.option', 0, Text::_('JSITE')), + HTMLHelper::_('select.option', 1, Text::_('JADMINISTRATOR')) + ); + } - /** - * Returns an array of published state filter options. - * - * @return string The HTML code for the select tag. - * - * @since 1.6 - */ - public function publishedOptions() - { - // Build the active state filter options. - $options = array(); - $options[] = HTMLHelper::_('select.option', '1', 'JPUBLISHED'); - $options[] = HTMLHelper::_('select.option', '0', 'JUNPUBLISHED'); - $options[] = HTMLHelper::_('select.option', '-2', 'JTRASHED'); - $options[] = HTMLHelper::_('select.option', '*', 'JALL'); + /** + * Returns an array of published state filter options. + * + * @return string The HTML code for the select tag. + * + * @since 1.6 + */ + public function publishedOptions() + { + // Build the active state filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', 'JPUBLISHED'); + $options[] = HTMLHelper::_('select.option', '0', 'JUNPUBLISHED'); + $options[] = HTMLHelper::_('select.option', '-2', 'JTRASHED'); + $options[] = HTMLHelper::_('select.option', '*', 'JALL'); - return $options; - } + return $options; + } } diff --git a/administrator/components/com_languages/src/View/Installed/HtmlView.php b/administrator/components/com_languages/src/View/Installed/HtmlView.php index dc657fed998e5..bf3e3f84eed8d 100644 --- a/administrator/components/com_languages/src/View/Installed/HtmlView.php +++ b/administrator/components/com_languages/src/View/Installed/HtmlView.php @@ -1,4 +1,5 @@ option = $this->get('Option'); - $this->pagination = $this->get('Pagination'); - $this->rows = $this->get('Data'); - $this->total = $this->get('Total'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_languages'); - - if ((int) $this->state->get('client_id') === 1) - { - ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_INSTALLED_ADMIN_TITLE'), 'comments langmanager'); - } - else - { - ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_INSTALLED_SITE_TITLE'), 'comments langmanager'); - } - - if ($canDo->get('core.edit.state')) - { - ToolbarHelper::makeDefault('installed.setDefault'); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.admin')) - { - // Add install languages link to the lang installer component. - $bar = Toolbar::getInstance('toolbar'); - - // Switch administrator language - if ($this->state->get('client_id', 0) == 1) - { - ToolbarHelper::custom('installed.switchadminlanguage', 'refresh', '', 'COM_LANGUAGES_SWITCH_ADMIN', true); - ToolbarHelper::divider(); - } - - $bar->appendButton('Link', 'upload', 'COM_LANGUAGES_INSTALL', 'index.php?option=com_installer&view=languages'); - ToolbarHelper::divider(); - - ToolbarHelper::preferences('com_languages'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Languages:_Installed'); - } + /** + * Option (component) name + * + * @var string + */ + protected $option = null; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * Languages information + * + * @var array + */ + protected $rows = null; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 4.0.0 + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse. + * + * @return void + */ + public function display($tpl = null) + { + $this->option = $this->get('Option'); + $this->pagination = $this->get('Pagination'); + $this->rows = $this->get('Data'); + $this->total = $this->get('Total'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_languages'); + + if ((int) $this->state->get('client_id') === 1) { + ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_INSTALLED_ADMIN_TITLE'), 'comments langmanager'); + } else { + ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_INSTALLED_SITE_TITLE'), 'comments langmanager'); + } + + if ($canDo->get('core.edit.state')) { + ToolbarHelper::makeDefault('installed.setDefault'); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.admin')) { + // Add install languages link to the lang installer component. + $bar = Toolbar::getInstance('toolbar'); + + // Switch administrator language + if ($this->state->get('client_id', 0) == 1) { + ToolbarHelper::custom('installed.switchadminlanguage', 'refresh', '', 'COM_LANGUAGES_SWITCH_ADMIN', true); + ToolbarHelper::divider(); + } + + $bar->appendButton('Link', 'upload', 'COM_LANGUAGES_INSTALL', 'index.php?option=com_installer&view=languages'); + ToolbarHelper::divider(); + + ToolbarHelper::preferences('com_languages'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Languages:_Installed'); + } } diff --git a/administrator/components/com_languages/src/View/Language/HtmlView.php b/administrator/components/com_languages/src/View/Language/HtmlView.php index 60abd3670b7e7..d33471bd1b4a6 100644 --- a/administrator/components/com_languages/src/View/Language/HtmlView.php +++ b/administrator/components/com_languages/src/View/Language/HtmlView.php @@ -1,4 +1,5 @@ item = $this->get('Item'); - $this->form = $this->get('Form'); - $this->state = $this->get('State'); - $this->canDo = ContentHelper::getActions('com_languages'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', 1); - $isNew = empty($this->item->lang_id); - $canDo = $this->canDo; - - ToolbarHelper::title( - Text::_($isNew ? 'COM_LANGUAGES_VIEW_LANGUAGE_EDIT_NEW_TITLE' : 'COM_LANGUAGES_VIEW_LANGUAGE_EDIT_EDIT_TITLE'), 'comments-2 langmanager' - ); - - $toolbarButtons = []; - - if (($isNew && $canDo->get('core.create')) || (!$isNew && $canDo->get('core.edit'))) - { - ToolbarHelper::apply('language.apply'); - - $toolbarButtons[] = ['save', 'language.save']; - } - - // If an existing item, can save to a copy only if we have create rights. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'language.save2new']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if ($isNew) - { - ToolbarHelper::cancel('language.cancel'); - } - else - { - ToolbarHelper::cancel('language.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Languages:_Edit_Content_Language'); - } + /** + * The active item + * + * @var object + */ + public $item; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + public $form; + + /** + * The model state + * + * @var CMSObject + */ + public $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * + * @since 4.0.0 + */ + protected $canDo; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse. + * + * @return void + */ + public function display($tpl = null) + { + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->canDo = ContentHelper::getActions('com_languages'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', 1); + $isNew = empty($this->item->lang_id); + $canDo = $this->canDo; + + ToolbarHelper::title( + Text::_($isNew ? 'COM_LANGUAGES_VIEW_LANGUAGE_EDIT_NEW_TITLE' : 'COM_LANGUAGES_VIEW_LANGUAGE_EDIT_EDIT_TITLE'), + 'comments-2 langmanager' + ); + + $toolbarButtons = []; + + if (($isNew && $canDo->get('core.create')) || (!$isNew && $canDo->get('core.edit'))) { + ToolbarHelper::apply('language.apply'); + + $toolbarButtons[] = ['save', 'language.save']; + } + + // If an existing item, can save to a copy only if we have create rights. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'language.save2new']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if ($isNew) { + ToolbarHelper::cancel('language.cancel'); + } else { + ToolbarHelper::cancel('language.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Languages:_Edit_Content_Language'); + } } diff --git a/administrator/components/com_languages/src/View/Languages/HtmlView.php b/administrator/components/com_languages/src/View/Languages/HtmlView.php index 8233611961c92..b90673a9c887b 100644 --- a/administrator/components/com_languages/src/View/Languages/HtmlView.php +++ b/administrator/components/com_languages/src/View/Languages/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_languages'); - - ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_LANGUAGES_TITLE'), 'comments langmanager'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('language.add'); - } - - if ($canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('languages.publish')->listCheck(true); - $childBar->unpublish('languages.unpublish')->listCheck(true); - - if ($this->state->get('filter.published') != -2) - { - $childBar->trash('languages.trash')->listCheck(true); - } - } - - if ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('languages.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin')) - { - // Add install languages link to the lang installer component. - $bar = Toolbar::getInstance('toolbar'); - $bar->appendButton('Link', 'upload', 'COM_LANGUAGES_INSTALL', 'index.php?option=com_installer&view=languages'); - ToolbarHelper::divider(); - - ToolbarHelper::preferences('com_languages'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Languages:_Content'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 4.0.0 + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_languages'); + + ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_LANGUAGES_TITLE'), 'comments langmanager'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('language.add'); + } + + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('languages.publish')->listCheck(true); + $childBar->unpublish('languages.unpublish')->listCheck(true); + + if ($this->state->get('filter.published') != -2) { + $childBar->trash('languages.trash')->listCheck(true); + } + } + + if ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('languages.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin')) { + // Add install languages link to the lang installer component. + $bar = Toolbar::getInstance('toolbar'); + $bar->appendButton('Link', 'upload', 'COM_LANGUAGES_INSTALL', 'index.php?option=com_installer&view=languages'); + ToolbarHelper::divider(); + + ToolbarHelper::preferences('com_languages'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Languages:_Content'); + } } diff --git a/administrator/components/com_languages/src/View/Multilangstatus/HtmlView.php b/administrator/components/com_languages/src/View/Multilangstatus/HtmlView.php index d083e24f92d6f..73a9a09e96763 100644 --- a/administrator/components/com_languages/src/View/Multilangstatus/HtmlView.php +++ b/administrator/components/com_languages/src/View/Multilangstatus/HtmlView.php @@ -1,4 +1,5 @@ homes = MultilangstatusHelper::getHomes(); - $this->language_filter = Multilanguage::isEnabled(); - $this->switchers = MultilangstatusHelper::getLangswitchers(); - $this->listUsersError = MultilangstatusHelper::getContacts(); - $this->contentlangs = MultilangstatusHelper::getContentlangs(); - $this->site_langs = LanguageHelper::getInstalledLanguages(0); - $this->statuses = MultilangstatusHelper::getStatus(); - $this->homepages = Multilanguage::getSiteHomePages(); - $this->defaultHome = MultilangstatusHelper::getDefaultHomeModule(); - $this->default_lang = ComponentHelper::getParams('com_languages')->get('site', 'en-GB'); + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse. + * + * @return void + */ + public function display($tpl = null) + { + $this->homes = MultilangstatusHelper::getHomes(); + $this->language_filter = Multilanguage::isEnabled(); + $this->switchers = MultilangstatusHelper::getLangswitchers(); + $this->listUsersError = MultilangstatusHelper::getContacts(); + $this->contentlangs = MultilangstatusHelper::getContentlangs(); + $this->site_langs = LanguageHelper::getInstalledLanguages(0); + $this->statuses = MultilangstatusHelper::getStatus(); + $this->homepages = Multilanguage::getSiteHomePages(); + $this->defaultHome = MultilangstatusHelper::getDefaultHomeModule(); + $this->default_lang = ComponentHelper::getParams('com_languages')->get('site', 'en-GB'); - parent::display($tpl); - } + parent::display($tpl); + } } diff --git a/administrator/components/com_languages/src/View/Override/HtmlView.php b/administrator/components/com_languages/src/View/Override/HtmlView.php index a89fc5b559616..c8849a4f85c2b 100644 --- a/administrator/components/com_languages/src/View/Override/HtmlView.php +++ b/administrator/components/com_languages/src/View/Override/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - $app = Factory::getApplication(); - - $languageClient = $app->getUserStateFromRequest('com_languages.overrides.language_client', 'language_client'); - - if ($languageClient == null) - { - $app->enqueueMessage(Text::_('COM_LANGUAGES_OVERRIDE_FIRST_SELECT_MESSAGE'), 'warning'); - - $app->redirect('index.php?option=com_languages&view=overrides'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors)); - } - - // Check whether the cache has to be refreshed. - $cached_time = Factory::getApplication()->getUserState( - 'com_languages.overrides.cachedtime.' . $this->state->get('filter.client') . '.' . $this->state->get('filter.language'), - 0 - ); - - if (time() - $cached_time > 60 * 5) - { - $this->state->set('cache_expired', true); - } - - // Add strings for translations in \Javascript. - Text::script('COM_LANGUAGES_VIEW_OVERRIDE_NO_RESULTS'); - Text::script('COM_LANGUAGES_VIEW_OVERRIDE_REQUEST_ERROR'); - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Adds the page title and toolbar. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $canDo = ContentHelper::getActions('com_languages'); - - ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_OVERRIDE_EDIT_TITLE'), 'comments langmanager'); - - $toolbarButtons = []; - - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('override.apply'); - - $toolbarButtons[] = ['save', 'override.save']; - } - - // This component does not support Save as Copy. - if ($canDo->get('core.edit') && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'override.save2new']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->key)) - { - ToolbarHelper::cancel('override.cancel'); - } - else - { - ToolbarHelper::cancel('override.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Languages:_Edit_Override'); - } + /** + * The form to use for the view. + * + * @var object + * @since 2.5 + */ + protected $form; + + /** + * The item to edit. + * + * @var object + * @since 2.5 + */ + protected $item; + + /** + * The model state. + * + * @var object + * @since 2.5 + */ + protected $state; + + /** + * Displays the view. + * + * @param string $tpl The name of the template file to parse + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + $app = Factory::getApplication(); + + $languageClient = $app->getUserStateFromRequest('com_languages.overrides.language_client', 'language_client'); + + if ($languageClient == null) { + $app->enqueueMessage(Text::_('COM_LANGUAGES_OVERRIDE_FIRST_SELECT_MESSAGE'), 'warning'); + + $app->redirect('index.php?option=com_languages&view=overrides'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors)); + } + + // Check whether the cache has to be refreshed. + $cached_time = Factory::getApplication()->getUserState( + 'com_languages.overrides.cachedtime.' . $this->state->get('filter.client') . '.' . $this->state->get('filter.language'), + 0 + ); + + if (time() - $cached_time > 60 * 5) { + $this->state->set('cache_expired', true); + } + + // Add strings for translations in \Javascript. + Text::script('COM_LANGUAGES_VIEW_OVERRIDE_NO_RESULTS'); + Text::script('COM_LANGUAGES_VIEW_OVERRIDE_REQUEST_ERROR'); + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Adds the page title and toolbar. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $canDo = ContentHelper::getActions('com_languages'); + + ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_OVERRIDE_EDIT_TITLE'), 'comments langmanager'); + + $toolbarButtons = []; + + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('override.apply'); + + $toolbarButtons[] = ['save', 'override.save']; + } + + // This component does not support Save as Copy. + if ($canDo->get('core.edit') && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'override.save2new']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->key)) { + ToolbarHelper::cancel('override.cancel'); + } else { + ToolbarHelper::cancel('override.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Languages:_Edit_Override'); + } } diff --git a/administrator/components/com_languages/src/View/Overrides/HtmlView.php b/administrator/components/com_languages/src/View/Overrides/HtmlView.php index 1795f4a2fcaed..9946fbbd97a8a 100644 --- a/administrator/components/com_languages/src/View/Overrides/HtmlView.php +++ b/administrator/components/com_languages/src/View/Overrides/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Overrides'); - $this->languages = $this->get('Languages'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors)); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Adds the page title and toolbar. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - // Get the results for each action - $canDo = ContentHelper::getActions('com_languages'); - - ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_OVERRIDES_TITLE'), 'comments langmanager'); - - if ($canDo->get('core.create')) - { - ToolbarHelper::addNew('override.add'); - } - - if ($canDo->get('core.delete') && $this->pagination->total) - { - ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'overrides.delete', 'JTOOLBAR_DELETE'); - } - - if ($this->getCurrentUser()->authorise('core.admin')) - { - ToolbarHelper::custom('overrides.purge', 'refresh', '', 'COM_LANGUAGES_VIEW_OVERRIDES_PURGE', false); - } - - if ($canDo->get('core.admin')) - { - ToolbarHelper::preferences('com_languages'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Languages:_Overrides'); - } + /** + * The items to list. + * + * @var array + * @since 2.5 + */ + protected $items; + + /** + * The pagination object. + * + * @var object + * @since 2.5 + */ + protected $pagination; + + /** + * The model state. + * + * @var object + * @since 2.5 + */ + protected $state; + + /** + * An array containing all frontend and backend languages + * + * @var array + * @since 4.0.0 + */ + protected $languages; + + /** + * Displays the view. + * + * @param string $tpl The name of the template file to parse. + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Overrides'); + $this->languages = $this->get('Languages'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors)); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Adds the page title and toolbar. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + // Get the results for each action + $canDo = ContentHelper::getActions('com_languages'); + + ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_OVERRIDES_TITLE'), 'comments langmanager'); + + if ($canDo->get('core.create')) { + ToolbarHelper::addNew('override.add'); + } + + if ($canDo->get('core.delete') && $this->pagination->total) { + ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'overrides.delete', 'JTOOLBAR_DELETE'); + } + + if ($this->getCurrentUser()->authorise('core.admin')) { + ToolbarHelper::custom('overrides.purge', 'refresh', '', 'COM_LANGUAGES_VIEW_OVERRIDES_PURGE', false); + } + + if ($canDo->get('core.admin')) { + ToolbarHelper::preferences('com_languages'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Languages:_Overrides'); + } } diff --git a/administrator/components/com_languages/tmpl/installed/default.php b/administrator/components/com_languages/tmpl/installed/default.php index fc58225bfd116..10eb42adb6a75 100644 --- a/administrator/components/com_languages/tmpl/installed/default.php +++ b/administrator/components/com_languages/tmpl/installed/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
    -
    -
    -
    - $this)); ?> - rows)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - - getShortVersion()); - foreach ($this->rows as $i => $row) : - $canCreate = $user->authorise('core.create', 'com_languages'); - $canEdit = $user->authorise('core.edit', 'com_languages'); - $canChange = $user->authorise('core.edit.state', 'com_languages'); - ?> - - - - - - - - - - - - - - -
    - , - , - -
    -   - - - - - - - - - - - - - - - - - - -
    - language); ?> - - - - escape($row->nativeName); ?> - - escape($row->language); ?> - - published, $i, 'installed.', !$row->published && $canChange); ?> - - - - version, $minorVersion) !== 0 || strpos($row->version, $currentShortVersion) !== 0) : ?> - version; ?> - - version; ?> - - - escape($row->creationDate); ?> - - escape($row->author); ?> - - escape($row->authorEmail)); ?> - - escape($row->extension_id); ?> -
    +
    +
    +
    + $this)); ?> + rows)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + + + getShortVersion()); + foreach ($this->rows as $i => $row) : + $canCreate = $user->authorise('core.create', 'com_languages'); + $canEdit = $user->authorise('core.edit', 'com_languages'); + $canChange = $user->authorise('core.edit.state', 'com_languages'); + ?> + + + + + + + + + + + + + + +
    + , + , + +
    +   + + + + + + + + + + + + + + + + + + +
    + language); ?> + + + + escape($row->nativeName); ?> + + escape($row->language); ?> + + published, $i, 'installed.', !$row->published && $canChange); ?> + + + + version, $minorVersion) !== 0 || strpos($row->version, $currentShortVersion) !== 0) : ?> + version; ?> + + version; ?> + + + escape($row->creationDate); ?> + + escape($row->author); ?> + + escape($row->authorEmail)); ?> + + escape($row->extension_id); ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
    -
    -
    + + + + +
    +
    +
    diff --git a/administrator/components/com_languages/tmpl/language/edit.php b/administrator/components/com_languages/tmpl/language/edit.php index 21999ae811e22..0f2cf809a22a2 100644 --- a/administrator/components/com_languages/tmpl/language/edit.php +++ b/administrator/components/com_languages/tmpl/language/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_languages.admin-language-edit-change-flag'); + ->useScript('form.validate') + ->useScript('com_languages.admin-language-edit-change-flag'); ?> @@ -25,61 +26,61 @@
    - 'details', 'recall' => true, 'breakpoint' => 768]); ?> + 'details', 'recall' => true, 'breakpoint' => 768]); ?> - -
    - -
    - form->renderField('title'); ?> - form->renderField('title_native'); ?> - form->renderField('lang_code'); ?> - form->renderField('sef'); ?> -
    -
    - form->getLabel('image'); ?> -
    -
    - form->getInput('image'); ?> - - form->getValue('image') . '.gif', $this->form->getValue('image'), null, true); ?> - -
    -
    - canDo->get('core.edit.state')) : ?> - form->renderField('published'); ?> - + +
    + +
    + form->renderField('title'); ?> + form->renderField('title_native'); ?> + form->renderField('lang_code'); ?> + form->renderField('sef'); ?> +
    +
    + form->getLabel('image'); ?> +
    +
    + form->getInput('image'); ?> + + form->getValue('image') . '.gif', $this->form->getValue('image'), null, true); ?> + +
    +
    + canDo->get('core.edit.state')) : ?> + form->renderField('published'); ?> + - form->renderField('access'); ?> - form->renderField('description'); ?> - form->renderField('lang_id'); ?> -
    -
    - + form->renderField('access'); ?> + form->renderField('description'); ?> + form->renderField('lang_id'); ?> +
    +
    + - -
    -
    -
    - -
    - form->renderFieldset('site_name'); ?> -
    -
    -
    -
    -
    - -
    - form->renderFieldset('metadata'); ?> -
    -
    -
    -
    - + +
    +
    +
    + +
    + form->renderFieldset('site_name'); ?> +
    +
    +
    +
    +
    + +
    + form->renderFieldset('metadata'); ?> +
    +
    +
    +
    + - + - - + +
    diff --git a/administrator/components/com_languages/tmpl/languages/default.php b/administrator/components/com_languages/tmpl/languages/default.php index d41787d26e461..131e65d5f1cb2 100644 --- a/administrator/components/com_languages/tmpl/languages/default.php +++ b/administrator/components/com_languages/tmpl/languages/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'a.ordering'; -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_languages&task=languages.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_languages&task=languages.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
    -
    -
    -
    - $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction=""> - items as $i => $item) : - $canCreate = $user->authorise('core.create', 'com_languages'); - $canEdit = $user->authorise('core.edit', 'com_languages'); - $canChange = $user->authorise('core.edit.state', 'com_languages'); - ?> - - - + + + + + + + + + + + + +
    - , - , - -
    - - - - - - - - - - - - - - - - - - - - - -
    - lang_id, false, 'cid', 'cb', $item->title); ?> - - +
    +
    + $this)); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction=""> + items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_languages'); + $canEdit = $user->authorise('core.edit', 'com_languages'); + $canChange = $user->authorise('core.edit.state', 'com_languages'); + ?> + + + - - - - - - - - - - - - -
    + , + , + +
    + + + + + + + + + + + + + + + + + + + + + +
    + lang_id, false, 'cid', 'cb', $item->title); ?> + + - - - - - - - - - - - published, $i, 'languages.', $canChange); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - - - escape($item->title_native); ?> - - escape($item->lang_code); ?> - - escape($item->sef); ?> - - image) : ?> - image . '.gif', $item->image, array('class'=>'me-1'), true); ?>escape($item->image); ?> - - - - - escape($item->access_level); ?> - - home == '1') ? Text::_('JYES') : Text::_('JNO'); ?> - - escape($item->lang_id); ?> -
    + if (!$saveOrder) : + $disabledLabel = Text::_('JORDERINGDISABLED'); + $disableClassName = 'inactive'; + endif; ?> + + + + + + + + + +
    + published, $i, 'languages.', $canChange); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + + + escape($item->title_native); ?> + + escape($item->lang_code); ?> + + escape($item->sef); ?> + + image) : ?> + image . '.gif', $item->image, array('class' => 'me-1'), true); ?>escape($item->image); ?> + + + + + escape($item->access_level); ?> + + home == '1') ? Text::_('JYES') : Text::_('JNO'); ?> + + escape($item->lang_id); ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
    -
    -
    + + + + +
    + + diff --git a/administrator/components/com_languages/tmpl/multilangstatus/default.php b/administrator/components/com_languages/tmpl/multilangstatus/default.php index 270845822397c..00ab90ae7648b 100644 --- a/administrator/components/com_languages/tmpl/multilangstatus/default.php +++ b/administrator/components/com_languages/tmpl/multilangstatus/default.php @@ -1,4 +1,5 @@ homepages, 'language'); ?>
    - language_filter && $this->switchers == 0) : ?> - homes == 1) : ?> -
    - - - -
    - -
    - - - -
    - - - default_lang, $content_languages)) : ?> -
    - - - default_lang); ?> -
    - - contentlangs as $contentlang) : ?> - lang_code == $this->default_lang && $contentlang->published != 1) : ?> -
    - - - default_lang); ?> -
    - - - - defaultHome == true) : ?> -
    - - - -
    - - statuses as $status) : ?> - - lang_code && $status->published == 1 && $status->home_published != 1) : ?> -
    - - - lang_code, $status->lang_code); ?> -
    - - - lang_code && $status->published == 0 && $status->home_published != 1) : ?> -
    - - - lang_code, $status->lang_code); ?> -
    - - - -
    - - - -
    - - -
    - - - -
    - - contentlangs as $contentlang) : ?> - lang_code, $this->homepages) && (!array_key_exists($contentlang->lang_code, $this->site_langs) || $contentlang->published != 1)) : ?> -
    - - - lang_code); ?> -
    - - lang_code, $this->site_langs)) : ?> -
    - - - lang_code); ?> -
    - - published == -2) : ?> -
    - - - lang_code); ?> -
    - - sef)) : ?> -
    - - - lang_code); ?> -
    - - - listUsersError) : ?> -
    - - - -
      - listUsersError as $user) : ?> -
    • - name); ?> -
    • - -
    -
    - - - - -
    - - - -
    - - - - - - - - - - - - - - - + language_filter && $this->switchers == 0) : ?> + homes == 1) : ?> +
    + + + +
    + +
    + + + +
    + + + default_lang, $content_languages)) : ?> +
    + + + default_lang); ?> +
    + + contentlangs as $contentlang) : ?> + lang_code == $this->default_lang && $contentlang->published != 1) : ?> +
    + + + default_lang); ?> +
    + + + + defaultHome == true) : ?> +
    + + + +
    + + statuses as $status) : ?> + + lang_code && $status->published == 1 && $status->home_published != 1) : ?> +
    + + + lang_code, $status->lang_code); ?> +
    + + + lang_code && $status->published == 0 && $status->home_published != 1) : ?> +
    + + + lang_code, $status->lang_code); ?> +
    + + + +
    + + + +
    + + +
    + + + +
    + + contentlangs as $contentlang) : ?> + lang_code, $this->homepages) && (!array_key_exists($contentlang->lang_code, $this->site_langs) || $contentlang->published != 1)) : ?> +
    + + + lang_code); ?> +
    + + lang_code, $this->site_langs)) : ?> +
    + + + lang_code); ?> +
    + + published == -2) : ?> +
    + + + lang_code); ?> +
    + + sef)) : ?> +
    + + + lang_code); ?> +
    + + + listUsersError) : ?> +
    + + + +
      + listUsersError as $user) : ?> +
    • + name); ?> +
    • + +
    +
    + + + + +
    + + + +
    + + +
    - - - -
    - - - language_filter) : ?> - - - - -
    + + + + + + + + + + + + - - - - - - - - - -
    + + + +
    + + + language_filter) : ?> + + + + +
    - - - switchers != 0) : ?> - switchers; ?> - - - -
    - homes > 1) : ?> - - - - - - homes > 1) : ?> - homes; ?> - - - -
    - - - - - - - - - - - - statuses as $status) : ?> - element) : ?> - - - - - element) : ?> - - - - - - - - - - - contentlangs as $contentlang) : ?> - lang_code, $this->site_langs)) : ?> - - - - - - - - - - - - - - - - - - - - -
    - - - - - - - -
    - element; ?> - - - - - - - lang_code && $status->published == 1) : ?> - - - lang_code && $status->published == 0) : ?> - - - lang_code && $status->published == -2) : ?> - - - - - - - - home_published == 1) : ?> - - - home_published == 0) : ?> - - - home_published == -2) : ?> - - - - - - -
    - lang_code; ?> - - - - - published == 1) : ?> - - - published == 0 && array_key_exists($contentlang->lang_code, $this->homepages)) : ?> - - - published == -2 && array_key_exists($contentlang->lang_code, $this->homepages)) : ?> - - - - - lang_code, $this->homepages)) : ?> - - - - - - -
    - - - - - - - - - - -
    - + + + + + + switchers != 0) : ?> + switchers; ?> + + + + + + + + homes > 1) : ?> + + + + + + + homes > 1) : ?> + homes; ?> + + + + + + + + + + + + + + + + + + + statuses as $status) : ?> + element) : ?> + + + + + element) : ?> + + + + + + + + + + + contentlangs as $contentlang) : ?> + lang_code, $this->site_langs)) : ?> + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + +
    + element; ?> + + + + + + + lang_code && $status->published == 1) : ?> + + + lang_code && $status->published == 0) : ?> + + + lang_code && $status->published == -2) : ?> + + + + + + + + home_published == 1) : ?> + + + home_published == 0) : ?> + + + home_published == -2) : ?> + + + + + + +
    + lang_code; ?> + + + + + published == 1) : ?> + + + published == 0 && array_key_exists($contentlang->lang_code, $this->homepages)) : ?> + + + published == -2 && array_key_exists($contentlang->lang_code, $this->homepages)) : ?> + + + + + lang_code, $this->homepages)) : ?> + + + + + + +
    + + + + + + + + + + +
    +
    diff --git a/administrator/components/com_languages/tmpl/override/edit.php b/administrator/components/com_languages/tmpl/override/edit.php index 929c4cb36c117..fbad39d067997 100644 --- a/administrator/components/com_languages/tmpl/override/edit.php +++ b/administrator/components/com_languages/tmpl/override/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->usePreset('com_languages.overrider') - ->useScript('com_languages.admin-override-edit-refresh-searchstring'); + ->useScript('form.validate') + ->usePreset('com_languages.overrider') + ->useScript('com_languages.admin-override-edit-refresh-searchstring'); ?>
    -
    -
    -
    - item->key) ? Text::_('COM_LANGUAGES_VIEW_OVERRIDE_EDIT_NEW_OVERRIDE_LEGEND') : Text::_('COM_LANGUAGES_VIEW_OVERRIDE_EDIT_EDIT_OVERRIDE_LEGEND'); ?> -
    - form->renderField('language'); ?> - form->renderField('client'); ?> - form->renderField('key'); ?> - form->renderField('override'); ?> +
    +
    +
    + item->key) ? Text::_('COM_LANGUAGES_VIEW_OVERRIDE_EDIT_NEW_OVERRIDE_LEGEND') : Text::_('COM_LANGUAGES_VIEW_OVERRIDE_EDIT_EDIT_OVERRIDE_LEGEND'); ?> +
    + form->renderField('language'); ?> + form->renderField('client'); ?> + form->renderField('key'); ?> + form->renderField('override'); ?> - state->get('filter.client') == 'administrator') : ?> - form->renderField('both'); ?> - + state->get('filter.client') == 'administrator') : ?> + form->renderField('both'); ?> + - form->renderField('file'); ?> -
    -
    -
    + form->renderField('file'); ?> +
    +
    +
    -
    - +
    + -
    - -
    - - - -
    +
    + +
    + + + +
    - - + + - -
    -
    + +
    +
    diff --git a/administrator/components/com_languages/tmpl/overrides/default.php b/administrator/components/com_languages/tmpl/overrides/default.php index 7b4081d370457..954a765486f3e 100644 --- a/administrator/components/com_languages/tmpl/overrides/default.php +++ b/administrator/components/com_languages/tmpl/overrides/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $client = $this->state->get('filter.client') == 'site' ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); $language = $this->state->get('filter.language'); @@ -28,93 +29,92 @@ $oppositeClient = $this->state->get('filter.client') == 'administrator' ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); $oppositeFilename = constant('JPATH_' . strtoupper($this->state->get('filter.client') === 'site' ? 'administrator' : 'site')) - . '/language/overrides/' . $this->state->get('filter.language', 'en-GB') . '.override.ini'; + . '/language/overrides/' . $this->state->get('filter.language', 'en-GB') . '.override.ini'; $oppositeStrings = LanguageHelper::parseIniFile($oppositeFilename); ?>
    -
    -
    -
    - $this, 'options' => ['selectorFieldName' => 'language_client'])); ?> -
    - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - authorise('core.edit', 'com_languages'); ?> - - items as $key => $text) : ?> - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - -
    - - - - - escape($key); ?> - - escape($key); ?> - - - escape($text), 200); ?> - - - - - -
    +
    +
    +
    + $this, 'options' => ['selectorFieldName' => 'language_client'])); ?> +
    + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + authorise('core.edit', 'com_languages'); ?> + + items as $key => $text) : ?> + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + +
    + + + + + escape($key); ?> + + escape($key); ?> + + + escape($text), 200); ?> + + + + + +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
    -
    -
    + + + +
    +
    +
    diff --git a/administrator/components/com_login/services/provider.php b/administrator/components/com_login/services/provider.php index bf55bc6227c68..eb064b3428dff 100644 --- a/administrator/components/com_login/services/provider.php +++ b/administrator/components/com_login/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Login')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Login')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Login')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Login')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_login/src/Controller/DisplayController.php b/administrator/components/com_login/src/Controller/DisplayController.php index db93e6ebdb584..344c2e8bcd1d7 100644 --- a/administrator/components/com_login/src/Controller/DisplayController.php +++ b/administrator/components/com_login/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->set('view', 'login'); - $this->input->set('layout', 'default'); - - // For non-html formats we do not have login view, so just display 403 instead - if ($this->input->get('format', 'html') !== 'html') - { - throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - /** - * To prevent clickjacking, only allow the login form to be used inside a frame in the same origin. - * So send a X-Frame-Options HTTP Header with the SAMEORIGIN value. - * - * @link https://www.owasp.org/index.php/Clickjacking_Defense_Cheat_Sheet - * @link https://tools.ietf.org/html/rfc7034 - */ - $this->app->setHeader('X-Frame-Options', 'SAMEORIGIN'); - - return parent::display(); - } - - /** - * Method to log in a user. - * - * @return void - */ - public function login() - { - // Check for request forgeries. - $this->checkToken(); - - $app = $this->app; - - $model = $this->getModel('login'); - $credentials = $model->getState('credentials'); - $return = $model->getState('return'); - - $app->login($credentials, array('action' => 'core.login.admin')); - - if (Uri::isInternal($return) && strpos($return, 'tmpl=component') === false) - { - $app->redirect($return); - } - else - { - $app->redirect('index.php'); - } - } - - /** - * Method to log out a user. - * - * @return void - */ - public function logout() - { - $this->checkToken('request'); - - $app = $this->app; - - $userid = $this->input->getInt('uid', null); - - if ($app->get('shared_session', '0')) - { - $clientid = null; - } - else - { - $clientid = $userid ? 0 : 1; - } - - $options = array( - 'clientid' => $clientid, - ); - - $result = $app->logout($userid, $options); - - if (!($result instanceof \Exception)) - { - $model = $this->getModel('login'); - $return = $model->getState('return'); - - // Only redirect to an internal URL. - if (Uri::isInternal($return)) - { - $app->redirect($return); - } - } - - parent::display(); - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 1.5 + * @throws \Exception + */ + public function display($cachable = false, $urlparams = false) + { + /* + * Special treatment is required for this component, as this view may be called + * after a session timeout. We must reset the view and layout prior to display + * otherwise an error will occur. + */ + $this->input->set('view', 'login'); + $this->input->set('layout', 'default'); + + // For non-html formats we do not have login view, so just display 403 instead + if ($this->input->get('format', 'html') !== 'html') { + throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + /** + * To prevent clickjacking, only allow the login form to be used inside a frame in the same origin. + * So send a X-Frame-Options HTTP Header with the SAMEORIGIN value. + * + * @link https://www.owasp.org/index.php/Clickjacking_Defense_Cheat_Sheet + * @link https://tools.ietf.org/html/rfc7034 + */ + $this->app->setHeader('X-Frame-Options', 'SAMEORIGIN'); + + return parent::display(); + } + + /** + * Method to log in a user. + * + * @return void + */ + public function login() + { + // Check for request forgeries. + $this->checkToken(); + + $app = $this->app; + + $model = $this->getModel('login'); + $credentials = $model->getState('credentials'); + $return = $model->getState('return'); + + $app->login($credentials, array('action' => 'core.login.admin')); + + if (Uri::isInternal($return) && strpos($return, 'tmpl=component') === false) { + $app->redirect($return); + } else { + $app->redirect('index.php'); + } + } + + /** + * Method to log out a user. + * + * @return void + */ + public function logout() + { + $this->checkToken('request'); + + $app = $this->app; + + $userid = $this->input->getInt('uid', null); + + if ($app->get('shared_session', '0')) { + $clientid = null; + } else { + $clientid = $userid ? 0 : 1; + } + + $options = array( + 'clientid' => $clientid, + ); + + $result = $app->logout($userid, $options); + + if (!($result instanceof \Exception)) { + $model = $this->getModel('login'); + $return = $model->getState('return'); + + // Only redirect to an internal URL. + if (Uri::isInternal($return)) { + $app->redirect($return); + } + } + + parent::display(); + } } diff --git a/administrator/components/com_login/src/Dispatcher/Dispatcher.php b/administrator/components/com_login/src/Dispatcher/Dispatcher.php index f580d9d185382..f52354e2a75c2 100644 --- a/administrator/components/com_login/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_login/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->get('task'); - - if ($task != 'login' && $task != 'logout') - { - $this->input->set('task', ''); - } - - // Reset controller name - $this->input->set('controller', null); - - parent::dispatch(); - } - - /** - * com_login does not require check permission, so we override checkAccess method and have it empty - * - * @return void - */ - protected function checkAccess() - { - } + /** + * Dispatch a controller task. + * + * @return void + * + * @since 4.0.0 + */ + public function dispatch() + { + // Only accept two values login and logout for `task` + $task = $this->input->get('task'); + + if ($task != 'login' && $task != 'logout') { + $this->input->set('task', ''); + } + + // Reset controller name + $this->input->set('controller', null); + + parent::dispatch(); + } + + /** + * com_login does not require check permission, so we override checkAccess method and have it empty + * + * @return void + */ + protected function checkAccess() + { + } } diff --git a/administrator/components/com_login/src/Model/LoginModel.php b/administrator/components/com_login/src/Model/LoginModel.php index 59377adbd106d..2ff0c89d9976a 100644 --- a/administrator/components/com_login/src/Model/LoginModel.php +++ b/administrator/components/com_login/src/Model/LoginModel.php @@ -1,4 +1,5 @@ input->getInputForRequestMethod(); - - $credentials = array( - 'username' => $input->get('username', '', 'USERNAME'), - 'password' => $input->get('passwd', '', 'RAW'), - 'secretkey' => $input->get('secretkey', '', 'RAW'), - ); - - $this->setState('credentials', $credentials); - - // Check for return URL from the request first. - if ($return = $input->get('return', '', 'BASE64')) - { - $return = base64_decode($return); - - if (!Uri::isInternal($return)) - { - $return = ''; - } - } - - // Set the return URL if empty. - if (empty($return)) - { - $return = 'index.php'; - } - - $this->setState('return', $return); - } - - /** - * Get the administrator login module by name (real, eg 'login' or folder, eg 'mod_login'). - * - * @param string $name The name of the module. - * @param string $title The title of the module, optional. - * - * @return object The Module object. - * - * @since 1.7.0 - */ - public static function getLoginModule($name = 'mod_login', $title = null) - { - $result = null; - $modules = self::_load($name); - $total = count($modules); - - for ($i = 0; $i < $total; $i++) - { - // Match the title if we're looking for a specific instance of the module. - if (!$title || $modules[$i]->title == $title) - { - $result = $modules[$i]; - break; - } - } - - // If we didn't find it, and the name is mod_something, create a dummy object. - if (is_null($result) && substr($name, 0, 4) == 'mod_') - { - $result = new \stdClass; - $result->id = 0; - $result->title = ''; - $result->module = $name; - $result->position = ''; - $result->content = ''; - $result->showtitle = 0; - $result->control = ''; - $result->params = ''; - $result->user = 0; - } - - return $result; - } - - /** - * Load login modules. - * - * Note that we load regardless of state or access level since access - * for public is the only thing that makes sense since users are not logged in - * and the module lets them log in. - * This is put in as a failsafe to avoid super user lock out caused by an unpublished - * login module or by a module set to have a viewing access level that is not Public. - * - * @param string $module The name of the module. - * - * @return array - * - * @since 1.7.0 - */ - protected static function _load($module) - { - static $clean; - - if (isset($clean)) - { - return $clean; - } - - $app = Factory::getApplication(); - $lang = Factory::getLanguage()->getTag(); - $clientId = (int) $app->getClientId(); - - /** @var \Joomla\CMS\Cache\Controller\CallbackController $cache */ - $cache = Factory::getCache('com_modules', 'callback'); - - $loader = function () use ($app, $lang, $module) { - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 'm.id', - 'm.title', - 'm.module', - 'm.position', - 'm.showtitle', - 'm.params' - ] - ) - ) - ->from($db->quoteName('#__modules', 'm')) - ->where($db->quoteName('m.module') . ' = :module') - ->where($db->quoteName('m.client_id') . ' = 1') - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.element') . ' = ' . $db->quoteName('m.module') . - ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('m.client_id') - ) - ->where($db->quoteName('e.enabled') . ' = 1') - ->bind(':module', $module); - - // Filter by language. - if ($app->isClient('site') && $app->getLanguageFilter()) - { - $query->whereIn($db->quoteName('m.language'), [$lang, '*']); - } - - $query->order('m.position, m.ordering'); - - // Set the query. - $db->setQuery($query); - - return $db->loadObjectList(); - }; - - try - { - return $clean = $cache->get($loader, array(), md5(serialize(array($clientId, $lang)))); - } - catch (CacheExceptionInterface $cacheException) - { - try - { - return $loader(); - } - catch (ExecutionFailureException $databaseException) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $databaseException->getMessage()), - 'error' - ); - - return array(); - } - } - catch (ExecutionFailureException $databaseException) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $databaseException->getMessage()), 'error'); - - return array(); - } - } + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $input = Factory::getApplication()->input->getInputForRequestMethod(); + + $credentials = array( + 'username' => $input->get('username', '', 'USERNAME'), + 'password' => $input->get('passwd', '', 'RAW'), + 'secretkey' => $input->get('secretkey', '', 'RAW'), + ); + + $this->setState('credentials', $credentials); + + // Check for return URL from the request first. + if ($return = $input->get('return', '', 'BASE64')) { + $return = base64_decode($return); + + if (!Uri::isInternal($return)) { + $return = ''; + } + } + + // Set the return URL if empty. + if (empty($return)) { + $return = 'index.php'; + } + + $this->setState('return', $return); + } + + /** + * Get the administrator login module by name (real, eg 'login' or folder, eg 'mod_login'). + * + * @param string $name The name of the module. + * @param string $title The title of the module, optional. + * + * @return object The Module object. + * + * @since 1.7.0 + */ + public static function getLoginModule($name = 'mod_login', $title = null) + { + $result = null; + $modules = self::_load($name); + $total = count($modules); + + for ($i = 0; $i < $total; $i++) { + // Match the title if we're looking for a specific instance of the module. + if (!$title || $modules[$i]->title == $title) { + $result = $modules[$i]; + break; + } + } + + // If we didn't find it, and the name is mod_something, create a dummy object. + if (is_null($result) && substr($name, 0, 4) == 'mod_') { + $result = new \stdClass(); + $result->id = 0; + $result->title = ''; + $result->module = $name; + $result->position = ''; + $result->content = ''; + $result->showtitle = 0; + $result->control = ''; + $result->params = ''; + $result->user = 0; + } + + return $result; + } + + /** + * Load login modules. + * + * Note that we load regardless of state or access level since access + * for public is the only thing that makes sense since users are not logged in + * and the module lets them log in. + * This is put in as a failsafe to avoid super user lock out caused by an unpublished + * login module or by a module set to have a viewing access level that is not Public. + * + * @param string $module The name of the module. + * + * @return array + * + * @since 1.7.0 + */ + protected static function _load($module) + { + static $clean; + + if (isset($clean)) { + return $clean; + } + + $app = Factory::getApplication(); + $lang = Factory::getLanguage()->getTag(); + $clientId = (int) $app->getClientId(); + + /** @var \Joomla\CMS\Cache\Controller\CallbackController $cache */ + $cache = Factory::getCache('com_modules', 'callback'); + + $loader = function () use ($app, $lang, $module) { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 'm.id', + 'm.title', + 'm.module', + 'm.position', + 'm.showtitle', + 'm.params' + ] + ) + ) + ->from($db->quoteName('#__modules', 'm')) + ->where($db->quoteName('m.module') . ' = :module') + ->where($db->quoteName('m.client_id') . ' = 1') + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.element') . ' = ' . $db->quoteName('m.module') . + ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('m.client_id') + ) + ->where($db->quoteName('e.enabled') . ' = 1') + ->bind(':module', $module); + + // Filter by language. + if ($app->isClient('site') && $app->getLanguageFilter()) { + $query->whereIn($db->quoteName('m.language'), [$lang, '*']); + } + + $query->order('m.position, m.ordering'); + + // Set the query. + $db->setQuery($query); + + return $db->loadObjectList(); + }; + + try { + return $clean = $cache->get($loader, array(), md5(serialize(array($clientId, $lang)))); + } catch (CacheExceptionInterface $cacheException) { + try { + return $loader(); + } catch (ExecutionFailureException $databaseException) { + Factory::getApplication()->enqueueMessage( + Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $databaseException->getMessage()), + 'error' + ); + + return array(); + } + } catch (ExecutionFailureException $databaseException) { + Factory::getApplication()->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $databaseException->getMessage()), 'error'); + + return array(); + } + } } diff --git a/administrator/components/com_login/src/View/Login/HtmlView.php b/administrator/components/com_login/src/View/Login/HtmlView.php index 40bcab2f8ae89..c8c365543ebf2 100644 --- a/administrator/components/com_login/src/View/Login/HtmlView.php +++ b/administrator/components/com_login/src/View/Login/HtmlView.php @@ -1,4 +1,5 @@ module != 'mod_login'){ - echo ModuleHelper::renderModule($module, array('id' => 'section-box')); + if ($module->module != 'mod_login') { + echo ModuleHelper::renderModule($module, array('id' => 'section-box')); + } } diff --git a/administrator/components/com_mails/services/provider.php b/administrator/components/com_mails/services/provider.php index 7790122707ca7..794bdfe532688 100644 --- a/administrator/components/com_mails/services/provider.php +++ b/administrator/components/com_mails/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Mails')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Mails')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Mails')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Mails')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_mails/src/Controller/DisplayController.php b/administrator/components/com_mails/src/Controller/DisplayController.php index b6cd1e43eea00..37b3ef58db72c 100644 --- a/administrator/components/com_mails/src/Controller/DisplayController.php +++ b/administrator/components/com_mails/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'templates'); - $layout = $this->input->get('layout', ''); - $id = $this->input->getString('template_id'); - - // Check for edit form. - if ($view == 'template' && $layout == 'edit' && !$this->checkEditId('com_mails.edit.template', $id)) - { - // Somehow the person just went to the form - we don't allow that. - $this->setMessage(Text::sprintf('COM_MAILS_ERROR_UNHELD_ID', $id), 'error'); - $this->setRedirect(Route::_('index.php?option=com_mails&view=templates', false)); - - return false; - } - - return parent::display(); - } + /** + * The default view. + * + * @var string + * @since 4.0.0 + */ + protected $default_view = 'templates'; + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return BaseController|boolean This object to support chaining. + * + * @since 4.0.0 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', 'templates'); + $layout = $this->input->get('layout', ''); + $id = $this->input->getString('template_id'); + + // Check for edit form. + if ($view == 'template' && $layout == 'edit' && !$this->checkEditId('com_mails.edit.template', $id)) { + // Somehow the person just went to the form - we don't allow that. + $this->setMessage(Text::sprintf('COM_MAILS_ERROR_UNHELD_ID', $id), 'error'); + $this->setRedirect(Route::_('index.php?option=com_mails&view=templates', false)); + + return false; + } + + return parent::display(); + } } diff --git a/administrator/components/com_mails/src/Controller/TemplateController.php b/administrator/components/com_mails/src/Controller/TemplateController.php index 2b980daaf07de..deadbc1e6b273 100644 --- a/administrator/components/com_mails/src/Controller/TemplateController.php +++ b/administrator/components/com_mails/src/Controller/TemplateController.php @@ -1,4 +1,5 @@ view_item = 'template'; - $this->view_list = 'templates'; - } - - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowAdd($data = []) - { - return false; - } - - /** - * Method to edit an existing record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key - * (sometimes required to avoid router collisions). - * - * @return boolean True if access level check and checkout passes, false otherwise. - * - * @since 4.0.0 - */ - public function edit($key = null, $urlVar = null) - { - // Do not cache the response to this, its a redirect, and mod_expires and google chrome browser bugs cache it forever! - $this->app->allowCache(false); - - $context = "$this->option.edit.$this->context"; - - // Get the previous record id (if any) and the current record id. - $template_id = $this->input->getCmd('template_id'); - $language = $this->input->getCmd('language'); - - // Access check. - if (!$this->allowEdit(array('template_id' => $template_id, 'language' => $language), $template_id)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false - ) - ); - - return false; - } - - // Check-out succeeded, push the new record id into the session. - $this->holdEditId($context, $template_id . '.' . $language); - $this->app->setUserState($context . '.data', null); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend(array($template_id, $language), 'template_id'), false - ) - ); - - return true; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param string[] $recordId The primary key id for the item in the first element and the language of the - * mail template in the second key. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $language = array_pop($recordId); - $return = parent::getRedirectToItemAppend(array_pop($recordId), $urlVar); - $return .= '&language=' . $language; - - return $return; - } - - /** - * Method to save a record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 4.0.0 - */ - public function save($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ - $model = $this->getModel(); - $data = $this->input->post->get('jform', array(), 'array'); - $context = "$this->option.edit.$this->context"; - $task = $this->getTask(); - - $recordId = $this->input->getCmd('template_id'); - $language = $this->input->getCmd('language'); - - // Populate the row id from the session. - $data['template_id'] = $recordId; - $data['language'] = $language; - - // Access check. - if (!$this->allowSave($data, 'template_id')) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false - ) - ); - - return false; - } - - // Validate the posted data. - // Sometimes the form needs some posted data, such as for plugins and modules. - $form = $model->getForm($data, false); - - if (!$form) - { - $this->app->enqueueMessage($model->getError(), 'error'); - - return false; - } - - // Send an object which can be modified through the plugin event - $objData = (object) $data; - $this->app->triggerEvent( - 'onContentNormaliseRequestData', - array($this->option . '.' . $this->context, $objData, $form) - ); - $data = (array) $objData; - - // Test whether the data is valid. - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $this->app->setUserState($context . '.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), false - ) - ); - - return false; - } - - // Attempt to save the data. - if (!$model->save($validData)) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $validData); - - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), false - ) - ); - - return false; - } - - $langKey = $this->text_prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS'; - $prefix = Factory::getLanguage()->hasKey($langKey) ? $this->text_prefix : 'COM_MAILS'; - - $this->setMessage(Text::_($prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS')); - - // Redirect the user and adjust session state based on the chosen task. - switch ($task) - { - case 'apply': - // Set the record data in the session. - $this->holdEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), false - ) - ); - break; - - default: - // Clear the record id and data from the session. - $this->releaseEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - - $url = 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(); - - // Check if there is a return value - $return = $this->input->get('return', null, 'base64'); - - if (!is_null($return) && Uri::isInternal(base64_decode($return))) - { - $url = base64_decode($return); - } - - // Redirect to the list screen. - $this->setRedirect(Route::_($url, false)); - break; - } - - // Invoke the postSave method to allow for the child class to access the model. - $this->postSaveHook($model, $validData); - - return true; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->view_item = 'template'; + $this->view_list = 'templates'; + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowAdd($data = []) + { + return false; + } + + /** + * Method to edit an existing record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key + * (sometimes required to avoid router collisions). + * + * @return boolean True if access level check and checkout passes, false otherwise. + * + * @since 4.0.0 + */ + public function edit($key = null, $urlVar = null) + { + // Do not cache the response to this, its a redirect, and mod_expires and google chrome browser bugs cache it forever! + $this->app->allowCache(false); + + $context = "$this->option.edit.$this->context"; + + // Get the previous record id (if any) and the current record id. + $template_id = $this->input->getCmd('template_id'); + $language = $this->input->getCmd('language'); + + // Access check. + if (!$this->allowEdit(array('template_id' => $template_id, 'language' => $language), $template_id)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(), + false + ) + ); + + return false; + } + + // Check-out succeeded, push the new record id into the session. + $this->holdEditId($context, $template_id . '.' . $language); + $this->app->setUserState($context . '.data', null); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_item + . $this->getRedirectToItemAppend(array($template_id, $language), 'template_id'), + false + ) + ); + + return true; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param string[] $recordId The primary key id for the item in the first element and the language of the + * mail template in the second key. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $language = array_pop($recordId); + $return = parent::getRedirectToItemAppend(array_pop($recordId), $urlVar); + $return .= '&language=' . $language; + + return $return; + } + + /** + * Method to save a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 4.0.0 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(); + $data = $this->input->post->get('jform', array(), 'array'); + $context = "$this->option.edit.$this->context"; + $task = $this->getTask(); + + $recordId = $this->input->getCmd('template_id'); + $language = $this->input->getCmd('language'); + + // Populate the row id from the session. + $data['template_id'] = $recordId; + $data['language'] = $language; + + // Access check. + if (!$this->allowSave($data, 'template_id')) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(), + false + ) + ); + + return false; + } + + // Validate the posted data. + // Sometimes the form needs some posted data, such as for plugins and modules. + $form = $model->getForm($data, false); + + if (!$form) { + $this->app->enqueueMessage($model->getError(), 'error'); + + return false; + } + + // Send an object which can be modified through the plugin event + $objData = (object) $data; + $this->app->triggerEvent( + 'onContentNormaliseRequestData', + array($this->option . '.' . $this->context, $objData, $form) + ); + $data = (array) $objData; + + // Test whether the data is valid. + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $this->app->setUserState($context . '.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_item + . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), + false + ) + ); + + return false; + } + + // Attempt to save the data. + if (!$model->save($validData)) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $validData); + + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_item + . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), + false + ) + ); + + return false; + } + + $langKey = $this->text_prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS'; + $prefix = Factory::getLanguage()->hasKey($langKey) ? $this->text_prefix : 'COM_MAILS'; + + $this->setMessage(Text::_($prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS')); + + // Redirect the user and adjust session state based on the chosen task. + switch ($task) { + case 'apply': + // Set the record data in the session. + $this->holdEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_item + . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), + false + ) + ); + break; + + default: + // Clear the record id and data from the session. + $this->releaseEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + + $url = 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(); + + // Check if there is a return value + $return = $this->input->get('return', null, 'base64'); + + if (!is_null($return) && Uri::isInternal(base64_decode($return))) { + $url = base64_decode($return); + } + + // Redirect to the list screen. + $this->setRedirect(Route::_($url, false)); + break; + } + + // Invoke the postSave method to allow for the child class to access the model. + $this->postSaveHook($model, $validData); + + return true; + } } diff --git a/administrator/components/com_mails/src/Helper/MailsHelper.php b/administrator/components/com_mails/src/Helper/MailsHelper.php index 87aff21a2eba1..7004ba9451f43 100644 --- a/administrator/components/com_mails/src/Helper/MailsHelper.php +++ b/administrator/components/com_mails/src/Helper/MailsHelper.php @@ -1,4 +1,5 @@ triggerEvent('onMailBeforeTagsRendering', array($mail->template_id, &$mail)); - - if (!isset($mail->params['tags']) || !count($mail->params['tags'])) - { - return ''; - } - - $html = '
      '; - - foreach ($mail->params['tags'] as $tag) - { - $html .= '
    • ' - . '' . $tag . '' - . '
    • '; - } - - $html .= '
    '; - - return $html; - } - - /** - * Load the translation files for an extension - * - * @param string $extension Extension name - * - * @return void - * - * @since 4.0.0 - */ - public static function loadTranslationFiles($extension) - { - static $cache = array(); - - $extension = strtolower($extension); - - if (isset($cache[$extension])) - { - return; - } - - $lang = Factory::getLanguage(); - $source = ''; - - switch (substr($extension, 0, 3)) - { - case 'com': - default: - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - break; - - case 'mod': - $source = JPATH_SITE . '/modules/' . $extension; - break; - - case 'plg': - $parts = explode('_', $extension, 3); - - if (count($parts) > 2) - { - $source = JPATH_PLUGINS . '/' . $parts[1] . '/' . $parts[2]; - } - break; - } - - $lang->load($extension, JPATH_ADMINISTRATOR) - || $lang->load($extension, $source); - - if (!$lang->hasKey(strtoupper($extension))) - { - $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($extension . '.sys', $source); - } - - $cache[$extension] = true; - } + /** + * Display a clickable list of tags for a mail template + * + * @param object $mail Row of the mail template. + * @param string $fieldname Name of the target field. + * + * @return string List of tags that can be inserted into a field. + * + * @since 4.0.0 + */ + public static function mailtags($mail, $fieldname) + { + Factory::getApplication()->triggerEvent('onMailBeforeTagsRendering', array($mail->template_id, &$mail)); + + if (!isset($mail->params['tags']) || !count($mail->params['tags'])) { + return ''; + } + + $html = '
      '; + + foreach ($mail->params['tags'] as $tag) { + $html .= '
    • ' + . '' . $tag . '' + . '
    • '; + } + + $html .= '
    '; + + return $html; + } + + /** + * Load the translation files for an extension + * + * @param string $extension Extension name + * + * @return void + * + * @since 4.0.0 + */ + public static function loadTranslationFiles($extension) + { + static $cache = array(); + + $extension = strtolower($extension); + + if (isset($cache[$extension])) { + return; + } + + $lang = Factory::getLanguage(); + $source = ''; + + switch (substr($extension, 0, 3)) { + case 'com': + default: + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + break; + + case 'mod': + $source = JPATH_SITE . '/modules/' . $extension; + break; + + case 'plg': + $parts = explode('_', $extension, 3); + + if (count($parts) > 2) { + $source = JPATH_PLUGINS . '/' . $parts[1] . '/' . $parts[2]; + } + break; + } + + $lang->load($extension, JPATH_ADMINISTRATOR) + || $lang->load($extension, $source); + + if (!$lang->hasKey(strtoupper($extension))) { + $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($extension . '.sys', $source); + } + + $cache[$extension] = true; + } } diff --git a/administrator/components/com_mails/src/Model/TemplateModel.php b/administrator/components/com_mails/src/Model/TemplateModel.php index fb5e2146e06f2..136a23403dfcf 100644 --- a/administrator/components/com_mails/src/Model/TemplateModel.php +++ b/administrator/components/com_mails/src/Model/TemplateModel.php @@ -1,4 +1,5 @@ loadForm('com_mails.template', 'template', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - $params = ComponentHelper::getParams('com_mails'); - - if ($params->get('mail_style', 'plaintext') == 'plaintext') - { - $form->removeField('htmlbody'); - } - - if ($params->get('mail_style', 'plaintext') == 'html') - { - $form->removeField('body'); - } - - if (!$params->get('alternative_mailconfig', '0')) - { - $form->removeField('alternative_mailconfig', 'params'); - $form->removeField('mailfrom', 'params'); - $form->removeField('fromname', 'params'); - $form->removeField('replyto', 'params'); - $form->removeField('replytoname', 'params'); - $form->removeField('mailer', 'params'); - $form->removeField('sendmail', 'params'); - $form->removeField('smtphost', 'params'); - $form->removeField('smtpport', 'params'); - $form->removeField('smtpsecure', 'params'); - $form->removeField('smtpauth', 'params'); - $form->removeField('smtpuser', 'params'); - $form->removeField('smtppass', 'params'); - } - - if (!$params->get('copy_mails')) - { - $form->removeField('copyto', 'params'); - } - - if (!trim($params->get('attachment_folder', ''))) - { - $form->removeField('attachments'); - - return $form; - } - - try - { - $attachmentPath = rtrim(Path::check(JPATH_ROOT . '/' . $params->get('attachment_folder')), \DIRECTORY_SEPARATOR); - } - catch (\Exception $e) - { - $attachmentPath = ''; - } - - if (!$attachmentPath || $attachmentPath === Path::clean(JPATH_ROOT) || !is_dir($attachmentPath)) - { - $form->removeField('attachments'); - - return $form; - } - - $field = $form->getField('attachments'); - $subform = new \SimpleXMLElement($field->formsource); - $files = $subform->xpath('field[@name="file"]'); - $files[0]->addAttribute('directory', $attachmentPath); - $form->load('
    ' - . str_replace('', '', $subform->asXML()) - . '
    ' - ); - - return $form; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return CMSObject|boolean Object on success, false on failure. - * - * @since 4.0.0 - */ - public function getItem($pk = null) - { - $templateId = $this->getState($this->getName() . '.template_id'); - $language = $this->getState($this->getName() . '.language'); - $table = $this->getTable('Template', 'Table'); - - if ($templateId != '' && $language != '') - { - // Attempt to load the row. - $return = $table->load(array('template_id' => $templateId, 'language' => $language)); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - } - - // Convert to the CMSObject before adding other data. - $properties = $table->getProperties(1); - $item = ArrayHelper::toObject($properties, CMSObject::class); - - if (property_exists($item, 'params')) - { - $registry = new Registry($item->params); - $item->params = $registry->toArray(); - } - - if (!$item->template_id) - { - $item->template_id = $templateId; - } - - if (!$item->language) - { - $item->language = $language; - } - - return $item; - } - - /** - * Get the master data for a mail template. - * - * @param integer $pk The id of the primary key. - * - * @return CMSObject|boolean Object on success, false on failure. - * - * @since 4.0.0 - */ - public function getMaster($pk = null) - { - $template_id = $this->getState($this->getName() . '.template_id'); - $table = $this->getTable('Template', 'Table'); - - if ($template_id != '') - { - // Attempt to load the row. - $return = $table->load(array('template_id' => $template_id, 'language' => '')); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - } - - // Convert to the CMSObject before adding other data. - $properties = $table->getProperties(1); - $item = ArrayHelper::toObject($properties, CMSObject::class); - - if (property_exists($item, 'params')) - { - $registry = new Registry($item->params); - $item->params = $registry->toArray(); - } - - return $item; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 4.0.0 - * @throws \Exception - */ - public function getTable($name = 'Template', $prefix = 'Administrator', $options = array()) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_mails.edit.template.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_mails.template', $data); - - return $data; - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @since 4.0.0 - */ - public function validate($form, $data, $group = null) - { - $validLanguages = LanguageHelper::getContentLanguages(array(0, 1)); - - if (!array_key_exists($data['language'], $validLanguages)) - { - $this->setError(Text::_('COM_MAILS_FIELD_LANGUAGE_CODE_INVALID')); - - return false; - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success, False on error. - * - * @since 4.0.0 - */ - public function save($data) - { - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - - $key = $table->getKeyName(); - $template_id = (!empty($data['template_id'])) ? $data['template_id'] : $this->getState($this->getName() . '.template_id'); - $language = (!empty($data['language'])) ? $data['language'] : $this->getState($this->getName() . '.language'); - $isNew = true; - - // Include the plugins for the save events. - \Joomla\CMS\Plugin\PluginHelper::importPlugin($this->events_map['save']); - - // Allow an exception to be thrown. - try - { - // Load the row if saving an existing record. - $table->load(array('template_id' => $template_id, 'language' => $language)); - - if ($table->subject) - { - $isNew = false; - } - - // Load the default row - $table->load(array('template_id' => $template_id, 'language' => '')); - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Prepare the row for saving - $this->prepareTable($table); - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, $table, $isNew, $data)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Clean the cache. - $this->cleanCache(); - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, $table, $isNew, $data)); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $this->setState($this->getName() . '.new', $isNew); - - return true; - } - - /** - * Prepare and sanitise the table data prior to saving. - * - * @param Table $table A reference to a Table object. - * - * @return void - * - * @since 4.0.0 - */ - protected function prepareTable($table) - { - - } - - /** - * Stock method to auto-populate the model state. - * - * @return void - * - * @since 4.0.0 - */ - protected function populateState() - { - parent::populateState(); - - $template_id = Factory::getApplication()->input->getCmd('template_id'); - $this->setState($this->getName() . '.template_id', $template_id); - - $language = Factory::getApplication()->input->getCmd('language'); - $this->setState($this->getName() . '.language', $language); - } + /** + * The prefix to use with controller messages. + * + * @var string + * @since 4.0.0 + */ + protected $text_prefix = 'COM_MAILS'; + + /** + * The type alias for this content type (for example, 'com_content.article'). + * + * @var string + * @since 4.0.0 + */ + public $typeAlias = 'com_mails.template'; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 4.0.0 + */ + protected function canDelete($record) + { + return false; + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|bool A JForm object on success, false on failure + * + * @since 4.0.0 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_mails.template', 'template', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + $params = ComponentHelper::getParams('com_mails'); + + if ($params->get('mail_style', 'plaintext') == 'plaintext') { + $form->removeField('htmlbody'); + } + + if ($params->get('mail_style', 'plaintext') == 'html') { + $form->removeField('body'); + } + + if (!$params->get('alternative_mailconfig', '0')) { + $form->removeField('alternative_mailconfig', 'params'); + $form->removeField('mailfrom', 'params'); + $form->removeField('fromname', 'params'); + $form->removeField('replyto', 'params'); + $form->removeField('replytoname', 'params'); + $form->removeField('mailer', 'params'); + $form->removeField('sendmail', 'params'); + $form->removeField('smtphost', 'params'); + $form->removeField('smtpport', 'params'); + $form->removeField('smtpsecure', 'params'); + $form->removeField('smtpauth', 'params'); + $form->removeField('smtpuser', 'params'); + $form->removeField('smtppass', 'params'); + } + + if (!$params->get('copy_mails')) { + $form->removeField('copyto', 'params'); + } + + if (!trim($params->get('attachment_folder', ''))) { + $form->removeField('attachments'); + + return $form; + } + + try { + $attachmentPath = rtrim(Path::check(JPATH_ROOT . '/' . $params->get('attachment_folder')), \DIRECTORY_SEPARATOR); + } catch (\Exception $e) { + $attachmentPath = ''; + } + + if (!$attachmentPath || $attachmentPath === Path::clean(JPATH_ROOT) || !is_dir($attachmentPath)) { + $form->removeField('attachments'); + + return $form; + } + + $field = $form->getField('attachments'); + $subform = new \SimpleXMLElement($field->formsource); + $files = $subform->xpath('field[@name="file"]'); + $files[0]->addAttribute('directory', $attachmentPath); + $form->load('
    ' + . str_replace('', '', $subform->asXML()) + . '
    '); + + return $form; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return CMSObject|boolean Object on success, false on failure. + * + * @since 4.0.0 + */ + public function getItem($pk = null) + { + $templateId = $this->getState($this->getName() . '.template_id'); + $language = $this->getState($this->getName() . '.language'); + $table = $this->getTable('Template', 'Table'); + + if ($templateId != '' && $language != '') { + // Attempt to load the row. + $return = $table->load(array('template_id' => $templateId, 'language' => $language)); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + } + + // Convert to the CMSObject before adding other data. + $properties = $table->getProperties(1); + $item = ArrayHelper::toObject($properties, CMSObject::class); + + if (property_exists($item, 'params')) { + $registry = new Registry($item->params); + $item->params = $registry->toArray(); + } + + if (!$item->template_id) { + $item->template_id = $templateId; + } + + if (!$item->language) { + $item->language = $language; + } + + return $item; + } + + /** + * Get the master data for a mail template. + * + * @param integer $pk The id of the primary key. + * + * @return CMSObject|boolean Object on success, false on failure. + * + * @since 4.0.0 + */ + public function getMaster($pk = null) + { + $template_id = $this->getState($this->getName() . '.template_id'); + $table = $this->getTable('Template', 'Table'); + + if ($template_id != '') { + // Attempt to load the row. + $return = $table->load(array('template_id' => $template_id, 'language' => '')); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + } + + // Convert to the CMSObject before adding other data. + $properties = $table->getProperties(1); + $item = ArrayHelper::toObject($properties, CMSObject::class); + + if (property_exists($item, 'params')) { + $registry = new Registry($item->params); + $item->params = $registry->toArray(); + } + + return $item; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 4.0.0 + * @throws \Exception + */ + public function getTable($name = 'Template', $prefix = 'Administrator', $options = array()) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_mails.edit.template.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_mails.template', $data); + + return $data; + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @since 4.0.0 + */ + public function validate($form, $data, $group = null) + { + $validLanguages = LanguageHelper::getContentLanguages(array(0, 1)); + + if (!array_key_exists($data['language'], $validLanguages)) { + $this->setError(Text::_('COM_MAILS_FIELD_LANGUAGE_CODE_INVALID')); + + return false; + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success, False on error. + * + * @since 4.0.0 + */ + public function save($data) + { + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + + $key = $table->getKeyName(); + $template_id = (!empty($data['template_id'])) ? $data['template_id'] : $this->getState($this->getName() . '.template_id'); + $language = (!empty($data['language'])) ? $data['language'] : $this->getState($this->getName() . '.language'); + $isNew = true; + + // Include the plugins for the save events. + \Joomla\CMS\Plugin\PluginHelper::importPlugin($this->events_map['save']); + + // Allow an exception to be thrown. + try { + // Load the row if saving an existing record. + $table->load(array('template_id' => $template_id, 'language' => $language)); + + if ($table->subject) { + $isNew = false; + } + + // Load the default row + $table->load(array('template_id' => $template_id, 'language' => '')); + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Prepare the row for saving + $this->prepareTable($table); + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, $table, $isNew, $data)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Clean the cache. + $this->cleanCache(); + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, $table, $isNew, $data)); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $this->setState($this->getName() . '.new', $isNew); + + return true; + } + + /** + * Prepare and sanitise the table data prior to saving. + * + * @param Table $table A reference to a Table object. + * + * @return void + * + * @since 4.0.0 + */ + protected function prepareTable($table) + { + } + + /** + * Stock method to auto-populate the model state. + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState() + { + parent::populateState(); + + $template_id = Factory::getApplication()->input->getCmd('template_id'); + $this->setState($this->getName() . '.template_id', $template_id); + + $language = Factory::getApplication()->input->getCmd('language'); + $this->setState($this->getName() . '.language', $language); + } } diff --git a/administrator/components/com_mails/src/Model/TemplatesModel.php b/administrator/components/com_mails/src/Model/TemplatesModel.php index 3e415ca54ed81..e0d55ca8ecd9e 100644 --- a/administrator/components/com_mails/src/Model/TemplatesModel.php +++ b/administrator/components/com_mails/src/Model/TemplatesModel.php @@ -1,4 +1,5 @@ setState('params', $params); - - // List state information. - parent::populateState('a.template_id', 'asc'); - } - - /** - * Get a list of mail templates - * - * @return array - * - * @since 4.0.0 - */ - public function getItems() - { - $items = parent::getItems(); - $id = ''; - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('language')) - ->from($db->quoteName('#__mail_templates')) - ->where($db->quoteName('template_id') . ' = :id') - ->where($db->quoteName('language') . ' != ' . $db->quote('')) - ->order($db->quoteName('language') . ' ASC') - ->bind(':id', $id); - - foreach ($items as $item) - { - $id = $item->template_id; - $db->setQuery($query); - $item->languages = $db->loadColumn(); - } - - return $items; - } - - /** - * Build an SQL query to load the list data. - * - * @return QueryInterface - * - * @since 4.0.0 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - $db->quoteName('a') . '.*' - ) - ); - $query->from($db->quoteName('#__mail_templates', 'a')) - ->where($db->quoteName('a.language') . ' = ' . $db->quote('')); - - // Filter by search in title. - if ($search = trim($this->getState('filter.search', ''))) - { - if (stripos($search, 'id:') === 0) - { - $search = substr($search, 3); - $query->where($db->quoteName('a.template_id') . ' = :search') - ->bind(':search', $search); - } - else - { - $search = '%' . str_replace(' ', '%', $search) . '%'; - $query->where( - '(' . $db->quoteName('a.template_id') . ' LIKE :search1' - . ' OR ' . $db->quoteName('a.subject') . ' LIKE :search2' - . ' OR ' . $db->quoteName('a.body') . ' LIKE :search3' - . ' OR ' . $db->quoteName('a.htmlbody') . ' LIKE :search4)' - ) - ->bind([':search1', ':search2', ':search3', ':search4'], $search); - } - } - - // Filter on the extension. - if ($extension = $this->getState('filter.extension')) - { - $query->where($db->quoteName('a.extension') . ' = :extension') - ->bind(':extension', $extension); - } - else - { - // Only show mail template from enabled extensions - $subQuery = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('enabled') . ' = 1'); - - $query->where($db->quoteName('a.extension') . ' IN(' . $subQuery . ')'); - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->join( - 'INNER', - $db->quoteName('#__mail_templates', 'b'), - $db->quoteName('b.template_id') . ' = ' . $db->quoteName('a.template_id') - . ' AND ' . $db->quoteName('b.language') . ' = :language' - ) - ->bind(':language', $language); - } - - // Add the list ordering clause - $listOrdering = $this->state->get('list.ordering', 'a.template_id'); - $orderDirn = $this->state->get('list.direction', 'ASC'); - - $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); - - return $query; - } - - /** - * Get list of extensions which are using mail templates - * - * @return array - * - * @since 4.0.0 - */ - public function getExtensions() - { - $db = $this->getDatabase(); - $subQuery = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('enabled') . ' = 1'); - - $query = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('extension')) - ->from($db->quoteName('#__mail_templates')) - ->where($db->quoteName('extension') . ' IN (' . $subQuery . ')'); - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Get a list of the current content languages - * - * @return array - * - * @since 4.0.0 - */ - public function getLanguages() - { - return LanguageHelper::getContentLanguages(array(0,1)); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 4.0.0 + * @throws \Exception + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'template_id', 'a.template_id', + 'language', 'a.language', + 'subject', 'a.subject', + 'body', 'a.body', + 'htmlbody', 'a.htmlbody', + 'extension' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState($ordering = null, $direction = null) + { + // Load the parameters. + $params = ComponentHelper::getParams('com_mails'); + $this->setState('params', $params); + + // List state information. + parent::populateState('a.template_id', 'asc'); + } + + /** + * Get a list of mail templates + * + * @return array + * + * @since 4.0.0 + */ + public function getItems() + { + $items = parent::getItems(); + $id = ''; + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('language')) + ->from($db->quoteName('#__mail_templates')) + ->where($db->quoteName('template_id') . ' = :id') + ->where($db->quoteName('language') . ' != ' . $db->quote('')) + ->order($db->quoteName('language') . ' ASC') + ->bind(':id', $id); + + foreach ($items as $item) { + $id = $item->template_id; + $db->setQuery($query); + $item->languages = $db->loadColumn(); + } + + return $items; + } + + /** + * Build an SQL query to load the list data. + * + * @return QueryInterface + * + * @since 4.0.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + $db->quoteName('a') . '.*' + ) + ); + $query->from($db->quoteName('#__mail_templates', 'a')) + ->where($db->quoteName('a.language') . ' = ' . $db->quote('')); + + // Filter by search in title. + if ($search = trim($this->getState('filter.search', ''))) { + if (stripos($search, 'id:') === 0) { + $search = substr($search, 3); + $query->where($db->quoteName('a.template_id') . ' = :search') + ->bind(':search', $search); + } else { + $search = '%' . str_replace(' ', '%', $search) . '%'; + $query->where( + '(' . $db->quoteName('a.template_id') . ' LIKE :search1' + . ' OR ' . $db->quoteName('a.subject') . ' LIKE :search2' + . ' OR ' . $db->quoteName('a.body') . ' LIKE :search3' + . ' OR ' . $db->quoteName('a.htmlbody') . ' LIKE :search4)' + ) + ->bind([':search1', ':search2', ':search3', ':search4'], $search); + } + } + + // Filter on the extension. + if ($extension = $this->getState('filter.extension')) { + $query->where($db->quoteName('a.extension') . ' = :extension') + ->bind(':extension', $extension); + } else { + // Only show mail template from enabled extensions + $subQuery = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('enabled') . ' = 1'); + + $query->where($db->quoteName('a.extension') . ' IN(' . $subQuery . ')'); + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->join( + 'INNER', + $db->quoteName('#__mail_templates', 'b'), + $db->quoteName('b.template_id') . ' = ' . $db->quoteName('a.template_id') + . ' AND ' . $db->quoteName('b.language') . ' = :language' + ) + ->bind(':language', $language); + } + + // Add the list ordering clause + $listOrdering = $this->state->get('list.ordering', 'a.template_id'); + $orderDirn = $this->state->get('list.direction', 'ASC'); + + $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); + + return $query; + } + + /** + * Get list of extensions which are using mail templates + * + * @return array + * + * @since 4.0.0 + */ + public function getExtensions() + { + $db = $this->getDatabase(); + $subQuery = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('enabled') . ' = 1'); + + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('extension')) + ->from($db->quoteName('#__mail_templates')) + ->where($db->quoteName('extension') . ' IN (' . $subQuery . ')'); + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Get a list of the current content languages + * + * @return array + * + * @since 4.0.0 + */ + public function getLanguages() + { + return LanguageHelper::getContentLanguages(array(0,1)); + } } diff --git a/administrator/components/com_mails/src/Table/TemplateTable.php b/administrator/components/com_mails/src/Table/TemplateTable.php index c4c8f2f5b2792..420a75aa31cf2 100644 --- a/administrator/components/com_mails/src/Table/TemplateTable.php +++ b/administrator/components/com_mails/src/Table/TemplateTable.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); - $this->master = $this->get('Master'); - $this->form = $this->get('Form'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - list($component, $template_id) = explode('.', $this->item->template_id, 2); - $fields = array('subject', 'body', 'htmlbody'); - $this->templateData = array(); - $language = Factory::getLanguage(); - $language->load($component, JPATH_SITE, $this->item->language, true); - $language->load($component, JPATH_SITE . '/components/' . $component, $this->item->language, true); - $language->load($component, JPATH_ADMINISTRATOR, $this->item->language, true); - $language->load($component, JPATH_ADMINISTRATOR . '/components/' . $component, $this->item->language, true); - - $this->master->subject = Text::_($this->master->subject); - $this->master->body = Text::_($this->master->body); - - if ($this->master->htmlbody) - { - $this->master->htmlbody = Text::_($this->master->htmlbody); - } - else - { - $this->master->htmlbody = nl2br($this->master->body, false); - } - - $this->templateData = [ - 'subject' => $this->master->subject, - 'body' => $this->master->body, - 'htmlbody' => $this->master->htmlbody, - ]; - - foreach ($fields as $field) - { - if (is_null($this->item->$field) || $this->item->$field == '') - { - $this->item->$field = $this->master->$field; - $this->form->setValue($field, null, $this->item->$field); - } - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - $toolbar = Toolbar::getInstance(); - - ToolbarHelper::title( - Text::_('COM_MAILS_PAGE_EDIT_MAIL'), - 'pencil-2 article-add' - ); - - $saveGroup = $toolbar->dropdownButton('save-group'); - - $saveGroup->configure( - function (Toolbar $childBar) - { - $childBar->apply('template.apply'); - $childBar->save('template.save'); - } - ); - - $toolbar->cancel('template.cancel', 'JTOOLBAR_CLOSE'); - - $toolbar->divider(); - $toolbar->help('Mail_Template:_Edit'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var CMSObject + */ + protected $item; + + /** + * The model state + * + * @var object + */ + protected $state; + + /** + * The template data + * + * @var array + */ + protected $templateData; + + /** + * Master data for the mail template + * + * @var CMSObject + */ + protected $master; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->master = $this->get('Master'); + $this->form = $this->get('Form'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + list($component, $template_id) = explode('.', $this->item->template_id, 2); + $fields = array('subject', 'body', 'htmlbody'); + $this->templateData = array(); + $language = Factory::getLanguage(); + $language->load($component, JPATH_SITE, $this->item->language, true); + $language->load($component, JPATH_SITE . '/components/' . $component, $this->item->language, true); + $language->load($component, JPATH_ADMINISTRATOR, $this->item->language, true); + $language->load($component, JPATH_ADMINISTRATOR . '/components/' . $component, $this->item->language, true); + + $this->master->subject = Text::_($this->master->subject); + $this->master->body = Text::_($this->master->body); + + if ($this->master->htmlbody) { + $this->master->htmlbody = Text::_($this->master->htmlbody); + } else { + $this->master->htmlbody = nl2br($this->master->body, false); + } + + $this->templateData = [ + 'subject' => $this->master->subject, + 'body' => $this->master->body, + 'htmlbody' => $this->master->htmlbody, + ]; + + foreach ($fields as $field) { + if (is_null($this->item->$field) || $this->item->$field == '') { + $this->item->$field = $this->master->$field; + $this->form->setValue($field, null, $this->item->$field); + } + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + $toolbar = Toolbar::getInstance(); + + ToolbarHelper::title( + Text::_('COM_MAILS_PAGE_EDIT_MAIL'), + 'pencil-2 article-add' + ); + + $saveGroup = $toolbar->dropdownButton('save-group'); + + $saveGroup->configure( + function (Toolbar $childBar) { + $childBar->apply('template.apply'); + $childBar->save('template.save'); + } + ); + + $toolbar->cancel('template.cancel', 'JTOOLBAR_CLOSE'); + + $toolbar->divider(); + $toolbar->help('Mail_Template:_Edit'); + } } diff --git a/administrator/components/com_mails/src/View/Templates/HtmlView.php b/administrator/components/com_mails/src/View/Templates/HtmlView.php index f70abbb4604b1..b6ee16479ba94 100644 --- a/administrator/components/com_mails/src/View/Templates/HtmlView.php +++ b/administrator/components/com_mails/src/View/Templates/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->languages = $this->get('Languages'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $extensions = $this->get('Extensions'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Find and set site default language - $defaultLanguageTag = ComponentHelper::getParams('com_languages')->get('site'); - - foreach ($this->languages as $tag => $language) - { - if ($tag === $defaultLanguageTag) - { - $this->defaultLanguage = $language; - break; - } - } - - foreach ($extensions as $extension) - { - MailsHelper::loadTranslationFiles($extension); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - $user = $this->getCurrentUser(); - - ToolbarHelper::title(Text::_('COM_MAILS_MAILS_TITLE'), 'envelope'); - - if ($user->authorise('core.admin', 'com_mails') || $user->authorise('core.options', 'com_mails')) - { - $toolbar->preferences('com_mails'); - } - - $toolbar->help('Mail_Templates'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * An array of installed languages + * + * @var array + */ + protected $languages; + + /** + * Site default language + * + * @var \stdClass + */ + protected $defaultLanguage; + + /** + * The pagination object + * + * @var Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->languages = $this->get('Languages'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $extensions = $this->get('Extensions'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Find and set site default language + $defaultLanguageTag = ComponentHelper::getParams('com_languages')->get('site'); + + foreach ($this->languages as $tag => $language) { + if ($tag === $defaultLanguageTag) { + $this->defaultLanguage = $language; + break; + } + } + + foreach ($extensions as $extension) { + MailsHelper::loadTranslationFiles($extension); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + $user = $this->getCurrentUser(); + + ToolbarHelper::title(Text::_('COM_MAILS_MAILS_TITLE'), 'envelope'); + + if ($user->authorise('core.admin', 'com_mails') || $user->authorise('core.options', 'com_mails')) { + $toolbar->preferences('com_mails'); + } + + $toolbar->help('Mail_Templates'); + } } diff --git a/administrator/components/com_mails/tmpl/template/edit.php b/administrator/components/com_mails/tmpl/template/edit.php index f25a8e15f78e2..b0f14e322bb2f 100644 --- a/administrator/components/com_mails/tmpl/template/edit.php +++ b/administrator/components/com_mails/tmpl/template/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_mails.admin-email-template-edit'); + ->useScript('form.validate') + ->useScript('com_mails.admin-email-template-edit'); $this->useCoreUI = true; @@ -36,84 +37,84 @@ ?>
    -
    - 'general', 'recall' => true, 'breakpoint' => 768]); ?> - - -
    -
    -

    - escape($this->item->language); ?> -

    -
    - escape($this->master->template_id); ?> -
    -

    -
    -
    - -
    -
    - form->renderField('subject'); ?> -
    -
    - -
    -
    - form->getField('body')) : ?> -
    -
    - form->renderField('body'); ?> -
    -
    - -
    -

    - master, 'body'); ?> -
    -
    -
    - - - form->getField('htmlbody')) : ?> -
    -
    - form->renderField('htmlbody'); ?> -
    -
    - -
    -

    - master, 'htmlbody'); ?> -
    -
    -
    - - - form->getField('attachments')) : ?> -
    -
    - form->renderField('attachments'); ?> -
    -
    - - - - - form->getFieldset('basic'))) : ?> - - - - -
    - form->renderField('template_id'); ?> - form->renderField('language'); ?> - - - +
    + 'general', 'recall' => true, 'breakpoint' => 768]); ?> + + +
    +
    +

    - escape($this->item->language); ?> +

    +
    + escape($this->master->template_id); ?> +
    +

    +
    +
    + +
    +
    + form->renderField('subject'); ?> +
    +
    + +
    +
    + form->getField('body')) : ?> +
    +
    + form->renderField('body'); ?> +
    +
    + +
    +

    + master, 'body'); ?> +
    +
    +
    + + + form->getField('htmlbody')) : ?> +
    +
    + form->renderField('htmlbody'); ?> +
    +
    + +
    +

    + master, 'htmlbody'); ?> +
    +
    +
    + + + form->getField('attachments')) : ?> +
    +
    + form->renderField('attachments'); ?> +
    +
    + + + + + form->getFieldset('basic'))) : ?> + + + + +
    + form->renderField('template_id'); ?> + form->renderField('language'); ?> + + +
    diff --git a/administrator/components/com_mails/tmpl/templates/default.php b/administrator/components/com_mails/tmpl/templates/default.php index d0c3d023b9a08..17e770ba763ea 100644 --- a/administrator/components/com_mails/tmpl/templates/default.php +++ b/administrator/components/com_mails/tmpl/templates/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
    -
    -
    -
    - $this)); - ?> - items)) : ?> -
    - - -
    - - - - - - - - languages) > 1) : ?> - - - - - - - - items as $i => $item) : - list($component, $sub_id) = explode('.', $item->template_id, 2); - $sub_id = str_replace('.', '_', $sub_id); - ?> - - - - languages) > 1) : ?> - - - - - - - -
    - , - , - -
    - - - - - - - - - -
    - - - - - - - - - - - template_id; ?> -
    +
    +
    +
    + $this)); + ?> + items)) : ?> +
    + + +
    + + + + + + + + languages) > 1) : ?> + + + + + + + + items as $i => $item) : + list($component, $sub_id) = explode('.', $item->template_id, 2); + $sub_id = str_replace('.', '_', $sub_id); + ?> + + + + languages) > 1) : ?> + + + + + + + +
    + , + , + +
    + + + + + + + + + +
    + + + + + + + + + + + template_id; ?> +
    - - pagination->getListFooter(); ?> - + + pagination->getListFooter(); ?> + - - - -
    -
    -
    + + + +
    +
    +
    diff --git a/administrator/components/com_media/helpers/media.php b/administrator/components/com_media/helpers/media.php index 9896de4ab32eb..dbb1ae27d1e22 100644 --- a/administrator/components/com_media/helpers/media.php +++ b/administrator/components/com_media/helpers/media.php @@ -1,4 +1,5 @@ get('adapter'); - $uploadedPath = $mediaObject->get('path'); + $link = 'index.php?option=com_media'; + $adapter = $mediaObject->get('adapter'); + $uploadedPath = $mediaObject->get('path'); - if (!empty($adapter) && !empty($uploadedPath)) - { - $link = $link . '&path=' . $adapter . ':' . $uploadedPath; - } + if (!empty($adapter) && !empty($uploadedPath)) { + $link = $link . '&path=' . $adapter . ':' . $uploadedPath; + } - return $link; - } + return $link; + } } diff --git a/administrator/components/com_media/layouts/toolbar/create-folder.php b/administrator/components/com_media/layouts/toolbar/create-folder.php index d5de1dd66c960..52ddda3bbac77 100644 --- a/administrator/components/com_media/layouts/toolbar/create-folder.php +++ b/administrator/components/com_media/layouts/toolbar/create-folder.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('webcomponent.toolbar-button'); + ->useScript('webcomponent.toolbar-button'); $title = Text::_('COM_MEDIA_CREATE_NEW_FOLDER'); ?> - + diff --git a/administrator/components/com_media/layouts/toolbar/delete.php b/administrator/components/com_media/layouts/toolbar/delete.php index 2fc8cccbc0150..392e0e1f22c83 100644 --- a/administrator/components/com_media/layouts/toolbar/delete.php +++ b/administrator/components/com_media/layouts/toolbar/delete.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('webcomponent.toolbar-button'); + ->useScript('webcomponent.toolbar-button'); $title = Text::_('JTOOLBAR_DELETE'); ?> - + diff --git a/administrator/components/com_media/layouts/toolbar/upload.php b/administrator/components/com_media/layouts/toolbar/upload.php index 988542e2cf3a9..11691117f375c 100644 --- a/administrator/components/com_media/layouts/toolbar/upload.php +++ b/administrator/components/com_media/layouts/toolbar/upload.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('webcomponent.toolbar-button'); + ->useScript('webcomponent.toolbar-button'); $title = Text::_('JTOOLBAR_UPLOAD'); ?> - + diff --git a/administrator/components/com_media/services/provider.php b/administrator/components/com_media/services/provider.php index 53a08de23fd86..dbab9ac0dd137 100644 --- a/administrator/components/com_media/services/provider.php +++ b/administrator/components/com_media/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Media')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Media')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Media')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Media')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_media/src/Adapter/AdapterInterface.php b/administrator/components/com_media/src/Adapter/AdapterInterface.php index a752e347bb232..417a6bc6c3344 100644 --- a/administrator/components/com_media/src/Adapter/AdapterInterface.php +++ b/administrator/components/com_media/src/Adapter/AdapterInterface.php @@ -1,4 +1,5 @@ input->getMethod(); - - $this->task = $task; - $this->method = $method; - - try - { - // Check token for requests which do modify files (all except get requests) - if ($method !== 'GET' && !Session::checkToken('json')) - { - throw new \InvalidArgumentException(Text::_('JINVALID_TOKEN_NOTICE'), 403); - } - - $doTask = strtolower($method) . ucfirst($task); - - // Record the actual task being fired - $this->doTask = $doTask; - - if (!in_array($this->doTask, $this->taskMap)) - { - throw new \Exception(Text::sprintf('JLIB_APPLICATION_ERROR_TASK_NOT_FOUND', $task), 405); - } - - $data = $this->$doTask(); - - // Return the data - $this->sendResponse($data); - } - catch (FileNotFoundException $e) - { - $this->sendResponse($e, 404); - } - catch (FileExistsException $e) - { - $this->sendResponse($e, 409); - } - catch (InvalidPathException $e) - { - $this->sendResponse($e, 400); - } - catch (\Exception $e) - { - $errorCode = 500; - - if ($e->getCode() > 0) - { - $errorCode = $e->getCode(); - } - - $this->sendResponse($e, $errorCode); - } - } - - /** - * Files Get Method - * - * Examples: - * - * - GET a list of folders below the root: - * index.php?option=com_media&task=api.files - * /api/files - * - GET a list of files and subfolders of a given folder: - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia - * /api/files/sampledata/cassiopeia - * - GET a list of files and subfolders of a given folder for a given search term: - * use recursive=1 to search recursively in the working directory - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia&search=nasa5 - * /api/files/sampledata/cassiopeia?search=nasa5 - * To look up in same working directory set flag recursive=0 - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia&search=nasa5&recursive=0 - * /api/files/sampledata/cassiopeia?search=nasa5&recursive=0 - * - GET file information for a specific file: - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg - * /api/files/sampledata/cassiopeia/test.jpg - * - GET a temporary URL to a given file - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg&url=1&temp=1 - * /api/files/sampledata/cassiopeia/test.jpg&url=1&temp=1 - * - GET a temporary URL to a given file - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg&url=1 - * /api/files/sampledata/cassiopeia/test.jpg&url=1 - * - * @return array The data to send with the response - * - * @since 4.0.0 - * @throws \Exception - */ - public function getFiles() - { - // Grab options - $options = []; - $options['url'] = $this->input->getBool('url', false); - $options['search'] = $this->input->getString('search', ''); - $options['recursive'] = $this->input->getBool('recursive', true); - $options['content'] = $this->input->getBool('content', false); - - return $this->getModel()->getFiles($this->getAdapter(), $this->getPath(), $options); - } - - /** - * Files delete Method - * - * Examples: - * - * - DELETE an existing folder in a specific folder: - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test - * /api/files/sampledata/cassiopeia/test - * - DELETE an existing file in a specific folder: - * index.php?option=com_media&task=api.files&path=/sampledata/cassiopeia/test.jpg - * /api/files/sampledata/cassiopeia/test.jpg - * - * @return null - * - * @since 4.0.0 - * @throws \Exception - */ - public function deleteFiles() - { - if (!$this->app->getIdentity()->authorise('core.delete', 'com_media')) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403); - } - - $this->getModel()->delete($this->getAdapter(), $this->getPath()); - - return null; - } - - /** - * Files Post Method - * - * Examples: - * - * - POST a new file or folder into a specific folder, the file or folder information is returned: - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia - * /api/files/sampledata/cassiopeia - * - * New file body: - * { - * "name": "test.jpg", - * "content":"base64 encoded image" - * } - * New folder body: - * { - * "name": "test", - * } - * - * @return array The data to send with the response - * - * @since 4.0.0 - * @throws \Exception - */ - public function postFiles() - { - if (!$this->app->getIdentity()->authorise('core.create', 'com_media')) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED'), 403); - } - - $adapter = $this->getAdapter(); - $path = $this->getPath(); - $content = $this->input->json; - $name = $content->getString('name'); - $mediaContent = base64_decode($content->get('content', '', 'raw')); - $override = $content->get('override', false); - - if ($mediaContent) - { - $this->checkContent(); - - // A file needs to be created - $name = $this->getModel()->createFile($adapter, $name, $path, $mediaContent, $override); - } - else - { - // A file needs to be created - $name = $this->getModel()->createFolder($adapter, $name, $path, $override); - } - - $options = []; - $options['url'] = $this->input->getBool('url', false); - - return $this->getModel()->getFile($adapter, $path . '/' . $name, $options); - } - - /** - * Files Put method - * - * Examples: - * - * - PUT a media file, the file or folder information is returned: - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg - * /api/files/sampledata/cassiopeia/test.jpg - * - * Update file body: - * { - * "content":"base64 encoded image" - * } - * - * - PUT move a file, folder to another one - * path : will be taken as the source - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg - * /api/files/sampledata/cassiopeia/test.jpg - * - * JSON body: - * { - * "newPath" : "/path/to/destination", - * "move" : "1" - * } - * - * - PUT copy a file, folder to another one - * path : will be taken as the source - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg - * /api/files/sampledata/cassiopeia/test.jpg - * - * JSON body: - * { - * "newPath" : "/path/to/destination", - * "move" : "0" - * } - * - * @return array The data to send with the response - * - * @since 4.0.0 - * @throws \Exception - */ - public function putFiles() - { - if (!$this->app->getIdentity()->authorise('core.edit', 'com_media')) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 403); - } - - $adapter = $this->getAdapter(); - $path = $this->getPath(); - - $content = $this->input->json; - $name = basename($path); - $mediaContent = base64_decode($content->get('content', '', 'raw')); - $newPath = $content->getString('newPath', null); - $move = $content->get('move', true); - - if ($mediaContent != null) - { - $this->checkContent(); - - $this->getModel()->updateFile($adapter, $name, str_replace($name, '', $path), $mediaContent); - } - - if ($newPath != null && $newPath !== $adapter . ':' . $path) - { - list($destinationAdapter, $destinationPath) = explode(':', $newPath, 2); - - if ($move) - { - $destinationPath = $this->getModel()->move($adapter, $path, $destinationPath, false); - } - else - { - $destinationPath = $this->getModel()->copy($adapter, $path, $destinationPath, false); - } - - $path = $destinationPath; - } - - return $this->getModel()->getFile($adapter, $path); - } - - /** - * Send the given data as JSON response in the following format: - * - * {"success":true,"message":"ok","messages":null,"data":[{"type":"dir","name":"banners","path":"//"}]} - * - * @param mixed $data The data to send - * @param integer $responseCode The response code - * - * @return void - * - * @since 4.0.0 - */ - private function sendResponse($data = null, int $responseCode = 200) - { - // Set the correct content type - $this->app->setHeader('Content-Type', 'application/json'); - - // Set the status code for the response - http_response_code($responseCode); - - // Send the data - echo new JsonResponse($data); - - $this->app->close(); - } - - /** - * Method to get a model object, loading it if required. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return BaseModel|boolean Model object on success; otherwise false on failure. - * - * @since 4.0.0 - */ - public function getModel($name = 'Api', $prefix = 'Administrator', $config = []) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Performs various checks if it is allowed to save the content. - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - private function checkContent() - { - $helper = new MediaHelper; - $contentLength = $this->input->server->getInt('CONTENT_LENGTH'); - $params = ComponentHelper::getParams('com_media'); - $paramsUploadMaxsize = $params->get('upload_maxsize', 0) * 1024 * 1024; - $uploadMaxFilesize = $helper->toBytes(ini_get('upload_max_filesize')); - $postMaxSize = $helper->toBytes(ini_get('post_max_size')); - $memoryLimit = $helper->toBytes(ini_get('memory_limit')); - - if (($paramsUploadMaxsize > 0 && $contentLength > $paramsUploadMaxsize) - || ($uploadMaxFilesize > 0 && $contentLength > $uploadMaxFilesize) - || ($postMaxSize > 0 && $contentLength > $postMaxSize) - || ($memoryLimit > -1 && $contentLength > $memoryLimit) - ) - { - throw new \Exception(Text::_('COM_MEDIA_ERROR_WARNFILETOOLARGE'), 403); - } - } - - /** - * Get the Adapter. - * - * @return string - * - * @since 4.0.0 - */ - private function getAdapter() - { - $parts = explode(':', $this->input->getString('path', ''), 2); - - if (count($parts) < 1) - { - return null; - } - - return $parts[0]; - } - - /** - * Get the Path. - * - * @return string - * - * @since 4.0.0 - */ - private function getPath() - { - $parts = explode(':', $this->input->getString('path', ''), 2); - - if (count($parts) < 2) - { - return null; - } - - return $parts[1]; - } + /** + * Execute a task by triggering a method in the derived class. + * + * @param string $task The task to perform. If no matching task is found, the '__default' task is executed, if defined. + * + * @return mixed The value returned by the called method. + * + * @since 4.0.0 + * @throws \Exception + */ + public function execute($task) + { + $method = $this->input->getMethod(); + + $this->task = $task; + $this->method = $method; + + try { + // Check token for requests which do modify files (all except get requests) + if ($method !== 'GET' && !Session::checkToken('json')) { + throw new \InvalidArgumentException(Text::_('JINVALID_TOKEN_NOTICE'), 403); + } + + $doTask = strtolower($method) . ucfirst($task); + + // Record the actual task being fired + $this->doTask = $doTask; + + if (!in_array($this->doTask, $this->taskMap)) { + throw new \Exception(Text::sprintf('JLIB_APPLICATION_ERROR_TASK_NOT_FOUND', $task), 405); + } + + $data = $this->$doTask(); + + // Return the data + $this->sendResponse($data); + } catch (FileNotFoundException $e) { + $this->sendResponse($e, 404); + } catch (FileExistsException $e) { + $this->sendResponse($e, 409); + } catch (InvalidPathException $e) { + $this->sendResponse($e, 400); + } catch (\Exception $e) { + $errorCode = 500; + + if ($e->getCode() > 0) { + $errorCode = $e->getCode(); + } + + $this->sendResponse($e, $errorCode); + } + } + + /** + * Files Get Method + * + * Examples: + * + * - GET a list of folders below the root: + * index.php?option=com_media&task=api.files + * /api/files + * - GET a list of files and subfolders of a given folder: + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia + * /api/files/sampledata/cassiopeia + * - GET a list of files and subfolders of a given folder for a given search term: + * use recursive=1 to search recursively in the working directory + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia&search=nasa5 + * /api/files/sampledata/cassiopeia?search=nasa5 + * To look up in same working directory set flag recursive=0 + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia&search=nasa5&recursive=0 + * /api/files/sampledata/cassiopeia?search=nasa5&recursive=0 + * - GET file information for a specific file: + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg + * /api/files/sampledata/cassiopeia/test.jpg + * - GET a temporary URL to a given file + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg&url=1&temp=1 + * /api/files/sampledata/cassiopeia/test.jpg&url=1&temp=1 + * - GET a temporary URL to a given file + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg&url=1 + * /api/files/sampledata/cassiopeia/test.jpg&url=1 + * + * @return array The data to send with the response + * + * @since 4.0.0 + * @throws \Exception + */ + public function getFiles() + { + // Grab options + $options = []; + $options['url'] = $this->input->getBool('url', false); + $options['search'] = $this->input->getString('search', ''); + $options['recursive'] = $this->input->getBool('recursive', true); + $options['content'] = $this->input->getBool('content', false); + + return $this->getModel()->getFiles($this->getAdapter(), $this->getPath(), $options); + } + + /** + * Files delete Method + * + * Examples: + * + * - DELETE an existing folder in a specific folder: + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test + * /api/files/sampledata/cassiopeia/test + * - DELETE an existing file in a specific folder: + * index.php?option=com_media&task=api.files&path=/sampledata/cassiopeia/test.jpg + * /api/files/sampledata/cassiopeia/test.jpg + * + * @return null + * + * @since 4.0.0 + * @throws \Exception + */ + public function deleteFiles() + { + if (!$this->app->getIdentity()->authorise('core.delete', 'com_media')) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403); + } + + $this->getModel()->delete($this->getAdapter(), $this->getPath()); + + return null; + } + + /** + * Files Post Method + * + * Examples: + * + * - POST a new file or folder into a specific folder, the file or folder information is returned: + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia + * /api/files/sampledata/cassiopeia + * + * New file body: + * { + * "name": "test.jpg", + * "content":"base64 encoded image" + * } + * New folder body: + * { + * "name": "test", + * } + * + * @return array The data to send with the response + * + * @since 4.0.0 + * @throws \Exception + */ + public function postFiles() + { + if (!$this->app->getIdentity()->authorise('core.create', 'com_media')) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED'), 403); + } + + $adapter = $this->getAdapter(); + $path = $this->getPath(); + $content = $this->input->json; + $name = $content->getString('name'); + $mediaContent = base64_decode($content->get('content', '', 'raw')); + $override = $content->get('override', false); + + if ($mediaContent) { + $this->checkContent(); + + // A file needs to be created + $name = $this->getModel()->createFile($adapter, $name, $path, $mediaContent, $override); + } else { + // A file needs to be created + $name = $this->getModel()->createFolder($adapter, $name, $path, $override); + } + + $options = []; + $options['url'] = $this->input->getBool('url', false); + + return $this->getModel()->getFile($adapter, $path . '/' . $name, $options); + } + + /** + * Files Put method + * + * Examples: + * + * - PUT a media file, the file or folder information is returned: + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg + * /api/files/sampledata/cassiopeia/test.jpg + * + * Update file body: + * { + * "content":"base64 encoded image" + * } + * + * - PUT move a file, folder to another one + * path : will be taken as the source + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg + * /api/files/sampledata/cassiopeia/test.jpg + * + * JSON body: + * { + * "newPath" : "/path/to/destination", + * "move" : "1" + * } + * + * - PUT copy a file, folder to another one + * path : will be taken as the source + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg + * /api/files/sampledata/cassiopeia/test.jpg + * + * JSON body: + * { + * "newPath" : "/path/to/destination", + * "move" : "0" + * } + * + * @return array The data to send with the response + * + * @since 4.0.0 + * @throws \Exception + */ + public function putFiles() + { + if (!$this->app->getIdentity()->authorise('core.edit', 'com_media')) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 403); + } + + $adapter = $this->getAdapter(); + $path = $this->getPath(); + + $content = $this->input->json; + $name = basename($path); + $mediaContent = base64_decode($content->get('content', '', 'raw')); + $newPath = $content->getString('newPath', null); + $move = $content->get('move', true); + + if ($mediaContent != null) { + $this->checkContent(); + + $this->getModel()->updateFile($adapter, $name, str_replace($name, '', $path), $mediaContent); + } + + if ($newPath != null && $newPath !== $adapter . ':' . $path) { + list($destinationAdapter, $destinationPath) = explode(':', $newPath, 2); + + if ($move) { + $destinationPath = $this->getModel()->move($adapter, $path, $destinationPath, false); + } else { + $destinationPath = $this->getModel()->copy($adapter, $path, $destinationPath, false); + } + + $path = $destinationPath; + } + + return $this->getModel()->getFile($adapter, $path); + } + + /** + * Send the given data as JSON response in the following format: + * + * {"success":true,"message":"ok","messages":null,"data":[{"type":"dir","name":"banners","path":"//"}]} + * + * @param mixed $data The data to send + * @param integer $responseCode The response code + * + * @return void + * + * @since 4.0.0 + */ + private function sendResponse($data = null, int $responseCode = 200) + { + // Set the correct content type + $this->app->setHeader('Content-Type', 'application/json'); + + // Set the status code for the response + http_response_code($responseCode); + + // Send the data + echo new JsonResponse($data); + + $this->app->close(); + } + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return BaseModel|boolean Model object on success; otherwise false on failure. + * + * @since 4.0.0 + */ + public function getModel($name = 'Api', $prefix = 'Administrator', $config = []) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Performs various checks if it is allowed to save the content. + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + private function checkContent() + { + $helper = new MediaHelper(); + $contentLength = $this->input->server->getInt('CONTENT_LENGTH'); + $params = ComponentHelper::getParams('com_media'); + $paramsUploadMaxsize = $params->get('upload_maxsize', 0) * 1024 * 1024; + $uploadMaxFilesize = $helper->toBytes(ini_get('upload_max_filesize')); + $postMaxSize = $helper->toBytes(ini_get('post_max_size')); + $memoryLimit = $helper->toBytes(ini_get('memory_limit')); + + if ( + ($paramsUploadMaxsize > 0 && $contentLength > $paramsUploadMaxsize) + || ($uploadMaxFilesize > 0 && $contentLength > $uploadMaxFilesize) + || ($postMaxSize > 0 && $contentLength > $postMaxSize) + || ($memoryLimit > -1 && $contentLength > $memoryLimit) + ) { + throw new \Exception(Text::_('COM_MEDIA_ERROR_WARNFILETOOLARGE'), 403); + } + } + + /** + * Get the Adapter. + * + * @return string + * + * @since 4.0.0 + */ + private function getAdapter() + { + $parts = explode(':', $this->input->getString('path', ''), 2); + + if (count($parts) < 1) { + return null; + } + + return $parts[0]; + } + + /** + * Get the Path. + * + * @return string + * + * @since 4.0.0 + */ + private function getPath() + { + $parts = explode(':', $this->input->getString('path', ''), 2); + + if (count($parts) < 2) { + return null; + } + + return $parts[1]; + } } diff --git a/administrator/components/com_media/src/Controller/DisplayController.php b/administrator/components/com_media/src/Controller/DisplayController.php index 1afaa99e974bd..a4ee39d28d455 100644 --- a/administrator/components/com_media/src/Controller/DisplayController.php +++ b/administrator/components/com_media/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->getString('plugin', null); - $plugins = PluginHelper::getPlugin('filesystem'); - - // If plugin name was not found in parameters redirect back to control panel - if (!$pluginName || !$this->containsPlugin($plugins, $pluginName)) - { - throw new \Exception('Plugin not found!'); - } - - // Check if the plugin is disabled, if so redirect to control panel - if (!PluginHelper::isEnabled('filesystem', $pluginName)) - { - throw new \Exception('Plugin ' . $pluginName . ' is disabled.'); - } - - // Only import our required plugin, not entire group - PluginHelper::importPlugin('filesystem', $pluginName); - - // Event parameters - $eventParameters = ['context' => $pluginName, 'input' => $this->input]; - $event = new OAuthCallbackEvent('onFileSystemOAuthCallback', $eventParameters); - - // Get results from event - $eventResults = (array) $this->app->triggerEvent('onFileSystemOAuthCallback', $event); - - // If event was not triggered in the selected Plugin, raise a warning and fallback to Control Panel - if (!$eventResults) - { - throw new \Exception( - 'Plugin ' . $pluginName . ' should have implemented onFileSystemOAuthCallback method' - ); - } - - $action = $eventResults['action'] ?? null; - - // If there are any messages display them - if (isset($eventResults['message'])) - { - $message = $eventResults['message']; - $messageType = ($eventResults['message_type'] ?? ''); - - $this->app->enqueueMessage($message, $messageType); - } - - /** - * Execute actions defined by the plugin - * Supported actions - * - close : Closes the current window, use this only for windows opened by javascript - * - redirect : Redirect to a URI defined in 'redirect_uri' parameter, if not fallback to control panel - * - media-manager : Redirect to Media Manager - * - control-panel : Redirect to Control Panel - */ - switch ($action) - { - /** - * Close a window opened by developer - * Use this for close New Windows opened for OAuth Process - */ - case 'close': - $this->setRedirect(Route::_('index.php?option=com_media&view=plugin&action=close', false)); - break; - - // Redirect browser to any page specified by the user - case 'redirect': - if (!isset($eventResults['redirect_uri'])) - { - throw new \Exception("Redirect URI must be set in the plugin"); - } - - $this->setRedirect($eventResults['redirect_uri']); - break; - - // Redirect browser to Control Panel - case 'control-panel': - $this->setRedirect(Route::_('index.php', false)); - break; - - // Redirect browser to Media Manager - case 'media-manager': - default: - $this->setRedirect(Route::_('index.php?option=com_media&view=media', false)); - } - } - catch (\Exception $e) - { - // Display any error - $this->app->enqueueMessage($e->getMessage(), 'error'); - $this->setRedirect(Route::_('index.php', false)); - } - - // Redirect - $this->redirect(); - } - - /** - * Check whether a plugin exists in given plugin array. - * - * @param array $plugins Array of plugin names - * @param string $pluginName Plugin name to look up - * - * @return bool - * - * @since 4.0.0 - */ - private function containsPlugin($plugins, $pluginName) - { - foreach ($plugins as $plugin) - { - if ($plugin->name == $pluginName) - { - return true; - } - } - - return false; - } + /** + * Handles an OAuth Callback request for a specified plugin. + * + * URLs containing [sitename]/administrator/index.php?option=com_media&task=plugin.oauthcallback + * &plugin=[plugin_name] + * + * will be handled by this endpoint. + * It will select the plugin specified by plugin_name and pass all the data received from the provider + * + * @return void + * + * @since 4.0.0 + */ + public function oauthcallback() + { + try { + // Load plugin names + $pluginName = $this->input->getString('plugin', null); + $plugins = PluginHelper::getPlugin('filesystem'); + + // If plugin name was not found in parameters redirect back to control panel + if (!$pluginName || !$this->containsPlugin($plugins, $pluginName)) { + throw new \Exception('Plugin not found!'); + } + + // Check if the plugin is disabled, if so redirect to control panel + if (!PluginHelper::isEnabled('filesystem', $pluginName)) { + throw new \Exception('Plugin ' . $pluginName . ' is disabled.'); + } + + // Only import our required plugin, not entire group + PluginHelper::importPlugin('filesystem', $pluginName); + + // Event parameters + $eventParameters = ['context' => $pluginName, 'input' => $this->input]; + $event = new OAuthCallbackEvent('onFileSystemOAuthCallback', $eventParameters); + + // Get results from event + $eventResults = (array) $this->app->triggerEvent('onFileSystemOAuthCallback', $event); + + // If event was not triggered in the selected Plugin, raise a warning and fallback to Control Panel + if (!$eventResults) { + throw new \Exception( + 'Plugin ' . $pluginName . ' should have implemented onFileSystemOAuthCallback method' + ); + } + + $action = $eventResults['action'] ?? null; + + // If there are any messages display them + if (isset($eventResults['message'])) { + $message = $eventResults['message']; + $messageType = ($eventResults['message_type'] ?? ''); + + $this->app->enqueueMessage($message, $messageType); + } + + /** + * Execute actions defined by the plugin + * Supported actions + * - close : Closes the current window, use this only for windows opened by javascript + * - redirect : Redirect to a URI defined in 'redirect_uri' parameter, if not fallback to control panel + * - media-manager : Redirect to Media Manager + * - control-panel : Redirect to Control Panel + */ + switch ($action) { + /** + * Close a window opened by developer + * Use this for close New Windows opened for OAuth Process + */ + case 'close': + $this->setRedirect(Route::_('index.php?option=com_media&view=plugin&action=close', false)); + break; + + // Redirect browser to any page specified by the user + case 'redirect': + if (!isset($eventResults['redirect_uri'])) { + throw new \Exception("Redirect URI must be set in the plugin"); + } + + $this->setRedirect($eventResults['redirect_uri']); + break; + + // Redirect browser to Control Panel + case 'control-panel': + $this->setRedirect(Route::_('index.php', false)); + break; + + // Redirect browser to Media Manager + case 'media-manager': + default: + $this->setRedirect(Route::_('index.php?option=com_media&view=media', false)); + } + } catch (\Exception $e) { + // Display any error + $this->app->enqueueMessage($e->getMessage(), 'error'); + $this->setRedirect(Route::_('index.php', false)); + } + + // Redirect + $this->redirect(); + } + + /** + * Check whether a plugin exists in given plugin array. + * + * @param array $plugins Array of plugin names + * @param string $pluginName Plugin name to look up + * + * @return bool + * + * @since 4.0.0 + */ + private function containsPlugin($plugins, $pluginName) + { + foreach ($plugins as $plugin) { + if ($plugin->name == $pluginName) { + return true; + } + } + + return false; + } } diff --git a/administrator/components/com_media/src/Dispatcher/Dispatcher.php b/administrator/components/com_media/src/Dispatcher/Dispatcher.php index f405c09d95a08..cfa1e6031d7ab 100644 --- a/administrator/components/com_media/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_media/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getIdentity(); - $asset = $this->input->get('asset'); - $author = $this->input->get('author'); + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + */ + protected function checkAccess() + { + $user = $this->app->getIdentity(); + $asset = $this->input->get('asset'); + $author = $this->input->get('author'); - // Access check - if (!$user->authorise('core.manage', 'com_media') - && (!$asset || (!$user->authorise('core.edit', $asset) - && !$user->authorise('core.create', $asset) - && count($user->getAuthorisedCategories($asset, 'core.create')) == 0) - && !($user->id == $author && $user->authorise('core.edit.own', $asset)))) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + // Access check + if ( + !$user->authorise('core.manage', 'com_media') + && (!$asset || (!$user->authorise('core.edit', $asset) + && !$user->authorise('core.create', $asset) + && count($user->getAuthorisedCategories($asset, 'core.create')) == 0) + && !($user->id == $author && $user->authorise('core.edit.own', $asset))) + ) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_media/src/Event/AbstractMediaItemValidationEvent.php b/administrator/components/com_media/src/Event/AbstractMediaItemValidationEvent.php index 83f2921865cf6..935c9651f7d97 100644 --- a/administrator/components/com_media/src/Event/AbstractMediaItemValidationEvent.php +++ b/administrator/components/com_media/src/Event/AbstractMediaItemValidationEvent.php @@ -1,4 +1,5 @@ type) || ($item->type !== 'dir' && $item->type !== 'file')) - { - throw new \BadMethodCallException("Property 'type' of argument 'item' of event {$this->name} has a wrong item. Valid: 'dir' or 'file'"); - } - - // Non empty string - if (empty($item->name) || !is_string($item->name)) - { - throw new \BadMethodCallException("Property 'name' of argument 'item' of event {$this->name} has a wrong item. Valid: non empty string"); - } - - // Non empty string - if (empty($item->path) || !is_string($item->path)) - { - throw new \BadMethodCallException("Property 'path' of argument 'item' of event {$this->name} has a wrong item. Valid: non empty string"); - } - - // A string - if ($item->type === 'file' && (!isset($item->extension) || !is_string($item->extension))) - { - throw new \BadMethodCallException("Property 'extension' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } - - // An empty string or an integer - if (!isset($item->size) || - (!is_integer($item->size) && !is_string($item->size)) || - (is_string($item->size) && $item->size !== '') - ) - { - throw new \BadMethodCallException("Property 'size' of argument 'item' of event {$this->name} has a wrong item. Valid: empty string or integer"); - } - - // A string - if (!isset($item->mime_type) || !is_string($item->mime_type)) - { - throw new \BadMethodCallException("Property 'mime_type' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } - - // An integer - if (!isset($item->width) || !is_integer($item->width)) - { - throw new \BadMethodCallException("Property 'width' of argument 'item' of event {$this->name} has a wrong item. Valid: integer"); - } - - // An integer - if (!isset($item->height) || !is_integer($item->height)) - { - throw new \BadMethodCallException("Property 'height' of argument 'item' of event {$this->name} has a wrong item. Valid: integer"); - } - - // A string - if (!isset($item->create_date) || !is_string($item->create_date)) - { - throw new \BadMethodCallException("Property 'create_date' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } - - // A string - if (!isset($item->create_date_formatted) || !is_string($item->create_date_formatted)) - { - throw new \BadMethodCallException("Property 'create_date_formatted' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } - - // A string - if (!isset($item->modified_date) || !is_string($item->modified_date)) - { - throw new \BadMethodCallException("Property 'modified_date' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } - - // A string - if (!isset($item->modified_date_formatted) || !is_string($item->modified_date_formatted)) - { - throw new \BadMethodCallException("Property 'modified_date_formatted' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } - } + /** + * Validate $item to have all attributes with a valid type. + * + * Properties validated: + * - type: The type can be file or dir + * - name: The name of the item + * - path: The relative path to the root + * - extension: The file extension + * - size: The size of the file + * - create_date: The date created + * - modified_date: The date modified + * - mime_type: The mime type + * - width: The width, when available + * - height: The height, when available + * + * Properties generated: + * - created_date_formatted: DATE_FORMAT_LC5 formatted string based on create_date + * - modified_date_formatted: DATE_FORMAT_LC5 formatted string based on modified_date + * + * @param \stdClass $item The item to set + * + * @return void + * + * @since 4.1.0 + * + * @throws \BadMethodCallException + */ + protected function validate(\stdClass $item): void + { + // Only "dir" or "file" is allowed + if (!isset($item->type) || ($item->type !== 'dir' && $item->type !== 'file')) { + throw new \BadMethodCallException("Property 'type' of argument 'item' of event {$this->name} has a wrong item. Valid: 'dir' or 'file'"); + } + + // Non empty string + if (empty($item->name) || !is_string($item->name)) { + throw new \BadMethodCallException("Property 'name' of argument 'item' of event {$this->name} has a wrong item. Valid: non empty string"); + } + + // Non empty string + if (empty($item->path) || !is_string($item->path)) { + throw new \BadMethodCallException("Property 'path' of argument 'item' of event {$this->name} has a wrong item. Valid: non empty string"); + } + + // A string + if ($item->type === 'file' && (!isset($item->extension) || !is_string($item->extension))) { + throw new \BadMethodCallException("Property 'extension' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } + + // An empty string or an integer + if ( + !isset($item->size) || + (!is_integer($item->size) && !is_string($item->size)) || + (is_string($item->size) && $item->size !== '') + ) { + throw new \BadMethodCallException("Property 'size' of argument 'item' of event {$this->name} has a wrong item. Valid: empty string or integer"); + } + + // A string + if (!isset($item->mime_type) || !is_string($item->mime_type)) { + throw new \BadMethodCallException("Property 'mime_type' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } + + // An integer + if (!isset($item->width) || !is_integer($item->width)) { + throw new \BadMethodCallException("Property 'width' of argument 'item' of event {$this->name} has a wrong item. Valid: integer"); + } + + // An integer + if (!isset($item->height) || !is_integer($item->height)) { + throw new \BadMethodCallException("Property 'height' of argument 'item' of event {$this->name} has a wrong item. Valid: integer"); + } + + // A string + if (!isset($item->create_date) || !is_string($item->create_date)) { + throw new \BadMethodCallException("Property 'create_date' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } + + // A string + if (!isset($item->create_date_formatted) || !is_string($item->create_date_formatted)) { + throw new \BadMethodCallException("Property 'create_date_formatted' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } + + // A string + if (!isset($item->modified_date) || !is_string($item->modified_date)) { + throw new \BadMethodCallException("Property 'modified_date' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } + + // A string + if (!isset($item->modified_date_formatted) || !is_string($item->modified_date_formatted)) { + throw new \BadMethodCallException("Property 'modified_date_formatted' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } + } } diff --git a/administrator/components/com_media/src/Event/FetchMediaItemEvent.php b/administrator/components/com_media/src/Event/FetchMediaItemEvent.php index 82479be97bc98..34adbc23c6c15 100644 --- a/administrator/components/com_media/src/Event/FetchMediaItemEvent.php +++ b/administrator/components/com_media/src/Event/FetchMediaItemEvent.php @@ -1,4 +1,5 @@ validate($item); + $this->validate($item); - return $item; - } + return $item; + } } diff --git a/administrator/components/com_media/src/Event/FetchMediaItemUrlEvent.php b/administrator/components/com_media/src/Event/FetchMediaItemUrlEvent.php index c137e62507d39..7162e002be893 100644 --- a/administrator/components/com_media/src/Event/FetchMediaItemUrlEvent.php +++ b/administrator/components/com_media/src/Event/FetchMediaItemUrlEvent.php @@ -1,4 +1,5 @@ arguments[$arguments['adapter']] = $arguments['adapter']; - unset($arguments['adapter']); + $this->arguments[$arguments['adapter']] = $arguments['adapter']; + unset($arguments['adapter']); - // Check for required arguments - if (!\array_key_exists('path', $arguments) || !is_string($arguments['path'])) - { - throw new \BadMethodCallException("Argument 'path' of event $name is not of the expected type"); - } + // Check for required arguments + if (!\array_key_exists('path', $arguments) || !is_string($arguments['path'])) { + throw new \BadMethodCallException("Argument 'path' of event $name is not of the expected type"); + } - $this->arguments[$arguments['path']] = $arguments['path']; - unset($arguments['path']); + $this->arguments[$arguments['path']] = $arguments['path']; + unset($arguments['path']); - // Check for required arguments - if (!\array_key_exists('url', $arguments) || !is_string($arguments['url'])) - { - throw new \BadMethodCallException("Argument 'url' of event $name is not of the expected type"); - } + // Check for required arguments + if (!\array_key_exists('url', $arguments) || !is_string($arguments['url'])) { + throw new \BadMethodCallException("Argument 'url' of event $name is not of the expected type"); + } - parent::__construct($name, $arguments); - } + parent::__construct($name, $arguments); + } - /** - * Validate $value to be a string - * - * @param string $value The value to set - * - * @return string - * - * @since 4.1.0 - */ - protected function setUrl(string $value): string - { - return $value; - } + /** + * Validate $value to be a string + * + * @param string $value The value to set + * + * @return string + * + * @since 4.1.0 + */ + protected function setUrl(string $value): string + { + return $value; + } - /** - * Forbid setting $path - * - * @param string $value The value to set - * - * @since 4.1.0 - * - * @throws \BadMethodCallException - */ - protected function setPath(string $value): string - { - throw new \BadMethodCallException('Cannot set the argument "path" of the immutable event ' . $this->name . '.'); - } + /** + * Forbid setting $path + * + * @param string $value The value to set + * + * @since 4.1.0 + * + * @throws \BadMethodCallException + */ + protected function setPath(string $value): string + { + throw new \BadMethodCallException('Cannot set the argument "path" of the immutable event ' . $this->name . '.'); + } - /** - * Forbid setting $path - * - * @param string $value The value to set - * - * @since 4.1.0 - * - * @throws \BadMethodCallException - */ - protected function setAdapter(string $value): string - { - throw new \BadMethodCallException('Cannot set the argument "adapter" of the immutable event ' . $this->name . '.'); - } + /** + * Forbid setting $path + * + * @param string $value The value to set + * + * @since 4.1.0 + * + * @throws \BadMethodCallException + */ + protected function setAdapter(string $value): string + { + throw new \BadMethodCallException('Cannot set the argument "adapter" of the immutable event ' . $this->name . '.'); + } } diff --git a/administrator/components/com_media/src/Event/FetchMediaItemsEvent.php b/administrator/components/com_media/src/Event/FetchMediaItemsEvent.php index e541e8ac617eb..9b533518d9b3c 100644 --- a/administrator/components/com_media/src/Event/FetchMediaItemsEvent.php +++ b/administrator/components/com_media/src/Event/FetchMediaItemsEvent.php @@ -1,4 +1,5 @@ validate($clone); + $this->validate($clone); - $result[] = $clone; - } + $result[] = $clone; + } - return $result; - } + return $result; + } - /** - * Returns the items. - * - * @param array $items The value to set - * - * @return array - * - * @since 4.1.0 - */ - protected function getItems(array $items): array - { - $result = []; + /** + * Returns the items. + * + * @param array $items The value to set + * + * @return array + * + * @since 4.1.0 + */ + protected function getItems(array $items): array + { + $result = []; - foreach($items as $item) - { - $result[] = clone $item; - } + foreach ($items as $item) { + $result[] = clone $item; + } - return $result; - } + return $result; + } } diff --git a/administrator/components/com_media/src/Event/MediaProviderEvent.php b/administrator/components/com_media/src/Event/MediaProviderEvent.php index 8aeb7d60d71cc..d7473a1568663 100644 --- a/administrator/components/com_media/src/Event/MediaProviderEvent.php +++ b/administrator/components/com_media/src/Event/MediaProviderEvent.php @@ -1,4 +1,5 @@ providerManager; - } + /** + * Return the ProviderManager + * + * @return ProviderManager + * + * @since 4.0.0 + */ + public function getProviderManager(): ProviderManager + { + return $this->providerManager; + } - /** - * Set the ProviderManager - * - * @param ProviderManager $providerManager The Provider Manager to be set - * - * @return void - * - * @since 4.0.0 - */ - public function setProviderManager(ProviderManager $providerManager) - { - $this->providerManager = $providerManager; - } + /** + * Set the ProviderManager + * + * @param ProviderManager $providerManager The Provider Manager to be set + * + * @return void + * + * @since 4.0.0 + */ + public function setProviderManager(ProviderManager $providerManager) + { + $this->providerManager = $providerManager; + } } diff --git a/administrator/components/com_media/src/Event/OAuthCallbackEvent.php b/administrator/components/com_media/src/Event/OAuthCallbackEvent.php index d6d612c2565eb..69421f2f84eb9 100644 --- a/administrator/components/com_media/src/Event/OAuthCallbackEvent.php +++ b/administrator/components/com_media/src/Event/OAuthCallbackEvent.php @@ -1,4 +1,5 @@ context; - } + /** + * Get the event context. + * + * @return string + * + * @since 4.0.0 + */ + public function getContext() + { + return $this->context; + } - /** - * Set the event context. - * - * @param string $context Event context - * - * @return void - * - * @since 4.0.0 - */ - public function setContext($context) - { - $this->context = $context; - } + /** + * Set the event context. + * + * @param string $context Event context + * + * @return void + * + * @since 4.0.0 + */ + public function setContext($context) + { + $this->context = $context; + } - /** - * Get the event input. - * - * @return Input - * - * @since 4.0.0 - */ - public function getInput() - { - return $this->input; - } + /** + * Get the event input. + * + * @return Input + * + * @since 4.0.0 + */ + public function getInput() + { + return $this->input; + } - /** - * Set the event input. - * - * @param Input $input Event input - * - * @return void - * - * @since 4.0.0 - */ - public function setInput($input) - { - $this->input = $input; - } + /** + * Set the event input. + * + * @param Input $input Event input + * + * @return void + * + * @since 4.0.0 + */ + public function setInput($input) + { + $this->input = $input; + } } diff --git a/administrator/components/com_media/src/Exception/FileExistsException.php b/administrator/components/com_media/src/Exception/FileExistsException.php index 1e629684b1a0a..c119190192505 100644 --- a/administrator/components/com_media/src/Exception/FileExistsException.php +++ b/administrator/components/com_media/src/Exception/FileExistsException.php @@ -1,4 +1,5 @@ getAdapter($adapter)->getFile($path); - - // Check if it is a media file - if ($file->type == 'file' && !$this->isMediaFile($file->path)) - { - throw new InvalidPathException; - } - - if (isset($options['url']) && $options['url'] && $file->type == 'file') - { - $file->url = $this->getUrl($adapter, $file->path); - } - - if (isset($options['content']) && $options['content'] && $file->type == 'file') - { - $resource = $this->getAdapter($adapter)->getResource($file->path); - - if ($resource) - { - $file->content = base64_encode(stream_get_contents($resource)); - } - } - - $file->path = $adapter . ":" . $file->path; - $file->adapter = $adapter; - - $event = new FetchMediaItemEvent('onFetchMediaItem', ['item' => $file]); - Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); - - return $event->getArgument('item'); - } - - /** - * Returns the folders and files for the given path. More information - * can be found in AdapterInterface::getFiles(). - * - * @param string $adapter The adapter - * @param string $path The folder - * @param array $options The options - * - * @return \stdClass[] - * - * @since 4.0.0 - * @throws \Exception - * @see AdapterInterface::getFile() - */ - public function getFiles($adapter, $path = '/', $options = []) - { - // Check whether user searching - if ($options['search'] != null) - { - // Do search - $files = $this->search($adapter, $options['search'], $path, $options['recursive']); - } - else - { - // Grab files for the path - $files = $this->getAdapter($adapter)->getFiles($path); - } - - // Add adapter prefix to all the files to be returned - foreach ($files as $key => $file) - { - // Check if the file is valid - if ($file->type == 'file' && !$this->isMediaFile($file->path)) - { - // Remove the file from the data - unset($files[$key]); - continue; - } - - // Check if we need more information - if (isset($options['url']) && $options['url'] && $file->type == 'file') - { - $file->url = $this->getUrl($adapter, $file->path); - } - - if (isset($options['content']) && $options['content'] && $file->type == 'file') - { - $resource = $this->getAdapter($adapter)->getResource($file->path); - - if ($resource) - { - $file->content = base64_encode(stream_get_contents($resource)); - } - } - - $file->path = $adapter . ":" . $file->path; - $file->adapter = $adapter; - } - - // Make proper indexes - $files = array_values($files); - - $event = new FetchMediaItemsEvent('onFetchMediaItems', ['items' => $files]); - Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); - - return $event->getArgument('items'); - } - - /** - * Creates a folder with the given name in the given path. More information - * can be found in AdapterInterface::createFolder(). - * - * @param string $adapter The adapter - * @param string $name The name - * @param string $path The folder - * @param boolean $override Should the folder being overridden when it exists - * - * @return string - * - * @since 4.0.0 - * @throws \Exception - * @see AdapterInterface::createFolder() - */ - public function createFolder($adapter, $name, $path, $override) - { - try - { - $file = $this->getFile($adapter, $path . '/' . $name); - } - catch (FileNotFoundException $e) - { - // Do nothing - } - - // Check if the file exists - if (isset($file) && !$override) - { - throw new FileExistsException; - } - - $app = Factory::getApplication(); - $object = new CMSObject; - $object->adapter = $adapter; - $object->name = $name; - $object->path = $path; - - PluginHelper::importPlugin('content'); - - $result = $app->triggerEvent('onContentBeforeSave', ['com_media.folder', $object, true, $object]); - - if (in_array(false, $result, true)) - { - throw new \Exception($object->getError()); - } - - $object->name = $this->getAdapter($object->adapter)->createFolder($object->name, $object->path); - - $app->triggerEvent('onContentAfterSave', ['com_media.folder', $object, true, $object]); - - return $object->name; - } - - /** - * Creates a file with the given name in the given path with the data. More information - * can be found in AdapterInterface::createFile(). - * - * @param string $adapter The adapter - * @param string $name The name - * @param string $path The folder - * @param string $data The data - * @param boolean $override Should the file being overridden when it exists - * - * @return string - * - * @since 4.0.0 - * @throws \Exception - * @see AdapterInterface::createFile() - */ - public function createFile($adapter, $name, $path, $data, $override) - { - try - { - $file = $this->getFile($adapter, $path . '/' . $name); - } - catch (FileNotFoundException $e) - { - // Do nothing - } - - // Check if the file exists - if (isset($file) && !$override) - { - throw new FileExistsException; - } - - // Check if it is a media file - if (!$this->isMediaFile($path . '/' . $name)) - { - throw new InvalidPathException; - } - - $app = Factory::getApplication(); - $object = new CMSObject; - $object->adapter = $adapter; - $object->name = $name; - $object->path = $path; - $object->data = $data; - $object->extension = strtolower(File::getExt($name)); - - PluginHelper::importPlugin('content'); - - // Also include the filesystem plugins, perhaps they support batch processing too - PluginHelper::importPlugin('media-action'); - - $result = $app->triggerEvent('onContentBeforeSave', ['com_media.file', $object, true, $object]); - - if (in_array(false, $result, true)) - { - throw new \Exception($object->getError()); - } - - $object->name = $this->getAdapter($object->adapter)->createFile($object->name, $object->path, $object->data); - - $app->triggerEvent('onContentAfterSave', ['com_media.file', $object, true, $object]); - - return $object->name; - } - - /** - * Updates the file with the given name in the given path with the data. More information - * can be found in AdapterInterface::updateFile(). - * - * @param string $adapter The adapter - * @param string $name The name - * @param string $path The folder - * @param string $data The data - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - * @see AdapterInterface::updateFile() - */ - public function updateFile($adapter, $name, $path, $data) - { - // Check if it is a media file - if (!$this->isMediaFile($path . '/' . $name)) - { - throw new InvalidPathException; - } - - $app = Factory::getApplication(); - $object = new CMSObject; - $object->adapter = $adapter; - $object->name = $name; - $object->path = $path; - $object->data = $data; - $object->extension = strtolower(File::getExt($name)); - - PluginHelper::importPlugin('content'); - - // Also include the filesystem plugins, perhaps they support batch processing too - PluginHelper::importPlugin('media-action'); - - $result = $app->triggerEvent('onContentBeforeSave', ['com_media.file', $object, false, $object]); - - if (in_array(false, $result, true)) - { - throw new \Exception($object->getError()); - } - - $this->getAdapter($object->adapter)->updateFile($object->name, $object->path, $object->data); - - $app->triggerEvent('onContentAfterSave', ['com_media.file', $object, false, $object]); - } - - /** - * Deletes the folder or file of the given path. More information - * can be found in AdapterInterface::delete(). - * - * @param string $adapter The adapter - * @param string $path The path to the file or folder - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - * @see AdapterInterface::delete() - */ - public function delete($adapter, $path) - { - $file = $this->getFile($adapter, $path); - - // Check if it is a media file - if ($file->type == 'file' && !$this->isMediaFile($file->path)) - { - throw new InvalidPathException; - } - - $type = $file->type === 'file' ? 'file' : 'folder'; - $app = Factory::getApplication(); - $object = new CMSObject; - $object->adapter = $adapter; - $object->path = $path; - - PluginHelper::importPlugin('content'); - - // Also include the filesystem plugins, perhaps they support batch processing too - PluginHelper::importPlugin('media-action'); - - $result = $app->triggerEvent('onContentBeforeDelete', ['com_media.' . $type, $object]); - - if (in_array(false, $result, true)) - { - throw new \Exception($object->getError()); - } - - $this->getAdapter($object->adapter)->delete($object->path); - - $app->triggerEvent('onContentAfterDelete', ['com_media.' . $type, $object]); - } - - /** - * Copies file or folder from source path to destination path - * If forced, existing files/folders would be overwritten - * - * @param string $adapter The adapter - * @param string $sourcePath Source path of the file or folder (relative) - * @param string $destinationPath Destination path(relative) - * @param bool $force Force to overwrite - * - * @return string - * - * @since 4.0.0 - * @throws \Exception - */ - public function copy($adapter, $sourcePath, $destinationPath, $force = false) - { - return $this->getAdapter($adapter)->copy($sourcePath, $destinationPath, $force); - } - - /** - * Moves file or folder from source path to destination path - * If forced, existing files/folders would be overwritten - * - * @param string $adapter The adapter - * @param string $sourcePath Source path of the file or folder (relative) - * @param string $destinationPath Destination path(relative) - * @param bool $force Force to overwrite - * - * @return string - * - * @since 4.0.0 - * @throws \Exception - */ - public function move($adapter, $sourcePath, $destinationPath, $force = false) - { - return $this->getAdapter($adapter)->move($sourcePath, $destinationPath, $force); - } - - /** - * Returns a url for serve media files from adapter. - * Url must provide a valid image type to be displayed on Joomla! site. - * - * @param string $adapter The adapter - * @param string $path The relative path for the file - * - * @return string Permalink to the relative file - * - * @since 4.0.0 - * @throws FileNotFoundException - */ - public function getUrl($adapter, $path) - { - // Check if it is a media file - if (!$this->isMediaFile($path)) - { - throw new InvalidPathException; - } - - $url = $this->getAdapter($adapter)->getUrl($path); - - $event = new FetchMediaItemUrlEvent('onFetchMediaFileUrl', ['adapter' => $adapter, 'path' => $path, 'url' => $url]); - Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); - - return $event->getArgument('url'); - } - - /** - * Search for a pattern in a given path - * - * @param string $adapter The adapter to work on - * @param string $needle The search therm - * @param string $path The base path for the search - * @param bool $recursive Do a recursive search - * - * @return \stdClass[] - * - * @since 4.0.0 - * @throws \Exception - */ - public function search($adapter, $needle, $path = '/', $recursive = true) - { - return $this->getAdapter($adapter)->search($path, $needle, $recursive); - } - - /** - * Checks if the given path is an allowed media file. - * - * @param string $path The path to file - * - * @return boolean - * - * @since 4.0.0 - */ - private function isMediaFile($path) - { - // Check if there is an extension available - if (!strrpos($path, '.')) - { - return false; - } - - // Initialize the allowed extensions - if ($this->allowedExtensions === null) - { - // Get options from the input or fallback to images only - $mediaTypes = explode(',', Factory::getApplication()->input->getString('mediatypes', '0')); - $types = []; - $extensions = []; - - // Default to showing all supported formats - if (count($mediaTypes) === 0) { - $mediaTypes = ['0', '1', '2', '3']; - } - - array_map( - function ($mediaType) use (&$types) { - switch ($mediaType) { - case '0': - $types[] = 'images'; - break; - case '1': - $types[] = 'audios'; - break; - case '2': - $types[] = 'videos'; - break; - case '3': - $types[] = 'documents'; - break; - default: - break; - } - }, - $mediaTypes - ); - - $images = array_map( - 'trim', - explode( - ',', - ComponentHelper::getParams('com_media')->get( - 'image_extensions', - 'bmp,gif,jpg,jpeg,png,webp' - ) - ) - ); - $audios = array_map( - 'trim', - explode( - ',', - ComponentHelper::getParams('com_media')->get( - 'audio_extensions', - 'mp3,m4a,mp4a,ogg' - ) - ) - ); - $videos = array_map( - 'trim', - explode( - ',', - ComponentHelper::getParams('com_media')->get( - 'video_extensions', - 'mp4,mp4v,mpeg,mov,webm' - ) - ) - ); - $documents = array_map( - 'trim', - explode( - ',', - ComponentHelper::getParams('com_media')->get( - 'doc_extensions', - 'doc,odg,odp,ods,odt,pdf,ppt,txt,xcf,xls,csv' - ) - ) - ); - - foreach ($types as $type) { - if (in_array($type, ['images', 'audios', 'videos', 'documents'])) { - $extensions = array_merge($extensions, ${$type}); - } - } - - // Make them an array - $this->allowedExtensions = $extensions; - } - - // Extract the extension - $extension = strtolower(substr($path, strrpos($path, '.') + 1)); - - // Check if the extension exists in the allowed extensions - return in_array($extension, $this->allowedExtensions); - } + use ProviderManagerHelperTrait; + + /** + * The available extensions. + * + * @var string[] + * @since 4.0.0 + */ + private $allowedExtensions = null; + + /** + * Returns the requested file or folder information. More information + * can be found in AdapterInterface::getFile(). + * + * @param string $adapter The adapter + * @param string $path The path to the file or folder + * @param array $options The options + * + * @return \stdClass + * + * @since 4.0.0 + * @throws \Exception + * @see AdapterInterface::getFile() + */ + public function getFile($adapter, $path = '/', $options = []) + { + // Add adapter prefix to the file returned + $file = $this->getAdapter($adapter)->getFile($path); + + // Check if it is a media file + if ($file->type == 'file' && !$this->isMediaFile($file->path)) { + throw new InvalidPathException(); + } + + if (isset($options['url']) && $options['url'] && $file->type == 'file') { + $file->url = $this->getUrl($adapter, $file->path); + } + + if (isset($options['content']) && $options['content'] && $file->type == 'file') { + $resource = $this->getAdapter($adapter)->getResource($file->path); + + if ($resource) { + $file->content = base64_encode(stream_get_contents($resource)); + } + } + + $file->path = $adapter . ":" . $file->path; + $file->adapter = $adapter; + + $event = new FetchMediaItemEvent('onFetchMediaItem', ['item' => $file]); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + + return $event->getArgument('item'); + } + + /** + * Returns the folders and files for the given path. More information + * can be found in AdapterInterface::getFiles(). + * + * @param string $adapter The adapter + * @param string $path The folder + * @param array $options The options + * + * @return \stdClass[] + * + * @since 4.0.0 + * @throws \Exception + * @see AdapterInterface::getFile() + */ + public function getFiles($adapter, $path = '/', $options = []) + { + // Check whether user searching + if ($options['search'] != null) { + // Do search + $files = $this->search($adapter, $options['search'], $path, $options['recursive']); + } else { + // Grab files for the path + $files = $this->getAdapter($adapter)->getFiles($path); + } + + // Add adapter prefix to all the files to be returned + foreach ($files as $key => $file) { + // Check if the file is valid + if ($file->type == 'file' && !$this->isMediaFile($file->path)) { + // Remove the file from the data + unset($files[$key]); + continue; + } + + // Check if we need more information + if (isset($options['url']) && $options['url'] && $file->type == 'file') { + $file->url = $this->getUrl($adapter, $file->path); + } + + if (isset($options['content']) && $options['content'] && $file->type == 'file') { + $resource = $this->getAdapter($adapter)->getResource($file->path); + + if ($resource) { + $file->content = base64_encode(stream_get_contents($resource)); + } + } + + $file->path = $adapter . ":" . $file->path; + $file->adapter = $adapter; + } + + // Make proper indexes + $files = array_values($files); + + $event = new FetchMediaItemsEvent('onFetchMediaItems', ['items' => $files]); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + + return $event->getArgument('items'); + } + + /** + * Creates a folder with the given name in the given path. More information + * can be found in AdapterInterface::createFolder(). + * + * @param string $adapter The adapter + * @param string $name The name + * @param string $path The folder + * @param boolean $override Should the folder being overridden when it exists + * + * @return string + * + * @since 4.0.0 + * @throws \Exception + * @see AdapterInterface::createFolder() + */ + public function createFolder($adapter, $name, $path, $override) + { + try { + $file = $this->getFile($adapter, $path . '/' . $name); + } catch (FileNotFoundException $e) { + // Do nothing + } + + // Check if the file exists + if (isset($file) && !$override) { + throw new FileExistsException(); + } + + $app = Factory::getApplication(); + $object = new CMSObject(); + $object->adapter = $adapter; + $object->name = $name; + $object->path = $path; + + PluginHelper::importPlugin('content'); + + $result = $app->triggerEvent('onContentBeforeSave', ['com_media.folder', $object, true, $object]); + + if (in_array(false, $result, true)) { + throw new \Exception($object->getError()); + } + + $object->name = $this->getAdapter($object->adapter)->createFolder($object->name, $object->path); + + $app->triggerEvent('onContentAfterSave', ['com_media.folder', $object, true, $object]); + + return $object->name; + } + + /** + * Creates a file with the given name in the given path with the data. More information + * can be found in AdapterInterface::createFile(). + * + * @param string $adapter The adapter + * @param string $name The name + * @param string $path The folder + * @param string $data The data + * @param boolean $override Should the file being overridden when it exists + * + * @return string + * + * @since 4.0.0 + * @throws \Exception + * @see AdapterInterface::createFile() + */ + public function createFile($adapter, $name, $path, $data, $override) + { + try { + $file = $this->getFile($adapter, $path . '/' . $name); + } catch (FileNotFoundException $e) { + // Do nothing + } + + // Check if the file exists + if (isset($file) && !$override) { + throw new FileExistsException(); + } + + // Check if it is a media file + if (!$this->isMediaFile($path . '/' . $name)) { + throw new InvalidPathException(); + } + + $app = Factory::getApplication(); + $object = new CMSObject(); + $object->adapter = $adapter; + $object->name = $name; + $object->path = $path; + $object->data = $data; + $object->extension = strtolower(File::getExt($name)); + + PluginHelper::importPlugin('content'); + + // Also include the filesystem plugins, perhaps they support batch processing too + PluginHelper::importPlugin('media-action'); + + $result = $app->triggerEvent('onContentBeforeSave', ['com_media.file', $object, true, $object]); + + if (in_array(false, $result, true)) { + throw new \Exception($object->getError()); + } + + $object->name = $this->getAdapter($object->adapter)->createFile($object->name, $object->path, $object->data); + + $app->triggerEvent('onContentAfterSave', ['com_media.file', $object, true, $object]); + + return $object->name; + } + + /** + * Updates the file with the given name in the given path with the data. More information + * can be found in AdapterInterface::updateFile(). + * + * @param string $adapter The adapter + * @param string $name The name + * @param string $path The folder + * @param string $data The data + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + * @see AdapterInterface::updateFile() + */ + public function updateFile($adapter, $name, $path, $data) + { + // Check if it is a media file + if (!$this->isMediaFile($path . '/' . $name)) { + throw new InvalidPathException(); + } + + $app = Factory::getApplication(); + $object = new CMSObject(); + $object->adapter = $adapter; + $object->name = $name; + $object->path = $path; + $object->data = $data; + $object->extension = strtolower(File::getExt($name)); + + PluginHelper::importPlugin('content'); + + // Also include the filesystem plugins, perhaps they support batch processing too + PluginHelper::importPlugin('media-action'); + + $result = $app->triggerEvent('onContentBeforeSave', ['com_media.file', $object, false, $object]); + + if (in_array(false, $result, true)) { + throw new \Exception($object->getError()); + } + + $this->getAdapter($object->adapter)->updateFile($object->name, $object->path, $object->data); + + $app->triggerEvent('onContentAfterSave', ['com_media.file', $object, false, $object]); + } + + /** + * Deletes the folder or file of the given path. More information + * can be found in AdapterInterface::delete(). + * + * @param string $adapter The adapter + * @param string $path The path to the file or folder + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + * @see AdapterInterface::delete() + */ + public function delete($adapter, $path) + { + $file = $this->getFile($adapter, $path); + + // Check if it is a media file + if ($file->type == 'file' && !$this->isMediaFile($file->path)) { + throw new InvalidPathException(); + } + + $type = $file->type === 'file' ? 'file' : 'folder'; + $app = Factory::getApplication(); + $object = new CMSObject(); + $object->adapter = $adapter; + $object->path = $path; + + PluginHelper::importPlugin('content'); + + // Also include the filesystem plugins, perhaps they support batch processing too + PluginHelper::importPlugin('media-action'); + + $result = $app->triggerEvent('onContentBeforeDelete', ['com_media.' . $type, $object]); + + if (in_array(false, $result, true)) { + throw new \Exception($object->getError()); + } + + $this->getAdapter($object->adapter)->delete($object->path); + + $app->triggerEvent('onContentAfterDelete', ['com_media.' . $type, $object]); + } + + /** + * Copies file or folder from source path to destination path + * If forced, existing files/folders would be overwritten + * + * @param string $adapter The adapter + * @param string $sourcePath Source path of the file or folder (relative) + * @param string $destinationPath Destination path(relative) + * @param bool $force Force to overwrite + * + * @return string + * + * @since 4.0.0 + * @throws \Exception + */ + public function copy($adapter, $sourcePath, $destinationPath, $force = false) + { + return $this->getAdapter($adapter)->copy($sourcePath, $destinationPath, $force); + } + + /** + * Moves file or folder from source path to destination path + * If forced, existing files/folders would be overwritten + * + * @param string $adapter The adapter + * @param string $sourcePath Source path of the file or folder (relative) + * @param string $destinationPath Destination path(relative) + * @param bool $force Force to overwrite + * + * @return string + * + * @since 4.0.0 + * @throws \Exception + */ + public function move($adapter, $sourcePath, $destinationPath, $force = false) + { + return $this->getAdapter($adapter)->move($sourcePath, $destinationPath, $force); + } + + /** + * Returns a url for serve media files from adapter. + * Url must provide a valid image type to be displayed on Joomla! site. + * + * @param string $adapter The adapter + * @param string $path The relative path for the file + * + * @return string Permalink to the relative file + * + * @since 4.0.0 + * @throws FileNotFoundException + */ + public function getUrl($adapter, $path) + { + // Check if it is a media file + if (!$this->isMediaFile($path)) { + throw new InvalidPathException(); + } + + $url = $this->getAdapter($adapter)->getUrl($path); + + $event = new FetchMediaItemUrlEvent('onFetchMediaFileUrl', ['adapter' => $adapter, 'path' => $path, 'url' => $url]); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + + return $event->getArgument('url'); + } + + /** + * Search for a pattern in a given path + * + * @param string $adapter The adapter to work on + * @param string $needle The search therm + * @param string $path The base path for the search + * @param bool $recursive Do a recursive search + * + * @return \stdClass[] + * + * @since 4.0.0 + * @throws \Exception + */ + public function search($adapter, $needle, $path = '/', $recursive = true) + { + return $this->getAdapter($adapter)->search($path, $needle, $recursive); + } + + /** + * Checks if the given path is an allowed media file. + * + * @param string $path The path to file + * + * @return boolean + * + * @since 4.0.0 + */ + private function isMediaFile($path) + { + // Check if there is an extension available + if (!strrpos($path, '.')) { + return false; + } + + // Initialize the allowed extensions + if ($this->allowedExtensions === null) { + // Get options from the input or fallback to images only + $mediaTypes = explode(',', Factory::getApplication()->input->getString('mediatypes', '0')); + $types = []; + $extensions = []; + + // Default to showing all supported formats + if (count($mediaTypes) === 0) { + $mediaTypes = ['0', '1', '2', '3']; + } + + array_map( + function ($mediaType) use (&$types) { + switch ($mediaType) { + case '0': + $types[] = 'images'; + break; + case '1': + $types[] = 'audios'; + break; + case '2': + $types[] = 'videos'; + break; + case '3': + $types[] = 'documents'; + break; + default: + break; + } + }, + $mediaTypes + ); + + $images = array_map( + 'trim', + explode( + ',', + ComponentHelper::getParams('com_media')->get( + 'image_extensions', + 'bmp,gif,jpg,jpeg,png,webp' + ) + ) + ); + $audios = array_map( + 'trim', + explode( + ',', + ComponentHelper::getParams('com_media')->get( + 'audio_extensions', + 'mp3,m4a,mp4a,ogg' + ) + ) + ); + $videos = array_map( + 'trim', + explode( + ',', + ComponentHelper::getParams('com_media')->get( + 'video_extensions', + 'mp4,mp4v,mpeg,mov,webm' + ) + ) + ); + $documents = array_map( + 'trim', + explode( + ',', + ComponentHelper::getParams('com_media')->get( + 'doc_extensions', + 'doc,odg,odp,ods,odt,pdf,ppt,txt,xcf,xls,csv' + ) + ) + ); + + foreach ($types as $type) { + if (in_array($type, ['images', 'audios', 'videos', 'documents'])) { + $extensions = array_merge($extensions, ${$type}); + } + } + + // Make them an array + $this->allowedExtensions = $extensions; + } + + // Extract the extension + $extension = strtolower(substr($path, strrpos($path, '.') + 1)); + + // Check if the extension exists in the allowed extensions + return in_array($extension, $this->allowedExtensions); + } } diff --git a/administrator/components/com_media/src/Model/FileModel.php b/administrator/components/com_media/src/Model/FileModel.php index eb2e5c26fcece..3047939ee9991 100644 --- a/administrator/components/com_media/src/Model/FileModel.php +++ b/administrator/components/com_media/src/Model/FileModel.php @@ -1,4 +1,5 @@ loadForm('com_media.file', 'file', ['control' => 'jform', 'load_data' => $loadData]); + // Get the form. + $form = $this->loadForm('com_media.file', 'file', ['control' => 'jform', 'load_data' => $loadData]); - if (empty($form)) - { - return false; - } + if (empty($form)) { + return false; + } - return $form; - } + return $form; + } - /** - * Method to get the file information for the given path. Path must be - * in the format: adapter:path/to/file.extension - * - * @param string $path The path to get the information from. - * - * @return \stdClass An object with file information - * - * @since 4.0.0 - * @see ApiModel::getFile() - */ - public function getFileInformation($path) - { - list($adapter, $path) = explode(':', $path, 2); + /** + * Method to get the file information for the given path. Path must be + * in the format: adapter:path/to/file.extension + * + * @param string $path The path to get the information from. + * + * @return \stdClass An object with file information + * + * @since 4.0.0 + * @see ApiModel::getFile() + */ + public function getFileInformation($path) + { + list($adapter, $path) = explode(':', $path, 2); - return $this->bootComponent('com_media')->getMVCFactory()->createModel('Api', 'Administrator') - ->getFile($adapter, $path, ['url' => true, 'content' => true]); - } + return $this->bootComponent('com_media')->getMVCFactory()->createModel('Api', 'Administrator') + ->getFile($adapter, $path, ['url' => true, 'content' => true]); + } } diff --git a/administrator/components/com_media/src/Model/MediaModel.php b/administrator/components/com_media/src/Model/MediaModel.php index 257113bc153fe..08cbeac4066fa 100644 --- a/administrator/components/com_media/src/Model/MediaModel.php +++ b/administrator/components/com_media/src/Model/MediaModel.php @@ -1,4 +1,5 @@ getProviderManager()->getProviders() as $provider) - { - $result = new \stdClass; - $result->name = $provider->getID(); - $result->displayName = $provider->getDisplayName(); - $result->adapterNames = []; - - foreach ($provider->getAdapters() as $adapter) - { - $result->adapterNames[] = $adapter->getAdapterName(); - } - - $results[] = $result; - } - - return $results; - } + use ProviderManagerHelperTrait; + + /** + * Obtain list of supported providers + * + * @return array + * + * @since 4.0.0 + */ + public function getProviders() + { + $results = []; + + foreach ($this->getProviderManager()->getProviders() as $provider) { + $result = new \stdClass(); + $result->name = $provider->getID(); + $result->displayName = $provider->getDisplayName(); + $result->adapterNames = []; + + foreach ($provider->getAdapters() as $adapter) { + $result->adapterNames[] = $adapter->getAdapterName(); + } + + $results[] = $result; + } + + return $results; + } } diff --git a/administrator/components/com_media/src/Plugin/MediaActionPlugin.php b/administrator/components/com_media/src/Plugin/MediaActionPlugin.php index 830aeec699147..4894d7d167ef5 100644 --- a/administrator/components/com_media/src/Plugin/MediaActionPlugin.php +++ b/administrator/components/com_media/src/Plugin/MediaActionPlugin.php @@ -1,4 +1,5 @@ getName() != 'com_media.file') - { - return; - } + /** + * The form event. Load additional parameters when available into the field form. + * Only when the type of the form is of interest. + * + * @param Form $form The form + * @param \stdClass $data The data + * + * @return void + * + * @since 4.0.0 + */ + public function onContentPrepareForm(Form $form, $data) + { + // Check if it is the right form + if ($form->getName() != 'com_media.file') { + return; + } - $this->loadCss(); - $this->loadJs(); + $this->loadCss(); + $this->loadJs(); - // The file with the params for the edit view - $paramsFile = JPATH_PLUGINS . '/media-action/' . $this->_name . '/form/' . $this->_name . '.xml'; + // The file with the params for the edit view + $paramsFile = JPATH_PLUGINS . '/media-action/' . $this->_name . '/form/' . $this->_name . '.xml'; - // When the file exists, load it into the form - if (file_exists($paramsFile)) - { - $form->loadFile($paramsFile); - } - } + // When the file exists, load it into the form + if (file_exists($paramsFile)) { + $form->loadFile($paramsFile); + } + } - /** - * Load the javascript files of the plugin. - * - * @return void - * - * @since 4.0.0 - */ - protected function loadJs() - { - HTMLHelper::_( - 'script', - 'plg_media-action_' . $this->_name . '/' . $this->_name . '.js', - ['version' => 'auto', 'relative' => true], - ['type' => 'module'] - ); - } + /** + * Load the javascript files of the plugin. + * + * @return void + * + * @since 4.0.0 + */ + protected function loadJs() + { + HTMLHelper::_( + 'script', + 'plg_media-action_' . $this->_name . '/' . $this->_name . '.js', + ['version' => 'auto', 'relative' => true], + ['type' => 'module'] + ); + } - /** - * Load the CSS files of the plugin. - * - * @return void - * - * @since 4.0.0 - */ - protected function loadCss() - { - HTMLHelper::_( - 'stylesheet', - 'plg_media-action_' . $this->_name . '/' . $this->_name . '.css', - ['version' => 'auto', 'relative' => true] - ); - } + /** + * Load the CSS files of the plugin. + * + * @return void + * + * @since 4.0.0 + */ + protected function loadCss() + { + HTMLHelper::_( + 'stylesheet', + 'plg_media-action_' . $this->_name . '/' . $this->_name . '.css', + ['version' => 'auto', 'relative' => true] + ); + } } diff --git a/administrator/components/com_media/src/Provider/ProviderInterface.php b/administrator/components/com_media/src/Provider/ProviderInterface.php index 13a270c14b783..f461cbf4a658e 100644 --- a/administrator/components/com_media/src/Provider/ProviderInterface.php +++ b/administrator/components/com_media/src/Provider/ProviderInterface.php @@ -1,4 +1,5 @@ providers; - } - - /** - * Register a provider into the ProviderManager - * - * @param ProviderInterface $provider The provider to be registered - * - * @return void - * - * @since 4.0.0 - */ - public function registerProvider(ProviderInterface $provider) - { - $this->providers[$provider->getID()] = $provider; - } - - /** - * Unregister a provider from the ProviderManager. - * When no provider, or null is passed in, then all providers are cleared. - * - * @param ProviderInterface|null $provider The provider to be unregistered - * - * @return void - * - * @since 4.0.6 - */ - public function unregisterProvider(ProviderInterface $provider = null): void - { - if ($provider === null) - { - $this->providers = []; - return; - } - - if (!array_key_exists($provider->getID(), $this->providers)) - { - return; - } - - unset($this->providers[$provider->getID()]); - } - - /** - * Returns the provider for a particular ID - * - * @param string $id The ID for the provider - * - * @return ProviderInterface - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function getProvider($id) - { - if (!isset($this->providers[$id])) - { - throw new \Exception(Text::_('COM_MEDIA_ERROR_MEDIA_PROVIDER_NOT_FOUND')); - } - - return $this->providers[$id]; - } - - /** - * Returns an adapter for an account - * - * @param string $name The name of an adapter - * - * @return AdapterInterface - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function getAdapter($name) - { - list($provider, $account) = array_pad(explode('-', $name, 2), 2, null); - - if ($account == null) - { - throw new \Exception(Text::_('COM_MEDIA_ERROR_ACCOUNT_NOT_SET')); - } - - $adapters = $this->getProvider($provider)->getAdapters(); - - if (!isset($adapters[$account])) - { - throw new \Exception(Text::_('COM_MEDIA_ERROR_ACCOUNT_NOT_FOUND')); - } - - return $adapters[$account]; - } + /** + * The array of providers + * + * @var ProviderInterface[] + * + * @since 4.0.0 + */ + private $providers = []; + + /** + * Returns an associative array of adapters with provider name as the key + * + * @return ProviderInterface[] + * + * @since 4.0.0 + */ + public function getProviders() + { + return $this->providers; + } + + /** + * Register a provider into the ProviderManager + * + * @param ProviderInterface $provider The provider to be registered + * + * @return void + * + * @since 4.0.0 + */ + public function registerProvider(ProviderInterface $provider) + { + $this->providers[$provider->getID()] = $provider; + } + + /** + * Unregister a provider from the ProviderManager. + * When no provider, or null is passed in, then all providers are cleared. + * + * @param ProviderInterface|null $provider The provider to be unregistered + * + * @return void + * + * @since 4.0.6 + */ + public function unregisterProvider(ProviderInterface $provider = null): void + { + if ($provider === null) { + $this->providers = []; + return; + } + + if (!array_key_exists($provider->getID(), $this->providers)) { + return; + } + + unset($this->providers[$provider->getID()]); + } + + /** + * Returns the provider for a particular ID + * + * @param string $id The ID for the provider + * + * @return ProviderInterface + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function getProvider($id) + { + if (!isset($this->providers[$id])) { + throw new \Exception(Text::_('COM_MEDIA_ERROR_MEDIA_PROVIDER_NOT_FOUND')); + } + + return $this->providers[$id]; + } + + /** + * Returns an adapter for an account + * + * @param string $name The name of an adapter + * + * @return AdapterInterface + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function getAdapter($name) + { + list($provider, $account) = array_pad(explode('-', $name, 2), 2, null); + + if ($account == null) { + throw new \Exception(Text::_('COM_MEDIA_ERROR_ACCOUNT_NOT_SET')); + } + + $adapters = $this->getProvider($provider)->getAdapters(); + + if (!isset($adapters[$account])) { + throw new \Exception(Text::_('COM_MEDIA_ERROR_ACCOUNT_NOT_FOUND')); + } + + return $adapters[$account]; + } } diff --git a/administrator/components/com_media/src/Provider/ProviderManagerHelperTrait.php b/administrator/components/com_media/src/Provider/ProviderManagerHelperTrait.php index 7c95165602386..735544e3d9d7a 100644 --- a/administrator/components/com_media/src/Provider/ProviderManagerHelperTrait.php +++ b/administrator/components/com_media/src/Provider/ProviderManagerHelperTrait.php @@ -1,4 +1,5 @@ providerManager) - { - // Fire the event to get the results - $eventParameters = ['context' => 'AdapterManager', 'providerManager' => new ProviderManager]; - $event = new MediaProviderEvent('onSetupProviders', $eventParameters); - PluginHelper::importPlugin('filesystem'); - Factory::getApplication()->triggerEvent('onSetupProviders', $event); - $this->providerManager = $event->getProviderManager(); - } - - return $this->providerManager; - } - - /** - * Returns a provider for the given id. - * - * @return ProviderInterface - * - * @throws \Exception - * - * @since 4.1.0 - */ - public function getProvider(String $id): ProviderInterface - { - return $this->getProviderManager()->getProvider($id); - } - - /** - * Return an adapter for the given name. - * - * @return AdapterInterface - * - * @throws \Exception - * - * @since 4.1.0 - */ - public function getAdapter(String $name): AdapterInterface - { - return $this->getProviderManager()->getAdapter($name); - } - - /** - * Returns an array with the adapter name as key and the path of the file. - * - * @return array - * - * @throws \InvalidArgumentException - * - * @since 4.1.0 - */ - protected function resolveAdapterAndPath(String $path): array - { - $result = []; - $parts = explode(':', $path, 2); - - // If we have 2 parts, we have both an adapter name and a file path - if (\count($parts) === 2) - { - $result['adapter'] = $parts[0]; - $result['path'] = $parts[1]; - - return $result; - } - - if (!$this->getDefaultAdapterName()) - { - throw new \InvalidArgumentException(Text::_('COM_MEDIA_ERROR_NO_ADAPTER_FOUND')); - } - - // If we have less than 2 parts, we return a default adapter name - $result['adapter'] = $this->getDefaultAdapterName(); - - // If we have 1 part, we return it as the path. Otherwise we return a default path - $result['path'] = \count($parts) ? $parts[0] : '/'; - - return $result; - } - - /** - * Returns the default adapter name. - * - * @return string|null - * - * @throws \Exception - * - * @since 4.1.0 - */ - protected function getDefaultAdapterName(): ?string - { - if ($this->defaultAdapterName) - { - return $this->defaultAdapterName; - } - - $defaultAdapter = $this->getAdapter('local-' . ComponentHelper::getParams('com_media')->get('file_path', 'images')); - - if (!$defaultAdapter - && $this->getProviderManager()->getProvider('local') - && $this->getProviderManager()->getProvider('local')->getAdapters()) - { - $defaultAdapter = $this->getProviderManager()->getProvider('local')->getAdapters()[0]; - } - - if (!$defaultAdapter) - { - return null; - } - - $this->defaultAdapterName = 'local-' . $defaultAdapter->getAdapterName(); - - return $this->defaultAdapterName; - } + /** + * Holds the available media file adapters. + * + * @var ProviderManager + * + * @since 4.1.0 + */ + private $providerManager = null; + + /** + * The default adapter name. + * + * @var string + * + * @since 4.1.0 + */ + private $defaultAdapterName = null; + + /** + * Return a provider manager. + * + * @return ProviderManager + * + * @since 4.1.0 + */ + public function getProviderManager(): ProviderManager + { + if (!$this->providerManager) { + // Fire the event to get the results + $eventParameters = ['context' => 'AdapterManager', 'providerManager' => new ProviderManager()]; + $event = new MediaProviderEvent('onSetupProviders', $eventParameters); + PluginHelper::importPlugin('filesystem'); + Factory::getApplication()->triggerEvent('onSetupProviders', $event); + $this->providerManager = $event->getProviderManager(); + } + + return $this->providerManager; + } + + /** + * Returns a provider for the given id. + * + * @return ProviderInterface + * + * @throws \Exception + * + * @since 4.1.0 + */ + public function getProvider(string $id): ProviderInterface + { + return $this->getProviderManager()->getProvider($id); + } + + /** + * Return an adapter for the given name. + * + * @return AdapterInterface + * + * @throws \Exception + * + * @since 4.1.0 + */ + public function getAdapter(string $name): AdapterInterface + { + return $this->getProviderManager()->getAdapter($name); + } + + /** + * Returns an array with the adapter name as key and the path of the file. + * + * @return array + * + * @throws \InvalidArgumentException + * + * @since 4.1.0 + */ + protected function resolveAdapterAndPath(string $path): array + { + $result = []; + $parts = explode(':', $path, 2); + + // If we have 2 parts, we have both an adapter name and a file path + if (\count($parts) === 2) { + $result['adapter'] = $parts[0]; + $result['path'] = $parts[1]; + + return $result; + } + + if (!$this->getDefaultAdapterName()) { + throw new \InvalidArgumentException(Text::_('COM_MEDIA_ERROR_NO_ADAPTER_FOUND')); + } + + // If we have less than 2 parts, we return a default adapter name + $result['adapter'] = $this->getDefaultAdapterName(); + + // If we have 1 part, we return it as the path. Otherwise we return a default path + $result['path'] = \count($parts) ? $parts[0] : '/'; + + return $result; + } + + /** + * Returns the default adapter name. + * + * @return string|null + * + * @throws \Exception + * + * @since 4.1.0 + */ + protected function getDefaultAdapterName(): ?string + { + if ($this->defaultAdapterName) { + return $this->defaultAdapterName; + } + + $defaultAdapter = $this->getAdapter('local-' . ComponentHelper::getParams('com_media')->get('file_path', 'images')); + + if ( + !$defaultAdapter + && $this->getProviderManager()->getProvider('local') + && $this->getProviderManager()->getProvider('local')->getAdapters() + ) { + $defaultAdapter = $this->getProviderManager()->getProvider('local')->getAdapters()[0]; + } + + if (!$defaultAdapter) { + return null; + } + + $this->defaultAdapterName = 'local-' . $defaultAdapter->getAdapterName(); + + return $this->defaultAdapterName; + } } diff --git a/administrator/components/com_media/src/View/File/HtmlView.php b/administrator/components/com_media/src/View/File/HtmlView.php index 1681758459c89..3dfebb86e61b5 100644 --- a/administrator/components/com_media/src/View/File/HtmlView.php +++ b/administrator/components/com_media/src/View/File/HtmlView.php @@ -1,4 +1,5 @@ input; - /** - * Execute and display a template script. - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - $input = Factory::getApplication()->input; - - $this->form = $this->get('Form'); + $this->form = $this->get('Form'); - // The component params - $this->params = ComponentHelper::getParams('com_media'); + // The component params + $this->params = ComponentHelper::getParams('com_media'); - // The requested file - $this->file = $this->getModel()->getFileInformation($input->getString('path', null)); + // The requested file + $this->file = $this->getModel()->getFileInformation($input->getString('path', null)); - if (empty($this->file->content)) - { - // @todo error handling controller redirect files - throw new \Exception(Text::_('COM_MEDIA_ERROR_NO_CONTENT_AVAILABLE')); - } + if (empty($this->file->content)) { + // @todo error handling controller redirect files + throw new \Exception(Text::_('COM_MEDIA_ERROR_NO_CONTENT_AVAILABLE')); + } - $this->addToolbar(); + $this->addToolbar(); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Add the toolbar buttons - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_MEDIA_EDIT'), 'images mediamanager'); + /** + * Add the toolbar buttons + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_MEDIA_EDIT'), 'images mediamanager'); - ToolbarHelper::apply('apply'); - ToolbarHelper::save('save'); - ToolbarHelper::custom('reset', 'refresh', '', 'COM_MEDIA_RESET', false); + ToolbarHelper::apply('apply'); + ToolbarHelper::save('save'); + ToolbarHelper::custom('reset', 'refresh', '', 'COM_MEDIA_RESET', false); - ToolbarHelper::cancel('cancel', 'JTOOLBAR_CLOSE'); - } + ToolbarHelper::cancel('cancel', 'JTOOLBAR_CLOSE'); + } } diff --git a/administrator/components/com_media/tmpl/file/default.php b/administrator/components/com_media/tmpl/file/default.php index 83a9fd379435a..6ae34c01068d4 100644 --- a/administrator/components/com_media/tmpl/file/default.php +++ b/administrator/components/com_media/tmpl/file/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useStyle('com_media.mediamanager'); + ->useScript('form.validate') + ->useStyle('com_media.mediamanager'); $script = $wa->getAsset('script', 'com_media.edit-images')->getUri(true); @@ -37,25 +38,25 @@ // Load the toolbar when we are in an iframe if ($tmpl == 'component') { - echo '
    '; - echo Toolbar::getInstance('toolbar')->render(); - echo '
    '; + echo '
    '; + echo Toolbar::getInstance('toolbar')->render(); + echo '
    '; } $mediaTypes = $input->getString('mediatypes', '0'); // Populate the media config $config = [ - 'apiBaseUrl' => Uri::base() . 'index.php?option=com_media&format=json' . '&mediatypes=' . $mediaTypes, - 'csrfToken' => Session::getFormToken(), - 'uploadPath' => $this->file->path, - 'editViewUrl' => Uri::base() . 'index.php?option=com_media&view=file' . ($tmpl ? '&tmpl=' . $tmpl : '') . '&mediatypes=' . $mediaTypes, - 'imagesExtensions' => explode(',', $params->get('image_extensions', 'bmp,gif,jpg,jpeg,png,webp')), - 'audioExtensions' => explode(',', $params->get('audio_extensions', 'mp3,m4a,mp4a,ogg')), - 'videoExtensions' => explode(',', $params->get('video_extensions', 'mp4,mp4v,mpeg,mov,webm')), - 'documentExtensions' => explode(',', $params->get('doc_extensions', 'doc,odg,odp,ods,odt,pdf,ppt,txt,xcf,xls,csv')), - 'maxUploadSizeMb' => $params->get('upload_maxsize', 10), - 'contents' => $this->file->content, + 'apiBaseUrl' => Uri::base() . 'index.php?option=com_media&format=json' . '&mediatypes=' . $mediaTypes, + 'csrfToken' => Session::getFormToken(), + 'uploadPath' => $this->file->path, + 'editViewUrl' => Uri::base() . 'index.php?option=com_media&view=file' . ($tmpl ? '&tmpl=' . $tmpl : '') . '&mediatypes=' . $mediaTypes, + 'imagesExtensions' => explode(',', $params->get('image_extensions', 'bmp,gif,jpg,jpeg,png,webp')), + 'audioExtensions' => explode(',', $params->get('audio_extensions', 'mp3,m4a,mp4a,ogg')), + 'videoExtensions' => explode(',', $params->get('video_extensions', 'mp4,mp4v,mpeg,mov,webm')), + 'documentExtensions' => explode(',', $params->get('doc_extensions', 'doc,odg,odp,ods,odt,pdf,ppt,txt,xcf,xls,csv')), + 'maxUploadSizeMb' => $params->get('upload_maxsize', 10), + 'contents' => $this->file->content, ]; $this->document->addScriptOptions('com_media', $config); @@ -63,13 +64,13 @@ $this->useCoreUI = true; ?>
    - getFieldsets(); ?> - - 'attrib-' . reset($fieldSets)->name, 'breakpoint' => 768]); ?> - - '; ?> - - - + getFieldsets(); ?> + + 'attrib-' . reset($fieldSets)->name, 'breakpoint' => 768]); ?> + + '; ?> + + +
    diff --git a/administrator/components/com_menus/helpers/menus.php b/administrator/components/com_menus/helpers/menus.php index 570b89ca30513..e5c69a85c81df 100644 --- a/administrator/components/com_menus/helpers/menus.php +++ b/administrator/components/com_menus/helpers/menus.php @@ -1,4 +1,5 @@ input; $component = $input->getCmd('option', 'com_content'); -if ($component == 'com_categories') -{ - $extension = $input->getCmd('extension', 'com_content'); - $parts = explode('.', $extension); - $component = $parts[0]; +if ($component == 'com_categories') { + $extension = $input->getCmd('extension', 'com_content'); + $parts = explode('.', $extension); + $component = $parts[0]; } $saveHistory = ComponentHelper::getParams($component)->get('save_history', 0); $fields = $displayData->get('fields') ?: array( - array('parent', 'parent_id'), - array('published', 'state', 'enabled'), - array('category', 'catid'), - 'featured', - 'sticky', - 'access', - 'language', - 'tags', - 'note', - 'version_note', + array('parent', 'parent_id'), + array('published', 'state', 'enabled'), + array('category', 'catid'), + 'featured', + 'sticky', + 'access', + 'language', + 'tags', + 'note', + 'version_note', ); $hiddenFields = $displayData->get('hidden_fields') ?: array(); -if (!$saveHistory) -{ - $hiddenFields[] = 'version_note'; +if (!$saveHistory) { + $hiddenFields[] = 'version_note'; } $html = array(); $html[] = '
      '; -foreach ($fields as $field) -{ - $field = is_array($field) ? $field : array($field); +foreach ($fields as $field) { + $field = is_array($field) ? $field : array($field); - foreach ($field as $f) - { - if ($form->getField($f)) - { - if (in_array($f, $hiddenFields)) - { - $form->setFieldAttribute($f, 'type', 'hidden'); - } + foreach ($field as $f) { + if ($form->getField($f)) { + if (in_array($f, $hiddenFields)) { + $form->setFieldAttribute($f, 'type', 'hidden'); + } - $html[] = '
    • ' . $form->renderField($f) . '
    • '; - break; - } - } + $html[] = '
    • ' . $form->renderField($f) . '
    • '; + break; + } + } } $html[] = '
    '; diff --git a/administrator/components/com_menus/layouts/joomla/searchtools/default.php b/administrator/components/com_menus/layouts/joomla/searchtools/default.php index 9f0758024b86b..0901324668cab 100644 --- a/administrator/components/com_menus/layouts/joomla/searchtools/default.php +++ b/administrator/components/com_menus/layouts/joomla/searchtools/default.php @@ -1,4 +1,5 @@ filterForm) && !empty($data['view']->filterForm)) -{ - // Checks if a selector (e.g. client_id) exists. - if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) - { - $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; - - // Checks if a selector should be shown in the current layout. - if (isset($data['view']->layout)) - { - $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; - } - - // Unset the selector field from active filters group. - unset($data['view']->activeFilters[$selectorFieldName]); - } - - if ($data['view'] instanceof \Joomla\Component\Menus\Administrator\View\Items\HtmlView) : - unset($data['view']->activeFilters['client_id']); - endif; - - // Checks if the filters button should exist. - $filters = $data['view']->filterForm->getGroup('filter'); - $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; - - // Checks if it should show the be hidden. - $hideActiveFilters = empty($data['view']->activeFilters); - - // Check if the no results message should appear. - if (isset($data['view']->total) && (int) $data['view']->total === 0) - { - $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); - if (!empty($noResults)) - { - $noResultsText = Text::_($noResults); - } - } +if (isset($data['view']->filterForm) && !empty($data['view']->filterForm)) { + // Checks if a selector (e.g. client_id) exists. + if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) { + $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; + + // Checks if a selector should be shown in the current layout. + if (isset($data['view']->layout)) { + $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; + } + + // Unset the selector field from active filters group. + unset($data['view']->activeFilters[$selectorFieldName]); + } + + if ($data['view'] instanceof \Joomla\Component\Menus\Administrator\View\Items\HtmlView) : + unset($data['view']->activeFilters['client_id']); + endif; + + // Checks if the filters button should exist. + $filters = $data['view']->filterForm->getGroup('filter'); + $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; + + // Checks if it should show the be hidden. + $hideActiveFilters = empty($data['view']->activeFilters); + + // Check if the no results message should appear. + if (isset($data['view']->total) && (int) $data['view']->total === 0) { + $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); + if (!empty($noResults)) { + $noResultsText = Text::_($noResults); + } + } } // Set some basic options. $customOptions = array( - 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, - 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, - 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), - 'searchFieldSelector' => '#filter_search', - 'selectorFieldName' => $selectorFieldName, - 'showSelector' => $showSelector, - 'orderFieldSelector' => '#list_fullordering', - 'showNoResults' => !empty($noResultsText), - 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', - 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', + 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, + 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, + 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), + 'searchFieldSelector' => '#filter_search', + 'selectorFieldName' => $selectorFieldName, + 'showSelector' => $showSelector, + 'orderFieldSelector' => '#list_fullordering', + 'showNoResults' => !empty($noResultsText), + 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', + 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', ); // Merge custom options in the options array. @@ -89,39 +85,39 @@ HTMLHelper::_('searchtools.form', $data['options']['formSelector'], $data['options']); ?> - sublayout('noitems', $data); ?> + sublayout('noitems', $data); ?> diff --git a/administrator/components/com_menus/services/provider.php b/administrator/components/com_menus/services/provider.php index 3f6056d7734c4..2251234727ec5 100644 --- a/administrator/components/com_menus/services/provider.php +++ b/administrator/components/com_menus/services/provider.php @@ -1,4 +1,5 @@ set(AssociationExtensionInterface::class, new AssociationsHelper); - - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Menus')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Menus')); - - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MenusComponent($container->get(ComponentDispatcherFactoryInterface::class)); - - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); - - return $component; - } - ); - } + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->set(AssociationExtensionInterface::class, new AssociationsHelper()); + + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Menus')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Menus')); + + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MenusComponent($container->get(ComponentDispatcherFactoryInterface::class)); + + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); + + return $component; + } + ); + } }; diff --git a/administrator/components/com_menus/src/Controller/AjaxController.php b/administrator/components/com_menus/src/Controller/AjaxController.php index 32787bc54a12c..84f9828cd427a 100644 --- a/administrator/components/com_menus/src/Controller/AjaxController.php +++ b/administrator/components/com_menus/src/Controller/AjaxController.php @@ -1,4 +1,5 @@ input->getInt('assocId', 0); + /** + * Method to fetch associations of a menu item + * + * The method assumes that the following http parameters are passed in an Ajax Get request: + * token: the form token + * assocId: the id of the menu item whose associations are to be returned + * excludeLang: the association for this language is to be excluded + * + * @return null + * + * @since 3.9.0 + */ + public function fetchAssociations() + { + if (!Session::checkToken('get')) { + echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true); + } else { + $assocId = $this->input->getInt('assocId', 0); - if ($assocId == 0) - { - echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); + if ($assocId == 0) { + echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); - return; - } + return; + } - $excludeLang = $this->input->get('excludeLang', '', 'STRING'); + $excludeLang = $this->input->get('excludeLang', '', 'STRING'); - $associations = Associations::getAssociations('com_menus', '#__menu', 'com_menus.item', (int) $assocId, 'id', '', ''); + $associations = Associations::getAssociations('com_menus', '#__menu', 'com_menus.item', (int) $assocId, 'id', '', ''); - unset($associations[$excludeLang]); + unset($associations[$excludeLang]); - // Add the title to each of the associated records - Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/tables'); - $menuTable = Table::getInstance('Menu', 'JTable', array()); + // Add the title to each of the associated records + Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/tables'); + $menuTable = Table::getInstance('Menu', 'JTable', array()); - foreach ($associations as $lang => $association) - { - $menuTable->load($association->id); - $associations[$lang]->title = $menuTable->title; - } + foreach ($associations as $lang => $association) { + $menuTable->load($association->id); + $associations[$lang]->title = $menuTable->title; + } - $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); + $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); - if (count($associations) == 0) - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); - } - elseif ($countContentLanguages > count($associations) + 2) - { - $tags = implode(', ', array_keys($associations)); - $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); - } - else - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); - } + if (count($associations) == 0) { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); + } elseif ($countContentLanguages > count($associations) + 2) { + $tags = implode(', ', array_keys($associations)); + $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); + } else { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); + } - echo new JsonResponse($associations, $message); - } - } + echo new JsonResponse($associations, $message); + } + } } diff --git a/administrator/components/com_menus/src/Controller/DisplayController.php b/administrator/components/com_menus/src/Controller/DisplayController.php index 4f6047b026eb7..27eb00f1b738a 100644 --- a/administrator/components/com_menus/src/Controller/DisplayController.php +++ b/administrator/components/com_menus/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->post->getCmd('menutype', ''); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array|boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + // Verify menu + $menuType = $this->input->post->getCmd('menutype', ''); - if ($menuType !== '') - { - $uri = Uri::getInstance(); + if ($menuType !== '') { + $uri = Uri::getInstance(); - if ($uri->getVar('menutype') !== $menuType) - { - $uri->setVar('menutype', $menuType); + if ($uri->getVar('menutype') !== $menuType) { + $uri->setVar('menutype', $menuType); - if ($forcedLanguage = $this->input->post->get('forcedLanguage')) - { - $uri->setVar('forcedLanguage', $forcedLanguage); - } + if ($forcedLanguage = $this->input->post->get('forcedLanguage')) { + $uri->setVar('forcedLanguage', $forcedLanguage); + } - $this->setRedirect(Route::_('index.php' . $uri->toString(['query']), false)); + $this->setRedirect(Route::_('index.php' . $uri->toString(['query']), false)); - return parent::display(); - } - } + return parent::display(); + } + } - // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. - if ($langMissing = $this->getModel('Menus', 'Administrator')->getMissingModuleLanguages()) - { - $this->app->enqueueMessage(Text::sprintf('JMENU_MULTILANG_WARNING_MISSING_MODULES', implode(', ', $langMissing)), 'warning'); - } + // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. + if ($langMissing = $this->getModel('Menus', 'Administrator')->getMissingModuleLanguages()) { + $this->app->enqueueMessage(Text::sprintf('JMENU_MULTILANG_WARNING_MISSING_MODULES', implode(', ', $langMissing)), 'warning'); + } - return parent::display(); - } + return parent::display(); + } } diff --git a/administrator/components/com_menus/src/Controller/ItemController.php b/administrator/components/com_menus/src/Controller/ItemController.php index cc1b0a5682d9f..c995496055e8e 100644 --- a/administrator/components/com_menus/src/Controller/ItemController.php +++ b/administrator/components/com_menus/src/Controller/ItemController.php @@ -1,4 +1,5 @@ app->getIdentity(); - - $menuType = $this->input->getCmd('menutype', $data['menutype'] ?? ''); - - $menutypeID = 0; - - // Load menutype ID - if ($menuType) - { - $menutypeID = (int) $this->getMenuTypeId($menuType); - } - - return $user->authorise('core.create', 'com_menus.menu.' . $menutypeID); - } - - /** - * Method to check if you edit a record. - * - * Extended classes can override this if necessary. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key; default is id. - * - * @return boolean - * - * @since 3.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $user = $this->app->getIdentity(); - - $menutypeID = 0; - - if (isset($data[$key])) - { - $model = $this->getModel(); - $item = $model->getItem($data[$key]); - - if (!empty($item->menutype)) - { - // Protected menutype, do not allow edit - if ($item->menutype == 'main') - { - return false; - } - - $menutypeID = (int) $this->getMenuTypeId($item->menutype); - } - } - - return $user->authorise('core.edit', 'com_menus.menu.' . (int) $menutypeID); - } - - /** - * Loads the menutype ID by a given menutype string - * - * @param string $menutype The given menutype - * - * @return integer - * - * @since 3.6 - */ - protected function getMenuTypeId($menutype) - { - $model = $this->getModel(); - $table = $model->getTable('MenuType'); - - $table->load(array('menutype' => $menutype)); - - return (int) $table->id; - } - - /** - * Method to add a new menu item. - * - * @return mixed True if the record can be added, otherwise false. - * - * @since 1.6 - */ - public function add() - { - $result = parent::add(); - - if ($result) - { - $context = 'com_menus.edit.item'; - - $this->app->setUserState($context . '.type', null); - $this->app->setUserState($context . '.link', null); - } - - return $result; - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 1.6 - */ - public function batch($model = null) - { - $this->checkToken(); - - /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ - $model = $this->getModel('Item', 'Administrator', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_menus&view=items' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } - - /** - * Method to cancel an edit. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 1.6 - */ - public function cancel($key = null) - { - $this->checkToken(); - - $result = parent::cancel(); - - if ($result) - { - // Clear the ancillary data from the session. - $context = 'com_menus.edit.item'; - $this->app->setUserState($context . '.type', null); - $this->app->setUserState($context . '.link', null); - - // Redirect to the list screen. - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend() - . '&menutype=' . $this->app->getUserState('com_menus.items.menutype'), false - ) - ); - } - - return $result; - } - - /** - * Method to edit an existing record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key - * (sometimes required to avoid router collisions). - * - * @return boolean True if access level check and checkout passes, false otherwise. - * - * @since 1.6 - */ - public function edit($key = null, $urlVar = null) - { - $result = parent::edit(); - - if ($result) - { - // Push the new ancillary data into the session. - $this->app->setUserState('com_menus.edit.item.type', null); - $this->app->setUserState('com_menus.edit.item.link', null); - } - - return $result; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 3.0.1 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId, $urlVar); - - if ($recordId) - { - /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ - $model = $this->getModel(); - $item = $model->getItem($recordId); - $clientId = $item->client_id; - $append = '&client_id=' . $clientId . $append; - } - else - { - $clientId = $this->input->get('client_id', '0', 'int'); - $menuType = $this->input->get('menutype', 'mainmenu', 'cmd'); - $append = '&client_id=' . $clientId . ($menuType ? '&menutype=' . $menuType : '') . $append; - } - - return $append; - } - - /** - * Method to save a record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 1.6 - */ - public function save($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ - $model = $this->getModel('Item', 'Administrator', array()); - $table = $model->getTable(); - $data = $this->input->post->get('jform', array(), 'array'); - $task = $this->getTask(); - $context = 'com_menus.edit.item'; - $app = $this->app; - - // Set the menutype should we need it. - if ($data['menutype'] !== '') - { - $this->input->set('menutype', $data['menutype']); - } - - // Determine the name of the primary key for the data. - if (empty($key)) - { - $key = $table->getKeyName(); - } - - // To avoid data collisions the urlVar may be different from the primary key. - if (empty($urlVar)) - { - $urlVar = $key; - } - - $recordId = $this->input->getInt($urlVar); - - // Populate the row id from the session. - $data[$key] = $recordId; - - // The save2copy task needs to be handled slightly differently. - if ($task == 'save2copy') - { - // Check-in the original row. - if ($model->checkin($data['id']) === false) - { - // Check-in failed, go back to the item and display a notice. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'warning'); - - return false; - } - - // Reset the ID and then treat the request as for Apply. - $data['id'] = 0; - $data['associations'] = array(); - $task = 'apply'; - } - - // Access check. - if (!$this->allowSave($data, $key)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false - ) - ); - - return false; - } - - // Validate the posted data. - // This post is made up of two forms, one for the item and one for params. - $form = $model->getForm($data); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - if ($data['type'] == 'url') - { - $data['link'] = str_replace(array('"', '>', '<'), '', $data['link']); - - if (strstr($data['link'], ':')) - { - $segments = explode(':', $data['link']); - $protocol = strtolower($segments[0]); - $scheme = array( - 'http', 'https', 'ftp', 'ftps', 'gopher', 'mailto', - 'news', 'prospero', 'telnet', 'rlogin', 'tn3270', 'wais', - 'mid', 'cid', 'nntp', 'tel', 'urn', 'ldap', 'file', 'fax', - 'modem', 'git', 'sms', - ); - - if (!in_array($protocol, $scheme)) - { - $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'warning'); - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId), false) - ); - - return false; - } - } - } - - $data = $model->validate($form, $data); - - // Preprocess request fields to ensure that we remove not set or empty request params - $request = $form->getGroup('request', true); - - // Check for the special 'request' entry. - if ($data['type'] == 'component' && !empty($request)) - { - $removeArgs = array(); - - if (!isset($data['request']) || !is_array($data['request'])) - { - $data['request'] = array(); - } - - foreach ($request as $field) - { - $fieldName = $field->getAttribute('name'); - - if (!isset($data['request'][$fieldName]) || $data['request'][$fieldName] == '') - { - $removeArgs[$fieldName] = ''; - } - } - - // Parse the submitted link arguments. - $args = array(); - parse_str(parse_url($data['link'], PHP_URL_QUERY), $args); - - // Merge in the user supplied request arguments. - $args = array_merge($args, $data['request']); - - // Remove the unused request params - if (!empty($args) && !empty($removeArgs)) - { - $args = array_diff_key($args, $removeArgs); - } - - $data['link'] = 'index.php?' . urldecode(http_build_query($args, '', '&')); - } - - // Check for validation errors. - if ($data === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $app->setUserState('com_menus.edit.item.data', $data); - - // Redirect back to the edit screen. - $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); - $this->setRedirect(Route::_($editUrl, false)); - - return false; - } - - // Attempt to save the data. - if (!$model->save($data)) - { - // Save the data in the session. - $app->setUserState('com_menus.edit.item.data', $data); - - // Redirect back to the edit screen. - $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - $this->setRedirect(Route::_($editUrl, false)); - - return false; - } - - // Save succeeded, check-in the row. - if ($model->checkin($data['id']) === false) - { - // Check-in failed, go back to the row and display a notice. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'warning'); - $redirectUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); - $this->setRedirect(Route::_($redirectUrl, false)); - - return false; - } - - $this->setMessage(Text::_('COM_MENUS_SAVE_SUCCESS')); - - // Redirect the user and adjust session state based on the chosen task. - switch ($task) - { - case 'apply': - // Set the row data in the session. - $recordId = $model->getState($this->context . '.id'); - $this->holdEditId($context, $recordId); - $app->setUserState('com_menus.edit.item.data', null); - $app->setUserState('com_menus.edit.item.type', null); - $app->setUserState('com_menus.edit.item.link', null); - - // Redirect back to the edit screen. - $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); - $this->setRedirect(Route::_($editUrl, false)); - break; - - case 'save2new': - // Clear the row id and data in the session. - $this->releaseEditId($context, $recordId); - $app->setUserState('com_menus.edit.item.data', null); - $app->setUserState('com_menus.edit.item.type', null); - $app->setUserState('com_menus.edit.item.link', null); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend(), false)); - break; - - default: - // Clear the row id and data in the session. - $this->releaseEditId($context, $recordId); - $app->setUserState('com_menus.edit.item.data', null); - $app->setUserState('com_menus.edit.item.type', null); - $app->setUserState('com_menus.edit.item.link', null); - - // Redirect to the list screen. - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend() - . '&menutype=' . $app->getUserState('com_menus.items.menutype'), false - ) - ); - break; - } - - return true; - } - - /** - * Sets the type of the menu item currently being edited. - * - * @return void - * - * @since 1.6 - */ - public function setType() - { - $this->checkToken(); - - $app = $this->app; - - // Get the posted values from the request. - $data = $this->input->post->get('jform', array(), 'array'); - - // Get the type. - $type = $data['type']; - - $type = json_decode(base64_decode($type)); - $title = $type->title ?? null; - $recordId = $type->id ?? 0; - - $specialTypes = array('alias', 'separator', 'url', 'heading', 'container'); - - if (!in_array($title, $specialTypes)) - { - $title = 'component'; - } - else - { - // Set correct component id to ensure proper 404 messages with system links - $data['component_id'] = 0; - } - - $app->setUserState('com_menus.edit.item.type', $title); - - if ($title == 'component') - { - if (isset($type->request)) - { - // Clean component name - $type->request->option = InputFilter::getInstance()->clean($type->request->option, 'CMD'); - - $component = ComponentHelper::getComponent($type->request->option); - $data['component_id'] = $component->id; - - $app->setUserState('com_menus.edit.item.link', 'index.php?' . Uri::buildQuery((array) $type->request)); - } - } - // If the type is alias you just need the item id from the menu item referenced. - elseif ($title == 'alias') - { - $app->setUserState('com_menus.edit.item.link', 'index.php?Itemid='); - } - - unset($data['request']); - - $data['type'] = $title; - - if ($this->input->get('fieldtype') == 'type') - { - $data['link'] = $app->getUserState('com_menus.edit.item.link'); - } - - // Save the data in the session. - $app->setUserState('com_menus.edit.item.data', $data); - - $this->type = $type; - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId), false) - ); - } - - /** - * Gets the parent items of the menu location currently. - * - * @return void - * - * @since 3.2 - */ - public function getParentItem() - { - $app = $this->app; - - $results = array(); - $menutype = $this->input->get->get('menutype'); - - if ($menutype) - { - /** @var \Joomla\Component\Menus\Administrator\Model\ItemsModel $model */ - $model = $this->getModel('Items', 'Administrator', array()); - $model->getState(); - $model->setState('filter.menutype', $menutype); - $model->setState('list.select', 'a.id, a.title, a.level'); - $model->setState('list.start', '0'); - $model->setState('list.limit', '0'); - - $results = $model->getItems(); - - // Pad the option text with spaces using depth level as a multiplier. - for ($i = 0, $n = count($results); $i < $n; $i++) - { - $results[$i]->title = str_repeat(' - ', $results[$i]->level) . $results[$i]->title; - } - } - - // Output a \JSON object - echo json_encode($results); - - $app->close(); - } + /** + * Method to check if you can add a new record. + * + * Extended classes can override this if necessary. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 3.6 + */ + protected function allowAdd($data = array()) + { + $user = $this->app->getIdentity(); + + $menuType = $this->input->getCmd('menutype', $data['menutype'] ?? ''); + + $menutypeID = 0; + + // Load menutype ID + if ($menuType) { + $menutypeID = (int) $this->getMenuTypeId($menuType); + } + + return $user->authorise('core.create', 'com_menus.menu.' . $menutypeID); + } + + /** + * Method to check if you edit a record. + * + * Extended classes can override this if necessary. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key; default is id. + * + * @return boolean + * + * @since 3.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $user = $this->app->getIdentity(); + + $menutypeID = 0; + + if (isset($data[$key])) { + $model = $this->getModel(); + $item = $model->getItem($data[$key]); + + if (!empty($item->menutype)) { + // Protected menutype, do not allow edit + if ($item->menutype == 'main') { + return false; + } + + $menutypeID = (int) $this->getMenuTypeId($item->menutype); + } + } + + return $user->authorise('core.edit', 'com_menus.menu.' . (int) $menutypeID); + } + + /** + * Loads the menutype ID by a given menutype string + * + * @param string $menutype The given menutype + * + * @return integer + * + * @since 3.6 + */ + protected function getMenuTypeId($menutype) + { + $model = $this->getModel(); + $table = $model->getTable('MenuType'); + + $table->load(array('menutype' => $menutype)); + + return (int) $table->id; + } + + /** + * Method to add a new menu item. + * + * @return mixed True if the record can be added, otherwise false. + * + * @since 1.6 + */ + public function add() + { + $result = parent::add(); + + if ($result) { + $context = 'com_menus.edit.item'; + + $this->app->setUserState($context . '.type', null); + $this->app->setUserState($context . '.link', null); + } + + return $result; + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 1.6 + */ + public function batch($model = null) + { + $this->checkToken(); + + /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ + $model = $this->getModel('Item', 'Administrator', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_menus&view=items' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } + + /** + * Method to cancel an edit. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 1.6 + */ + public function cancel($key = null) + { + $this->checkToken(); + + $result = parent::cancel(); + + if ($result) { + // Clear the ancillary data from the session. + $context = 'com_menus.edit.item'; + $this->app->setUserState($context . '.type', null); + $this->app->setUserState($context . '.link', null); + + // Redirect to the list screen. + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend() + . '&menutype=' . $this->app->getUserState('com_menus.items.menutype'), + false + ) + ); + } + + return $result; + } + + /** + * Method to edit an existing record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key + * (sometimes required to avoid router collisions). + * + * @return boolean True if access level check and checkout passes, false otherwise. + * + * @since 1.6 + */ + public function edit($key = null, $urlVar = null) + { + $result = parent::edit(); + + if ($result) { + // Push the new ancillary data into the session. + $this->app->setUserState('com_menus.edit.item.type', null); + $this->app->setUserState('com_menus.edit.item.link', null); + } + + return $result; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 3.0.1 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId, $urlVar); + + if ($recordId) { + /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ + $model = $this->getModel(); + $item = $model->getItem($recordId); + $clientId = $item->client_id; + $append = '&client_id=' . $clientId . $append; + } else { + $clientId = $this->input->get('client_id', '0', 'int'); + $menuType = $this->input->get('menutype', 'mainmenu', 'cmd'); + $append = '&client_id=' . $clientId . ($menuType ? '&menutype=' . $menuType : '') . $append; + } + + return $append; + } + + /** + * Method to save a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 1.6 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ + $model = $this->getModel('Item', 'Administrator', array()); + $table = $model->getTable(); + $data = $this->input->post->get('jform', array(), 'array'); + $task = $this->getTask(); + $context = 'com_menus.edit.item'; + $app = $this->app; + + // Set the menutype should we need it. + if ($data['menutype'] !== '') { + $this->input->set('menutype', $data['menutype']); + } + + // Determine the name of the primary key for the data. + if (empty($key)) { + $key = $table->getKeyName(); + } + + // To avoid data collisions the urlVar may be different from the primary key. + if (empty($urlVar)) { + $urlVar = $key; + } + + $recordId = $this->input->getInt($urlVar); + + // Populate the row id from the session. + $data[$key] = $recordId; + + // The save2copy task needs to be handled slightly differently. + if ($task == 'save2copy') { + // Check-in the original row. + if ($model->checkin($data['id']) === false) { + // Check-in failed, go back to the item and display a notice. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'warning'); + + return false; + } + + // Reset the ID and then treat the request as for Apply. + $data['id'] = 0; + $data['associations'] = array(); + $task = 'apply'; + } + + // Access check. + if (!$this->allowSave($data, $key)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(), + false + ) + ); + + return false; + } + + // Validate the posted data. + // This post is made up of two forms, one for the item and one for params. + $form = $model->getForm($data); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + if ($data['type'] == 'url') { + $data['link'] = str_replace(array('"', '>', '<'), '', $data['link']); + + if (strstr($data['link'], ':')) { + $segments = explode(':', $data['link']); + $protocol = strtolower($segments[0]); + $scheme = array( + 'http', 'https', 'ftp', 'ftps', 'gopher', 'mailto', + 'news', 'prospero', 'telnet', 'rlogin', 'tn3270', 'wais', + 'mid', 'cid', 'nntp', 'tel', 'urn', 'ldap', 'file', 'fax', + 'modem', 'git', 'sms', + ); + + if (!in_array($protocol, $scheme)) { + $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'warning'); + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId), false) + ); + + return false; + } + } + } + + $data = $model->validate($form, $data); + + // Preprocess request fields to ensure that we remove not set or empty request params + $request = $form->getGroup('request', true); + + // Check for the special 'request' entry. + if ($data['type'] == 'component' && !empty($request)) { + $removeArgs = array(); + + if (!isset($data['request']) || !is_array($data['request'])) { + $data['request'] = array(); + } + + foreach ($request as $field) { + $fieldName = $field->getAttribute('name'); + + if (!isset($data['request'][$fieldName]) || $data['request'][$fieldName] == '') { + $removeArgs[$fieldName] = ''; + } + } + + // Parse the submitted link arguments. + $args = array(); + parse_str(parse_url($data['link'], PHP_URL_QUERY), $args); + + // Merge in the user supplied request arguments. + $args = array_merge($args, $data['request']); + + // Remove the unused request params + if (!empty($args) && !empty($removeArgs)) { + $args = array_diff_key($args, $removeArgs); + } + + $data['link'] = 'index.php?' . urldecode(http_build_query($args, '', '&')); + } + + // Check for validation errors. + if ($data === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $app->setUserState('com_menus.edit.item.data', $data); + + // Redirect back to the edit screen. + $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); + $this->setRedirect(Route::_($editUrl, false)); + + return false; + } + + // Attempt to save the data. + if (!$model->save($data)) { + // Save the data in the session. + $app->setUserState('com_menus.edit.item.data', $data); + + // Redirect back to the edit screen. + $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + $this->setRedirect(Route::_($editUrl, false)); + + return false; + } + + // Save succeeded, check-in the row. + if ($model->checkin($data['id']) === false) { + // Check-in failed, go back to the row and display a notice. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'warning'); + $redirectUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); + $this->setRedirect(Route::_($redirectUrl, false)); + + return false; + } + + $this->setMessage(Text::_('COM_MENUS_SAVE_SUCCESS')); + + // Redirect the user and adjust session state based on the chosen task. + switch ($task) { + case 'apply': + // Set the row data in the session. + $recordId = $model->getState($this->context . '.id'); + $this->holdEditId($context, $recordId); + $app->setUserState('com_menus.edit.item.data', null); + $app->setUserState('com_menus.edit.item.type', null); + $app->setUserState('com_menus.edit.item.link', null); + + // Redirect back to the edit screen. + $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); + $this->setRedirect(Route::_($editUrl, false)); + break; + + case 'save2new': + // Clear the row id and data in the session. + $this->releaseEditId($context, $recordId); + $app->setUserState('com_menus.edit.item.data', null); + $app->setUserState('com_menus.edit.item.type', null); + $app->setUserState('com_menus.edit.item.link', null); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend(), false)); + break; + + default: + // Clear the row id and data in the session. + $this->releaseEditId($context, $recordId); + $app->setUserState('com_menus.edit.item.data', null); + $app->setUserState('com_menus.edit.item.type', null); + $app->setUserState('com_menus.edit.item.link', null); + + // Redirect to the list screen. + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend() + . '&menutype=' . $app->getUserState('com_menus.items.menutype'), + false + ) + ); + break; + } + + return true; + } + + /** + * Sets the type of the menu item currently being edited. + * + * @return void + * + * @since 1.6 + */ + public function setType() + { + $this->checkToken(); + + $app = $this->app; + + // Get the posted values from the request. + $data = $this->input->post->get('jform', array(), 'array'); + + // Get the type. + $type = $data['type']; + + $type = json_decode(base64_decode($type)); + $title = $type->title ?? null; + $recordId = $type->id ?? 0; + + $specialTypes = array('alias', 'separator', 'url', 'heading', 'container'); + + if (!in_array($title, $specialTypes)) { + $title = 'component'; + } else { + // Set correct component id to ensure proper 404 messages with system links + $data['component_id'] = 0; + } + + $app->setUserState('com_menus.edit.item.type', $title); + + if ($title == 'component') { + if (isset($type->request)) { + // Clean component name + $type->request->option = InputFilter::getInstance()->clean($type->request->option, 'CMD'); + + $component = ComponentHelper::getComponent($type->request->option); + $data['component_id'] = $component->id; + + $app->setUserState('com_menus.edit.item.link', 'index.php?' . Uri::buildQuery((array) $type->request)); + } + } + // If the type is alias you just need the item id from the menu item referenced. + elseif ($title == 'alias') { + $app->setUserState('com_menus.edit.item.link', 'index.php?Itemid='); + } + + unset($data['request']); + + $data['type'] = $title; + + if ($this->input->get('fieldtype') == 'type') { + $data['link'] = $app->getUserState('com_menus.edit.item.link'); + } + + // Save the data in the session. + $app->setUserState('com_menus.edit.item.data', $data); + + $this->type = $type; + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId), false) + ); + } + + /** + * Gets the parent items of the menu location currently. + * + * @return void + * + * @since 3.2 + */ + public function getParentItem() + { + $app = $this->app; + + $results = array(); + $menutype = $this->input->get->get('menutype'); + + if ($menutype) { + /** @var \Joomla\Component\Menus\Administrator\Model\ItemsModel $model */ + $model = $this->getModel('Items', 'Administrator', array()); + $model->getState(); + $model->setState('filter.menutype', $menutype); + $model->setState('list.select', 'a.id, a.title, a.level'); + $model->setState('list.start', '0'); + $model->setState('list.limit', '0'); + + $results = $model->getItems(); + + // Pad the option text with spaces using depth level as a multiplier. + for ($i = 0, $n = count($results); $i < $n; $i++) { + $results[$i]->title = str_repeat(' - ', $results[$i]->level) . $results[$i]->title; + } + } + + // Output a \JSON object + echo json_encode($results); + + $app->close(); + } } diff --git a/administrator/components/com_menus/src/Controller/ItemsController.php b/administrator/components/com_menus/src/Controller/ItemsController.php index 484ee17a0ba5c..9df8a8e14b23a 100644 --- a/administrator/components/com_menus/src/Controller/ItemsController.php +++ b/administrator/components/com_menus/src/Controller/ItemsController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Menus\Administrator\Controller; \defined('_JEXEC') or die; @@ -27,247 +29,219 @@ */ class ItemsController extends AdminController { - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 1.6 - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - $this->registerTask('unsetDefault', 'setDefault'); - } - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return object The model. - * - * @since 1.6 - */ - public function getModel($name = 'Item', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to get the number of published frontend menu items for quickicons - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('Items'); - - $model->setState('filter.published', 1); - $model->setState('filter.client_id', 0); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_MENUS_ITEMS_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_MENUS_ITEMS_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } - - /** - * Rebuild the nested set tree. - * - * @return boolean False on failure or error, true on success. - * - * @since 1.6 - */ - public function rebuild() - { - $this->checkToken(); - - $this->setRedirect('index.php?option=com_menus&view=items&menutype=' . $this->input->getCmd('menutype')); - - /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ - $model = $this->getModel(); - - if ($model->rebuild()) - { - // Reorder succeeded. - $this->setMessage(Text::_('COM_MENUS_ITEMS_REBUILD_SUCCESS')); - - return true; - } - else - { - // Rebuild failed. - $this->setMessage(Text::sprintf('COM_MENUS_ITEMS_REBUILD_FAILED'), 'error'); - - return false; - } - } - - /** - * Method to set the home property for a list of items - * - * @return void - * - * @since 1.6 - */ - public function setDefault() - { - // Check for request forgeries - $this->checkToken('request'); - - $app = $this->app; - - // Get items to publish from the request. - $cid = (array) $this->input->get('cid', array(), 'int'); - $data = array('setDefault' => 1, 'unsetDefault' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($data, $task, 0, 'int'); - - // Remove zero values resulting from input filter - $cid = array_filter($cid); - - if (empty($cid)) - { - $this->setMessage(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), 'warning'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Publish the items. - if (!$model->setHome($cid, $value)) - { - $this->setMessage($model->getError(), 'warning'); - } - else - { - if ($value == 1) - { - $ntext = 'COM_MENUS_ITEMS_SET_HOME'; - } - else - { - $ntext = 'COM_MENUS_ITEMS_UNSET_HOME'; - } - - $this->setMessage(Text::plural($ntext, count($cid))); - } - } - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . '&menutype=' . $app->getUserState('com_menus.items.menutype'), false - ) - ); - } - - /** - * Method to publish a list of items - * - * @return void - * - * @since 3.6.0 - */ - public function publish() - { - // Check for request forgeries - $this->checkToken(); - - // Get items to publish from the request. - $cid = (array) $this->input->get('cid', array(), 'int'); - $data = array('publish' => 1, 'unpublish' => 0, 'trash' => -2, 'report' => -3); - $task = $this->getTask(); - $value = ArrayHelper::getValue($data, $task, 0, 'int'); - - // Remove zero values resulting from input filter - $cid = array_filter($cid); - - if (empty($cid)) - { - try - { - Log::add(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - $this->setMessage(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), 'warning'); - } - } - else - { - // Get the model. - $model = $this->getModel(); - - // Publish the items. - try - { - $model->publish($cid, $value); - $errors = $model->getErrors(); - $messageType = 'message'; - - if ($value == 1) - { - if ($errors) - { - $messageType = 'error'; - $ntext = $this->text_prefix . '_N_ITEMS_FAILED_PUBLISHING'; - } - else - { - $ntext = $this->text_prefix . '_N_ITEMS_PUBLISHED'; - } - } - elseif ($value == 0) - { - $ntext = $this->text_prefix . '_N_ITEMS_UNPUBLISHED'; - } - else - { - $ntext = $this->text_prefix . '_N_ITEMS_TRASHED'; - } - - $this->setMessage(Text::plural($ntext, count($cid)), $messageType); - } - catch (\Exception $e) - { - $this->setMessage($e->getMessage(), 'error'); - } - } - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list . '&menutype=' . - $this->app->getUserState('com_menus.items.menutype'), - false - ) - ); - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - return '&menutype=' . $this->app->getUserState('com_menus.items.menutype'); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('unsetDefault', 'setDefault'); + } + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Item', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to get the number of published frontend menu items for quickicons + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Items'); + + $model->setState('filter.published', 1); + $model->setState('filter.client_id', 0); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_MENUS_ITEMS_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_MENUS_ITEMS_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } + + /** + * Rebuild the nested set tree. + * + * @return boolean False on failure or error, true on success. + * + * @since 1.6 + */ + public function rebuild() + { + $this->checkToken(); + + $this->setRedirect('index.php?option=com_menus&view=items&menutype=' . $this->input->getCmd('menutype')); + + /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ + $model = $this->getModel(); + + if ($model->rebuild()) { + // Reorder succeeded. + $this->setMessage(Text::_('COM_MENUS_ITEMS_REBUILD_SUCCESS')); + + return true; + } else { + // Rebuild failed. + $this->setMessage(Text::sprintf('COM_MENUS_ITEMS_REBUILD_FAILED'), 'error'); + + return false; + } + } + + /** + * Method to set the home property for a list of items + * + * @return void + * + * @since 1.6 + */ + public function setDefault() + { + // Check for request forgeries + $this->checkToken('request'); + + $app = $this->app; + + // Get items to publish from the request. + $cid = (array) $this->input->get('cid', array(), 'int'); + $data = array('setDefault' => 1, 'unsetDefault' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($data, $task, 0, 'int'); + + // Remove zero values resulting from input filter + $cid = array_filter($cid); + + if (empty($cid)) { + $this->setMessage(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), 'warning'); + } else { + // Get the model. + $model = $this->getModel(); + + // Publish the items. + if (!$model->setHome($cid, $value)) { + $this->setMessage($model->getError(), 'warning'); + } else { + if ($value == 1) { + $ntext = 'COM_MENUS_ITEMS_SET_HOME'; + } else { + $ntext = 'COM_MENUS_ITEMS_UNSET_HOME'; + } + + $this->setMessage(Text::plural($ntext, count($cid))); + } + } + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&menutype=' . $app->getUserState('com_menus.items.menutype'), + false + ) + ); + } + + /** + * Method to publish a list of items + * + * @return void + * + * @since 3.6.0 + */ + public function publish() + { + // Check for request forgeries + $this->checkToken(); + + // Get items to publish from the request. + $cid = (array) $this->input->get('cid', array(), 'int'); + $data = array('publish' => 1, 'unpublish' => 0, 'trash' => -2, 'report' => -3); + $task = $this->getTask(); + $value = ArrayHelper::getValue($data, $task, 0, 'int'); + + // Remove zero values resulting from input filter + $cid = array_filter($cid); + + if (empty($cid)) { + try { + Log::add(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + $this->setMessage(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), 'warning'); + } + } else { + // Get the model. + $model = $this->getModel(); + + // Publish the items. + try { + $model->publish($cid, $value); + $errors = $model->getErrors(); + $messageType = 'message'; + + if ($value == 1) { + if ($errors) { + $messageType = 'error'; + $ntext = $this->text_prefix . '_N_ITEMS_FAILED_PUBLISHING'; + } else { + $ntext = $this->text_prefix . '_N_ITEMS_PUBLISHED'; + } + } elseif ($value == 0) { + $ntext = $this->text_prefix . '_N_ITEMS_UNPUBLISHED'; + } else { + $ntext = $this->text_prefix . '_N_ITEMS_TRASHED'; + } + + $this->setMessage(Text::plural($ntext, count($cid)), $messageType); + } catch (\Exception $e) { + $this->setMessage($e->getMessage(), 'error'); + } + } + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list . '&menutype=' . + $this->app->getUserState('com_menus.items.menutype'), + false + ) + ); + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + return '&menutype=' . $this->app->getUserState('com_menus.items.menutype'); + } } diff --git a/administrator/components/com_menus/src/Controller/MenuController.php b/administrator/components/com_menus/src/Controller/MenuController.php index c2d2cd536c49b..ab184687475c2 100644 --- a/administrator/components/com_menus/src/Controller/MenuController.php +++ b/administrator/components/com_menus/src/Controller/MenuController.php @@ -1,4 +1,5 @@ setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); - } - - /** - * Method to save a menu item. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 1.6 - */ - public function save($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - $app = $this->app; - $data = $this->input->post->get('jform', array(), 'array'); - $context = 'com_menus.edit.menu'; - $task = $this->getTask(); - $recordId = $this->input->getInt('id'); - - // Prevent using 'main' as menutype as this is reserved for backend menus - if (strtolower($data['menutype']) == 'main') - { - $this->setMessage(Text::_('COM_MENUS_ERROR_MENUTYPE'), 'error'); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); - - return false; - } - - $data['menutype'] = InputFilter::getInstance()->clean($data['menutype'], 'TRIM'); - - // Populate the row id from the session. - $data['id'] = $recordId; - - // Get the model and attempt to validate the posted data. - /** @var \Joomla\Component\Menus\Administrator\Model\MenuModel $model */ - $model = $this->getModel('Menu', '', ['ignore_request' => false]); - $form = $model->getForm(); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $app->setUserState($context . '.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); - - return false; - } - - if (isset($validData['preset'])) - { - $preset = trim($validData['preset']) ?: null; - - unset($validData['preset']); - } - - // Attempt to save the data. - if (!$model->save($validData)) - { - // Save the data in the session. - $app->setUserState($context . '.data', $validData); - - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); - - return false; - } - - // Import the preset selected - if (isset($preset) && $data['client_id'] == 1) - { - // Menu Type has not been saved yet. Make sure items get the real menutype. - $menutype = ApplicationHelper::stringURLSafe($data['menutype']); - - try - { - MenusHelper::installPreset($preset, $menutype); - - $this->setMessage(Text::_('COM_MENUS_PRESET_IMPORT_SUCCESS')); - } - catch (\Exception $e) - { - // Save was successful but the preset could not be loaded. Let it through with just a warning - $this->setMessage(Text::sprintf('COM_MENUS_PRESET_IMPORT_FAILED', $e->getMessage())); - } - } - else - { - $this->setMessage(Text::_('COM_MENUS_MENU_SAVE_SUCCESS')); - } - - // Redirect the user and adjust session state based on the chosen task. - switch ($task) - { - case 'apply': - // Set the record data in the session. - $recordId = $model->getState($this->context . '.id'); - $this->holdEditId($context, $recordId); - $app->setUserState($context . '.data', null); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); - break; - - case 'save2new': - // Clear the record id and data from the session. - $this->releaseEditId($context, $recordId); - $app->setUserState($context . '.data', null); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit', false)); - break; - - default: - // Clear the record id and data from the session. - $this->releaseEditId($context, $recordId); - $app->setUserState($context . '.data', null); - - // Redirect to the list screen. - $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); - break; - } - } - - /** - * Method to display a menu as preset xml. - * - * @return boolean True if successful, false otherwise. - * - * @since 3.8.0 - */ - public function exportXml() - { - // Check for request forgeries. - $this->checkToken(); - - $cid = (array) $this->input->get('cid', array(), 'int'); - - // We know the first element is the one we need because we don't allow multi selection of rows - $id = empty($cid) ? 0 : reset($cid); - - if ($id === 0) - { - $this->setMessage(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), 'warning'); - - $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); - - return false; - } - - $model = $this->getModel('Menu'); - $item = $model->getItem($id); - - if (!$item->menutype) - { - $this->setMessage(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), 'warning'); - - $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); - - return false; - } - - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&menutype=' . $item->menutype . '&format=xml', false)); - - return true; - } + /** + * Dummy method to redirect back to standard controller + * + * @param boolean $cachable If true, the view output will be cached. + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return void + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); + } + + /** + * Method to save a menu item. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 1.6 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + $app = $this->app; + $data = $this->input->post->get('jform', array(), 'array'); + $context = 'com_menus.edit.menu'; + $task = $this->getTask(); + $recordId = $this->input->getInt('id'); + + // Prevent using 'main' as menutype as this is reserved for backend menus + if (strtolower($data['menutype']) == 'main') { + $this->setMessage(Text::_('COM_MENUS_ERROR_MENUTYPE'), 'error'); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); + + return false; + } + + $data['menutype'] = InputFilter::getInstance()->clean($data['menutype'], 'TRIM'); + + // Populate the row id from the session. + $data['id'] = $recordId; + + // Get the model and attempt to validate the posted data. + /** @var \Joomla\Component\Menus\Administrator\Model\MenuModel $model */ + $model = $this->getModel('Menu', '', ['ignore_request' => false]); + $form = $model->getForm(); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $app->setUserState($context . '.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); + + return false; + } + + if (isset($validData['preset'])) { + $preset = trim($validData['preset']) ?: null; + + unset($validData['preset']); + } + + // Attempt to save the data. + if (!$model->save($validData)) { + // Save the data in the session. + $app->setUserState($context . '.data', $validData); + + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); + + return false; + } + + // Import the preset selected + if (isset($preset) && $data['client_id'] == 1) { + // Menu Type has not been saved yet. Make sure items get the real menutype. + $menutype = ApplicationHelper::stringURLSafe($data['menutype']); + + try { + MenusHelper::installPreset($preset, $menutype); + + $this->setMessage(Text::_('COM_MENUS_PRESET_IMPORT_SUCCESS')); + } catch (\Exception $e) { + // Save was successful but the preset could not be loaded. Let it through with just a warning + $this->setMessage(Text::sprintf('COM_MENUS_PRESET_IMPORT_FAILED', $e->getMessage())); + } + } else { + $this->setMessage(Text::_('COM_MENUS_MENU_SAVE_SUCCESS')); + } + + // Redirect the user and adjust session state based on the chosen task. + switch ($task) { + case 'apply': + // Set the record data in the session. + $recordId = $model->getState($this->context . '.id'); + $this->holdEditId($context, $recordId); + $app->setUserState($context . '.data', null); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); + break; + + case 'save2new': + // Clear the record id and data from the session. + $this->releaseEditId($context, $recordId); + $app->setUserState($context . '.data', null); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit', false)); + break; + + default: + // Clear the record id and data from the session. + $this->releaseEditId($context, $recordId); + $app->setUserState($context . '.data', null); + + // Redirect to the list screen. + $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); + break; + } + } + + /** + * Method to display a menu as preset xml. + * + * @return boolean True if successful, false otherwise. + * + * @since 3.8.0 + */ + public function exportXml() + { + // Check for request forgeries. + $this->checkToken(); + + $cid = (array) $this->input->get('cid', array(), 'int'); + + // We know the first element is the one we need because we don't allow multi selection of rows + $id = empty($cid) ? 0 : reset($cid); + + if ($id === 0) { + $this->setMessage(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), 'warning'); + + $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); + + return false; + } + + $model = $this->getModel('Menu'); + $item = $model->getItem($id); + + if (!$item->menutype) { + $this->setMessage(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), 'warning'); + + $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); + + return false; + } + + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&menutype=' . $item->menutype . '&format=xml', false)); + + return true; + } } diff --git a/administrator/components/com_menus/src/Controller/MenusController.php b/administrator/components/com_menus/src/Controller/MenusController.php index 3bde6a0490a23..8f26c06e0c95d 100644 --- a/administrator/components/com_menus/src/Controller/MenusController.php +++ b/administrator/components/com_menus/src/Controller/MenusController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Remove an item. - * - * @return void - * - * @since 1.6 - */ - public function delete() - { - // Check for request forgeries - $this->checkToken(); - - $user = $this->app->getIdentity(); - $cids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $cids = array_filter($cids); - - if (empty($cids)) - { - $this->setMessage(Text::_('COM_MENUS_NO_MENUS_SELECTED'), 'warning'); - } - else - { - // Access checks. - foreach ($cids as $i => $id) - { - if (!$user->authorise('core.delete', 'com_menus.menu.' . (int) $id)) - { - // Prune items that you can't change. - unset($cids[$i]); - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'error'); - } - } - - if (count($cids) > 0) - { - // Get the model. - /** @var \Joomla\Component\Menus\Administrator\Model\MenuModel $model */ - $model = $this->getModel(); - - // Remove the items. - if (!$model->delete($cids)) - { - $this->setMessage($model->getError(), 'error'); - } - else - { - $this->setMessage(Text::plural('COM_MENUS_N_MENUS_DELETED', count($cids))); - } - } - } - - $this->setRedirect('index.php?option=com_menus&view=menus'); - } - - /** - * Temporary method. This should go into the 1.5 to 1.6 upgrade routines. - * - * @return void - * - * @since 1.6 - * - * @deprecated 5.0 Will be removed without replacement as it was only used for the 1.5 to 1.6 upgrade - */ - public function resync() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $parts = null; - - try - { - $query->select( - [ - $db->quoteName('element'), - $db->quoteName('extension_id'), - ] - ) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('component')); - $db->setQuery($query); - - $components = $db->loadAssocList('element', 'extension_id'); - } - catch (\RuntimeException $e) - { - $this->setMessage($e->getMessage(), 'warning'); - - return; - } - - // Load all the component menu links - $query->select( - [ - $db->quoteName('id'), - $db->quoteName('link'), - $db->quoteName('component_id'), - ] - ) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('type') . ' = ' . $db->quote('component.item')); - $db->setQuery($query); - - try - { - $items = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setMessage($e->getMessage(), 'warning'); - - return; - } - - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('component_id') . ' = :componentId') - ->where($db->quoteName('id') . ' = :itemId') - ->bind(':componentId', $componentId, ParameterType::INTEGER) - ->bind(':itemId', $itemId, ParameterType::INTEGER); - - foreach ($items as $item) - { - // Parse the link. - parse_str(parse_url($item->link, PHP_URL_QUERY), $parts); - $itemId = $item->id; - - // Tease out the option. - if (isset($parts['option'])) - { - $option = $parts['option']; - - // Lookup the component ID - if (isset($components[$option])) - { - $componentId = $components[$option]; - } - else - { - // Mismatch. Needs human intervention. - $componentId = -1; - } - - // Check for mis-matched component ids in the menu link. - if ($item->component_id != $componentId) - { - // Update the menu table. - $log = "Link $item->id refers to $item->component_id, converting to $componentId ($item->link)"; - echo "
    $log"; - - try - { - $db->setQuery($query)->execute(); - } - catch (\RuntimeException $e) - { - $this->setMessage($e->getMessage(), 'warning'); - - return; - } - } - } - } - } + /** + * Display the view + * + * @param boolean $cachable If true, the view output will be cached. + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return void + * + * @since 1.6 + */ + public function display($cachable = false, $urlparams = false) + { + } + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Menu', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Remove an item. + * + * @return void + * + * @since 1.6 + */ + public function delete() + { + // Check for request forgeries + $this->checkToken(); + + $user = $this->app->getIdentity(); + $cids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $cids = array_filter($cids); + + if (empty($cids)) { + $this->setMessage(Text::_('COM_MENUS_NO_MENUS_SELECTED'), 'warning'); + } else { + // Access checks. + foreach ($cids as $i => $id) { + if (!$user->authorise('core.delete', 'com_menus.menu.' . (int) $id)) { + // Prune items that you can't change. + unset($cids[$i]); + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'error'); + } + } + + if (count($cids) > 0) { + // Get the model. + /** @var \Joomla\Component\Menus\Administrator\Model\MenuModel $model */ + $model = $this->getModel(); + + // Remove the items. + if (!$model->delete($cids)) { + $this->setMessage($model->getError(), 'error'); + } else { + $this->setMessage(Text::plural('COM_MENUS_N_MENUS_DELETED', count($cids))); + } + } + } + + $this->setRedirect('index.php?option=com_menus&view=menus'); + } + + /** + * Temporary method. This should go into the 1.5 to 1.6 upgrade routines. + * + * @return void + * + * @since 1.6 + * + * @deprecated 5.0 Will be removed without replacement as it was only used for the 1.5 to 1.6 upgrade + */ + public function resync() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $parts = null; + + try { + $query->select( + [ + $db->quoteName('element'), + $db->quoteName('extension_id'), + ] + ) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')); + $db->setQuery($query); + + $components = $db->loadAssocList('element', 'extension_id'); + } catch (\RuntimeException $e) { + $this->setMessage($e->getMessage(), 'warning'); + + return; + } + + // Load all the component menu links + $query->select( + [ + $db->quoteName('id'), + $db->quoteName('link'), + $db->quoteName('component_id'), + ] + ) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component.item')); + $db->setQuery($query); + + try { + $items = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setMessage($e->getMessage(), 'warning'); + + return; + } + + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('component_id') . ' = :componentId') + ->where($db->quoteName('id') . ' = :itemId') + ->bind(':componentId', $componentId, ParameterType::INTEGER) + ->bind(':itemId', $itemId, ParameterType::INTEGER); + + foreach ($items as $item) { + // Parse the link. + parse_str(parse_url($item->link, PHP_URL_QUERY), $parts); + $itemId = $item->id; + + // Tease out the option. + if (isset($parts['option'])) { + $option = $parts['option']; + + // Lookup the component ID + if (isset($components[$option])) { + $componentId = $components[$option]; + } else { + // Mismatch. Needs human intervention. + $componentId = -1; + } + + // Check for mis-matched component ids in the menu link. + if ($item->component_id != $componentId) { + // Update the menu table. + $log = "Link $item->id refers to $item->component_id, converting to $componentId ($item->link)"; + echo "
    $log"; + + try { + $db->setQuery($query)->execute(); + } catch (\RuntimeException $e) { + $this->setMessage($e->getMessage(), 'warning'); + + return; + } + } + } + } + } } diff --git a/administrator/components/com_menus/src/Extension/MenusComponent.php b/administrator/components/com_menus/src/Extension/MenusComponent.php index cc611ed3f26e3..d16fc8dc60b30 100644 --- a/administrator/components/com_menus/src/Extension/MenusComponent.php +++ b/administrator/components/com_menus/src/Extension/MenusComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('menus', new Menus); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('menus', new Menus()); + } } diff --git a/administrator/components/com_menus/src/Field/MenuItemByTypeField.php b/administrator/components/com_menus/src/Field/MenuItemByTypeField.php index 5df789ff8640e..8a26f494b84a7 100644 --- a/administrator/components/com_menus/src/Field/MenuItemByTypeField.php +++ b/administrator/components/com_menus/src/Field/MenuItemByTypeField.php @@ -1,4 +1,5 @@ $name; - } - - return parent::__get($name); - } - - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 3.8.0 - */ - public function __set($name, $value) - { - switch ($name) - { - case 'menuType': - $this->menuType = (string) $value; - break; - - case 'clientId': - $this->clientId = (int) $value; - break; - - case 'language': - case 'published': - case 'disable': - $value = (string) $value; - $this->$name = $value ? explode(',', $value) : array(); - break; - - default: - parent::__set($name, $value); - } - } - - /** - * Method to attach a JForm object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see \Joomla\CMS\Form\FormField::setup() - * @since 3.8.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $result = parent::setup($element, $value, $group); - - if ($result == true) - { - $menuType = (string) $this->element['menu_type']; - - if (!$menuType) - { - $app = Factory::getApplication(); - $currentMenuType = $app->getUserState('com_menus.items.menutype', ''); - $menuType = $app->input->getString('menutype', $currentMenuType); - } - - $this->menuType = $menuType; - $this->clientId = (int) $this->element['client_id']; - $this->published = $this->element['published'] ? explode(',', (string) $this->element['published']) : array(); - $this->disable = $this->element['disable'] ? explode(',', (string) $this->element['disable']) : array(); - $this->language = $this->element['language'] ? explode(',', (string) $this->element['language']) : array(); - } - - return $result; - } - - /** - * Method to get the field option groups. - * - * @return array The field option objects as a nested array in groups. - * - * @since 3.8.0 - */ - protected function getGroups() - { - $groups = array(); - - $menuType = $this->menuType; - - // Get the menu items. - $items = MenusHelper::getMenuLinks($menuType, 0, 0, $this->published, $this->language, $this->clientId); - - // Build group for a specific menu type. - if ($menuType) - { - // If the menutype is empty, group the items by menutype. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__menu_types')) - ->where($db->quoteName('menutype') . ' = :menuType') - ->bind(':menuType', $menuType); - $db->setQuery($query); - - try - { - $menuTitle = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $menuTitle = $menuType; - } - - // Initialize the group. - $groups[$menuTitle] = array(); - - // Build the options array. - foreach ($items as $key => $link) - { - // Unset if item is menu_item_root - if ($link->text === 'Menu_Item_Root') - { - unset($items[$key]); - continue; - } - - $levelPrefix = str_repeat('- ', max(0, $link->level - 1)); - - // Displays language code if not set to All - if ($link->language !== '*') - { - $lang = ' (' . $link->language . ')'; - } - else - { - $lang = ''; - } - - $groups[$menuTitle][] = HTMLHelper::_('select.option', - $link->value, $levelPrefix . $link->text . $lang, - 'value', - 'text', - in_array($link->type, $this->disable) - ); - } - } - // Build groups for all menu types. - else - { - // Build the groups arrays. - foreach ($items as $menu) - { - // Initialize the group. - $groups[$menu->title] = array(); - - // Build the options array. - foreach ($menu->links as $link) - { - $levelPrefix = str_repeat('- ', max(0, $link->level - 1)); - - // Displays language code if not set to All - if ($link->language !== '*') - { - $lang = ' (' . $link->language . ')'; - } - else - { - $lang = ''; - } - - $groups[$menu->title][] = HTMLHelper::_('select.option', - $link->value, - $levelPrefix . $link->text . $lang, - 'value', - 'text', - in_array($link->type, $this->disable) - ); - } - } - } - - // Merge any additional groups in the XML definition. - $groups = array_merge(parent::getGroups(), $groups); - - return $groups; - } + /** + * The form field type. + * + * @var string + * @since 3.8.0 + */ + public $type = 'MenuItemByType'; + + /** + * The menu type. + * + * @var string + * @since 3.8.0 + */ + protected $menuType; + + /** + * The client id. + * + * @var string + * @since 3.8.0 + */ + protected $clientId; + + /** + * The language. + * + * @var array + * @since 3.8.0 + */ + protected $language; + + /** + * The published status. + * + * @var array + * @since 3.8.0 + */ + protected $published; + + /** + * The disabled status. + * + * @var array + * @since 3.8.0 + */ + protected $disable; + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 3.8.0 + */ + public function __get($name) + { + switch ($name) { + case 'menuType': + case 'clientId': + case 'language': + case 'published': + case 'disable': + return $this->$name; + } + + return parent::__get($name); + } + + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 3.8.0 + */ + public function __set($name, $value) + { + switch ($name) { + case 'menuType': + $this->menuType = (string) $value; + break; + + case 'clientId': + $this->clientId = (int) $value; + break; + + case 'language': + case 'published': + case 'disable': + $value = (string) $value; + $this->$name = $value ? explode(',', $value) : array(); + break; + + default: + parent::__set($name, $value); + } + } + + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see \Joomla\CMS\Form\FormField::setup() + * @since 3.8.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $result = parent::setup($element, $value, $group); + + if ($result == true) { + $menuType = (string) $this->element['menu_type']; + + if (!$menuType) { + $app = Factory::getApplication(); + $currentMenuType = $app->getUserState('com_menus.items.menutype', ''); + $menuType = $app->input->getString('menutype', $currentMenuType); + } + + $this->menuType = $menuType; + $this->clientId = (int) $this->element['client_id']; + $this->published = $this->element['published'] ? explode(',', (string) $this->element['published']) : array(); + $this->disable = $this->element['disable'] ? explode(',', (string) $this->element['disable']) : array(); + $this->language = $this->element['language'] ? explode(',', (string) $this->element['language']) : array(); + } + + return $result; + } + + /** + * Method to get the field option groups. + * + * @return array The field option objects as a nested array in groups. + * + * @since 3.8.0 + */ + protected function getGroups() + { + $groups = array(); + + $menuType = $this->menuType; + + // Get the menu items. + $items = MenusHelper::getMenuLinks($menuType, 0, 0, $this->published, $this->language, $this->clientId); + + // Build group for a specific menu type. + if ($menuType) { + // If the menutype is empty, group the items by menutype. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__menu_types')) + ->where($db->quoteName('menutype') . ' = :menuType') + ->bind(':menuType', $menuType); + $db->setQuery($query); + + try { + $menuTitle = $db->loadResult(); + } catch (\RuntimeException $e) { + $menuTitle = $menuType; + } + + // Initialize the group. + $groups[$menuTitle] = array(); + + // Build the options array. + foreach ($items as $key => $link) { + // Unset if item is menu_item_root + if ($link->text === 'Menu_Item_Root') { + unset($items[$key]); + continue; + } + + $levelPrefix = str_repeat('- ', max(0, $link->level - 1)); + + // Displays language code if not set to All + if ($link->language !== '*') { + $lang = ' (' . $link->language . ')'; + } else { + $lang = ''; + } + + $groups[$menuTitle][] = HTMLHelper::_( + 'select.option', + $link->value, + $levelPrefix . $link->text . $lang, + 'value', + 'text', + in_array($link->type, $this->disable) + ); + } + } + // Build groups for all menu types. + else { + // Build the groups arrays. + foreach ($items as $menu) { + // Initialize the group. + $groups[$menu->title] = array(); + + // Build the options array. + foreach ($menu->links as $link) { + $levelPrefix = str_repeat('- ', max(0, $link->level - 1)); + + // Displays language code if not set to All + if ($link->language !== '*') { + $lang = ' (' . $link->language . ')'; + } else { + $lang = ''; + } + + $groups[$menu->title][] = HTMLHelper::_( + 'select.option', + $link->value, + $levelPrefix . $link->text . $lang, + 'value', + 'text', + in_array($link->type, $this->disable) + ); + } + } + } + + // Merge any additional groups in the XML definition. + $groups = array_merge(parent::getGroups(), $groups); + + return $groups; + } } diff --git a/administrator/components/com_menus/src/Field/MenuOrderingField.php b/administrator/components/com_menus/src/Field/MenuOrderingField.php index b0c89afd2bd5c..9de10a964ab44 100644 --- a/administrator/components/com_menus/src/Field/MenuOrderingField.php +++ b/administrator/components/com_menus/src/Field/MenuOrderingField.php @@ -1,4 +1,5 @@ form->getValue('parent_id', 0); - - if (!$parent_id) - { - return false; - } - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('a.id', 'value'), - $db->quoteName('a.title', 'text'), - $db->quoteName('a.client_id', 'clientId'), - ] - ) - ->from($db->quoteName('#__menu', 'a')) - - ->where($db->quoteName('a.published') . ' >= 0') - ->where($db->quoteName('a.parent_id') . ' = :parentId') - ->bind(':parentId', $parent_id, ParameterType::INTEGER); - - if ($menuType = $this->form->getValue('menutype')) - { - $query->where($db->quoteName('a.menutype') . ' = :menuType') - ->bind(':menuType', $menuType); - } - else - { - $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('')); - } - - $query->order($db->quoteName('a.lft') . ' ASC'); - - // Get the options. - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Allow translation of custom admin menus - foreach ($options as &$option) - { - if ($option->clientId != 0) - { - $option->text = Text::_($option->text); - } - } - - $options = array_merge( - array(array('value' => '-1', 'text' => Text::_('COM_MENUS_ITEM_FIELD_ORDERING_VALUE_FIRST'))), - $options, - array(array('value' => '-2', 'text' => Text::_('COM_MENUS_ITEM_FIELD_ORDERING_VALUE_LAST'))) - ); - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); - - return $options; - } - - /** - * Method to get the field input markup. - * - * @return string The field input markup. - * - * @since 1.7 - */ - protected function getInput() - { - if ($this->form->getValue('id', 0) == 0) - { - return '' . Text::_('COM_MENUS_ITEM_FIELD_ORDERING_TEXT') . ''; - } - else - { - return parent::getInput(); - } - } + /** + * The form field type. + * + * @var string + * @since 1.7 + */ + protected $type = 'MenuOrdering'; + + /** + * Method to get the list of siblings in a menu. + * The method requires that parent be set. + * + * @return array|boolean The field option objects or false if the parent field has not been set + * + * @since 1.7 + */ + protected function getOptions() + { + $options = array(); + + // Get the parent + $parent_id = (int) $this->form->getValue('parent_id', 0); + + if (!$parent_id) { + return false; + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('a.id', 'value'), + $db->quoteName('a.title', 'text'), + $db->quoteName('a.client_id', 'clientId'), + ] + ) + ->from($db->quoteName('#__menu', 'a')) + + ->where($db->quoteName('a.published') . ' >= 0') + ->where($db->quoteName('a.parent_id') . ' = :parentId') + ->bind(':parentId', $parent_id, ParameterType::INTEGER); + + if ($menuType = $this->form->getValue('menutype')) { + $query->where($db->quoteName('a.menutype') . ' = :menuType') + ->bind(':menuType', $menuType); + } else { + $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('')); + } + + $query->order($db->quoteName('a.lft') . ' ASC'); + + // Get the options. + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Allow translation of custom admin menus + foreach ($options as &$option) { + if ($option->clientId != 0) { + $option->text = Text::_($option->text); + } + } + + $options = array_merge( + array(array('value' => '-1', 'text' => Text::_('COM_MENUS_ITEM_FIELD_ORDERING_VALUE_FIRST'))), + $options, + array(array('value' => '-2', 'text' => Text::_('COM_MENUS_ITEM_FIELD_ORDERING_VALUE_LAST'))) + ); + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); + + return $options; + } + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.7 + */ + protected function getInput() + { + if ($this->form->getValue('id', 0) == 0) { + return '' . Text::_('COM_MENUS_ITEM_FIELD_ORDERING_TEXT') . ''; + } else { + return parent::getInput(); + } + } } diff --git a/administrator/components/com_menus/src/Field/MenuParentField.php b/administrator/components/com_menus/src/Field/MenuParentField.php index 1eaa109792acd..5fa3de007bc35 100644 --- a/administrator/components/com_menus/src/Field/MenuParentField.php +++ b/administrator/components/com_menus/src/Field/MenuParentField.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - 'DISTINCT ' . $db->quoteName('a.id', 'value'), - $db->quoteName('a.title', 'text'), - $db->quoteName('a.level'), - $db->quoteName('a.lft'), - ] - ) - ->from($db->quoteName('#__menu', 'a')); - - // Filter by menu type. - if ($menuType = $this->form->getValue('menutype')) - { - $query->where($db->quoteName('a.menutype') . ' = :menuType') - ->bind(':menuType', $menuType); - } - else - { - // Skip special menu types - $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('')); - $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('main')); - } - - // Filter by client id. - $clientId = $this->getAttribute('clientid'); - - if (!is_null($clientId)) - { - $clientId = (int) $clientId; - $query->where($db->quoteName('a.client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - // Prevent parenting to children of this item. - if ($id = (int) $this->form->getValue('id')) - { - $query->join('LEFT', $db->quoteName('#__menu', 'p'), $db->quoteName('p.id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER) - ->where( - 'NOT(' . $db->quoteName('a.lft') . ' >= ' . $db->quoteName('p.lft') - . ' AND ' . $db->quoteName('a.rgt') . ' <= ' . $db->quoteName('p.rgt') . ')' - ); - } - - $query->where($db->quoteName('a.published') . ' != -2') - ->order($db->quoteName('a.lft') . ' ASC'); - - // Get the options. - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Pad the option text with spaces using depth level as a multiplier. - for ($i = 0, $n = count($options); $i < $n; $i++) - { - if ($clientId != 0) - { - // Allow translation of custom admin menus - $options[$i]->text = str_repeat('- ', $options[$i]->level) . Text::_($options[$i]->text); - } - else - { - $options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->text; - } - } - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'MenuParent'; + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 1.6 + */ + protected function getOptions() + { + $options = array(); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + 'DISTINCT ' . $db->quoteName('a.id', 'value'), + $db->quoteName('a.title', 'text'), + $db->quoteName('a.level'), + $db->quoteName('a.lft'), + ] + ) + ->from($db->quoteName('#__menu', 'a')); + + // Filter by menu type. + if ($menuType = $this->form->getValue('menutype')) { + $query->where($db->quoteName('a.menutype') . ' = :menuType') + ->bind(':menuType', $menuType); + } else { + // Skip special menu types + $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('')); + $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('main')); + } + + // Filter by client id. + $clientId = $this->getAttribute('clientid'); + + if (!is_null($clientId)) { + $clientId = (int) $clientId; + $query->where($db->quoteName('a.client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + // Prevent parenting to children of this item. + if ($id = (int) $this->form->getValue('id')) { + $query->join('LEFT', $db->quoteName('#__menu', 'p'), $db->quoteName('p.id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER) + ->where( + 'NOT(' . $db->quoteName('a.lft') . ' >= ' . $db->quoteName('p.lft') + . ' AND ' . $db->quoteName('a.rgt') . ' <= ' . $db->quoteName('p.rgt') . ')' + ); + } + + $query->where($db->quoteName('a.published') . ' != -2') + ->order($db->quoteName('a.lft') . ' ASC'); + + // Get the options. + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Pad the option text with spaces using depth level as a multiplier. + for ($i = 0, $n = count($options); $i < $n; $i++) { + if ($clientId != 0) { + // Allow translation of custom admin menus + $options[$i]->text = str_repeat('- ', $options[$i]->level) . Text::_($options[$i]->text); + } else { + $options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->text; + } + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); + + return $options; + } } diff --git a/administrator/components/com_menus/src/Field/MenuPresetField.php b/administrator/components/com_menus/src/Field/MenuPresetField.php index 288da3c3185d9..8b76ccc42ff43 100644 --- a/administrator/components/com_menus/src/Field/MenuPresetField.php +++ b/administrator/components/com_menus/src/Field/MenuPresetField.php @@ -1,4 +1,5 @@ name, Text::_($preset->title)); - } - - return array_merge(parent::getOptions(), $options); - } + /** + * The form field type. + * + * @var string + * + * @since 3.8.0 + */ + protected $type = 'MenuPreset'; + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.8.0 + */ + protected function getOptions() + { + $options = array(); + $presets = MenusHelper::getPresets(); + + foreach ($presets as $preset) { + $options[] = HTMLHelper::_('select.option', $preset->name, Text::_($preset->title)); + } + + return array_merge(parent::getOptions(), $options); + } } diff --git a/administrator/components/com_menus/src/Field/MenutypeField.php b/administrator/components/com_menus/src/Field/MenutypeField.php index c784466371fe4..7b6dea5ea681a 100644 --- a/administrator/components/com_menus/src/Field/MenutypeField.php +++ b/administrator/components/com_menus/src/Field/MenutypeField.php @@ -1,4 +1,5 @@ form->getValue('id'); - $size = (string) ($v = $this->element['size']) ? ' size="' . $v . '"' : ''; - $class = (string) ($v = $this->element['class']) ? ' class="form-control ' . $v . '"' : ' class="form-control"'; - $required = (string) $this->element['required'] ? ' required="required"' : ''; - $clientId = (int) $this->element['clientid'] ?: 0; - - // Get a reverse lookup of the base link URL to Title - switch ($this->value) - { - case 'url': - $value = Text::_('COM_MENUS_TYPE_EXTERNAL_URL'); - break; - - case 'alias': - $value = Text::_('COM_MENUS_TYPE_ALIAS'); - break; - - case 'separator': - $value = Text::_('COM_MENUS_TYPE_SEPARATOR'); - break; - - case 'heading': - $value = Text::_('COM_MENUS_TYPE_HEADING'); - break; - - case 'container': - $value = Text::_('COM_MENUS_TYPE_CONTAINER'); - break; - - default: - $link = $this->form->getValue('link'); - $value = ''; - - if ($link !== null) - { - $model = Factory::getApplication()->bootComponent('com_menus') - ->getMVCFactory()->createModel('Menutypes', 'Administrator', array('ignore_request' => true)); - $model->setState('client_id', $clientId); - - $rlu = $model->getReverseLookup(); - - // Clean the link back to the option, view and layout - $value = Text::_(ArrayHelper::getValue($rlu, MenusHelper::getLinkKey($link))); - } - break; - } - - $link = Route::_('index.php?option=com_menus&view=menutypes&tmpl=component&client_id=' . $clientId . '&recordId=' . $recordId); - $html[] = ''; - $html[] = ''; - $html[] = HTMLHelper::_( - 'bootstrap.renderModal', - 'menuTypeModal', - array( - 'url' => $link, - 'title' => Text::_('COM_MENUS_ITEM_FIELD_TYPE_LABEL'), - 'width' => '800px', - 'height' => '300px', - 'modalWidth' => 80, - 'bodyHeight' => 70, - 'footer' => '' - ) - ); - - // This hidden field has an ID so it can be used for showon attributes - $html[] = ''; - - return implode("\n", $html); - } + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'menutype'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $html = array(); + $recordId = (int) $this->form->getValue('id'); + $size = (string) ($v = $this->element['size']) ? ' size="' . $v . '"' : ''; + $class = (string) ($v = $this->element['class']) ? ' class="form-control ' . $v . '"' : ' class="form-control"'; + $required = (string) $this->element['required'] ? ' required="required"' : ''; + $clientId = (int) $this->element['clientid'] ?: 0; + + // Get a reverse lookup of the base link URL to Title + switch ($this->value) { + case 'url': + $value = Text::_('COM_MENUS_TYPE_EXTERNAL_URL'); + break; + + case 'alias': + $value = Text::_('COM_MENUS_TYPE_ALIAS'); + break; + + case 'separator': + $value = Text::_('COM_MENUS_TYPE_SEPARATOR'); + break; + + case 'heading': + $value = Text::_('COM_MENUS_TYPE_HEADING'); + break; + + case 'container': + $value = Text::_('COM_MENUS_TYPE_CONTAINER'); + break; + + default: + $link = $this->form->getValue('link'); + $value = ''; + + if ($link !== null) { + $model = Factory::getApplication()->bootComponent('com_menus') + ->getMVCFactory()->createModel('Menutypes', 'Administrator', array('ignore_request' => true)); + $model->setState('client_id', $clientId); + + $rlu = $model->getReverseLookup(); + + // Clean the link back to the option, view and layout + $value = Text::_(ArrayHelper::getValue($rlu, MenusHelper::getLinkKey($link))); + } + break; + } + + $link = Route::_('index.php?option=com_menus&view=menutypes&tmpl=component&client_id=' . $clientId . '&recordId=' . $recordId); + $html[] = ''; + $html[] = ''; + $html[] = HTMLHelper::_( + 'bootstrap.renderModal', + 'menuTypeModal', + array( + 'url' => $link, + 'title' => Text::_('COM_MENUS_ITEM_FIELD_TYPE_LABEL'), + 'width' => '800px', + 'height' => '300px', + 'modalWidth' => 80, + 'bodyHeight' => 70, + 'footer' => '' + ) + ); + + // This hidden field has an ID so it can be used for showon attributes + $html[] = ''; + + return implode("\n", $html); + } } diff --git a/administrator/components/com_menus/src/Field/Modal/MenuField.php b/administrator/components/com_menus/src/Field/Modal/MenuField.php index 092c1cc3b3529..257f0597ff791 100644 --- a/administrator/components/com_menus/src/Field/Modal/MenuField.php +++ b/administrator/components/com_menus/src/Field/Modal/MenuField.php @@ -1,4 +1,5 @@ $name; - } - - return parent::__get($name); - } - - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 3.7.0 - */ - public function __set($name, $value) - { - switch ($name) - { - case 'allowSelect': - case 'allowClear': - case 'allowNew': - case 'allowEdit': - case 'allowPropagate': - $value = (string) $value; - $this->$name = !($value === 'false' || $value === 'off' || $value === '0'); - break; - - default: - parent::__set($name, $value); - } - } - - /** - * Method to attach a JForm object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see FormField::setup() - * @since 3.7.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $return = parent::setup($element, $value, $group); - - if ($return) - { - $this->allowSelect = ((string) $this->element['select']) !== 'false'; - $this->allowClear = ((string) $this->element['clear']) !== 'false'; - $this->allowPropagate = ((string) $this->element['propagate']) === 'true'; - - // Creating/editing menu items is not supported in frontend. - $isAdministrator = Factory::getApplication()->isClient('administrator'); - $this->allowNew = $isAdministrator ? ((string) $this->element['new']) === 'true' : false; - $this->allowEdit = $isAdministrator ? ((string) $this->element['edit']) === 'true' : false; - } - - return $return; - } - - /** - * Method to get the field input markup. - * - * @return string The field input markup. - * - * @since 3.7.0 - */ - protected function getInput() - { - $clientId = (int) $this->element['clientid']; - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - - // Load language - Factory::getLanguage()->load('com_menus', JPATH_ADMINISTRATOR); - - // The active article id field. - $value = (int) $this->value ?: ''; - - // Create the modal id. - $modalId = 'Item_' . $this->id; - - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - - // Add the modal field script to the document head. - $wa->useScript('field.modal-fields'); - - // Script to proxy the select modal function to the modal-fields.js file. - if ($this->allowSelect) - { - static $scriptSelect = null; - - if (is_null($scriptSelect)) - { - $scriptSelect = array(); - } - - if (!isset($scriptSelect[$this->id])) - { - $wa->addInlineScript(" + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'Modal_Menu'; + + /** + * Determinate, if the select button is shown + * + * @var boolean + * @since 3.7.0 + */ + protected $allowSelect = true; + + /** + * Determinate, if the clear button is shown + * + * @var boolean + * @since 3.7.0 + */ + protected $allowClear = true; + + /** + * Determinate, if the create button is shown + * + * @var boolean + * @since 3.7.0 + */ + protected $allowNew = false; + + /** + * Determinate, if the edit button is shown + * + * @var boolean + * @since 3.7.0 + */ + protected $allowEdit = false; + + /** + * Determinate, if the propagate button is shown + * + * @var boolean + * @since 3.9.0 + */ + protected $allowPropagate = false; + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 3.7.0 + */ + public function __get($name) + { + switch ($name) { + case 'allowSelect': + case 'allowClear': + case 'allowNew': + case 'allowEdit': + case 'allowPropagate': + return $this->$name; + } + + return parent::__get($name); + } + + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 3.7.0 + */ + public function __set($name, $value) + { + switch ($name) { + case 'allowSelect': + case 'allowClear': + case 'allowNew': + case 'allowEdit': + case 'allowPropagate': + $value = (string) $value; + $this->$name = !($value === 'false' || $value === 'off' || $value === '0'); + break; + + default: + parent::__set($name, $value); + } + } + + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see FormField::setup() + * @since 3.7.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $return = parent::setup($element, $value, $group); + + if ($return) { + $this->allowSelect = ((string) $this->element['select']) !== 'false'; + $this->allowClear = ((string) $this->element['clear']) !== 'false'; + $this->allowPropagate = ((string) $this->element['propagate']) === 'true'; + + // Creating/editing menu items is not supported in frontend. + $isAdministrator = Factory::getApplication()->isClient('administrator'); + $this->allowNew = $isAdministrator ? ((string) $this->element['new']) === 'true' : false; + $this->allowEdit = $isAdministrator ? ((string) $this->element['edit']) === 'true' : false; + } + + return $return; + } + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 3.7.0 + */ + protected function getInput() + { + $clientId = (int) $this->element['clientid']; + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + + // Load language + Factory::getLanguage()->load('com_menus', JPATH_ADMINISTRATOR); + + // The active article id field. + $value = (int) $this->value ?: ''; + + // Create the modal id. + $modalId = 'Item_' . $this->id; + + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + + // Add the modal field script to the document head. + $wa->useScript('field.modal-fields'); + + // Script to proxy the select modal function to the modal-fields.js file. + if ($this->allowSelect) { + static $scriptSelect = null; + + if (is_null($scriptSelect)) { + $scriptSelect = array(); + } + + if (!isset($scriptSelect[$this->id])) { + $wa->addInlineScript( + " window.jSelectMenu_" . $this->id . " = function (id, title, object) { window.processModalSelect('Item', '" . $this->id . "', id, title, '', object); }", - [], - ['type' => 'module'] - ); - - Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); - - $scriptSelect[$this->id] = true; - } - } - - // Setup variables for display. - $linkSuffix = '&layout=modal&client_id=' . $clientId . '&tmpl=component&' . Session::getFormToken() . '=1'; - $linkItems = 'index.php?option=com_menus&view=items' . $linkSuffix; - $linkItem = 'index.php?option=com_menus&view=item' . $linkSuffix; - $modalTitle = Text::_('COM_MENUS_SELECT_A_MENUITEM'); - - if (isset($this->element['language'])) - { - $linkItems .= '&forcedLanguage=' . $this->element['language']; - $linkItem .= '&forcedLanguage=' . $this->element['language']; - $modalTitle .= ' — ' . $this->element['label']; - } - - $urlSelect = $linkItems . '&function=jSelectMenu_' . $this->id; - $urlEdit = $linkItem . '&task=item.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; - $urlNew = $linkItem . '&task=item.add'; - - if ($value) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $value, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $title = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - // Placeholder if option is present or not - if (empty($title)) - { - if ($this->element->option && (string) $this->element->option['value'] == '') - { - $title_holder = Text::_($this->element->option); - } - else - { - $title_holder = Text::_('COM_MENUS_SELECT_A_MENUITEM'); - } - } - - $title = empty($title) ? $title_holder : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); - - // The current menu item display field. - $html = ''; - - if ($this->allowSelect || $this->allowNew || $this->allowEdit || $this->allowClear) - { - $html .= ''; - } - - $html .= ''; - - // Select menu item button - if ($this->allowSelect) - { - $html .= '' - . ' ' . Text::_('JSELECT') - . ''; - } - - // New menu item button - if ($this->allowNew) - { - $html .= '' - . ' ' . Text::_('JACTION_CREATE') - . ''; - } - - // Edit menu item button - if ($this->allowEdit) - { - $html .= '' - . ' ' . Text::_('JACTION_EDIT') - . ''; - } - - // Clear menu item button - if ($this->allowClear) - { - $html .= '' - . ' ' . Text::_('JCLEAR') - . ''; - } - - // Propagate menu item button - if ($this->allowPropagate && count($languages) > 2) - { - // Strip off language tag at the end - $tagLength = (int) strlen($this->element['language']); - $callbackFunctionStem = substr("jSelectMenu_" . $this->id, 0, -$tagLength); - - $html .= '' - . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') - . ''; - } - - if ($this->allowSelect || $this->allowNew || $this->allowEdit || $this->allowClear) - { - $html .= ''; - } - - // Select menu item modal - if ($this->allowSelect) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalSelect' . $modalId, - array( - 'title' => $modalTitle, - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - } - - // New menu item modal - if ($this->allowNew) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalNew' . $modalId, - array( - 'title' => Text::_('COM_MENUS_NEW_MENUITEM'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlNew, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Edit menu item modal - if ($this->allowEdit) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalEdit' . $modalId, - array( - 'title' => Text::_('COM_MENUS_EDIT_MENUITEM'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlEdit, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Note: class='required' for client side validation. - $class = $this->required ? ' class="required modal-value"' : ''; - - // Placeholder if option is present or not when clearing field - if ($this->element->option && (string) $this->element->option['value'] == '') - { - $title_holder = Text::_($this->element->option); - } - else - { - $title_holder = Text::_('COM_MENUS_SELECT_A_MENUITEM'); - } - - $html .= ''; - - return $html; - } - - /** - * Method to get the field label markup. - * - * @return string The field label markup. - * - * @since 3.7.0 - */ - protected function getLabel() - { - return str_replace($this->id, $this->id . '_name', parent::getLabel()); - } + [], + ['type' => 'module'] + ); + + Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); + + $scriptSelect[$this->id] = true; + } + } + + // Setup variables for display. + $linkSuffix = '&layout=modal&client_id=' . $clientId . '&tmpl=component&' . Session::getFormToken() . '=1'; + $linkItems = 'index.php?option=com_menus&view=items' . $linkSuffix; + $linkItem = 'index.php?option=com_menus&view=item' . $linkSuffix; + $modalTitle = Text::_('COM_MENUS_SELECT_A_MENUITEM'); + + if (isset($this->element['language'])) { + $linkItems .= '&forcedLanguage=' . $this->element['language']; + $linkItem .= '&forcedLanguage=' . $this->element['language']; + $modalTitle .= ' — ' . $this->element['label']; + } + + $urlSelect = $linkItems . '&function=jSelectMenu_' . $this->id; + $urlEdit = $linkItem . '&task=item.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; + $urlNew = $linkItem . '&task=item.add'; + + if ($value) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $value, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $title = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + // Placeholder if option is present or not + if (empty($title)) { + if ($this->element->option && (string) $this->element->option['value'] == '') { + $title_holder = Text::_($this->element->option); + } else { + $title_holder = Text::_('COM_MENUS_SELECT_A_MENUITEM'); + } + } + + $title = empty($title) ? $title_holder : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + + // The current menu item display field. + $html = ''; + + if ($this->allowSelect || $this->allowNew || $this->allowEdit || $this->allowClear) { + $html .= ''; + } + + $html .= ''; + + // Select menu item button + if ($this->allowSelect) { + $html .= '' + . ' ' . Text::_('JSELECT') + . ''; + } + + // New menu item button + if ($this->allowNew) { + $html .= '' + . ' ' . Text::_('JACTION_CREATE') + . ''; + } + + // Edit menu item button + if ($this->allowEdit) { + $html .= '' + . ' ' . Text::_('JACTION_EDIT') + . ''; + } + + // Clear menu item button + if ($this->allowClear) { + $html .= '' + . ' ' . Text::_('JCLEAR') + . ''; + } + + // Propagate menu item button + if ($this->allowPropagate && count($languages) > 2) { + // Strip off language tag at the end + $tagLength = (int) strlen($this->element['language']); + $callbackFunctionStem = substr("jSelectMenu_" . $this->id, 0, -$tagLength); + + $html .= '' + . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') + . ''; + } + + if ($this->allowSelect || $this->allowNew || $this->allowEdit || $this->allowClear) { + $html .= ''; + } + + // Select menu item modal + if ($this->allowSelect) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalSelect' . $modalId, + array( + 'title' => $modalTitle, + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + } + + // New menu item modal + if ($this->allowNew) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalNew' . $modalId, + array( + 'title' => Text::_('COM_MENUS_NEW_MENUITEM'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlNew, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Edit menu item modal + if ($this->allowEdit) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalEdit' . $modalId, + array( + 'title' => Text::_('COM_MENUS_EDIT_MENUITEM'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlEdit, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Note: class='required' for client side validation. + $class = $this->required ? ' class="required modal-value"' : ''; + + // Placeholder if option is present or not when clearing field + if ($this->element->option && (string) $this->element->option['value'] == '') { + $title_holder = Text::_($this->element->option); + } else { + $title_holder = Text::_('COM_MENUS_SELECT_A_MENUITEM'); + } + + $html .= ''; + + return $html; + } + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + * + * @since 3.7.0 + */ + protected function getLabel() + { + return str_replace($this->id, $this->id . '_name', parent::getLabel()); + } } diff --git a/administrator/components/com_menus/src/Helper/AssociationsHelper.php b/administrator/components/com_menus/src/Helper/AssociationsHelper.php index 14f0c37dddcee..da951922b936a 100644 --- a/administrator/components/com_menus/src/Helper/AssociationsHelper.php +++ b/administrator/components/com_menus/src/Helper/AssociationsHelper.php @@ -1,4 +1,5 @@ getType($typeName); - - $context = $this->extension . '.item'; - - // Get the associations. - $associations = Associations::getAssociations( - $this->extension, - $type['tables']['a'], - $context, - $id, - 'id', - 'alias', - '' - ); - - return $associations; - } - - /** - * Get item information - * - * @param string $typeName The item type - * @param int $id The id of item for which we need the associated items - * - * @return Table|null - * - * @since 3.7.0 - */ - public function getItem($typeName, $id) - { - if (empty($id)) - { - return null; - } - - $table = null; - - switch ($typeName) - { - case 'item': - $table = Table::getInstance('menu'); - break; - } - - if (is_null($table)) - { - return null; - } - - $table->load($id); - - return $table; - } - - /** - * Get information about the type - * - * @param string $typeName The item type - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getType($typeName = '') - { - $fields = $this->getFieldsTemplate(); - $tables = array(); - $joins = array(); - $support = $this->getSupportTemplate(); - $title = ''; - - if (in_array($typeName, $this->itemTypes)) - { - switch ($typeName) - { - case 'item': - $fields['ordering'] = 'a.lft'; - $fields['level'] = 'a.level'; - $fields['catid'] = ''; - $fields['state'] = 'a.published'; - $fields['created_user_id'] = ''; - $fields['menutype'] = 'a.menutype'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['level'] = true; - - $tables = array( - 'a' => '#__menu' - ); - - $title = 'menu'; - break; - } - } - - return array( - 'fields' => $fields, - 'support' => $support, - 'tables' => $tables, - 'joins' => $joins, - 'title' => $title - ); - } + /** + * The extension name + * + * @var array $extension + * + * @since 3.7.0 + */ + protected $extension = 'com_menus'; + + /** + * Array of item types + * + * @var array $itemTypes + * + * @since 3.7.0 + */ + protected $itemTypes = array('item'); + + /** + * Has the extension association support + * + * @var boolean $associationsSupport + * + * @since 3.7.0 + */ + protected $associationsSupport = true; + + /** + * Method to get the associations for a given item. + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 4.0.0 + */ + public function getAssociationsForItem($id = 0, $view = null) + { + return []; + } + + /** + * Get the associated items for an item + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public function getAssociations($typeName, $id) + { + $type = $this->getType($typeName); + + $context = $this->extension . '.item'; + + // Get the associations. + $associations = Associations::getAssociations( + $this->extension, + $type['tables']['a'], + $context, + $id, + 'id', + 'alias', + '' + ); + + return $associations; + } + + /** + * Get item information + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return Table|null + * + * @since 3.7.0 + */ + public function getItem($typeName, $id) + { + if (empty($id)) { + return null; + } + + $table = null; + + switch ($typeName) { + case 'item': + $table = Table::getInstance('menu'); + break; + } + + if (is_null($table)) { + return null; + } + + $table->load($id); + + return $table; + } + + /** + * Get information about the type + * + * @param string $typeName The item type + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getType($typeName = '') + { + $fields = $this->getFieldsTemplate(); + $tables = array(); + $joins = array(); + $support = $this->getSupportTemplate(); + $title = ''; + + if (in_array($typeName, $this->itemTypes)) { + switch ($typeName) { + case 'item': + $fields['ordering'] = 'a.lft'; + $fields['level'] = 'a.level'; + $fields['catid'] = ''; + $fields['state'] = 'a.published'; + $fields['created_user_id'] = ''; + $fields['menutype'] = 'a.menutype'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['level'] = true; + + $tables = array( + 'a' => '#__menu' + ); + + $title = 'menu'; + break; + } + } + + return array( + 'fields' => $fields, + 'support' => $support, + 'tables' => $tables, + 'joins' => $joins, + 'title' => $title + ); + } } diff --git a/administrator/components/com_menus/src/Helper/MenusHelper.php b/administrator/components/com_menus/src/Helper/MenusHelper.php index 7d515381fd32d..0565c4ce5a43b 100644 --- a/administrator/components/com_menus/src/Helper/MenusHelper.php +++ b/administrator/components/com_menus/src/Helper/MenusHelper.php @@ -1,4 +1,5 @@ $value) - { - if ((!in_array($name, self::$_filter)) && (!($name == 'task' && !array_key_exists('view', $request)))) - { - // Remove the variables we want to ignore. - unset($request[$name]); - } - } - - ksort($request); - - return 'index.php?' . http_build_query($request, '', '&'); - } - - /** - * Get the menu list for create a menu module - * - * @param int $clientId Optional client id - viz 0 = site, 1 = administrator, can be NULL for all - * - * @return array The menu array list - * - * @since 1.6 - */ - public static function getMenuTypes($clientId = 0) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('a.menutype')) - ->from($db->quoteName('#__menu_types', 'a')); - - if (isset($clientId)) - { - $clientId = (int) $clientId; - $query->where($db->quoteName('a.client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Get a list of menu links for one or all menus. - * - * @param string $menuType An option menu to filter the list on, otherwise all menu with given client id links - * are returned as a grouped array. - * @param integer $parentId An optional parent ID to pivot results around. - * @param integer $mode An optional mode. If parent ID is set and mode=2, the parent and children are excluded from the list. - * @param array $published An optional array of states - * @param array $languages Optional array of specify which languages we want to filter - * @param int $clientId Optional client id - viz 0 = site, 1 = administrator, can be NULL for all (used only if menutype not given) - * - * @return array|boolean - * - * @since 1.6 - */ - public static function getMenuLinks($menuType = null, $parentId = 0, $mode = 0, $published = array(), $languages = array(), $clientId = 0) - { - $hasClientId = $clientId !== null; - $clientId = (int) $clientId; - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - 'DISTINCT ' . $db->quoteName('a.id', 'value'), - $db->quoteName('a.title', 'text'), - $db->quoteName('a.alias'), - $db->quoteName('a.level'), - $db->quoteName('a.menutype'), - $db->quoteName('a.client_id'), - $db->quoteName('a.type'), - $db->quoteName('a.published'), - $db->quoteName('a.template_style_id'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.language'), - $db->quoteName('a.lft'), - $db->quoteName('e.name', 'componentname'), - $db->quoteName('e.element'), - ] - ) - ->from($db->quoteName('#__menu', 'a')) - ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')); - - if (Multilanguage::isEnabled()) - { - $query->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - $db->quoteName('l.sef', 'language_sef'), - ] - ) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); - } - - // Filter by the type if given, this is more specific than client id - if ($menuType) - { - $query->where('(' . $db->quoteName('a.menutype') . ' = :menuType OR ' . $db->quoteName('a.parent_id') . ' = 0)') - ->bind(':menuType', $menuType); - } - elseif ($hasClientId) - { - $query->where($db->quoteName('a.client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - // Prevent the parent and children from showing if requested. - if ($parentId && $mode == 2) - { - $query->join('LEFT', $db->quoteName('#__menu', 'p'), $db->quoteName('p.id') . ' = :parentId') - ->where( - '(' . $db->quoteName('a.lft') . ' <= ' . $db->quoteName('p.lft') - . ' OR ' . $db->quoteName('a.rgt') . ' >= ' . $db->quoteName('p.rgt') . ')' - ) - ->bind(':parentId', $parentId, ParameterType::INTEGER); - } - - if (!empty($languages)) - { - $query->whereIn($db->quoteName('a.language'), (array) $languages, ParameterType::STRING); - } - - if (!empty($published)) - { - $query->whereIn($db->quoteName('a.published'), (array) $published); - } - - $query->where($db->quoteName('a.published') . ' != -2'); - $query->order($db->quoteName('a.lft') . ' ASC'); - - try - { - // Get the options. - $db->setQuery($query); - $links = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - if (empty($menuType)) - { - // If the menutype is empty, group the items by menutype. - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__menu_types')) - ->where($db->quoteName('menutype') . ' <> ' . $db->quote('')) - ->order( - [ - $db->quoteName('title'), - $db->quoteName('menutype'), - ] - ); - - if ($hasClientId) - { - $query->where($db->quoteName('client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - try - { - $db->setQuery($query); - $menuTypes = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // Create a reverse lookup and aggregate the links. - $rlu = array(); - - foreach ($menuTypes as &$type) - { - $rlu[$type->menutype] = & $type; - $type->links = array(); - } - - // Loop through the list of menu links. - foreach ($links as &$link) - { - if (isset($rlu[$link->menutype])) - { - $rlu[$link->menutype]->links[] = & $link; - - // Cleanup garbage. - unset($link->menutype); - } - } - - return $menuTypes; - } - else - { - return $links; - } - } - - /** - * Get the associations - * - * @param integer $pk Menu item id - * - * @return array - * - * @since 3.0 - */ - public static function getAssociations($pk) - { - $langAssociations = Associations::getAssociations('com_menus', '#__menu', 'com_menus.item', $pk, 'id', '', ''); - $associations = array(); - - foreach ($langAssociations as $langAssociation) - { - $associations[$langAssociation->language] = $langAssociation->id; - } - - return $associations; - } - - /** - * Load the menu items from database for the given menutype - * - * @param string $menutype The selected menu type - * @param boolean $enabledOnly Whether to load only enabled/published menu items. - * @param int[] $exclude The menu items to exclude from the list - * - * @return AdministratorMenuItem A root node with the menu items as children - * - * @since 4.0.0 - */ - public static function getMenuItems($menutype, $enabledOnly = false, $exclude = array()) - { - $root = new AdministratorMenuItem; - $db = Factory::getContainer()->get(DatabaseInterface::class); - $query = $db->getQuery(true); - - // Prepare the query. - $query->select($db->quoteName('m') . '.*') - ->from($db->quoteName('#__menu', 'm')) - ->where( - [ - $db->quoteName('m.menutype') . ' = :menutype', - $db->quoteName('m.client_id') . ' = 1', - $db->quoteName('m.id') . ' > 1', - ] - ) - ->bind(':menutype', $menutype); - - if ($enabledOnly) - { - $query->where($db->quoteName('m.published') . ' = 1'); - } - - // Filter on the enabled states. - $query->select($db->quoteName('e.element')) - ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('m.component_id') . ' = ' . $db->quoteName('e.extension_id')) - ->extendWhere( - 'AND', - [ - $db->quoteName('e.enabled') . ' = 1', - $db->quoteName('e.enabled') . ' IS NULL', - ], - 'OR' - ); - - if (count($exclude)) - { - $exId = array_map('intval', array_filter($exclude, 'is_numeric')); - $exEl = array_filter($exclude, 'is_string'); - - if ($exId) - { - $query->whereNotIn($db->quoteName('m.id'), $exId) - ->whereNotIn($db->quoteName('m.parent_id'), $exId); - } - - if ($exEl) - { - $query->whereNotIn($db->quoteName('e.element'), $exEl, ParameterType::STRING); - } - } - - // Order by lft. - $query->order($db->quoteName('m.lft')); - - try - { - $menuItems = []; - $iterator = $db->setQuery($query)->getIterator(); - - foreach ($iterator as $item) - { - $menuItems[$item->id] = new AdministratorMenuItem((array) $item); - } - - unset($iterator); - - foreach ($menuItems as $menuitem) - { - // Resolve the alias item to get the original item - if ($menuitem->type == 'alias') - { - static::resolveAlias($menuitem); - } - - if ($menuitem->link = in_array($menuitem->type, array('separator', 'heading', 'container')) ? '#' : trim($menuitem->link)) - { - $menuitem->submenu = array(); - $menuitem->class = $menuitem->img ?? ''; - $menuitem->scope = $menuitem->scope ?? null; - $menuitem->target = $menuitem->browserNav ? '_blank' : ''; - } - - $menuitem->ajaxbadge = $menuitem->getParams()->get('ajax-badge'); - $menuitem->dashboard = $menuitem->getParams()->get('dashboard'); - - if ($menuitem->parent_id > 1) - { - if (isset($menuItems[$menuitem->parent_id])) - { - $menuItems[$menuitem->parent_id]->addChild($menuitem); - } - } - else - { - $root->addChild($menuitem); - } - } - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage(Text::_('JERROR_AN_ERROR_HAS_OCCURRED'), 'error'); - } - - return $root; - } - - /** - * Method to install a preset menu into database and link them to the given menutype - * - * @param string $preset The preset name - * @param string $menutype The target menutype - * - * @return void - * - * @throws \Exception - * - * @since 4.0.0 - */ - public static function installPreset($preset, $menutype) - { - $root = static::loadPreset($preset, false); - - if (count($root->getChildren()) == 0) - { - throw new \Exception(Text::_('COM_MENUS_PRESET_LOAD_FAILED')); - } - - static::installPresetItems($root, $menutype); - } - - /** - * Method to install a preset menu item into database and link it to the given menutype - * - * @param AdministratorMenuItem $node The parent node of the items to process - * @param string $menutype The target menutype - * - * @return void - * - * @throws \Exception - * - * @since 4.0.0 - */ - protected static function installPresetItems($node, $menutype) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $items = $node->getChildren(); - - static $components = array(); - - if (!$components) - { - $query->select( - [ - $db->quoteName('extension_id'), - $db->quoteName('element'), - ] - ) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('component')); - $components = $db->setQuery($query)->loadObjectList(); - $components = array_column((array) $components, 'element', 'extension_id'); - } - - Factory::getApplication()->triggerEvent('onPreprocessMenuItems', array('com_menus.administrator.import', &$items, null, true)); - - foreach ($items as $item) - { - /** @var \Joomla\CMS\Table\Menu $table */ - $table = Table::getInstance('Menu'); - - $item->alias = $menutype . '-' . $item->title; - - // Temporarily set unicodeslugs if a menu item has an unicode alias - $unicode = Factory::getApplication()->set('unicodeslugs', 1); - $item->alias = ApplicationHelper::stringURLSafe($item->alias); - Factory::getApplication()->set('unicodeslugs', $unicode); - - if ($item->type == 'separator') - { - // Do not reuse a separator - $item->title = $item->title ?: '-'; - $item->alias = microtime(true); - } - elseif ($item->type == 'heading' || $item->type == 'container') - { - // Try to match an existing record to have minimum collision for a heading - $keys = array( - 'menutype' => $menutype, - 'type' => $item->type, - 'title' => $item->title, - 'parent_id' => (int) $item->getParent()->id, - 'client_id' => 1, - ); - $table->load($keys); - } - elseif ($item->type == 'url' || $item->type == 'component') - { - if (substr($item->link, 0, 8) === 'special:') - { - $special = substr($item->link, 8); - - if ($special === 'language-forum') - { - $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; - } - elseif ($special === 'custom-forum') - { - $item->link = ''; - } - } - - // Try to match an existing record to have minimum collision for a link - $keys = array( - 'menutype' => $menutype, - 'type' => $item->type, - 'link' => $item->link, - 'parent_id' => (int) $item->getParent()->id, - 'client_id' => 1, - ); - $table->load($keys); - } - - // Translate "hideitems" param value from "element" into "menu-item-id" - if ($item->type == 'container' && count($hideitems = (array) $item->getParams()->get('hideitems'))) - { - foreach ($hideitems as &$hel) - { - if (!is_numeric($hel)) - { - $hel = array_search($hel, $components); - } - } - - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->whereIn($db->quoteName('component_id'), $hideitems); - $hideitems = $db->setQuery($query)->loadColumn(); - - $item->getParams()->set('hideitems', $hideitems); - } - - $record = array( - 'menutype' => $menutype, - 'title' => $item->title, - 'alias' => $item->alias, - 'type' => $item->type, - 'link' => $item->link, - 'browserNav' => $item->browserNav, - 'img' => $item->class, - 'access' => $item->access, - 'component_id' => array_search($item->element, $components) ?: 0, - 'parent_id' => (int) $item->getParent()->id, - 'client_id' => 1, - 'published' => 1, - 'language' => '*', - 'home' => 0, - 'params' => (string) $item->getParams(), - ); - - if (!$table->bind($record)) - { - throw new \Exception($table->getError()); - } - - $table->setLocation($item->getParent()->id, 'last-child'); - - if (!$table->check()) - { - throw new \Exception($table->getError()); - } - - if (!$table->store()) - { - throw new \Exception($table->getError()); - } - - $item->id = $table->get('id'); - - if ($item->hasChildren()) - { - static::installPresetItems($item, $menutype); - } - } - } - - /** - * Add a custom preset externally via plugin or any other means. - * WARNING: Presets with same name will replace previously added preset *except* Joomla's default preset (joomla) - * - * @param string $name The unique identifier for the preset. - * @param string $title The display label for the preset. - * @param string $path The path to the preset file. - * @param bool $replace Whether to replace the preset with the same name if any (except 'joomla'). - * - * @return void - * - * @since 4.0.0 - */ - public static function addPreset($name, $title, $path, $replace = true) - { - if (static::$presets === null) - { - static::getPresets(); - } - - if ($name == 'joomla') - { - $replace = false; - } - - if (($replace || !array_key_exists($name, static::$presets)) && is_file($path)) - { - $preset = new \stdClass; - - $preset->name = $name; - $preset->title = $title; - $preset->path = $path; - - static::$presets[$name] = $preset; - } - } - - /** - * Get a list of available presets. - * - * @return \stdClass[] - * - * @since 4.0.0 - */ - public static function getPresets() - { - if (static::$presets === null) - { - // Important: 'null' will cause infinite recursion. - static::$presets = array(); - - $components = ComponentHelper::getComponents(); - $lang = Factory::getApplication()->getLanguage(); - - foreach ($components as $component) - { - if (!$component->enabled) - { - continue; - } - - $folder = JPATH_ADMINISTRATOR . '/components/' . $component->option . '/presets/'; - - if (!Folder::exists($folder)) - { - continue; - } - - $lang->load($component->option . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($component->option . '.sys', JPATH_ADMINISTRATOR . '/components/' . $component->option); - - $presets = Folder::files($folder, '.xml'); - - foreach ($presets as $preset) - { - $name = File::stripExt($preset); - $title = strtoupper($component->option . '_MENUS_PRESET_' . $name); - static::addPreset($name, $title, $folder . $preset); - } - } - - // Load from template folder automatically - $app = Factory::getApplication(); - $tpl = JPATH_THEMES . '/' . $app->getTemplate() . '/html/com_menus/presets'; - - if (is_dir($tpl)) - { - $files = Folder::files($tpl, '\.xml$'); - - foreach ($files as $file) - { - $name = substr($file, 0, -4); - $title = str_replace('-', ' ', $name); - - static::addPreset(strtolower($name), ucwords($title), $tpl . '/' . $file); - } - } - } - - return static::$presets; - } - - /** - * Load the menu items from a preset file into a hierarchical list of objects - * - * @param string $name The preset name - * @param bool $fallback Fallback to default (joomla) preset if the specified one could not be loaded? - * @param AdministratorMenuItem $parent Root node of the menu - * - * @return AdministratorMenuItem - * - * @since 4.0.0 - */ - public static function loadPreset($name, $fallback = true, $parent = null) - { - $presets = static::getPresets(); - - if (!$parent) - { - $parent = new AdministratorMenuItem; - } - - if (isset($presets[$name]) && ($xml = simplexml_load_file($presets[$name]->path, null, LIBXML_NOCDATA)) && $xml instanceof \SimpleXMLElement) - { - static::loadXml($xml, $parent); - } - elseif ($fallback && isset($presets['default'])) - { - if (($xml = simplexml_load_file($presets['default']->path, null, LIBXML_NOCDATA)) && $xml instanceof \SimpleXMLElement) - { - static::loadXml($xml, $parent); - } - } - - return $parent; - } - - /** - * Method to resolve the menu item alias type menu item - * - * @param AdministratorMenuItem &$item The alias object - * - * @return void - * - * @since 4.0.0 - */ - public static function resolveAlias(&$item) - { - $obj = $item; - - while ($obj->type == 'alias') - { - $aliasTo = (int) $obj->getParams()->get('aliasoptions'); - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('a.id'), - $db->quoteName('a.link'), - $db->quoteName('a.type'), - $db->quoteName('e.element'), - ] - ) - ->from($db->quoteName('#__menu', 'a')) - ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')) - ->where($db->quoteName('a.id') . ' = :aliasTo') - ->bind(':aliasTo', $aliasTo, ParameterType::INTEGER); - - try - { - $obj = new AdministratorMenuItem($db->setQuery($query)->loadAssoc()); - - if (!$obj) - { - $item->link = ''; - - return; - } - } - catch (\Exception $e) - { - $item->link = ''; - - return; - } - } - - $item->id = $obj->id; - $item->link = $obj->link; - $item->type = $obj->type; - $item->element = $obj->element; - } - - /** - * Parse the flat list of menu items and prepare the hierarchy of them using parent-child relationship. - * - * @param AdministratorMenuItem $item Menu item to preprocess - * - * @return void - * - * @since 4.0.0 - */ - public static function preprocess($item) - { - // Resolve the alias item to get the original item - if ($item->type == 'alias') - { - static::resolveAlias($item); - } - - if ($item->link = in_array($item->type, array('separator', 'heading', 'container')) ? '#' : trim($item->link)) - { - $item->class = $item->img ?? ''; - $item->scope = $item->scope ?? null; - $item->target = $item->browserNav ? '_blank' : ''; - } - } - - /** - * Load a menu tree from an XML file - * - * @param \SimpleXMLElement[] $elements The xml menuitem nodes - * @param AdministratorMenuItem $parent The menu hierarchy list to be populated - * @param string[] $replace The substring replacements for iterator type items - * - * @return void - * - * @since 4.0.0 - */ - protected static function loadXml($elements, $parent, $replace = array()) - { - foreach ($elements as $element) - { - if ($element->getName() != 'menuitem') - { - continue; - } - - $select = (string) $element['sql_select']; - $from = (string) $element['sql_from']; - - /** - * Following is a repeatable group based on simple database query. This requires sql_* attributes (sql_select and sql_from are required) - * The values can be used like - "{sql:columnName}" in any attribute of repeated elements. - * The repeated elements are place inside this xml node but they will be populated in the same level in the rendered menu - */ - if ($select && $from) - { - $hidden = $element['hidden'] == 'true'; - $where = (string) $element['sql_where']; - $order = (string) $element['sql_order']; - $group = (string) $element['sql_group']; - $lJoin = (string) $element['sql_leftjoin']; - $iJoin = (string) $element['sql_innerjoin']; - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $query->select($select)->from($from); - - if ($where) - { - $query->where($where); - } - - if ($order) - { - $query->order($order); - } - - if ($group) - { - $query->group($group); - } - - if ($lJoin) - { - $query->join('LEFT', $lJoin); - } - - if ($iJoin) - { - $query->join('INNER', $iJoin); - } - - $results = $db->setQuery($query)->loadObjectList(); - - // Skip the entire group if no items to iterate over. - if ($results) - { - // Show the repeatable group heading node only if not set as hidden. - if (!$hidden) - { - $child = static::parseXmlNode($element, $replace); - $parent->addChild($child); - } - - // Iterate over the matching records, items goes in the same level (not $item->submenu) as this node. - if ('self' == (string) $element['sql_target']) - { - foreach ($results as $result) - { - static::loadXml($element->menuitem, $child, $result); - } - } - else - { - foreach ($results as $result) - { - static::loadXml($element->menuitem, $parent, $result); - } - } - } - } - else - { - $item = static::parseXmlNode($element, $replace); - - // Process the child nodes - static::loadXml($element->menuitem, $item, $replace); - - $parent->addChild($item); - } - } - } - - /** - * Create a menu item node from an xml element - * - * @param \SimpleXMLElement $node A menuitem element from preset xml - * @param string[] $replace The values to substitute in the title, link and element texts - * - * @return \stdClass - * - * @since 4.0.0 - */ - protected static function parseXmlNode($node, $replace = array()) - { - $item = new AdministratorMenuItem; - - $item->id = null; - $item->type = (string) $node['type']; - $item->title = (string) $node['title']; - $item->alias = (string) $node['alias']; - $item->link = (string) $node['link']; - $item->target = (string) $node['target']; - $item->element = (string) $node['element']; - $item->class = (string) $node['class']; - $item->icon = (string) $node['icon']; - $item->access = (int) $node['access']; - $item->scope = (string) $node['scope'] ?: 'default'; - $item->ajaxbadge = (string) $node['ajax-badge']; - $item->dashboard = (string) $node['dashboard']; - - $params = new Registry(trim($node->params)); - $params->set('menu-permission', (string) $node['permission']); - - if ($item->type == 'separator' && trim($item->title, '- ')) - { - $params->set('text_separator', 1); - } - - if ($item->type == 'heading' || $item->type == 'container') - { - $item->link = '#'; - } - - if ((string) $node['quicktask']) - { - $params->set('menu-quicktask', (string) $node['quicktask']); - $params->set('menu-quicktask-title', (string) $node['quicktask-title']); - $params->set('menu-quicktask-icon', (string) $node['quicktask-icon']); - $params->set('menu-quicktask-permission', (string) $node['quicktask-permission']); - } - - // Translate attributes for iterator values - foreach ($replace as $var => $val) - { - $item->title = str_replace("{sql:$var}", $val, $item->title); - $item->element = str_replace("{sql:$var}", $val, $item->element); - $item->link = str_replace("{sql:$var}", $val, $item->link); - $item->class = str_replace("{sql:$var}", $val, $item->class); - $item->icon = str_replace("{sql:$var}", $val, $item->icon); - $params->set('menu-quicktask', str_replace("{sql:$var}", $val, $params->get('menu-quicktask'))); - } - - $item->setParams($params); - - return $item; - } + /** + * Defines the valid request variables for the reverse lookup. + * + * @var array + */ + protected static $_filter = array('option', 'view', 'layout'); + + /** + * List of preset include paths + * + * @var array + * + * @since 4.0.0 + */ + protected static $presets = null; + + /** + * Gets a standard form of a link for lookups. + * + * @param mixed $request A link string or array of request variables. + * + * @return mixed A link in standard option-view-layout form, or false if the supplied response is invalid. + * + * @since 1.6 + */ + public static function getLinkKey($request) + { + if (empty($request)) { + return false; + } + + // Check if the link is in the form of index.php?... + if (is_string($request)) { + $args = array(); + + if (strpos($request, 'index.php') === 0) { + parse_str(parse_url(htmlspecialchars_decode($request), PHP_URL_QUERY), $args); + } else { + parse_str($request, $args); + } + + $request = $args; + } + + // Only take the option, view and layout parts. + foreach ($request as $name => $value) { + if ((!in_array($name, self::$_filter)) && (!($name == 'task' && !array_key_exists('view', $request)))) { + // Remove the variables we want to ignore. + unset($request[$name]); + } + } + + ksort($request); + + return 'index.php?' . http_build_query($request, '', '&'); + } + + /** + * Get the menu list for create a menu module + * + * @param int $clientId Optional client id - viz 0 = site, 1 = administrator, can be NULL for all + * + * @return array The menu array list + * + * @since 1.6 + */ + public static function getMenuTypes($clientId = 0) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('a.menutype')) + ->from($db->quoteName('#__menu_types', 'a')); + + if (isset($clientId)) { + $clientId = (int) $clientId; + $query->where($db->quoteName('a.client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Get a list of menu links for one or all menus. + * + * @param string $menuType An option menu to filter the list on, otherwise all menu with given client id links + * are returned as a grouped array. + * @param integer $parentId An optional parent ID to pivot results around. + * @param integer $mode An optional mode. If parent ID is set and mode=2, the parent and children are excluded from the list. + * @param array $published An optional array of states + * @param array $languages Optional array of specify which languages we want to filter + * @param int $clientId Optional client id - viz 0 = site, 1 = administrator, can be NULL for all (used only if menutype not given) + * + * @return array|boolean + * + * @since 1.6 + */ + public static function getMenuLinks($menuType = null, $parentId = 0, $mode = 0, $published = array(), $languages = array(), $clientId = 0) + { + $hasClientId = $clientId !== null; + $clientId = (int) $clientId; + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + 'DISTINCT ' . $db->quoteName('a.id', 'value'), + $db->quoteName('a.title', 'text'), + $db->quoteName('a.alias'), + $db->quoteName('a.level'), + $db->quoteName('a.menutype'), + $db->quoteName('a.client_id'), + $db->quoteName('a.type'), + $db->quoteName('a.published'), + $db->quoteName('a.template_style_id'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.language'), + $db->quoteName('a.lft'), + $db->quoteName('e.name', 'componentname'), + $db->quoteName('e.element'), + ] + ) + ->from($db->quoteName('#__menu', 'a')) + ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')); + + if (Multilanguage::isEnabled()) { + $query->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + $db->quoteName('l.sef', 'language_sef'), + ] + ) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); + } + + // Filter by the type if given, this is more specific than client id + if ($menuType) { + $query->where('(' . $db->quoteName('a.menutype') . ' = :menuType OR ' . $db->quoteName('a.parent_id') . ' = 0)') + ->bind(':menuType', $menuType); + } elseif ($hasClientId) { + $query->where($db->quoteName('a.client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + // Prevent the parent and children from showing if requested. + if ($parentId && $mode == 2) { + $query->join('LEFT', $db->quoteName('#__menu', 'p'), $db->quoteName('p.id') . ' = :parentId') + ->where( + '(' . $db->quoteName('a.lft') . ' <= ' . $db->quoteName('p.lft') + . ' OR ' . $db->quoteName('a.rgt') . ' >= ' . $db->quoteName('p.rgt') . ')' + ) + ->bind(':parentId', $parentId, ParameterType::INTEGER); + } + + if (!empty($languages)) { + $query->whereIn($db->quoteName('a.language'), (array) $languages, ParameterType::STRING); + } + + if (!empty($published)) { + $query->whereIn($db->quoteName('a.published'), (array) $published); + } + + $query->where($db->quoteName('a.published') . ' != -2'); + $query->order($db->quoteName('a.lft') . ' ASC'); + + try { + // Get the options. + $db->setQuery($query); + $links = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + if (empty($menuType)) { + // If the menutype is empty, group the items by menutype. + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__menu_types')) + ->where($db->quoteName('menutype') . ' <> ' . $db->quote('')) + ->order( + [ + $db->quoteName('title'), + $db->quoteName('menutype'), + ] + ); + + if ($hasClientId) { + $query->where($db->quoteName('client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + try { + $db->setQuery($query); + $menuTypes = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // Create a reverse lookup and aggregate the links. + $rlu = array(); + + foreach ($menuTypes as &$type) { + $rlu[$type->menutype] = & $type; + $type->links = array(); + } + + // Loop through the list of menu links. + foreach ($links as &$link) { + if (isset($rlu[$link->menutype])) { + $rlu[$link->menutype]->links[] = & $link; + + // Cleanup garbage. + unset($link->menutype); + } + } + + return $menuTypes; + } else { + return $links; + } + } + + /** + * Get the associations + * + * @param integer $pk Menu item id + * + * @return array + * + * @since 3.0 + */ + public static function getAssociations($pk) + { + $langAssociations = Associations::getAssociations('com_menus', '#__menu', 'com_menus.item', $pk, 'id', '', ''); + $associations = array(); + + foreach ($langAssociations as $langAssociation) { + $associations[$langAssociation->language] = $langAssociation->id; + } + + return $associations; + } + + /** + * Load the menu items from database for the given menutype + * + * @param string $menutype The selected menu type + * @param boolean $enabledOnly Whether to load only enabled/published menu items. + * @param int[] $exclude The menu items to exclude from the list + * + * @return AdministratorMenuItem A root node with the menu items as children + * + * @since 4.0.0 + */ + public static function getMenuItems($menutype, $enabledOnly = false, $exclude = array()) + { + $root = new AdministratorMenuItem(); + $db = Factory::getContainer()->get(DatabaseInterface::class); + $query = $db->getQuery(true); + + // Prepare the query. + $query->select($db->quoteName('m') . '.*') + ->from($db->quoteName('#__menu', 'm')) + ->where( + [ + $db->quoteName('m.menutype') . ' = :menutype', + $db->quoteName('m.client_id') . ' = 1', + $db->quoteName('m.id') . ' > 1', + ] + ) + ->bind(':menutype', $menutype); + + if ($enabledOnly) { + $query->where($db->quoteName('m.published') . ' = 1'); + } + + // Filter on the enabled states. + $query->select($db->quoteName('e.element')) + ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('m.component_id') . ' = ' . $db->quoteName('e.extension_id')) + ->extendWhere( + 'AND', + [ + $db->quoteName('e.enabled') . ' = 1', + $db->quoteName('e.enabled') . ' IS NULL', + ], + 'OR' + ); + + if (count($exclude)) { + $exId = array_map('intval', array_filter($exclude, 'is_numeric')); + $exEl = array_filter($exclude, 'is_string'); + + if ($exId) { + $query->whereNotIn($db->quoteName('m.id'), $exId) + ->whereNotIn($db->quoteName('m.parent_id'), $exId); + } + + if ($exEl) { + $query->whereNotIn($db->quoteName('e.element'), $exEl, ParameterType::STRING); + } + } + + // Order by lft. + $query->order($db->quoteName('m.lft')); + + try { + $menuItems = []; + $iterator = $db->setQuery($query)->getIterator(); + + foreach ($iterator as $item) { + $menuItems[$item->id] = new AdministratorMenuItem((array) $item); + } + + unset($iterator); + + foreach ($menuItems as $menuitem) { + // Resolve the alias item to get the original item + if ($menuitem->type == 'alias') { + static::resolveAlias($menuitem); + } + + if ($menuitem->link = in_array($menuitem->type, array('separator', 'heading', 'container')) ? '#' : trim($menuitem->link)) { + $menuitem->submenu = array(); + $menuitem->class = $menuitem->img ?? ''; + $menuitem->scope = $menuitem->scope ?? null; + $menuitem->target = $menuitem->browserNav ? '_blank' : ''; + } + + $menuitem->ajaxbadge = $menuitem->getParams()->get('ajax-badge'); + $menuitem->dashboard = $menuitem->getParams()->get('dashboard'); + + if ($menuitem->parent_id > 1) { + if (isset($menuItems[$menuitem->parent_id])) { + $menuItems[$menuitem->parent_id]->addChild($menuitem); + } + } else { + $root->addChild($menuitem); + } + } + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage(Text::_('JERROR_AN_ERROR_HAS_OCCURRED'), 'error'); + } + + return $root; + } + + /** + * Method to install a preset menu into database and link them to the given menutype + * + * @param string $preset The preset name + * @param string $menutype The target menutype + * + * @return void + * + * @throws \Exception + * + * @since 4.0.0 + */ + public static function installPreset($preset, $menutype) + { + $root = static::loadPreset($preset, false); + + if (count($root->getChildren()) == 0) { + throw new \Exception(Text::_('COM_MENUS_PRESET_LOAD_FAILED')); + } + + static::installPresetItems($root, $menutype); + } + + /** + * Method to install a preset menu item into database and link it to the given menutype + * + * @param AdministratorMenuItem $node The parent node of the items to process + * @param string $menutype The target menutype + * + * @return void + * + * @throws \Exception + * + * @since 4.0.0 + */ + protected static function installPresetItems($node, $menutype) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $items = $node->getChildren(); + + static $components = array(); + + if (!$components) { + $query->select( + [ + $db->quoteName('extension_id'), + $db->quoteName('element'), + ] + ) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')); + $components = $db->setQuery($query)->loadObjectList(); + $components = array_column((array) $components, 'element', 'extension_id'); + } + + Factory::getApplication()->triggerEvent('onPreprocessMenuItems', array('com_menus.administrator.import', &$items, null, true)); + + foreach ($items as $item) { + /** @var \Joomla\CMS\Table\Menu $table */ + $table = Table::getInstance('Menu'); + + $item->alias = $menutype . '-' . $item->title; + + // Temporarily set unicodeslugs if a menu item has an unicode alias + $unicode = Factory::getApplication()->set('unicodeslugs', 1); + $item->alias = ApplicationHelper::stringURLSafe($item->alias); + Factory::getApplication()->set('unicodeslugs', $unicode); + + if ($item->type == 'separator') { + // Do not reuse a separator + $item->title = $item->title ?: '-'; + $item->alias = microtime(true); + } elseif ($item->type == 'heading' || $item->type == 'container') { + // Try to match an existing record to have minimum collision for a heading + $keys = array( + 'menutype' => $menutype, + 'type' => $item->type, + 'title' => $item->title, + 'parent_id' => (int) $item->getParent()->id, + 'client_id' => 1, + ); + $table->load($keys); + } elseif ($item->type == 'url' || $item->type == 'component') { + if (substr($item->link, 0, 8) === 'special:') { + $special = substr($item->link, 8); + + if ($special === 'language-forum') { + $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; + } elseif ($special === 'custom-forum') { + $item->link = ''; + } + } + + // Try to match an existing record to have minimum collision for a link + $keys = array( + 'menutype' => $menutype, + 'type' => $item->type, + 'link' => $item->link, + 'parent_id' => (int) $item->getParent()->id, + 'client_id' => 1, + ); + $table->load($keys); + } + + // Translate "hideitems" param value from "element" into "menu-item-id" + if ($item->type == 'container' && count($hideitems = (array) $item->getParams()->get('hideitems'))) { + foreach ($hideitems as &$hel) { + if (!is_numeric($hel)) { + $hel = array_search($hel, $components); + } + } + + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->whereIn($db->quoteName('component_id'), $hideitems); + $hideitems = $db->setQuery($query)->loadColumn(); + + $item->getParams()->set('hideitems', $hideitems); + } + + $record = array( + 'menutype' => $menutype, + 'title' => $item->title, + 'alias' => $item->alias, + 'type' => $item->type, + 'link' => $item->link, + 'browserNav' => $item->browserNav, + 'img' => $item->class, + 'access' => $item->access, + 'component_id' => array_search($item->element, $components) ?: 0, + 'parent_id' => (int) $item->getParent()->id, + 'client_id' => 1, + 'published' => 1, + 'language' => '*', + 'home' => 0, + 'params' => (string) $item->getParams(), + ); + + if (!$table->bind($record)) { + throw new \Exception($table->getError()); + } + + $table->setLocation($item->getParent()->id, 'last-child'); + + if (!$table->check()) { + throw new \Exception($table->getError()); + } + + if (!$table->store()) { + throw new \Exception($table->getError()); + } + + $item->id = $table->get('id'); + + if ($item->hasChildren()) { + static::installPresetItems($item, $menutype); + } + } + } + + /** + * Add a custom preset externally via plugin or any other means. + * WARNING: Presets with same name will replace previously added preset *except* Joomla's default preset (joomla) + * + * @param string $name The unique identifier for the preset. + * @param string $title The display label for the preset. + * @param string $path The path to the preset file. + * @param bool $replace Whether to replace the preset with the same name if any (except 'joomla'). + * + * @return void + * + * @since 4.0.0 + */ + public static function addPreset($name, $title, $path, $replace = true) + { + if (static::$presets === null) { + static::getPresets(); + } + + if ($name == 'joomla') { + $replace = false; + } + + if (($replace || !array_key_exists($name, static::$presets)) && is_file($path)) { + $preset = new \stdClass(); + + $preset->name = $name; + $preset->title = $title; + $preset->path = $path; + + static::$presets[$name] = $preset; + } + } + + /** + * Get a list of available presets. + * + * @return \stdClass[] + * + * @since 4.0.0 + */ + public static function getPresets() + { + if (static::$presets === null) { + // Important: 'null' will cause infinite recursion. + static::$presets = array(); + + $components = ComponentHelper::getComponents(); + $lang = Factory::getApplication()->getLanguage(); + + foreach ($components as $component) { + if (!$component->enabled) { + continue; + } + + $folder = JPATH_ADMINISTRATOR . '/components/' . $component->option . '/presets/'; + + if (!Folder::exists($folder)) { + continue; + } + + $lang->load($component->option . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($component->option . '.sys', JPATH_ADMINISTRATOR . '/components/' . $component->option); + + $presets = Folder::files($folder, '.xml'); + + foreach ($presets as $preset) { + $name = File::stripExt($preset); + $title = strtoupper($component->option . '_MENUS_PRESET_' . $name); + static::addPreset($name, $title, $folder . $preset); + } + } + + // Load from template folder automatically + $app = Factory::getApplication(); + $tpl = JPATH_THEMES . '/' . $app->getTemplate() . '/html/com_menus/presets'; + + if (is_dir($tpl)) { + $files = Folder::files($tpl, '\.xml$'); + + foreach ($files as $file) { + $name = substr($file, 0, -4); + $title = str_replace('-', ' ', $name); + + static::addPreset(strtolower($name), ucwords($title), $tpl . '/' . $file); + } + } + } + + return static::$presets; + } + + /** + * Load the menu items from a preset file into a hierarchical list of objects + * + * @param string $name The preset name + * @param bool $fallback Fallback to default (joomla) preset if the specified one could not be loaded? + * @param AdministratorMenuItem $parent Root node of the menu + * + * @return AdministratorMenuItem + * + * @since 4.0.0 + */ + public static function loadPreset($name, $fallback = true, $parent = null) + { + $presets = static::getPresets(); + + if (!$parent) { + $parent = new AdministratorMenuItem(); + } + + if (isset($presets[$name]) && ($xml = simplexml_load_file($presets[$name]->path, null, LIBXML_NOCDATA)) && $xml instanceof \SimpleXMLElement) { + static::loadXml($xml, $parent); + } elseif ($fallback && isset($presets['default'])) { + if (($xml = simplexml_load_file($presets['default']->path, null, LIBXML_NOCDATA)) && $xml instanceof \SimpleXMLElement) { + static::loadXml($xml, $parent); + } + } + + return $parent; + } + + /** + * Method to resolve the menu item alias type menu item + * + * @param AdministratorMenuItem &$item The alias object + * + * @return void + * + * @since 4.0.0 + */ + public static function resolveAlias(&$item) + { + $obj = $item; + + while ($obj->type == 'alias') { + $aliasTo = (int) $obj->getParams()->get('aliasoptions'); + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('a.id'), + $db->quoteName('a.link'), + $db->quoteName('a.type'), + $db->quoteName('e.element'), + ] + ) + ->from($db->quoteName('#__menu', 'a')) + ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')) + ->where($db->quoteName('a.id') . ' = :aliasTo') + ->bind(':aliasTo', $aliasTo, ParameterType::INTEGER); + + try { + $obj = new AdministratorMenuItem($db->setQuery($query)->loadAssoc()); + + if (!$obj) { + $item->link = ''; + + return; + } + } catch (\Exception $e) { + $item->link = ''; + + return; + } + } + + $item->id = $obj->id; + $item->link = $obj->link; + $item->type = $obj->type; + $item->element = $obj->element; + } + + /** + * Parse the flat list of menu items and prepare the hierarchy of them using parent-child relationship. + * + * @param AdministratorMenuItem $item Menu item to preprocess + * + * @return void + * + * @since 4.0.0 + */ + public static function preprocess($item) + { + // Resolve the alias item to get the original item + if ($item->type == 'alias') { + static::resolveAlias($item); + } + + if ($item->link = in_array($item->type, array('separator', 'heading', 'container')) ? '#' : trim($item->link)) { + $item->class = $item->img ?? ''; + $item->scope = $item->scope ?? null; + $item->target = $item->browserNav ? '_blank' : ''; + } + } + + /** + * Load a menu tree from an XML file + * + * @param \SimpleXMLElement[] $elements The xml menuitem nodes + * @param AdministratorMenuItem $parent The menu hierarchy list to be populated + * @param string[] $replace The substring replacements for iterator type items + * + * @return void + * + * @since 4.0.0 + */ + protected static function loadXml($elements, $parent, $replace = array()) + { + foreach ($elements as $element) { + if ($element->getName() != 'menuitem') { + continue; + } + + $select = (string) $element['sql_select']; + $from = (string) $element['sql_from']; + + /** + * Following is a repeatable group based on simple database query. This requires sql_* attributes (sql_select and sql_from are required) + * The values can be used like - "{sql:columnName}" in any attribute of repeated elements. + * The repeated elements are place inside this xml node but they will be populated in the same level in the rendered menu + */ + if ($select && $from) { + $hidden = $element['hidden'] == 'true'; + $where = (string) $element['sql_where']; + $order = (string) $element['sql_order']; + $group = (string) $element['sql_group']; + $lJoin = (string) $element['sql_leftjoin']; + $iJoin = (string) $element['sql_innerjoin']; + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select($select)->from($from); + + if ($where) { + $query->where($where); + } + + if ($order) { + $query->order($order); + } + + if ($group) { + $query->group($group); + } + + if ($lJoin) { + $query->join('LEFT', $lJoin); + } + + if ($iJoin) { + $query->join('INNER', $iJoin); + } + + $results = $db->setQuery($query)->loadObjectList(); + + // Skip the entire group if no items to iterate over. + if ($results) { + // Show the repeatable group heading node only if not set as hidden. + if (!$hidden) { + $child = static::parseXmlNode($element, $replace); + $parent->addChild($child); + } + + // Iterate over the matching records, items goes in the same level (not $item->submenu) as this node. + if ('self' == (string) $element['sql_target']) { + foreach ($results as $result) { + static::loadXml($element->menuitem, $child, $result); + } + } else { + foreach ($results as $result) { + static::loadXml($element->menuitem, $parent, $result); + } + } + } + } else { + $item = static::parseXmlNode($element, $replace); + + // Process the child nodes + static::loadXml($element->menuitem, $item, $replace); + + $parent->addChild($item); + } + } + } + + /** + * Create a menu item node from an xml element + * + * @param \SimpleXMLElement $node A menuitem element from preset xml + * @param string[] $replace The values to substitute in the title, link and element texts + * + * @return \stdClass + * + * @since 4.0.0 + */ + protected static function parseXmlNode($node, $replace = array()) + { + $item = new AdministratorMenuItem(); + + $item->id = null; + $item->type = (string) $node['type']; + $item->title = (string) $node['title']; + $item->alias = (string) $node['alias']; + $item->link = (string) $node['link']; + $item->target = (string) $node['target']; + $item->element = (string) $node['element']; + $item->class = (string) $node['class']; + $item->icon = (string) $node['icon']; + $item->access = (int) $node['access']; + $item->scope = (string) $node['scope'] ?: 'default'; + $item->ajaxbadge = (string) $node['ajax-badge']; + $item->dashboard = (string) $node['dashboard']; + + $params = new Registry(trim($node->params)); + $params->set('menu-permission', (string) $node['permission']); + + if ($item->type == 'separator' && trim($item->title, '- ')) { + $params->set('text_separator', 1); + } + + if ($item->type == 'heading' || $item->type == 'container') { + $item->link = '#'; + } + + if ((string) $node['quicktask']) { + $params->set('menu-quicktask', (string) $node['quicktask']); + $params->set('menu-quicktask-title', (string) $node['quicktask-title']); + $params->set('menu-quicktask-icon', (string) $node['quicktask-icon']); + $params->set('menu-quicktask-permission', (string) $node['quicktask-permission']); + } + + // Translate attributes for iterator values + foreach ($replace as $var => $val) { + $item->title = str_replace("{sql:$var}", $val, $item->title); + $item->element = str_replace("{sql:$var}", $val, $item->element); + $item->link = str_replace("{sql:$var}", $val, $item->link); + $item->class = str_replace("{sql:$var}", $val, $item->class); + $item->icon = str_replace("{sql:$var}", $val, $item->icon); + $params->set('menu-quicktask', str_replace("{sql:$var}", $val, $params->get('menu-quicktask'))); + } + + $item->setParams($params); + + return $item; + } } diff --git a/administrator/components/com_menus/src/Model/ItemModel.php b/administrator/components/com_menus/src/Model/ItemModel.php index 01437f6a9a13c..f9bdc0d8a522a 100644 --- a/administrator/components/com_menus/src/Model/ItemModel.php +++ b/administrator/components/com_menus/src/Model/ItemModel.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage' - ); - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - $menuTypeId = 0; - - if (!empty($record->menutype)) - { - $menuTypeId = $this->getMenuTypeId($record->menutype); - } - - return Factory::getUser()->authorise('core.delete', 'com_menus.menu.' . (int) $menuTypeId); - } - - /** - * Method to test whether the state of a record can be edited. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. - * - * @since 3.6 - */ - protected function canEditState($record) - { - $menuTypeId = !empty($record->menutype) ? $this->getMenuTypeId($record->menutype) : 0; - $assetKey = $menuTypeId ? 'com_menus.menu.' . (int) $menuTypeId : 'com_menus'; - - return Factory::getUser()->authorise('core.edit.state', $assetKey); - } - - /** - * Batch copy menu items to a new menu or parent. - * - * @param integer $value The new menu or sub-item. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return mixed An array of new IDs on success, boolean false on failure. - * - * @since 1.6 - */ - protected function batchCopy($value, $pks, $contexts) - { - // $value comes as {menutype}.{parent_id} - $parts = explode('.', $value); - $menuType = $parts[0]; - $parentId = ArrayHelper::getValue($parts, 1, 0, 'int'); - - $table = $this->getTable(); - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $newIds = array(); - - // Check that the parent exists - if ($parentId) - { - if (!$table->load($parentId)) - { - if ($error = $table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Non-fatal error - $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); - $parentId = 0; - } - } - } - - // If the parent is 0, set it to the ID of the root item in the tree - if (empty($parentId)) - { - if (!$parentId = $table->getRootId()) - { - $this->setError($table->getError()); - - return false; - } - } - - // Check that user has create permission for menus - $user = Factory::getUser(); - - $menuTypeId = (int) $this->getMenuTypeId($menuType); - - if (!$user->authorise('core.create', 'com_menus.menu.' . $menuTypeId)) - { - $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_CREATE')); - - return false; - } - - // We need to log the parent ID - $parents = array(); - - // Calculate the emergency stop count as a precaution against a runaway loop bug - $query->select('COUNT(' . $db->quoteName('id') . ')') - ->from($db->quoteName('#__menu')); - $db->setQuery($query); - - try - { - $count = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Parent exists so let's proceed - while (!empty($pks) && $count > 0) - { - // Pop the first id off the stack - $pk = array_shift($pks); - - $table->reset(); - - // Check that the row actually exists - if (!$table->load($pk)) - { - if ($error = $table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Not fatal error - $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); - continue; - } - } - - // Copy is a bit tricky, because we also need to copy the children - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->where( - [ - $db->quoteName('lft') . ' > :lft', - $db->quoteName('rgt') . ' < :rgt', - ] - ) - ->bind(':lft', $table->lft, ParameterType::INTEGER) - ->bind(':rgt', $table->rgt, ParameterType::INTEGER); - $db->setQuery($query); - $childIds = $db->loadColumn(); - - // Add child IDs to the array only if they aren't already there. - foreach ($childIds as $childId) - { - if (!in_array($childId, $pks)) - { - $pks[] = $childId; - } - } - - // Make a copy of the old ID and Parent ID - $oldId = $table->id; - $oldParentId = $table->parent_id; - - // Reset the id because we are making a copy. - $table->id = 0; - - // If we a copying children, the Old ID will turn up in the parents list - // otherwise it's a new top level item - $table->parent_id = isset($parents[$oldParentId]) ? $parents[$oldParentId] : $parentId; - $table->menutype = $menuType; - - // Set the new location in the tree for the node. - $table->setLocation($table->parent_id, 'last-child'); - - // @todo: Deal with ordering? - // $table->ordering = 1; - $table->level = null; - $table->lft = null; - $table->rgt = null; - $table->home = 0; - - // Alter the title & alias - list($title, $alias) = $this->generateNewTitle($table->parent_id, $table->alias, $table->title); - $table->title = $title; - $table->alias = $alias; - - // Check the row. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Store the row. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Get the new item ID - $newId = $table->get('id'); - - // Add the new ID to the array - $newIds[$pk] = $newId; - - // Now we log the old 'parent' to the new 'parent' - $parents[$oldId] = $table->id; - $count--; - } - - // Rebuild the hierarchy. - if (!$table->rebuild()) - { - $this->setError($table->getError()); - - return false; - } - - // Rebuild the tree path. - if (!$table->rebuildPath($table->id)) - { - $this->setError($table->getError()); - - return false; - } - - // Clean the cache - $this->cleanCache(); - - return $newIds; - } - - /** - * Batch move menu items to a new menu or parent. - * - * @param integer $value The new menu or sub-item. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True on success. - * - * @since 1.6 - */ - protected function batchMove($value, $pks, $contexts) - { - // $value comes as {menutype}.{parent_id} - $parts = explode('.', $value); - $menuType = $parts[0]; - $parentId = ArrayHelper::getValue($parts, 1, 0, 'int'); - - $table = $this->getTable(); - $db = $this->getDatabase(); - - // Check that the parent exists. - if ($parentId) - { - if (!$table->load($parentId)) - { - if ($error = $table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Non-fatal error - $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); - $parentId = 0; - } - } - } - - // Check that user has create and edit permission for menus - $user = Factory::getUser(); - - $menuTypeId = (int) $this->getMenuTypeId($menuType); - - if (!$user->authorise('core.create', 'com_menus.menu.' . $menuTypeId)) - { - $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_CREATE')); - - return false; - } - - if (!$user->authorise('core.edit', 'com_menus.menu.' . $menuTypeId)) - { - $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_EDIT')); - - return false; - } - - // We are going to store all the children and just moved the menutype - $children = array(); - - // Parent exists so let's proceed - foreach ($pks as $pk) - { - // Check that the row actually exists - if (!$table->load($pk)) - { - if ($error = $table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Not fatal error - $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); - continue; - } - } - - // Set the new location in the tree for the node. - $table->setLocation($parentId, 'last-child'); - - // Set the new Parent Id - $table->parent_id = $parentId; - - // Check if we are moving to a different menu - if ($menuType != $table->menutype) - { - // Add the child node ids to the children array. - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('lft') . ' BETWEEN :lft AND :rgt') - ->bind(':lft', $table->lft, ParameterType::INTEGER) - ->bind(':rgt', $table->rgt, ParameterType::INTEGER); - $db->setQuery($query); - $children = array_merge($children, (array) $db->loadColumn()); - } - - // Check the row. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Store the row. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Rebuild the tree path. - if (!$table->rebuildPath()) - { - $this->setError($table->getError()); - - return false; - } - } - - // Process the child rows - if (!empty($children)) - { - // Remove any duplicates and sanitize ids. - $children = array_unique($children); - $children = ArrayHelper::toInteger($children); - - // Update the menutype field in all nodes where necessary. - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('menutype') . ' = :menuType') - ->whereIn($db->quoteName('id'), $children) - ->bind(':menuType', $menuType); - - try - { - $db->setQuery($query); - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to check if you can save a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function canSave($data = array(), $key = 'id') - { - return Factory::getUser()->authorise('core.edit', $this->option); - } - - /** - * Method to get the row form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return mixed A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // The folder and element vars are passed when saving the form. - if (empty($data)) - { - $item = $this->getItem(); - - // The type should already be set. - $this->setState('item.link', $item->link); - } - else - { - $this->setState('item.link', ArrayHelper::getValue($data, 'link')); - $this->setState('item.type', ArrayHelper::getValue($data, 'type')); - } - - $clientId = $this->getState('item.client_id'); - - // Get the form. - if ($clientId == 1) - { - $form = $this->loadForm('com_menus.item.admin', 'itemadmin', array('control' => 'jform', 'load_data' => $loadData), true); - } - else - { - $form = $this->loadForm('com_menus.item', 'item', array('control' => 'jform', 'load_data' => $loadData), true); - } - - if (empty($form)) - { - return false; - } - - if ($loadData) - { - $data = $this->loadFormData(); - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('menuordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is an article you can edit. - $form->setFieldAttribute('menuordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - // Filter available menus - $action = $this->getState('item.id') > 0 ? 'edit' : 'create'; - - $form->setFieldAttribute('menutype', 'accesstype', $action); - $form->setFieldAttribute('type', 'clientid', $clientId); - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data, providing it has an ID and it is the same. - $itemData = (array) $this->getItem(); - - // When a new item is requested, unset the access as it will be set later from the filter - if (empty($itemData['id'])) - { - unset($itemData['access']); - } - - $sessionData = (array) Factory::getApplication()->getUserState('com_menus.edit.item.data', array()); - - // Only merge if there is a session and itemId or itemid is null. - if (isset($sessionData['id']) && isset($itemData['id']) && $sessionData['id'] === $itemData['id'] - || is_null($itemData['id'])) - { - $data = array_merge($itemData, $sessionData); - } - else - { - $data = $itemData; - } - - // For a new menu item, pre-select some filters (Status, Language, Access) in edit form if those have been selected in Menu Manager - if (empty($data['id'])) - { - // Get selected fields - $filters = Factory::getApplication()->getUserState('com_menus.items.filter'); - $data['parent_id'] = $data['parent_id'] ?? ($filters['parent_id'] ?? null); - $data['published'] = $data['published'] ?? ($filters['published'] ?? null); - $data['language'] = $data['language'] ?? ($filters['language'] ?? null); - $data['access'] = $data['access'] ?? ($filters['access'] ?? Factory::getApplication()->get('access')); - } - - if (isset($data['menutype']) && !$this->getState('item.menutypeid')) - { - $menuTypeId = (int) $this->getMenuTypeId($data['menutype']); - - $this->setState('item.menutypeid', $menuTypeId); - } - - $data = (object) $data; - - $this->preprocessData('com_menus.item', $data); - - return $data; - } - - /** - * Get the necessary data to load an item help screen. - * - * @return object An object with key, url, and local properties for loading the item help screen. - * - * @since 1.6 - */ - public function getHelp() - { - return (object) array('key' => $this->helpKey, 'url' => $this->helpURL, 'local' => $this->helpLocal); - } - - /** - * Method to get a menu item. - * - * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. - * - * @return mixed Menu item data object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('item.id'); - - // Get a level row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $table->load($pk); - - // Check for a table object error. - if ($error = $table->getError()) - { - $this->setError($error); - - return false; - } - - // Prime required properties. - - if ($type = $this->getState('item.type')) - { - $table->type = $type; - } - - if (empty($table->id)) - { - $table->parent_id = $this->getState('item.parent_id'); - $table->menutype = $this->getState('item.menutype'); - $table->client_id = $this->getState('item.client_id'); - $table->params = '{}'; - } - - // If the link has been set in the state, possibly changing link type. - if ($link = $this->getState('item.link')) - { - // Check if we are changing away from the actual link type. - if (MenusHelper::getLinkKey($table->link) !== MenusHelper::getLinkKey($link) && (int) $table->id === (int) $this->getState('item.id')) - { - $table->link = $link; - } - } - - switch ($table->type) - { - case 'alias': - case 'url': - $table->component_id = 0; - $args = array(); - - if ($table->link) - { - $q = parse_url($table->link, PHP_URL_QUERY); - - if ($q) - { - parse_str($q, $args); - } - } - - break; - - case 'separator': - case 'heading': - case 'container': - $table->link = ''; - $table->component_id = 0; - break; - - case 'component': - default: - // Enforce a valid type. - $table->type = 'component'; - - // Ensure the integrity of the component_id field is maintained, particularly when changing the menu item type. - $args = []; - - if ($table->link) - { - $q = parse_url($table->link, PHP_URL_QUERY); - - if ($q) - { - parse_str($q, $args); - } - } - - if (isset($args['option'])) - { - // Load the language file for the component. - $lang = Factory::getLanguage(); - $lang->load($args['option'], JPATH_ADMINISTRATOR) - || $lang->load($args['option'], JPATH_ADMINISTRATOR . '/components/' . $args['option']); - - // Determine the component id. - $component = ComponentHelper::getComponent($args['option']); - - if (isset($component->id)) - { - $table->component_id = $component->id; - } - } - break; - } - - // We have a valid type, inject it into the state for forms to use. - $this->setState('item.type', $table->type); - - // Convert to the \Joomla\CMS\Object\CMSObject before adding the params. - $properties = $table->getProperties(1); - $result = ArrayHelper::toObject($properties); - - // Convert the params field to an array. - $registry = new Registry($table->params); - $result->params = $registry->toArray(); - - // Merge the request arguments in to the params for a component. - if ($table->type == 'component') - { - // Note that all request arguments become reserved parameter names. - $result->request = $args; - $result->params = array_merge($result->params, $args); - - // Special case for the Login menu item. - // Display the login or logout redirect URL fields if not empty - if ($table->link == 'index.php?option=com_users&view=login') - { - if (!empty($result->params['login_redirect_url'])) - { - $result->params['loginredirectchoice'] = '0'; - } - - if (!empty($result->params['logout_redirect_url'])) - { - $result->params['logoutredirectchoice'] = '0'; - } - } - } - - if ($table->type == 'alias') - { - // Note that all request arguments become reserved parameter names. - $result->params = array_merge($result->params, $args); - } - - if ($table->type == 'url') - { - // Note that all request arguments become reserved parameter names. - $result->params = array_merge($result->params, $args); - } - - // Load associated menu items, only supported for frontend for now - if ($this->getState('item.client_id') == 0 && Associations::isEnabled()) - { - if ($pk != null) - { - $result->associations = MenusHelper::getAssociations($pk); - } - else - { - $result->associations = array(); - } - } - - $result->menuordering = $pk; - - return $result; - } - - /** - * Get the list of modules not in trash. - * - * @return mixed An array of module records (id, title, position), or false on error. - * - * @since 1.6 - */ - public function getModules() - { - $clientId = (int) $this->getState('item.client_id'); - $id = (int) $this->getState('item.id'); - - // Currently any setting that affects target page for a backend menu is not supported, hence load no modules. - if ($clientId == 1) - { - return false; - } - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - /** - * Join on the module-to-menu mapping table. - * We are only interested if the module is displayed on ALL or THIS menu item (or the inverse ID number). - * sqlsrv changes for modulelink to menu manager - */ - $query->select( - [ - $db->quoteName('a.id'), - $db->quoteName('a.title'), - $db->quoteName('a.position'), - $db->quoteName('a.published'), - $db->quoteName('map.menuid'), - ] - ) - ->from($db->quoteName('#__modules', 'a')) - ->join( - 'LEFT', - $db->quoteName('#__modules_menu', 'map'), - $db->quoteName('map.moduleid') . ' = ' . $db->quoteName('a.id') - . ' AND ' . $db->quoteName('map.menuid') . ' IN (' . implode(',', $query->bindArray([0, $id, -$id])) . ')' - ); - - $subQuery = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__modules_menu')) - ->where( - [ - $db->quoteName('moduleid') . ' = ' . $db->quoteName('a.id'), - $db->quoteName('menuid') . ' < 0', - ] - ); - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('except')); - - // Join on the asset groups table. - $query->select($db->quoteName('ag.title', 'access_title')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) - ->where( - [ - $db->quoteName('a.published') . ' >= 0', - $db->quoteName('a.client_id') . ' = :clientId', - ] - ) - ->bind(':clientId', $clientId, ParameterType::INTEGER) - ->order( - [ - $db->quoteName('a.position'), - $db->quoteName('a.ordering'), - ] - ); - - $db->setQuery($query); - - try - { - $result = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $result; - } - - /** - * Get the list of all view levels - * - * @return \stdClass[]|boolean An array of all view levels (id, title). - * - * @since 3.4 - */ - public function getViewLevels() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Get all the available view levels - $query->select($db->quoteName('id')) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__viewlevels')) - ->order($db->quoteName('id')); - - $db->setQuery($query); - - try - { - $result = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $result; - } - - /** - * Returns a Table object, always creating it - * - * @param string $type The table type to instantiate. - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\Cms\Table\Table|\Joomla\Cms\Table\Nested A database object. - * - * @since 1.6 - */ - public function getTable($type = 'Menu', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * A protected method to get the where clause for the reorder. - * This ensures that the row will be moved relative to a row with the same menutype. - * - * @param \Joomla\CMS\Table\Menu $table - * - * @return array An array of conditions to add to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - $db = $this->getDatabase(); - - return [ - $db->quoteName('menutype') . ' = ' . $db->quote($table->menutype), - ]; - } - - /** - * Auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState('item.id', $pk); - - if (!$app->isClient('api')) - { - $parentId = $app->getUserState('com_menus.edit.item.parent_id'); - $menuType = $app->getUserStateFromRequest('com_menus.items.menutype', 'menutype', '', 'string'); - } - else - { - $parentId = null; - $menuType = $app->input->get('com_menus.items.menutype'); - } - - if (!$parentId) - { - $parentId = $app->input->getInt('parent_id'); - } - - $this->setState('item.parent_id', $parentId); - - // If we have a menutype we take client_id from there, unless forced otherwise - if ($menuType) - { - $menuTypeObj = $this->getMenuType($menuType); - - // An invalid menutype will be handled as clientId = 0 and menuType = '' - $menuType = (string) $menuTypeObj->menutype; - $menuTypeId = (int) $menuTypeObj->client_id; - $clientId = (int) $menuTypeObj->client_id; - } - else - { - $menuTypeId = 0; - $clientId = $app->isClient('api') ? $app->input->get('client_id') : - $app->getUserState('com_menus.items.client_id', 0); - } - - // Forced client id will override/clear menuType if conflicted - $forcedClientId = $app->input->get('client_id', null, 'string'); - - if (!$app->isClient('api')) - { - // Set the menu type and client id on the list view state, so we return to this menu after saving. - $app->setUserState('com_menus.items.menutype', $menuType); - $app->setUserState('com_menus.items.client_id', $clientId); - } - - // Current item if not new, we don't allow changing client id at all - if ($pk) - { - $table = $this->getTable(); - $table->load($pk); - $forcedClientId = $table->get('client_id', $forcedClientId); - } - - if (isset($forcedClientId) && $forcedClientId != $clientId) - { - $clientId = $forcedClientId; - $menuType = ''; - $menuTypeId = 0; - } - - $this->setState('item.menutype', $menuType); - $this->setState('item.client_id', $clientId); - $this->setState('item.menutypeid', $menuTypeId); - - if (!($type = $app->getUserState('com_menus.edit.item.type'))) - { - $type = $app->input->get('type'); - - /** - * Note: a new menu item will have no field type. - * The field is required so the user has to change it. - */ - } - - $this->setState('item.type', $type); - - $link = $app->isClient('api') ? $app->input->get('link') : - $app->getUserState('com_menus.edit.item.link'); - - if ($link) - { - $this->setState('item.link', $link); - } - - // Load the parameters. - $params = ComponentHelper::getParams('com_menus'); - $this->setState('params', $params); - } - - /** - * Loads the menutype object by a given menutype string - * - * @param string $menutype The given menutype - * - * @return \stdClass - * - * @since 3.7.0 - */ - protected function getMenuType($menutype) - { - $table = $this->getTable('MenuType'); - - $table->load(array('menutype' => $menutype)); - - return (object) $table->getProperties(); - } - - /** - * Loads the menutype ID by a given menutype string - * - * @param string $menutype The given menutype - * - * @return integer - * - * @since 3.6 - */ - protected function getMenuTypeId($menutype) - { - $menu = $this->getMenuType($menutype); - - return (int) $menu->id; - } - - /** - * Method to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import. - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $link = $this->getState('item.link'); - $type = $this->getState('item.type'); - $clientId = $this->getState('item.client_id'); - $formFile = false; - - // Load the specific type file - $typeFile = $clientId == 1 ? 'itemadmin_' . $type : 'item_' . $type; - $clientInfo = ApplicationHelper::getClientInfo($clientId); - - // Initialise form with component view params if available. - if ($type == 'component') - { - $link = $link ? htmlspecialchars_decode($link) : ''; - - // Parse the link arguments. - $args = []; - - if ($link) - { - parse_str(parse_url(htmlspecialchars_decode($link), PHP_URL_QUERY), $args); - } - - // Confirm that the option is defined. - $option = ''; - $base = ''; - - if (isset($args['option'])) - { - // The option determines the base path to work with. - $option = $args['option']; - $base = $clientInfo->path . '/components/' . $option; - } - - if (isset($args['view'])) - { - $view = $args['view']; - - // Determine the layout to search for. - if (isset($args['layout'])) - { - $layout = $args['layout']; - } - else - { - $layout = 'default'; - } - - // Check for the layout XML file. Use standard xml file if it exists. - $tplFolders = array( - $base . '/tmpl/' . $view, - $base . '/views/' . $view . '/tmpl', - $base . '/view/' . $view . '/tmpl', - ); - $path = Path::find($tplFolders, $layout . '.xml'); - - if (is_file($path)) - { - $formFile = $path; - } - - // If custom layout, get the xml file from the template folder - // template folder is first part of file name -- template:folder - if (!$formFile && (strpos($layout, ':') > 0)) - { - list($altTmpl, $altLayout) = explode(':', $layout); - - $templatePath = Path::clean($clientInfo->path . '/templates/' . $altTmpl . '/html/' . $option . '/' . $view . '/' . $altLayout . '.xml'); - - if (is_file($templatePath)) - { - $formFile = $templatePath; - } - } - } - - // Now check for a view manifest file - if (!$formFile) - { - if (isset($view)) - { - $metadataFolders = array( - $base . '/view/' . $view, - $base . '/views/' . $view - ); - $metaPath = Path::find($metadataFolders, 'metadata.xml'); - - if (is_file($path = Path::clean($metaPath))) - { - $formFile = $path; - } - } - elseif ($base) - { - // Now check for a component manifest file - $path = Path::clean($base . '/metadata.xml'); - - if (is_file($path)) - { - $formFile = $path; - } - } - } - } - - if ($formFile) - { - // If an XML file was found in the component, load it first. - // We need to qualify the full path to avoid collisions with component file names. - - if ($form->loadFile($formFile, true, '/metadata') == false) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Get the help data from the XML file if present. - $help = $xml->xpath('/metadata/layout/help'); - } - else - { - // We don't have a component. Load the form XML to get the help path - $xmlFile = Path::find(JPATH_ADMINISTRATOR . '/components/com_menus/forms', $typeFile . '.xml'); - - if ($xmlFile) - { - if (!$xml = simplexml_load_file($xmlFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Get the help data from the XML file if present. - $help = $xml->xpath('/form/help'); - } - } - - if (!empty($help)) - { - $helpKey = trim((string) $help[0]['key']); - $helpURL = trim((string) $help[0]['url']); - $helpLoc = trim((string) $help[0]['local']); - - $this->helpKey = $helpKey ?: $this->helpKey; - $this->helpURL = $helpURL ?: $this->helpURL; - $this->helpLocal = (($helpLoc == 'true') || ($helpLoc == '1') || ($helpLoc == 'local')); - } - - if (!$form->loadFile($typeFile, true, false)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Association menu items, we currently do not support this for admin menu… may be later - if ($clientId == 0 && Associations::isEnabled()) - { - $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); - - if (count($languages) > 1) - { - $addform = new \SimpleXMLElement('
    '); - $fields = $addform->addChild('fields'); - $fields->addAttribute('name', 'associations'); - $fieldset = $fields->addChild('fieldset'); - $fieldset->addAttribute('name', 'item_associations'); - $fieldset->addAttribute('addfieldprefix', 'Joomla\Component\Menus\Administrator\Field'); - - foreach ($languages as $language) - { - $field = $fieldset->addChild('field'); - $field->addAttribute('name', $language->lang_code); - $field->addAttribute('type', 'modal_menu'); - $field->addAttribute('language', $language->lang_code); - $field->addAttribute('label', $language->title); - $field->addAttribute('translate_label', 'false'); - $field->addAttribute('select', 'true'); - $field->addAttribute('new', 'true'); - $field->addAttribute('edit', 'true'); - $field->addAttribute('clear', 'true'); - $field->addAttribute('propagate', 'true'); - $option = $field->addChild('option', 'COM_MENUS_ITEM_FIELD_ASSOCIATION_NO_VALUE'); - $option->addAttribute('value', ''); - } - - $form->load($addform, false); - } - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Method rebuild the entire nested set tree. - * - * @return boolean Boolean true on success, boolean false - * - * @since 1.6 - */ - public function rebuild() - { - // Initialise variables. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $table = $this->getTable(); - - try - { - $rebuildResult = $table->rebuild(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (!$rebuildResult) - { - $this->setError($table->getError()); - - return false; - } - - $query->select( - [ - $db->quoteName('id'), - $db->quoteName('params'), - ] - ) - ->from($db->quoteName('#__menu')) - ->where( - [ - $db->quoteName('params') . ' NOT LIKE ' . $db->quote('{%'), - $db->quoteName('params') . ' <> ' . $db->quote(''), - ] - ); - $db->setQuery($query); - - try - { - $items = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('params') . ' = :params') - ->where($db->quoteName('id') . ' = :id') - ->bind(':params', $params) - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - - foreach ($items as &$item) - { - // Update query parameters. - $id = $item->id; - $params = new Registry($item->params); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $pk = isset($data['id']) ? $data['id'] : (int) $this->getState('item.id'); - $isNew = true; - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - - // Include the plugins for the on save events. - PluginHelper::importPlugin($this->events_map['save']); - - // Load the row if saving an existing item. - if ($pk > 0) - { - $table->load($pk); - $isNew = false; - } - - if (!$isNew) - { - if ($table->parent_id == $data['parent_id']) - { - // If first is chosen make the item the first child of the selected parent. - if ($data['menuordering'] == -1) - { - $table->setLocation($data['parent_id'], 'first-child'); - } - // If last is chosen make it the last child of the selected parent. - elseif ($data['menuordering'] == -2) - { - $table->setLocation($data['parent_id'], 'last-child'); - } - // Don't try to put an item after itself. All other ones put after the selected item. - // $data['id'] is empty means it's a save as copy - elseif ($data['menuordering'] && $table->id != $data['menuordering'] || empty($data['id'])) - { - $table->setLocation($data['menuordering'], 'after'); - } - // \Just leave it where it is if no change is made. - elseif ($data['menuordering'] && $table->id == $data['menuordering']) - { - unset($data['menuordering']); - } - } - // Set the new parent id if parent id not matched and put in last position - else - { - $table->setLocation($data['parent_id'], 'last-child'); - } - - // Check if we are moving to a different menu - if ($data['menutype'] != $table->menutype) - { - // Add the child node ids to the children array. - $query->clear() - ->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('lft') . ' BETWEEN ' . (int) $table->lft . ' AND ' . (int) $table->rgt); - $db->setQuery($query); - $children = (array) $db->loadColumn(); - } - } - // We have a new item, so it is not a change. - else - { - $menuType = $this->getMenuType($data['menutype']); - - $data['client_id'] = $menuType->client_id; - - $table->setLocation($data['parent_id'], 'last-child'); - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Alter the title & alias for save2copy when required. Also, unset the home record. - if (Factory::getApplication()->input->get('task') === 'save2copy' && $data['id'] === 0) - { - $origTable = $this->getTable(); - $origTable->load($this->getState('item.id')); - - if ($table->title === $origTable->title) - { - list($title, $alias) = $this->generateNewTitle($table->parent_id, $table->alias, $table->title); - $table->title = $title; - $table->alias = $alias; - } - - if ($table->alias === $origTable->alias) - { - $table->alias = ''; - } - - $table->published = 0; - $table->home = 0; - } - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew, $data)); - - // Store the data. - if (in_array(false, $result, true)|| !$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew)); - - // Rebuild the tree path. - if (!$table->rebuildPath($table->id)) - { - $this->setError($table->getError()); - - return false; - } - - // Process the child rows - if (!empty($children)) - { - // Remove any duplicates and sanitize ids. - $children = array_unique($children); - $children = ArrayHelper::toInteger($children); - - // Update the menutype field in all nodes where necessary. - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('menutype') . ' = :menutype') - ->whereIn($db->quoteName('id'), $children) - ->bind(':menutype', $data['menutype']); - - try - { - $db->setQuery($query); - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - $this->setState('item.id', $table->id); - $this->setState('item.menutype', $table->menutype); - - // Load associated menu items, for now not supported for admin menu… may be later - if ($table->get('client_id') == 0 && Associations::isEnabled()) - { - // Adding self to the association - $associations = isset($data['associations']) ? $data['associations'] : array(); - - // Unset any invalid associations - $associations = ArrayHelper::toInteger($associations); - - foreach ($associations as $tag => $id) - { - if (!$id) - { - unset($associations[$tag]); - } - } - - // Detecting all item menus - $all_language = $table->language == '*'; - - if ($all_language && !empty($associations)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_ALL_LANGUAGE_ASSOCIATED'), 'notice'); - } - - // Get associationskey for edited item - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('key')) - ->from($db->quoteName('#__associations')) - ->where( - [ - $db->quoteName('context') . ' = :context', - $db->quoteName('id') . ' = :id', - ] - ) - ->bind(':context', $this->associationsContext) - ->bind(':id', $table->id, ParameterType::INTEGER); - $db->setQuery($query); - $oldKey = $db->loadResult(); - - if ($associations || $oldKey !== null) - { - // Deleting old associations for the associated items - $where = []; - $query = $db->getQuery(true) - ->delete($db->quoteName('#__associations')) - ->where($db->quoteName('context') . ' = :context') - ->bind(':context', $this->associationsContext); - - if ($associations) - { - $where[] = $db->quoteName('id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')'; - } - - if ($oldKey !== null) - { - $where[] = $db->quoteName('key') . ' = :oldKey'; - $query->bind(':oldKey', $oldKey); - } - - $query->extendWhere('AND', $where, 'OR'); - - try - { - $db->setQuery($query); - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - // Adding self to the association - if (!$all_language) - { - $associations[$table->language] = (int) $table->id; - } - - if (count($associations) > 1) - { - // Adding new association for these items - $key = md5(json_encode($associations)); - $query = $db->getQuery(true) - ->insert($db->quoteName('#__associations')) - ->columns( - [ - $db->quoteName('id'), - $db->quoteName('context'), - $db->quoteName('key'), - ] - ); - - foreach ($associations as $id) - { - $query->values( - implode( - ',', - $query->bindArray( - [$id, $this->associationsContext, $key], - [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] - ) - ) - ); - } - - try - { - $db->setQuery($query); - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - } - - // Clean the cache - $this->cleanCache(); - - if (isset($data['link'])) - { - $base = Uri::base(); - $juri = Uri::getInstance($base . $data['link']); - $option = $juri->getVar('option'); - - // Clean the cache - parent::cleanCache($option); - } - - if (Factory::getApplication()->input->get('task') === 'editAssociations') - { - return $this->redirectToAssociations($data); - } - - return true; - } - - /** - * Method to save the reordered nested set tree. - * First we save the new order values in the lft values of the changed ids. - * Then we invoke the table rebuild to implement the new ordering. - * - * @param array $idArray Rows identifiers to be reordered - * @param array $lftArray lft values of rows to be reordered - * - * @return boolean false on failure or error, true otherwise. - * - * @since 1.6 - */ - public function saveorder($idArray = null, $lftArray = null) - { - // Get an instance of the table object. - $table = $this->getTable(); - - if (!$table->saveorder($idArray, $lftArray)) - { - $this->setError($table->getError()); - - return false; - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the home state of one or more items. - * - * @param array $pks A list of the primary keys to change. - * @param integer $value The value of the home state. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function setHome(&$pks, $value = 1) - { - $table = $this->getTable(); - $pks = (array) $pks; - - $languages = array(); - $onehome = false; - - // Remember that we can set a home page for different languages, - // so we need to loop through the primary key array. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if (!array_key_exists($table->language, $languages)) - { - $languages[$table->language] = true; - - if ($table->home == $value) - { - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_ALREADY_HOME'), 'notice'); - } - elseif ($table->menutype == 'main') - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_MENUTYPE_HOME'), 'error'); - } - else - { - $table->home = $value; - - if ($table->language == '*') - { - $table->published = 1; - } - - if (!$this->canSave($table)) - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - } - elseif (!$table->check()) - { - // Prune the items that failed pre-save checks. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage($table->getError(), 'error'); - } - elseif (!$table->store()) - { - // Prune the items that could not be stored. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage($table->getError(), 'error'); - } - } - } - else - { - unset($pks[$i]); - - if (!$onehome) - { - $onehome = true; - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_MENUS_ERROR_ONE_HOME'), 'notice'); - } - } - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the published state of one or more records. - * - * @param array $pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function publish(&$pks, $value = 1) - { - $table = $this->getTable(); - $pks = (array) $pks; - - // Default menu item existence checks. - if ($value != 1) - { - foreach ($pks as $i => $pk) - { - if ($table->load($pk) && $table->home && $table->language == '*') - { - // Prune items that you can't change. - Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_MENU_UNPUBLISH_DEFAULT_HOME'), 'error'); - unset($pks[$i]); - break; - } - } - } - - // Clean the cache - $this->cleanCache(); - - // Ensure that previous checks doesn't empty the array - if (empty($pks)) - { - return true; - } - - return parent::publish($pks, $value); - } - - /** - * Method to change the title & alias. - * - * @param integer $parentId The id of the parent. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 1.6 - */ - protected function generateNewTitle($parentId, $alias, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) - { - if ($title == $table->title) - { - $title = StringHelper::increment($title); - } - - $alias = StringHelper::increment($alias, 'dash'); - } - - return array($title, $alias); - } - - /** - * Custom clean the cache - * - * @param string $group Cache group name. - * @param integer $clientId @deprecated 5.0 No Longer Used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_menus'); - parent::cleanCache('com_modules'); - parent::cleanCache('mod_menu'); - } + /** + * The type alias for this content type. + * + * @var string + * @since 3.4 + */ + public $typeAlias = 'com_menus.item'; + + /** + * The context used for the associations table + * + * @var string + * @since 3.4.4 + */ + protected $associationsContext = 'com_menus.item'; + + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_MENUS_ITEM'; + + /** + * @var string The help screen key for the menu item. + * @since 1.6 + */ + protected $helpKey = 'Menu_Item:_New_Item'; + + /** + * @var string The help screen base URL for the menu item. + * @since 1.6 + */ + protected $helpURL; + + /** + * @var boolean True to use local lookup for the help screen. + * @since 1.6 + */ + protected $helpLocal = false; + + /** + * Batch copy/move command. If set to false, + * the batch copy/move command is not supported + * + * @var string + */ + protected $batch_copymove = 'menu_id'; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage' + ); + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + $menuTypeId = 0; + + if (!empty($record->menutype)) { + $menuTypeId = $this->getMenuTypeId($record->menutype); + } + + return Factory::getUser()->authorise('core.delete', 'com_menus.menu.' . (int) $menuTypeId); + } + + /** + * Method to test whether the state of a record can be edited. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. + * + * @since 3.6 + */ + protected function canEditState($record) + { + $menuTypeId = !empty($record->menutype) ? $this->getMenuTypeId($record->menutype) : 0; + $assetKey = $menuTypeId ? 'com_menus.menu.' . (int) $menuTypeId : 'com_menus'; + + return Factory::getUser()->authorise('core.edit.state', $assetKey); + } + + /** + * Batch copy menu items to a new menu or parent. + * + * @param integer $value The new menu or sub-item. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return mixed An array of new IDs on success, boolean false on failure. + * + * @since 1.6 + */ + protected function batchCopy($value, $pks, $contexts) + { + // $value comes as {menutype}.{parent_id} + $parts = explode('.', $value); + $menuType = $parts[0]; + $parentId = ArrayHelper::getValue($parts, 1, 0, 'int'); + + $table = $this->getTable(); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $newIds = array(); + + // Check that the parent exists + if ($parentId) { + if (!$table->load($parentId)) { + if ($error = $table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Non-fatal error + $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); + $parentId = 0; + } + } + } + + // If the parent is 0, set it to the ID of the root item in the tree + if (empty($parentId)) { + if (!$parentId = $table->getRootId()) { + $this->setError($table->getError()); + + return false; + } + } + + // Check that user has create permission for menus + $user = Factory::getUser(); + + $menuTypeId = (int) $this->getMenuTypeId($menuType); + + if (!$user->authorise('core.create', 'com_menus.menu.' . $menuTypeId)) { + $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_CREATE')); + + return false; + } + + // We need to log the parent ID + $parents = array(); + + // Calculate the emergency stop count as a precaution against a runaway loop bug + $query->select('COUNT(' . $db->quoteName('id') . ')') + ->from($db->quoteName('#__menu')); + $db->setQuery($query); + + try { + $count = $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Parent exists so let's proceed + while (!empty($pks) && $count > 0) { + // Pop the first id off the stack + $pk = array_shift($pks); + + $table->reset(); + + // Check that the row actually exists + if (!$table->load($pk)) { + if ($error = $table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Not fatal error + $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); + continue; + } + } + + // Copy is a bit tricky, because we also need to copy the children + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->where( + [ + $db->quoteName('lft') . ' > :lft', + $db->quoteName('rgt') . ' < :rgt', + ] + ) + ->bind(':lft', $table->lft, ParameterType::INTEGER) + ->bind(':rgt', $table->rgt, ParameterType::INTEGER); + $db->setQuery($query); + $childIds = $db->loadColumn(); + + // Add child IDs to the array only if they aren't already there. + foreach ($childIds as $childId) { + if (!in_array($childId, $pks)) { + $pks[] = $childId; + } + } + + // Make a copy of the old ID and Parent ID + $oldId = $table->id; + $oldParentId = $table->parent_id; + + // Reset the id because we are making a copy. + $table->id = 0; + + // If we a copying children, the Old ID will turn up in the parents list + // otherwise it's a new top level item + $table->parent_id = isset($parents[$oldParentId]) ? $parents[$oldParentId] : $parentId; + $table->menutype = $menuType; + + // Set the new location in the tree for the node. + $table->setLocation($table->parent_id, 'last-child'); + + // @todo: Deal with ordering? + // $table->ordering = 1; + $table->level = null; + $table->lft = null; + $table->rgt = null; + $table->home = 0; + + // Alter the title & alias + list($title, $alias) = $this->generateNewTitle($table->parent_id, $table->alias, $table->title); + $table->title = $title; + $table->alias = $alias; + + // Check the row. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Store the row. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Get the new item ID + $newId = $table->get('id'); + + // Add the new ID to the array + $newIds[$pk] = $newId; + + // Now we log the old 'parent' to the new 'parent' + $parents[$oldId] = $table->id; + $count--; + } + + // Rebuild the hierarchy. + if (!$table->rebuild()) { + $this->setError($table->getError()); + + return false; + } + + // Rebuild the tree path. + if (!$table->rebuildPath($table->id)) { + $this->setError($table->getError()); + + return false; + } + + // Clean the cache + $this->cleanCache(); + + return $newIds; + } + + /** + * Batch move menu items to a new menu or parent. + * + * @param integer $value The new menu or sub-item. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True on success. + * + * @since 1.6 + */ + protected function batchMove($value, $pks, $contexts) + { + // $value comes as {menutype}.{parent_id} + $parts = explode('.', $value); + $menuType = $parts[0]; + $parentId = ArrayHelper::getValue($parts, 1, 0, 'int'); + + $table = $this->getTable(); + $db = $this->getDatabase(); + + // Check that the parent exists. + if ($parentId) { + if (!$table->load($parentId)) { + if ($error = $table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Non-fatal error + $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); + $parentId = 0; + } + } + } + + // Check that user has create and edit permission for menus + $user = Factory::getUser(); + + $menuTypeId = (int) $this->getMenuTypeId($menuType); + + if (!$user->authorise('core.create', 'com_menus.menu.' . $menuTypeId)) { + $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_CREATE')); + + return false; + } + + if (!$user->authorise('core.edit', 'com_menus.menu.' . $menuTypeId)) { + $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_EDIT')); + + return false; + } + + // We are going to store all the children and just moved the menutype + $children = array(); + + // Parent exists so let's proceed + foreach ($pks as $pk) { + // Check that the row actually exists + if (!$table->load($pk)) { + if ($error = $table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Not fatal error + $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); + continue; + } + } + + // Set the new location in the tree for the node. + $table->setLocation($parentId, 'last-child'); + + // Set the new Parent Id + $table->parent_id = $parentId; + + // Check if we are moving to a different menu + if ($menuType != $table->menutype) { + // Add the child node ids to the children array. + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('lft') . ' BETWEEN :lft AND :rgt') + ->bind(':lft', $table->lft, ParameterType::INTEGER) + ->bind(':rgt', $table->rgt, ParameterType::INTEGER); + $db->setQuery($query); + $children = array_merge($children, (array) $db->loadColumn()); + } + + // Check the row. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Store the row. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Rebuild the tree path. + if (!$table->rebuildPath()) { + $this->setError($table->getError()); + + return false; + } + } + + // Process the child rows + if (!empty($children)) { + // Remove any duplicates and sanitize ids. + $children = array_unique($children); + $children = ArrayHelper::toInteger($children); + + // Update the menutype field in all nodes where necessary. + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('menutype') . ' = :menuType') + ->whereIn($db->quoteName('id'), $children) + ->bind(':menuType', $menuType); + + try { + $db->setQuery($query); + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to check if you can save a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function canSave($data = array(), $key = 'id') + { + return Factory::getUser()->authorise('core.edit', $this->option); + } + + /** + * Method to get the row form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return mixed A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // The folder and element vars are passed when saving the form. + if (empty($data)) { + $item = $this->getItem(); + + // The type should already be set. + $this->setState('item.link', $item->link); + } else { + $this->setState('item.link', ArrayHelper::getValue($data, 'link')); + $this->setState('item.type', ArrayHelper::getValue($data, 'type')); + } + + $clientId = $this->getState('item.client_id'); + + // Get the form. + if ($clientId == 1) { + $form = $this->loadForm('com_menus.item.admin', 'itemadmin', array('control' => 'jform', 'load_data' => $loadData), true); + } else { + $form = $this->loadForm('com_menus.item', 'item', array('control' => 'jform', 'load_data' => $loadData), true); + } + + if (empty($form)) { + return false; + } + + if ($loadData) { + $data = $this->loadFormData(); + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('menuordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is an article you can edit. + $form->setFieldAttribute('menuordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + // Filter available menus + $action = $this->getState('item.id') > 0 ? 'edit' : 'create'; + + $form->setFieldAttribute('menutype', 'accesstype', $action); + $form->setFieldAttribute('type', 'clientid', $clientId); + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data, providing it has an ID and it is the same. + $itemData = (array) $this->getItem(); + + // When a new item is requested, unset the access as it will be set later from the filter + if (empty($itemData['id'])) { + unset($itemData['access']); + } + + $sessionData = (array) Factory::getApplication()->getUserState('com_menus.edit.item.data', array()); + + // Only merge if there is a session and itemId or itemid is null. + if ( + isset($sessionData['id']) && isset($itemData['id']) && $sessionData['id'] === $itemData['id'] + || is_null($itemData['id']) + ) { + $data = array_merge($itemData, $sessionData); + } else { + $data = $itemData; + } + + // For a new menu item, pre-select some filters (Status, Language, Access) in edit form if those have been selected in Menu Manager + if (empty($data['id'])) { + // Get selected fields + $filters = Factory::getApplication()->getUserState('com_menus.items.filter'); + $data['parent_id'] = $data['parent_id'] ?? ($filters['parent_id'] ?? null); + $data['published'] = $data['published'] ?? ($filters['published'] ?? null); + $data['language'] = $data['language'] ?? ($filters['language'] ?? null); + $data['access'] = $data['access'] ?? ($filters['access'] ?? Factory::getApplication()->get('access')); + } + + if (isset($data['menutype']) && !$this->getState('item.menutypeid')) { + $menuTypeId = (int) $this->getMenuTypeId($data['menutype']); + + $this->setState('item.menutypeid', $menuTypeId); + } + + $data = (object) $data; + + $this->preprocessData('com_menus.item', $data); + + return $data; + } + + /** + * Get the necessary data to load an item help screen. + * + * @return object An object with key, url, and local properties for loading the item help screen. + * + * @since 1.6 + */ + public function getHelp() + { + return (object) array('key' => $this->helpKey, 'url' => $this->helpURL, 'local' => $this->helpLocal); + } + + /** + * Method to get a menu item. + * + * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. + * + * @return mixed Menu item data object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('item.id'); + + // Get a level row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $table->load($pk); + + // Check for a table object error. + if ($error = $table->getError()) { + $this->setError($error); + + return false; + } + + // Prime required properties. + + if ($type = $this->getState('item.type')) { + $table->type = $type; + } + + if (empty($table->id)) { + $table->parent_id = $this->getState('item.parent_id'); + $table->menutype = $this->getState('item.menutype'); + $table->client_id = $this->getState('item.client_id'); + $table->params = '{}'; + } + + // If the link has been set in the state, possibly changing link type. + if ($link = $this->getState('item.link')) { + // Check if we are changing away from the actual link type. + if (MenusHelper::getLinkKey($table->link) !== MenusHelper::getLinkKey($link) && (int) $table->id === (int) $this->getState('item.id')) { + $table->link = $link; + } + } + + switch ($table->type) { + case 'alias': + case 'url': + $table->component_id = 0; + $args = array(); + + if ($table->link) { + $q = parse_url($table->link, PHP_URL_QUERY); + + if ($q) { + parse_str($q, $args); + } + } + + break; + + case 'separator': + case 'heading': + case 'container': + $table->link = ''; + $table->component_id = 0; + break; + + case 'component': + default: + // Enforce a valid type. + $table->type = 'component'; + + // Ensure the integrity of the component_id field is maintained, particularly when changing the menu item type. + $args = []; + + if ($table->link) { + $q = parse_url($table->link, PHP_URL_QUERY); + + if ($q) { + parse_str($q, $args); + } + } + + if (isset($args['option'])) { + // Load the language file for the component. + $lang = Factory::getLanguage(); + $lang->load($args['option'], JPATH_ADMINISTRATOR) + || $lang->load($args['option'], JPATH_ADMINISTRATOR . '/components/' . $args['option']); + + // Determine the component id. + $component = ComponentHelper::getComponent($args['option']); + + if (isset($component->id)) { + $table->component_id = $component->id; + } + } + break; + } + + // We have a valid type, inject it into the state for forms to use. + $this->setState('item.type', $table->type); + + // Convert to the \Joomla\CMS\Object\CMSObject before adding the params. + $properties = $table->getProperties(1); + $result = ArrayHelper::toObject($properties); + + // Convert the params field to an array. + $registry = new Registry($table->params); + $result->params = $registry->toArray(); + + // Merge the request arguments in to the params for a component. + if ($table->type == 'component') { + // Note that all request arguments become reserved parameter names. + $result->request = $args; + $result->params = array_merge($result->params, $args); + + // Special case for the Login menu item. + // Display the login or logout redirect URL fields if not empty + if ($table->link == 'index.php?option=com_users&view=login') { + if (!empty($result->params['login_redirect_url'])) { + $result->params['loginredirectchoice'] = '0'; + } + + if (!empty($result->params['logout_redirect_url'])) { + $result->params['logoutredirectchoice'] = '0'; + } + } + } + + if ($table->type == 'alias') { + // Note that all request arguments become reserved parameter names. + $result->params = array_merge($result->params, $args); + } + + if ($table->type == 'url') { + // Note that all request arguments become reserved parameter names. + $result->params = array_merge($result->params, $args); + } + + // Load associated menu items, only supported for frontend for now + if ($this->getState('item.client_id') == 0 && Associations::isEnabled()) { + if ($pk != null) { + $result->associations = MenusHelper::getAssociations($pk); + } else { + $result->associations = array(); + } + } + + $result->menuordering = $pk; + + return $result; + } + + /** + * Get the list of modules not in trash. + * + * @return mixed An array of module records (id, title, position), or false on error. + * + * @since 1.6 + */ + public function getModules() + { + $clientId = (int) $this->getState('item.client_id'); + $id = (int) $this->getState('item.id'); + + // Currently any setting that affects target page for a backend menu is not supported, hence load no modules. + if ($clientId == 1) { + return false; + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + /** + * Join on the module-to-menu mapping table. + * We are only interested if the module is displayed on ALL or THIS menu item (or the inverse ID number). + * sqlsrv changes for modulelink to menu manager + */ + $query->select( + [ + $db->quoteName('a.id'), + $db->quoteName('a.title'), + $db->quoteName('a.position'), + $db->quoteName('a.published'), + $db->quoteName('map.menuid'), + ] + ) + ->from($db->quoteName('#__modules', 'a')) + ->join( + 'LEFT', + $db->quoteName('#__modules_menu', 'map'), + $db->quoteName('map.moduleid') . ' = ' . $db->quoteName('a.id') + . ' AND ' . $db->quoteName('map.menuid') . ' IN (' . implode(',', $query->bindArray([0, $id, -$id])) . ')' + ); + + $subQuery = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__modules_menu')) + ->where( + [ + $db->quoteName('moduleid') . ' = ' . $db->quoteName('a.id'), + $db->quoteName('menuid') . ' < 0', + ] + ); + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('except')); + + // Join on the asset groups table. + $query->select($db->quoteName('ag.title', 'access_title')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) + ->where( + [ + $db->quoteName('a.published') . ' >= 0', + $db->quoteName('a.client_id') . ' = :clientId', + ] + ) + ->bind(':clientId', $clientId, ParameterType::INTEGER) + ->order( + [ + $db->quoteName('a.position'), + $db->quoteName('a.ordering'), + ] + ); + + $db->setQuery($query); + + try { + $result = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $result; + } + + /** + * Get the list of all view levels + * + * @return \stdClass[]|boolean An array of all view levels (id, title). + * + * @since 3.4 + */ + public function getViewLevels() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Get all the available view levels + $query->select($db->quoteName('id')) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__viewlevels')) + ->order($db->quoteName('id')); + + $db->setQuery($query); + + try { + $result = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $result; + } + + /** + * Returns a Table object, always creating it + * + * @param string $type The table type to instantiate. + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\Cms\Table\Table|\Joomla\Cms\Table\Nested A database object. + * + * @since 1.6 + */ + public function getTable($type = 'Menu', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * A protected method to get the where clause for the reorder. + * This ensures that the row will be moved relative to a row with the same menutype. + * + * @param \Joomla\CMS\Table\Menu $table + * + * @return array An array of conditions to add to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('menutype') . ' = ' . $db->quote($table->menutype), + ]; + } + + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState('item.id', $pk); + + if (!$app->isClient('api')) { + $parentId = $app->getUserState('com_menus.edit.item.parent_id'); + $menuType = $app->getUserStateFromRequest('com_menus.items.menutype', 'menutype', '', 'string'); + } else { + $parentId = null; + $menuType = $app->input->get('com_menus.items.menutype'); + } + + if (!$parentId) { + $parentId = $app->input->getInt('parent_id'); + } + + $this->setState('item.parent_id', $parentId); + + // If we have a menutype we take client_id from there, unless forced otherwise + if ($menuType) { + $menuTypeObj = $this->getMenuType($menuType); + + // An invalid menutype will be handled as clientId = 0 and menuType = '' + $menuType = (string) $menuTypeObj->menutype; + $menuTypeId = (int) $menuTypeObj->client_id; + $clientId = (int) $menuTypeObj->client_id; + } else { + $menuTypeId = 0; + $clientId = $app->isClient('api') ? $app->input->get('client_id') : + $app->getUserState('com_menus.items.client_id', 0); + } + + // Forced client id will override/clear menuType if conflicted + $forcedClientId = $app->input->get('client_id', null, 'string'); + + if (!$app->isClient('api')) { + // Set the menu type and client id on the list view state, so we return to this menu after saving. + $app->setUserState('com_menus.items.menutype', $menuType); + $app->setUserState('com_menus.items.client_id', $clientId); + } + + // Current item if not new, we don't allow changing client id at all + if ($pk) { + $table = $this->getTable(); + $table->load($pk); + $forcedClientId = $table->get('client_id', $forcedClientId); + } + + if (isset($forcedClientId) && $forcedClientId != $clientId) { + $clientId = $forcedClientId; + $menuType = ''; + $menuTypeId = 0; + } + + $this->setState('item.menutype', $menuType); + $this->setState('item.client_id', $clientId); + $this->setState('item.menutypeid', $menuTypeId); + + if (!($type = $app->getUserState('com_menus.edit.item.type'))) { + $type = $app->input->get('type'); + + /** + * Note: a new menu item will have no field type. + * The field is required so the user has to change it. + */ + } + + $this->setState('item.type', $type); + + $link = $app->isClient('api') ? $app->input->get('link') : + $app->getUserState('com_menus.edit.item.link'); + + if ($link) { + $this->setState('item.link', $link); + } + + // Load the parameters. + $params = ComponentHelper::getParams('com_menus'); + $this->setState('params', $params); + } + + /** + * Loads the menutype object by a given menutype string + * + * @param string $menutype The given menutype + * + * @return \stdClass + * + * @since 3.7.0 + */ + protected function getMenuType($menutype) + { + $table = $this->getTable('MenuType'); + + $table->load(array('menutype' => $menutype)); + + return (object) $table->getProperties(); + } + + /** + * Loads the menutype ID by a given menutype string + * + * @param string $menutype The given menutype + * + * @return integer + * + * @since 3.6 + */ + protected function getMenuTypeId($menutype) + { + $menu = $this->getMenuType($menutype); + + return (int) $menu->id; + } + + /** + * Method to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import. + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $link = $this->getState('item.link'); + $type = $this->getState('item.type'); + $clientId = $this->getState('item.client_id'); + $formFile = false; + + // Load the specific type file + $typeFile = $clientId == 1 ? 'itemadmin_' . $type : 'item_' . $type; + $clientInfo = ApplicationHelper::getClientInfo($clientId); + + // Initialise form with component view params if available. + if ($type == 'component') { + $link = $link ? htmlspecialchars_decode($link) : ''; + + // Parse the link arguments. + $args = []; + + if ($link) { + parse_str(parse_url(htmlspecialchars_decode($link), PHP_URL_QUERY), $args); + } + + // Confirm that the option is defined. + $option = ''; + $base = ''; + + if (isset($args['option'])) { + // The option determines the base path to work with. + $option = $args['option']; + $base = $clientInfo->path . '/components/' . $option; + } + + if (isset($args['view'])) { + $view = $args['view']; + + // Determine the layout to search for. + if (isset($args['layout'])) { + $layout = $args['layout']; + } else { + $layout = 'default'; + } + + // Check for the layout XML file. Use standard xml file if it exists. + $tplFolders = array( + $base . '/tmpl/' . $view, + $base . '/views/' . $view . '/tmpl', + $base . '/view/' . $view . '/tmpl', + ); + $path = Path::find($tplFolders, $layout . '.xml'); + + if (is_file($path)) { + $formFile = $path; + } + + // If custom layout, get the xml file from the template folder + // template folder is first part of file name -- template:folder + if (!$formFile && (strpos($layout, ':') > 0)) { + list($altTmpl, $altLayout) = explode(':', $layout); + + $templatePath = Path::clean($clientInfo->path . '/templates/' . $altTmpl . '/html/' . $option . '/' . $view . '/' . $altLayout . '.xml'); + + if (is_file($templatePath)) { + $formFile = $templatePath; + } + } + } + + // Now check for a view manifest file + if (!$formFile) { + if (isset($view)) { + $metadataFolders = array( + $base . '/view/' . $view, + $base . '/views/' . $view + ); + $metaPath = Path::find($metadataFolders, 'metadata.xml'); + + if (is_file($path = Path::clean($metaPath))) { + $formFile = $path; + } + } elseif ($base) { + // Now check for a component manifest file + $path = Path::clean($base . '/metadata.xml'); + + if (is_file($path)) { + $formFile = $path; + } + } + } + } + + if ($formFile) { + // If an XML file was found in the component, load it first. + // We need to qualify the full path to avoid collisions with component file names. + + if ($form->loadFile($formFile, true, '/metadata') == false) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Get the help data from the XML file if present. + $help = $xml->xpath('/metadata/layout/help'); + } else { + // We don't have a component. Load the form XML to get the help path + $xmlFile = Path::find(JPATH_ADMINISTRATOR . '/components/com_menus/forms', $typeFile . '.xml'); + + if ($xmlFile) { + if (!$xml = simplexml_load_file($xmlFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Get the help data from the XML file if present. + $help = $xml->xpath('/form/help'); + } + } + + if (!empty($help)) { + $helpKey = trim((string) $help[0]['key']); + $helpURL = trim((string) $help[0]['url']); + $helpLoc = trim((string) $help[0]['local']); + + $this->helpKey = $helpKey ?: $this->helpKey; + $this->helpURL = $helpURL ?: $this->helpURL; + $this->helpLocal = (($helpLoc == 'true') || ($helpLoc == '1') || ($helpLoc == 'local')); + } + + if (!$form->loadFile($typeFile, true, false)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Association menu items, we currently do not support this for admin menu… may be later + if ($clientId == 0 && Associations::isEnabled()) { + $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); + + if (count($languages) > 1) { + $addform = new \SimpleXMLElement(''); + $fields = $addform->addChild('fields'); + $fields->addAttribute('name', 'associations'); + $fieldset = $fields->addChild('fieldset'); + $fieldset->addAttribute('name', 'item_associations'); + $fieldset->addAttribute('addfieldprefix', 'Joomla\Component\Menus\Administrator\Field'); + + foreach ($languages as $language) { + $field = $fieldset->addChild('field'); + $field->addAttribute('name', $language->lang_code); + $field->addAttribute('type', 'modal_menu'); + $field->addAttribute('language', $language->lang_code); + $field->addAttribute('label', $language->title); + $field->addAttribute('translate_label', 'false'); + $field->addAttribute('select', 'true'); + $field->addAttribute('new', 'true'); + $field->addAttribute('edit', 'true'); + $field->addAttribute('clear', 'true'); + $field->addAttribute('propagate', 'true'); + $option = $field->addChild('option', 'COM_MENUS_ITEM_FIELD_ASSOCIATION_NO_VALUE'); + $option->addAttribute('value', ''); + } + + $form->load($addform, false); + } + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Method rebuild the entire nested set tree. + * + * @return boolean Boolean true on success, boolean false + * + * @since 1.6 + */ + public function rebuild() + { + // Initialise variables. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $table = $this->getTable(); + + try { + $rebuildResult = $table->rebuild(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (!$rebuildResult) { + $this->setError($table->getError()); + + return false; + } + + $query->select( + [ + $db->quoteName('id'), + $db->quoteName('params'), + ] + ) + ->from($db->quoteName('#__menu')) + ->where( + [ + $db->quoteName('params') . ' NOT LIKE ' . $db->quote('{%'), + $db->quoteName('params') . ' <> ' . $db->quote(''), + ] + ); + $db->setQuery($query); + + try { + $items = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('params') . ' = :params') + ->where($db->quoteName('id') . ' = :id') + ->bind(':params', $params) + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + + foreach ($items as &$item) { + // Update query parameters. + $id = $item->id; + $params = new Registry($item->params); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $pk = isset($data['id']) ? $data['id'] : (int) $this->getState('item.id'); + $isNew = true; + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + + // Include the plugins for the on save events. + PluginHelper::importPlugin($this->events_map['save']); + + // Load the row if saving an existing item. + if ($pk > 0) { + $table->load($pk); + $isNew = false; + } + + if (!$isNew) { + if ($table->parent_id == $data['parent_id']) { + // If first is chosen make the item the first child of the selected parent. + if ($data['menuordering'] == -1) { + $table->setLocation($data['parent_id'], 'first-child'); + } + // If last is chosen make it the last child of the selected parent. + elseif ($data['menuordering'] == -2) { + $table->setLocation($data['parent_id'], 'last-child'); + } + // Don't try to put an item after itself. All other ones put after the selected item. + // $data['id'] is empty means it's a save as copy + elseif ($data['menuordering'] && $table->id != $data['menuordering'] || empty($data['id'])) { + $table->setLocation($data['menuordering'], 'after'); + } + // \Just leave it where it is if no change is made. + elseif ($data['menuordering'] && $table->id == $data['menuordering']) { + unset($data['menuordering']); + } + } + // Set the new parent id if parent id not matched and put in last position + else { + $table->setLocation($data['parent_id'], 'last-child'); + } + + // Check if we are moving to a different menu + if ($data['menutype'] != $table->menutype) { + // Add the child node ids to the children array. + $query->clear() + ->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('lft') . ' BETWEEN ' . (int) $table->lft . ' AND ' . (int) $table->rgt); + $db->setQuery($query); + $children = (array) $db->loadColumn(); + } + } + // We have a new item, so it is not a change. + else { + $menuType = $this->getMenuType($data['menutype']); + + $data['client_id'] = $menuType->client_id; + + $table->setLocation($data['parent_id'], 'last-child'); + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Alter the title & alias for save2copy when required. Also, unset the home record. + if (Factory::getApplication()->input->get('task') === 'save2copy' && $data['id'] === 0) { + $origTable = $this->getTable(); + $origTable->load($this->getState('item.id')); + + if ($table->title === $origTable->title) { + list($title, $alias) = $this->generateNewTitle($table->parent_id, $table->alias, $table->title); + $table->title = $title; + $table->alias = $alias; + } + + if ($table->alias === $origTable->alias) { + $table->alias = ''; + } + + $table->published = 0; + $table->home = 0; + } + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew, $data)); + + // Store the data. + if (in_array(false, $result, true) || !$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew)); + + // Rebuild the tree path. + if (!$table->rebuildPath($table->id)) { + $this->setError($table->getError()); + + return false; + } + + // Process the child rows + if (!empty($children)) { + // Remove any duplicates and sanitize ids. + $children = array_unique($children); + $children = ArrayHelper::toInteger($children); + + // Update the menutype field in all nodes where necessary. + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('menutype') . ' = :menutype') + ->whereIn($db->quoteName('id'), $children) + ->bind(':menutype', $data['menutype']); + + try { + $db->setQuery($query); + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + $this->setState('item.id', $table->id); + $this->setState('item.menutype', $table->menutype); + + // Load associated menu items, for now not supported for admin menu… may be later + if ($table->get('client_id') == 0 && Associations::isEnabled()) { + // Adding self to the association + $associations = isset($data['associations']) ? $data['associations'] : array(); + + // Unset any invalid associations + $associations = ArrayHelper::toInteger($associations); + + foreach ($associations as $tag => $id) { + if (!$id) { + unset($associations[$tag]); + } + } + + // Detecting all item menus + $all_language = $table->language == '*'; + + if ($all_language && !empty($associations)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_ALL_LANGUAGE_ASSOCIATED'), 'notice'); + } + + // Get associationskey for edited item + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('key')) + ->from($db->quoteName('#__associations')) + ->where( + [ + $db->quoteName('context') . ' = :context', + $db->quoteName('id') . ' = :id', + ] + ) + ->bind(':context', $this->associationsContext) + ->bind(':id', $table->id, ParameterType::INTEGER); + $db->setQuery($query); + $oldKey = $db->loadResult(); + + if ($associations || $oldKey !== null) { + // Deleting old associations for the associated items + $where = []; + $query = $db->getQuery(true) + ->delete($db->quoteName('#__associations')) + ->where($db->quoteName('context') . ' = :context') + ->bind(':context', $this->associationsContext); + + if ($associations) { + $where[] = $db->quoteName('id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')'; + } + + if ($oldKey !== null) { + $where[] = $db->quoteName('key') . ' = :oldKey'; + $query->bind(':oldKey', $oldKey); + } + + $query->extendWhere('AND', $where, 'OR'); + + try { + $db->setQuery($query); + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + // Adding self to the association + if (!$all_language) { + $associations[$table->language] = (int) $table->id; + } + + if (count($associations) > 1) { + // Adding new association for these items + $key = md5(json_encode($associations)); + $query = $db->getQuery(true) + ->insert($db->quoteName('#__associations')) + ->columns( + [ + $db->quoteName('id'), + $db->quoteName('context'), + $db->quoteName('key'), + ] + ); + + foreach ($associations as $id) { + $query->values( + implode( + ',', + $query->bindArray( + [$id, $this->associationsContext, $key], + [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] + ) + ) + ); + } + + try { + $db->setQuery($query); + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + } + + // Clean the cache + $this->cleanCache(); + + if (isset($data['link'])) { + $base = Uri::base(); + $juri = Uri::getInstance($base . $data['link']); + $option = $juri->getVar('option'); + + // Clean the cache + parent::cleanCache($option); + } + + if (Factory::getApplication()->input->get('task') === 'editAssociations') { + return $this->redirectToAssociations($data); + } + + return true; + } + + /** + * Method to save the reordered nested set tree. + * First we save the new order values in the lft values of the changed ids. + * Then we invoke the table rebuild to implement the new ordering. + * + * @param array $idArray Rows identifiers to be reordered + * @param array $lftArray lft values of rows to be reordered + * + * @return boolean false on failure or error, true otherwise. + * + * @since 1.6 + */ + public function saveorder($idArray = null, $lftArray = null) + { + // Get an instance of the table object. + $table = $this->getTable(); + + if (!$table->saveorder($idArray, $lftArray)) { + $this->setError($table->getError()); + + return false; + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the home state of one or more items. + * + * @param array $pks A list of the primary keys to change. + * @param integer $value The value of the home state. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function setHome(&$pks, $value = 1) + { + $table = $this->getTable(); + $pks = (array) $pks; + + $languages = array(); + $onehome = false; + + // Remember that we can set a home page for different languages, + // so we need to loop through the primary key array. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if (!array_key_exists($table->language, $languages)) { + $languages[$table->language] = true; + + if ($table->home == $value) { + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_ALREADY_HOME'), 'notice'); + } elseif ($table->menutype == 'main') { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_MENUTYPE_HOME'), 'error'); + } else { + $table->home = $value; + + if ($table->language == '*') { + $table->published = 1; + } + + if (!$this->canSave($table)) { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + } elseif (!$table->check()) { + // Prune the items that failed pre-save checks. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage($table->getError(), 'error'); + } elseif (!$table->store()) { + // Prune the items that could not be stored. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage($table->getError(), 'error'); + } + } + } else { + unset($pks[$i]); + + if (!$onehome) { + $onehome = true; + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_MENUS_ERROR_ONE_HOME'), 'notice'); + } + } + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the published state of one or more records. + * + * @param array $pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function publish(&$pks, $value = 1) + { + $table = $this->getTable(); + $pks = (array) $pks; + + // Default menu item existence checks. + if ($value != 1) { + foreach ($pks as $i => $pk) { + if ($table->load($pk) && $table->home && $table->language == '*') { + // Prune items that you can't change. + Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_MENU_UNPUBLISH_DEFAULT_HOME'), 'error'); + unset($pks[$i]); + break; + } + } + } + + // Clean the cache + $this->cleanCache(); + + // Ensure that previous checks doesn't empty the array + if (empty($pks)) { + return true; + } + + return parent::publish($pks, $value); + } + + /** + * Method to change the title & alias. + * + * @param integer $parentId The id of the parent. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 1.6 + */ + protected function generateNewTitle($parentId, $alias, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) { + if ($title == $table->title) { + $title = StringHelper::increment($title); + } + + $alias = StringHelper::increment($alias, 'dash'); + } + + return array($title, $alias); + } + + /** + * Custom clean the cache + * + * @param string $group Cache group name. + * @param integer $clientId @deprecated 5.0 No Longer Used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_menus'); + parent::cleanCache('com_modules'); + parent::cleanCache('mod_menu'); + } } diff --git a/administrator/components/com_menus/src/Model/ItemsModel.php b/administrator/components/com_menus/src/Model/ItemsModel.php index 313cdd999ee05..e58df15a47157 100644 --- a/administrator/components/com_menus/src/Model/ItemsModel.php +++ b/administrator/components/com_menus/src/Model/ItemsModel.php @@ -1,4 +1,5 @@ input->get('forcedLanguage', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - $search = $this->getUserStateFromRequest($this->context . '.search', 'filter_search'); - $this->setState('filter.search', $search); - - $published = $this->getUserStateFromRequest($this->context . '.published', 'filter_published', ''); - $this->setState('filter.published', $published); - - $access = $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access'); - $this->setState('filter.access', $access); - - $parentId = $this->getUserStateFromRequest($this->context . '.filter.parent_id', 'filter_parent_id'); - $this->setState('filter.parent_id', $parentId); - - $level = $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level'); - $this->setState('filter.level', $level); - - // Watch changes in client_id and menutype and keep sync whenever needed. - $currentClientId = $app->getUserState($this->context . '.client_id', 0); - $clientId = $app->input->getInt('client_id', $currentClientId); - - // Load mod_menu.ini file when client is administrator - if ($clientId == 1) - { - Factory::getLanguage()->load('mod_menu', JPATH_ADMINISTRATOR); - } - - $currentMenuType = $app->getUserState($this->context . '.menutype', ''); - $menuType = $app->input->getString('menutype', $currentMenuType); - - // If client_id changed clear menutype and reset pagination - if ($clientId != $currentClientId) - { - $menuType = ''; - - $app->input->set('limitstart', 0); - $app->input->set('menutype', ''); - } - - // If menutype changed reset pagination. - if ($menuType != $currentMenuType) - { - $app->input->set('limitstart', 0); - } - - if (!$menuType) - { - $app->setUserState($this->context . '.menutype', ''); - $this->setState('menutypetitle', ''); - $this->setState('menutypeid', ''); - } - // Special menu types, if selected explicitly, will be allowed as a filter - elseif ($menuType == 'main') - { - // Adjust client_id to match the menutype. This is safe as client_id was not changed in this request. - $app->input->set('client_id', 1); - - $app->setUserState($this->context . '.menutype', $menuType); - $this->setState('menutypetitle', ucfirst($menuType)); - $this->setState('menutypeid', -1); - } - // Get the menutype object with appropriate checks. - elseif ($cMenu = $this->getMenu($menuType, true)) - { - // Adjust client_id to match the menutype. This is safe as client_id was not changed in this request. - $app->input->set('client_id', $cMenu->client_id); - - $app->setUserState($this->context . '.menutype', $menuType); - $this->setState('menutypetitle', $cMenu->title); - $this->setState('menutypeid', $cMenu->id); - } - // This menutype does not exist, leave client id unchanged but reset menutype and pagination - else - { - $menuType = ''; - - $app->input->set('limitstart', 0); - $app->input->set('menutype', $menuType); - - $app->setUserState($this->context . '.menutype', $menuType); - $this->setState('menutypetitle', ''); - $this->setState('menutypeid', ''); - } - - // Client id filter - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $this->setState('filter.client_id', $clientId); - - // Use a different filter file when client is administrator - if ($clientId == 1) - { - $this->filterFormName = 'filter_itemsadmin'; - } - - $this->setState('filter.menutype', $menuType); - - $language = $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', ''); - $this->setState('filter.language', $language); - - // Component parameters. - $params = ComponentHelper::getParams('com_menus'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language. - if (!empty($forcedLanguage)) - { - $this->setState('filter.language', $forcedLanguage); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.parent_id'); - $id .= ':' . $this->getState('filter.menutype'); - $id .= ':' . $this->getState('filter.client_id'); - - return parent::getStoreId($id); - } - - /** - * Builds an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery A query object. - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - $clientId = (int) $this->getState('filter.client_id'); - - // Select all fields from the table. - $query->select( - // We can't quote state values because they could contain expressions. - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.menutype'), - $db->quoteName('a.title'), - $db->quoteName('a.alias'), - $db->quoteName('a.note'), - $db->quoteName('a.path'), - $db->quoteName('a.link'), - $db->quoteName('a.type'), - $db->quoteName('a.parent_id'), - $db->quoteName('a.level'), - $db->quoteName('a.component_id'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.browserNav'), - $db->quoteName('a.access'), - $db->quoteName('a.img'), - $db->quoteName('a.template_style_id'), - $db->quoteName('a.params'), - $db->quoteName('a.lft'), - $db->quoteName('a.rgt'), - $db->quoteName('a.home'), - $db->quoteName('a.language'), - $db->quoteName('a.client_id'), - $db->quoteName('a.publish_up'), - $db->quoteName('a.publish_down'), - ] - ) - ) - ->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - $db->quoteName('l.sef', 'language_sef'), - $db->quoteName('u.name', 'editor'), - $db->quoteName('c.element', 'componentname'), - $db->quoteName('ag.title', 'access_level'), - $db->quoteName('mt.id', 'menutype_id'), - $db->quoteName('mt.title', 'menutype_title'), - $db->quoteName('e.enabled'), - $db->quoteName('e.name'), - 'CASE WHEN ' . $db->quoteName('a.type') . ' = ' . $db->quote('component') - . ' THEN ' . $db->quoteName('a.published') . ' +2 * (' . $db->quoteName('e.enabled') . ' -1)' - . ' ELSE ' . $db->quoteName('a.published') . ' END AS ' . $db->quoteName('published'), - ] - ) - ->from($db->quoteName('#__menu', 'a')); - - // Join over the language - $query->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); - - // Join over the users. - $query->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Join over components - $query->join('LEFT', $db->quoteName('#__extensions', 'c'), $db->quoteName('c.extension_id') . ' = ' . $db->quoteName('a.component_id')); - - // Join over the asset groups. - $query->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')); - - // Join over the menu types. - $query->join('LEFT', $db->quoteName('#__menu_types', 'mt'), $db->quoteName('mt.menutype') . ' = ' . $db->quoteName('a.menutype')); - - // Join over the extensions - $query->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')); - - // Join over the associations. - if (Associations::isEnabled()) - { - $subQuery = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') - ->from($db->quoteName('#__associations', 'asso1')) - ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) - ->where( - [ - $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), - $db->quoteName('asso1.context') . ' = ' . $db->quote('com_menus.item'), - ] - ); - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); - } - - // Exclude the root category. - $query->where( - [ - $db->quoteName('a.id') . ' > 1', - $db->quoteName('a.client_id') . ' = :clientId', - ] - ) - ->bind(':clientId', $clientId, ParameterType::INTEGER); - - // Filter on the published state. - $published = $this->getState('filter.published'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where($db->quoteName('a.published') . ' IN (0, 1)'); - } - - // Filter by search in title, alias or id - if ($search = trim($this->getState('filter.search', ''))) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - elseif (stripos($search, 'link:') === 0) - { - if ($search = str_replace(' ', '%', trim(substr($search, 5)))) - { - $query->where($db->quoteName('a.link') . ' LIKE :search') - ->bind(':search', $search); - } - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.title') . ' LIKE :search1', - $db->quoteName('a.alias') . ' LIKE :search2', - $db->quoteName('a.note') . ' LIKE :search3', - ], - 'OR' - ) - ->bind([':search1', ':search2', ':search3'], $search); - } - } - - // Filter the items over the parent id if set. - $parentId = (int) $this->getState('filter.parent_id'); - $level = (int) $this->getState('filter.level'); - - if ($parentId) - { - // Create a subquery for the sub-items list - $subQuery = $db->getQuery(true) - ->select($db->quoteName('sub.id')) - ->from($db->quoteName('#__menu', 'sub')) - ->join( - 'INNER', - $db->quoteName('#__menu', 'this'), - $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') - . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') - ) - ->where($db->quoteName('this.id') . ' = :parentId1'); - - if ($level) - { - $subQuery->where($db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :level - 1'); - $query->bind(':level', $level, ParameterType::INTEGER); - } - - // Add the subquery to the main query - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.parent_id') . ' = :parentId2', - $db->quoteName('a.parent_id') . ' IN (' . (string) $subQuery . ')', - ], - 'OR' - ) - ->bind([':parentId1', ':parentId2'], $parentId, ParameterType::INTEGER); - } - - // Filter on the level. - elseif ($level) - { - $query->where($db->quoteName('a.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter the items over the menu id if set. - $menuType = $this->getState('filter.menutype'); - - // A value "" means all - if ($menuType == '') - { - // Load all menu types we have manage access - $query2 = $db->getQuery(true) - ->select( - [ - $db->quoteName('id'), - $db->quoteName('menutype'), - ] - ) - ->from($db->quoteName('#__menu_types')) - ->where($db->quoteName('client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER) - ->order($db->quoteName('title')); - - // Show protected items on explicit filter only - $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('main')); - - $menuTypes = $db->setQuery($query2)->loadObjectList(); - - if ($menuTypes) - { - $types = array(); - - foreach ($menuTypes as $type) - { - if ($user->authorise('core.manage', 'com_menus.menu.' . (int) $type->id)) - { - $types[] = $type->menutype; - } - } - - if ($types) - { - $query->whereIn($db->quoteName('a.menutype'), $types); - } - else - { - $query->where(0); - } - } - } - // Default behavior => load all items from a specific menu - elseif (strlen($menuType)) - { - $query->where($db->quoteName('a.menutype') . ' = :menuType') - ->bind(':menuType', $menuType); - } - // Empty menu type => error - else - { - $query->where('1 != 1'); - } - - // Filter on the access level. - if ($access = (int) $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - if ($groups = $user->getAuthorisedViewLevels()) - { - $query->whereIn($db->quoteName('a.access'), $groups); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to allow derived classes to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 3.2 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $name = $form->getName(); - - if ($name == 'com_menus.items.filter') - { - $clientId = $this->getState('filter.client_id'); - $form->setFieldAttribute('menutype', 'clientid', $clientId); - } - elseif (false !== strpos($name, 'com_menus.items.modal.')) - { - $form->removeField('client_id'); - - $clientId = $this->getState('filter.client_id'); - $form->setFieldAttribute('menutype', 'clientid', $clientId); - } - } - - /** - * Get the client id for a menu - * - * @param string $menuType The menutype identifier for the menu - * @param boolean $check Flag whether to perform check against ACL as well as existence - * - * @return integer - * - * @since 3.7.0 - */ - protected function getMenu($menuType, $check = false) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select($db->quoteName('a') . '.*') - ->from($db->quoteName('#__menu_types', 'a')) - ->where($db->quoteName('menutype') . ' = :menuType') - ->bind(':menuType', $menuType); - - $cMenu = $db->setQuery($query)->loadObject(); - - if ($check) - { - // Check if menu type exists. - if (!$cMenu) - { - Log::add(Text::_('COM_MENUS_ERROR_MENUTYPE_NOT_FOUND'), Log::ERROR, 'jerror'); - - return false; - } - // Check if menu type is valid against ACL. - elseif (!Factory::getUser()->authorise('core.manage', 'com_menus.menu.' . $cMenu->id)) - { - Log::add(Text::_('JERROR_ALERTNOAUTHOR'), Log::ERROR, 'jerror'); - - return false; - } - } - - return $cMenu; - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.0.1 - */ - public function getItems() - { - $store = $this->getStoreId(); - - if (!isset($this->cache[$store])) - { - $items = parent::getItems(); - $lang = Factory::getLanguage(); - $client = $this->state->get('filter.client_id'); - - if ($items) - { - foreach ($items as $item) - { - if ($extension = $item->componentname) - { - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) - || $lang->load("$extension.sys", JPATH_ADMINISTRATOR . '/components/' . $extension); - } - - // Translate component name - if ($client === 1) - { - $item->title = Text::_($item->title); - } - } - } - - $this->cache[$store] = $items; - } - - return $this->cache[$store]; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'menutype', 'a.menutype', 'menutype_title', + 'title', 'a.title', + 'alias', 'a.alias', + 'published', 'a.published', + 'access', 'a.access', 'access_level', + 'language', 'a.language', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'lft', 'a.lft', + 'rgt', 'a.rgt', + 'level', 'a.level', + 'path', 'a.path', + 'client_id', 'a.client_id', + 'home', 'a.home', + 'parent_id', 'a.parent_id', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'a.ordering' + ); + + if (Associations::isEnabled()) { + $config['filter_fields'][] = 'association'; + } + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + $search = $this->getUserStateFromRequest($this->context . '.search', 'filter_search'); + $this->setState('filter.search', $search); + + $published = $this->getUserStateFromRequest($this->context . '.published', 'filter_published', ''); + $this->setState('filter.published', $published); + + $access = $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access'); + $this->setState('filter.access', $access); + + $parentId = $this->getUserStateFromRequest($this->context . '.filter.parent_id', 'filter_parent_id'); + $this->setState('filter.parent_id', $parentId); + + $level = $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level'); + $this->setState('filter.level', $level); + + // Watch changes in client_id and menutype and keep sync whenever needed. + $currentClientId = $app->getUserState($this->context . '.client_id', 0); + $clientId = $app->input->getInt('client_id', $currentClientId); + + // Load mod_menu.ini file when client is administrator + if ($clientId == 1) { + Factory::getLanguage()->load('mod_menu', JPATH_ADMINISTRATOR); + } + + $currentMenuType = $app->getUserState($this->context . '.menutype', ''); + $menuType = $app->input->getString('menutype', $currentMenuType); + + // If client_id changed clear menutype and reset pagination + if ($clientId != $currentClientId) { + $menuType = ''; + + $app->input->set('limitstart', 0); + $app->input->set('menutype', ''); + } + + // If menutype changed reset pagination. + if ($menuType != $currentMenuType) { + $app->input->set('limitstart', 0); + } + + if (!$menuType) { + $app->setUserState($this->context . '.menutype', ''); + $this->setState('menutypetitle', ''); + $this->setState('menutypeid', ''); + } + // Special menu types, if selected explicitly, will be allowed as a filter + elseif ($menuType == 'main') { + // Adjust client_id to match the menutype. This is safe as client_id was not changed in this request. + $app->input->set('client_id', 1); + + $app->setUserState($this->context . '.menutype', $menuType); + $this->setState('menutypetitle', ucfirst($menuType)); + $this->setState('menutypeid', -1); + } + // Get the menutype object with appropriate checks. + elseif ($cMenu = $this->getMenu($menuType, true)) { + // Adjust client_id to match the menutype. This is safe as client_id was not changed in this request. + $app->input->set('client_id', $cMenu->client_id); + + $app->setUserState($this->context . '.menutype', $menuType); + $this->setState('menutypetitle', $cMenu->title); + $this->setState('menutypeid', $cMenu->id); + } + // This menutype does not exist, leave client id unchanged but reset menutype and pagination + else { + $menuType = ''; + + $app->input->set('limitstart', 0); + $app->input->set('menutype', $menuType); + + $app->setUserState($this->context . '.menutype', $menuType); + $this->setState('menutypetitle', ''); + $this->setState('menutypeid', ''); + } + + // Client id filter + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $this->setState('filter.client_id', $clientId); + + // Use a different filter file when client is administrator + if ($clientId == 1) { + $this->filterFormName = 'filter_itemsadmin'; + } + + $this->setState('filter.menutype', $menuType); + + $language = $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', ''); + $this->setState('filter.language', $language); + + // Component parameters. + $params = ComponentHelper::getParams('com_menus'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language. + if (!empty($forcedLanguage)) { + $this->setState('filter.language', $forcedLanguage); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.parent_id'); + $id .= ':' . $this->getState('filter.menutype'); + $id .= ':' . $this->getState('filter.client_id'); + + return parent::getStoreId($id); + } + + /** + * Builds an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery A query object. + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + $clientId = (int) $this->getState('filter.client_id'); + + // Select all fields from the table. + $query->select( + // We can't quote state values because they could contain expressions. + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.menutype'), + $db->quoteName('a.title'), + $db->quoteName('a.alias'), + $db->quoteName('a.note'), + $db->quoteName('a.path'), + $db->quoteName('a.link'), + $db->quoteName('a.type'), + $db->quoteName('a.parent_id'), + $db->quoteName('a.level'), + $db->quoteName('a.component_id'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.browserNav'), + $db->quoteName('a.access'), + $db->quoteName('a.img'), + $db->quoteName('a.template_style_id'), + $db->quoteName('a.params'), + $db->quoteName('a.lft'), + $db->quoteName('a.rgt'), + $db->quoteName('a.home'), + $db->quoteName('a.language'), + $db->quoteName('a.client_id'), + $db->quoteName('a.publish_up'), + $db->quoteName('a.publish_down'), + ] + ) + ) + ->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + $db->quoteName('l.sef', 'language_sef'), + $db->quoteName('u.name', 'editor'), + $db->quoteName('c.element', 'componentname'), + $db->quoteName('ag.title', 'access_level'), + $db->quoteName('mt.id', 'menutype_id'), + $db->quoteName('mt.title', 'menutype_title'), + $db->quoteName('e.enabled'), + $db->quoteName('e.name'), + 'CASE WHEN ' . $db->quoteName('a.type') . ' = ' . $db->quote('component') + . ' THEN ' . $db->quoteName('a.published') . ' +2 * (' . $db->quoteName('e.enabled') . ' -1)' + . ' ELSE ' . $db->quoteName('a.published') . ' END AS ' . $db->quoteName('published'), + ] + ) + ->from($db->quoteName('#__menu', 'a')); + + // Join over the language + $query->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); + + // Join over the users. + $query->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Join over components + $query->join('LEFT', $db->quoteName('#__extensions', 'c'), $db->quoteName('c.extension_id') . ' = ' . $db->quoteName('a.component_id')); + + // Join over the asset groups. + $query->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')); + + // Join over the menu types. + $query->join('LEFT', $db->quoteName('#__menu_types', 'mt'), $db->quoteName('mt.menutype') . ' = ' . $db->quoteName('a.menutype')); + + // Join over the extensions + $query->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')); + + // Join over the associations. + if (Associations::isEnabled()) { + $subQuery = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') + ->from($db->quoteName('#__associations', 'asso1')) + ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) + ->where( + [ + $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), + $db->quoteName('asso1.context') . ' = ' . $db->quote('com_menus.item'), + ] + ); + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); + } + + // Exclude the root category. + $query->where( + [ + $db->quoteName('a.id') . ' > 1', + $db->quoteName('a.client_id') . ' = :clientId', + ] + ) + ->bind(':clientId', $clientId, ParameterType::INTEGER); + + // Filter on the published state. + $published = $this->getState('filter.published'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where($db->quoteName('a.published') . ' IN (0, 1)'); + } + + // Filter by search in title, alias or id + if ($search = trim($this->getState('filter.search', ''))) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } elseif (stripos($search, 'link:') === 0) { + if ($search = str_replace(' ', '%', trim(substr($search, 5)))) { + $query->where($db->quoteName('a.link') . ' LIKE :search') + ->bind(':search', $search); + } + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.title') . ' LIKE :search1', + $db->quoteName('a.alias') . ' LIKE :search2', + $db->quoteName('a.note') . ' LIKE :search3', + ], + 'OR' + ) + ->bind([':search1', ':search2', ':search3'], $search); + } + } + + // Filter the items over the parent id if set. + $parentId = (int) $this->getState('filter.parent_id'); + $level = (int) $this->getState('filter.level'); + + if ($parentId) { + // Create a subquery for the sub-items list + $subQuery = $db->getQuery(true) + ->select($db->quoteName('sub.id')) + ->from($db->quoteName('#__menu', 'sub')) + ->join( + 'INNER', + $db->quoteName('#__menu', 'this'), + $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') + . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') + ) + ->where($db->quoteName('this.id') . ' = :parentId1'); + + if ($level) { + $subQuery->where($db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :level - 1'); + $query->bind(':level', $level, ParameterType::INTEGER); + } + + // Add the subquery to the main query + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.parent_id') . ' = :parentId2', + $db->quoteName('a.parent_id') . ' IN (' . (string) $subQuery . ')', + ], + 'OR' + ) + ->bind([':parentId1', ':parentId2'], $parentId, ParameterType::INTEGER); + } + + // Filter on the level. + elseif ($level) { + $query->where($db->quoteName('a.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter the items over the menu id if set. + $menuType = $this->getState('filter.menutype'); + + // A value "" means all + if ($menuType == '') { + // Load all menu types we have manage access + $query2 = $db->getQuery(true) + ->select( + [ + $db->quoteName('id'), + $db->quoteName('menutype'), + ] + ) + ->from($db->quoteName('#__menu_types')) + ->where($db->quoteName('client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER) + ->order($db->quoteName('title')); + + // Show protected items on explicit filter only + $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('main')); + + $menuTypes = $db->setQuery($query2)->loadObjectList(); + + if ($menuTypes) { + $types = array(); + + foreach ($menuTypes as $type) { + if ($user->authorise('core.manage', 'com_menus.menu.' . (int) $type->id)) { + $types[] = $type->menutype; + } + } + + if ($types) { + $query->whereIn($db->quoteName('a.menutype'), $types); + } else { + $query->where(0); + } + } + } + // Default behavior => load all items from a specific menu + elseif (strlen($menuType)) { + $query->where($db->quoteName('a.menutype') . ' = :menuType') + ->bind(':menuType', $menuType); + } + // Empty menu type => error + else { + $query->where('1 != 1'); + } + + // Filter on the access level. + if ($access = (int) $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + if ($groups = $user->getAuthorisedViewLevels()) { + $query->whereIn($db->quoteName('a.access'), $groups); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to allow derived classes to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 3.2 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $name = $form->getName(); + + if ($name == 'com_menus.items.filter') { + $clientId = $this->getState('filter.client_id'); + $form->setFieldAttribute('menutype', 'clientid', $clientId); + } elseif (false !== strpos($name, 'com_menus.items.modal.')) { + $form->removeField('client_id'); + + $clientId = $this->getState('filter.client_id'); + $form->setFieldAttribute('menutype', 'clientid', $clientId); + } + } + + /** + * Get the client id for a menu + * + * @param string $menuType The menutype identifier for the menu + * @param boolean $check Flag whether to perform check against ACL as well as existence + * + * @return integer + * + * @since 3.7.0 + */ + protected function getMenu($menuType, $check = false) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select($db->quoteName('a') . '.*') + ->from($db->quoteName('#__menu_types', 'a')) + ->where($db->quoteName('menutype') . ' = :menuType') + ->bind(':menuType', $menuType); + + $cMenu = $db->setQuery($query)->loadObject(); + + if ($check) { + // Check if menu type exists. + if (!$cMenu) { + Log::add(Text::_('COM_MENUS_ERROR_MENUTYPE_NOT_FOUND'), Log::ERROR, 'jerror'); + + return false; + } + // Check if menu type is valid against ACL. + elseif (!Factory::getUser()->authorise('core.manage', 'com_menus.menu.' . $cMenu->id)) { + Log::add(Text::_('JERROR_ALERTNOAUTHOR'), Log::ERROR, 'jerror'); + + return false; + } + } + + return $cMenu; + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.0.1 + */ + public function getItems() + { + $store = $this->getStoreId(); + + if (!isset($this->cache[$store])) { + $items = parent::getItems(); + $lang = Factory::getLanguage(); + $client = $this->state->get('filter.client_id'); + + if ($items) { + foreach ($items as $item) { + if ($extension = $item->componentname) { + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) + || $lang->load("$extension.sys", JPATH_ADMINISTRATOR . '/components/' . $extension); + } + + // Translate component name + if ($client === 1) { + $item->title = Text::_($item->title); + } + } + } + + $this->cache[$store] = $items; + } + + return $this->cache[$store]; + } } diff --git a/administrator/components/com_menus/src/Model/MenuModel.php b/administrator/components/com_menus/src/Model/MenuModel.php index 3a26565ace426..a8d224aee0aa9 100644 --- a/administrator/components/com_menus/src/Model/MenuModel.php +++ b/administrator/components/com_menus/src/Model/MenuModel.php @@ -1,4 +1,5 @@ authorise('core.delete', 'com_menus.menu.' . (int) $record->id); - } - - /** - * Method to test whether the state of a record can be edited. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - return Factory::getUser()->authorise('core.edit.state', 'com_menus.menu.' . (int) $record->id); - } - - /** - * Returns a Table object, always creating it - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - * - * @since 1.6 - */ - public function getTable($type = 'MenuType', $prefix = '\JTable', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - - /** - * Auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $id = $app->input->getInt('id'); - $this->setState('menu.id', $id); - - // Load the parameters. - $params = ComponentHelper::getParams('com_menus'); - $this->setState('params', $params); - - // Load the clientId. - $clientId = $app->getUserStateFromRequest('com_menus.menus.client_id', 'client_id', 0, 'int'); - $this->setState('client_id', $clientId); - } - - /** - * Method to get a menu item. - * - * @param integer $itemId The id of the menu item to get. - * - * @return mixed Menu item data object on success, false on failure. - * - * @since 1.6 - */ - public function &getItem($itemId = null) - { - $itemId = (!empty($itemId)) ? $itemId : (int) $this->getState('menu.id'); - - // Get a menu item row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load($itemId); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - - $properties = $table->getProperties(1); - $value = ArrayHelper::toObject($properties, CMSObject::class); - - return $value; - } - - /** - * Method to get the menu item form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_menus.menu', 'menu', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - if (!$this->getState('client_id', 0)) - { - $form->removeField('preset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_menus.edit.menu.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - if (empty($data->id)) - { - $data->client_id = $this->state->get('client_id', 0); - } - } - else - { - unset($data['preset']); - } - - $this->preprocessData('com_menus.menu', $data); - - return $data; - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see JFormRule - * @see JFilterInput - * @since 3.9.23 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', 'com_menus')) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $id = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('menu.id'); - $isNew = true; - - // Get a row instance. - $table = $this->getTable(); - - // Include the plugins for the save events. - PluginHelper::importPlugin('content'); - - // Load the row if saving an existing item. - if ($id > 0) - { - $isNew = false; - $table->load($id); - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before event. - $result = Factory::getApplication()->triggerEvent('onContentBeforeSave', array($this->_context, &$table, $isNew, $data)); - - // Store the data. - if (in_array(false, $result, true) || !$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent('onContentAfterSave', array($this->_context, &$table, $isNew)); - - $this->setState('menu.id', $table->id); - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to delete groups. - * - * @param array $itemIds An array of item ids. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - */ - public function delete($itemIds) - { - // Sanitize the ids. - $itemIds = ArrayHelper::toInteger((array) $itemIds); - - // Get a group row instance. - $table = $this->getTable(); - - // Include the plugins for the delete events. - PluginHelper::importPlugin('content'); - - // Iterate the items to delete each one. - foreach ($itemIds as $itemId) - { - if ($table->load($itemId)) - { - // Trigger the before delete event. - $result = Factory::getApplication()->triggerEvent('onContentBeforeDelete', array($this->_context, $table)); - - if (in_array(false, $result, true) || !$table->delete($itemId)) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after delete event. - Factory::getApplication()->triggerEvent('onContentAfterDelete', array($this->_context, $table)); - - // @todo: Delete the menu associations - Menu items and Modules - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Gets a list of all mod_mainmenu modules and collates them by menutype - * - * @return array - * - * @since 1.6 - */ - public function &getModules() - { - $db = $this->getDatabase(); - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('a.id'), - $db->quoteName('a.title'), - $db->quoteName('a.params'), - $db->quoteName('a.position'), - $db->quoteName('ag.title', 'access_title'), - ] - ) - ->from($db->quoteName('#__modules', 'a')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) - ->where($db->quoteName('a.module') . ' = ' . $db->quote('mod_menu')); - $db->setQuery($query); - - $modules = $db->loadObjectList(); - - $result = array(); - - foreach ($modules as &$module) - { - $params = new Registry($module->params); - - $menuType = $params->get('menutype'); - - if (!isset($result[$menuType])) - { - $result[$menuType] = array(); - } - - $result[$menuType][] = & $module; - } - - return $result; - } - - /** - * Returns the extension elements for the given items - * - * @param array $itemIds The item ids - * - * @return array - * - * @since 4.2.0 - */ - public function getExtensionElementsForMenuItems(array $itemIds): array - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query - ->select($db->quoteName('e.element')) - ->from($db->quoteName('#__extensions', 'e')) - ->join('INNER', $db->quoteName('#__menu', 'm'), $db->quoteName('m.component_id') . ' = ' . $db->quoteName('e.extension_id')) - ->whereIn($db->quoteName('m.id'), ArrayHelper::toInteger($itemIds)); - - return $db->setQuery($query)->loadColumn(); - } - - /** - * Custom clean the cache - * - * @param string $group Cache group name. - * @param integer $clientId @deprecated 5.0 No Longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_menus'); - parent::cleanCache('com_modules'); - parent::cleanCache('mod_menu'); - } + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_MENUS_MENU'; + + /** + * Model context string. + * + * @var string + */ + protected $_context = 'com_menus.menu'; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + return Factory::getUser()->authorise('core.delete', 'com_menus.menu.' . (int) $record->id); + } + + /** + * Method to test whether the state of a record can be edited. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + return Factory::getUser()->authorise('core.edit.state', 'com_menus.menu.' . (int) $record->id); + } + + /** + * Returns a Table object, always creating it + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + * + * @since 1.6 + */ + public function getTable($type = 'MenuType', $prefix = '\JTable', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $id = $app->input->getInt('id'); + $this->setState('menu.id', $id); + + // Load the parameters. + $params = ComponentHelper::getParams('com_menus'); + $this->setState('params', $params); + + // Load the clientId. + $clientId = $app->getUserStateFromRequest('com_menus.menus.client_id', 'client_id', 0, 'int'); + $this->setState('client_id', $clientId); + } + + /** + * Method to get a menu item. + * + * @param integer $itemId The id of the menu item to get. + * + * @return mixed Menu item data object on success, false on failure. + * + * @since 1.6 + */ + public function &getItem($itemId = null) + { + $itemId = (!empty($itemId)) ? $itemId : (int) $this->getState('menu.id'); + + // Get a menu item row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load($itemId); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + + $properties = $table->getProperties(1); + $value = ArrayHelper::toObject($properties, CMSObject::class); + + return $value; + } + + /** + * Method to get the menu item form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_menus.menu', 'menu', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + if (!$this->getState('client_id', 0)) { + $form->removeField('preset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_menus.edit.menu.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + if (empty($data->id)) { + $data->client_id = $this->state->get('client_id', 0); + } + } else { + unset($data['preset']); + } + + $this->preprocessData('com_menus.menu', $data); + + return $data; + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see JFormRule + * @see JFilterInput + * @since 3.9.23 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', 'com_menus')) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $id = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('menu.id'); + $isNew = true; + + // Get a row instance. + $table = $this->getTable(); + + // Include the plugins for the save events. + PluginHelper::importPlugin('content'); + + // Load the row if saving an existing item. + if ($id > 0) { + $isNew = false; + $table->load($id); + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before event. + $result = Factory::getApplication()->triggerEvent('onContentBeforeSave', array($this->_context, &$table, $isNew, $data)); + + // Store the data. + if (in_array(false, $result, true) || !$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent('onContentAfterSave', array($this->_context, &$table, $isNew)); + + $this->setState('menu.id', $table->id); + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to delete groups. + * + * @param array $itemIds An array of item ids. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + */ + public function delete($itemIds) + { + // Sanitize the ids. + $itemIds = ArrayHelper::toInteger((array) $itemIds); + + // Get a group row instance. + $table = $this->getTable(); + + // Include the plugins for the delete events. + PluginHelper::importPlugin('content'); + + // Iterate the items to delete each one. + foreach ($itemIds as $itemId) { + if ($table->load($itemId)) { + // Trigger the before delete event. + $result = Factory::getApplication()->triggerEvent('onContentBeforeDelete', array($this->_context, $table)); + + if (in_array(false, $result, true) || !$table->delete($itemId)) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after delete event. + Factory::getApplication()->triggerEvent('onContentAfterDelete', array($this->_context, $table)); + + // @todo: Delete the menu associations - Menu items and Modules + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Gets a list of all mod_mainmenu modules and collates them by menutype + * + * @return array + * + * @since 1.6 + */ + public function &getModules() + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('a.id'), + $db->quoteName('a.title'), + $db->quoteName('a.params'), + $db->quoteName('a.position'), + $db->quoteName('ag.title', 'access_title'), + ] + ) + ->from($db->quoteName('#__modules', 'a')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) + ->where($db->quoteName('a.module') . ' = ' . $db->quote('mod_menu')); + $db->setQuery($query); + + $modules = $db->loadObjectList(); + + $result = array(); + + foreach ($modules as &$module) { + $params = new Registry($module->params); + + $menuType = $params->get('menutype'); + + if (!isset($result[$menuType])) { + $result[$menuType] = array(); + } + + $result[$menuType][] = & $module; + } + + return $result; + } + + /** + * Returns the extension elements for the given items + * + * @param array $itemIds The item ids + * + * @return array + * + * @since 4.2.0 + */ + public function getExtensionElementsForMenuItems(array $itemIds): array + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query + ->select($db->quoteName('e.element')) + ->from($db->quoteName('#__extensions', 'e')) + ->join('INNER', $db->quoteName('#__menu', 'm'), $db->quoteName('m.component_id') . ' = ' . $db->quoteName('e.extension_id')) + ->whereIn($db->quoteName('m.id'), ArrayHelper::toInteger($itemIds)); + + return $db->setQuery($query)->loadColumn(); + } + + /** + * Custom clean the cache + * + * @param string $group Cache group name. + * @param integer $clientId @deprecated 5.0 No Longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_menus'); + parent::cleanCache('com_modules'); + parent::cleanCache('mod_menu'); + } } diff --git a/administrator/components/com_menus/src/Model/MenusModel.php b/administrator/components/com_menus/src/Model/MenusModel.php index 8ae09b42d569b..33e775b5a7aa3 100644 --- a/administrator/components/com_menus/src/Model/MenusModel.php +++ b/administrator/components/com_menus/src/Model/MenusModel.php @@ -1,4 +1,5 @@ getStoreId('getItems'); - - // Try to load the data from internal storage. - if (!empty($this->cache[$store])) - { - return $this->cache[$store]; - } - - // Load the list items. - $items = parent::getItems(); - - // If empty or an error, just return. - if (empty($items)) - { - return array(); - } - - // Getting the following metric by joins is WAY TOO SLOW. - // Faster to do three queries for very large menu trees. - - // Get the menu types of menus in the list. - $db = $this->getDatabase(); - $menuTypes = array_column((array) $items, 'menutype'); - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('m.menutype'), - 'COUNT(DISTINCT ' . $db->quoteName('m.id') . ') AS ' . $db->quoteName('count_published'), - ] - ) - ->from($db->quoteName('#__menu', 'm')) - ->where($db->quoteName('m.published') . ' = :published') - ->whereIn($db->quoteName('m.menutype'), $menuTypes, ParameterType::STRING) - ->group($db->quoteName('m.menutype')) - ->bind(':published', $published, ParameterType::INTEGER); - - $db->setQuery($query); - - // Get the published menu counts. - try - { - $published = 1; - $countPublished = $db->loadAssocList('menutype', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get the unpublished menu counts. - try - { - $published = 0; - $countUnpublished = $db->loadAssocList('menutype', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get the trashed menu counts. - try - { - $published = -2; - $countTrashed = $db->loadAssocList('menutype', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Inject the values back into the array. - foreach ($items as $item) - { - $item->count_published = $countPublished[$item->menutype] ?? 0; - $item->count_unpublished = $countUnpublished[$item->menutype] ?? 0; - $item->count_trashed = $countTrashed[$item->menutype] ?? 0; - } - - // Add the items to the internal cache. - $this->cache[$store] = $items; - - return $this->cache[$store]; - } - - /** - * Method to build an SQL query to load the list data. - * - * @return string An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $clientId = (int) $this->getState('client_id'); - - // Select all fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.menutype'), - $db->quoteName('a.title'), - $db->quoteName('a.description'), - $db->quoteName('a.client_id'), - ] - ) - ) - ->from($db->quoteName('#__menu_types', 'a')) - ->where( - [ - $db->quoteName('a.id') . ' > 0', - $db->quoteName('a.client_id') . ' = :clientId', - ] - ) - ->bind(':clientId', $clientId, ParameterType::INTEGER); - - // Filter by search in title or menutype - if ($search = trim($this->getState('filter.search', ''))) - { - $search = '%' . str_replace(' ', '%', $search) . '%'; - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.title') . ' LIKE :search1' , - $db->quoteName('a.menutype') . ' LIKE :search2', - ], - 'OR' - ) - ->bind([':search1', ':search2'], $search); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.id')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.title', $direction = 'asc') - { - $search = $this->getUserStateFromRequest($this->context . '.search', 'filter_search'); - $this->setState('filter.search', $search); - - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $this->setState('client_id', $clientId); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Gets the extension id of the core mod_menu module. - * - * @return integer - * - * @since 2.5 - */ - public function getModMenuId() - { - $clientId = (int) $this->getState('client_id'); - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('e.extension_id')) - ->from($db->quoteName('#__extensions', 'e')) - ->where( - [ - $db->quoteName('e.type') . ' = ' . $db->quote('module'), - $db->quoteName('e.element') . ' = ' . $db->quote('mod_menu'), - $db->quoteName('e.client_id') . ' = :clientId', - ] - ) - ->bind(':clientId', $clientId, ParameterType::INTEGER); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Gets a list of all mod_mainmenu modules and collates them by menutype - * - * @return array - * - * @since 1.6 - */ - public function &getModules() - { - $model = $this->bootComponent('com_menus') - ->getMVCFactory()->createModel('Menu', 'Administrator', ['ignore_request' => true]); - $result = $model->getModules(); - - return $result; - } - - /** - * Returns the missing module languages. - * - * @return array - * - * @since _DEPLOY_VERSION__ - */ - public function getMissingModuleLanguages(): array - { - // Check custom administrator menu modules - if (!ModuleHelper::isAdminMultilang()) - { - return []; - } - - $languages = LanguageHelper::getInstalledLanguages(1, true); - $langCodes = []; - - foreach ($languages as $language) - { - if (isset($language->metadata['nativeName'])) - { - $languageName = $language->metadata['nativeName']; - } - else - { - $languageName = $language->metadata['name']; - } - - $langCodes[$language->metadata['tag']] = $languageName; - } - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select($db->quoteName('m.language')) - ->from($db->quoteName('#__modules', 'm')) - ->where( - [ - $db->quoteName('m.module') . ' = ' . $db->quote('mod_menu'), - $db->quoteName('m.published') . ' = 1', - $db->quoteName('m.client_id') . ' = 1', - ] - ) - ->group($db->quoteName('m.language')); - - $mLanguages = $db->setQuery($query)->loadColumn(); - - // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. - if (!in_array('*', $mLanguages) && count($langMissing = array_diff(array_keys($langCodes), $mLanguages))) - { - return array_intersect_key($langCodes, array_flip($langMissing)); - } - - return []; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'menutype', 'a.menutype', + 'client_id', 'a.client_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Overrides the getItems method to attach additional metrics to the list. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 1.6.1 + */ + public function getItems() + { + // Get a storage key. + $store = $this->getStoreId('getItems'); + + // Try to load the data from internal storage. + if (!empty($this->cache[$store])) { + return $this->cache[$store]; + } + + // Load the list items. + $items = parent::getItems(); + + // If empty or an error, just return. + if (empty($items)) { + return array(); + } + + // Getting the following metric by joins is WAY TOO SLOW. + // Faster to do three queries for very large menu trees. + + // Get the menu types of menus in the list. + $db = $this->getDatabase(); + $menuTypes = array_column((array) $items, 'menutype'); + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('m.menutype'), + 'COUNT(DISTINCT ' . $db->quoteName('m.id') . ') AS ' . $db->quoteName('count_published'), + ] + ) + ->from($db->quoteName('#__menu', 'm')) + ->where($db->quoteName('m.published') . ' = :published') + ->whereIn($db->quoteName('m.menutype'), $menuTypes, ParameterType::STRING) + ->group($db->quoteName('m.menutype')) + ->bind(':published', $published, ParameterType::INTEGER); + + $db->setQuery($query); + + // Get the published menu counts. + try { + $published = 1; + $countPublished = $db->loadAssocList('menutype', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get the unpublished menu counts. + try { + $published = 0; + $countUnpublished = $db->loadAssocList('menutype', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get the trashed menu counts. + try { + $published = -2; + $countTrashed = $db->loadAssocList('menutype', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Inject the values back into the array. + foreach ($items as $item) { + $item->count_published = $countPublished[$item->menutype] ?? 0; + $item->count_unpublished = $countUnpublished[$item->menutype] ?? 0; + $item->count_trashed = $countTrashed[$item->menutype] ?? 0; + } + + // Add the items to the internal cache. + $this->cache[$store] = $items; + + return $this->cache[$store]; + } + + /** + * Method to build an SQL query to load the list data. + * + * @return string An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $clientId = (int) $this->getState('client_id'); + + // Select all fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.menutype'), + $db->quoteName('a.title'), + $db->quoteName('a.description'), + $db->quoteName('a.client_id'), + ] + ) + ) + ->from($db->quoteName('#__menu_types', 'a')) + ->where( + [ + $db->quoteName('a.id') . ' > 0', + $db->quoteName('a.client_id') . ' = :clientId', + ] + ) + ->bind(':clientId', $clientId, ParameterType::INTEGER); + + // Filter by search in title or menutype + if ($search = trim($this->getState('filter.search', ''))) { + $search = '%' . str_replace(' ', '%', $search) . '%'; + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.title') . ' LIKE :search1' , + $db->quoteName('a.menutype') . ' LIKE :search2', + ], + 'OR' + ) + ->bind([':search1', ':search2'], $search); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.id')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.title', $direction = 'asc') + { + $search = $this->getUserStateFromRequest($this->context . '.search', 'filter_search'); + $this->setState('filter.search', $search); + + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $this->setState('client_id', $clientId); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Gets the extension id of the core mod_menu module. + * + * @return integer + * + * @since 2.5 + */ + public function getModMenuId() + { + $clientId = (int) $this->getState('client_id'); + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('e.extension_id')) + ->from($db->quoteName('#__extensions', 'e')) + ->where( + [ + $db->quoteName('e.type') . ' = ' . $db->quote('module'), + $db->quoteName('e.element') . ' = ' . $db->quote('mod_menu'), + $db->quoteName('e.client_id') . ' = :clientId', + ] + ) + ->bind(':clientId', $clientId, ParameterType::INTEGER); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Gets a list of all mod_mainmenu modules and collates them by menutype + * + * @return array + * + * @since 1.6 + */ + public function &getModules() + { + $model = $this->bootComponent('com_menus') + ->getMVCFactory()->createModel('Menu', 'Administrator', ['ignore_request' => true]); + $result = $model->getModules(); + + return $result; + } + + /** + * Returns the missing module languages. + * + * @return array + * + * @since _DEPLOY_VERSION__ + */ + public function getMissingModuleLanguages(): array + { + // Check custom administrator menu modules + if (!ModuleHelper::isAdminMultilang()) { + return []; + } + + $languages = LanguageHelper::getInstalledLanguages(1, true); + $langCodes = []; + + foreach ($languages as $language) { + if (isset($language->metadata['nativeName'])) { + $languageName = $language->metadata['nativeName']; + } else { + $languageName = $language->metadata['name']; + } + + $langCodes[$language->metadata['tag']] = $languageName; + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select($db->quoteName('m.language')) + ->from($db->quoteName('#__modules', 'm')) + ->where( + [ + $db->quoteName('m.module') . ' = ' . $db->quote('mod_menu'), + $db->quoteName('m.published') . ' = 1', + $db->quoteName('m.client_id') . ' = 1', + ] + ) + ->group($db->quoteName('m.language')); + + $mLanguages = $db->setQuery($query)->loadColumn(); + + // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. + if (!in_array('*', $mLanguages) && count($langMissing = array_diff(array_keys($langCodes), $mLanguages))) { + return array_intersect_key($langCodes, array_flip($langMissing)); + } + + return []; + } } diff --git a/administrator/components/com_menus/src/Model/MenutypesModel.php b/administrator/components/com_menus/src/Model/MenutypesModel.php index 85ed3f75a8e26..65ff871bad303 100644 --- a/administrator/components/com_menus/src/Model/MenutypesModel.php +++ b/administrator/components/com_menus/src/Model/MenutypesModel.php @@ -1,4 +1,5 @@ input->get('client_id', 0); - - $this->state->set('client_id', $clientId); - } - - /** - * Method to get the reverse lookup of the base link URL to Title - * - * @return array Array of reverse lookup of the base link URL to Title - * - * @since 1.6 - */ - public function getReverseLookup() - { - if (empty($this->rlu)) - { - $this->getTypeOptions(); - } - - return $this->rlu; - } - - /** - * Method to get the available menu item type options. - * - * @return array Array of groups with menu item types. - * - * @since 1.6 - */ - public function getTypeOptions() - { - $lang = Factory::getLanguage(); - $list = array(); - - // Get the list of components. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('name'), - $db->quoteName('element', 'option'), - ] - ) - ->from($db->quoteName('#__extensions')) - ->where( - [ - $db->quoteName('type') . ' = ' . $db->quote('component'), - $db->quoteName('enabled') . ' = 1', - ] - ) - ->order($db->quoteName('name') . ' ASC'); - $db->setQuery($query); - $components = $db->loadObjectList(); - - foreach ($components as $component) - { - $options = $this->getTypeOptionsByComponent($component->option); - - if ($options) - { - $list[$component->name] = $options; - - // Create the reverse lookup for link-to-name. - foreach ($options as $option) - { - if (isset($option->request)) - { - $this->addReverseLookupUrl($option); - - if (isset($option->request['option'])) - { - $componentLanguageFolder = JPATH_ADMINISTRATOR . '/components/' . $option->request['option']; - $lang->load($option->request['option'] . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($option->request['option'] . '.sys', $componentLanguageFolder); - } - } - } - } - } - - // Allow a system plugin to insert dynamic menu types to the list shown in menus: - Factory::getApplication()->triggerEvent('onAfterGetMenuTypeOptions', array(&$list, $this)); - - return $list; - } - - /** - * Method to create the reverse lookup for link-to-name. - * (can be used from onAfterGetMenuTypeOptions handlers) - * - * @param CMSObject $option Object with request array or string and title public variables - * - * @return void - * - * @since 3.1 - */ - public function addReverseLookupUrl($option) - { - $this->rlu[MenusHelper::getLinkKey($option->request)] = $option->get('title'); - } - - /** - * Get menu types by component. - * - * @param string $component Component URL option. - * - * @return array - * - * @since 1.6 - */ - protected function getTypeOptionsByComponent($component) - { - $options = array(); - $client = ApplicationHelper::getClientInfo($this->getState('client_id')); - $mainXML = $client->path . '/components/' . $component . '/metadata.xml'; - - if (is_file($mainXML)) - { - $options = $this->getTypeOptionsFromXml($mainXML, $component); - } - - if (empty($options)) - { - $options = $this->getTypeOptionsFromMvc($component); - } - - if ($client->id == 1 && empty($options)) - { - $options = $this->getTypeOptionsFromManifest($component); - } - - return $options; - } - - /** - * Get the menu types from an XML file - * - * @param string $file File path - * @param string $component Component option as in URL - * - * @return array|boolean - * - * @since 1.6 - */ - protected function getTypeOptionsFromXml($file, $component) - { - $options = array(); - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($file)) - { - return false; - } - - // Look for the first menu node off of the root node. - if (!$menu = $xml->xpath('menu[1]')) - { - return false; - } - else - { - $menu = $menu[0]; - } - - // If we have no options to parse, just add the base component to the list of options. - if (!empty($menu['options']) && $menu['options'] == 'none') - { - // Create the menu option for the component. - $o = new CMSObject; - $o->title = (string) $menu['name']; - $o->description = (string) $menu['msg']; - $o->request = array('option' => $component); - - $options[] = $o; - - return $options; - } - - // Look for the first options node off of the menu node. - if (!$optionsNode = $menu->xpath('options[1]')) - { - return false; - } - else - { - $optionsNode = $optionsNode[0]; - } - - // Make sure the options node has children. - if (!$children = $optionsNode->children()) - { - return false; - } - - // Process each child as an option. - foreach ($children as $child) - { - if ($child->getName() == 'option') - { - // Create the menu option for the component. - $o = new CMSObject; - $o->title = (string) $child['name']; - $o->description = (string) $child['msg']; - $o->request = array('option' => $component, (string) $optionsNode['var'] => (string) $child['value']); - - $options[] = $o; - } - elseif ($child->getName() == 'default') - { - // Create the menu option for the component. - $o = new CMSObject; - $o->title = (string) $child['name']; - $o->description = (string) $child['msg']; - $o->request = array('option' => $component); - - $options[] = $o; - } - } - - return $options; - } - - /** - * Get menu types from MVC - * - * @param string $component Component option like in URLs - * - * @return array|boolean - * - * @since 1.6 - */ - protected function getTypeOptionsFromMvc($component) - { - $options = array(); - $views = array(); - - foreach ($this->getFolders($component) as $path) - { - if (!is_dir($path)) - { - continue; - } - - $views = array_merge($views, Folder::folders($path, '.', false, true)); - } - - foreach ($views as $viewPath) - { - $view = basename($viewPath); - - // Ignore private views. - if (strpos($view, '_') !== 0) - { - // Determine if a metadata file exists for the view. - $file = $viewPath . '/metadata.xml'; - - if (is_file($file)) - { - // Attempt to load the xml file. - if ($xml = simplexml_load_file($file)) - { - // Look for the first view node off of the root node. - if ($menu = $xml->xpath('view[1]')) - { - $menu = $menu[0]; - - // If the view is hidden from the menu, discard it and move on to the next view. - if (!empty($menu['hidden']) && $menu['hidden'] == 'true') - { - unset($xml); - continue; - } - - // Do we have an options node or should we process layouts? - // Look for the first options node off of the menu node. - if ($optionsNode = $menu->xpath('options[1]')) - { - $optionsNode = $optionsNode[0]; - - // Make sure the options node has children. - if ($children = $optionsNode->children()) - { - // Process each child as an option. - foreach ($children as $child) - { - if ($child->getName() == 'option') - { - // Create the menu option for the component. - $o = new CMSObject; - $o->title = (string) $child['name']; - $o->description = (string) $child['msg']; - $o->request = array('option' => $component, 'view' => $view, (string) $optionsNode['var'] => (string) $child['value']); - - $options[] = $o; - } - elseif ($child->getName() == 'default') - { - // Create the menu option for the component. - $o = new CMSObject; - $o->title = (string) $child['name']; - $o->description = (string) $child['msg']; - $o->request = array('option' => $component, 'view' => $view); - - $options[] = $o; - } - } - } - } - else - { - $options = array_merge($options, (array) $this->getTypeOptionsFromLayouts($component, $view)); - } - } - - unset($xml); - } - } - else - { - $options = array_merge($options, (array) $this->getTypeOptionsFromLayouts($component, $view)); - } - } - } - - return $options; - } - - /** - * Get menu types from Component manifest - * - * @param string $component Component option like in URLs - * - * @return array|boolean - * - * @since 3.7.0 - */ - protected function getTypeOptionsFromManifest($component) - { - // Load the component manifest - $fileName = JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml'; - - if (!is_file($fileName)) - { - return false; - } - - if (!($manifest = simplexml_load_file($fileName))) - { - return false; - } - - // Check for a valid XML root tag. - if ($manifest->getName() != 'extension') - { - return false; - } - - $options = array(); - - // Start with the component root menu. - $rootMenu = $manifest->administration->menu; - - // If the menu item doesn't exist or is hidden do nothing. - if (!$rootMenu || in_array((string) $rootMenu['hidden'], array('true', 'hidden'))) - { - return $options; - } - - // Create the root menu option. - $ro = new \stdClass; - $ro->title = (string) trim($rootMenu); - $ro->description = ''; - $ro->request = array('option' => $component); - - // Process submenu options. - $submenu = $manifest->administration->submenu; - - if (!$submenu) - { - return $options; - } - - foreach ($submenu->menu as $child) - { - $attributes = $child->attributes(); - - $o = new \stdClass; - $o->title = (string) trim($child); - $o->description = ''; - - if ((string) $attributes->link) - { - parse_str((string) $attributes->link, $request); - } - else - { - $request = array(); - - $request['option'] = $component; - $request['act'] = (string) $attributes->act; - $request['task'] = (string) $attributes->task; - $request['controller'] = (string) $attributes->controller; - $request['view'] = (string) $attributes->view; - $request['layout'] = (string) $attributes->layout; - $request['sub'] = (string) $attributes->sub; - } - - $o->request = array_filter($request, 'strlen'); - $options[] = new CMSObject($o); - - // Do not repeat the default view link (index.php?option=com_abc). - if (count($o->request) == 1) - { - $ro = null; - } - } - - if ($ro) - { - $options[] = new CMSObject($ro); - } - - return $options; - } - - /** - * Get the menu types from component layouts - * - * @param string $component Component option as in URLs - * @param string $view Name of the view - * - * @return array - * - * @since 1.6 - */ - protected function getTypeOptionsFromLayouts($component, $view) - { - $options = array(); - $layouts = array(); - $layoutNames = array(); - $lang = Factory::getLanguage(); - $client = ApplicationHelper::getClientInfo($this->getState('client_id')); - - // Get the views for this component. - foreach ($this->getFolders($component) as $folder) - { - $path = $folder . '/' . $view . '/tmpl'; - - if (!is_dir($path)) - { - $path = $folder . '/' . $view; - } - - if (!is_dir($path)) - { - continue; - } - - $layouts = array_merge($layouts, Folder::files($path, '.xml$', false, true)); - } - - // Build list of standard layout names - foreach ($layouts as $layout) - { - // Ignore private layouts. - if (strpos(basename($layout), '_') === false) - { - // Get the layout name. - $layoutNames[] = basename($layout, '.xml'); - } - } - - // Get the template layouts - // @todo: This should only search one template -- the current template for this item (default of specified) - $folders = Folder::folders($client->path . '/templates', '', false, true); - - // Array to hold association between template file names and templates - $templateName = array(); - - foreach ($folders as $folder) - { - if (is_dir($folder . '/html/' . $component . '/' . $view)) - { - $template = basename($folder); - $lang->load('tpl_' . $template . '.sys', $client->path) - || $lang->load('tpl_' . $template . '.sys', $client->path . '/templates/' . $template); - - $templateLayouts = Folder::files($folder . '/html/' . $component . '/' . $view, '.xml$', false, true); - - foreach ($templateLayouts as $layout) - { - // Get the layout name. - $templateLayoutName = basename($layout, '.xml'); - - // Add to the list only if it is not a standard layout - if (array_search($templateLayoutName, $layoutNames) === false) - { - $layouts[] = $layout; - - // Set template name array so we can get the right template for the layout - $templateName[$layout] = basename($folder); - } - } - } - } - - // Process the found layouts. - foreach ($layouts as $layout) - { - // Ignore private layouts. - if (strpos(basename($layout), '_') === false) - { - $file = $layout; - - // Get the layout name. - $layout = basename($layout, '.xml'); - - // Create the menu option for the layout. - $o = new CMSObject; - $o->title = ucfirst($layout); - $o->description = ''; - $o->request = array('option' => $component, 'view' => $view); - - // Only add the layout request argument if not the default layout. - if ($layout != 'default') - { - // If the template is set, add in format template:layout so we save the template name - $o->request['layout'] = isset($templateName[$file]) ? $templateName[$file] . ':' . $layout : $layout; - } - - // Load layout metadata if it exists. - if (is_file($file)) - { - // Attempt to load the xml file. - if ($xml = simplexml_load_file($file)) - { - // Look for the first view node off of the root node. - if ($menu = $xml->xpath('layout[1]')) - { - $menu = $menu[0]; - - // If the view is hidden from the menu, discard it and move on to the next view. - if (!empty($menu['hidden']) && $menu['hidden'] == 'true') - { - unset($xml); - unset($o); - continue; - } - - // Populate the title and description if they exist. - if (!empty($menu['title'])) - { - $o->title = trim((string) $menu['title']); - } - - if (!empty($menu->message[0])) - { - $o->description = trim((string) $menu->message[0]); - } - } - } - } - - // Add the layout to the options array. - $options[] = $o; - } - } - - return $options; - } - - /** - * Get the folders with template files for the given component. - * - * @param string $component Component option as in URLs - * - * @return array - * - * @since 4.0.0 - */ - private function getFolders($component) - { - $client = ApplicationHelper::getClientInfo($this->getState('client_id')); - - if (!is_dir($client->path . '/components/' . $component)) - { - return array(); - } - - $folders = Folder::folders($client->path . '/components/' . $component, '^view[s]?$', false, true); - $folders = array_merge($folders, Folder::folders($client->path . '/components/' . $component, '^tmpl?$', false, true)); - - if (!$folders) - { - return array(); - } - - return $folders; - } + /** + * A reverse lookup of the base link URL to Title + * + * @var array + */ + protected $rlu = array(); + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * @return void + * + * @note Calling getState in this method will result in recursion. + * @since 3.0.1 + */ + protected function populateState() + { + parent::populateState(); + + $clientId = Factory::getApplication()->input->get('client_id', 0); + + $this->state->set('client_id', $clientId); + } + + /** + * Method to get the reverse lookup of the base link URL to Title + * + * @return array Array of reverse lookup of the base link URL to Title + * + * @since 1.6 + */ + public function getReverseLookup() + { + if (empty($this->rlu)) { + $this->getTypeOptions(); + } + + return $this->rlu; + } + + /** + * Method to get the available menu item type options. + * + * @return array Array of groups with menu item types. + * + * @since 1.6 + */ + public function getTypeOptions() + { + $lang = Factory::getLanguage(); + $list = array(); + + // Get the list of components. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('name'), + $db->quoteName('element', 'option'), + ] + ) + ->from($db->quoteName('#__extensions')) + ->where( + [ + $db->quoteName('type') . ' = ' . $db->quote('component'), + $db->quoteName('enabled') . ' = 1', + ] + ) + ->order($db->quoteName('name') . ' ASC'); + $db->setQuery($query); + $components = $db->loadObjectList(); + + foreach ($components as $component) { + $options = $this->getTypeOptionsByComponent($component->option); + + if ($options) { + $list[$component->name] = $options; + + // Create the reverse lookup for link-to-name. + foreach ($options as $option) { + if (isset($option->request)) { + $this->addReverseLookupUrl($option); + + if (isset($option->request['option'])) { + $componentLanguageFolder = JPATH_ADMINISTRATOR . '/components/' . $option->request['option']; + $lang->load($option->request['option'] . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($option->request['option'] . '.sys', $componentLanguageFolder); + } + } + } + } + } + + // Allow a system plugin to insert dynamic menu types to the list shown in menus: + Factory::getApplication()->triggerEvent('onAfterGetMenuTypeOptions', array(&$list, $this)); + + return $list; + } + + /** + * Method to create the reverse lookup for link-to-name. + * (can be used from onAfterGetMenuTypeOptions handlers) + * + * @param CMSObject $option Object with request array or string and title public variables + * + * @return void + * + * @since 3.1 + */ + public function addReverseLookupUrl($option) + { + $this->rlu[MenusHelper::getLinkKey($option->request)] = $option->get('title'); + } + + /** + * Get menu types by component. + * + * @param string $component Component URL option. + * + * @return array + * + * @since 1.6 + */ + protected function getTypeOptionsByComponent($component) + { + $options = array(); + $client = ApplicationHelper::getClientInfo($this->getState('client_id')); + $mainXML = $client->path . '/components/' . $component . '/metadata.xml'; + + if (is_file($mainXML)) { + $options = $this->getTypeOptionsFromXml($mainXML, $component); + } + + if (empty($options)) { + $options = $this->getTypeOptionsFromMvc($component); + } + + if ($client->id == 1 && empty($options)) { + $options = $this->getTypeOptionsFromManifest($component); + } + + return $options; + } + + /** + * Get the menu types from an XML file + * + * @param string $file File path + * @param string $component Component option as in URL + * + * @return array|boolean + * + * @since 1.6 + */ + protected function getTypeOptionsFromXml($file, $component) + { + $options = array(); + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($file)) { + return false; + } + + // Look for the first menu node off of the root node. + if (!$menu = $xml->xpath('menu[1]')) { + return false; + } else { + $menu = $menu[0]; + } + + // If we have no options to parse, just add the base component to the list of options. + if (!empty($menu['options']) && $menu['options'] == 'none') { + // Create the menu option for the component. + $o = new CMSObject(); + $o->title = (string) $menu['name']; + $o->description = (string) $menu['msg']; + $o->request = array('option' => $component); + + $options[] = $o; + + return $options; + } + + // Look for the first options node off of the menu node. + if (!$optionsNode = $menu->xpath('options[1]')) { + return false; + } else { + $optionsNode = $optionsNode[0]; + } + + // Make sure the options node has children. + if (!$children = $optionsNode->children()) { + return false; + } + + // Process each child as an option. + foreach ($children as $child) { + if ($child->getName() == 'option') { + // Create the menu option for the component. + $o = new CMSObject(); + $o->title = (string) $child['name']; + $o->description = (string) $child['msg']; + $o->request = array('option' => $component, (string) $optionsNode['var'] => (string) $child['value']); + + $options[] = $o; + } elseif ($child->getName() == 'default') { + // Create the menu option for the component. + $o = new CMSObject(); + $o->title = (string) $child['name']; + $o->description = (string) $child['msg']; + $o->request = array('option' => $component); + + $options[] = $o; + } + } + + return $options; + } + + /** + * Get menu types from MVC + * + * @param string $component Component option like in URLs + * + * @return array|boolean + * + * @since 1.6 + */ + protected function getTypeOptionsFromMvc($component) + { + $options = array(); + $views = array(); + + foreach ($this->getFolders($component) as $path) { + if (!is_dir($path)) { + continue; + } + + $views = array_merge($views, Folder::folders($path, '.', false, true)); + } + + foreach ($views as $viewPath) { + $view = basename($viewPath); + + // Ignore private views. + if (strpos($view, '_') !== 0) { + // Determine if a metadata file exists for the view. + $file = $viewPath . '/metadata.xml'; + + if (is_file($file)) { + // Attempt to load the xml file. + if ($xml = simplexml_load_file($file)) { + // Look for the first view node off of the root node. + if ($menu = $xml->xpath('view[1]')) { + $menu = $menu[0]; + + // If the view is hidden from the menu, discard it and move on to the next view. + if (!empty($menu['hidden']) && $menu['hidden'] == 'true') { + unset($xml); + continue; + } + + // Do we have an options node or should we process layouts? + // Look for the first options node off of the menu node. + if ($optionsNode = $menu->xpath('options[1]')) { + $optionsNode = $optionsNode[0]; + + // Make sure the options node has children. + if ($children = $optionsNode->children()) { + // Process each child as an option. + foreach ($children as $child) { + if ($child->getName() == 'option') { + // Create the menu option for the component. + $o = new CMSObject(); + $o->title = (string) $child['name']; + $o->description = (string) $child['msg']; + $o->request = array('option' => $component, 'view' => $view, (string) $optionsNode['var'] => (string) $child['value']); + + $options[] = $o; + } elseif ($child->getName() == 'default') { + // Create the menu option for the component. + $o = new CMSObject(); + $o->title = (string) $child['name']; + $o->description = (string) $child['msg']; + $o->request = array('option' => $component, 'view' => $view); + + $options[] = $o; + } + } + } + } else { + $options = array_merge($options, (array) $this->getTypeOptionsFromLayouts($component, $view)); + } + } + + unset($xml); + } + } else { + $options = array_merge($options, (array) $this->getTypeOptionsFromLayouts($component, $view)); + } + } + } + + return $options; + } + + /** + * Get menu types from Component manifest + * + * @param string $component Component option like in URLs + * + * @return array|boolean + * + * @since 3.7.0 + */ + protected function getTypeOptionsFromManifest($component) + { + // Load the component manifest + $fileName = JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml'; + + if (!is_file($fileName)) { + return false; + } + + if (!($manifest = simplexml_load_file($fileName))) { + return false; + } + + // Check for a valid XML root tag. + if ($manifest->getName() != 'extension') { + return false; + } + + $options = array(); + + // Start with the component root menu. + $rootMenu = $manifest->administration->menu; + + // If the menu item doesn't exist or is hidden do nothing. + if (!$rootMenu || in_array((string) $rootMenu['hidden'], array('true', 'hidden'))) { + return $options; + } + + // Create the root menu option. + $ro = new \stdClass(); + $ro->title = (string) trim($rootMenu); + $ro->description = ''; + $ro->request = array('option' => $component); + + // Process submenu options. + $submenu = $manifest->administration->submenu; + + if (!$submenu) { + return $options; + } + + foreach ($submenu->menu as $child) { + $attributes = $child->attributes(); + + $o = new \stdClass(); + $o->title = (string) trim($child); + $o->description = ''; + + if ((string) $attributes->link) { + parse_str((string) $attributes->link, $request); + } else { + $request = array(); + + $request['option'] = $component; + $request['act'] = (string) $attributes->act; + $request['task'] = (string) $attributes->task; + $request['controller'] = (string) $attributes->controller; + $request['view'] = (string) $attributes->view; + $request['layout'] = (string) $attributes->layout; + $request['sub'] = (string) $attributes->sub; + } + + $o->request = array_filter($request, 'strlen'); + $options[] = new CMSObject($o); + + // Do not repeat the default view link (index.php?option=com_abc). + if (count($o->request) == 1) { + $ro = null; + } + } + + if ($ro) { + $options[] = new CMSObject($ro); + } + + return $options; + } + + /** + * Get the menu types from component layouts + * + * @param string $component Component option as in URLs + * @param string $view Name of the view + * + * @return array + * + * @since 1.6 + */ + protected function getTypeOptionsFromLayouts($component, $view) + { + $options = array(); + $layouts = array(); + $layoutNames = array(); + $lang = Factory::getLanguage(); + $client = ApplicationHelper::getClientInfo($this->getState('client_id')); + + // Get the views for this component. + foreach ($this->getFolders($component) as $folder) { + $path = $folder . '/' . $view . '/tmpl'; + + if (!is_dir($path)) { + $path = $folder . '/' . $view; + } + + if (!is_dir($path)) { + continue; + } + + $layouts = array_merge($layouts, Folder::files($path, '.xml$', false, true)); + } + + // Build list of standard layout names + foreach ($layouts as $layout) { + // Ignore private layouts. + if (strpos(basename($layout), '_') === false) { + // Get the layout name. + $layoutNames[] = basename($layout, '.xml'); + } + } + + // Get the template layouts + // @todo: This should only search one template -- the current template for this item (default of specified) + $folders = Folder::folders($client->path . '/templates', '', false, true); + + // Array to hold association between template file names and templates + $templateName = array(); + + foreach ($folders as $folder) { + if (is_dir($folder . '/html/' . $component . '/' . $view)) { + $template = basename($folder); + $lang->load('tpl_' . $template . '.sys', $client->path) + || $lang->load('tpl_' . $template . '.sys', $client->path . '/templates/' . $template); + + $templateLayouts = Folder::files($folder . '/html/' . $component . '/' . $view, '.xml$', false, true); + + foreach ($templateLayouts as $layout) { + // Get the layout name. + $templateLayoutName = basename($layout, '.xml'); + + // Add to the list only if it is not a standard layout + if (array_search($templateLayoutName, $layoutNames) === false) { + $layouts[] = $layout; + + // Set template name array so we can get the right template for the layout + $templateName[$layout] = basename($folder); + } + } + } + } + + // Process the found layouts. + foreach ($layouts as $layout) { + // Ignore private layouts. + if (strpos(basename($layout), '_') === false) { + $file = $layout; + + // Get the layout name. + $layout = basename($layout, '.xml'); + + // Create the menu option for the layout. + $o = new CMSObject(); + $o->title = ucfirst($layout); + $o->description = ''; + $o->request = array('option' => $component, 'view' => $view); + + // Only add the layout request argument if not the default layout. + if ($layout != 'default') { + // If the template is set, add in format template:layout so we save the template name + $o->request['layout'] = isset($templateName[$file]) ? $templateName[$file] . ':' . $layout : $layout; + } + + // Load layout metadata if it exists. + if (is_file($file)) { + // Attempt to load the xml file. + if ($xml = simplexml_load_file($file)) { + // Look for the first view node off of the root node. + if ($menu = $xml->xpath('layout[1]')) { + $menu = $menu[0]; + + // If the view is hidden from the menu, discard it and move on to the next view. + if (!empty($menu['hidden']) && $menu['hidden'] == 'true') { + unset($xml); + unset($o); + continue; + } + + // Populate the title and description if they exist. + if (!empty($menu['title'])) { + $o->title = trim((string) $menu['title']); + } + + if (!empty($menu->message[0])) { + $o->description = trim((string) $menu->message[0]); + } + } + } + } + + // Add the layout to the options array. + $options[] = $o; + } + } + + return $options; + } + + /** + * Get the folders with template files for the given component. + * + * @param string $component Component option as in URLs + * + * @return array + * + * @since 4.0.0 + */ + private function getFolders($component) + { + $client = ApplicationHelper::getClientInfo($this->getState('client_id')); + + if (!is_dir($client->path . '/components/' . $component)) { + return array(); + } + + $folders = Folder::folders($client->path . '/components/' . $component, '^view[s]?$', false, true); + $folders = array_merge($folders, Folder::folders($client->path . '/components/' . $component, '^tmpl?$', false, true)); + + if (!$folders) { + return array(); + } + + return $folders; + } } diff --git a/administrator/components/com_menus/src/Service/HTML/Menus.php b/administrator/components/com_menus/src/Service/HTML/Menus.php index 4fac4b103af72..fbb603e18e694 100644 --- a/administrator/components/com_menus/src/Service/HTML/Menus.php +++ b/administrator/components/com_menus/src/Service/HTML/Menus.php @@ -1,4 +1,5 @@ getQuery(true) - ->select( - [ - $db->quoteName('m.id'), - $db->quoteName('m.title'), - $db->quoteName('l.sef', 'lang_sef'), - $db->quoteName('l.lang_code'), - $db->quoteName('mt.title', 'menu_title'), - $db->quoteName('l.image'), - $db->quoteName('l.title', 'language_title'), - ] - ) - ->from($db->quoteName('#__menu', 'm')) - ->join('LEFT', $db->quoteName('#__menu_types', 'mt'), $db->quoteName('mt.menutype') . ' = ' . $db->quoteName('m.menutype')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('m.language') . ' = ' . $db->quoteName('l.lang_code')) - ->whereIn($db->quoteName('m.id'), array_values($associations)) - ->where($db->quoteName('m.id') . ' != :itemid') - ->bind(':itemid', $itemid, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $items = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500); - } - - // Construct html - if ($items) - { - $languages = LanguageHelper::getContentLanguages(array(0, 1)); - $content_languages = array_column($languages, 'lang_code'); - - foreach ($items as &$item) - { - if (in_array($item->lang_code, $content_languages)) - { - $text = $item->lang_code; - $url = Route::_('index.php?option=com_menus&task=item.edit&id=' . (int) $item->id); - $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
    ' - . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
    ' . Text::sprintf('COM_MENUS_MENU_SPRINTF', $item->menu_title); - $classes = 'badge bg-secondary'; - - $item->link = '' . $text . '' - . ''; - } - else - { - // Display warning if Content Language is trashed or deleted - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); - } - } - } - - $html = LayoutHelper::render('joomla.content.associations', $items); - } - - return $html; - } - - /** - * Returns a visibility state on a grid - * - * @param integer $params Params of item. - * - * @return string The Html code - * - * @since 3.7.0 - */ - public function visibility($params) - { - $registry = new Registry; - - try - { - $registry->loadString($params); - } - catch (\Exception $e) - { - // Invalid JSON - } - - $show_menu = $registry->get('menu_show'); - - return ($show_menu === 0) ? '' . Text::_('COM_MENUS_LABEL_HIDDEN') . '' : ''; - } + /** + * Generate the markup to display the item associations + * + * @param int $itemid The menu item id + * + * @return string + * + * @since 3.0 + * + * @throws \Exception If there is an error on the query + */ + public function association($itemid) + { + // Defaults + $html = ''; + + // Get the associations + if ($associations = MenusHelper::getAssociations($itemid)) { + // Get the associated menu items + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('m.id'), + $db->quoteName('m.title'), + $db->quoteName('l.sef', 'lang_sef'), + $db->quoteName('l.lang_code'), + $db->quoteName('mt.title', 'menu_title'), + $db->quoteName('l.image'), + $db->quoteName('l.title', 'language_title'), + ] + ) + ->from($db->quoteName('#__menu', 'm')) + ->join('LEFT', $db->quoteName('#__menu_types', 'mt'), $db->quoteName('mt.menutype') . ' = ' . $db->quoteName('m.menutype')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('m.language') . ' = ' . $db->quoteName('l.lang_code')) + ->whereIn($db->quoteName('m.id'), array_values($associations)) + ->where($db->quoteName('m.id') . ' != :itemid') + ->bind(':itemid', $itemid, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $items = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500); + } + + // Construct html + if ($items) { + $languages = LanguageHelper::getContentLanguages(array(0, 1)); + $content_languages = array_column($languages, 'lang_code'); + + foreach ($items as &$item) { + if (in_array($item->lang_code, $content_languages)) { + $text = $item->lang_code; + $url = Route::_('index.php?option=com_menus&task=item.edit&id=' . (int) $item->id); + $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
    ' + . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
    ' . Text::sprintf('COM_MENUS_MENU_SPRINTF', $item->menu_title); + $classes = 'badge bg-secondary'; + + $item->link = '' . $text . '' + . ''; + } else { + // Display warning if Content Language is trashed or deleted + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); + } + } + } + + $html = LayoutHelper::render('joomla.content.associations', $items); + } + + return $html; + } + + /** + * Returns a visibility state on a grid + * + * @param integer $params Params of item. + * + * @return string The Html code + * + * @since 3.7.0 + */ + public function visibility($params) + { + $registry = new Registry(); + + try { + $registry->loadString($params); + } catch (\Exception $e) { + // Invalid JSON + } + + $show_menu = $registry->get('menu_show'); + + return ($show_menu === 0) ? '' . Text::_('COM_MENUS_LABEL_HIDDEN') . '' : ''; + } } diff --git a/administrator/components/com_menus/src/Table/MenuTable.php b/administrator/components/com_menus/src/Table/MenuTable.php index 33a60da57b6ff..1ce56140e6728 100644 --- a/administrator/components/com_menus/src/Table/MenuTable.php +++ b/administrator/components/com_menus/src/Table/MenuTable.php @@ -1,4 +1,5 @@ getDbo(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__modules_menu')) - ->where($db->quoteName('menuid') . ' = :pk') - ->bind(':pk', $pk, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - } + if ($return) { + // Delete key from the #__modules_menu table + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__modules_menu')) + ->where($db->quoteName('menuid') . ' = :pk') + ->bind(':pk', $pk, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + } - return $return; - } + return $return; + } - /** - * Overloaded check function - * - * @return boolean True on success, false on failure - * - * @see JTable::check - * @since 4.0.0 - */ - public function check() - { - $return = parent::check(); + /** + * Overloaded check function + * + * @return boolean True on success, false on failure + * + * @see JTable::check + * @since 4.0.0 + */ + public function check() + { + $return = parent::check(); - if ($return) - { - // Set publish_up to null date if not set - if (!$this->publish_up) - { - $this->publish_up = null; - } + if ($return) { + // Set publish_up to null date if not set + if (!$this->publish_up) { + $this->publish_up = null; + } - // Set publish_down to null date if not set - if (!$this->publish_down) - { - $this->publish_down = null; - } + // Set publish_down to null date if not set + if (!$this->publish_down) { + $this->publish_down = null; + } - // Check the publish down date is not earlier than publish up. - if (!is_null($this->publish_down) && !is_null($this->publish_up) && $this->publish_down < $this->publish_up) - { - $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); + // Check the publish down date is not earlier than publish up. + if (!is_null($this->publish_down) && !is_null($this->publish_up) && $this->publish_down < $this->publish_up) { + $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); - return false; - } + return false; + } - if ((int) $this->home) - { - // Set the publish down/up always for home. - $this->publish_up = null; - $this->publish_down = null; - } - } + if ((int) $this->home) { + // Set the publish down/up always for home. + $this->publish_up = null; + $this->publish_down = null; + } + } - return $return; - } + return $return; + } } diff --git a/administrator/components/com_menus/src/Table/MenuTypeTable.php b/administrator/components/com_menus/src/Table/MenuTypeTable.php index 108656290abae..5d6aa3b98b671 100644 --- a/administrator/components/com_menus/src/Table/MenuTypeTable.php +++ b/administrator/components/com_menus/src/Table/MenuTypeTable.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->modules = $this->get('Modules'); - $this->levels = $this->get('ViewLevels'); - $this->canDo = ContentHelper::getActions('com_menus', 'menu', (int) $this->state->get('item.menutypeid')); - - // Check if we're allowed to edit this item - // No need to check for create, because then the moduletype select is empty - if (!empty($this->item->id) && !$this->canDo->get('core.edit')) - { - throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // If we are forcing a language in modal (used for associations). - if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) - { - // Set the language field to the forcedLanguage and disable changing it. - $this->form->setValue('language', null, $forcedLanguage); - $this->form->setFieldAttribute('language', 'readonly', 'true'); - - // Only allow to select categories with All language or with the forced language. - $this->form->setFieldAttribute('parent_id', 'language', '*,' . $forcedLanguage); - } - - parent::display($tpl); - $this->addToolbar(); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $input = Factory::getApplication()->input; - $input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); - $canDo = $this->canDo; - $clientId = $this->state->get('item.client_id', 0); - - ToolbarHelper::title(Text::_($isNew ? 'COM_MENUS_VIEW_NEW_ITEM_TITLE' : 'COM_MENUS_VIEW_EDIT_ITEM_TITLE'), 'list menu-add'); - - $toolbarButtons = []; - - // If a new item, can save the item. Allow users with edit permissions to apply changes to prevent returning to grid. - if ($isNew && $canDo->get('core.create')) - { - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('item.apply'); - } - - $toolbarButtons[] = ['save', 'item.save']; - } - - // If not checked out, can save the item. - if (!$isNew && !$checkedOut && $canDo->get('core.edit')) - { - ToolbarHelper::apply('item.apply'); - - $toolbarButtons[] = ['save', 'item.save']; - } - - // If the user can create new items, allow them to see Save & New - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'item.save2new']; - } - - // If an existing item, can save to a copy only if we have create rights. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'item.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (!$isNew && Associations::isEnabled() && ComponentHelper::isEnabled('com_associations') && $clientId != 1) - { - ToolbarHelper::custom('item.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); - } - - if ($isNew) - { - ToolbarHelper::cancel('item.cancel'); - } - else - { - ToolbarHelper::cancel('item.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - - // Get the help information for the menu item. - $lang = Factory::getLanguage(); - - $help = $this->get('Help'); - - if ($lang->hasKey($help->url)) - { - $debug = $lang->setDebug(false); - $url = Text::_($help->url); - $lang->setDebug($debug); - } - else - { - $url = $help->url; - } - - ToolbarHelper::help($help->key, $help->local, $url); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var CMSObject + */ + protected $item; + + /** + * @var mixed + */ + protected $modules; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * @since 3.7.0 + */ + protected $canDo; + + /** + * A list of view levels containing the id and title of the view level + * + * @var \stdClass[] + * @since 4.0.0 + */ + protected $levels; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->modules = $this->get('Modules'); + $this->levels = $this->get('ViewLevels'); + $this->canDo = ContentHelper::getActions('com_menus', 'menu', (int) $this->state->get('item.menutypeid')); + + // Check if we're allowed to edit this item + // No need to check for create, because then the moduletype select is empty + if (!empty($this->item->id) && !$this->canDo->get('core.edit')) { + throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // If we are forcing a language in modal (used for associations). + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) { + // Set the language field to the forcedLanguage and disable changing it. + $this->form->setValue('language', null, $forcedLanguage); + $this->form->setFieldAttribute('language', 'readonly', 'true'); + + // Only allow to select categories with All language or with the forced language. + $this->form->setFieldAttribute('parent_id', 'language', '*,' . $forcedLanguage); + } + + parent::display($tpl); + $this->addToolbar(); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $input = Factory::getApplication()->input; + $input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); + $canDo = $this->canDo; + $clientId = $this->state->get('item.client_id', 0); + + ToolbarHelper::title(Text::_($isNew ? 'COM_MENUS_VIEW_NEW_ITEM_TITLE' : 'COM_MENUS_VIEW_EDIT_ITEM_TITLE'), 'list menu-add'); + + $toolbarButtons = []; + + // If a new item, can save the item. Allow users with edit permissions to apply changes to prevent returning to grid. + if ($isNew && $canDo->get('core.create')) { + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('item.apply'); + } + + $toolbarButtons[] = ['save', 'item.save']; + } + + // If not checked out, can save the item. + if (!$isNew && !$checkedOut && $canDo->get('core.edit')) { + ToolbarHelper::apply('item.apply'); + + $toolbarButtons[] = ['save', 'item.save']; + } + + // If the user can create new items, allow them to see Save & New + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'item.save2new']; + } + + // If an existing item, can save to a copy only if we have create rights. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'item.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (!$isNew && Associations::isEnabled() && ComponentHelper::isEnabled('com_associations') && $clientId != 1) { + ToolbarHelper::custom('item.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); + } + + if ($isNew) { + ToolbarHelper::cancel('item.cancel'); + } else { + ToolbarHelper::cancel('item.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + + // Get the help information for the menu item. + $lang = Factory::getLanguage(); + + $help = $this->get('Help'); + + if ($lang->hasKey($help->url)) { + $debug = $lang->setDebug(false); + $url = Text::_($help->url); + $lang->setDebug($debug); + } else { + $url = $help->url; + } + + ToolbarHelper::help($help->key, $help->local, $url); + } } diff --git a/administrator/components/com_menus/src/View/Items/HtmlView.php b/administrator/components/com_menus/src/View/Items/HtmlView.php index fe1c86bdd9521..c8131d5077a3e 100644 --- a/administrator/components/com_menus/src/View/Items/HtmlView.php +++ b/administrator/components/com_menus/src/View/Items/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->total = $this->get('Total'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->ordering = array(); - - // Preprocess the list of items to find ordering divisions. - foreach ($this->items as $item) - { - $this->ordering[$item->parent_id][] = $item->id; - - // Item type text - switch ($item->type) - { - case 'url': - $value = Text::_('COM_MENUS_TYPE_EXTERNAL_URL'); - break; - - case 'alias': - $value = Text::_('COM_MENUS_TYPE_ALIAS'); - break; - - case 'separator': - $value = Text::_('COM_MENUS_TYPE_SEPARATOR'); - break; - - case 'heading': - $value = Text::_('COM_MENUS_TYPE_HEADING'); - break; - - case 'container': - $value = Text::_('COM_MENUS_TYPE_CONTAINER'); - break; - - case 'component': - default: - // Load language - $lang->load($item->componentname . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($item->componentname . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->componentname); - - if (!empty($item->componentname)) - { - $titleParts = array(); - $titleParts[] = Text::_($item->componentname); - $vars = null; - - parse_str($item->link, $vars); - - if (isset($vars['view'])) - { - // Attempt to load the view xml file. - $file = JPATH_SITE . '/components/' . $item->componentname . '/views/' . $vars['view'] . '/metadata.xml'; - - if (!is_file($file)) - { - $file = JPATH_SITE . '/components/' . $item->componentname . '/view/' . $vars['view'] . '/metadata.xml'; - } - - if (is_file($file) && $xml = simplexml_load_file($file)) - { - // Look for the first view node off of the root node. - if ($view = $xml->xpath('view[1]')) - { - // Add view title if present. - if (!empty($view[0]['title'])) - { - $viewTitle = trim((string) $view[0]['title']); - - // Check if the key is valid. Needed due to B/C so we don't show untranslated keys. This check should be removed with Joomla 4. - if ($lang->hasKey($viewTitle)) - { - $titleParts[] = Text::_($viewTitle); - } - } - } - } - - $vars['layout'] = $vars['layout'] ?? 'default'; - - // Attempt to load the layout xml file. - // If Alternative Menu Item, get template folder for layout file - if (strpos($vars['layout'], ':') > 0) - { - // Use template folder for layout file - $temp = explode(':', $vars['layout']); - $file = JPATH_SITE . '/templates/' . $temp[0] . '/html/' . $item->componentname . '/' . $vars['view'] . '/' . $temp[1] . '.xml'; - - // Load template language file - $lang->load('tpl_' . $temp[0] . '.sys', JPATH_SITE) - || $lang->load('tpl_' . $temp[0] . '.sys', JPATH_SITE . '/templates/' . $temp[0]); - } - else - { - $base = $this->state->get('filter.client_id') == 0 ? JPATH_SITE : JPATH_ADMINISTRATOR; - - // Get XML file from component folder for standard layouts - $file = $base . '/components/' . $item->componentname . '/tmpl/' . $vars['view'] - . '/' . $vars['layout'] . '.xml'; - - if (!file_exists($file)) - { - $file = $base . '/components/' . $item->componentname . '/views/' - . $vars['view'] . '/tmpl/' . $vars['layout'] . '.xml'; - - if (!file_exists($file)) - { - $file = $base . '/components/' . $item->componentname . '/view/' - . $vars['view'] . '/tmpl/' . $vars['layout'] . '.xml'; - } - } - } - - if (is_file($file) && $xml = simplexml_load_file($file)) - { - // Look for the first view node off of the root node. - if ($layout = $xml->xpath('layout[1]')) - { - if (!empty($layout[0]['title'])) - { - $titleParts[] = Text::_(trim((string) $layout[0]['title'])); - } - } - - if (!empty($layout[0]->message[0])) - { - $item->item_type_desc = Text::_(trim((string) $layout[0]->message[0])); - } - } - - unset($xml); - - // Special case if neither a view nor layout title is found - if (count($titleParts) == 1) - { - $titleParts[] = $vars['view']; - } - } - - $value = implode(' » ', $titleParts); - } - else - { - if (preg_match("/^index.php\?option=([a-zA-Z\-0-9_]*)/", $item->link, $result)) - { - $value = Text::sprintf('COM_MENUS_TYPE_UNEXISTING', $result[1]); - } - else - { - $value = Text::_('COM_MENUS_TYPE_UNKNOWN'); - } - } - break; - } - - $item->item_type = $value; - $item->protected = $item->menutype == 'main'; - } - - // Levels filter. - $options = array(); - $options[] = HTMLHelper::_('select.option', '1', Text::_('J1')); - $options[] = HTMLHelper::_('select.option', '2', Text::_('J2')); - $options[] = HTMLHelper::_('select.option', '3', Text::_('J3')); - $options[] = HTMLHelper::_('select.option', '4', Text::_('J4')); - $options[] = HTMLHelper::_('select.option', '5', Text::_('J5')); - $options[] = HTMLHelper::_('select.option', '6', Text::_('J6')); - $options[] = HTMLHelper::_('select.option', '7', Text::_('J7')); - $options[] = HTMLHelper::_('select.option', '8', Text::_('J8')); - $options[] = HTMLHelper::_('select.option', '9', Text::_('J9')); - $options[] = HTMLHelper::_('select.option', '10', Text::_('J10')); - - $this->f_levels = $options; - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - else - { - // In menu associations modal we need to remove language filter if forcing a language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. - $languageXml = new \SimpleXMLElement(''); - $this->filterForm->setField($languageXml, 'filter', true); - - // Also, unset the active language filter so the search tools is not open by default with this filter. - unset($this->activeFilters['language']); - } - } - - // Allow a system plugin to insert dynamic menu types to the list shown in menus: - Factory::getApplication()->triggerEvent('onBeforeRenderMenuItems', array($this)); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $menutypeId = (int) $this->state->get('menutypeid'); - - $canDo = ContentHelper::getActions('com_menus', 'menu', (int) $menutypeId); - $user = $this->getCurrentUser(); - - // Get the menu title - $menuTypeTitle = $this->get('State')->get('menutypetitle'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($menuTypeTitle) - { - ToolbarHelper::title(Text::sprintf('COM_MENUS_VIEW_ITEMS_MENU_TITLE', $menuTypeTitle), 'list menumgr'); - } - else - { - ToolbarHelper::title(Text::_('COM_MENUS_VIEW_ITEMS_ALL_TITLE'), 'list menumgr'); - } - - if ($canDo->get('core.create')) - { - $toolbar->addNew('item.add'); - } - - $protected = $this->state->get('filter.menutype') == 'main'; - - if (($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin')) && !$protected - || $canDo->get('core.edit.state') && $this->state->get('filter.client_id') == 0) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state') && !$protected) - { - $childBar->publish('items.publish')->listCheck(true); - - $childBar->unpublish('items.unpublish')->listCheck(true); - } - - if ($this->getCurrentUser()->authorise('core.admin') && !$protected) - { - $childBar->checkin('items.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) - { - if ($this->state->get('filter.client_id') == 0) - { - $childBar->makeDefault('items.setDefault')->listCheck(true); - } - - if (!$protected) - { - $childBar->trash('items.trash')->listCheck(true); - } - } - - // Add a batch button - if (!$protected && $user->authorise('core.create', 'com_menus') - && $user->authorise('core.edit', 'com_menus') - && $user->authorise('core.edit.state', 'com_menus')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if ($this->getCurrentUser()->authorise('core.admin')) - { - $toolbar->standardButton('refresh') - ->text('JTOOLBAR_REBUILD') - ->task('items.rebuild'); - } - - if (!$protected && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('items.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_menus'); - } - - $toolbar->help('Menus:_Items'); - } + /** + * Array used for displaying the levels filter + * + * @var \stdClass[] + * @since 4.0.0 + */ + protected $f_levels; + + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Ordering of the items + * + * @var array + * @since 4.0.0 + */ + protected $ordering; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $lang = Factory::getLanguage(); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->total = $this->get('Total'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->ordering = array(); + + // Preprocess the list of items to find ordering divisions. + foreach ($this->items as $item) { + $this->ordering[$item->parent_id][] = $item->id; + + // Item type text + switch ($item->type) { + case 'url': + $value = Text::_('COM_MENUS_TYPE_EXTERNAL_URL'); + break; + + case 'alias': + $value = Text::_('COM_MENUS_TYPE_ALIAS'); + break; + + case 'separator': + $value = Text::_('COM_MENUS_TYPE_SEPARATOR'); + break; + + case 'heading': + $value = Text::_('COM_MENUS_TYPE_HEADING'); + break; + + case 'container': + $value = Text::_('COM_MENUS_TYPE_CONTAINER'); + break; + + case 'component': + default: + // Load language + $lang->load($item->componentname . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($item->componentname . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->componentname); + + if (!empty($item->componentname)) { + $titleParts = array(); + $titleParts[] = Text::_($item->componentname); + $vars = null; + + parse_str($item->link, $vars); + + if (isset($vars['view'])) { + // Attempt to load the view xml file. + $file = JPATH_SITE . '/components/' . $item->componentname . '/views/' . $vars['view'] . '/metadata.xml'; + + if (!is_file($file)) { + $file = JPATH_SITE . '/components/' . $item->componentname . '/view/' . $vars['view'] . '/metadata.xml'; + } + + if (is_file($file) && $xml = simplexml_load_file($file)) { + // Look for the first view node off of the root node. + if ($view = $xml->xpath('view[1]')) { + // Add view title if present. + if (!empty($view[0]['title'])) { + $viewTitle = trim((string) $view[0]['title']); + + // Check if the key is valid. Needed due to B/C so we don't show untranslated keys. This check should be removed with Joomla 4. + if ($lang->hasKey($viewTitle)) { + $titleParts[] = Text::_($viewTitle); + } + } + } + } + + $vars['layout'] = $vars['layout'] ?? 'default'; + + // Attempt to load the layout xml file. + // If Alternative Menu Item, get template folder for layout file + if (strpos($vars['layout'], ':') > 0) { + // Use template folder for layout file + $temp = explode(':', $vars['layout']); + $file = JPATH_SITE . '/templates/' . $temp[0] . '/html/' . $item->componentname . '/' . $vars['view'] . '/' . $temp[1] . '.xml'; + + // Load template language file + $lang->load('tpl_' . $temp[0] . '.sys', JPATH_SITE) + || $lang->load('tpl_' . $temp[0] . '.sys', JPATH_SITE . '/templates/' . $temp[0]); + } else { + $base = $this->state->get('filter.client_id') == 0 ? JPATH_SITE : JPATH_ADMINISTRATOR; + + // Get XML file from component folder for standard layouts + $file = $base . '/components/' . $item->componentname . '/tmpl/' . $vars['view'] + . '/' . $vars['layout'] . '.xml'; + + if (!file_exists($file)) { + $file = $base . '/components/' . $item->componentname . '/views/' + . $vars['view'] . '/tmpl/' . $vars['layout'] . '.xml'; + + if (!file_exists($file)) { + $file = $base . '/components/' . $item->componentname . '/view/' + . $vars['view'] . '/tmpl/' . $vars['layout'] . '.xml'; + } + } + } + + if (is_file($file) && $xml = simplexml_load_file($file)) { + // Look for the first view node off of the root node. + if ($layout = $xml->xpath('layout[1]')) { + if (!empty($layout[0]['title'])) { + $titleParts[] = Text::_(trim((string) $layout[0]['title'])); + } + } + + if (!empty($layout[0]->message[0])) { + $item->item_type_desc = Text::_(trim((string) $layout[0]->message[0])); + } + } + + unset($xml); + + // Special case if neither a view nor layout title is found + if (count($titleParts) == 1) { + $titleParts[] = $vars['view']; + } + } + + $value = implode(' » ', $titleParts); + } else { + if (preg_match("/^index.php\?option=([a-zA-Z\-0-9_]*)/", $item->link, $result)) { + $value = Text::sprintf('COM_MENUS_TYPE_UNEXISTING', $result[1]); + } else { + $value = Text::_('COM_MENUS_TYPE_UNKNOWN'); + } + } + break; + } + + $item->item_type = $value; + $item->protected = $item->menutype == 'main'; + } + + // Levels filter. + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', Text::_('J1')); + $options[] = HTMLHelper::_('select.option', '2', Text::_('J2')); + $options[] = HTMLHelper::_('select.option', '3', Text::_('J3')); + $options[] = HTMLHelper::_('select.option', '4', Text::_('J4')); + $options[] = HTMLHelper::_('select.option', '5', Text::_('J5')); + $options[] = HTMLHelper::_('select.option', '6', Text::_('J6')); + $options[] = HTMLHelper::_('select.option', '7', Text::_('J7')); + $options[] = HTMLHelper::_('select.option', '8', Text::_('J8')); + $options[] = HTMLHelper::_('select.option', '9', Text::_('J9')); + $options[] = HTMLHelper::_('select.option', '10', Text::_('J10')); + + $this->f_levels = $options; + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // In menu associations modal we need to remove language filter if forcing a language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. + $languageXml = new \SimpleXMLElement(''); + $this->filterForm->setField($languageXml, 'filter', true); + + // Also, unset the active language filter so the search tools is not open by default with this filter. + unset($this->activeFilters['language']); + } + } + + // Allow a system plugin to insert dynamic menu types to the list shown in menus: + Factory::getApplication()->triggerEvent('onBeforeRenderMenuItems', array($this)); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $menutypeId = (int) $this->state->get('menutypeid'); + + $canDo = ContentHelper::getActions('com_menus', 'menu', (int) $menutypeId); + $user = $this->getCurrentUser(); + + // Get the menu title + $menuTypeTitle = $this->get('State')->get('menutypetitle'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($menuTypeTitle) { + ToolbarHelper::title(Text::sprintf('COM_MENUS_VIEW_ITEMS_MENU_TITLE', $menuTypeTitle), 'list menumgr'); + } else { + ToolbarHelper::title(Text::_('COM_MENUS_VIEW_ITEMS_ALL_TITLE'), 'list menumgr'); + } + + if ($canDo->get('core.create')) { + $toolbar->addNew('item.add'); + } + + $protected = $this->state->get('filter.menutype') == 'main'; + + if ( + ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin')) && !$protected + || $canDo->get('core.edit.state') && $this->state->get('filter.client_id') == 0 + ) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state') && !$protected) { + $childBar->publish('items.publish')->listCheck(true); + + $childBar->unpublish('items.unpublish')->listCheck(true); + } + + if ($this->getCurrentUser()->authorise('core.admin') && !$protected) { + $childBar->checkin('items.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) { + if ($this->state->get('filter.client_id') == 0) { + $childBar->makeDefault('items.setDefault')->listCheck(true); + } + + if (!$protected) { + $childBar->trash('items.trash')->listCheck(true); + } + } + + // Add a batch button + if ( + !$protected && $user->authorise('core.create', 'com_menus') + && $user->authorise('core.edit', 'com_menus') + && $user->authorise('core.edit.state', 'com_menus') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if ($this->getCurrentUser()->authorise('core.admin')) { + $toolbar->standardButton('refresh') + ->text('JTOOLBAR_REBUILD') + ->task('items.rebuild'); + } + + if (!$protected && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('items.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_menus'); + } + + $toolbar->help('Menus:_Items'); + } } diff --git a/administrator/components/com_menus/src/View/Menu/HtmlView.php b/administrator/components/com_menus/src/View/Menu/HtmlView.php index 4ceeff9dd6030..2d5af16a3ae62 100644 --- a/administrator/components/com_menus/src/View/Menu/HtmlView.php +++ b/administrator/components/com_menus/src/View/Menu/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - $this->canDo = ContentHelper::getActions('com_menus', 'menu', $this->item->id); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - parent::display($tpl); - $this->addToolbar(); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $input = Factory::getApplication()->input; - $input->set('hidemainmenu', true); - - $isNew = ($this->item->id == 0); - - ToolbarHelper::title(Text::_($isNew ? 'COM_MENUS_VIEW_NEW_MENU_TITLE' : 'COM_MENUS_VIEW_EDIT_MENU_TITLE'), 'list menu'); - - $toolbarButtons = []; - - // If a new item, can save the item. Allow users with edit permissions to apply changes to prevent returning to grid. - if ($isNew && $this->canDo->get('core.create')) - { - if ($this->canDo->get('core.edit')) - { - ToolbarHelper::apply('menu.apply'); - } - - $toolbarButtons[] = ['save', 'menu.save']; - } - - // If user can edit, can save the item. - if (!$isNew && $this->canDo->get('core.edit')) - { - ToolbarHelper::apply('menu.apply'); - - $toolbarButtons[] = ['save', 'menu.save']; - } - - // If the user can create new items, allow them to see Save & New - if ($this->canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'menu.save2new']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if ($isNew) - { - ToolbarHelper::cancel('menu.cancel'); - } - else - { - ToolbarHelper::cancel('menu.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Menus:_Edit'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + */ + protected $canDo; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + $this->canDo = ContentHelper::getActions('com_menus', 'menu', $this->item->id); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + parent::display($tpl); + $this->addToolbar(); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $input = Factory::getApplication()->input; + $input->set('hidemainmenu', true); + + $isNew = ($this->item->id == 0); + + ToolbarHelper::title(Text::_($isNew ? 'COM_MENUS_VIEW_NEW_MENU_TITLE' : 'COM_MENUS_VIEW_EDIT_MENU_TITLE'), 'list menu'); + + $toolbarButtons = []; + + // If a new item, can save the item. Allow users with edit permissions to apply changes to prevent returning to grid. + if ($isNew && $this->canDo->get('core.create')) { + if ($this->canDo->get('core.edit')) { + ToolbarHelper::apply('menu.apply'); + } + + $toolbarButtons[] = ['save', 'menu.save']; + } + + // If user can edit, can save the item. + if (!$isNew && $this->canDo->get('core.edit')) { + ToolbarHelper::apply('menu.apply'); + + $toolbarButtons[] = ['save', 'menu.save']; + } + + // If the user can create new items, allow them to see Save & New + if ($this->canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'menu.save2new']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if ($isNew) { + ToolbarHelper::cancel('menu.cancel'); + } else { + ToolbarHelper::cancel('menu.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Menus:_Edit'); + } } diff --git a/administrator/components/com_menus/src/View/Menu/XmlView.php b/administrator/components/com_menus/src/View/Menu/XmlView.php index c10e4a5a56909..58557d6c39891 100644 --- a/administrator/components/com_menus/src/View/Menu/XmlView.php +++ b/administrator/components/com_menus/src/View/Menu/XmlView.php @@ -1,4 +1,5 @@ input->getCmd('menutype'); - - if ($menutype) - { - $root = MenusHelper::getMenuItems($menutype, true); - } - - if (!$root->hasChildren()) - { - Log::add(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), Log::WARNING, 'jerror'); - - $app->redirect(Route::_('index.php?option=com_menus&view=menus', false)); - - return; - } - - $this->items = $root->getChildren(true); - - $xml = new \SimpleXMLElement('' - ); - - foreach ($this->items as $item) - { - $this->addXmlChild($xml, $item); - } - - if (headers_sent($file, $line)) - { - Log::add("Headers already sent at $file:$line.", Log::ERROR, 'jerror'); - - return; - } - - header('content-type: application/xml'); - header('content-disposition: attachment; filename="' . $menutype . '.xml"'); - header("Cache-Control: no-cache, must-revalidate"); - header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); - - $dom = new \DOMDocument; - $dom->preserveWhiteSpace = true; - $dom->formatOutput = true; - $dom->loadXML($xml->asXML()); - - echo $dom->saveXML(); - - $app->close(); - } - - /** - * Add a child node to the xml - * - * @param \SimpleXMLElement $xml The current XML node which would become the parent to the new node - * @param \stdClass $item The menuitem object to create the child XML node from - * - * @return void - * - * @since 3.8.0 - */ - protected function addXmlChild($xml, $item) - { - $node = $xml->addChild('menuitem'); - - $node['type'] = $item->type; - - if ($item->title) - { - $node['title'] = htmlentities($item->title, ENT_XML1); - } - - if ($item->link) - { - $node['link'] = $item->link; - } - - if ($item->element) - { - $node['element'] = $item->element; - } - - if (isset($item->class) && $item->class) - { - $node['class'] = htmlentities($item->class, ENT_XML1); - } - - if ($item->access) - { - $node['access'] = $item->access; - } - - if ($item->browserNav) - { - $node['target'] = '_blank'; - } - - if ($item->getParams() && $hideitems = $item->getParams()->get('hideitems')) - { - $item->getParams()->set('hideitems', $this->getModel('Menu')->getExtensionElementsForMenuItems($hideitems)); - - $node->addChild('params', htmlentities((string) $item->getParams(), ENT_XML1)); - } - - if (isset($item->submenu)) - { - foreach ($item->submenu as $sub) - { - $this->addXmlChild($node, $sub); - } - } - } + /** + * @var \stdClass[] + * + * @since 3.8.0 + */ + protected $items; + + /** + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.8.0 + */ + protected $state; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.8.0 + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $menutype = $app->input->getCmd('menutype'); + + if ($menutype) { + $root = MenusHelper::getMenuItems($menutype, true); + } + + if (!$root->hasChildren()) { + Log::add(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), Log::WARNING, 'jerror'); + + $app->redirect(Route::_('index.php?option=com_menus&view=menus', false)); + + return; + } + + $this->items = $root->getChildren(true); + + $xml = new \SimpleXMLElement(''); + + foreach ($this->items as $item) { + $this->addXmlChild($xml, $item); + } + + if (headers_sent($file, $line)) { + Log::add("Headers already sent at $file:$line.", Log::ERROR, 'jerror'); + + return; + } + + header('content-type: application/xml'); + header('content-disposition: attachment; filename="' . $menutype . '.xml"'); + header("Cache-Control: no-cache, must-revalidate"); + header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); + + $dom = new \DOMDocument(); + $dom->preserveWhiteSpace = true; + $dom->formatOutput = true; + $dom->loadXML($xml->asXML()); + + echo $dom->saveXML(); + + $app->close(); + } + + /** + * Add a child node to the xml + * + * @param \SimpleXMLElement $xml The current XML node which would become the parent to the new node + * @param \stdClass $item The menuitem object to create the child XML node from + * + * @return void + * + * @since 3.8.0 + */ + protected function addXmlChild($xml, $item) + { + $node = $xml->addChild('menuitem'); + + $node['type'] = $item->type; + + if ($item->title) { + $node['title'] = htmlentities($item->title, ENT_XML1); + } + + if ($item->link) { + $node['link'] = $item->link; + } + + if ($item->element) { + $node['element'] = $item->element; + } + + if (isset($item->class) && $item->class) { + $node['class'] = htmlentities($item->class, ENT_XML1); + } + + if ($item->access) { + $node['access'] = $item->access; + } + + if ($item->browserNav) { + $node['target'] = '_blank'; + } + + if ($item->getParams() && $hideitems = $item->getParams()->get('hideitems')) { + $item->getParams()->set('hideitems', $this->getModel('Menu')->getExtensionElementsForMenuItems($hideitems)); + + $node->addChild('params', htmlentities((string) $item->getParams(), ENT_XML1)); + } + + if (isset($item->submenu)) { + foreach ($item->submenu as $sub) { + $this->addXmlChild($node, $sub); + } + } + } } diff --git a/administrator/components/com_menus/src/View/Menus/HtmlView.php b/administrator/components/com_menus/src/View/Menus/HtmlView.php index e3ca25e2e3a13..598238e5298ba 100644 --- a/administrator/components/com_menus/src/View/Menus/HtmlView.php +++ b/administrator/components/com_menus/src/View/Menus/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->modules = $this->get('Modules'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - - if ($this->getLayout() == 'default') - { - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_menus'); - - ToolbarHelper::title(Text::_('COM_MENUS_VIEW_MENUS_TITLE'), 'list menumgr'); - - if ($canDo->get('core.create')) - { - ToolbarHelper::addNew('menu.add'); - } - - if ($canDo->get('core.delete')) - { - ToolbarHelper::divider(); - ToolbarHelper::deleteList('COM_MENUS_MENU_CONFIRM_DELETE', 'menus.delete', 'JTOOLBAR_DELETE'); - } - - if ($canDo->get('core.admin') && $this->state->get('client_id') == 1) - { - ToolbarHelper::custom('menu.exportXml', 'download', '', 'COM_MENUS_MENU_EXPORT_BUTTON', true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::divider(); - ToolbarHelper::preferences('com_menus'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Menus'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * List of all mod_mainmenu modules collated by menutype + * + * @var array + */ + protected $modules; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->modules = $this->get('Modules'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + + if ($this->getLayout() == 'default') { + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_menus'); + + ToolbarHelper::title(Text::_('COM_MENUS_VIEW_MENUS_TITLE'), 'list menumgr'); + + if ($canDo->get('core.create')) { + ToolbarHelper::addNew('menu.add'); + } + + if ($canDo->get('core.delete')) { + ToolbarHelper::divider(); + ToolbarHelper::deleteList('COM_MENUS_MENU_CONFIRM_DELETE', 'menus.delete', 'JTOOLBAR_DELETE'); + } + + if ($canDo->get('core.admin') && $this->state->get('client_id') == 1) { + ToolbarHelper::custom('menu.exportXml', 'download', '', 'COM_MENUS_MENU_EXPORT_BUTTON', true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::divider(); + ToolbarHelper::preferences('com_menus'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Menus'); + } } diff --git a/administrator/components/com_menus/src/View/Menutypes/HtmlView.php b/administrator/components/com_menus/src/View/Menutypes/HtmlView.php index 5844c40e6fb82..219ca4c4354c6 100644 --- a/administrator/components/com_menus/src/View/Menutypes/HtmlView.php +++ b/administrator/components/com_menus/src/View/Menutypes/HtmlView.php @@ -1,4 +1,5 @@ recordId = $app->input->getInt('recordId'); - - $types = $this->get('TypeOptions'); - - $this->addCustomTypes($types); - - $sortedTypes = array(); - - foreach ($types as $name => $list) - { - $tmp = array(); - - foreach ($list as $item) - { - $tmp[Text::_($item->title)] = $item; - } - - uksort($tmp, 'strcasecmp'); - $sortedTypes[Text::_($name)] = $tmp; - } - - uksort($sortedTypes, 'strcasecmp'); - - $this->types = $sortedTypes; - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.0 - */ - protected function addToolbar() - { - // Add page title - ToolbarHelper::title(Text::_('COM_MENUS'), 'list menumgr'); - - // Get the toolbar object instance - $bar = Toolbar::getInstance('toolbar'); - - // Cancel - $title = Text::_('JTOOLBAR_CANCEL'); - $dhtml = ""; - $bar->appendButton('Custom', $dhtml, 'new'); - } - - /** - * Method to add system link types to the link types array - * - * @param array $types The list of link types - * - * @return void - * - * @since 3.7.0 - */ - protected function addCustomTypes(&$types) - { - if (empty($types)) - { - $types = array(); - } - - // Adding System Links - $list = array(); - $o = new CMSObject; - $o->title = 'COM_MENUS_TYPE_EXTERNAL_URL'; - $o->type = 'url'; - $o->description = 'COM_MENUS_TYPE_EXTERNAL_URL_DESC'; - $o->request = null; - $list[] = $o; - - $o = new CMSObject; - $o->title = 'COM_MENUS_TYPE_ALIAS'; - $o->type = 'alias'; - $o->description = 'COM_MENUS_TYPE_ALIAS_DESC'; - $o->request = null; - $list[] = $o; - - $o = new CMSObject; - $o->title = 'COM_MENUS_TYPE_SEPARATOR'; - $o->type = 'separator'; - $o->description = 'COM_MENUS_TYPE_SEPARATOR_DESC'; - $o->request = null; - $list[] = $o; - - $o = new CMSObject; - $o->title = 'COM_MENUS_TYPE_HEADING'; - $o->type = 'heading'; - $o->description = 'COM_MENUS_TYPE_HEADING_DESC'; - $o->request = null; - $list[] = $o; - - if ($this->get('state')->get('client_id') == 1) - { - $o = new CMSObject; - $o->title = 'COM_MENUS_TYPE_CONTAINER'; - $o->type = 'container'; - $o->description = 'COM_MENUS_TYPE_CONTAINER_DESC'; - $o->request = null; - $list[] = $o; - } - - $types['COM_MENUS_TYPE_SYSTEM'] = $list; - } + $bar->appendButton('Custom', $dhtml, 'new'); + } + + /** + * Method to add system link types to the link types array + * + * @param array $types The list of link types + * + * @return void + * + * @since 3.7.0 + */ + protected function addCustomTypes(&$types) + { + if (empty($types)) { + $types = array(); + } + + // Adding System Links + $list = array(); + $o = new CMSObject(); + $o->title = 'COM_MENUS_TYPE_EXTERNAL_URL'; + $o->type = 'url'; + $o->description = 'COM_MENUS_TYPE_EXTERNAL_URL_DESC'; + $o->request = null; + $list[] = $o; + + $o = new CMSObject(); + $o->title = 'COM_MENUS_TYPE_ALIAS'; + $o->type = 'alias'; + $o->description = 'COM_MENUS_TYPE_ALIAS_DESC'; + $o->request = null; + $list[] = $o; + + $o = new CMSObject(); + $o->title = 'COM_MENUS_TYPE_SEPARATOR'; + $o->type = 'separator'; + $o->description = 'COM_MENUS_TYPE_SEPARATOR_DESC'; + $o->request = null; + $list[] = $o; + + $o = new CMSObject(); + $o->title = 'COM_MENUS_TYPE_HEADING'; + $o->type = 'heading'; + $o->description = 'COM_MENUS_TYPE_HEADING_DESC'; + $o->request = null; + $list[] = $o; + + if ($this->get('state')->get('client_id') == 1) { + $o = new CMSObject(); + $o->title = 'COM_MENUS_TYPE_CONTAINER'; + $o->type = 'container'; + $o->description = 'COM_MENUS_TYPE_CONTAINER_DESC'; + $o->request = null; + $list[] = $o; + } + + $types['COM_MENUS_TYPE_SYSTEM'] = $list; + } } diff --git a/administrator/components/com_menus/tmpl/item/edit.php b/administrator/components/com_menus/tmpl/item/edit.php index dc9f5991e1774..bd89bd47306ef 100644 --- a/administrator/components/com_menus/tmpl/item/edit.php +++ b/administrator/components/com_menus/tmpl/item/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_menus.admin-item-edit'); + ->useScript('form.validate') + ->useScript('com_menus.admin-item-edit'); $assoc = Associations::isEnabled(); $input = Factory::getApplication()->input; @@ -40,166 +41,158 @@ $lang = Factory::getLanguage()->getTag(); // Load mod_menu.ini file when client is administrator -if ($clientId === 1) -{ - Factory::getLanguage()->load('mod_menu', JPATH_ADMINISTRATOR); +if ($clientId === 1) { + Factory::getLanguage()->load('mod_menu', JPATH_ADMINISTRATOR); } ?> - - - - item->id != 0) : ?> -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - - -
    - - 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - -
    -
    - form->renderField('type'); - - if ($this->item->type == 'alias') - { - echo $this->form->renderField('aliasoptions', 'params'); - } - - if ($this->item->type == 'separator') - { - echo $this->form->renderField('text_separator', 'params'); - } - - echo $this->form->renderFieldset('request'); - - if ($this->item->type == 'url') - { - $this->form->setFieldAttribute('link', 'readonly', 'false'); - $this->form->setFieldAttribute('link', 'required', 'true'); - } - - echo $this->form->renderField('link'); - - if ($this->item->type == 'alias') - { - echo $this->form->renderField('alias_redirect', 'params'); - } - - echo $this->form->renderField('browserNav'); - echo $this->form->renderField('template_style_id'); - - if (!$isModal && $this->item->type == 'container') - { - echo $this->loadTemplate('container'); - } - ?> -
    -
    - fields = array( - 'id', - 'client_id', - 'menutype', - 'parent_id', - 'menuordering', - 'published', - 'publish_up', - 'publish_down', - 'home', - 'access', - 'language', - 'note', - ); - - if ($this->item->type != 'component') - { - $this->fields = array_diff($this->fields, array('home')); - $this->form->setFieldAttribute('publish_up', 'showon', ''); - $this->form->setFieldAttribute('publish_down', 'showon', ''); - } - ?> - fields = array( - 'id', - 'client_id', - 'menutype', - 'parent_id', - 'menuordering', - 'published', - 'home', - 'publish_up', - 'publish_down', - 'access', - 'language', - 'note', - ); - - if ($this->item->type != 'component') - { - $this->fields = array_diff($this->fields, array('home')); - $this->form->setFieldAttribute('publish_up', 'showon', ''); - $this->form->setFieldAttribute('publish_down', 'showon', ''); - } - - echo LayoutHelper::render('joomla.edit.global', $this); ?> -
    -
    - - - fieldsets = array(); - $this->ignore_fieldsets = array('aliasoptions', 'request', 'item_associations'); - echo LayoutHelper::render('joomla.edit.params', $this); - ?> - - state->get('item.client_id') != 1) : ?> - -
    - -
    - -
    -
    - - - - - - modules)) : ?> - -
    - -
    - loadTemplate('modules'); ?> -
    -
    - - - - -
    - - - - - form->getInput('component_id'); ?> - - + + + + item->id != 0) : ?> +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + + +
    + + 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + +
    +
    + form->renderField('type'); + + if ($this->item->type == 'alias') { + echo $this->form->renderField('aliasoptions', 'params'); + } + + if ($this->item->type == 'separator') { + echo $this->form->renderField('text_separator', 'params'); + } + + echo $this->form->renderFieldset('request'); + + if ($this->item->type == 'url') { + $this->form->setFieldAttribute('link', 'readonly', 'false'); + $this->form->setFieldAttribute('link', 'required', 'true'); + } + + echo $this->form->renderField('link'); + + if ($this->item->type == 'alias') { + echo $this->form->renderField('alias_redirect', 'params'); + } + + echo $this->form->renderField('browserNav'); + echo $this->form->renderField('template_style_id'); + + if (!$isModal && $this->item->type == 'container') { + echo $this->loadTemplate('container'); + } + ?> +
    +
    + fields = array( + 'id', + 'client_id', + 'menutype', + 'parent_id', + 'menuordering', + 'published', + 'publish_up', + 'publish_down', + 'home', + 'access', + 'language', + 'note', + ); + + if ($this->item->type != 'component') { + $this->fields = array_diff($this->fields, array('home')); + $this->form->setFieldAttribute('publish_up', 'showon', ''); + $this->form->setFieldAttribute('publish_down', 'showon', ''); + } + ?> + fields = array( + 'id', + 'client_id', + 'menutype', + 'parent_id', + 'menuordering', + 'published', + 'home', + 'publish_up', + 'publish_down', + 'access', + 'language', + 'note', + ); + + if ($this->item->type != 'component') { + $this->fields = array_diff($this->fields, array('home')); + $this->form->setFieldAttribute('publish_up', 'showon', ''); + $this->form->setFieldAttribute('publish_down', 'showon', ''); + } + + echo LayoutHelper::render('joomla.edit.global', $this); ?> +
    +
    + + + fieldsets = array(); + $this->ignore_fieldsets = array('aliasoptions', 'request', 'item_associations'); + echo LayoutHelper::render('joomla.edit.params', $this); + ?> + + state->get('item.client_id') != 1) : ?> + +
    + +
    + +
    +
    + + + + + + modules)) : ?> + +
    + +
    + loadTemplate('modules'); ?> +
    +
    + + + + +
    + + + + + form->getInput('component_id'); ?> + + diff --git a/administrator/components/com_menus/tmpl/item/edit_container.php b/administrator/components/com_menus/tmpl/item/edit_container.php index 09d6ef94bec7f..53f7d07060971 100644 --- a/administrator/components/com_menus/tmpl/item/edit_container.php +++ b/administrator/components/com_menus/tmpl/item/edit_container.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; @@ -18,92 +20,86 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('joomla.treeselectmenu') - ->useStyle('com_menus.admin-item-edit-container') - ->useScript('com_menus.admin-item-edit-container'); + ->useStyle('com_menus.admin-item-edit-container') + ->useScript('com_menus.admin-item-edit-container'); ?> diff --git a/administrator/components/com_menus/tmpl/item/edit_modules.php b/administrator/components/com_menus/tmpl/item/edit_modules.php index 2088aa583a49b..d8f7ae83481a8 100644 --- a/administrator/components/com_menus/tmpl/item/edit_modules.php +++ b/administrator/components/com_menus/tmpl/item/edit_modules.php @@ -1,4 +1,5 @@ levels as $key => $value) -{ - $allLevels[$value->id] = $value->title; +foreach ($this->levels as $key => $value) { + $allLevels[$value->id] = $value->title; } $this->document->addScriptOptions('menus-edit-modules', ['viewLevels' => $allLevels, 'itemId' => $this->item->id]); @@ -23,26 +23,26 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useStyle('com_menus.admin-item-edit-modules') - ->useScript('com_menus.admin-item-edit-modules'); + ->useScript('com_menus.admin-item-edit-modules'); // Set up the bootstrap modal that will be used for all module editors echo HTMLHelper::_( - 'bootstrap.renderModal', - 'moduleEditModal', - array( - 'title' => Text::_('COM_MENUS_EDIT_MODULE_SETTINGS'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'footer' => '' - . '' - . '', - ) + 'bootstrap.renderModal', + 'moduleEditModal', + array( + 'title' => Text::_('COM_MENUS_EDIT_MODULE_SETTINGS'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'footer' => '' + . '' + . '', + ) ); ?> @@ -53,97 +53,97 @@ echo LayoutHelper::render('joomla.menu.edit_modules', $this); ?> - - - - - - - - - - - - modules as $i => &$module) : ?> - menuid)) : ?> - except || $module->menuid < 0) : ?> - - - - - - - - published) : ?> - - - - - - - - - - - - - + + + + + + + + + + + + modules as $i => &$module) : ?> + menuid)) : ?> + except || $module->menuid < 0) : ?> + + + + + + + + published) : ?> + + + + + + + + + + + + +
    - -
    - - - - - - - - - -
    - - - escape($module->access_title); ?> - - escape($module->position); ?> - - published) : ?> - - - - - - - - -
    + +
    + + + + + + + + + +
    + + + escape($module->access_title); ?> + + escape($module->position); ?> + + published) : ?> + + + + + + + + +
    diff --git a/administrator/components/com_menus/tmpl/item/modal.php b/administrator/components/com_menus/tmpl/item/modal.php index 2a62e7aa4c3a1..f5c2253fc4208 100644 --- a/administrator/components/com_menus/tmpl/item/modal.php +++ b/administrator/components/com_menus/tmpl/item/modal.php @@ -1,4 +1,5 @@
    - setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
    diff --git a/administrator/components/com_menus/tmpl/items/default.php b/administrator/components/com_menus/tmpl/items/default.php index 7b8a79ba495f3..33900dd7c4346 100644 --- a/administrator/components/com_menus/tmpl/items/default.php +++ b/administrator/components/com_menus/tmpl/items/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $app = Factory::getApplication(); @@ -32,10 +33,9 @@ $saveOrder = ($listOrder == 'a.lft' && strtolower($listDirn) == 'asc'); $menuType = (string) $app->getUserState('com_menus.items.menutype', ''); -if ($saveOrder && $menuType && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_menus&task=items.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && $menuType && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_menus&task=items.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $assoc = Associations::isEnabled() && $this->state->get('filter.client_id') == 0; @@ -43,248 +43,241 @@ ?>
    -
    -
    -
    - $this, 'options' => array('selectorFieldName' => 'menutype'))); ?> - items)) : ?> - - - - - - - - - - - - state->get('filter.client_id') == 0) : ?> - - - state->get('filter.client_id') == 0) : ?> - - - - - - state->get('filter.client_id') == 0) && (Multilanguage::isEnabled())) : ?> - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="false"> - items as $i => $item) : - $orderkey = array_search($item->id, $this->ordering[$item->parent_id]); - $canCreate = $user->authorise('core.create', 'com_menus.menu.' . $item->menutype_id); - $canEdit = $user->authorise('core.edit', 'com_menus.menu.' . $item->menutype_id); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_menus.menu.' . $item->menutype_id) && $canCheckin; + id="adminForm"> +
    +
    +
    + $this, 'options' => array('selectorFieldName' => 'menutype'))); ?> + items)) : ?> +
    + + + + + + + + + + + state->get('filter.client_id') == 0) : ?> + + + state->get('filter.client_id') == 0) : ?> + + + + + + state->get('filter.client_id') == 0) && (Multilanguage::isEnabled())) : ?> + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="false"> + items as $i => $item) : + $orderkey = array_search($item->id, $this->ordering[$item->parent_id]); + $canCreate = $user->authorise('core.create', 'com_menus.menu.' . $item->menutype_id); + $canEdit = $user->authorise('core.edit', 'com_menus.menu.' . $item->menutype_id); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_menus.menu.' . $item->menutype_id) && $canCheckin; - // Get the parents of item for sorting - if ($item->level > 1) - { - $parentsStr = ''; - $_currentParentId = $item->parent_id; - $parentsStr = ' ' . $_currentParentId; + // Get the parents of item for sorting + if ($item->level > 1) { + $parentsStr = ''; + $_currentParentId = $item->parent_id; + $parentsStr = ' ' . $_currentParentId; - for ($j = 0; $j < $item->level; $j++) - { - foreach ($this->ordering as $k => $v) - { - $v = implode('-', $v); - $v = '-' . $v . '-'; + for ($j = 0; $j < $item->level; $j++) { + foreach ($this->ordering as $k => $v) { + $v = implode('-', $v); + $v = '-' . $v . '-'; - if (strpos($v, '-' . $_currentParentId . '-') !== false) - { - $parentsStr .= ' ' . $k; - $_currentParentId = $k; - break; - } - } - } - } - else - { - $parentsStr = ''; - } - ?> - - - - + + + - - - - - state->get('filter.client_id') == 0) : ?> - - - state->get('filter.client_id') == 0) : ?> - - - - - - state->get('filter.client_id') == 0 && Multilanguage::isEnabled()) : ?> - - - - - - - + if (!$canChange) { + $iconClass = ' inactive'; + } elseif (!$saveOrder) { + $iconClass = ' inactive" title="' . Text::_('JORDERINGDISABLED'); + } + ?> + + + + + + + + + + published, $i, 'items.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + + + $item->level)); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'items.', $canCheckin); ?> + + protected) : ?> + + escape($item->title); ?> + + escape($item->title); ?> + + params); ?> +
    + + + type != 'url') : ?> + note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + + type == 'url' && $item->note) : ?> + escape($item->note)); ?> + + +
    +
    + + + escape($item->item_type); ?> + +
    + type === 'component' && !$item->enabled) : ?> +
    + + enabled === null ? 'JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND' : 'COM_MENUS_LABEL_DISABLED'); ?> + +
    + + + + escape($item->menutype_title ?: ucwords($item->menutype)); ?> + + state->get('filter.client_id') == 0) : ?> + + type == 'component') : ?> + language == '*' || $item->home == '0') : ?> + home, $i, 'items.', ($item->language != '*' || !$item->home) && $canChange && !$item->protected, 'cb', null, 'icon-home', 'icon-circle'); ?> + + + language_image) : ?> + language_image . '.gif', $item->language_title, array('title' => Text::sprintf('COM_MENUS_GRID_UNSET_LANGUAGE', $item->language_title)), true); ?> + + language; ?> + + + + language_image) : ?> + language_image . '.gif', $item->language_title, array('title' => $item->language_title), true); ?> + + language; ?> + + + + + + state->get('filter.client_id') == 0) : ?> + + escape($item->access_level); ?> + + + + + association) : ?> + id); ?> + + + + state->get('filter.client_id') == 0 && Multilanguage::isEnabled()) : ?> + + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_menus') || $user->authorise('core.edit', 'com_menus')) : ?> - Text::_('COM_MENUS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer') - ), - $this->loadTemplate('batch_body') - ); ?> - - + + authorise('core.create', 'com_menus') || $user->authorise('core.edit', 'com_menus')) : ?> + Text::_('COM_MENUS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer') + ), + $this->loadTemplate('batch_body') + ); ?> + + - - - -
    -
    -
    + + + + + +
    diff --git a/administrator/components/com_menus/tmpl/items/default_batch_body.php b/administrator/components/com_menus/tmpl/items/default_batch_body.php index acb8b8e97b71f..8a0c950eab285 100644 --- a/administrator/components/com_menus/tmpl/items/default_batch_body.php +++ b/administrator/components/com_menus/tmpl/items/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -15,73 +17,72 @@ use Joomla\CMS\Layout\LayoutHelper; $options = [ - HTMLHelper::_('select.option', 'c', Text::_('JLIB_HTML_BATCH_COPY')), - HTMLHelper::_('select.option', 'm', Text::_('JLIB_HTML_BATCH_MOVE')) + HTMLHelper::_('select.option', 'c', Text::_('JLIB_HTML_BATCH_COPY')), + HTMLHelper::_('select.option', 'm', Text::_('JLIB_HTML_BATCH_MOVE')) ]; $published = (int) $this->state->get('filter.published'); $clientId = (int) $this->state->get('filter.client_id'); $menuType = Factory::getApplication()->getUserState('com_menus.items.menutype', ''); -if ($clientId == 1) -{ - /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = $this->document->getWebAssetManager(); - $wa->useScript('com_menus.batch-body'); - $wa->useScript('joomla.batch-copymove'); +if ($clientId == 1) { + /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = $this->document->getWebAssetManager(); + $wa->useScript('com_menus.batch-body'); + $wa->useScript('joomla.batch-copymove'); } ?>
    - - -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    - -
    - = 0) : ?> -
    -
    - - -
    + + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + +
    + = 0) : ?> +
    +
    + + +
    -
    - - -
    -
    - +
    + + +
    +
    + - -

    - -
    - -
    -

    -
    - + +

    + +
    + +
    +

    +
    +
    diff --git a/administrator/components/com_menus/tmpl/items/default_batch_footer.php b/administrator/components/com_menus/tmpl/items/default_batch_footer.php index 6ce0e9df6c672..4f2df096275cf 100644 --- a/administrator/components/com_menus/tmpl/items/default_batch_footer.php +++ b/administrator/components/com_menus/tmpl/items/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -16,10 +18,10 @@ $menuType = Factory::getApplication()->getUserState('com_menus.items.menutype', ''); ?> -= 0 && $clientId == 1)): ?> - += 0 && $clientId == 1)) : ?> + diff --git a/administrator/components/com_menus/tmpl/items/modal.php b/administrator/components/com_menus/tmpl/items/modal.php index f4c6afef18278..e592075877504 100644 --- a/administrator/components/com_menus/tmpl/items/modal.php +++ b/administrator/components/com_menus/tmpl/items/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if ($app->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ @@ -35,162 +35,155 @@ $link = 'index.php?option=com_menus&view=items&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; $multilang = Multilanguage::isEnabled(); -if (!empty($editor)) -{ - // This view is used also in com_menus. Load the xtd script only if the editor is set! - $this->document->addScriptOptions('xtd-menus', array('editor' => $editor)); - $onclick = "jSelectMenuItem"; - $link = 'index.php?option=com_menus&view=items&layout=modal&tmpl=component&editor=' . $editor . '&' . Session::getFormToken() . '=1'; +if (!empty($editor)) { + // This view is used also in com_menus. Load the xtd script only if the editor is set! + $this->document->addScriptOptions('xtd-menus', array('editor' => $editor)); + $onclick = "jSelectMenuItem"; + $link = 'index.php?option=com_menus&view=items&layout=modal&tmpl=component&editor=' . $editor . '&' . Session::getFormToken() . '=1'; } ?>
    -
    - $this, 'options' => array('selectorFieldName' => 'menutype'))); ?> + + $this, 'options' => array('selectorFieldName' => 'menutype'))); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - items as $i => $item) : ?> - type, array('separator', 'heading', 'alias', 'url', 'container')); ?> - language && $multilang) - { - if ($item->language !== '*') - { - $language = $item->language; - } - else - { - $language = ''; - } - } - elseif (!$multilang) - { - $language = ''; - } - ?> - - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - -
    - published, $i, 'items.', false, 'cb', $item->publish_up, $item->publish_down); ?> - - $item->level)); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - - params); ?> -
    - - - note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - - -
    -
    - - - escape($item->item_type); ?> - -
    - type === 'component' && !$item->enabled) : ?> -
    - - enabled === null ? 'JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND' : 'COM_MENUS_LABEL_DISABLED'); ?> - -
    - -
    - escape($item->menutype_title); ?> - - type == 'component') : ?> - language == '*' || $item->home == '0') : ?> - home, $i, 'items.', ($item->language != '*' || !$item->home) && false && !$item->protected, 'cb', null, 'home', 'circle'); ?> - - language_image) : ?> - language_image . '.gif', $item->language_title, array('title' => $item->language_title), true); ?> - - language; ?> - - - - - escape($item->access_level); ?> - - language == '') : ?> - - language == '*') : ?> - - - - - - id; ?> -
    + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + + items as $i => $item) : ?> + type, array('separator', 'heading', 'alias', 'url', 'container')); ?> + language && $multilang) { + if ($item->language !== '*') { + $language = $item->language; + } else { + $language = ''; + } + } elseif (!$multilang) { + $language = ''; + } + ?> + + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + +
    + published, $i, 'items.', false, 'cb', $item->publish_up, $item->publish_down); ?> + + $item->level)); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + + params); ?> +
    + + + note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + + +
    +
    + + + escape($item->item_type); ?> + +
    + type === 'component' && !$item->enabled) : ?> +
    + + enabled === null ? 'JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND' : 'COM_MENUS_LABEL_DISABLED'); ?> + +
    + +
    + escape($item->menutype_title); ?> + + type == 'component') : ?> + language == '*' || $item->home == '0') : ?> + home, $i, 'items.', ($item->language != '*' || !$item->home) && false && !$item->protected, 'cb', null, 'home', 'circle'); ?> + + language_image) : ?> + language_image . '.gif', $item->language_title, array('title' => $item->language_title), true); ?> + + language; ?> + + + + + escape($item->access_level); ?> + + language == '') : ?> + + language == '*') : ?> + + + + + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - - + + + + + -
    +
    diff --git a/administrator/components/com_menus/tmpl/menu/edit.php b/administrator/components/com_menus/tmpl/menu/edit.php index 7959fda2742d5..186b659dce12b 100644 --- a/administrator/components/com_menus/tmpl/menu/edit.php +++ b/administrator/components/com_menus/tmpl/menu/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('keepalive') + ->useScript('form.validate'); Text::script('ERROR'); ?>
    - + -
    - 'details', 'recall' => true, 'breakpoint' => 768]); ?> +
    + 'details', 'recall' => true, 'breakpoint' => 768]); ?> - + -
    - +
    + -
    -
    - form->renderField('menutype'); +
    +
    + form->renderField('menutype'); - echo $this->form->renderField('description'); + echo $this->form->renderField('description'); - echo $this->form->renderField('client_id'); + echo $this->form->renderField('client_id'); - echo $this->form->renderField('preset'); - ?> -
    -
    -
    + echo $this->form->renderField('preset'); + ?> +
    +
    + - + - canDo->get('core.admin')) : ?> - -
    - -
    - form->getInput('rules'); ?> -
    -
    - - + canDo->get('core.admin')) : ?> + +
    + +
    + form->getInput('rules'); ?> +
    +
    + + - - - - + + + +
    diff --git a/administrator/components/com_menus/tmpl/menus/default.php b/administrator/components/com_menus/tmpl/menus/default.php index 29d4da506d5da..09cc238824ea0 100644 --- a/administrator/components/com_menus/tmpl/menus/default.php +++ b/administrator/components/com_menus/tmpl/menus/default.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -18,8 +20,8 @@ /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect') - ->useScript('com_menus.admin-menus'); + ->useScript('multiselect') + ->useScript('com_menus.admin-menus'); $uri = Uri::getInstance(); $return = base64_encode($uri); @@ -29,242 +31,240 @@ $modMenuId = (int) $this->get('ModMenuId'); $itemIds = []; -foreach ($this->items as $item) -{ - if ($user->authorise('core.edit', 'com_menus')) - { - $itemIds[] = $item->id; - } +foreach ($this->items as $item) { + if ($user->authorise('core.edit', 'com_menus')) { + $itemIds[] = $item->id; + } } $this->document->addScriptOptions('menus-default', ['items' => $itemIds]); ?>
    -
    -
    -
    - $this, 'options' => array('filterButton' => false))); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - items as $i => $item) : - $canEdit = $user->authorise('core.edit', 'com_menus.menu.' . (int) $item->id); - $canManageItems = $user->authorise('core.manage', 'com_menus.menu.' . (int) $item->id); - ?> - - - - - - - - - - - - - +
    +
    +
    + $this, 'options' => array('filterButton' => false))); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + items as $i => $item) : + $canEdit = $user->authorise('core.edit', 'com_menus.menu.' . (int) $item->id); + $canManageItems = $user->authorise('core.manage', 'com_menus.menu.' . (int) $item->id); + ?> + + + + + + + + + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
    -
    -
    + + + +
    +
    +
    diff --git a/administrator/components/com_menus/tmpl/menutypes/default.php b/administrator/components/com_menus/tmpl/menutypes/default.php index 9a5c7efeae1b1..a0a32a9d6f658 100644 --- a/administrator/components/com_menus/tmpl/menutypes/default.php +++ b/administrator/components/com_menus/tmpl/menutypes/default.php @@ -1,4 +1,5 @@ 'slide1')); ?> - - types as $name => $list) : ?> - -
    - $item) : ?> - $this->recordId, 'title' => $item->type ?? $item->title, 'request' => $item->request); ?> - - -
    - -
    - - description); ?> - -
    - -
    - - + + types as $name => $list) : ?> + +
    + $item) : ?> + $this->recordId, 'title' => $item->type ?? $item->title, 'request' => $item->request); ?> + + +
    + +
    + + description); ?> + +
    + +
    + + registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Messages')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Messages')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Messages')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Messages')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MessagesComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MessagesComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_messages/src/Controller/ConfigController.php b/administrator/components/com_messages/src/Controller/ConfigController.php index cf76dd1ddb86d..54802c98eac9f 100644 --- a/administrator/components/com_messages/src/Controller/ConfigController.php +++ b/administrator/components/com_messages/src/Controller/ConfigController.php @@ -1,4 +1,5 @@ checkToken(); - - $model = $this->getModel('Config'); - $data = $this->input->post->get('jform', array(), 'array'); - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - $data = $model->validate($form, $data); - - // Check for validation errors. - if ($data === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Redirect back to the main list. - $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); - - return false; - } - - // Attempt to save the data. - if (!$model->save($data)) - { - // Redirect back to the main list. - $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED', $model->getError()), 'warning'); - $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); - - return false; - } - - // Redirect to the list screen. - $this->setMessage(Text::_('COM_MESSAGES_CONFIG_SAVED')); - $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); - - return true; - } - - /** - * Cancel operation. - * - * @return void - * - * @since 4.0.0 - */ - public function cancel() - { - $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); - } + /** + * Method to save a record. + * + * @return boolean + * + * @since 1.6 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + $model = $this->getModel('Config'); + $data = $this->input->post->get('jform', array(), 'array'); + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + $data = $model->validate($form, $data); + + // Check for validation errors. + if ($data === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Redirect back to the main list. + $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); + + return false; + } + + // Attempt to save the data. + if (!$model->save($data)) { + // Redirect back to the main list. + $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED', $model->getError()), 'warning'); + $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); + + return false; + } + + // Redirect to the list screen. + $this->setMessage(Text::_('COM_MESSAGES_CONFIG_SAVED')); + $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); + + return true; + } + + /** + * Cancel operation. + * + * @return void + * + * @since 4.0.0 + */ + public function cancel() + { + $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); + } } diff --git a/administrator/components/com_messages/src/Controller/DisplayController.php b/administrator/components/com_messages/src/Controller/DisplayController.php index f7115d296aa79..b928a1bf22293 100644 --- a/administrator/components/com_messages/src/Controller/DisplayController.php +++ b/administrator/components/com_messages/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'messages'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached. + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', 'messages'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($view == 'message' && $layout == 'edit' && !$this->checkEditId('com_messages.edit.message', $id)) - { - // 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', $id), 'error'); - } + // Check for edit form. + if ($view == 'message' && $layout == 'edit' && !$this->checkEditId('com_messages.edit.message', $id)) { + // 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', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); + $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/administrator/components/com_messages/src/Controller/MessageController.php b/administrator/components/com_messages/src/Controller/MessageController.php index 44b8361751cf1..c65f60c465566 100644 --- a/administrator/components/com_messages/src/Controller/MessageController.php +++ b/administrator/components/com_messages/src/Controller/MessageController.php @@ -1,4 +1,5 @@ input->getInt('reply_id')) - { - $this->setRedirect('index.php?option=com_messages&view=message&layout=edit&reply_id=' . $replyId); - } - else - { - $this->setMessage(Text::_('COM_MESSAGES_INVALID_REPLY_ID')); - $this->setRedirect('index.php?option=com_messages&view=messages'); - } - } + /** + * Reply to an existing message. + * + * This is a simple redirect to the compose form. + * + * @return void + * + * @since 1.6 + */ + public function reply() + { + if ($replyId = $this->input->getInt('reply_id')) { + $this->setRedirect('index.php?option=com_messages&view=message&layout=edit&reply_id=' . $replyId); + } else { + $this->setMessage(Text::_('COM_MESSAGES_INVALID_REPLY_ID')); + $this->setRedirect('index.php?option=com_messages&view=messages'); + } + } } diff --git a/administrator/components/com_messages/src/Controller/MessagesController.php b/administrator/components/com_messages/src/Controller/MessagesController.php index 0d160b86d827f..4a550031e2087 100644 --- a/administrator/components/com_messages/src/Controller/MessagesController.php +++ b/administrator/components/com_messages/src/Controller/MessagesController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Message', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_messages/src/Extension/MessagesComponent.php b/administrator/components/com_messages/src/Extension/MessagesComponent.php index ab09ea87e936d..eecae1cddbc2d 100644 --- a/administrator/components/com_messages/src/Extension/MessagesComponent.php +++ b/administrator/components/com_messages/src/Extension/MessagesComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('messages', new Messages); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('messages', new Messages()); + } } diff --git a/administrator/components/com_messages/src/Field/MessageStatesField.php b/administrator/components/com_messages/src/Field/MessageStatesField.php index 5e5dbfdc15fc6..1c521faa0d31e 100644 --- a/administrator/components/com_messages/src/Field/MessageStatesField.php +++ b/administrator/components/com_messages/src/Field/MessageStatesField.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select('id') - ->from('#__usergroups'); - $db->setQuery($query); + /** + * Method to get the filtering groups (null means no filtering) + * + * @return array|null array of filtering groups or null. + * + * @since 1.6 + */ + protected function getGroups() + { + // Compute usergroups + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('id') + ->from('#__usergroups'); + $db->setQuery($query); - try - { - $groups = $db->loadColumn(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'notice'); + try { + $groups = $db->loadColumn(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'notice'); - return null; - } + return null; + } - foreach ($groups as $i => $group) - { - if (Access::checkGroup($group, 'core.admin')) - { - continue; - } + foreach ($groups as $i => $group) { + if (Access::checkGroup($group, 'core.admin')) { + continue; + } - if (!Access::checkGroup($group, 'core.manage', 'com_messages')) - { - unset($groups[$i]); - continue; - } + if (!Access::checkGroup($group, 'core.manage', 'com_messages')) { + unset($groups[$i]); + continue; + } - if (!Access::checkGroup($group, 'core.login.admin')) - { - unset($groups[$i]); - } - } + if (!Access::checkGroup($group, 'core.login.admin')) { + unset($groups[$i]); + } + } - return array_values($groups); - } + return array_values($groups); + } - /** - * Method to get the users to exclude from the list of users - * - * @return array|null array of users to exclude or null to to not exclude them - * - * @since 1.6 - */ - protected function getExcluded() - { - return array(Factory::getUser()->id); - } + /** + * Method to get the users to exclude from the list of users + * + * @return array|null array of users to exclude or null to to not exclude them + * + * @since 1.6 + */ + protected function getExcluded() + { + return array(Factory::getUser()->id); + } } diff --git a/administrator/components/com_messages/src/Helper/MessagesHelper.php b/administrator/components/com_messages/src/Helper/MessagesHelper.php index 28598ba737f4b..039e811150bc1 100644 --- a/administrator/components/com_messages/src/Helper/MessagesHelper.php +++ b/administrator/components/com_messages/src/Helper/MessagesHelper.php @@ -1,4 +1,5 @@ setState('user.id', $user->get('id')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_messages'); - $this->setState('params', $params); - } - - /** - * Method to get a single record. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function &getItem() - { - $item = new CMSObject; - $userid = (int) $this->getState('user.id'); - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('cfg_name'), - $db->quoteName('cfg_value'), - ] - ) - ->from($db->quoteName('#__messages_cfg')) - ->where($db->quoteName('user_id') . ' = :userid') - ->bind(':userid', $userid, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - foreach ($rows as $row) - { - $item->set($row->cfg_name, $row->cfg_value); - } - - $this->preprocessData('com_messages.config', $item); - - return $item; - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return \Joomla\CMS\Form\Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_messages.config', 'config', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $db = $this->getDatabase(); - - if ($userId = (int) $this->getState('user.id')) - { - $query = $db->getQuery(true) - ->delete($db->quoteName('#__messages_cfg')) - ->where($db->quoteName('user_id') . ' = :userid') - ->bind(':userid', $userId, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (count($data)) - { - $query = $db->getQuery(true) - ->insert($db->quoteName('#__messages_cfg')) - ->columns( - [ - $db->quoteName('user_id'), - $db->quoteName('cfg_name'), - $db->quoteName('cfg_value'), - ] - ); - - foreach ($data as $k => $v) - { - $query->values( - implode( - ',', - $query->bindArray( - [$userId , $k, $v], - [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] - ) - ) - ); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - return true; - } - else - { - $this->setError('COM_MESSAGES_ERR_INVALID_USER'); - - return false; - } - } + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $user = Factory::getUser(); + + $this->setState('user.id', $user->get('id')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_messages'); + $this->setState('params', $params); + } + + /** + * Method to get a single record. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function &getItem() + { + $item = new CMSObject(); + $userid = (int) $this->getState('user.id'); + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('cfg_name'), + $db->quoteName('cfg_value'), + ] + ) + ->from($db->quoteName('#__messages_cfg')) + ->where($db->quoteName('user_id') . ' = :userid') + ->bind(':userid', $userid, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + foreach ($rows as $row) { + $item->set($row->cfg_name, $row->cfg_value); + } + + $this->preprocessData('com_messages.config', $item); + + return $item; + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_messages.config', 'config', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $db = $this->getDatabase(); + + if ($userId = (int) $this->getState('user.id')) { + $query = $db->getQuery(true) + ->delete($db->quoteName('#__messages_cfg')) + ->where($db->quoteName('user_id') . ' = :userid') + ->bind(':userid', $userId, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (count($data)) { + $query = $db->getQuery(true) + ->insert($db->quoteName('#__messages_cfg')) + ->columns( + [ + $db->quoteName('user_id'), + $db->quoteName('cfg_name'), + $db->quoteName('cfg_value'), + ] + ); + + foreach ($data as $k => $v) { + $query->values( + implode( + ',', + $query->bindArray( + [$userId , $k, $v], + [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] + ) + ) + ); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + return true; + } else { + $this->setError('COM_MESSAGES_ERR_INVALID_USER'); + + return false; + } + } } diff --git a/administrator/components/com_messages/src/Model/MessageModel.php b/administrator/components/com_messages/src/Model/MessageModel.php index 3864871baeff5..7eaebe844170d 100644 --- a/administrator/components/com_messages/src/Model/MessageModel.php +++ b/administrator/components/com_messages/src/Model/MessageModel.php @@ -1,4 +1,5 @@ input; - - $user = Factory::getUser(); - $this->setState('user.id', $user->get('id')); - - $messageId = (int) $input->getInt('message_id'); - $this->setState('message.id', $messageId); - - $replyId = (int) $input->getInt('reply_id'); - $this->setState('reply.id', $replyId); - } - - /** - * Check that recipient user is the one trying to delete and then call parent delete method - * - * @param array &$pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 3.1 - */ - public function delete(&$pks) - { - $pks = (array) $pks; - $table = $this->getTable(); - $user = Factory::getUser(); - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if ($table->user_id_to != $user->id) - { - // Prune items that you can't change. - unset($pks[$i]); - - try - { - Log::add(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'warning'); - } - - return false; - } - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - return parent::delete($pks); - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - if (!isset($this->item)) - { - if ($this->item = parent::getItem($pk)) - { - // Invalid message_id returns 0 - if ($this->item->user_id_to === '0') - { - $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); - - return false; - } - - // Prime required properties. - if (empty($this->item->message_id)) - { - // Prepare data for a new record. - if ($replyId = (int) $this->getState('reply.id')) - { - // If replying to a message, preload some data. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['subject', 'user_id_from', 'user_id_to'])) - ->from($db->quoteName('#__messages')) - ->where($db->quoteName('message_id') . ' = :messageid') - ->bind(':messageid', $replyId, ParameterType::INTEGER); - - try - { - $message = $db->setQuery($query)->loadObject(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (!$message || $message->user_id_to != Factory::getUser()->id) - { - $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); - - return false; - } - - $this->item->set('user_id_to', $message->user_id_from); - $re = Text::_('COM_MESSAGES_RE'); - - if (stripos($message->subject, $re) !== 0) - { - $this->item->set('subject', $re . ' ' . $message->subject); - } - } - } - elseif ($this->item->user_id_to != Factory::getUser()->id) - { - $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); - - return false; - } - else - { - // Mark message read - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->update($db->quoteName('#__messages')) - ->set($db->quoteName('state') . ' = 1') - ->where($db->quoteName('message_id') . ' = :messageid') - ->bind(':messageid', $this->item->message_id, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - } - } - - // Get the user name for an existing message. - if ($this->item->user_id_from && $fromUser = new User($this->item->user_id_from)) - { - $this->item->set('from_user_name', $fromUser->name); - } - } - - return $this->item; - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return \Joomla\CMS\Form\Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_messages.message', 'message', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_messages.edit.message.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_messages.message', $data); - - return $data; - } - - /** - * Checks that the current user matches the message recipient and calls the parent publish method - * - * @param array &$pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function publish(&$pks, $value = 1) - { - $user = Factory::getUser(); - $table = $this->getTable(); - $pks = (array) $pks; - - // Check that the recipient matches the current user - foreach ($pks as $i => $pk) - { - $table->reset(); - - if ($table->load($pk)) - { - if ($table->user_id_to != $user->id) - { - // Prune items that you can't change. - unset($pks[$i]); - - try - { - Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'warning'); - } - - return false; - } - } - } - - return parent::publish($pks, $value); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $table = $this->getTable(); - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Assign empty values. - if (empty($table->user_id_from)) - { - $table->user_id_from = Factory::getUser()->get('id'); - } - - if ((int) $table->date_time == 0) - { - $table->date_time = Factory::getDate()->toSql(); - } - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Load the user details (already valid from table check). - $toUser = User::getInstance($table->user_id_to); - - // Check if recipient can access com_messages. - if (!$toUser->authorise('core.login.admin') || !$toUser->authorise('core.manage', 'com_messages')) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_RECIPIENT_NOT_AUTHORISED')); - - return false; - } - - // Load the recipient user configuration. - $model = $this->bootComponent('com_messages') - ->getMVCFactory()->createModel('Config', 'Administrator', ['ignore_request' => true]); - $model->setState('user.id', $table->user_id_to); - $config = $model->getItem(); - - if (empty($config)) - { - $this->setError($model->getError()); - - return false; - } - - if ($config->get('lock', false)) - { - $this->setError(Text::_('COM_MESSAGES_ERR_SEND_FAILED')); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - $key = $table->getKeyName(); - - if (isset($table->$key)) - { - $this->setState($this->getName() . '.id', $table->$key); - } - - if ($config->get('mail_on_new', true)) - { - $fromUser = User::getInstance($table->user_id_from); - $debug = Factory::getApplication()->get('debug_lang'); - $default_language = ComponentHelper::getParams('com_languages')->get('administrator'); - $lang = Language::getInstance($toUser->getParam('admin_language', $default_language), $debug); - $lang->load('com_messages', JPATH_ADMINISTRATOR); - - // Build the email subject and message - $app = Factory::getApplication(); - $linkMode = $app->get('force_ssl', 0) >= 1 ? Route::TLS_FORCE : Route::TLS_IGNORE; - $sitename = $app->get('sitename'); - $fromName = $fromUser->get('name'); - $siteURL = Route::link( - 'administrator', - 'index.php?option=com_messages&view=message&message_id=' . $table->message_id, - false, - $linkMode, - true - ); - $subject = html_entity_decode($table->subject, ENT_COMPAT, 'UTF-8'); - $message = strip_tags(html_entity_decode($table->message, ENT_COMPAT, 'UTF-8')); - - // Send the email - $mailer = new MailTemplate('com_messages.new_message', $lang->getTag()); - $data = [ - 'subject' => $subject, - 'message' => $message, - 'fromname' => $fromName, - 'sitename' => $sitename, - 'siteurl' => $siteURL, - 'fromemail' => $fromUser->email, - 'toname' => $toUser->name, - 'toemail' => $toUser->email - ]; - $mailer->addTemplateData($data); - $mailer->setReplyTo($fromUser->email, $fromUser->name); - $mailer->addRecipient($toUser->email, $toUser->name); - - try - { - $mailer->send(); - } - catch (MailDisabledException | phpMailerException $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); - - return false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); - - return false; - } - } - } - - return true; - } - - /** - * Sends a message to the site's super users - * - * @param string $subject The message subject - * @param string $message The message - * - * @return boolean - * - * @since 3.9.0 - */ - public function notifySuperUsers($subject, $message, $fromUser = null) - { - $db = $this->getDatabase(); - - try - { - /** @var Asset $table */ - $table = Table::getInstance('Asset'); - $rootId = $table->getRootId(); - - /** @var Rule[] $rules */ - $rules = Access::getAssetRules($rootId)->getData(); - $rawGroups = $rules['core.admin']->getData(); - - if (empty($rawGroups)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_MISSING_ROOT_ASSET_GROUPS')); - - return false; - } - - $groups = array(); - - foreach ($rawGroups as $g => $enabled) - { - if ($enabled) - { - $groups[] = $g; - } - } - - if (empty($groups)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_NO_GROUPS_SET_AS_SUPER_USER')); - - return false; - } - - $query = $db->getQuery(true) - ->select($db->quoteName('map.user_id')) - ->from($db->quoteName('#__user_usergroup_map', 'map')) - ->join('LEFT', - $db->quoteName('#__users', 'u'), - $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id') - ) - ->whereIn($db->quoteName('map.group_id'), $groups) - ->where($db->quoteName('u.block') . ' = 0') - ->where($db->quoteName('u.sendEmail') . ' = 1'); - - $userIDs = $db->setQuery($query)->loadColumn(0); - - if (empty($userIDs)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_NO_USERS_SET_AS_SUPER_USER')); - - return false; - } - - foreach ($userIDs as $id) - { - /* - * All messages must have a valid from user, we have use cases where an unauthenticated user may trigger this - * so we will set the from user as the to user - */ - $data = [ - 'user_id_from' => $id, - 'user_id_to' => $id, - 'subject' => $subject, - 'message' => $message, - ]; - - if (!$this->save($data)) - { - return false; - } - } - - return true; - } - catch (\Exception $exception) - { - $this->setError($exception->getMessage()); - - return false; - } - } + /** + * Message + * + * @var \stdClass + */ + protected $item; + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + parent::populateState(); + + $input = Factory::getApplication()->input; + + $user = Factory::getUser(); + $this->setState('user.id', $user->get('id')); + + $messageId = (int) $input->getInt('message_id'); + $this->setState('message.id', $messageId); + + $replyId = (int) $input->getInt('reply_id'); + $this->setState('reply.id', $replyId); + } + + /** + * Check that recipient user is the one trying to delete and then call parent delete method + * + * @param array &$pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 3.1 + */ + public function delete(&$pks) + { + $pks = (array) $pks; + $table = $this->getTable(); + $user = Factory::getUser(); + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if ($table->user_id_to != $user->id) { + // Prune items that you can't change. + unset($pks[$i]); + + try { + Log::add(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'warning'); + } + + return false; + } + } else { + $this->setError($table->getError()); + + return false; + } + } + + return parent::delete($pks); + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + if (!isset($this->item)) { + if ($this->item = parent::getItem($pk)) { + // Invalid message_id returns 0 + if ($this->item->user_id_to === '0') { + $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); + + return false; + } + + // Prime required properties. + if (empty($this->item->message_id)) { + // Prepare data for a new record. + if ($replyId = (int) $this->getState('reply.id')) { + // If replying to a message, preload some data. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['subject', 'user_id_from', 'user_id_to'])) + ->from($db->quoteName('#__messages')) + ->where($db->quoteName('message_id') . ' = :messageid') + ->bind(':messageid', $replyId, ParameterType::INTEGER); + + try { + $message = $db->setQuery($query)->loadObject(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (!$message || $message->user_id_to != Factory::getUser()->id) { + $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); + + return false; + } + + $this->item->set('user_id_to', $message->user_id_from); + $re = Text::_('COM_MESSAGES_RE'); + + if (stripos($message->subject, $re) !== 0) { + $this->item->set('subject', $re . ' ' . $message->subject); + } + } + } elseif ($this->item->user_id_to != Factory::getUser()->id) { + $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); + + return false; + } else { + // Mark message read + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__messages')) + ->set($db->quoteName('state') . ' = 1') + ->where($db->quoteName('message_id') . ' = :messageid') + ->bind(':messageid', $this->item->message_id, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + } + } + + // Get the user name for an existing message. + if ($this->item->user_id_from && $fromUser = new User($this->item->user_id_from)) { + $this->item->set('from_user_name', $fromUser->name); + } + } + + return $this->item; + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_messages.message', 'message', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_messages.edit.message.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_messages.message', $data); + + return $data; + } + + /** + * Checks that the current user matches the message recipient and calls the parent publish method + * + * @param array &$pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function publish(&$pks, $value = 1) + { + $user = Factory::getUser(); + $table = $this->getTable(); + $pks = (array) $pks; + + // Check that the recipient matches the current user + foreach ($pks as $i => $pk) { + $table->reset(); + + if ($table->load($pk)) { + if ($table->user_id_to != $user->id) { + // Prune items that you can't change. + unset($pks[$i]); + + try { + Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'warning'); + } + + return false; + } + } + } + + return parent::publish($pks, $value); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $table = $this->getTable(); + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Assign empty values. + if (empty($table->user_id_from)) { + $table->user_id_from = Factory::getUser()->get('id'); + } + + if ((int) $table->date_time == 0) { + $table->date_time = Factory::getDate()->toSql(); + } + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Load the user details (already valid from table check). + $toUser = User::getInstance($table->user_id_to); + + // Check if recipient can access com_messages. + if (!$toUser->authorise('core.login.admin') || !$toUser->authorise('core.manage', 'com_messages')) { + $this->setError(Text::_('COM_MESSAGES_ERROR_RECIPIENT_NOT_AUTHORISED')); + + return false; + } + + // Load the recipient user configuration. + $model = $this->bootComponent('com_messages') + ->getMVCFactory()->createModel('Config', 'Administrator', ['ignore_request' => true]); + $model->setState('user.id', $table->user_id_to); + $config = $model->getItem(); + + if (empty($config)) { + $this->setError($model->getError()); + + return false; + } + + if ($config->get('lock', false)) { + $this->setError(Text::_('COM_MESSAGES_ERR_SEND_FAILED')); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + $key = $table->getKeyName(); + + if (isset($table->$key)) { + $this->setState($this->getName() . '.id', $table->$key); + } + + if ($config->get('mail_on_new', true)) { + $fromUser = User::getInstance($table->user_id_from); + $debug = Factory::getApplication()->get('debug_lang'); + $default_language = ComponentHelper::getParams('com_languages')->get('administrator'); + $lang = Language::getInstance($toUser->getParam('admin_language', $default_language), $debug); + $lang->load('com_messages', JPATH_ADMINISTRATOR); + + // Build the email subject and message + $app = Factory::getApplication(); + $linkMode = $app->get('force_ssl', 0) >= 1 ? Route::TLS_FORCE : Route::TLS_IGNORE; + $sitename = $app->get('sitename'); + $fromName = $fromUser->get('name'); + $siteURL = Route::link( + 'administrator', + 'index.php?option=com_messages&view=message&message_id=' . $table->message_id, + false, + $linkMode, + true + ); + $subject = html_entity_decode($table->subject, ENT_COMPAT, 'UTF-8'); + $message = strip_tags(html_entity_decode($table->message, ENT_COMPAT, 'UTF-8')); + + // Send the email + $mailer = new MailTemplate('com_messages.new_message', $lang->getTag()); + $data = [ + 'subject' => $subject, + 'message' => $message, + 'fromname' => $fromName, + 'sitename' => $sitename, + 'siteurl' => $siteURL, + 'fromemail' => $fromUser->email, + 'toname' => $toUser->name, + 'toemail' => $toUser->email + ]; + $mailer->addTemplateData($data); + $mailer->setReplyTo($fromUser->email, $fromUser->name); + $mailer->addRecipient($toUser->email, $toUser->name); + + try { + $mailer->send(); + } catch (MailDisabledException | phpMailerException $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); + + return false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); + + return false; + } + } + } + + return true; + } + + /** + * Sends a message to the site's super users + * + * @param string $subject The message subject + * @param string $message The message + * + * @return boolean + * + * @since 3.9.0 + */ + public function notifySuperUsers($subject, $message, $fromUser = null) + { + $db = $this->getDatabase(); + + try { + /** @var Asset $table */ + $table = Table::getInstance('Asset'); + $rootId = $table->getRootId(); + + /** @var Rule[] $rules */ + $rules = Access::getAssetRules($rootId)->getData(); + $rawGroups = $rules['core.admin']->getData(); + + if (empty($rawGroups)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_MISSING_ROOT_ASSET_GROUPS')); + + return false; + } + + $groups = array(); + + foreach ($rawGroups as $g => $enabled) { + if ($enabled) { + $groups[] = $g; + } + } + + if (empty($groups)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_NO_GROUPS_SET_AS_SUPER_USER')); + + return false; + } + + $query = $db->getQuery(true) + ->select($db->quoteName('map.user_id')) + ->from($db->quoteName('#__user_usergroup_map', 'map')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'u'), + $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id') + ) + ->whereIn($db->quoteName('map.group_id'), $groups) + ->where($db->quoteName('u.block') . ' = 0') + ->where($db->quoteName('u.sendEmail') . ' = 1'); + + $userIDs = $db->setQuery($query)->loadColumn(0); + + if (empty($userIDs)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_NO_USERS_SET_AS_SUPER_USER')); + + return false; + } + + foreach ($userIDs as $id) { + /* + * All messages must have a valid from user, we have use cases where an unauthenticated user may trigger this + * so we will set the from user as the to user + */ + $data = [ + 'user_id_from' => $id, + 'user_id_to' => $id, + 'subject' => $subject, + 'message' => $message, + ]; + + if (!$this->save($data)) { + return false; + } + } + + return true; + } catch (\Exception $exception) { + $this->setError($exception->getMessage()); + + return false; + } + } } diff --git a/administrator/components/com_messages/src/Model/MessagesModel.php b/administrator/components/com_messages/src/Model/MessagesModel.php index 0077db55d982d..1c7698c71b04a 100644 --- a/administrator/components/com_messages/src/Model/MessagesModel.php +++ b/administrator/components/com_messages/src/Model/MessagesModel.php @@ -1,4 +1,5 @@ getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - $id = (int) $user->get('id'); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a') . '.*', - $db->quoteName('u.name', 'user_from'), - ] - ) - ); - $query->from($db->quoteName('#__messages', 'a')); - - // Join over the users for message owner. - $query->join('INNER', - $db->quoteName('#__users', 'u'), - $db->quoteName('u.id') . ' = ' . $db->quoteName('a.user_id_from') - ) - ->where($db->quoteName('a.user_id_to') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - // Filter by published state. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif ($state !== '*') - { - $query->whereIn($db->quoteName('a.state'), [0, 1]); - } - - // Filter by search in subject or message. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.subject') . ' LIKE :subject', - $db->quoteName('a.message') . ' LIKE :message', - ], - 'OR' - ) - ->bind(':subject', $search) - ->bind(':message', $search); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.date_time')) . ' ' . $db->escape($this->getState('list.direction', 'DESC'))); - - return $query; - } - - /** - * Purge the messages table of old messages for the given user ID. - * - * @param int $userId The user id - * - * @return void - * - * @since 4.2.0 - */ - public function purge(int $userId): void - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['cfg_name', 'cfg_value'])) - ->from($db->quoteName('#__messages_cfg')) - ->where( - [ - $db->quoteName('user_id') . ' = :userId', - $db->quoteName('cfg_name') . ' = ' . $db->quote('auto_purge'), - ] - ) - ->bind(':userId', $userId, ParameterType::INTEGER); - - $db->setQuery($query); - $config = $db->loadObject(); - - // Default is 7 days - $purge = 7; - - // Check if auto_purge value set - if (\is_object($config) && $config->cfg_name === 'auto_purge') - { - $purge = $config->cfg_value; - } - - // If purge value is not 0, then allow purging of old messages - if ($purge > 0) - { - // Purge old messages at day set in message configuration - $past = Factory::getDate(time() - $purge * 86400)->toSql(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__messages')) - ->where( - [ - $db->quoteName('date_time') . ' < :past', - $db->quoteName('user_id_to') . ' = :userId', - ] - ) - ->bind(':past', $past) - ->bind(':userId', $userId, ParameterType::INTEGER); - - $db->setQuery($query); - $db->execute(); - } - } + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'message_id', 'a.id', + 'subject', 'a.subject', + 'state', 'a.state', + 'user_id_from', 'a.user_id_from', + 'user_id_to', 'a.user_id_to', + 'date_time', 'a.date_time', + 'priority', 'a.priority', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.date_time', $direction = 'desc') + { + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + $id = (int) $user->get('id'); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a') . '.*', + $db->quoteName('u.name', 'user_from'), + ] + ) + ); + $query->from($db->quoteName('#__messages', 'a')); + + // Join over the users for message owner. + $query->join( + 'INNER', + $db->quoteName('#__users', 'u'), + $db->quoteName('u.id') . ' = ' . $db->quoteName('a.user_id_from') + ) + ->where($db->quoteName('a.user_id_to') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + // Filter by published state. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif ($state !== '*') { + $query->whereIn($db->quoteName('a.state'), [0, 1]); + } + + // Filter by search in subject or message. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.subject') . ' LIKE :subject', + $db->quoteName('a.message') . ' LIKE :message', + ], + 'OR' + ) + ->bind(':subject', $search) + ->bind(':message', $search); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.date_time')) . ' ' . $db->escape($this->getState('list.direction', 'DESC'))); + + return $query; + } + + /** + * Purge the messages table of old messages for the given user ID. + * + * @param int $userId The user id + * + * @return void + * + * @since 4.2.0 + */ + public function purge(int $userId): void + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['cfg_name', 'cfg_value'])) + ->from($db->quoteName('#__messages_cfg')) + ->where( + [ + $db->quoteName('user_id') . ' = :userId', + $db->quoteName('cfg_name') . ' = ' . $db->quote('auto_purge'), + ] + ) + ->bind(':userId', $userId, ParameterType::INTEGER); + + $db->setQuery($query); + $config = $db->loadObject(); + + // Default is 7 days + $purge = 7; + + // Check if auto_purge value set + if (\is_object($config) && $config->cfg_name === 'auto_purge') { + $purge = $config->cfg_value; + } + + // If purge value is not 0, then allow purging of old messages + if ($purge > 0) { + // Purge old messages at day set in message configuration + $past = Factory::getDate(time() - $purge * 86400)->toSql(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__messages')) + ->where( + [ + $db->quoteName('date_time') . ' < :past', + $db->quoteName('user_id_to') . ' = :userId', + ] + ) + ->bind(':past', $past) + ->bind(':userId', $userId, ParameterType::INTEGER); + + $db->setQuery($query); + $db->execute(); + } + } } diff --git a/administrator/components/com_messages/src/Service/HTML/Messages.php b/administrator/components/com_messages/src/Service/HTML/Messages.php index 17be8a3280f74..4266f79392d4c 100644 --- a/administrator/components/com_messages/src/Service/HTML/Messages.php +++ b/administrator/components/com_messages/src/Service/HTML/Messages.php @@ -1,4 +1,5 @@ array('trash', 'messages.unpublish', 'JTRASHED', 'COM_MESSAGES_MARK_AS_UNREAD'), - 1 => array('publish', 'messages.unpublish', 'COM_MESSAGES_OPTION_READ', 'COM_MESSAGES_MARK_AS_UNREAD'), - 0 => array('unpublish', 'messages.publish', 'COM_MESSAGES_OPTION_UNREAD', 'COM_MESSAGES_MARK_AS_READ'), - ); - - $state = ArrayHelper::getValue($states, (int) $value, $states[0]); - $icon = $state[0]; - - if ($canChange) - { - $html = ''; - } - - return $html; - } + /** + * Get the HTML code of the state switcher + * + * @param int $i Row number + * @param int $value The state value + * @param boolean $canChange Can the user change the state? + * + * @return string + * + * @since 3.4 + */ + public function status($i, $value = 0, $canChange = false) + { + // Array of image, task, title, action. + $states = array( + -2 => array('trash', 'messages.unpublish', 'JTRASHED', 'COM_MESSAGES_MARK_AS_UNREAD'), + 1 => array('publish', 'messages.unpublish', 'COM_MESSAGES_OPTION_READ', 'COM_MESSAGES_MARK_AS_UNREAD'), + 0 => array('unpublish', 'messages.publish', 'COM_MESSAGES_OPTION_UNREAD', 'COM_MESSAGES_MARK_AS_READ'), + ); + + $state = ArrayHelper::getValue($states, (int) $value, $states[0]); + $icon = $state[0]; + + if ($canChange) { + $html = ''; + } + + return $html; + } } diff --git a/administrator/components/com_messages/src/Table/MessageTable.php b/administrator/components/com_messages/src/Table/MessageTable.php index a750d7d7c17a3..74c0e5b7a51d8 100644 --- a/administrator/components/com_messages/src/Table/MessageTable.php +++ b/administrator/components/com_messages/src/Table/MessageTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - } - - /** - * Validation and filtering. - * - * @return boolean - * - * @since 1.5 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Check the to and from users. - $user = new User($this->user_id_from); - - if (empty($user->id)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_FROM_USER')); - - return false; - } - - $user = new User($this->user_id_to); - - if (empty($user->id)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_TO_USER')); - - return false; - } - - if (empty($this->subject)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_SUBJECT')); - - return false; - } - - if (empty($this->message)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_MESSAGE')); - - return false; - } - - return true; - } + /** + * Constructor + * + * @param DatabaseDriver $db Database connector object + * + * @since 1.5 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__messages', 'message_id', $db); + + $this->setColumnAlias('published', 'state'); + } + + /** + * Validation and filtering. + * + * @return boolean + * + * @since 1.5 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Check the to and from users. + $user = new User($this->user_id_from); + + if (empty($user->id)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_FROM_USER')); + + return false; + } + + $user = new User($this->user_id_to); + + if (empty($user->id)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_TO_USER')); + + return false; + } + + if (empty($this->subject)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_SUBJECT')); + + return false; + } + + if (empty($this->message)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_MESSAGE')); + + return false; + } + + return true; + } } diff --git a/administrator/components/com_messages/src/View/Config/HtmlView.php b/administrator/components/com_messages/src/View/Config/HtmlView.php index c4b9e79201721..d9cb400ab9ef7 100644 --- a/administrator/components/com_messages/src/View/Config/HtmlView.php +++ b/administrator/components/com_messages/src/View/Config/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Bind the record to the form. - $this->form->bind($this->item); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - ToolbarHelper::title(Text::_('COM_MESSAGES_TOOLBAR_MY_SETTINGS'), 'envelope'); - - ToolbarHelper::apply('config.save', 'JSAVE'); - - ToolbarHelper::cancel('config.cancel', 'JCANCEL'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Bind the record to the form. + $this->form->bind($this->item); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + ToolbarHelper::title(Text::_('COM_MESSAGES_TOOLBAR_MY_SETTINGS'), 'envelope'); + + ToolbarHelper::apply('config.save', 'JSAVE'); + + ToolbarHelper::cancel('config.cancel', 'JCANCEL'); + } } diff --git a/administrator/components/com_messages/src/View/Message/HtmlView.php b/administrator/components/com_messages/src/View/Message/HtmlView.php index 8d6a13cadfe03..a22771b94040c 100644 --- a/administrator/components/com_messages/src/View/Message/HtmlView.php +++ b/administrator/components/com_messages/src/View/Message/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - elseif ($this->getLayout() !== 'edit' && empty($this->item->message_id)) - { - throw new GenericDataException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } elseif ($this->getLayout() !== 'edit' && empty($this->item->message_id)) { + throw new GenericDataException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - parent::display($tpl); - $this->addToolbar(); - } + parent::display($tpl); + $this->addToolbar(); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $app = Factory::getApplication(); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $app = Factory::getApplication(); - if ($this->getLayout() == 'edit') - { - $app->input->set('hidemainmenu', true); - ToolbarHelper::title(Text::_('COM_MESSAGES_WRITE_PRIVATE_MESSAGE'), 'envelope-open-text new-privatemessage'); - ToolbarHelper::custom('message.save', 'envelope', '', 'COM_MESSAGES_TOOLBAR_SEND', false); - ToolbarHelper::cancel('message.cancel'); - ToolbarHelper::help('Private_Messages:_Write'); - } - else - { - ToolbarHelper::title(Text::_('COM_MESSAGES_VIEW_PRIVATE_MESSAGE'), 'envelope inbox'); - $sender = User::getInstance($this->item->user_id_from); + if ($this->getLayout() == 'edit') { + $app->input->set('hidemainmenu', true); + ToolbarHelper::title(Text::_('COM_MESSAGES_WRITE_PRIVATE_MESSAGE'), 'envelope-open-text new-privatemessage'); + ToolbarHelper::custom('message.save', 'envelope', '', 'COM_MESSAGES_TOOLBAR_SEND', false); + ToolbarHelper::cancel('message.cancel'); + ToolbarHelper::help('Private_Messages:_Write'); + } else { + ToolbarHelper::title(Text::_('COM_MESSAGES_VIEW_PRIVATE_MESSAGE'), 'envelope inbox'); + $sender = User::getInstance($this->item->user_id_from); - if ($sender->id !== $app->getIdentity()->get('id') && ($sender->authorise('core.admin') - || $sender->authorise('core.manage', 'com_messages') && $sender->authorise('core.login.admin')) - && $app->getIdentity()->authorise('core.manage', 'com_users') - ) - { - ToolbarHelper::custom('message.reply', 'redo', '', 'COM_MESSAGES_TOOLBAR_REPLY', false); - } + if ( + $sender->id !== $app->getIdentity()->get('id') && ($sender->authorise('core.admin') + || $sender->authorise('core.manage', 'com_messages') && $sender->authorise('core.login.admin')) + && $app->getIdentity()->authorise('core.manage', 'com_users') + ) { + ToolbarHelper::custom('message.reply', 'redo', '', 'COM_MESSAGES_TOOLBAR_REPLY', false); + } - ToolbarHelper::cancel('message.cancel'); - ToolbarHelper::help('Private_Messages:_Read'); - } - } + ToolbarHelper::cancel('message.cancel'); + ToolbarHelper::help('Private_Messages:_Read'); + } + } } diff --git a/administrator/components/com_messages/src/View/Messages/HtmlView.php b/administrator/components/com_messages/src/View/Messages/HtmlView.php index ec7d978dbd503..5e283ee51ba4b 100644 --- a/administrator/components/com_messages/src/View/Messages/HtmlView.php +++ b/administrator/components/com_messages/src/View/Messages/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $state = $this->get('State'); - $canDo = ContentHelper::getActions('com_messages'); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_MESSAGES_MANAGER_MESSAGES'), 'envelope inbox'); - - // Only display the New button if the user has the access level to create a message and if they have access to the list of users - if ($canDo->get('core.create') && $user->authorise('core.manage', 'com_users')) - { - $toolbar->addNew('message.add'); - } - - if (!$this->isEmptyState && $canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('messages.publish') - ->text('COM_MESSAGES_TOOLBAR_MARK_AS_READ') - ->listCheck(true); - - $childBar->unpublish('messages.unpublish') - ->text('COM_MESSAGES_TOOLBAR_MARK_AS_UNREAD') - ->listCheck(true); - - if ($this->state->get('filter.state') != -2) - { - $childBar->trash('messages.trash')->listCheck(true); - } - } - - $toolbar->appendButton('Link', 'cog', 'COM_MESSAGES_TOOLBAR_MY_SETTINGS', 'index.php?option=com_messages&view=config'); - ToolbarHelper::divider(); - - if (!$this->isEmptyState && $this->state->get('filter.state') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('messages.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin')) - { - $toolbar->preferences('com_messages'); - } - - $toolbar->help('Private_Messages'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $state = $this->get('State'); + $canDo = ContentHelper::getActions('com_messages'); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_MESSAGES_MANAGER_MESSAGES'), 'envelope inbox'); + + // Only display the New button if the user has the access level to create a message and if they have access to the list of users + if ($canDo->get('core.create') && $user->authorise('core.manage', 'com_users')) { + $toolbar->addNew('message.add'); + } + + if (!$this->isEmptyState && $canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('messages.publish') + ->text('COM_MESSAGES_TOOLBAR_MARK_AS_READ') + ->listCheck(true); + + $childBar->unpublish('messages.unpublish') + ->text('COM_MESSAGES_TOOLBAR_MARK_AS_UNREAD') + ->listCheck(true); + + if ($this->state->get('filter.state') != -2) { + $childBar->trash('messages.trash')->listCheck(true); + } + } + + $toolbar->appendButton('Link', 'cog', 'COM_MESSAGES_TOOLBAR_MY_SETTINGS', 'index.php?option=com_messages&view=config'); + ToolbarHelper::divider(); + + if (!$this->isEmptyState && $this->state->get('filter.state') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('messages.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin')) { + $toolbar->preferences('com_messages'); + } + + $toolbar->help('Private_Messages'); + } } diff --git a/administrator/components/com_messages/tmpl/config/default.php b/administrator/components/com_messages/tmpl/config/default.php index 1c3d3dba57fa6..862c2a06f37d8 100644 --- a/administrator/components/com_messages/tmpl/config/default.php +++ b/administrator/components/com_messages/tmpl/config/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
    -
    -
    -
    -
    - - form->renderField('lock'); ?> - form->renderField('mail_on_new'); ?> - form->renderField('auto_purge'); ?> -
    -
    -
    -
    +
    +
    +
    +
    + + form->renderField('lock'); ?> + form->renderField('mail_on_new'); ?> + form->renderField('auto_purge'); ?> +
    +
    +
    +
    - - + +
    diff --git a/administrator/components/com_messages/tmpl/message/default.php b/administrator/components/com_messages/tmpl/message/default.php index ce8277f94cf91..7cb2ee34822d2 100644 --- a/administrator/components/com_messages/tmpl/message/default.php +++ b/administrator/components/com_messages/tmpl/message/default.php @@ -1,4 +1,5 @@
    -
    -
    -
    -
    -
    - -
    -
    - item->get('from_user_name'); ?> -
    -
    -
    -
    - -
    -
    - item->date_time, Text::_('DATE_FORMAT_LC2')); ?> -
    -
    -
    -
    - -
    -
    - item->subject; ?> -
    -
    -
    -
    - -
    -
    - item->message; ?> -
    -
    - - - -
    -
    -
    +
    +
    +
    +
    +
    + +
    +
    + item->get('from_user_name'); ?> +
    +
    +
    +
    + +
    +
    + item->date_time, Text::_('DATE_FORMAT_LC2')); ?> +
    +
    +
    +
    + +
    +
    + item->subject; ?> +
    +
    +
    +
    + +
    +
    + item->message; ?> +
    +
    + + + +
    +
    +
    diff --git a/administrator/components/com_messages/tmpl/message/edit.php b/administrator/components/com_messages/tmpl/message/edit.php index 2ddcf004df2b0..00b33c96a3702 100644 --- a/administrator/components/com_messages/tmpl/message/edit.php +++ b/administrator/components/com_messages/tmpl/message/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
    -
    -
    -
    -
    - form->getLabel('user_id_to'); ?> - form->getInput('user_id_to'); ?> -
    -
    - form->getLabel('subject'); ?> - form->getInput('subject'); ?> -
    -
    - form->getLabel('message'); ?> - form->getInput('message'); ?> -
    -
    -
    -
    - - +
    +
    +
    +
    + form->getLabel('user_id_to'); ?> + form->getInput('user_id_to'); ?> +
    +
    + form->getLabel('subject'); ?> + form->getInput('subject'); ?> +
    +
    + form->getLabel('message'); ?> + form->getInput('message'); ?> +
    +
    +
    +
    + +
    diff --git a/administrator/components/com_messages/tmpl/messages/default.php b/administrator/components/com_messages/tmpl/messages/default.php index 57ab907ee6a5f..0572dfdc0cf29 100644 --- a/administrator/components/com_messages/tmpl/messages/default.php +++ b/administrator/components/com_messages/tmpl/messages/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
    -
    - $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - items as $i => $item) : - $canChange = $user->authorise('core.edit.state', 'com_messages'); - ?> - - - - - - - - - -
    - , - , - -
    - - - - - - - - - -
    - message_id, false, 'cid', 'cb', $item->subject); ?> - - - escape($item->subject); ?> - - state, $canChange); ?> - - user_from; ?> - - date_time, Text::_('DATE_FORMAT_LC2')); ?> -
    +
    + $this)); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + items as $i => $item) : + $canChange = $user->authorise('core.edit.state', 'com_messages'); + ?> + + + + + + + + + +
    + , + , + +
    + + + + + + + + + +
    + message_id, false, 'cid', 'cb', $item->subject); ?> + + + escape($item->subject); ?> + + state, $canChange); ?> + + user_from; ?> + + date_time, Text::_('DATE_FORMAT_LC2')); ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - -
    - - - -
    -
    + +
    + + + +
    +
    diff --git a/administrator/components/com_messages/tmpl/messages/emptystate.php b/administrator/components/com_messages/tmpl/messages/emptystate.php index bd872cff2d773..43a4b1a7a54c4 100644 --- a/administrator/components/com_messages/tmpl/messages/emptystate.php +++ b/administrator/components/com_messages/tmpl/messages/emptystate.php @@ -1,4 +1,5 @@ 'COM_MESSAGES', - 'formURL' => 'index.php?option=com_messages&view=messages', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Private_Messages', - 'icon' => 'icon-envelope inbox', + 'textPrefix' => 'COM_MESSAGES', + 'formURL' => 'index.php?option=com_messages&view=messages', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Private_Messages', + 'icon' => 'icon-envelope inbox', ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_messages') - && Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_users')) -{ - $displayData['createURL'] = 'index.php?option=com_messages&task=message.add'; +if ( + Factory::getApplication()->getIdentity()->authorise('core.create', 'com_messages') + && Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_users') +) { + $displayData['createURL'] = 'index.php?option=com_messages&task=message.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_modules/helpers/modules.php b/administrator/components/com_modules/helpers/modules.php index a2419e69b6609..0959bf13727e7 100644 --- a/administrator/components/com_modules/helpers/modules.php +++ b/administrator/components/com_modules/helpers/modules.php @@ -1,4 +1,5 @@ escape(Text::_('JGLOBAL_TYPE_OR_SELECT_SOME_OPTIONS')) . '" ', + 'class="' . $class . '"', + ' allow-custom', + ' search-placeholder="' . $this->escape(Text::_('JGLOBAL_TYPE_OR_SELECT_SOME_OPTIONS')) . '" ', ); $selectAttr = array( - $disabled ? 'disabled' : '', - $readonly ? 'readonly' : '', - strlen($hint) ? 'placeholder="' . $this->escape($hint) . '"' : '', - $onchange ? ' onchange="' . $onchange . '"' : '', - $autofocus ? ' autofocus' : '', + $disabled ? 'disabled' : '', + $readonly ? 'readonly' : '', + strlen($hint) ? 'placeholder="' . $this->escape($hint) . '"' : '', + $onchange ? ' onchange="' . $onchange . '"' : '', + $autofocus ? ' autofocus' : '', ); -if ($required) -{ - $selectAttr[] = ' required class="required"'; - $attributes[] = ' required'; +if ($required) { + $selectAttr[] = ' required class="required"'; + $attributes[] = ' required'; } Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getDocument()->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select'); ?> > $id, - 'list.select' => $value, - 'list.attr' => implode(' ', $selectAttr), - ) - ); -?> + echo HTMLHelper::_('select.groupedlist', $positions, $name, array( + 'id' => $id, + 'list.select' => $value, + 'list.attr' => implode(' ', $selectAttr), + )); + ?> diff --git a/administrator/components/com_modules/layouts/toolbar/cancelselect.php b/administrator/components/com_modules/layouts/toolbar/cancelselect.php index b94d855fd051f..7637060854cd8 100644 --- a/administrator/components/com_modules/layouts/toolbar/cancelselect.php +++ b/administrator/components/com_modules/layouts/toolbar/cancelselect.php @@ -1,4 +1,5 @@ - + diff --git a/administrator/components/com_modules/services/provider.php b/administrator/components/com_modules/services/provider.php index 4998d4d42da02..e624191955b75 100644 --- a/administrator/components/com_modules/services/provider.php +++ b/administrator/components/com_modules/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Modules')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Modules')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Modules')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Modules')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new ModulesComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new ModulesComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_modules/src/Controller/DisplayController.php b/administrator/components/com_modules/src/Controller/DisplayController.php index 6751f988c7a0d..59765a93b558f 100644 --- a/administrator/components/com_modules/src/Controller/DisplayController.php +++ b/administrator/components/com_modules/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('layout', 'edit'); - $id = $this->input->getInt('id'); - - // Verify client - $clientId = $this->input->post->getInt('client_id'); - - if (!is_null($clientId)) - { - $uri = Uri::getInstance(); - - if ((int) $uri->getVar('client_id') !== (int) $clientId) - { - $this->setRedirect(Route::_('index.php?option=com_modules&view=modules&client_id=' . $clientId, false)); - - return false; - } - } - - // Check for edit form. - if ($layout == 'edit' && !$this->checkEditId('com_modules.edit.module', $id)) - { - // 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', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_modules&view=modules&client_id=' . $this->input->getInt('client_id'), false)); - - return false; - } - - // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. - $factory = $this->app->bootComponent('menus')->getMVCFactory(); - - if ($langMissing = $factory->createModel('Menus', 'Administrator')->getMissingModuleLanguages()) - { - $this->app->enqueueMessage(Text::sprintf('JMENU_MULTILANG_WARNING_MISSING_MODULES', implode(', ', $langMissing)), 'warning'); - } - - return parent::display(); - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'modules'; + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array|boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()} + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $layout = $this->input->get('layout', 'edit'); + $id = $this->input->getInt('id'); + + // Verify client + $clientId = $this->input->post->getInt('client_id'); + + if (!is_null($clientId)) { + $uri = Uri::getInstance(); + + if ((int) $uri->getVar('client_id') !== (int) $clientId) { + $this->setRedirect(Route::_('index.php?option=com_modules&view=modules&client_id=' . $clientId, false)); + + return false; + } + } + + // Check for edit form. + if ($layout == 'edit' && !$this->checkEditId('com_modules.edit.module', $id)) { + // 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', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_modules&view=modules&client_id=' . $this->input->getInt('client_id'), false)); + + return false; + } + + // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. + $factory = $this->app->bootComponent('menus')->getMVCFactory(); + + if ($langMissing = $factory->createModel('Menus', 'Administrator')->getMissingModuleLanguages()) { + $this->app->enqueueMessage(Text::sprintf('JMENU_MULTILANG_WARNING_MISSING_MODULES', implode(', ', $langMissing)), 'warning'); + } + + return parent::display(); + } } diff --git a/administrator/components/com_modules/src/Controller/ModuleController.php b/administrator/components/com_modules/src/Controller/ModuleController.php index ef53b80adf3fe..c948931d178ad 100644 --- a/administrator/components/com_modules/src/Controller/ModuleController.php +++ b/administrator/components/com_modules/src/Controller/ModuleController.php @@ -1,4 +1,5 @@ app; - - // Get the result of the parent method. If an error, just return it. - $result = parent::add(); - - if ($result instanceof \Exception) - { - return $result; - } - - // Look for the Extension ID. - $extensionId = $this->input->get('eid', 0, 'int'); - - if (empty($extensionId)) - { - $redirectUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . '&layout=edit'; - - $this->setRedirect(Route::_($redirectUrl, false)); - - $app->enqueueMessage(Text::_('COM_MODULES_ERROR_INVALID_EXTENSION'), 'warning'); - } - - $app->setUserState('com_modules.add.module.extension_id', $extensionId); - $app->setUserState('com_modules.add.module.params', null); - - // Parameters could be coming in for a new item, so let's set them. - $params = $this->input->get('params', array(), 'array'); - $app->setUserState('com_modules.add.module.params', $params); - } - - /** - * Override parent cancel method to reset the add module state. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 1.6 - */ - public function cancel($key = null) - { - $result = parent::cancel(); - - $this->app->setUserState('com_modules.add.module.extension_id', null); - $this->app->setUserState('com_modules.add.module.params', null); - - if ($return = $this->input->get('return', '', 'BASE64')) - { - $return = base64_decode($return); - - // Don't redirect to an external URL. - if (!Uri::isInternal($return)) - { - $return = Uri::base(); - } - - $this->app->redirect($return); - } - - return $result; - } - - /** - * Override parent allowSave method. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowSave($data, $key = 'id') - { - // Use custom position if selected - if (isset($data['custom_position'])) - { - if (empty($data['position'])) - { - $data['position'] = $data['custom_position']; - } - - unset($data['custom_position']); - } - - return parent::allowSave($data, $key); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 3.2 - */ - protected function allowEdit($data = array(), $key = 'id') - { - // Initialise variables. - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - - // Zero record (id:0), return component edit permission by calling parent controller method - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Check edit on the record asset (explicit or inherited) - if ($this->app->getIdentity()->authorise('core.edit', 'com_modules.module.' . $recordId)) - { - return true; - } - - return false; - } - - /** - * Method to run batch operations. - * - * @param string $model The model - * - * @return boolean True on success. - * - * @since 1.7 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('Module', 'Administrator', array()); - - // Preset the redirect - $redirectUrl = 'index.php?option=com_modules&view=modules' . $this->getRedirectToListAppend(); - - $this->setRedirect(Route::_($redirectUrl, false)); - - return parent::batch($model); - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 1.6 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - $task = $this->getTask(); - - switch ($task) - { - case 'save2new': - $this->app->setUserState('com_modules.add.module.extension_id', $model->getState('module.extension_id')); - break; - - default: - $this->app->setUserState('com_modules.add.module.extension_id', null); - break; - } - - $this->app->setUserState('com_modules.add.module.params', null); - } - - /** - * Method to save a record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key - * - * @return boolean True if successful, false otherwise. - */ - public function save($key = null, $urlVar = null) - { - $this->checkToken(); - - if ($this->app->getDocument()->getType() == 'json') - { - $model = $this->getModel(); - $data = $this->input->post->get('jform', array(), 'array'); - $item = $model->getItem($this->input->get('id')); - $properties = $item->getProperties(); - - if (isset($data['params'])) - { - unset($properties['params']); - } - - // Replace changed properties - $data = array_replace_recursive($properties, $data); - - if (!empty($data['assigned'])) - { - $data['assigned'] = array_map('abs', $data['assigned']); - } - - // Add new data to input before process by parent save() - $this->input->post->set('jform', $data); - - // Add path of forms directory - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/models/forms'); - } - - return parent::save($key, $urlVar); - - } - - /** - * Method to get the other modules in the same position - * - * @return string The data for the Ajax request. - * - * @since 3.6.3 - */ - public function orderPosition() - { - $app = $this->app; - - // Send json mime type. - $app->mimeType = 'application/json'; - $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); - $app->sendHeaders(); - - // Check if user token is valid. - if (!Session::checkToken('get')) - { - $app->enqueueMessage(Text::_('JINVALID_TOKEN_NOTICE'), 'error'); - echo new JsonResponse; - $app->close(); - } - - $clientId = $this->input->getValue('client_id'); - $position = $this->input->getValue('position'); - $moduleId = $this->input->getValue('module_id'); - - // Access check. - if (!$this->app->getIdentity()->authorise('core.create', 'com_modules') - && !$this->app->getIdentity()->authorise('core.edit.state', 'com_modules') - && ($moduleId && !$this->app->getIdentity()->authorise('core.edit.state', 'com_modules.module.' . $moduleId))) - { - $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error'); - echo new JsonResponse; - $app->close(); - } - - $db = Factory::getDbo(); - $clientId = (int) $clientId; - $query = $db->getQuery(true) - ->select($db->quoteName(['position', 'ordering', 'title'])) - ->from($db->quoteName('#__modules')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->where($db->quoteName('position') . ' = :position') - ->order($db->quoteName('ordering')) - ->bind(':clientid', $clientId, ParameterType::INTEGER) - ->bind(':position', $position); - - $db->setQuery($query); - - try - { - $orders = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return ''; - } - - $orders2 = array(); - $n = count($orders); - - if ($n > 0) - { - for ($i = 0; $i < $n; $i++) - { - if (!isset($orders2[$orders[$i]->position])) - { - $orders2[$orders[$i]->position] = 0; - } - - $orders2[$orders[$i]->position]++; - $ord = $orders2[$orders[$i]->position]; - $title = Text::sprintf('COM_MODULES_OPTION_ORDER_POSITION', $ord, htmlspecialchars($orders[$i]->title, ENT_QUOTES, 'UTF-8')); - - $html[] = $orders[$i]->position . ',' . $ord . ',' . $title; - } - } - else - { - $html[] = $position . ',' . 1 . ',' . Text::_('JNONE'); - } - - echo new JsonResponse($html); - $app->close(); - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - $append .= '&client_id=' . $this->input->getInt('client_id'); - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&client_id=' . $this->input->getInt('client_id'); - - return $append; - } + /** + * Override parent add method. + * + * @return \Exception|void True if the record can be added, a \Exception object if not. + * + * @since 1.6 + */ + public function add() + { + $app = $this->app; + + // Get the result of the parent method. If an error, just return it. + $result = parent::add(); + + if ($result instanceof \Exception) { + return $result; + } + + // Look for the Extension ID. + $extensionId = $this->input->get('eid', 0, 'int'); + + if (empty($extensionId)) { + $redirectUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . '&layout=edit'; + + $this->setRedirect(Route::_($redirectUrl, false)); + + $app->enqueueMessage(Text::_('COM_MODULES_ERROR_INVALID_EXTENSION'), 'warning'); + } + + $app->setUserState('com_modules.add.module.extension_id', $extensionId); + $app->setUserState('com_modules.add.module.params', null); + + // Parameters could be coming in for a new item, so let's set them. + $params = $this->input->get('params', array(), 'array'); + $app->setUserState('com_modules.add.module.params', $params); + } + + /** + * Override parent cancel method to reset the add module state. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 1.6 + */ + public function cancel($key = null) + { + $result = parent::cancel(); + + $this->app->setUserState('com_modules.add.module.extension_id', null); + $this->app->setUserState('com_modules.add.module.params', null); + + if ($return = $this->input->get('return', '', 'BASE64')) { + $return = base64_decode($return); + + // Don't redirect to an external URL. + if (!Uri::isInternal($return)) { + $return = Uri::base(); + } + + $this->app->redirect($return); + } + + return $result; + } + + /** + * Override parent allowSave method. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowSave($data, $key = 'id') + { + // Use custom position if selected + if (isset($data['custom_position'])) { + if (empty($data['position'])) { + $data['position'] = $data['custom_position']; + } + + unset($data['custom_position']); + } + + return parent::allowSave($data, $key); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 3.2 + */ + protected function allowEdit($data = array(), $key = 'id') + { + // Initialise variables. + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + + // Zero record (id:0), return component edit permission by calling parent controller method + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Check edit on the record asset (explicit or inherited) + if ($this->app->getIdentity()->authorise('core.edit', 'com_modules.module.' . $recordId)) { + return true; + } + + return false; + } + + /** + * Method to run batch operations. + * + * @param string $model The model + * + * @return boolean True on success. + * + * @since 1.7 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('Module', 'Administrator', array()); + + // Preset the redirect + $redirectUrl = 'index.php?option=com_modules&view=modules' . $this->getRedirectToListAppend(); + + $this->setRedirect(Route::_($redirectUrl, false)); + + return parent::batch($model); + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 1.6 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + $task = $this->getTask(); + + switch ($task) { + case 'save2new': + $this->app->setUserState('com_modules.add.module.extension_id', $model->getState('module.extension_id')); + break; + + default: + $this->app->setUserState('com_modules.add.module.extension_id', null); + break; + } + + $this->app->setUserState('com_modules.add.module.params', null); + } + + /** + * Method to save a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key + * + * @return boolean True if successful, false otherwise. + */ + public function save($key = null, $urlVar = null) + { + $this->checkToken(); + + if ($this->app->getDocument()->getType() == 'json') { + $model = $this->getModel(); + $data = $this->input->post->get('jform', array(), 'array'); + $item = $model->getItem($this->input->get('id')); + $properties = $item->getProperties(); + + if (isset($data['params'])) { + unset($properties['params']); + } + + // Replace changed properties + $data = array_replace_recursive($properties, $data); + + if (!empty($data['assigned'])) { + $data['assigned'] = array_map('abs', $data['assigned']); + } + + // Add new data to input before process by parent save() + $this->input->post->set('jform', $data); + + // Add path of forms directory + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/models/forms'); + } + + return parent::save($key, $urlVar); + } + + /** + * Method to get the other modules in the same position + * + * @return string The data for the Ajax request. + * + * @since 3.6.3 + */ + public function orderPosition() + { + $app = $this->app; + + // Send json mime type. + $app->mimeType = 'application/json'; + $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); + $app->sendHeaders(); + + // Check if user token is valid. + if (!Session::checkToken('get')) { + $app->enqueueMessage(Text::_('JINVALID_TOKEN_NOTICE'), 'error'); + echo new JsonResponse(); + $app->close(); + } + + $clientId = $this->input->getValue('client_id'); + $position = $this->input->getValue('position'); + $moduleId = $this->input->getValue('module_id'); + + // Access check. + if ( + !$this->app->getIdentity()->authorise('core.create', 'com_modules') + && !$this->app->getIdentity()->authorise('core.edit.state', 'com_modules') + && ($moduleId && !$this->app->getIdentity()->authorise('core.edit.state', 'com_modules.module.' . $moduleId)) + ) { + $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error'); + echo new JsonResponse(); + $app->close(); + } + + $db = Factory::getDbo(); + $clientId = (int) $clientId; + $query = $db->getQuery(true) + ->select($db->quoteName(['position', 'ordering', 'title'])) + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->where($db->quoteName('position') . ' = :position') + ->order($db->quoteName('ordering')) + ->bind(':clientid', $clientId, ParameterType::INTEGER) + ->bind(':position', $position); + + $db->setQuery($query); + + try { + $orders = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return ''; + } + + $orders2 = array(); + $n = count($orders); + + if ($n > 0) { + for ($i = 0; $i < $n; $i++) { + if (!isset($orders2[$orders[$i]->position])) { + $orders2[$orders[$i]->position] = 0; + } + + $orders2[$orders[$i]->position]++; + $ord = $orders2[$orders[$i]->position]; + $title = Text::sprintf('COM_MODULES_OPTION_ORDER_POSITION', $ord, htmlspecialchars($orders[$i]->title, ENT_QUOTES, 'UTF-8')); + + $html[] = $orders[$i]->position . ',' . $ord . ',' . $title; + } + } else { + $html[] = $position . ',' . 1 . ',' . Text::_('JNONE'); + } + + echo new JsonResponse($html); + $app->close(); + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + $append .= '&client_id=' . $this->input->getInt('client_id'); + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&client_id=' . $this->input->getInt('client_id'); + + return $append; + } } diff --git a/administrator/components/com_modules/src/Controller/ModulesController.php b/administrator/components/com_modules/src/Controller/ModulesController.php index 005e5e8ffa0f3..f1845a65067e1 100644 --- a/administrator/components/com_modules/src/Controller/ModulesController.php +++ b/administrator/components/com_modules/src/Controller/ModulesController.php @@ -1,4 +1,5 @@ checkToken(); - - $pks = (array) $this->input->post->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $pks = array_filter($pks); - - try - { - if (empty($pks)) - { - throw new \Exception(Text::_('COM_MODULES_ERROR_NO_MODULES_SELECTED')); - } - - $model = $this->getModel(); - $model->duplicate($pks); - $this->setMessage(Text::plural('COM_MODULES_N_MODULES_DUPLICATED', count($pks))); - } - catch (\Exception $e) - { - $this->app->enqueueMessage($e->getMessage(), 'warning'); - } - - $this->setRedirect('index.php?option=com_modules&view=modules' . $this->getRedirectToListAppend()); - } - - /** - * Method to get a model object, loading it if required. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return object The model. - * - * @since 1.6 - */ - public function getModel($name = 'Module', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to get the number of frontend modules - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('Modules'); - - $model->setState('filter.state', 1); - $model->setState('filter.client_id', 0); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_MODULES_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_MODULES_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&client_id=' . $this->input->getInt('client_id'); - - return $append; - } + /** + * Method to clone an existing module. + * + * @return void + * + * @since 1.6 + */ + public function duplicate() + { + // Check for request forgeries + $this->checkToken(); + + $pks = (array) $this->input->post->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $pks = array_filter($pks); + + try { + if (empty($pks)) { + throw new \Exception(Text::_('COM_MODULES_ERROR_NO_MODULES_SELECTED')); + } + + $model = $this->getModel(); + $model->duplicate($pks); + $this->setMessage(Text::plural('COM_MODULES_N_MODULES_DUPLICATED', count($pks))); + } catch (\Exception $e) { + $this->app->enqueueMessage($e->getMessage(), 'warning'); + } + + $this->setRedirect('index.php?option=com_modules&view=modules' . $this->getRedirectToListAppend()); + } + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Module', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to get the number of frontend modules + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Modules'); + + $model->setState('filter.state', 1); + $model->setState('filter.client_id', 0); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_MODULES_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_MODULES_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&client_id=' . $this->input->getInt('client_id'); + + return $append; + } } diff --git a/administrator/components/com_modules/src/Extension/ModulesComponent.php b/administrator/components/com_modules/src/Extension/ModulesComponent.php index 167c8b8ff60fd..ff9e5aec9bc93 100644 --- a/administrator/components/com_modules/src/Extension/ModulesComponent.php +++ b/administrator/components/com_modules/src/Extension/ModulesComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('modules', new Modules); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('modules', new Modules()); + } } diff --git a/administrator/components/com_modules/src/Field/ModulesModuleField.php b/administrator/components/com_modules/src/Field/ModulesModuleField.php index f748ff6f55af0..f7a6723e6aea0 100644 --- a/administrator/components/com_modules/src/Field/ModulesModuleField.php +++ b/administrator/components/com_modules/src/Field/ModulesModuleField.php @@ -1,4 +1,5 @@ $name; - } + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 4.0.0 + */ + public function __get($name) + { + switch ($name) { + case 'client': + return $this->$name; + } - return parent::__get($name); - } + return parent::__get($name); + } - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 4.0.0 - */ - public function __set($name, $value) - { - switch ($name) - { - case 'client': - $this->$name = (string) $value; - break; + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 4.0.0 + */ + public function __set($name, $value) + { + switch ($name) { + case 'client': + $this->$name = (string) $value; + break; - default: - parent::__set($name, $value); - } - } + default: + parent::__set($name, $value); + } + } - /** - * Method to attach a Form object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see FormField::setup() - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $result = parent::setup($element, $value, $group); + /** + * Method to attach a Form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see FormField::setup() + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $result = parent::setup($element, $value, $group); - if ($result === true) - { - $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; - } + if ($result === true) { + $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; + } - return $result; - } + return $result; + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 3.4.2 - */ - public function getOptions() - { - $clientId = $this->client === 'administrator' ? 1 : 0; - $options = ModulesHelper::getModules($clientId); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.4.2 + */ + public function getOptions() + { + $clientId = $this->client === 'administrator' ? 1 : 0; + $options = ModulesHelper::getModules($clientId); - return array_merge(parent::getOptions(), $options); - } + return array_merge(parent::getOptions(), $options); + } } diff --git a/administrator/components/com_modules/src/Field/ModulesPositionField.php b/administrator/components/com_modules/src/Field/ModulesPositionField.php index e7e73d25b377d..6e05a65759522 100644 --- a/administrator/components/com_modules/src/Field/ModulesPositionField.php +++ b/administrator/components/com_modules/src/Field/ModulesPositionField.php @@ -1,4 +1,5 @@ $name; - } + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 4.0.0 + */ + public function __get($name) + { + switch ($name) { + case 'client': + return $this->$name; + } - return parent::__get($name); - } + return parent::__get($name); + } - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 4.0.0 - */ - public function __set($name, $value) - { - switch ($name) - { - case 'client': - $this->$name = (string) $value; - break; + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 4.0.0 + */ + public function __set($name, $value) + { + switch ($name) { + case 'client': + $this->$name = (string) $value; + break; - default: - parent::__set($name, $value); - } - } + default: + parent::__set($name, $value); + } + } - /** - * Method to attach a Form object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see FormField::setup() - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $result = parent::setup($element, $value, $group); + /** + * Method to attach a Form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see FormField::setup() + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $result = parent::setup($element, $value, $group); - if ($result === true) - { - $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; - } + if ($result === true) { + $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; + } - return $result; - } + return $result; + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 3.4.2 - */ - public function getOptions() - { - $clientId = $this->client === 'administrator' ? 1 : 0; - $options = ModulesHelper::getPositions($clientId); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.4.2 + */ + public function getOptions() + { + $clientId = $this->client === 'administrator' ? 1 : 0; + $options = ModulesHelper::getPositions($clientId); - return array_merge(parent::getOptions(), $options); - } + return array_merge(parent::getOptions(), $options); + } } diff --git a/administrator/components/com_modules/src/Field/ModulesPositioneditField.php b/administrator/components/com_modules/src/Field/ModulesPositioneditField.php index 040f5f3d98eff..1e04576ae1def 100644 --- a/administrator/components/com_modules/src/Field/ModulesPositioneditField.php +++ b/administrator/components/com_modules/src/Field/ModulesPositioneditField.php @@ -1,4 +1,5 @@ $name; - } - - return parent::__get($name); - } - - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 4.0.0 - */ - public function __set($name, $value) - { - switch ($name) - { - case 'client': - $this->$name = (string) $value; - break; - - default: - parent::__set($name, $value); - } - } - - /** - * Method to attach a Form object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see FormField::setup() - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $result = parent::setup($element, $value, $group); - - if ($result === true) - { - $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; - } - - return $result; - } - - /** - * Method to get the field input markup. - * - * @return string The field input markup. - * - * @since 4.0.0 - */ - protected function getInput() - { - $data = $this->getLayoutData(); - - $clientId = $this->client === 'administrator' ? 1 : 0; - $positions = HTMLHelper::_('modules.positions', $clientId, 1, $this->value); - - $data['client'] = $clientId; - $data['positions'] = $positions; - - $renderer = $this->getRenderer($this->layout); - $renderer->setComponent('com_modules'); - $renderer->setClient(1); - - return $renderer->render($data); - } + /** + * The form field type. + * + * @var string + * @since 4.0.0 + */ + protected $type = 'ModulesPositionedit'; + + /** + * Name of the layout being used to render the field + * + * @var string + * @since 4.0.0 + */ + protected $layout = 'joomla.form.field.modulespositionedit'; + + /** + * Client name. + * + * @var string + * @since 4.0.0 + */ + protected $client; + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 4.0.0 + */ + public function __get($name) + { + switch ($name) { + case 'client': + return $this->$name; + } + + return parent::__get($name); + } + + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 4.0.0 + */ + public function __set($name, $value) + { + switch ($name) { + case 'client': + $this->$name = (string) $value; + break; + + default: + parent::__set($name, $value); + } + } + + /** + * Method to attach a Form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see FormField::setup() + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $result = parent::setup($element, $value, $group); + + if ($result === true) { + $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; + } + + return $result; + } + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 4.0.0 + */ + protected function getInput() + { + $data = $this->getLayoutData(); + + $clientId = $this->client === 'administrator' ? 1 : 0; + $positions = HTMLHelper::_('modules.positions', $clientId, 1, $this->value); + + $data['client'] = $clientId; + $data['positions'] = $positions; + + $renderer = $this->getRenderer($this->layout); + $renderer->setComponent('com_modules'); + $renderer->setClient(1); + + return $renderer->render($data); + } } diff --git a/administrator/components/com_modules/src/Helper/ModulesHelper.php b/administrator/components/com_modules/src/Helper/ModulesHelper.php index 6657d2f1b988a..49a19886b36a8 100644 --- a/administrator/components/com_modules/src/Helper/ModulesHelper.php +++ b/administrator/components/com_modules/src/Helper/ModulesHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('DISTINCT ' . $db->quoteName('position')) - ->from($db->quoteName('#__modules')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->order($db->quoteName('position')) - ->bind(':clientid', $clientId, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $positions = $db->loadColumn(); - $positions = is_array($positions) ? $positions : array(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return; - } - - // Build the list - $options = array(); - - foreach ($positions as $position) - { - if (!$position && !$editPositions) - { - $options[] = HTMLHelper::_('select.option', 'none', Text::_('COM_MODULES_NONE')); - } - elseif (!$position) - { - $options[] = HTMLHelper::_('select.option', '', Text::_('COM_MODULES_NONE')); - } - else - { - $options[] = HTMLHelper::_('select.option', $position, $position); - } - } - - return $options; - } - - /** - * Return a list of templates - * - * @param integer $clientId Client ID - * @param string $state State - * @param string $template Template name - * - * @return array List of templates - */ - public static function getTemplates($clientId = 0, $state = '', $template = '') - { - $db = Factory::getDbo(); - $clientId = (int) $clientId; - - // Get the database object and a new query object. - $query = $db->getQuery(true); - - // Build the query. - $query->select($db->quoteName(['element', 'name', 'enabled'])) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->where($db->quoteName('type') . ' = ' . $db->quote('template')); - - if ($state != '') - { - $query->where($db->quoteName('enabled') . ' = :state') - ->bind(':state', $state); - } - - if ($template != '') - { - $query->where($db->quoteName('element') . ' = :element') - ->bind(':element', $template); - } - - $query->bind(':clientid', $clientId, ParameterType::INTEGER); - - // Set the query and load the templates. - $db->setQuery($query); - $templates = $db->loadObjectList('element'); - - return $templates; - } - - /** - * Get a list of the unique modules installed in the client application. - * - * @param int $clientId The client id. - * - * @return array Array of unique modules - */ - public static function getModules($clientId) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('element AS value, name AS text') - ->from('#__extensions as e') - ->where('e.client_id = ' . (int) $clientId) - ->where('type = ' . $db->quote('module')) - ->join('LEFT', '#__modules as m ON m.module=e.element AND m.client_id=e.client_id') - ->where('m.module IS NOT NULL') - ->group('element,name'); - - $db->setQuery($query); - $modules = $db->loadObjectList(); - $lang = Factory::getLanguage(); - - foreach ($modules as $i => $module) - { - $extension = $module->value; - $path = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; - $source = $path . "/modules/$extension"; - $lang->load("$extension.sys", $path) - || $lang->load("$extension.sys", $source); - $modules[$i]->text = Text::_($module->text); - } - - $modules = ArrayHelper::sortObjects($modules, 'text', 1, true, true); - - return $modules; - } - - /** - * Get a list of the assignment options for modules to menus. - * - * @param int $clientId The client id. - * - * @return array - */ - public static function getAssignmentOptions($clientId) - { - $options = array(); - $options[] = HTMLHelper::_('select.option', '0', 'COM_MODULES_OPTION_MENU_ALL'); - $options[] = HTMLHelper::_('select.option', '-', 'COM_MODULES_OPTION_MENU_NONE'); - - if ($clientId == 0) - { - $options[] = HTMLHelper::_('select.option', '1', 'COM_MODULES_OPTION_MENU_INCLUDE'); - $options[] = HTMLHelper::_('select.option', '-1', 'COM_MODULES_OPTION_MENU_EXCLUDE'); - } - - return $options; - } - - /** - * Return a translated module position name - * - * @param integer $clientId Application client id 0: site | 1: admin - * @param string $template Template name - * @param string $position Position name - * - * @return string Return a translated position name - * - * @since 3.0 - */ - public static function getTranslatedModulePosition($clientId, $template, $position) - { - // Template translation - $lang = Factory::getLanguage(); - $path = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; - - $loaded = $lang->getPaths('tpl_' . $template . '.sys'); - - // Only load the template's language file if it hasn't been already - if (!$loaded) - { - $lang->load('tpl_' . $template . '.sys', $path, null, false, false) - || $lang->load('tpl_' . $template . '.sys', $path . '/templates/' . $template, null, false, false) - || $lang->load('tpl_' . $template . '.sys', $path, $lang->getDefault(), false, false) - || $lang->load('tpl_' . $template . '.sys', $path . '/templates/' . $template, $lang->getDefault(), false, false); - } - - $langKey = strtoupper('TPL_' . $template . '_POSITION_' . $position); - $text = Text::_($langKey); - - // Avoid untranslated strings - if (!self::isTranslatedText($langKey, $text)) - { - // Modules component translation - $langKey = strtoupper('COM_MODULES_POSITION_' . $position); - $text = Text::_($langKey); - - // Avoid untranslated strings - if (!self::isTranslatedText($langKey, $text)) - { - // Try to humanize the position name - $text = ucfirst(preg_replace('/^' . $template . '\-/', '', $position)); - $text = ucwords(str_replace(array('-', '_'), ' ', $text)); - } - } - - return $text; - } - - /** - * Check if the string was translated - * - * @param string $langKey Language file text key - * @param string $text The "translated" text to be checked - * - * @return boolean Return true for translated text - * - * @since 3.0 - */ - public static function isTranslatedText($langKey, $text) - { - return $text !== $langKey; - } - - /** - * Create and return a new Option - * - * @param string $value The option value [optional] - * @param string $text The option text [optional] - * - * @return object The option as an object (\stdClass instance) - * - * @since 3.0 - */ - public static function createOption($value = '', $text = '') - { - if (empty($text)) - { - $text = $value; - } - - $option = new \stdClass; - $option->value = $value; - $option->text = $text; - - return $option; - } - - /** - * Create and return a new Option Group - * - * @param string $label Value and label for group [optional] - * @param array $options Array of options to insert into group [optional] - * - * @return array Return the new group as an array - * - * @since 3.0 - */ - public static function createOptionGroup($label = '', $options = array()) - { - $group = array(); - $group['value'] = $label; - $group['text'] = $label; - $group['items'] = $options; - - return $group; - } + /** + * Get a list of filter options for the state of a module. + * + * @return array An array of \JHtmlOption elements. + */ + public static function getStateOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JPUBLISHED')); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JUNPUBLISHED')); + $options[] = HTMLHelper::_('select.option', '-2', Text::_('JTRASHED')); + $options[] = HTMLHelper::_('select.option', '*', Text::_('JALL')); + + return $options; + } + + /** + * Get a list of filter options for the application clients. + * + * @return array An array of \JHtmlOption elements. + */ + public static function getClientOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JSITE')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JADMINISTRATOR')); + + return $options; + } + + /** + * Get a list of modules positions + * + * @param integer $clientId Client ID + * @param boolean $editPositions Allow to edit the positions + * + * @return array A list of positions + */ + public static function getPositions($clientId, $editPositions = false) + { + $db = Factory::getDbo(); + $clientId = (int) $clientId; + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('position')) + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->order($db->quoteName('position')) + ->bind(':clientid', $clientId, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $positions = $db->loadColumn(); + $positions = is_array($positions) ? $positions : array(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return; + } + + // Build the list + $options = array(); + + foreach ($positions as $position) { + if (!$position && !$editPositions) { + $options[] = HTMLHelper::_('select.option', 'none', Text::_('COM_MODULES_NONE')); + } elseif (!$position) { + $options[] = HTMLHelper::_('select.option', '', Text::_('COM_MODULES_NONE')); + } else { + $options[] = HTMLHelper::_('select.option', $position, $position); + } + } + + return $options; + } + + /** + * Return a list of templates + * + * @param integer $clientId Client ID + * @param string $state State + * @param string $template Template name + * + * @return array List of templates + */ + public static function getTemplates($clientId = 0, $state = '', $template = '') + { + $db = Factory::getDbo(); + $clientId = (int) $clientId; + + // Get the database object and a new query object. + $query = $db->getQuery(true); + + // Build the query. + $query->select($db->quoteName(['element', 'name', 'enabled'])) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->where($db->quoteName('type') . ' = ' . $db->quote('template')); + + if ($state != '') { + $query->where($db->quoteName('enabled') . ' = :state') + ->bind(':state', $state); + } + + if ($template != '') { + $query->where($db->quoteName('element') . ' = :element') + ->bind(':element', $template); + } + + $query->bind(':clientid', $clientId, ParameterType::INTEGER); + + // Set the query and load the templates. + $db->setQuery($query); + $templates = $db->loadObjectList('element'); + + return $templates; + } + + /** + * Get a list of the unique modules installed in the client application. + * + * @param int $clientId The client id. + * + * @return array Array of unique modules + */ + public static function getModules($clientId) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('element AS value, name AS text') + ->from('#__extensions as e') + ->where('e.client_id = ' . (int) $clientId) + ->where('type = ' . $db->quote('module')) + ->join('LEFT', '#__modules as m ON m.module=e.element AND m.client_id=e.client_id') + ->where('m.module IS NOT NULL') + ->group('element,name'); + + $db->setQuery($query); + $modules = $db->loadObjectList(); + $lang = Factory::getLanguage(); + + foreach ($modules as $i => $module) { + $extension = $module->value; + $path = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; + $source = $path . "/modules/$extension"; + $lang->load("$extension.sys", $path) + || $lang->load("$extension.sys", $source); + $modules[$i]->text = Text::_($module->text); + } + + $modules = ArrayHelper::sortObjects($modules, 'text', 1, true, true); + + return $modules; + } + + /** + * Get a list of the assignment options for modules to menus. + * + * @param int $clientId The client id. + * + * @return array + */ + public static function getAssignmentOptions($clientId) + { + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', 'COM_MODULES_OPTION_MENU_ALL'); + $options[] = HTMLHelper::_('select.option', '-', 'COM_MODULES_OPTION_MENU_NONE'); + + if ($clientId == 0) { + $options[] = HTMLHelper::_('select.option', '1', 'COM_MODULES_OPTION_MENU_INCLUDE'); + $options[] = HTMLHelper::_('select.option', '-1', 'COM_MODULES_OPTION_MENU_EXCLUDE'); + } + + return $options; + } + + /** + * Return a translated module position name + * + * @param integer $clientId Application client id 0: site | 1: admin + * @param string $template Template name + * @param string $position Position name + * + * @return string Return a translated position name + * + * @since 3.0 + */ + public static function getTranslatedModulePosition($clientId, $template, $position) + { + // Template translation + $lang = Factory::getLanguage(); + $path = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; + + $loaded = $lang->getPaths('tpl_' . $template . '.sys'); + + // Only load the template's language file if it hasn't been already + if (!$loaded) { + $lang->load('tpl_' . $template . '.sys', $path, null, false, false) + || $lang->load('tpl_' . $template . '.sys', $path . '/templates/' . $template, null, false, false) + || $lang->load('tpl_' . $template . '.sys', $path, $lang->getDefault(), false, false) + || $lang->load('tpl_' . $template . '.sys', $path . '/templates/' . $template, $lang->getDefault(), false, false); + } + + $langKey = strtoupper('TPL_' . $template . '_POSITION_' . $position); + $text = Text::_($langKey); + + // Avoid untranslated strings + if (!self::isTranslatedText($langKey, $text)) { + // Modules component translation + $langKey = strtoupper('COM_MODULES_POSITION_' . $position); + $text = Text::_($langKey); + + // Avoid untranslated strings + if (!self::isTranslatedText($langKey, $text)) { + // Try to humanize the position name + $text = ucfirst(preg_replace('/^' . $template . '\-/', '', $position)); + $text = ucwords(str_replace(array('-', '_'), ' ', $text)); + } + } + + return $text; + } + + /** + * Check if the string was translated + * + * @param string $langKey Language file text key + * @param string $text The "translated" text to be checked + * + * @return boolean Return true for translated text + * + * @since 3.0 + */ + public static function isTranslatedText($langKey, $text) + { + return $text !== $langKey; + } + + /** + * Create and return a new Option + * + * @param string $value The option value [optional] + * @param string $text The option text [optional] + * + * @return object The option as an object (\stdClass instance) + * + * @since 3.0 + */ + public static function createOption($value = '', $text = '') + { + if (empty($text)) { + $text = $value; + } + + $option = new \stdClass(); + $option->value = $value; + $option->text = $text; + + return $option; + } + + /** + * Create and return a new Option Group + * + * @param string $label Value and label for group [optional] + * @param array $options Array of options to insert into group [optional] + * + * @return array Return the new group as an array + * + * @since 3.0 + */ + public static function createOptionGroup($label = '', $options = array()) + { + $group = array(); + $group['value'] = $label; + $group['text'] = $label; + $group['items'] = $options; + + return $group; + } } diff --git a/administrator/components/com_modules/src/Model/ModuleModel.php b/administrator/components/com_modules/src/Model/ModuleModel.php index fc5c49137da52..63f6527c6fa2c 100644 --- a/administrator/components/com_modules/src/Model/ModuleModel.php +++ b/administrator/components/com_modules/src/Model/ModuleModel.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage', - ); - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - */ - public function __construct($config = array()) - { - $config = array_merge( - array( - 'event_after_delete' => 'onExtensionAfterDelete', - 'event_after_save' => 'onExtensionAfterSave', - 'event_before_delete' => 'onExtensionBeforeDelete', - 'event_before_save' => 'onExtensionBeforeSave', - 'events_map' => array( - 'save' => 'extension', - 'delete' => 'extension' - ) - ), $config - ); - - parent::__construct($config); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('id'); - - if (!$pk) - { - if ($extensionId = (int) $app->getUserState('com_modules.add.module.extension_id')) - { - $this->setState('extension.id', $extensionId); - } - } - - $this->setState('module.id', $pk); - - // Load the parameters. - $params = ComponentHelper::getParams('com_modules'); - $this->setState('params', $params); - } - - /** - * Batch copy modules to a new position or current. - * - * @param integer $value The new value matching a module position. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - protected function batchCopy($value, $pks, $contexts) - { - // Set the variables - $user = Factory::getUser(); - $table = $this->getTable(); - $newIds = array(); - - foreach ($pks as $pk) - { - if ($user->authorise('core.create', 'com_modules')) - { - $table->reset(); - $table->load($pk); - - // Set the new position - if ($value == 'noposition') - { - $position = ''; - } - elseif ($value == 'nochange') - { - $position = $table->position; - } - else - { - $position = $value; - } - - $table->position = $position; - - // Copy of the Asset ID - $oldAssetId = $table->asset_id; - - // Alter the title if necessary - $data = $this->generateNewTitle(0, $table->title, $table->position); - $table->title = $data['0']; - - // Reset the ID because we are making a copy - $table->id = 0; - - // Unpublish the new module - $table->published = 0; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Get the new item ID - $newId = $table->get('id'); - - // Add the new ID to the array - $newIds[$pk] = $newId; - - // Now we need to handle the module assignments - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('menuid')) - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = :moduleid') - ->bind(':moduleid', $pk, ParameterType::INTEGER); - $db->setQuery($query); - $menus = $db->loadColumn(); - - // Insert the new records into the table - foreach ($menus as $i => $menu) - { - $query->clear() - ->insert($db->quoteName('#__modules_menu')) - ->columns($db->quoteName(['moduleid', 'menuid'])) - ->values(implode(', ', [':newid' . $i, ':menu' . $i])) - ->bind(':newid' . $i, $newId, ParameterType::INTEGER) - ->bind(':menu' . $i, $menu, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - } - - // Copy rules - $query->clear() - ->update($db->quoteName('#__assets', 't')) - ->join('INNER', $db->quoteName('#__assets', 's') . - ' ON ' . $db->quoteName('s.id') . ' = ' . $oldAssetId - ) - ->set($db->quoteName('t.rules') . ' = ' . $db->quoteName('s.rules')) - ->where($db->quoteName('t.id') . ' = ' . $table->asset_id); - - $db->setQuery($query)->execute(); - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_CREATE')); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return $newIds; - } - - /** - * Batch move modules to a new position or current. - * - * @param integer $value The new value matching a module position. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - protected function batchMove($value, $pks, $contexts) - { - // Set the variables - $user = Factory::getUser(); - $table = $this->getTable(); - - foreach ($pks as $pk) - { - if ($user->authorise('core.edit', 'com_modules')) - { - $table->reset(); - $table->load($pk); - - // Set the new position - if ($value == 'noposition') - { - $position = ''; - } - elseif ($value == 'nochange') - { - $position = $table->position; - } - else - { - $position = $value; - } - - $table->position = $position; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to test whether a record can have its state edited. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 3.2 - */ - protected function canEditState($record) - { - // Check for existing module. - if (!empty($record->id)) - { - return Factory::getUser()->authorise('core.edit.state', 'com_modules.module.' . (int) $record->id); - } - - // Default to component settings if module not known. - return parent::canEditState($record); - } - - /** - * Method to delete rows. - * - * @param array &$pks An array of item ids. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function delete(&$pks) - { - $app = Factory::getApplication(); - $pks = (array) $pks; - $user = Factory::getUser(); - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - - // Include the plugins for the on delete events. - PluginHelper::importPlugin($this->events_map['delete']); - - // Iterate the items to delete each one. - foreach ($pks as $pk) - { - if ($table->load($pk)) - { - // Access checks. - if (!$user->authorise('core.delete', 'com_modules.module.' . (int) $pk) || $table->published != -2) - { - Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); - - return; - } - - // Trigger the before delete event. - $result = $app->triggerEvent($this->event_before_delete, array($context, $table)); - - if (in_array(false, $result, true) || !$table->delete($pk)) - { - throw new \Exception($table->getError()); - } - else - { - // Delete the menu assignments - $pk = (int) $pk; - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = :moduleid') - ->bind(':moduleid', $pk, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - // Trigger the after delete event. - $app->triggerEvent($this->event_after_delete, array($context, $table)); - } - - // Clear module cache - parent::cleanCache($table->module); - } - else - { - throw new \Exception($table->getError()); - } - } - - // Clear modules cache - $this->cleanCache(); - - return true; - } - - /** - * Method to duplicate modules. - * - * @param array &$pks An array of primary key IDs. - * - * @return boolean Boolean true on success - * - * @since 1.6 - * @throws \Exception - */ - public function duplicate(&$pks) - { - $user = Factory::getUser(); - $db = $this->getDatabase(); - - // Access checks. - if (!$user->authorise('core.create', 'com_modules')) - { - throw new \Exception(Text::_('JERROR_CORE_CREATE_NOT_PERMITTED')); - } - - $table = $this->getTable(); - - foreach ($pks as $pk) - { - if ($table->load($pk, true)) - { - // Reset the id to create a new record. - $table->id = 0; - - // Alter the title. - $m = null; - - if (preg_match('#\((\d+)\)$#', $table->title, $m)) - { - $table->title = preg_replace('#\(\d+\)$#', '(' . ($m[1] + 1) . ')', $table->title); - } - - $data = $this->generateNewTitle(0, $table->title, $table->position); - $table->title = $data[0]; - - // Unpublish duplicate module - $table->published = 0; - - if (!$table->check() || !$table->store()) - { - throw new \Exception($table->getError()); - } - - $pk = (int) $pk; - $query = $db->getQuery(true) - ->select($db->quoteName('menuid')) - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = :moduleid') - ->bind(':moduleid', $pk, ParameterType::INTEGER); - - $db->setQuery($query); - $rows = $db->loadColumn(); - - foreach ($rows as $menuid) - { - $tuples[] = (int) $table->id . ',' . (int) $menuid; - } - } - else - { - throw new \Exception($table->getError()); - } - } - - if (!empty($tuples)) - { - // Module-Menu Mapping: Do it in one query - $query = $db->getQuery(true) - ->insert($db->quoteName('#__modules_menu')) - ->columns($db->quoteName(array('moduleid', 'menuid'))) - ->values($tuples); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - } - - // Clear modules cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the title. - * - * @param integer $categoryId The id of the category. Not used here. - * @param string $title The title. - * @param string $position The position. - * - * @return array Contains the modified title. - * - * @since 2.5 - */ - protected function generateNewTitle($categoryId, $title, $position) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('position' => $position, 'title' => $title))) - { - $title = StringHelper::increment($title); - } - - return array($title); - } - - /** - * Method to get the client object - * - * @return void - * - * @since 1.6 - */ - public function &getClient() - { - return $this->_client; - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // The folder and element vars are passed when saving the form. - if (empty($data)) - { - $item = $this->getItem(); - $clientId = $item->client_id; - $module = $item->module; - $id = $item->id; - } - else - { - $clientId = ArrayHelper::getValue($data, 'client_id'); - $module = ArrayHelper::getValue($data, 'module'); - $id = ArrayHelper::getValue($data, 'id'); - } - - // Add the default fields directory - $baseFolder = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; - Form::addFieldPath($baseFolder . '/modules/' . $module . '/field'); - - // These variables are used to add data from the plugin XML files. - $this->setState('item.client_id', $clientId); - $this->setState('item.module', $module); - - // Get the form. - if ($clientId == 1) - { - $form = $this->loadForm('com_modules.module.admin', 'moduleadmin', array('control' => 'jform', 'load_data' => $loadData), true); - - // Display language field to filter admin custom menus per language - if (!ModuleHelper::isAdminMultilang()) - { - $form->setFieldAttribute('language', 'type', 'hidden'); - } - } - else - { - $form = $this->loadForm('com_modules.module', 'module', array('control' => 'jform', 'load_data' => $loadData), true); - } - - if (empty($form)) - { - return false; - } - - $user = Factory::getUser(); - - /** - * Check for existing module - * Modify the form based on Edit State access controls. - */ - if ($id != 0 && (!$user->authorise('core.edit.state', 'com_modules.module.' . (int) $id)) - || ($id == 0 && !$user->authorise('core.edit.state', 'com_modules')) ) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - $form->setFieldAttribute('publish_up', 'disabled', 'true'); - $form->setFieldAttribute('publish_down', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - $form->setFieldAttribute('publish_up', 'filter', 'unset'); - $form->setFieldAttribute('publish_down', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $app = Factory::getApplication(); - - // Check the session for previously entered form data. - $data = $app->getUserState('com_modules.edit.module.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Pre-select some filters (Status, Module Position, Language, Access Level) in edit form if those have been selected in Module Manager - if (!$data->id) - { - $clientId = $app->input->getInt('client_id', 0); - $filters = (array) $app->getUserState('com_modules.modules.' . $clientId . '.filter'); - $data->set('published', $app->input->getInt('published', ((isset($filters['state']) && $filters['state'] !== '') ? $filters['state'] : null))); - $data->set('position', $app->input->getInt('position', (!empty($filters['position']) ? $filters['position'] : null))); - $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); - $data->set('access', $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access')))); - } - - // Avoid to delete params of a second module opened in a new browser tab while new one is not saved yet. - if (empty($data->params)) - { - // This allows us to inject parameter settings into a new module. - $params = $app->getUserState('com_modules.add.module.params'); - - if (is_array($params)) - { - $data->set('params', $params); - } - } - } - - $this->preprocessData('com_modules.module', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - $pk = (!empty($pk)) ? (int) $pk : (int) $this->getState('module.id'); - $db = $this->getDatabase(); - - if (!isset($this->_cache[$pk])) - { - // Get a row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load($pk); - - // Check for a table object error. - if ($return === false && $error = $table->getError()) - { - $this->setError($error); - - return false; - } - - // Check if we are creating a new extension. - if (empty($pk)) - { - if ($extensionId = (int) $this->getState('extension.id')) - { - $query = $db->getQuery(true) - ->select($db->quoteName(['element', 'client_id'])) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('extension_id') . ' = :extensionid') - ->where($db->quoteName('type') . ' = ' . $db->quote('module')) - ->bind(':extensionid', $extensionId, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $extension = $db->loadObject(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (empty($extension)) - { - $this->setError('COM_MODULES_ERROR_CANNOT_FIND_MODULE'); - - return false; - } - - // Extension found, prime some module values. - $table->module = $extension->element; - $table->client_id = $extension->client_id; - } - else - { - Factory::getApplication()->redirect(Route::_('index.php?option=com_modules&view=modules', false)); - - return false; - } - } - - // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. - $properties = $table->getProperties(1); - $this->_cache[$pk] = ArrayHelper::toObject($properties, CMSObject::class); - - // Convert the params field to an array. - $registry = new Registry($table->params); - $this->_cache[$pk]->params = $registry->toArray(); - - // Determine the page assignment mode. - $query = $db->getQuery(true) - ->select($db->quoteName('menuid')) - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = :moduleid') - ->bind(':moduleid', $pk, ParameterType::INTEGER); - $db->setQuery($query); - $assigned = $db->loadColumn(); - - if (empty($pk)) - { - // If this is a new module, assign to all pages. - $assignment = 0; - } - elseif (empty($assigned)) - { - // For an existing module it is assigned to none. - $assignment = '-'; - } - else - { - if ($assigned[0] > 0) - { - $assignment = 1; - } - elseif ($assigned[0] < 0) - { - $assignment = -1; - } - else - { - $assignment = 0; - } - } - - $this->_cache[$pk]->assigned = $assigned; - $this->_cache[$pk]->assignment = $assignment; - - // Get the module XML. - $client = ApplicationHelper::getClientInfo($table->client_id); - $path = Path::clean($client->path . '/modules/' . $table->module . '/' . $table->module . '.xml'); - - if (file_exists($path)) - { - $this->_cache[$pk]->xml = simplexml_load_file($path); - } - else - { - $this->_cache[$pk]->xml = null; - } - } - - return $this->_cache[$pk]; - } - - /** - * Get the necessary data to load an item help screen. - * - * @return object An object with key, url, and local properties for loading the item help screen. - * - * @since 1.6 - */ - public function getHelp() - { - return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); - } - - /** - * Returns a reference to the a Table object, always creating it. - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - * - * @since 1.6 - */ - public function getTable($type = 'Module', $prefix = 'JTable', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - - /** - * Prepare and sanitise the table prior to saving. - * - * @param Table $table The database object - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - $table->title = htmlspecialchars_decode($table->title, ENT_QUOTES); - $table->position = trim($table->position); - } - - /** - * Method to preprocess the form - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error loading the form. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $lang = Factory::getLanguage(); - $clientId = $this->getState('item.client_id'); - $module = $this->getState('item.module'); - - $client = ApplicationHelper::getClientInfo($clientId); - $formFile = Path::clean($client->path . '/modules/' . $module . '/' . $module . '.xml'); - - // Load the core and/or local language file(s). - $lang->load($module, $client->path) - || $lang->load($module, $client->path . '/modules/' . $module); - - if (file_exists($formFile)) - { - // Get the module form. - if (!$form->loadFile($formFile, false, '//config')) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Get the help data from the XML file if present. - $help = $xml->xpath('/extension/help'); - - if (!empty($help)) - { - $helpKey = trim((string) $help[0]['key']); - $helpURL = trim((string) $help[0]['url']); - - $this->helpKey = $helpKey ?: $this->helpKey; - $this->helpURL = $helpURL ?: $this->helpURL; - } - } - - // Load the default advanced params - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/models/forms'); - $form->loadFile('advanced', false); - - // Load chrome specific params for global files - $chromePath = JPATH_SITE . '/layouts/chromes'; - $chromeFormFiles = Folder::files($chromePath, '.*\.xml'); - - if ($chromeFormFiles) - { - Form::addFormPath($chromePath); - - foreach ($chromeFormFiles as $formFile) - { - $form->loadFile(basename($formFile, '.xml'), false); - } - } - - // Load chrome specific params for template files - $templates = ModulesHelper::getTemplates($clientId); - - foreach ($templates as $template) - { - $chromePath = $client->path . '/templates/' . $template->element . '/html/layouts/chromes'; - - // Skip if there is no chrome folder in that template. - if (!is_dir($chromePath)) - { - continue; - } - - $chromeFormFiles = Folder::files($chromePath, '.*\.xml'); - - if ($chromeFormFiles) - { - Form::addFormPath($chromePath); - - foreach ($chromeFormFiles as $formFile) - { - $form->loadFile(basename($formFile, '.xml'), false); - } - } - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Loads ContentHelper for filters before validating data. - * - * @param object $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the group(defaults to null). - * - * @return mixed Array of filtered data if valid, false otherwise. - * - * @since 1.1 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', 'com_modules')) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $input = Factory::getApplication()->input; - $table = $this->getTable(); - $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('module.id'); - $isNew = true; - $context = $this->option . '.' . $this->name; - - // Include the plugins for the save event. - PluginHelper::importPlugin($this->events_map['save']); - - // Load the row if saving an existing record. - if ($pk > 0) - { - $table->load($pk); - $isNew = false; - } - - // Alter the title and published state for Save as Copy - if ($input->get('task') == 'save2copy') - { - $orig_table = clone $this->getTable(); - $orig_table->load((int) $input->getInt('id')); - $data['published'] = 0; - - if ($data['title'] == $orig_table->title) - { - $data['title'] = StringHelper::increment($data['title']); - } - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Prepare the row for saving - $this->prepareTable($table); - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Process the menu link mappings. - $assignment = $data['assignment'] ?? 0; - - $table->id = (int) $table->id; - - // Delete old module to menu item associations - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = :moduleid') - ->bind(':moduleid', $table->id, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // If the assignment is numeric, then something is selected (otherwise it's none). - if (is_numeric($assignment)) - { - // Variable is numeric, but could be a string. - $assignment = (int) $assignment; - - // Logic check: if no module excluded then convert to display on all. - if ($assignment == -1 && empty($data['assigned'])) - { - $assignment = 0; - } - - // Check needed to stop a module being assigned to `All` - // and other menu items resulting in a module being displayed twice. - if ($assignment === 0) - { - // Assign new module to `all` menu item associations. - $query->clear() - ->insert($db->quoteName('#__modules_menu')) - ->columns($db->quoteName(['moduleid', 'menuid'])) - ->values(implode(', ', [':moduleid', 0])) - ->bind(':moduleid', $table->id, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - elseif (!empty($data['assigned'])) - { - // Get the sign of the number. - $sign = $assignment < 0 ? -1 : 1; - - $query->clear() - ->insert($db->quoteName('#__modules_menu')) - ->columns($db->quoteName(array('moduleid', 'menuid'))); - - foreach ($data['assigned'] as &$pk) - { - $query->values((int) $table->id . ',' . (int) $pk * $sign); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew)); - - // Compute the extension id of this module in case the controller wants it. - $query->clear() - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions', 'e')) - ->join( - 'LEFT', - $db->quoteName('#__modules', 'm') . ' ON ' . $db->quoteName('e.client_id') . ' = ' . (int) $table->client_id . - ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('m.module') - ) - ->where($db->quoteName('m.id') . ' = :id') - ->bind(':id', $table->id, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $extensionId = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - $this->setState('module.extension_id', $extensionId); - $this->setState('module.id', $table->id); - - // Clear modules cache - $this->cleanCache(); - - // Clean module cache - parent::cleanCache($table->module); - - return true; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - $db = $this->getDatabase(); - - return [ - $db->quoteName('client_id') . ' = ' . (int) $table->client_id, - $db->quoteName('position') . ' = ' . $db->quote($table->position), - ]; - } - - /** - * Custom clean cache method for different clients - * - * @param string $group The name of the plugin group to import (defaults to null). - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_modules'); - } + /** + * The type alias for this content type. + * + * @var string + * @since 3.4 + */ + public $typeAlias = 'com_modules.module'; + + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_MODULES'; + + /** + * @var string The help screen key for the module. + * @since 1.6 + */ + protected $helpKey = ''; + + /** + * @var string The help screen base URL for the module. + * @since 1.6 + */ + protected $helpURL; + + /** + * Batch copy/move command. If set to false, + * the batch copy/move command is not supported + * + * @var string + */ + protected $batch_copymove = 'position_id'; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage', + ); + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + */ + public function __construct($config = array()) + { + $config = array_merge( + array( + 'event_after_delete' => 'onExtensionAfterDelete', + 'event_after_save' => 'onExtensionAfterSave', + 'event_before_delete' => 'onExtensionBeforeDelete', + 'event_before_save' => 'onExtensionBeforeSave', + 'events_map' => array( + 'save' => 'extension', + 'delete' => 'extension' + ) + ), + $config + ); + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + + if (!$pk) { + if ($extensionId = (int) $app->getUserState('com_modules.add.module.extension_id')) { + $this->setState('extension.id', $extensionId); + } + } + + $this->setState('module.id', $pk); + + // Load the parameters. + $params = ComponentHelper::getParams('com_modules'); + $this->setState('params', $params); + } + + /** + * Batch copy modules to a new position or current. + * + * @param integer $value The new value matching a module position. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + protected function batchCopy($value, $pks, $contexts) + { + // Set the variables + $user = Factory::getUser(); + $table = $this->getTable(); + $newIds = array(); + + foreach ($pks as $pk) { + if ($user->authorise('core.create', 'com_modules')) { + $table->reset(); + $table->load($pk); + + // Set the new position + if ($value == 'noposition') { + $position = ''; + } elseif ($value == 'nochange') { + $position = $table->position; + } else { + $position = $value; + } + + $table->position = $position; + + // Copy of the Asset ID + $oldAssetId = $table->asset_id; + + // Alter the title if necessary + $data = $this->generateNewTitle(0, $table->title, $table->position); + $table->title = $data['0']; + + // Reset the ID because we are making a copy + $table->id = 0; + + // Unpublish the new module + $table->published = 0; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Get the new item ID + $newId = $table->get('id'); + + // Add the new ID to the array + $newIds[$pk] = $newId; + + // Now we need to handle the module assignments + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('menuid')) + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = :moduleid') + ->bind(':moduleid', $pk, ParameterType::INTEGER); + $db->setQuery($query); + $menus = $db->loadColumn(); + + // Insert the new records into the table + foreach ($menus as $i => $menu) { + $query->clear() + ->insert($db->quoteName('#__modules_menu')) + ->columns($db->quoteName(['moduleid', 'menuid'])) + ->values(implode(', ', [':newid' . $i, ':menu' . $i])) + ->bind(':newid' . $i, $newId, ParameterType::INTEGER) + ->bind(':menu' . $i, $menu, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + } + + // Copy rules + $query->clear() + ->update($db->quoteName('#__assets', 't')) + ->join('INNER', $db->quoteName('#__assets', 's') . + ' ON ' . $db->quoteName('s.id') . ' = ' . $oldAssetId) + ->set($db->quoteName('t.rules') . ' = ' . $db->quoteName('s.rules')) + ->where($db->quoteName('t.id') . ' = ' . $table->asset_id); + + $db->setQuery($query)->execute(); + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_CREATE')); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return $newIds; + } + + /** + * Batch move modules to a new position or current. + * + * @param integer $value The new value matching a module position. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + protected function batchMove($value, $pks, $contexts) + { + // Set the variables + $user = Factory::getUser(); + $table = $this->getTable(); + + foreach ($pks as $pk) { + if ($user->authorise('core.edit', 'com_modules')) { + $table->reset(); + $table->load($pk); + + // Set the new position + if ($value == 'noposition') { + $position = ''; + } elseif ($value == 'nochange') { + $position = $table->position; + } else { + $position = $value; + } + + $table->position = $position; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to test whether a record can have its state edited. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 3.2 + */ + protected function canEditState($record) + { + // Check for existing module. + if (!empty($record->id)) { + return Factory::getUser()->authorise('core.edit.state', 'com_modules.module.' . (int) $record->id); + } + + // Default to component settings if module not known. + return parent::canEditState($record); + } + + /** + * Method to delete rows. + * + * @param array &$pks An array of item ids. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function delete(&$pks) + { + $app = Factory::getApplication(); + $pks = (array) $pks; + $user = Factory::getUser(); + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + + // Include the plugins for the on delete events. + PluginHelper::importPlugin($this->events_map['delete']); + + // Iterate the items to delete each one. + foreach ($pks as $pk) { + if ($table->load($pk)) { + // Access checks. + if (!$user->authorise('core.delete', 'com_modules.module.' . (int) $pk) || $table->published != -2) { + Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); + + return; + } + + // Trigger the before delete event. + $result = $app->triggerEvent($this->event_before_delete, array($context, $table)); + + if (in_array(false, $result, true) || !$table->delete($pk)) { + throw new \Exception($table->getError()); + } else { + // Delete the menu assignments + $pk = (int) $pk; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = :moduleid') + ->bind(':moduleid', $pk, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + // Trigger the after delete event. + $app->triggerEvent($this->event_after_delete, array($context, $table)); + } + + // Clear module cache + parent::cleanCache($table->module); + } else { + throw new \Exception($table->getError()); + } + } + + // Clear modules cache + $this->cleanCache(); + + return true; + } + + /** + * Method to duplicate modules. + * + * @param array &$pks An array of primary key IDs. + * + * @return boolean Boolean true on success + * + * @since 1.6 + * @throws \Exception + */ + public function duplicate(&$pks) + { + $user = Factory::getUser(); + $db = $this->getDatabase(); + + // Access checks. + if (!$user->authorise('core.create', 'com_modules')) { + throw new \Exception(Text::_('JERROR_CORE_CREATE_NOT_PERMITTED')); + } + + $table = $this->getTable(); + + foreach ($pks as $pk) { + if ($table->load($pk, true)) { + // Reset the id to create a new record. + $table->id = 0; + + // Alter the title. + $m = null; + + if (preg_match('#\((\d+)\)$#', $table->title, $m)) { + $table->title = preg_replace('#\(\d+\)$#', '(' . ($m[1] + 1) . ')', $table->title); + } + + $data = $this->generateNewTitle(0, $table->title, $table->position); + $table->title = $data[0]; + + // Unpublish duplicate module + $table->published = 0; + + if (!$table->check() || !$table->store()) { + throw new \Exception($table->getError()); + } + + $pk = (int) $pk; + $query = $db->getQuery(true) + ->select($db->quoteName('menuid')) + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = :moduleid') + ->bind(':moduleid', $pk, ParameterType::INTEGER); + + $db->setQuery($query); + $rows = $db->loadColumn(); + + foreach ($rows as $menuid) { + $tuples[] = (int) $table->id . ',' . (int) $menuid; + } + } else { + throw new \Exception($table->getError()); + } + } + + if (!empty($tuples)) { + // Module-Menu Mapping: Do it in one query + $query = $db->getQuery(true) + ->insert($db->quoteName('#__modules_menu')) + ->columns($db->quoteName(array('moduleid', 'menuid'))) + ->values($tuples); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + } + + // Clear modules cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the title. + * + * @param integer $categoryId The id of the category. Not used here. + * @param string $title The title. + * @param string $position The position. + * + * @return array Contains the modified title. + * + * @since 2.5 + */ + protected function generateNewTitle($categoryId, $title, $position) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('position' => $position, 'title' => $title))) { + $title = StringHelper::increment($title); + } + + return array($title); + } + + /** + * Method to get the client object + * + * @return void + * + * @since 1.6 + */ + public function &getClient() + { + return $this->_client; + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // The folder and element vars are passed when saving the form. + if (empty($data)) { + $item = $this->getItem(); + $clientId = $item->client_id; + $module = $item->module; + $id = $item->id; + } else { + $clientId = ArrayHelper::getValue($data, 'client_id'); + $module = ArrayHelper::getValue($data, 'module'); + $id = ArrayHelper::getValue($data, 'id'); + } + + // Add the default fields directory + $baseFolder = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; + Form::addFieldPath($baseFolder . '/modules/' . $module . '/field'); + + // These variables are used to add data from the plugin XML files. + $this->setState('item.client_id', $clientId); + $this->setState('item.module', $module); + + // Get the form. + if ($clientId == 1) { + $form = $this->loadForm('com_modules.module.admin', 'moduleadmin', array('control' => 'jform', 'load_data' => $loadData), true); + + // Display language field to filter admin custom menus per language + if (!ModuleHelper::isAdminMultilang()) { + $form->setFieldAttribute('language', 'type', 'hidden'); + } + } else { + $form = $this->loadForm('com_modules.module', 'module', array('control' => 'jform', 'load_data' => $loadData), true); + } + + if (empty($form)) { + return false; + } + + $user = Factory::getUser(); + + /** + * Check for existing module + * Modify the form based on Edit State access controls. + */ + if ( + $id != 0 && (!$user->authorise('core.edit.state', 'com_modules.module.' . (int) $id)) + || ($id == 0 && !$user->authorise('core.edit.state', 'com_modules')) + ) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + $form->setFieldAttribute('publish_up', 'disabled', 'true'); + $form->setFieldAttribute('publish_down', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + $form->setFieldAttribute('publish_up', 'filter', 'unset'); + $form->setFieldAttribute('publish_down', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $app = Factory::getApplication(); + + // Check the session for previously entered form data. + $data = $app->getUserState('com_modules.edit.module.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Pre-select some filters (Status, Module Position, Language, Access Level) in edit form if those have been selected in Module Manager + if (!$data->id) { + $clientId = $app->input->getInt('client_id', 0); + $filters = (array) $app->getUserState('com_modules.modules.' . $clientId . '.filter'); + $data->set('published', $app->input->getInt('published', ((isset($filters['state']) && $filters['state'] !== '') ? $filters['state'] : null))); + $data->set('position', $app->input->getInt('position', (!empty($filters['position']) ? $filters['position'] : null))); + $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); + $data->set('access', $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access')))); + } + + // Avoid to delete params of a second module opened in a new browser tab while new one is not saved yet. + if (empty($data->params)) { + // This allows us to inject parameter settings into a new module. + $params = $app->getUserState('com_modules.add.module.params'); + + if (is_array($params)) { + $data->set('params', $params); + } + } + } + + $this->preprocessData('com_modules.module', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + $pk = (!empty($pk)) ? (int) $pk : (int) $this->getState('module.id'); + $db = $this->getDatabase(); + + if (!isset($this->_cache[$pk])) { + // Get a row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load($pk); + + // Check for a table object error. + if ($return === false && $error = $table->getError()) { + $this->setError($error); + + return false; + } + + // Check if we are creating a new extension. + if (empty($pk)) { + if ($extensionId = (int) $this->getState('extension.id')) { + $query = $db->getQuery(true) + ->select($db->quoteName(['element', 'client_id'])) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('extension_id') . ' = :extensionid') + ->where($db->quoteName('type') . ' = ' . $db->quote('module')) + ->bind(':extensionid', $extensionId, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $extension = $db->loadObject(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (empty($extension)) { + $this->setError('COM_MODULES_ERROR_CANNOT_FIND_MODULE'); + + return false; + } + + // Extension found, prime some module values. + $table->module = $extension->element; + $table->client_id = $extension->client_id; + } else { + Factory::getApplication()->redirect(Route::_('index.php?option=com_modules&view=modules', false)); + + return false; + } + } + + // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. + $properties = $table->getProperties(1); + $this->_cache[$pk] = ArrayHelper::toObject($properties, CMSObject::class); + + // Convert the params field to an array. + $registry = new Registry($table->params); + $this->_cache[$pk]->params = $registry->toArray(); + + // Determine the page assignment mode. + $query = $db->getQuery(true) + ->select($db->quoteName('menuid')) + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = :moduleid') + ->bind(':moduleid', $pk, ParameterType::INTEGER); + $db->setQuery($query); + $assigned = $db->loadColumn(); + + if (empty($pk)) { + // If this is a new module, assign to all pages. + $assignment = 0; + } elseif (empty($assigned)) { + // For an existing module it is assigned to none. + $assignment = '-'; + } else { + if ($assigned[0] > 0) { + $assignment = 1; + } elseif ($assigned[0] < 0) { + $assignment = -1; + } else { + $assignment = 0; + } + } + + $this->_cache[$pk]->assigned = $assigned; + $this->_cache[$pk]->assignment = $assignment; + + // Get the module XML. + $client = ApplicationHelper::getClientInfo($table->client_id); + $path = Path::clean($client->path . '/modules/' . $table->module . '/' . $table->module . '.xml'); + + if (file_exists($path)) { + $this->_cache[$pk]->xml = simplexml_load_file($path); + } else { + $this->_cache[$pk]->xml = null; + } + } + + return $this->_cache[$pk]; + } + + /** + * Get the necessary data to load an item help screen. + * + * @return object An object with key, url, and local properties for loading the item help screen. + * + * @since 1.6 + */ + public function getHelp() + { + return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); + } + + /** + * Returns a reference to the a Table object, always creating it. + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + * + * @since 1.6 + */ + public function getTable($type = 'Module', $prefix = 'JTable', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + + /** + * Prepare and sanitise the table prior to saving. + * + * @param Table $table The database object + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + $table->title = htmlspecialchars_decode($table->title, ENT_QUOTES); + $table->position = trim($table->position); + } + + /** + * Method to preprocess the form + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error loading the form. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $lang = Factory::getLanguage(); + $clientId = $this->getState('item.client_id'); + $module = $this->getState('item.module'); + + $client = ApplicationHelper::getClientInfo($clientId); + $formFile = Path::clean($client->path . '/modules/' . $module . '/' . $module . '.xml'); + + // Load the core and/or local language file(s). + $lang->load($module, $client->path) + || $lang->load($module, $client->path . '/modules/' . $module); + + if (file_exists($formFile)) { + // Get the module form. + if (!$form->loadFile($formFile, false, '//config')) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Get the help data from the XML file if present. + $help = $xml->xpath('/extension/help'); + + if (!empty($help)) { + $helpKey = trim((string) $help[0]['key']); + $helpURL = trim((string) $help[0]['url']); + + $this->helpKey = $helpKey ?: $this->helpKey; + $this->helpURL = $helpURL ?: $this->helpURL; + } + } + + // Load the default advanced params + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/models/forms'); + $form->loadFile('advanced', false); + + // Load chrome specific params for global files + $chromePath = JPATH_SITE . '/layouts/chromes'; + $chromeFormFiles = Folder::files($chromePath, '.*\.xml'); + + if ($chromeFormFiles) { + Form::addFormPath($chromePath); + + foreach ($chromeFormFiles as $formFile) { + $form->loadFile(basename($formFile, '.xml'), false); + } + } + + // Load chrome specific params for template files + $templates = ModulesHelper::getTemplates($clientId); + + foreach ($templates as $template) { + $chromePath = $client->path . '/templates/' . $template->element . '/html/layouts/chromes'; + + // Skip if there is no chrome folder in that template. + if (!is_dir($chromePath)) { + continue; + } + + $chromeFormFiles = Folder::files($chromePath, '.*\.xml'); + + if ($chromeFormFiles) { + Form::addFormPath($chromePath); + + foreach ($chromeFormFiles as $formFile) { + $form->loadFile(basename($formFile, '.xml'), false); + } + } + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Loads ContentHelper for filters before validating data. + * + * @param object $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the group(defaults to null). + * + * @return mixed Array of filtered data if valid, false otherwise. + * + * @since 1.1 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', 'com_modules')) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $input = Factory::getApplication()->input; + $table = $this->getTable(); + $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('module.id'); + $isNew = true; + $context = $this->option . '.' . $this->name; + + // Include the plugins for the save event. + PluginHelper::importPlugin($this->events_map['save']); + + // Load the row if saving an existing record. + if ($pk > 0) { + $table->load($pk); + $isNew = false; + } + + // Alter the title and published state for Save as Copy + if ($input->get('task') == 'save2copy') { + $orig_table = clone $this->getTable(); + $orig_table->load((int) $input->getInt('id')); + $data['published'] = 0; + + if ($data['title'] == $orig_table->title) { + $data['title'] = StringHelper::increment($data['title']); + } + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Prepare the row for saving + $this->prepareTable($table); + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Process the menu link mappings. + $assignment = $data['assignment'] ?? 0; + + $table->id = (int) $table->id; + + // Delete old module to menu item associations + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = :moduleid') + ->bind(':moduleid', $table->id, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // If the assignment is numeric, then something is selected (otherwise it's none). + if (is_numeric($assignment)) { + // Variable is numeric, but could be a string. + $assignment = (int) $assignment; + + // Logic check: if no module excluded then convert to display on all. + if ($assignment == -1 && empty($data['assigned'])) { + $assignment = 0; + } + + // Check needed to stop a module being assigned to `All` + // and other menu items resulting in a module being displayed twice. + if ($assignment === 0) { + // Assign new module to `all` menu item associations. + $query->clear() + ->insert($db->quoteName('#__modules_menu')) + ->columns($db->quoteName(['moduleid', 'menuid'])) + ->values(implode(', ', [':moduleid', 0])) + ->bind(':moduleid', $table->id, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } elseif (!empty($data['assigned'])) { + // Get the sign of the number. + $sign = $assignment < 0 ? -1 : 1; + + $query->clear() + ->insert($db->quoteName('#__modules_menu')) + ->columns($db->quoteName(array('moduleid', 'menuid'))); + + foreach ($data['assigned'] as &$pk) { + $query->values((int) $table->id . ',' . (int) $pk * $sign); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew)); + + // Compute the extension id of this module in case the controller wants it. + $query->clear() + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions', 'e')) + ->join( + 'LEFT', + $db->quoteName('#__modules', 'm') . ' ON ' . $db->quoteName('e.client_id') . ' = ' . (int) $table->client_id . + ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('m.module') + ) + ->where($db->quoteName('m.id') . ' = :id') + ->bind(':id', $table->id, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $extensionId = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + $this->setState('module.extension_id', $extensionId); + $this->setState('module.id', $table->id); + + // Clear modules cache + $this->cleanCache(); + + // Clean module cache + parent::cleanCache($table->module); + + return true; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('client_id') . ' = ' . (int) $table->client_id, + $db->quoteName('position') . ' = ' . $db->quote($table->position), + ]; + } + + /** + * Custom clean cache method for different clients + * + * @param string $group The name of the plugin group to import (defaults to null). + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_modules'); + } } diff --git a/administrator/components/com_modules/src/Model/ModulesModel.php b/administrator/components/com_modules/src/Model/ModulesModel.php index 5abc720bda5c5..f0254875c98f1 100644 --- a/administrator/components/com_modules/src/Model/ModulesModel.php +++ b/administrator/components/com_modules/src/Model/ModulesModel.php @@ -1,4 +1,5 @@ input->get('layout', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout) - { - $this->context .= '.' . $layout; - } - - // Make context client aware - $this->context .= '.' . $app->input->get->getInt('client_id', 0); - - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.position', $this->getUserStateFromRequest($this->context . '.filter.position', 'filter_position', '', 'string')); - $this->setState('filter.module', $this->getUserStateFromRequest($this->context . '.filter.module', 'filter_module', '', 'string')); - $this->setState('filter.menuitem', $this->getUserStateFromRequest($this->context . '.filter.menuitem', 'filter_menuitem', '', 'cmd')); - $this->setState('filter.access', $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access', '', 'cmd')); - - // If in modal layout on the frontend, state and language are always forced. - if ($app->isClient('site') && $layout === 'modal') - { - $this->setState('filter.language', 'current'); - $this->setState('filter.state', 1); - } - // If in backend (modal or not) we get the same fields from the user request. - else - { - $this->setState('filter.language', $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', '', 'string')); - $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'string')); - } - - // Special case for the client id. - if ($app->isClient('site') || $layout === 'modal') - { - $this->setState('client_id', 0); - $clientId = 0; - } - else - { - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $clientId = (!in_array($clientId, array(0, 1))) ? 0 : $clientId; - $this->setState('client_id', $clientId); - } - - // Use a different filter file when client is administrator - if ($clientId == 1) - { - $this->filterFormName = 'filter_modulesadmin'; - } - - // Load the parameters. - $params = ComponentHelper::getParams('com_modules'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('client_id'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.position'); - $id .= ':' . $this->getState('filter.module'); - $id .= ':' . $this->getState('filter.menuitem'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.language'); - - return parent::getStoreId($id); - } - - /** - * Returns an object list - * - * @param DatabaseQuery $query The query - * @param int $limitstart Offset - * @param int $limit The number of records - * - * @return array - */ - protected function _getList($query, $limitstart = 0, $limit = 0) - { - $listOrder = $this->getState('list.ordering', 'a.position'); - $listDirn = $this->getState('list.direction', 'asc'); - - $db = $this->getDatabase(); - - // If ordering by fields that need translate we need to sort the array of objects after translating them. - if (in_array($listOrder, array('pages', 'name'))) - { - // Fetch the results. - $db->setQuery($query); - $result = $db->loadObjectList(); - - // Translate the results. - $this->translate($result); - - // Sort the array of translated objects. - $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) == 'desc' ? -1 : 1, true, true); - - // Process pagination. - $total = count($result); - $this->cache[$this->getStoreId('getTotal')] = $total; - - if ($total < $limitstart) - { - $limitstart = 0; - $this->setState('list.start', 0); - } - - return array_slice($result, $limitstart, $limit ?: null); - } - - // If ordering by fields that doesn't need translate just order the query. - if ($listOrder === 'a.ordering') - { - $query->order($db->quoteName('a.position') . ' ASC') - ->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); - } - elseif ($listOrder === 'a.position') - { - $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)) - ->order($db->quoteName('a.ordering') . ' ASC'); - } - else - { - $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); - } - - // Process pagination. - $result = parent::_getList($query, $limitstart, $limit); - - // Translate the results. - $this->translate($result); - - return $result; - } - - /** - * Translate a list of objects - * - * @param array &$items The array of objects - * - * @return array The array of translated objects - */ - protected function translate(&$items) - { - $lang = Factory::getLanguage(); - $clientPath = $this->getState('client_id') ? JPATH_ADMINISTRATOR : JPATH_SITE; - - foreach ($items as $item) - { - $extension = $item->module; - $source = $clientPath . "/modules/$extension"; - $lang->load("$extension.sys", $clientPath) - || $lang->load("$extension.sys", $source); - $item->name = Text::_($item->name); - - if (is_null($item->pages)) - { - $item->pages = Text::_('JNONE'); - } - elseif ($item->pages < 0) - { - $item->pages = Text::_('COM_MODULES_ASSIGNED_VARIES_EXCEPT'); - } - elseif ($item->pages > 0) - { - $item->pages = Text::_('COM_MODULES_ASSIGNED_VARIES_ONLY'); - } - else - { - $item->pages = Text::_('JALL'); - } - } - } - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields. - $query->select( - $this->getState( - 'list.select', - 'a.id, a.title, a.note, a.position, a.module, a.language,' . - 'a.checked_out, a.checked_out_time, a.published AS published, e.enabled AS enabled, a.access, a.ordering, a.publish_up, a.publish_down' - ) - ); - - // From modules table. - $query->from($db->quoteName('#__modules', 'a')); - - // Join over the language - $query->select($db->quoteName('l.title', 'language_title')) - ->select($db->quoteName('l.image', 'language_image')) - ->join('LEFT', $db->quoteName('#__languages', 'l') . ' ON ' . $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); - - // Join over the users for the checked out user. - $query->select($db->quoteName('uc.name', 'editor')) - ->join('LEFT', $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Join over the asset groups. - $query->select($db->quoteName('ag.title', 'access_level')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag') . ' ON ' . $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')); - - // Join over the module menus - $query->select('MIN(mm.menuid) AS pages') - ->join('LEFT', $db->quoteName('#__modules_menu', 'mm') . ' ON ' . $db->quoteName('mm.moduleid') . ' = ' . $db->quoteName('a.id')); - - // Join over the extensions - $query->select($db->quoteName('e.name', 'name')) - ->join('LEFT', $db->quoteName('#__extensions', 'e') . ' ON ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('a.module')); - - // Group (careful with PostgreSQL) - $query->group( - 'a.id, a.title, a.note, a.position, a.module, a.language, a.checked_out, ' - . 'a.checked_out_time, a.published, a.access, a.ordering, l.title, l.image, uc.name, ag.title, e.name, ' - . 'l.lang_code, uc.id, ag.id, mm.moduleid, e.element, a.publish_up, a.publish_down, e.enabled' - ); - - // Filter by client. - $clientId = (int) $this->getState('client_id'); - $query->where($db->quoteName('a.client_id') . ' = :aclientid') - ->where($db->quoteName('e.client_id') . ' = :eclientid') - ->bind(':aclientid', $clientId, ParameterType::INTEGER) - ->bind(':eclientid', $clientId, ParameterType::INTEGER); - - // Filter by current user access level. - $user = Factory::getUser(); - - // Get the current user for authorisation checks - if ($user->authorise('core.admin') !== true) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - } - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - $access = (int) $access; - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Filter by published state. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.published') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif ($state === '') - { - $query->whereIn($db->quoteName('a.published'), [0, 1]); - } - - // Filter by position. - if ($position = $this->getState('filter.position')) - { - $position = ($position === 'none') ? '' : $position; - $query->where($db->quoteName('a.position') . ' = :position') - ->bind(':position', $position); - } - - // Filter by module. - if ($module = $this->getState('filter.module')) - { - $query->where($db->quoteName('a.module') . ' = :module') - ->bind(':module', $module); - } - - // Filter by menuitem id (only for site client). - if ((int) $clientId === 0 && $menuItemId = $this->getState('filter.menuitem')) - { - // If user selected the modules not assigned to any page (menu item). - if ((int) $menuItemId === -1) - { - $query->having('MIN(' . $db->quoteName('mm.menuid') . ') IS NULL'); - } - // If user selected the modules assigned to some particular page (menu item). - else - { - // Modules in "All" pages. - $subQuery1 = $db->getQuery(true); - $subQuery1->select('MIN(' . $db->quoteName('menuid') . ')') - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = ' . $db->quoteName('a.id')); - - // Modules in "Selected" pages that have the chosen menu item id. - $menuItemId = (int) $menuItemId; - $minusMenuItemId = $menuItemId * -1; - $subQuery2 = $db->getQuery(true); - $subQuery2->select($db->quoteName('moduleid')) - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('menuid') . ' = :menuitemid2'); - - // Modules in "All except selected" pages that doesn't have the chosen menu item id. - $subQuery3 = $db->getQuery(true); - $subQuery3->select($db->quoteName('moduleid')) - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('menuid') . ' = :menuitemid3'); - - // Filter by modules assigned to the selected menu item. - $query->where('( + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see \JController + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'published', 'a.published', 'state', + 'access', 'a.access', + 'ag.title', 'access_level', + 'ordering', 'a.ordering', + 'module', 'a.module', + 'language', 'a.language', + 'l.title', 'language_title', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'client_id', 'a.client_id', + 'position', 'a.position', + 'pages', + 'name', 'e.name', + 'menuitem', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.position', $direction = 'asc') + { + $app = Factory::getApplication(); + + $layout = $app->input->get('layout', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout) { + $this->context .= '.' . $layout; + } + + // Make context client aware + $this->context .= '.' . $app->input->get->getInt('client_id', 0); + + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.position', $this->getUserStateFromRequest($this->context . '.filter.position', 'filter_position', '', 'string')); + $this->setState('filter.module', $this->getUserStateFromRequest($this->context . '.filter.module', 'filter_module', '', 'string')); + $this->setState('filter.menuitem', $this->getUserStateFromRequest($this->context . '.filter.menuitem', 'filter_menuitem', '', 'cmd')); + $this->setState('filter.access', $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access', '', 'cmd')); + + // If in modal layout on the frontend, state and language are always forced. + if ($app->isClient('site') && $layout === 'modal') { + $this->setState('filter.language', 'current'); + $this->setState('filter.state', 1); + } + // If in backend (modal or not) we get the same fields from the user request. + else { + $this->setState('filter.language', $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', '', 'string')); + $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'string')); + } + + // Special case for the client id. + if ($app->isClient('site') || $layout === 'modal') { + $this->setState('client_id', 0); + $clientId = 0; + } else { + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $clientId = (!in_array($clientId, array(0, 1))) ? 0 : $clientId; + $this->setState('client_id', $clientId); + } + + // Use a different filter file when client is administrator + if ($clientId == 1) { + $this->filterFormName = 'filter_modulesadmin'; + } + + // Load the parameters. + $params = ComponentHelper::getParams('com_modules'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('client_id'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.position'); + $id .= ':' . $this->getState('filter.module'); + $id .= ':' . $this->getState('filter.menuitem'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.language'); + + return parent::getStoreId($id); + } + + /** + * Returns an object list + * + * @param DatabaseQuery $query The query + * @param int $limitstart Offset + * @param int $limit The number of records + * + * @return array + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $listOrder = $this->getState('list.ordering', 'a.position'); + $listDirn = $this->getState('list.direction', 'asc'); + + $db = $this->getDatabase(); + + // If ordering by fields that need translate we need to sort the array of objects after translating them. + if (in_array($listOrder, array('pages', 'name'))) { + // Fetch the results. + $db->setQuery($query); + $result = $db->loadObjectList(); + + // Translate the results. + $this->translate($result); + + // Sort the array of translated objects. + $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) == 'desc' ? -1 : 1, true, true); + + // Process pagination. + $total = count($result); + $this->cache[$this->getStoreId('getTotal')] = $total; + + if ($total < $limitstart) { + $limitstart = 0; + $this->setState('list.start', 0); + } + + return array_slice($result, $limitstart, $limit ?: null); + } + + // If ordering by fields that doesn't need translate just order the query. + if ($listOrder === 'a.ordering') { + $query->order($db->quoteName('a.position') . ' ASC') + ->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); + } elseif ($listOrder === 'a.position') { + $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)) + ->order($db->quoteName('a.ordering') . ' ASC'); + } else { + $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); + } + + // Process pagination. + $result = parent::_getList($query, $limitstart, $limit); + + // Translate the results. + $this->translate($result); + + return $result; + } + + /** + * Translate a list of objects + * + * @param array &$items The array of objects + * + * @return array The array of translated objects + */ + protected function translate(&$items) + { + $lang = Factory::getLanguage(); + $clientPath = $this->getState('client_id') ? JPATH_ADMINISTRATOR : JPATH_SITE; + + foreach ($items as $item) { + $extension = $item->module; + $source = $clientPath . "/modules/$extension"; + $lang->load("$extension.sys", $clientPath) + || $lang->load("$extension.sys", $source); + $item->name = Text::_($item->name); + + if (is_null($item->pages)) { + $item->pages = Text::_('JNONE'); + } elseif ($item->pages < 0) { + $item->pages = Text::_('COM_MODULES_ASSIGNED_VARIES_EXCEPT'); + } elseif ($item->pages > 0) { + $item->pages = Text::_('COM_MODULES_ASSIGNED_VARIES_ONLY'); + } else { + $item->pages = Text::_('JALL'); + } + } + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.title, a.note, a.position, a.module, a.language,' . + 'a.checked_out, a.checked_out_time, a.published AS published, e.enabled AS enabled, a.access, a.ordering, a.publish_up, a.publish_down' + ) + ); + + // From modules table. + $query->from($db->quoteName('#__modules', 'a')); + + // Join over the language + $query->select($db->quoteName('l.title', 'language_title')) + ->select($db->quoteName('l.image', 'language_image')) + ->join('LEFT', $db->quoteName('#__languages', 'l') . ' ON ' . $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); + + // Join over the users for the checked out user. + $query->select($db->quoteName('uc.name', 'editor')) + ->join('LEFT', $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Join over the asset groups. + $query->select($db->quoteName('ag.title', 'access_level')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag') . ' ON ' . $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')); + + // Join over the module menus + $query->select('MIN(mm.menuid) AS pages') + ->join('LEFT', $db->quoteName('#__modules_menu', 'mm') . ' ON ' . $db->quoteName('mm.moduleid') . ' = ' . $db->quoteName('a.id')); + + // Join over the extensions + $query->select($db->quoteName('e.name', 'name')) + ->join('LEFT', $db->quoteName('#__extensions', 'e') . ' ON ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('a.module')); + + // Group (careful with PostgreSQL) + $query->group( + 'a.id, a.title, a.note, a.position, a.module, a.language, a.checked_out, ' + . 'a.checked_out_time, a.published, a.access, a.ordering, l.title, l.image, uc.name, ag.title, e.name, ' + . 'l.lang_code, uc.id, ag.id, mm.moduleid, e.element, a.publish_up, a.publish_down, e.enabled' + ); + + // Filter by client. + $clientId = (int) $this->getState('client_id'); + $query->where($db->quoteName('a.client_id') . ' = :aclientid') + ->where($db->quoteName('e.client_id') . ' = :eclientid') + ->bind(':aclientid', $clientId, ParameterType::INTEGER) + ->bind(':eclientid', $clientId, ParameterType::INTEGER); + + // Filter by current user access level. + $user = Factory::getUser(); + + // Get the current user for authorisation checks + if ($user->authorise('core.admin') !== true) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + } + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + $access = (int) $access; + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Filter by published state. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.published') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif ($state === '') { + $query->whereIn($db->quoteName('a.published'), [0, 1]); + } + + // Filter by position. + if ($position = $this->getState('filter.position')) { + $position = ($position === 'none') ? '' : $position; + $query->where($db->quoteName('a.position') . ' = :position') + ->bind(':position', $position); + } + + // Filter by module. + if ($module = $this->getState('filter.module')) { + $query->where($db->quoteName('a.module') . ' = :module') + ->bind(':module', $module); + } + + // Filter by menuitem id (only for site client). + if ((int) $clientId === 0 && $menuItemId = $this->getState('filter.menuitem')) { + // If user selected the modules not assigned to any page (menu item). + if ((int) $menuItemId === -1) { + $query->having('MIN(' . $db->quoteName('mm.menuid') . ') IS NULL'); + } + // If user selected the modules assigned to some particular page (menu item). + else { + // Modules in "All" pages. + $subQuery1 = $db->getQuery(true); + $subQuery1->select('MIN(' . $db->quoteName('menuid') . ')') + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = ' . $db->quoteName('a.id')); + + // Modules in "Selected" pages that have the chosen menu item id. + $menuItemId = (int) $menuItemId; + $minusMenuItemId = $menuItemId * -1; + $subQuery2 = $db->getQuery(true); + $subQuery2->select($db->quoteName('moduleid')) + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('menuid') . ' = :menuitemid2'); + + // Modules in "All except selected" pages that doesn't have the chosen menu item id. + $subQuery3 = $db->getQuery(true); + $subQuery3->select($db->quoteName('moduleid')) + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('menuid') . ' = :menuitemid3'); + + // Filter by modules assigned to the selected menu item. + $query->where('( (' . $subQuery1 . ') = 0 OR ((' . $subQuery1 . ') > 0 AND ' . $db->quoteName('a.id') . ' IN (' . $subQuery2 . ')) OR ((' . $subQuery1 . ') < 0 AND ' . $db->quoteName('a.id') . ' NOT IN (' . $subQuery3 . ')) - )' - ); - $query->bind(':menuitemid2', $menuItemId, ParameterType::INTEGER); - $query->bind(':menuitemid3', $minusMenuItemId, ParameterType::INTEGER); - } - } - - // Filter by search in title or note or id:. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . StringHelper::strtolower($search) . '%'; - $query->extendWhere( - 'AND', - [ - 'LOWER(' . $db->quoteName('a.title') . ') LIKE :title', - 'LOWER(' . $db->quoteName('a.note') . ') LIKE :note', - ], - 'OR' - ) - ->bind(':title', $search) - ->bind(':note', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - if ($language === 'current') - { - $language = [Factory::getLanguage()->getTag(), '*']; - $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); - } - else - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - } - - return $query; - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - $clientId = (int) $this->getState('client_id'); - - $query->where($this->getDatabase()->quoteName('a.client_id') . ' = :client_id') - ->bind(':client_id', $clientId, ParameterType::INTEGER); - - return $query; - } + )'); + $query->bind(':menuitemid2', $menuItemId, ParameterType::INTEGER); + $query->bind(':menuitemid3', $minusMenuItemId, ParameterType::INTEGER); + } + } + + // Filter by search in title or note or id:. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . StringHelper::strtolower($search) . '%'; + $query->extendWhere( + 'AND', + [ + 'LOWER(' . $db->quoteName('a.title') . ') LIKE :title', + 'LOWER(' . $db->quoteName('a.note') . ') LIKE :note', + ], + 'OR' + ) + ->bind(':title', $search) + ->bind(':note', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + if ($language === 'current') { + $language = [Factory::getLanguage()->getTag(), '*']; + $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); + } else { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + } + + return $query; + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + $clientId = (int) $this->getState('client_id'); + + $query->where($this->getDatabase()->quoteName('a.client_id') . ' = :client_id') + ->bind(':client_id', $clientId, ParameterType::INTEGER); + + return $query; + } } diff --git a/administrator/components/com_modules/src/Model/PositionsModel.php b/administrator/components/com_modules/src/Model/PositionsModel.php index 0213400e5f5d2..a1ac389a43624 100644 --- a/administrator/components/com_modules/src/Model/PositionsModel.php +++ b/administrator/components/com_modules/src/Model/PositionsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); - $this->setState('filter.search', $search); - - $state = $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'string'); - $this->setState('filter.state', $state); - - $template = $this->getUserStateFromRequest($this->context . '.filter.template', 'filter_template', '', 'string'); - $this->setState('filter.template', $template); - - $type = $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string'); - $this->setState('filter.type', $type); - - // Special case for the client id. - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $clientId = (!in_array((int) $clientId, array (0, 1))) ? 0 : (int) $clientId; - $this->setState('client_id', $clientId); - - // Load the parameters. - $params = ComponentHelper::getParams('com_modules'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 1.6 - */ - public function getItems() - { - if (!isset($this->items)) - { - $lang = Factory::getLanguage(); - $search = $this->getState('filter.search'); - $state = $this->getState('filter.state'); - $clientId = $this->getState('client_id'); - $filter_template = $this->getState('filter.template'); - $type = $this->getState('filter.type'); - $ordering = $this->getState('list.ordering'); - $direction = $this->getState('list.direction'); - $limitstart = $this->getState('list.start'); - $limit = $this->getState('list.limit'); - $client = ApplicationHelper::getClientInfo($clientId); - - if ($type != 'template') - { - $clientId = (int) $clientId; - - // Get the database object and a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('position', 'value')) - ->from($db->quoteName('#__modules')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - - if ($search) - { - $search = '%' . str_replace(' ', '%', trim($search), true) . '%'; - $query->where($db->quoteName('position') . ' LIKE :position') - ->bind(':position', $search); - } - - $db->setQuery($query); - - try - { - $positions = $db->loadObjectList('value'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - foreach ($positions as $value => $position) - { - $positions[$value] = array(); - } - } - else - { - $positions = array(); - } - - // Load the positions from the installed templates. - foreach (ModulesHelper::getTemplates($clientId) as $template) - { - $path = Path::clean($client->path . '/templates/' . $template->element . '/templateDetails.xml'); - - if (file_exists($path)) - { - $xml = simplexml_load_file($path); - - if (isset($xml->positions[0])) - { - $lang->load('tpl_' . $template->element . '.sys', $client->path) - || $lang->load('tpl_' . $template->element . '.sys', $client->path . '/templates/' . $template->element); - - foreach ($xml->positions[0] as $position) - { - $value = (string) $position['value']; - $label = (string) $position; - - if (!$value) - { - $value = $label; - $label = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'TPL_' . $template->element . '_POSITION_' . $value); - $altlabel = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'COM_MODULES_POSITION_' . $value); - - if (!$lang->hasKey($label) && $lang->hasKey($altlabel)) - { - $label = $altlabel; - } - } - - if ($type == 'user' || ($state != '' && $state != $template->enabled)) - { - unset($positions[$value]); - } - elseif (preg_match(chr(1) . $search . chr(1) . 'i', $value) && ($filter_template == '' || $filter_template == $template->element)) - { - if (!isset($positions[$value])) - { - $positions[$value] = array(); - } - - $positions[$value][$template->name] = $label; - } - } - } - } - } - - $this->total = count($positions); - - if ($limitstart >= $this->total) - { - $limitstart = $limitstart < $limit ? 0 : $limitstart - $limit; - $this->setState('list.start', $limitstart); - } - - if ($ordering == 'value') - { - if ($direction == 'asc') - { - ksort($positions); - } - else - { - krsort($positions); - } - } - else - { - if ($direction == 'asc') - { - asort($positions); - } - else - { - arsort($positions); - } - } - - $this->items = array_slice($positions, $limitstart, $limit ?: null); - } - - return $this->items; - } - - /** - * Method to get the total number of items. - * - * @return integer The total number of items. - * - * @since 1.6 - */ - public function getTotal() - { - if (!isset($this->total)) - { - $this->getItems(); - } - - return $this->total; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see \JController + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'value', + 'templates', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'ordering', $direction = 'asc') + { + // Load the filter state. + $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); + $this->setState('filter.search', $search); + + $state = $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'string'); + $this->setState('filter.state', $state); + + $template = $this->getUserStateFromRequest($this->context . '.filter.template', 'filter_template', '', 'string'); + $this->setState('filter.template', $template); + + $type = $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string'); + $this->setState('filter.type', $type); + + // Special case for the client id. + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $clientId = (!in_array((int) $clientId, array (0, 1))) ? 0 : (int) $clientId; + $this->setState('client_id', $clientId); + + // Load the parameters. + $params = ComponentHelper::getParams('com_modules'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 1.6 + */ + public function getItems() + { + if (!isset($this->items)) { + $lang = Factory::getLanguage(); + $search = $this->getState('filter.search'); + $state = $this->getState('filter.state'); + $clientId = $this->getState('client_id'); + $filter_template = $this->getState('filter.template'); + $type = $this->getState('filter.type'); + $ordering = $this->getState('list.ordering'); + $direction = $this->getState('list.direction'); + $limitstart = $this->getState('list.start'); + $limit = $this->getState('list.limit'); + $client = ApplicationHelper::getClientInfo($clientId); + + if ($type != 'template') { + $clientId = (int) $clientId; + + // Get the database object and a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('position', 'value')) + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + + if ($search) { + $search = '%' . str_replace(' ', '%', trim($search), true) . '%'; + $query->where($db->quoteName('position') . ' LIKE :position') + ->bind(':position', $search); + } + + $db->setQuery($query); + + try { + $positions = $db->loadObjectList('value'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + foreach ($positions as $value => $position) { + $positions[$value] = array(); + } + } else { + $positions = array(); + } + + // Load the positions from the installed templates. + foreach (ModulesHelper::getTemplates($clientId) as $template) { + $path = Path::clean($client->path . '/templates/' . $template->element . '/templateDetails.xml'); + + if (file_exists($path)) { + $xml = simplexml_load_file($path); + + if (isset($xml->positions[0])) { + $lang->load('tpl_' . $template->element . '.sys', $client->path) + || $lang->load('tpl_' . $template->element . '.sys', $client->path . '/templates/' . $template->element); + + foreach ($xml->positions[0] as $position) { + $value = (string) $position['value']; + $label = (string) $position; + + if (!$value) { + $value = $label; + $label = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'TPL_' . $template->element . '_POSITION_' . $value); + $altlabel = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'COM_MODULES_POSITION_' . $value); + + if (!$lang->hasKey($label) && $lang->hasKey($altlabel)) { + $label = $altlabel; + } + } + + if ($type == 'user' || ($state != '' && $state != $template->enabled)) { + unset($positions[$value]); + } elseif (preg_match(chr(1) . $search . chr(1) . 'i', $value) && ($filter_template == '' || $filter_template == $template->element)) { + if (!isset($positions[$value])) { + $positions[$value] = array(); + } + + $positions[$value][$template->name] = $label; + } + } + } + } + } + + $this->total = count($positions); + + if ($limitstart >= $this->total) { + $limitstart = $limitstart < $limit ? 0 : $limitstart - $limit; + $this->setState('list.start', $limitstart); + } + + if ($ordering == 'value') { + if ($direction == 'asc') { + ksort($positions); + } else { + krsort($positions); + } + } else { + if ($direction == 'asc') { + asort($positions); + } else { + arsort($positions); + } + } + + $this->items = array_slice($positions, $limitstart, $limit ?: null); + } + + return $this->items; + } + + /** + * Method to get the total number of items. + * + * @return integer The total number of items. + * + * @since 1.6 + */ + public function getTotal() + { + if (!isset($this->total)) { + $this->getItems(); + } + + return $this->total; + } } diff --git a/administrator/components/com_modules/src/Model/SelectModel.php b/administrator/components/com_modules/src/Model/SelectModel.php index eb901af523edc..7c880e033aa73 100644 --- a/administrator/components/com_modules/src/Model/SelectModel.php +++ b/administrator/components/com_modules/src/Model/SelectModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest('com_modules.modules.client_id', 'client_id', 0); - $this->setState('client_id', (int) $clientId); - - // Load the parameters. - $params = ComponentHelper::getParams('com_modules'); - $this->setState('params', $params); - - // Manually set limits to get all modules. - $this->setState('list.limit', 0); - $this->setState('list.start', 0); - $this->setState('list.ordering', 'a.name'); - $this->setState('list.direction', 'ASC'); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('client_id'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.extension_id, a.name, a.element AS module' - ) - ); - $query->from($db->quoteName('#__extensions', 'a')); - - // Filter by module - $query->where($db->quoteName('a.type') . ' = ' . $db->quote('module')); - - // Filter by client. - $clientId = (int) $this->getState('client_id'); - $query->where($db->quoteName('a.client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - - // Filter by enabled - $query->where($db->quoteName('a.enabled') . ' = 1'); - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to get a list of items. - * - * @return mixed An array of objects on success, false on failure. - */ - public function getItems() - { - // Get the list of items from the database. - $items = parent::getItems(); - - $client = ApplicationHelper::getClientInfo($this->getState('client_id', 0)); - $lang = Factory::getLanguage(); - - // Loop through the results to add the XML metadata, - // and load language support. - foreach ($items as &$item) - { - $path = Path::clean($client->path . '/modules/' . $item->module . '/' . $item->module . '.xml'); - - if (file_exists($path)) - { - $item->xml = simplexml_load_file($path); - } - else - { - $item->xml = null; - } - - // 1.5 Format; Core files or language packs then - // 1.6 3PD Extension Support - $lang->load($item->module . '.sys', $client->path) - || $lang->load($item->module . '.sys', $client->path . '/modules/' . $item->module); - $item->name = Text::_($item->name); - - if (isset($item->xml) && $text = trim($item->xml->description)) - { - $item->desc = Text::_($text); - } - else - { - $item->desc = Text::_('COM_MODULES_NODESCRIPTION'); - } - } - - $items = ArrayHelper::sortObjects($items, 'name', 1, true, true); - - // @todo: Use the cached XML from the extensions table? - - return $items; - } + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + + // Load the filter state. + $clientId = $app->getUserStateFromRequest('com_modules.modules.client_id', 'client_id', 0); + $this->setState('client_id', (int) $clientId); + + // Load the parameters. + $params = ComponentHelper::getParams('com_modules'); + $this->setState('params', $params); + + // Manually set limits to get all modules. + $this->setState('list.limit', 0); + $this->setState('list.start', 0); + $this->setState('list.ordering', 'a.name'); + $this->setState('list.direction', 'ASC'); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('client_id'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.extension_id, a.name, a.element AS module' + ) + ); + $query->from($db->quoteName('#__extensions', 'a')); + + // Filter by module + $query->where($db->quoteName('a.type') . ' = ' . $db->quote('module')); + + // Filter by client. + $clientId = (int) $this->getState('client_id'); + $query->where($db->quoteName('a.client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + + // Filter by enabled + $query->where($db->quoteName('a.enabled') . ' = 1'); + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to get a list of items. + * + * @return mixed An array of objects on success, false on failure. + */ + public function getItems() + { + // Get the list of items from the database. + $items = parent::getItems(); + + $client = ApplicationHelper::getClientInfo($this->getState('client_id', 0)); + $lang = Factory::getLanguage(); + + // Loop through the results to add the XML metadata, + // and load language support. + foreach ($items as &$item) { + $path = Path::clean($client->path . '/modules/' . $item->module . '/' . $item->module . '.xml'); + + if (file_exists($path)) { + $item->xml = simplexml_load_file($path); + } else { + $item->xml = null; + } + + // 1.5 Format; Core files or language packs then + // 1.6 3PD Extension Support + $lang->load($item->module . '.sys', $client->path) + || $lang->load($item->module . '.sys', $client->path . '/modules/' . $item->module); + $item->name = Text::_($item->name); + + if (isset($item->xml) && $text = trim($item->xml->description)) { + $item->desc = Text::_($text); + } else { + $item->desc = Text::_('COM_MODULES_NODESCRIPTION'); + } + } + + $items = ArrayHelper::sortObjects($items, 'name', 1, true, true); + + // @todo: Use the cached XML from the extensions table? + + return $items; + } } diff --git a/administrator/components/com_modules/src/Service/HTML/Modules.php b/administrator/components/com_modules/src/Service/HTML/Modules.php index 2e850cda51920..c78103167d353 100644 --- a/administrator/components/com_modules/src/Service/HTML/Modules.php +++ b/administrator/components/com_modules/src/Service/HTML/Modules.php @@ -1,4 +1,5 @@ element, $template->name); - } - - return $options; - } - - /** - * Builds an array of template type options - * - * @return array - */ - public function types() - { - $options = array(); - $options[] = HTMLHelper::_('select.option', 'user', 'COM_MODULES_OPTION_POSITION_USER_DEFINED'); - $options[] = HTMLHelper::_('select.option', 'template', 'COM_MODULES_OPTION_POSITION_TEMPLATE_DEFINED'); - - return $options; - } - - /** - * Builds an array of template state options - * - * @return array - */ - public function templateStates() - { - $options = array(); - $options[] = HTMLHelper::_('select.option', '1', 'JENABLED'); - $options[] = HTMLHelper::_('select.option', '0', 'JDISABLED'); - - return $options; - } - - /** - * Returns a published state on a grid - * - * @param integer $value The state value. - * @param integer $i The row index - * @param boolean $enabled An optional setting for access control on the action. - * @param string $checkbox An optional prefix for checkboxes. - * - * @return string The Html code - * - * @see HTMLHelperJGrid::state - * @since 1.7.1 - */ - public function state($value, $i, $enabled = true, $checkbox = 'cb') - { - $states = array( - 1 => array( - 'unpublish', - 'COM_MODULES_EXTENSION_PUBLISHED_ENABLED', - 'COM_MODULES_HTML_UNPUBLISH_ENABLED', - 'COM_MODULES_EXTENSION_PUBLISHED_ENABLED', - true, - 'publish', - 'publish', - ), - 0 => array( - 'publish', - 'COM_MODULES_EXTENSION_UNPUBLISHED_ENABLED', - 'COM_MODULES_HTML_PUBLISH_ENABLED', - 'COM_MODULES_EXTENSION_UNPUBLISHED_ENABLED', - true, - 'unpublish', - 'unpublish', - ), - -1 => array( - 'unpublish', - 'COM_MODULES_EXTENSION_PUBLISHED_DISABLED', - 'COM_MODULES_HTML_UNPUBLISH_DISABLED', - 'COM_MODULES_EXTENSION_PUBLISHED_DISABLED', - true, - 'warning', - 'warning', - ), - -2 => array( - 'publish', - 'COM_MODULES_EXTENSION_UNPUBLISHED_DISABLED', - 'COM_MODULES_HTML_PUBLISH_DISABLED', - 'COM_MODULES_EXTENSION_UNPUBLISHED_DISABLED', - true, - 'unpublish', - 'unpublish', - ), - ); - - return HTMLHelper::_('jgrid.state', $states, $value, $i, 'modules.', $enabled, true, $checkbox); - } - - /** - * Display a batch widget for the module position selector. - * - * @param integer $clientId The client ID. - * @param integer $state The state of the module (enabled, unenabled, trashed). - * @param string $selectedPosition The currently selected position for the module. - * - * @return string The necessary positions for the widget. - * - * @since 2.5 - */ - public function positions($clientId, $state = 1, $selectedPosition = '') - { - $templates = array_keys(ModulesHelper::getTemplates($clientId, $state)); - $templateGroups = array(); - - // Add an empty value to be able to deselect a module position - $option = ModulesHelper::createOption('', Text::_('COM_MODULES_NONE')); - $templateGroups[''] = ModulesHelper::createOptionGroup('', array($option)); - - // Add positions from templates - $isTemplatePosition = false; - - foreach ($templates as $template) - { - $options = array(); - - $positions = TemplatesHelper::getPositions($clientId, $template); - - if (is_array($positions)) - { - foreach ($positions as $position) - { - $text = ModulesHelper::getTranslatedModulePosition($clientId, $template, $position) . ' [' . $position . ']'; - $options[] = ModulesHelper::createOption($position, $text); - - if (!$isTemplatePosition && $selectedPosition === $position) - { - $isTemplatePosition = true; - } - } - - $options = ArrayHelper::sortObjects($options, 'text'); - } - - $templateGroups[$template] = ModulesHelper::createOptionGroup(ucfirst($template), $options); - } - - // Add custom position to options - $customGroupText = Text::_('COM_MODULES_CUSTOM_POSITION'); - $editPositions = true; - $customPositions = ModulesHelper::getPositions($clientId, $editPositions); - - $app = Factory::getApplication(); - - $position = $app->getUserState('com_modules.modules.' . $clientId . '.filter.position'); - - if ($position) - { - $customPositions[] = HTMLHelper::_('select.option', $position); - - $customPositions = array_unique($customPositions, SORT_REGULAR); - } - - $templateGroups[$customGroupText] = ModulesHelper::createOptionGroup($customGroupText, $customPositions); - - return $templateGroups; - } - - /** - * Get a select with the batch action options - * - * @return void - */ - public function batchOptions() - { - // Create the copy/move options. - $options = array( - HTMLHelper::_('select.option', 'c', Text::_('JLIB_HTML_BATCH_COPY')), - HTMLHelper::_('select.option', 'm', Text::_('JLIB_HTML_BATCH_MOVE')) - ); - - echo HTMLHelper::_('select.radiolist', $options, 'batch[move_copy]', '', 'value', 'text', 'm'); - } - - /** - * Method to get the field options. - * - * @param integer $clientId The client ID - * - * @return array The field option objects. - * - * @since 2.5 - * - * @deprecated 5.0 Will be removed with no replacement - */ - public function positionList($clientId = 0) - { - $clientId = (int) $clientId; - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('position', 'value')) - ->select($db->quoteName('position', 'text')) - ->from($db->quoteName('#__modules')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->order($db->quoteName('position')) - ->bind(':clientid', $clientId, ParameterType::INTEGER); - - // Get the options. - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Pop the first item off the array if it's blank - if (count($options)) - { - if (strlen($options[0]->text) < 1) - { - array_shift($options); - } - } - - return $options; - } + /** + * Builds an array of template options + * + * @param integer $clientId The client id. + * @param string $state The state of the template. + * + * @return array + */ + public function templates($clientId = 0, $state = '') + { + $options = array(); + $templates = ModulesHelper::getTemplates($clientId, $state); + + foreach ($templates as $template) { + $options[] = HTMLHelper::_('select.option', $template->element, $template->name); + } + + return $options; + } + + /** + * Builds an array of template type options + * + * @return array + */ + public function types() + { + $options = array(); + $options[] = HTMLHelper::_('select.option', 'user', 'COM_MODULES_OPTION_POSITION_USER_DEFINED'); + $options[] = HTMLHelper::_('select.option', 'template', 'COM_MODULES_OPTION_POSITION_TEMPLATE_DEFINED'); + + return $options; + } + + /** + * Builds an array of template state options + * + * @return array + */ + public function templateStates() + { + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', 'JENABLED'); + $options[] = HTMLHelper::_('select.option', '0', 'JDISABLED'); + + return $options; + } + + /** + * Returns a published state on a grid + * + * @param integer $value The state value. + * @param integer $i The row index + * @param boolean $enabled An optional setting for access control on the action. + * @param string $checkbox An optional prefix for checkboxes. + * + * @return string The Html code + * + * @see HTMLHelperJGrid::state + * @since 1.7.1 + */ + public function state($value, $i, $enabled = true, $checkbox = 'cb') + { + $states = array( + 1 => array( + 'unpublish', + 'COM_MODULES_EXTENSION_PUBLISHED_ENABLED', + 'COM_MODULES_HTML_UNPUBLISH_ENABLED', + 'COM_MODULES_EXTENSION_PUBLISHED_ENABLED', + true, + 'publish', + 'publish', + ), + 0 => array( + 'publish', + 'COM_MODULES_EXTENSION_UNPUBLISHED_ENABLED', + 'COM_MODULES_HTML_PUBLISH_ENABLED', + 'COM_MODULES_EXTENSION_UNPUBLISHED_ENABLED', + true, + 'unpublish', + 'unpublish', + ), + -1 => array( + 'unpublish', + 'COM_MODULES_EXTENSION_PUBLISHED_DISABLED', + 'COM_MODULES_HTML_UNPUBLISH_DISABLED', + 'COM_MODULES_EXTENSION_PUBLISHED_DISABLED', + true, + 'warning', + 'warning', + ), + -2 => array( + 'publish', + 'COM_MODULES_EXTENSION_UNPUBLISHED_DISABLED', + 'COM_MODULES_HTML_PUBLISH_DISABLED', + 'COM_MODULES_EXTENSION_UNPUBLISHED_DISABLED', + true, + 'unpublish', + 'unpublish', + ), + ); + + return HTMLHelper::_('jgrid.state', $states, $value, $i, 'modules.', $enabled, true, $checkbox); + } + + /** + * Display a batch widget for the module position selector. + * + * @param integer $clientId The client ID. + * @param integer $state The state of the module (enabled, unenabled, trashed). + * @param string $selectedPosition The currently selected position for the module. + * + * @return string The necessary positions for the widget. + * + * @since 2.5 + */ + public function positions($clientId, $state = 1, $selectedPosition = '') + { + $templates = array_keys(ModulesHelper::getTemplates($clientId, $state)); + $templateGroups = array(); + + // Add an empty value to be able to deselect a module position + $option = ModulesHelper::createOption('', Text::_('COM_MODULES_NONE')); + $templateGroups[''] = ModulesHelper::createOptionGroup('', array($option)); + + // Add positions from templates + $isTemplatePosition = false; + + foreach ($templates as $template) { + $options = array(); + + $positions = TemplatesHelper::getPositions($clientId, $template); + + if (is_array($positions)) { + foreach ($positions as $position) { + $text = ModulesHelper::getTranslatedModulePosition($clientId, $template, $position) . ' [' . $position . ']'; + $options[] = ModulesHelper::createOption($position, $text); + + if (!$isTemplatePosition && $selectedPosition === $position) { + $isTemplatePosition = true; + } + } + + $options = ArrayHelper::sortObjects($options, 'text'); + } + + $templateGroups[$template] = ModulesHelper::createOptionGroup(ucfirst($template), $options); + } + + // Add custom position to options + $customGroupText = Text::_('COM_MODULES_CUSTOM_POSITION'); + $editPositions = true; + $customPositions = ModulesHelper::getPositions($clientId, $editPositions); + + $app = Factory::getApplication(); + + $position = $app->getUserState('com_modules.modules.' . $clientId . '.filter.position'); + + if ($position) { + $customPositions[] = HTMLHelper::_('select.option', $position); + + $customPositions = array_unique($customPositions, SORT_REGULAR); + } + + $templateGroups[$customGroupText] = ModulesHelper::createOptionGroup($customGroupText, $customPositions); + + return $templateGroups; + } + + /** + * Get a select with the batch action options + * + * @return void + */ + public function batchOptions() + { + // Create the copy/move options. + $options = array( + HTMLHelper::_('select.option', 'c', Text::_('JLIB_HTML_BATCH_COPY')), + HTMLHelper::_('select.option', 'm', Text::_('JLIB_HTML_BATCH_MOVE')) + ); + + echo HTMLHelper::_('select.radiolist', $options, 'batch[move_copy]', '', 'value', 'text', 'm'); + } + + /** + * Method to get the field options. + * + * @param integer $clientId The client ID + * + * @return array The field option objects. + * + * @since 2.5 + * + * @deprecated 5.0 Will be removed with no replacement + */ + public function positionList($clientId = 0) + { + $clientId = (int) $clientId; + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('position', 'value')) + ->select($db->quoteName('position', 'text')) + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->order($db->quoteName('position')) + ->bind(':clientid', $clientId, ParameterType::INTEGER); + + // Get the options. + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Pop the first item off the array if it's blank + if (count($options)) { + if (strlen($options[0]->text) < 1) { + array_shift($options); + } + } + + return $options; + } } diff --git a/administrator/components/com_modules/src/View/Module/HtmlView.php b/administrator/components/com_modules/src/View/Module/HtmlView.php index 5a21ecb1ee68e..82d1da9a21b5b 100644 --- a/administrator/components/com_modules/src/View/Module/HtmlView.php +++ b/administrator/components/com_modules/src/View/Module/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - $this->canDo = ContentHelper::getActions('com_modules', 'module', $this->item->id); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); - $canDo = $this->canDo; - - ToolbarHelper::title(Text::sprintf('COM_MODULES_MANAGER_MODULE', Text::_($this->item->module)), 'cube module'); - - // For new records, check the create permission. - if ($isNew && $canDo->get('core.create')) - { - ToolbarHelper::apply('module.apply'); - - ToolbarHelper::saveGroup( - [ - ['save', 'module.save'], - ['save2new', 'module.save2new'] - ], - 'btn-success' - ); - - ToolbarHelper::cancel('module.cancel'); - } - else - { - $toolbarButtons = []; - - // Can't save the record if it's checked out. - if (!$checkedOut) - { - // Since it's an existing record, check the edit permission. - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('module.apply'); - - $toolbarButtons[] = ['save', 'module.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'module.save2new']; - } - } - } - - // If checked out, we can still save - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'module.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('module.cancel', 'JTOOLBAR_CLOSE'); - } - - // Get the help information for the menu item. - $lang = Factory::getLanguage(); - - $help = $this->get('Help'); - - if ($lang->hasKey($help->url)) - { - $debug = $lang->setDebug(false); - $url = Text::_($help->url); - $lang->setDebug($debug); - } - else - { - $url = null; - } - - ToolbarHelper::inlinehelp(); - ToolbarHelper::help($help->key, false, $url); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 4.0.0 + */ + protected $canDo; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + $this->canDo = ContentHelper::getActions('com_modules', 'module', $this->item->id); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); + $canDo = $this->canDo; + + ToolbarHelper::title(Text::sprintf('COM_MODULES_MANAGER_MODULE', Text::_($this->item->module)), 'cube module'); + + // For new records, check the create permission. + if ($isNew && $canDo->get('core.create')) { + ToolbarHelper::apply('module.apply'); + + ToolbarHelper::saveGroup( + [ + ['save', 'module.save'], + ['save2new', 'module.save2new'] + ], + 'btn-success' + ); + + ToolbarHelper::cancel('module.cancel'); + } else { + $toolbarButtons = []; + + // Can't save the record if it's checked out. + if (!$checkedOut) { + // Since it's an existing record, check the edit permission. + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('module.apply'); + + $toolbarButtons[] = ['save', 'module.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'module.save2new']; + } + } + } + + // If checked out, we can still save + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'module.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('module.cancel', 'JTOOLBAR_CLOSE'); + } + + // Get the help information for the menu item. + $lang = Factory::getLanguage(); + + $help = $this->get('Help'); + + if ($lang->hasKey($help->url)) { + $debug = $lang->setDebug(false); + $url = Text::_($help->url); + $lang->setDebug($debug); + } else { + $url = null; + } + + ToolbarHelper::inlinehelp(); + ToolbarHelper::help($help->key, false, $url); + } } diff --git a/administrator/components/com_modules/src/View/Modules/HtmlView.php b/administrator/components/com_modules/src/View/Modules/HtmlView.php index 853ae9b074b13..ebc4e71a23213 100644 --- a/administrator/components/com_modules/src/View/Modules/HtmlView.php +++ b/administrator/components/com_modules/src/View/Modules/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->total = $this->get('Total'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->clientId = $this->state->get('client_id'); - - if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - /** - * The code below make sure the remembered position will be available from filter dropdown even if there are no - * modules available for this position. This will make the UI less confusing for users in case there is only one - * module in the selected position and user: - * 1. Edit the module, change it to new position, save it and come back to Modules Management Screen - * 2. Or move that module to new position using Batch action - */ - if (count($this->items) === 0 && $this->state->get('filter.position')) - { - $selectedPosition = $this->state->get('filter.position'); - $positionField = $this->filterForm->getField('position', 'filter'); - - $positionExists = false; - - foreach ($positionField->getOptions() as $option) - { - if ($option->value === $selectedPosition) - { - $positionExists = true; - break; - } - } - - if ($positionExists === false) - { - $positionField->addOption($selectedPosition, ['value' => $selectedPosition]); - } - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // We do not need the Language filter when modules are not filtered - if ($this->clientId == 1 && !ModuleHelper::isAdminMultilang()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - - // We don't need the toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - // If in modal layout. - else - { - // Client id selector should not exist. - $this->filterForm->removeField('client_id', ''); - - // If in the frontend state and language should not activate the search tools. - if (Factory::getApplication()->isClient('site')) - { - unset($this->activeFilters['state']); - unset($this->activeFilters['language']); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $state = $this->get('State'); - $canDo = ContentHelper::getActions('com_modules'); - $user = $this->getCurrentUser(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($state->get('client_id') == 1) - { - ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_ADMIN'), 'cube module'); - } - else - { - ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_SITE'), 'cube module'); - } - - if ($canDo->get('core.create')) - { - $toolbar->standardButton('new', 'JTOOLBAR_NEW') - ->onclick("location.href='index.php?option=com_modules&view=select&client_id=" . $this->state->get('client_id', 0) . "'"); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('modules.publish')->listCheck(true); - - $childBar->unpublish('modules.unpublish')->listCheck(true); - } - - if ($this->getCurrentUser()->authorise('core.admin')) - { - $childBar->checkin('modules.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) - { - $childBar->trash('modules.trash')->listCheck(true); - } - - // Add a batch button - if ($user->authorise('core.create', 'com_modules') && $user->authorise('core.edit', 'com_modules') - && $user->authorise('core.edit.state', 'com_modules')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - - if ($canDo->get('core.create')) - { - $childBar->standardButton('copy') - ->text('JTOOLBAR_DUPLICATE') - ->task('modules.duplicate') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && ($state->get('filter.state') == -2 && $canDo->get('core.delete'))) - { - $toolbar->delete('modules.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin')) - { - $toolbar->preferences('com_modules'); - } - - $toolbar->help('Modules'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->total = $this->get('Total'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->clientId = $this->state->get('client_id'); + + if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + /** + * The code below make sure the remembered position will be available from filter dropdown even if there are no + * modules available for this position. This will make the UI less confusing for users in case there is only one + * module in the selected position and user: + * 1. Edit the module, change it to new position, save it and come back to Modules Management Screen + * 2. Or move that module to new position using Batch action + */ + if (count($this->items) === 0 && $this->state->get('filter.position')) { + $selectedPosition = $this->state->get('filter.position'); + $positionField = $this->filterForm->getField('position', 'filter'); + + $positionExists = false; + + foreach ($positionField->getOptions() as $option) { + if ($option->value === $selectedPosition) { + $positionExists = true; + break; + } + } + + if ($positionExists === false) { + $positionField->addOption($selectedPosition, ['value' => $selectedPosition]); + } + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // We do not need the Language filter when modules are not filtered + if ($this->clientId == 1 && !ModuleHelper::isAdminMultilang()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + + // We don't need the toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } + // If in modal layout. + else { + // Client id selector should not exist. + $this->filterForm->removeField('client_id', ''); + + // If in the frontend state and language should not activate the search tools. + if (Factory::getApplication()->isClient('site')) { + unset($this->activeFilters['state']); + unset($this->activeFilters['language']); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $state = $this->get('State'); + $canDo = ContentHelper::getActions('com_modules'); + $user = $this->getCurrentUser(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($state->get('client_id') == 1) { + ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_ADMIN'), 'cube module'); + } else { + ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_SITE'), 'cube module'); + } + + if ($canDo->get('core.create')) { + $toolbar->standardButton('new', 'JTOOLBAR_NEW') + ->onclick("location.href='index.php?option=com_modules&view=select&client_id=" . $this->state->get('client_id', 0) . "'"); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('modules.publish')->listCheck(true); + + $childBar->unpublish('modules.unpublish')->listCheck(true); + } + + if ($this->getCurrentUser()->authorise('core.admin')) { + $childBar->checkin('modules.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) { + $childBar->trash('modules.trash')->listCheck(true); + } + + // Add a batch button + if ( + $user->authorise('core.create', 'com_modules') && $user->authorise('core.edit', 'com_modules') + && $user->authorise('core.edit.state', 'com_modules') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + + if ($canDo->get('core.create')) { + $childBar->standardButton('copy') + ->text('JTOOLBAR_DUPLICATE') + ->task('modules.duplicate') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && ($state->get('filter.state') == -2 && $canDo->get('core.delete'))) { + $toolbar->delete('modules.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin')) { + $toolbar->preferences('com_modules'); + } + + $toolbar->help('Modules'); + } } diff --git a/administrator/components/com_modules/src/View/Select/HtmlView.php b/administrator/components/com_modules/src/View/Select/HtmlView.php index 3084005611456..562da54dd1c0e 100644 --- a/administrator/components/com_modules/src/View/Select/HtmlView.php +++ b/administrator/components/com_modules/src/View/Select/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->modalLink = ''; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $state = $this->get('State'); - $clientId = (int) $state->get('client_id', 0); - - // Add page title - ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_SITE'), 'cube module'); - - if ($clientId === 1) - { - ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_ADMIN'), 'cube module'); - } - - // Get the toolbar object instance - $bar = Toolbar::getInstance('toolbar'); - - // Instantiate a new FileLayout instance and render the layout - $layout = new FileLayout('toolbar.cancelselect'); - - $bar->appendButton('Custom', $layout->render(array('client_id' => $clientId)), 'new'); - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * A suffix for links for modal use + * + * @var string + */ + protected $modalLink; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->modalLink = ''; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $state = $this->get('State'); + $clientId = (int) $state->get('client_id', 0); + + // Add page title + ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_SITE'), 'cube module'); + + if ($clientId === 1) { + ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_ADMIN'), 'cube module'); + } + + // Get the toolbar object instance + $bar = Toolbar::getInstance('toolbar'); + + // Instantiate a new FileLayout instance and render the layout + $layout = new FileLayout('toolbar.cancelselect'); + + $bar->appendButton('Custom', $layout->render(array('client_id' => $clientId)), 'new'); + } } diff --git a/administrator/components/com_modules/tmpl/module/edit.php b/administrator/components/com_modules/tmpl/module/edit.php index 982bbf002d4fc..0dbdb4680d460 100644 --- a/administrator/components/com_modules/tmpl/module/edit.php +++ b/administrator/components/com_modules/tmpl/module/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_modules.admin-module-edit'); + ->useScript('form.validate') + ->useScript('com_modules.admin-module-edit'); $input = Factory::getApplication()->input; @@ -54,151 +54,144 @@
    - - -
    - 'general', 'recall' => true, 'breakpoint' => 768]); ?> - - - -
    -
    - item->xml) : ?> - item->xml->description) : ?> -

    - item->xml) - { - echo ($text = (string) $this->item->xml->name) ? Text::_($text) : $this->item->module; - } - else - { - echo Text::_('COM_MODULES_ERR_XML'); - } - ?> -

    -
    - - item->client_id == 0 ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); ?> - -
    -
    - fieldset = 'description'; - $short_description = Text::_($this->item->xml->description); - $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); - - if (!$long_description) - { - $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); - - if (strlen($truncated) > 500) - { - $long_description = $short_description; - $short_description = HTMLHelper::_('string.truncate', $truncated, 250); - - if ($short_description == $long_description) - { - $long_description = ''; - } - } - } - ?> -

    - -

    - - - -

    - -
    - - -
    - - -
    - - form->getInput($hasContentFieldName); - } - $this->fieldset = 'basic'; - $html = LayoutHelper::render('joomla.edit.fieldset', $this); - echo $html ? '
    ' . $html : ''; - ?> -
    -
    - fields = array( - 'showtitle', - 'position', - 'published', - 'publish_up', - 'publish_down', - 'access', - 'ordering', - 'language', - 'note' - ); - - ?> - item->client_id == 0) : ?> - - - - -
    -
    - - - - -
    -
    - -
    -
    - - - - item->client_id == 0) : ?> - -
    - -
    - loadTemplate('assignment'); ?> -
    -
    - - - - fieldsets = array(); - $this->ignore_fieldsets = array('basic', 'description'); - echo LayoutHelper::render('joomla.edit.params', $this); - ?> - - canDo->get('core.admin')) : ?> - -
    - -
    - form->getInput('rules'); ?> -
    -
    - - - - - - - - - form->getInput('module'); ?> - form->getInput('client_id'); ?> -
    + + +
    + 'general', 'recall' => true, 'breakpoint' => 768]); ?> + + + +
    +
    + item->xml) : ?> + item->xml->description) : ?> +

    + item->xml) { + echo ($text = (string) $this->item->xml->name) ? Text::_($text) : $this->item->module; + } else { + echo Text::_('COM_MODULES_ERR_XML'); + } + ?> +

    +
    + + item->client_id == 0 ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); ?> + +
    +
    + fieldset = 'description'; + $short_description = Text::_($this->item->xml->description); + $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); + + if (!$long_description) { + $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); + + if (strlen($truncated) > 500) { + $long_description = $short_description; + $short_description = HTMLHelper::_('string.truncate', $truncated, 250); + + if ($short_description == $long_description) { + $long_description = ''; + } + } + } + ?> +

    + +

    + + + +

    + +
    + + +
    + + +
    + + form->getInput($hasContentFieldName); + } + $this->fieldset = 'basic'; + $html = LayoutHelper::render('joomla.edit.fieldset', $this); + echo $html ? '
    ' . $html : ''; + ?> +
    +
    + fields = array( + 'showtitle', + 'position', + 'published', + 'publish_up', + 'publish_down', + 'access', + 'ordering', + 'language', + 'note' + ); + + ?> + item->client_id == 0) : ?> + + + + +
    +
    + + + + +
    +
    + +
    +
    + + + + item->client_id == 0) : ?> + +
    + +
    + loadTemplate('assignment'); ?> +
    +
    + + + + fieldsets = array(); + $this->ignore_fieldsets = array('basic', 'description'); + echo LayoutHelper::render('joomla.edit.params', $this); + ?> + + canDo->get('core.admin')) : ?> + +
    + +
    + form->getInput('rules'); ?> +
    +
    + + + + + + + + + form->getInput('module'); ?> + form->getInput('client_id'); ?> +
    diff --git a/administrator/components/com_modules/tmpl/module/edit_assignment.php b/administrator/components/com_modules/tmpl/module/edit_assignment.php index bcd98ef2b1056..311e19998e796 100644 --- a/administrator/components/com_modules/tmpl/module/edit_assignment.php +++ b/administrator/components/com_modules/tmpl/module/edit_assignment.php @@ -1,4 +1,5 @@ document->getWebAssetManager() - ->useScript('joomla.treeselectmenu') - ->useScript('com_modules.admin-module-edit-assignment'); + ->useScript('joomla.treeselectmenu') + ->useScript('com_modules.admin-module-edit-assignment'); ?>
    - -
    - -
    + +
    + +
    + + diff --git a/administrator/components/com_modules/tmpl/module/modal.php b/administrator/components/com_modules/tmpl/module/modal.php index bb8088a027279..b90680e779098 100644 --- a/administrator/components/com_modules/tmpl/module/modal.php +++ b/administrator/components/com_modules/tmpl/module/modal.php @@ -1,4 +1,5 @@
    - setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
    diff --git a/administrator/components/com_modules/tmpl/modules/default.php b/administrator/components/com_modules/tmpl/modules/default.php index c3bccaa60f501..d4ba1df7fdf59 100644 --- a/administrator/components/com_modules/tmpl/modules/default.php +++ b/administrator/components/com_modules/tmpl/modules/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $clientId = (int) $this->state->get('client_id', 0); $user = Factory::getUser(); @@ -29,191 +30,191 @@ $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = ($listOrder == 'a.ordering'); -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_modules&task=modules.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_modules&task=modules.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
    -
    - $this)); ?> - total > 0) : ?> - - - - - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="false"> - items as $i => $item) : - $ordering = ($listOrder == 'a.ordering'); - $canCreate = $user->authorise('core.create', 'com_modules'); - $canEdit = $user->authorise('core.edit', 'com_modules.module.' . $item->id); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id')|| is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_modules.module.' . $item->id) && $canCheckin; - ?> - - - - - + + + + + + + + + + + + + + + +
    - , - , - -
    - - - - - - - - - - - - - - - - - - - - - -
    - id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - - enabled > 0) : ?> - published, $i, 'modules.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> - - - - - - - -
    - checked_out) : ?> - editor, $item->checked_out_time, 'modules.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - +
    + $this)); ?> + total > 0) : ?> + + + + + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="false"> + items as $i => $item) : + $ordering = ($listOrder == 'a.ordering'); + $canCreate = $user->authorise('core.create', 'com_modules'); + $canEdit = $user->authorise('core.edit', 'com_modules.module.' . $item->id); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_modules.module.' . $item->id) && $canCheckin; + ?> + + + + + - - - - - - - - - - - - - - - -
    + , + , + +
    + + + + + + + + + + + + + + + + + + + + + +
    + id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + enabled > 0) : ?> + published, $i, 'modules.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + + + + + + + +
    + checked_out) : ?> + editor, $item->checked_out_time, 'modules.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + - note)) : ?> -
    - escape($item->note)); ?> -
    - -
    -
    - position) : ?> - - position; ?> - - - - - - - - name; ?> - - pages; ?> - - escape($item->access_level); ?> - - - - language == ''):?> - - language == '*'):?> - - - escape($item->language); ?> - - - id; ?> -
    + note)) : ?> +
    + escape($item->note)); ?> +
    + +
    +
    + position) : ?> + + position; ?> + + + + + + + + name; ?> + + pages; ?> + + escape($item->access_level); ?> + + + + language == '') :?> + + language == '*') :?> + + + escape($item->language); ?> + + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - authorise('core.create', 'com_modules') - && $user->authorise('core.edit', 'com_modules') - && $user->authorise('core.edit.state', 'com_modules')) : ?> - Text::_('COM_MODULES_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - - - -
    + + authorise('core.create', 'com_modules') + && $user->authorise('core.edit', 'com_modules') + && $user->authorise('core.edit.state', 'com_modules') +) : ?> + Text::_('COM_MODULES_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + + + +
    diff --git a/administrator/components/com_modules/tmpl/modules/default_batch_body.php b/administrator/components/com_modules/tmpl/modules/default_batch_body.php index 2a52e9c45f60c..4683baede1166 100644 --- a/administrator/components/com_modules/tmpl/modules/default_batch_body.php +++ b/administrator/components/com_modules/tmpl/modules/default_batch_body.php @@ -1,4 +1,5 @@ 'batch-position-id', + 'id' => 'batch-position-id', ); Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); $this->document->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select') - ->useScript('joomla.batch-copymove'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select') + ->useScript('joomla.batch-copymove'); ?>
    -

    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    - = 0) : ?> -
    -
    - -
    - - - -
    - -
    -
    -
    - -
    -
    +

    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + = 0) : ?> +
    +
    + +
    + + + +
    + +
    +
    +
    + +
    +
    diff --git a/administrator/components/com_modules/tmpl/modules/default_batch_footer.php b/administrator/components/com_modules/tmpl/modules/default_batch_footer.php index 44f8345ac2384..0a684547cb746 100644 --- a/administrator/components/com_modules/tmpl/modules/default_batch_footer.php +++ b/administrator/components/com_modules/tmpl/modules/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/administrator/components/com_modules/tmpl/modules/emptystate.php b/administrator/components/com_modules/tmpl/modules/emptystate.php index 323715f6070cd..f0b2b02ed1d2a 100644 --- a/administrator/components/com_modules/tmpl/modules/emptystate.php +++ b/administrator/components/com_modules/tmpl/modules/emptystate.php @@ -1,4 +1,5 @@ 'COM_MODULES', - 'formURL' => 'index.php?option=com_modules&view=select&client_id=' . $this->clientId, - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Module', - 'icon' => 'icon-cube module', - // Although it is (almost) impossible to get to this page with no created Administrator Modules, we add this for completeness. - 'title' => Text::_('COM_MODULES_EMPTYSTATE_TITLE_' . ($this->clientId ? 'ADMINISTRATOR' : 'SITE')), + 'textPrefix' => 'COM_MODULES', + 'formURL' => 'index.php?option=com_modules&view=select&client_id=' . $this->clientId, + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Module', + 'icon' => 'icon-cube module', + // Although it is (almost) impossible to get to this page with no created Administrator Modules, we add this for completeness. + 'title' => Text::_('COM_MODULES_EMPTYSTATE_TITLE_' . ($this->clientId ? 'ADMINISTRATOR' : 'SITE')), ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_modules')) -{ - $displayData['createURL'] = 'index.php?option=com_modules&view=select&client_id=' . $this->clientId; +if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_modules')) { + $displayData['createURL'] = 'index.php?option=com_modules&view=select&client_id=' . $this->clientId; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_modules/tmpl/modules/modal.php b/administrator/components/com_modules/tmpl/modules/modal.php index 972919a7dd79f..3c24c77f552c0 100644 --- a/administrator/components/com_modules/tmpl/modules/modal.php +++ b/administrator/components/com_modules/tmpl/modules/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if (Factory::getApplication()->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ @@ -30,109 +30,108 @@ $editor = Factory::getApplication()->input->get('editor', '', 'cmd'); $link = 'index.php?option=com_modules&view=modules&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; -if (!empty($editor)) -{ - $link .= '&editor=' . $editor; +if (!empty($editor)) { + $link .= '&editor=' . $editor; } ?>
    -
    + - $this)); ?> + $this)); ?> - total > 0) : ?> - - - - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', - ); - foreach ($this->items as $i => $item) : - ?> - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - - - -
    - - - - - - escape($item->title); ?> - - - position) : ?> - escape($item->position); ?> - - - - - name; ?> - - pages; ?> - - escape($item->access_level); ?> - - - - id; ?> -
    + total > 0) : ?> + + + + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', + ); + foreach ($this->items as $i => $item) : + ?> + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + + + +
    + + + + + + escape($item->title); ?> + + + position) : ?> + escape($item->position); ?> + + + + + name; ?> + + pages; ?> + + escape($item->access_level); ?> + + + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - + + + + -
    +
    diff --git a/administrator/components/com_modules/tmpl/select/default.php b/administrator/components/com_modules/tmpl/select/default.php index 344db412c06b9..e335ed5b81026 100644 --- a/administrator/components/com_modules/tmpl/select/default.php +++ b/administrator/components/com_modules/tmpl/select/default.php @@ -1,4 +1,5 @@ useScript('com_modules.admin-module-search'); if ($function) : - $wa->useScript('com_modules.admin-select-modal'); + $wa->useScript('com_modules.admin-select-modal'); endif; ?>
    -
    -
    - -
    - -
    - -
    -
    -
    -
    +
    +
    + +
    + +
    + +
    +
    +
    +
    -
    -
    - - -
    -

    - -

    -
    - items as &$item) : ?> - - state->get('client_id', 0) . $this->modalLink . '&eid=' . $item->extension_id; ?> - escape($item->name); ?> - escape(strip_tags($item->desc)), 200); ?> - -
    -

    -

    - -

    -
    - - - -
    - -
    -
    +
    +
    + + +
    +

    + +

    +
    + items as &$item) : ?> + + state->get('client_id', 0) . $this->modalLink . '&eid=' . $item->extension_id; ?> + escape($item->name); ?> + escape(strip_tags($item->desc)), 200); ?> + +
    +

    +

    + +

    +
    + + + +
    + +
    +
    diff --git a/administrator/components/com_modules/tmpl/select/modal.php b/administrator/components/com_modules/tmpl/select/modal.php index 2e6998d652bb3..25ac8d0afdee1 100644 --- a/administrator/components/com_modules/tmpl/select/modal.php +++ b/administrator/components/com_modules/tmpl/select/modal.php @@ -1,4 +1,5 @@ modalLink = '&tmpl=component&view=module&layout=modal'; ?>
    - setLayout('default'); ?> - loadTemplate(); ?> + setLayout('default'); ?> + loadTemplate(); ?>
    diff --git a/administrator/components/com_newsfeeds/helpers/newsfeeds.php b/administrator/components/com_newsfeeds/helpers/newsfeeds.php index 608aeabc716dc..2ddd74bb3ffbb 100644 --- a/administrator/components/com_newsfeeds/helpers/newsfeeds.php +++ b/administrator/components/com_newsfeeds/helpers/newsfeeds.php @@ -1,4 +1,5 @@ set(AssociationExtensionInterface::class, new AssociationsHelper); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->set(AssociationExtensionInterface::class, new AssociationsHelper()); - $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Newsfeeds')); - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Newsfeeds')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Newsfeeds')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Newsfeeds')); + $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Newsfeeds')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Newsfeeds')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Newsfeeds')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Newsfeeds')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new NewsfeedsComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new NewsfeedsComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); - $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); + $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_newsfeeds/src/Controller/AjaxController.php b/administrator/components/com_newsfeeds/src/Controller/AjaxController.php index 34487acbf8275..8f52ecc25b780 100644 --- a/administrator/components/com_newsfeeds/src/Controller/AjaxController.php +++ b/administrator/components/com_newsfeeds/src/Controller/AjaxController.php @@ -1,4 +1,5 @@ input->getInt('assocId', 0); + /** + * Method to fetch associations of a newsfeed + * + * The method assumes that the following http parameters are passed in an Ajax Get request: + * token: the form token + * assocId: the id of the newsfeed whose associations are to be returned + * excludeLang: the association for this language is to be excluded + * + * @return null + * + * @since 3.9.0 + */ + public function fetchAssociations() + { + if (!Session::checkToken('get')) { + echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true); + } else { + $assocId = $this->input->getInt('assocId', 0); - if ($assocId == 0) - { - echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); + if ($assocId == 0) { + echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); - return; - } + return; + } - $excludeLang = $this->input->get('excludeLang', '', 'STRING'); + $excludeLang = $this->input->get('excludeLang', '', 'STRING'); - $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', (int) $assocId); + $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', (int) $assocId); - unset($associations[$excludeLang]); + unset($associations[$excludeLang]); - // Add the title to each of the associated records - $newsfeedsTable = $this->factory->createTable('Newsfeed', 'Administrator'); + // Add the title to each of the associated records + $newsfeedsTable = $this->factory->createTable('Newsfeed', 'Administrator'); - foreach ($associations as $lang => $association) - { - $newsfeedsTable->load($association->id); - $associations[$lang]->title = $newsfeedsTable->name; - } + foreach ($associations as $lang => $association) { + $newsfeedsTable->load($association->id); + $associations[$lang]->title = $newsfeedsTable->name; + } - $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); + $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); - if (count($associations) == 0) - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); - } - elseif ($countContentLanguages > count($associations) + 2) - { - $tags = implode(', ', array_keys($associations)); - $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); - } - else - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); - } + if (count($associations) == 0) { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); + } elseif ($countContentLanguages > count($associations) + 2) { + $tags = implode(', ', array_keys($associations)); + $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); + } else { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); + } - echo new JsonResponse($associations, $message); - } - } + echo new JsonResponse($associations, $message); + } + } } diff --git a/administrator/components/com_newsfeeds/src/Controller/DisplayController.php b/administrator/components/com_newsfeeds/src/Controller/DisplayController.php index 7eeabbb78ade9..d03a6e7dcc032 100644 --- a/administrator/components/com_newsfeeds/src/Controller/DisplayController.php +++ b/administrator/components/com_newsfeeds/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'newsfeeds'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', 'newsfeeds'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($view == 'newsfeed' && $layout == 'edit' && !$this->checkEditId('com_newsfeeds.edit.newsfeed', $id)) - { - // 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', $id), 'error'); - } + // Check for edit form. + if ($view == 'newsfeed' && $layout == 'edit' && !$this->checkEditId('com_newsfeeds.edit.newsfeed', $id)) { + // 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', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_newsfeeds&view=newsfeeds', false)); + $this->setRedirect(Route::_('index.php?option=com_newsfeeds&view=newsfeeds', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/administrator/components/com_newsfeeds/src/Controller/NewsfeedController.php b/administrator/components/com_newsfeeds/src/Controller/NewsfeedController.php index 0ef1f40524d42..f4e3c9a354b37 100644 --- a/administrator/components/com_newsfeeds/src/Controller/NewsfeedController.php +++ b/administrator/components/com_newsfeeds/src/Controller/NewsfeedController.php @@ -1,4 +1,5 @@ input->getInt('filter_category_id'), 'int'); - $allow = null; - - if ($categoryId) - { - // If the category has been passed in the URL check it. - $allow = $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); - } - - if ($allow === null) - { - // In the absence of better information, revert to the component permissions. - return parent::allowAdd($data); - } - else - { - return $allow; - } - } - - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - - // Since there is no asset tracking, fallback to the component permissions. - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Get the item. - $item = $this->getModel()->getItem($recordId); - - // Since there is no item, return false. - if (empty($item)) - { - return false; - } - - $user = $this->app->getIdentity(); - - // Check if can edit own core.edit.own. - $canEditOwn = $user->authorise('core.edit.own', $this->option . '.category.' . (int) $item->catid) && $item->created_by == $user->id; - - // Check the category core.edit permissions. - return $canEditOwn || $user->authorise('core.edit', $this->option . '.category.' . (int) $item->catid); - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('Newsfeed', '', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_newsfeeds&view=newsfeeds' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } + use VersionableControllerTrait; + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('filter_category_id'), 'int'); + $allow = null; + + if ($categoryId) { + // If the category has been passed in the URL check it. + $allow = $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); + } + + if ($allow === null) { + // In the absence of better information, revert to the component permissions. + return parent::allowAdd($data); + } else { + return $allow; + } + } + + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + + // Since there is no asset tracking, fallback to the component permissions. + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Get the item. + $item = $this->getModel()->getItem($recordId); + + // Since there is no item, return false. + if (empty($item)) { + return false; + } + + $user = $this->app->getIdentity(); + + // Check if can edit own core.edit.own. + $canEditOwn = $user->authorise('core.edit.own', $this->option . '.category.' . (int) $item->catid) && $item->created_by == $user->id; + + // Check the category core.edit permissions. + return $canEditOwn || $user->authorise('core.edit', $this->option . '.category.' . (int) $item->catid); + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('Newsfeed', '', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_newsfeeds&view=newsfeeds' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } } diff --git a/administrator/components/com_newsfeeds/src/Controller/NewsfeedsController.php b/administrator/components/com_newsfeeds/src/Controller/NewsfeedsController.php index 72bbd6303e4dc..b7a8add2479f9 100644 --- a/administrator/components/com_newsfeeds/src/Controller/NewsfeedsController.php +++ b/administrator/components/com_newsfeeds/src/Controller/NewsfeedsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Newsfeed', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_newsfeeds/src/Extension/NewsfeedsComponent.php b/administrator/components/com_newsfeeds/src/Extension/NewsfeedsComponent.php index a9226c389ae80..8acf3a2d242f8 100644 --- a/administrator/components/com_newsfeeds/src/Extension/NewsfeedsComponent.php +++ b/administrator/components/com_newsfeeds/src/Extension/NewsfeedsComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('newsfeedsadministrator', new AdministratorService); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('newsfeedsadministrator', new AdministratorService()); + } - /** - * Returns the table for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getTableNameForSection(string $section = null) - { - return $section === 'category' ? 'categories' : 'newsfeeds'; - } + /** + * Returns the table for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getTableNameForSection(string $section = null) + { + return $section === 'category' ? 'categories' : 'newsfeeds'; + } - /** - * Returns the state column for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getStateColumnForSection(string $section = null) - { - return 'published'; - } + /** + * Returns the state column for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getStateColumnForSection(string $section = null) + { + return 'published'; + } } diff --git a/administrator/components/com_newsfeeds/src/Field/Modal/NewsfeedField.php b/administrator/components/com_newsfeeds/src/Field/Modal/NewsfeedField.php index 3dfdaa5a5b186..8f5d9f77d2d4d 100644 --- a/administrator/components/com_newsfeeds/src/Field/Modal/NewsfeedField.php +++ b/administrator/components/com_newsfeeds/src/Field/Modal/NewsfeedField.php @@ -1,4 +1,5 @@ element['new'] == 'true'); - $allowEdit = ((string) $this->element['edit'] == 'true'); - $allowClear = ((string) $this->element['clear'] != 'false'); - $allowSelect = ((string) $this->element['select'] != 'false'); - $allowPropagate = ((string) $this->element['propagate'] == 'true'); - - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - - // Load language - Factory::getLanguage()->load('com_newsfeeds', JPATH_ADMINISTRATOR); - - // The active newsfeed id field. - $value = (int) $this->value ?: ''; - - // Create the modal id. - $modalId = 'Newsfeed_' . $this->id; - - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - - // Add the modal field script to the document head. - $wa->useScript('field.modal-fields'); - - // Script to proxy the select modal function to the modal-fields.js file. - if ($allowSelect) - { - static $scriptSelect = null; - - if (is_null($scriptSelect)) - { - $scriptSelect = array(); - } - - if (!isset($scriptSelect[$this->id])) - { - $wa->addInlineScript(" + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'Modal_Newsfeed'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $allowNew = ((string) $this->element['new'] == 'true'); + $allowEdit = ((string) $this->element['edit'] == 'true'); + $allowClear = ((string) $this->element['clear'] != 'false'); + $allowSelect = ((string) $this->element['select'] != 'false'); + $allowPropagate = ((string) $this->element['propagate'] == 'true'); + + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + + // Load language + Factory::getLanguage()->load('com_newsfeeds', JPATH_ADMINISTRATOR); + + // The active newsfeed id field. + $value = (int) $this->value ?: ''; + + // Create the modal id. + $modalId = 'Newsfeed_' . $this->id; + + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + + // Add the modal field script to the document head. + $wa->useScript('field.modal-fields'); + + // Script to proxy the select modal function to the modal-fields.js file. + if ($allowSelect) { + static $scriptSelect = null; + + if (is_null($scriptSelect)) { + $scriptSelect = array(); + } + + if (!isset($scriptSelect[$this->id])) { + $wa->addInlineScript( + " window.jSelectNewsfeed_" . $this->id . " = function (id, title, object) { window.processModalSelect('Newsfeed', '" . $this->id . "', id, title, '', object); }", - [], - ['type' => 'module'] - ); - - Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); - - $scriptSelect[$this->id] = true; - } - } - - // Setup variables for display. - $linkNewsfeeds = 'index.php?option=com_newsfeeds&view=newsfeeds&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - $linkNewsfeed = 'index.php?option=com_newsfeeds&view=newsfeed&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - $modalTitle = Text::_('COM_NEWSFEEDS_SELECT_A_FEED'); - - if (isset($this->element['language'])) - { - $linkNewsfeeds .= '&forcedLanguage=' . $this->element['language']; - $linkNewsfeed .= '&forcedLanguage=' . $this->element['language']; - $modalTitle .= ' — ' . $this->element['label']; - } - - $urlSelect = $linkNewsfeeds . '&function=jSelectNewsfeed_' . $this->id; - $urlEdit = $linkNewsfeed . '&task=newsfeed.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; - $urlNew = $linkNewsfeed . '&task=newsfeed.add'; - - if ($value) - { - $id = (int) $value; - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__newsfeeds')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $title = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - $title = empty($title) ? Text::_('COM_NEWSFEEDS_SELECT_A_FEED') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); - - // The current newsfeed display field. - $html = ''; - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - $html .= ''; - - // Select newsfeed button - if ($allowSelect) - { - $html .= '' - . ' ' . Text::_('JSELECT') - . ''; - } - - // New newsfeed button - if ($allowNew) - { - $html .= '' - . ' ' . Text::_('JACTION_CREATE') - . ''; - } - - // Edit newsfeed button - if ($allowEdit) - { - $html .= '' - . ' ' . Text::_('JACTION_EDIT') - . ''; - } - - // Clear newsfeed button - if ($allowClear) - { - $html .= '' - . ' ' . Text::_('JCLEAR') - . ''; - } - - // Propagate newsfeed button - if ($allowPropagate && count($languages) > 2) - { - // Strip off language tag at the end - $tagLength = (int) strlen($this->element['language']); - $callbackFunctionStem = substr("jSelectNewsfeed_" . $this->id, 0, -$tagLength); - - $html .= '' - . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') - . ''; - } - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - // Select newsfeed modal - if ($allowSelect) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalSelect' . $modalId, - array( - 'title' => $modalTitle, - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - } - - // New newsfeed modal - if ($allowNew) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalNew' . $modalId, - array( - 'title' => Text::_('COM_NEWSFEEDS_NEW_NEWSFEED'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlNew, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Edit newsfeed modal. - if ($allowEdit) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalEdit' . $modalId, - array( - 'title' => Text::_('COM_NEWSFEEDS_EDIT_NEWSFEED'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlEdit, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Add class='required' for client side validation - $class = $this->required ? ' class="required modal-value"' : ''; - - $html .= ''; - - return $html; - } - - /** - * Method to get the field label markup. - * - * @return string The field label markup. - * - * @since 3.4 - */ - protected function getLabel() - { - return str_replace($this->id, $this->id . '_name', parent::getLabel()); - } + [], + ['type' => 'module'] + ); + + Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); + + $scriptSelect[$this->id] = true; + } + } + + // Setup variables for display. + $linkNewsfeeds = 'index.php?option=com_newsfeeds&view=newsfeeds&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + $linkNewsfeed = 'index.php?option=com_newsfeeds&view=newsfeed&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + $modalTitle = Text::_('COM_NEWSFEEDS_SELECT_A_FEED'); + + if (isset($this->element['language'])) { + $linkNewsfeeds .= '&forcedLanguage=' . $this->element['language']; + $linkNewsfeed .= '&forcedLanguage=' . $this->element['language']; + $modalTitle .= ' — ' . $this->element['label']; + } + + $urlSelect = $linkNewsfeeds . '&function=jSelectNewsfeed_' . $this->id; + $urlEdit = $linkNewsfeed . '&task=newsfeed.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; + $urlNew = $linkNewsfeed . '&task=newsfeed.add'; + + if ($value) { + $id = (int) $value; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__newsfeeds')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $title = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + $title = empty($title) ? Text::_('COM_NEWSFEEDS_SELECT_A_FEED') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + + // The current newsfeed display field. + $html = ''; + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + $html .= ''; + + // Select newsfeed button + if ($allowSelect) { + $html .= '' + . ' ' . Text::_('JSELECT') + . ''; + } + + // New newsfeed button + if ($allowNew) { + $html .= '' + . ' ' . Text::_('JACTION_CREATE') + . ''; + } + + // Edit newsfeed button + if ($allowEdit) { + $html .= '' + . ' ' . Text::_('JACTION_EDIT') + . ''; + } + + // Clear newsfeed button + if ($allowClear) { + $html .= '' + . ' ' . Text::_('JCLEAR') + . ''; + } + + // Propagate newsfeed button + if ($allowPropagate && count($languages) > 2) { + // Strip off language tag at the end + $tagLength = (int) strlen($this->element['language']); + $callbackFunctionStem = substr("jSelectNewsfeed_" . $this->id, 0, -$tagLength); + + $html .= '' + . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') + . ''; + } + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + // Select newsfeed modal + if ($allowSelect) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalSelect' . $modalId, + array( + 'title' => $modalTitle, + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + } + + // New newsfeed modal + if ($allowNew) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalNew' . $modalId, + array( + 'title' => Text::_('COM_NEWSFEEDS_NEW_NEWSFEED'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlNew, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Edit newsfeed modal. + if ($allowEdit) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalEdit' . $modalId, + array( + 'title' => Text::_('COM_NEWSFEEDS_EDIT_NEWSFEED'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlEdit, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Add class='required' for client side validation + $class = $this->required ? ' class="required modal-value"' : ''; + + $html .= ''; + + return $html; + } + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + * + * @since 3.4 + */ + protected function getLabel() + { + return str_replace($this->id, $this->id . '_name', parent::getLabel()); + } } diff --git a/administrator/components/com_newsfeeds/src/Field/NewsfeedsField.php b/administrator/components/com_newsfeeds/src/Field/NewsfeedsField.php index e85fa5578b1fb..d66452e4d3671 100644 --- a/administrator/components/com_newsfeeds/src/Field/NewsfeedsField.php +++ b/administrator/components/com_newsfeeds/src/Field/NewsfeedsField.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('id', 'value'), - $db->quoteName('name', 'text'), - ] - ) - ->from($db->quoteName('#__newsfeeds', 'a')) - ->order($db->quoteName('a.name')); + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('id', 'value'), + $db->quoteName('name', 'text'), + ] + ) + ->from($db->quoteName('#__newsfeeds', 'a')) + ->order($db->quoteName('a.name')); - // Get the options. - $db->setQuery($query); + // Get the options. + $db->setQuery($query); - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); - return $options; - } + return $options; + } } diff --git a/administrator/components/com_newsfeeds/src/Helper/AssociationsHelper.php b/administrator/components/com_newsfeeds/src/Helper/AssociationsHelper.php index 85c90141fbb19..7d4ff303c4abf 100644 --- a/administrator/components/com_newsfeeds/src/Helper/AssociationsHelper.php +++ b/administrator/components/com_newsfeeds/src/Helper/AssociationsHelper.php @@ -1,4 +1,5 @@ getType($typeName); - - $context = $this->extension . '.item'; - $catidField = 'catid'; - - if ($typeName === 'category') - { - $context = 'com_categories.item'; - $catidField = ''; - } - - // Get the associations. - $associations = Associations::getAssociations( - $this->extension, - $type['tables']['a'], - $context, - $id, - 'id', - 'alias', - $catidField - ); - - return $associations; - } - - /** - * Get item information - * - * @param string $typeName The item type - * @param int $id The id of item for which we need the associated items - * - * @return Table|null - * - * @since 3.7.0 - */ - public function getItem($typeName, $id) - { - if (empty($id)) - { - return null; - } - - $table = null; - - switch ($typeName) - { - case 'newsfeed': - $table = Table::getInstance('NewsfeedTable', 'Joomla\\Component\\Newsfeeds\\Administrator\\Table\\'); - break; - - case 'category': - $table = Table::getInstance('Category'); - break; - } - - if (empty($table)) - { - return null; - } - - $table->load($id); - - return $table; - } - - /** - * Get information about the type - * - * @param string $typeName The item type - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getType($typeName = '') - { - $fields = $this->getFieldsTemplate(); - $tables = array(); - $joins = array(); - $support = $this->getSupportTemplate(); - $title = ''; - - if (in_array($typeName, $this->itemTypes)) - { - switch ($typeName) - { - case 'newsfeed': - $fields['title'] = 'a.name'; - $fields['state'] = 'a.published'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['category'] = true; - $support['save2copy'] = true; - - $tables = array( - 'a' => '#__newsfeeds' - ); - $title = 'newsfeed'; - break; - - case 'category': - $fields['created_user_id'] = 'a.created_user_id'; - $fields['ordering'] = 'a.lft'; - $fields['level'] = 'a.level'; - $fields['catid'] = ''; - $fields['state'] = 'a.published'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['level'] = true; - - $tables = array( - 'a' => '#__categories' - ); - - $title = 'category'; - break; - } - } - - return array( - 'fields' => $fields, - 'support' => $support, - 'tables' => $tables, - 'joins' => $joins, - 'title' => $title - ); - } + /** + * The extension name + * + * @var array $extension + * + * @since 3.7.0 + */ + protected $extension = 'com_newsfeeds'; + + /** + * Array of item types + * + * @var array $itemTypes + * + * @since 3.7.0 + */ + protected $itemTypes = array('newsfeed', 'category'); + + /** + * Has the extension association support + * + * @var boolean $associationsSupport + * + * @since 3.7.0 + */ + protected $associationsSupport = true; + + /** + * Method to get the associations for a given item. + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 4.0.0 + */ + public function getAssociationsForItem($id = 0, $view = null) + { + return AssociationHelper::getAssociations($id, $view); + } + + /** + * Get the associated items for an item + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public function getAssociations($typeName, $id) + { + $type = $this->getType($typeName); + + $context = $this->extension . '.item'; + $catidField = 'catid'; + + if ($typeName === 'category') { + $context = 'com_categories.item'; + $catidField = ''; + } + + // Get the associations. + $associations = Associations::getAssociations( + $this->extension, + $type['tables']['a'], + $context, + $id, + 'id', + 'alias', + $catidField + ); + + return $associations; + } + + /** + * Get item information + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return Table|null + * + * @since 3.7.0 + */ + public function getItem($typeName, $id) + { + if (empty($id)) { + return null; + } + + $table = null; + + switch ($typeName) { + case 'newsfeed': + $table = Table::getInstance('NewsfeedTable', 'Joomla\\Component\\Newsfeeds\\Administrator\\Table\\'); + break; + + case 'category': + $table = Table::getInstance('Category'); + break; + } + + if (empty($table)) { + return null; + } + + $table->load($id); + + return $table; + } + + /** + * Get information about the type + * + * @param string $typeName The item type + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getType($typeName = '') + { + $fields = $this->getFieldsTemplate(); + $tables = array(); + $joins = array(); + $support = $this->getSupportTemplate(); + $title = ''; + + if (in_array($typeName, $this->itemTypes)) { + switch ($typeName) { + case 'newsfeed': + $fields['title'] = 'a.name'; + $fields['state'] = 'a.published'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['category'] = true; + $support['save2copy'] = true; + + $tables = array( + 'a' => '#__newsfeeds' + ); + $title = 'newsfeed'; + break; + + case 'category': + $fields['created_user_id'] = 'a.created_user_id'; + $fields['ordering'] = 'a.lft'; + $fields['level'] = 'a.level'; + $fields['catid'] = ''; + $fields['state'] = 'a.published'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['level'] = true; + + $tables = array( + 'a' => '#__categories' + ); + + $title = 'category'; + break; + } + } + + return array( + 'fields' => $fields, + 'support' => $support, + 'tables' => $tables, + 'joins' => $joins, + 'title' => $title + ); + } } diff --git a/administrator/components/com_newsfeeds/src/Helper/NewsfeedsHelper.php b/administrator/components/com_newsfeeds/src/Helper/NewsfeedsHelper.php index 55dc24d5ff30d..b7ce45f45bf40 100644 --- a/administrator/components/com_newsfeeds/src/Helper/NewsfeedsHelper.php +++ b/administrator/components/com_newsfeeds/src/Helper/NewsfeedsHelper.php @@ -1,4 +1,5 @@ getQuery(true); - $query->select( - [ - $db->quoteName('published', 'state'), - 'COUNT(*) AS ' . $db->quoteName('count'), - ] - ) - ->from($db->quoteName('#__newsfeeds')) - ->where($db->quoteName('catid') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER) - ->group($db->quoteName('state')); - $db->setQuery($query); - - foreach ($items as $item) - { - $item->count_trashed = 0; - $item->count_archived = 0; - $item->count_unpublished = 0; - $item->count_published = 0; - - $id = (int) $item->id; - $newfeeds = $db->loadObjectList(); - - foreach ($newfeeds as $newsfeed) - { - if ($newsfeed->state == 1) - { - $item->count_published = $newsfeed->count; - } - - if ($newsfeed->state == 0) - { - $item->count_unpublished = $newsfeed->count; - } - - if ($newsfeed->state == 2) - { - $item->count_archived = $newsfeed->count; - } - - if ($newsfeed->state == -2) - { - $item->count_trashed = $newsfeed->count; - } - } - } - - return $items; - } - - /** - * Adds Count Items for Tag Manager. - * - * @param \stdClass[] &$items The newsfeed tag objects - * @param string $extension The name of the active view. - * - * @return \stdClass[] - * - * @since 3.6 - */ - public static function countTagItems(&$items, $extension) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $parts = explode('.', $extension); - $section = null; - - if (count($parts) > 1) - { - $section = $parts[1]; - } - - $query->select( - [ - $db->quoteName('published', 'state'), - 'COUNT(*) AS ' . $db->quoteName('count'), - ] - ) - ->from($db->quoteName('#__contentitem_tag_map', 'ct')); - - if ($section === 'category') - { - $query->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('ct.content_item_id') . ' = ' . $db->quoteName('c.id')); - } - else - { - $query->join('LEFT', $db->quoteName('#__newsfeeds', 'c'), $db->quoteName('ct.content_item_id') . ' = ' . $db->quoteName('c.id')); - } - - $query->where( - [ - $db->quoteName('ct.tag_id') . ' = :id', - $db->quoteName('ct.type_alias') . ' = :extension', - ] - ) - ->bind(':id', $id, ParameterType::INTEGER) - ->bind(':extension', $extension) - ->group($db->quoteName('state')); - - $db->setQuery($query); - - foreach ($items as $item) - { - $item->count_trashed = 0; - $item->count_archived = 0; - $item->count_unpublished = 0; - $item->count_published = 0; - - // Update ID used in database query. - $id = (int) $item->id; - $newsfeeds = $db->loadObjectList(); - - foreach ($newsfeeds as $newsfeed) - { - if ($newsfeed->state == 1) - { - $item->count_published = $newsfeed->count; - } - - if ($newsfeed->state == 0) - { - $item->count_unpublished = $newsfeed->count; - } - - if ($newsfeed->state == 2) - { - $item->count_archived = $newsfeed->count; - } - - if ($newsfeed->state == -2) - { - $item->count_trashed = $newsfeed->count; - } - } - } - - return $items; - } + /** + * Name of the extension + * + * @var string + */ + public static $extension = 'com_newsfeeds'; + + /** + * Adds Count Items for Category Manager. + * + * @param \stdClass[] &$items The banner category objects + * + * @return \stdClass[] + * + * @since 3.5 + */ + public static function countItems(&$items) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('published', 'state'), + 'COUNT(*) AS ' . $db->quoteName('count'), + ] + ) + ->from($db->quoteName('#__newsfeeds')) + ->where($db->quoteName('catid') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER) + ->group($db->quoteName('state')); + $db->setQuery($query); + + foreach ($items as $item) { + $item->count_trashed = 0; + $item->count_archived = 0; + $item->count_unpublished = 0; + $item->count_published = 0; + + $id = (int) $item->id; + $newfeeds = $db->loadObjectList(); + + foreach ($newfeeds as $newsfeed) { + if ($newsfeed->state == 1) { + $item->count_published = $newsfeed->count; + } + + if ($newsfeed->state == 0) { + $item->count_unpublished = $newsfeed->count; + } + + if ($newsfeed->state == 2) { + $item->count_archived = $newsfeed->count; + } + + if ($newsfeed->state == -2) { + $item->count_trashed = $newsfeed->count; + } + } + } + + return $items; + } + + /** + * Adds Count Items for Tag Manager. + * + * @param \stdClass[] &$items The newsfeed tag objects + * @param string $extension The name of the active view. + * + * @return \stdClass[] + * + * @since 3.6 + */ + public static function countTagItems(&$items, $extension) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $parts = explode('.', $extension); + $section = null; + + if (count($parts) > 1) { + $section = $parts[1]; + } + + $query->select( + [ + $db->quoteName('published', 'state'), + 'COUNT(*) AS ' . $db->quoteName('count'), + ] + ) + ->from($db->quoteName('#__contentitem_tag_map', 'ct')); + + if ($section === 'category') { + $query->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('ct.content_item_id') . ' = ' . $db->quoteName('c.id')); + } else { + $query->join('LEFT', $db->quoteName('#__newsfeeds', 'c'), $db->quoteName('ct.content_item_id') . ' = ' . $db->quoteName('c.id')); + } + + $query->where( + [ + $db->quoteName('ct.tag_id') . ' = :id', + $db->quoteName('ct.type_alias') . ' = :extension', + ] + ) + ->bind(':id', $id, ParameterType::INTEGER) + ->bind(':extension', $extension) + ->group($db->quoteName('state')); + + $db->setQuery($query); + + foreach ($items as $item) { + $item->count_trashed = 0; + $item->count_archived = 0; + $item->count_unpublished = 0; + $item->count_published = 0; + + // Update ID used in database query. + $id = (int) $item->id; + $newsfeeds = $db->loadObjectList(); + + foreach ($newsfeeds as $newsfeed) { + if ($newsfeed->state == 1) { + $item->count_published = $newsfeed->count; + } + + if ($newsfeed->state == 0) { + $item->count_unpublished = $newsfeed->count; + } + + if ($newsfeed->state == 2) { + $item->count_archived = $newsfeed->count; + } + + if ($newsfeed->state == -2) { + $item->count_trashed = $newsfeed->count; + } + } + } + + return $items; + } } diff --git a/administrator/components/com_newsfeeds/src/Model/NewsfeedModel.php b/administrator/components/com_newsfeeds/src/Model/NewsfeedModel.php index 46640cb02322b..a258b7a0741f1 100644 --- a/administrator/components/com_newsfeeds/src/Model/NewsfeedModel.php +++ b/administrator/components/com_newsfeeds/src/Model/NewsfeedModel.php @@ -1,4 +1,5 @@ id) || $record->published != -2) - { - return false; - } - - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.delete', 'com_newsfeed.category.' . (int) $record->catid); - } - - return parent::canDelete($record); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.edit.state', 'com_newsfeeds.category.' . (int) $record->catid); - } - - return parent::canEditState($record); - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_newsfeeds.newsfeed', 'newsfeed', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - $form->setFieldAttribute('publish_up', 'disabled', 'true'); - $form->setFieldAttribute('publish_down', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - $form->setFieldAttribute('publish_up', 'filter', 'unset'); - $form->setFieldAttribute('publish_down', 'filter', 'unset'); - } - - // Don't allow to change the created_by user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_by', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_newsfeeds.edit.newsfeed.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Prime some default values. - if ($this->getState('newsfeed.id') == 0) - { - $app = Factory::getApplication(); - $data->set('catid', $app->input->get('catid', $app->getUserState('com_newsfeeds.newsfeeds.filter.category_id'), 'int')); - } - } - - $this->preprocessData('com_newsfeeds.newsfeed', $data); - - return $data; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 3.0 - */ - public function save($data) - { - $input = Factory::getApplication()->input; - - // Create new category, if needed. - $createCategory = true; - - // If category ID is provided, check if it's valid. - if (is_numeric($data['catid']) && $data['catid']) - { - $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_newsfeeds'); - } - - // Save New Category - if ($createCategory && $this->canCreateCategory()) - { - $category = [ - // Remove #new# prefix, if exists. - 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], - 'parent_id' => 1, - 'extension' => 'com_newsfeeds', - 'language' => $data['language'], - 'published' => 1, - ]; - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ - $categoryModel = Factory::getApplication()->bootComponent('com_categories') - ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); - - // Create new category. - if (!$categoryModel->save($category)) - { - $this->setError($categoryModel->getError()); - - return false; - } - - // Get the Category ID. - $data['catid'] = $categoryModel->getState('category.id'); - } - - // Alter the name for save as copy - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['name'] == $origTable->name) - { - list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); - $data['name'] = $name; - $data['alias'] = $alias; - } - else - { - if ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - } - - $data['published'] = 0; - } - - return parent::save($data); - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - if ($item = parent::getItem($pk)) - { - // Convert the params field to an array. - $registry = new Registry($item->metadata); - $item->metadata = $registry->toArray(); - - // Convert the images field to an array. - $registry = new Registry($item->images); - $item->images = $registry->toArray(); - } - - // Load associated newsfeeds items - $assoc = Associations::isEnabled(); - - if ($assoc) - { - $item->associations = array(); - - if ($item->id != null) - { - $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', $item->id); - - foreach ($associations as $tag => $association) - { - $item->associations[$tag] = $association->id; - } - } - } - - if (!empty($item->id)) - { - $item->tags = new TagsHelper; - $item->tags->getTagIds($item->id, 'com_newsfeeds.newsfeed'); - - // @todo: We probably don't need this in any client - but needs careful validation - if (!Factory::getApplication()->isClient('api')) - { - $item->metadata['tags'] = $item->tags; - } - } - - return $item; - } - - /** - * Prepare and sanitise the table prior to saving. - * - * @param \Joomla\CMS\Table\Table $table The table object - * - * @return void - */ - protected function prepareTable($table) - { - $date = Factory::getDate(); - $user = Factory::getUser(); - - $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); - $table->alias = ApplicationHelper::stringURLSafe($table->alias, $table->language); - - if (empty($table->alias)) - { - $table->alias = ApplicationHelper::stringURLSafe($table->name, $table->language); - } - - if (empty($table->id)) - { - // Set the values - $table->created = $date->toSql(); - - // Set ordering to the last item if not set - if (empty($table->ordering)) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('MAX(' . $db->quoteName('ordering') . ')') - ->from($db->quoteName('#__newsfeeds')); - $db->setQuery($query); - $max = $db->loadResult(); - - $table->ordering = $max + 1; - } - } - else - { - // Set the values - $table->modified = $date->toSql(); - $table->modified_by = $user->get('id'); - } - - // Increment the content version number. - $table->version++; - } - - /** - * Method to change the published state of one or more records. - * - * @param array &$pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function publish(&$pks, $value = 1) - { - $result = parent::publish($pks, $value); - - // Clean extra cache for newsfeeds - $this->cleanCache('feed_parser'); - - return $result; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - return [ - $this->getDatabase()->quoteName('catid') . ' = ' . (int) $table->catid, - ]; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param Form $form The form object. - * @param array $data The data to be injected into the form - * @param string $group The plugin group to process - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - if ($this->canCreateCategory()) - { - $form->setFieldAttribute('catid', 'allowAdd', 'true'); - - // Add a prefix for categories created on the fly. - $form->setFieldAttribute('catid', 'customPrefix', '#new#'); - } - - // Association newsfeeds items - if (Associations::isEnabled()) - { - $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); - - if (count($languages) > 1) - { - $addform = new \SimpleXMLElement('
    '); - $fields = $addform->addChild('fields'); - $fields->addAttribute('name', 'associations'); - $fieldset = $fields->addChild('fieldset'); - $fieldset->addAttribute('name', 'item_associations'); - - foreach ($languages as $language) - { - $field = $fieldset->addChild('field'); - $field->addAttribute('name', $language->lang_code); - $field->addAttribute('type', 'modal_newsfeed'); - $field->addAttribute('language', $language->lang_code); - $field->addAttribute('label', $language->title); - $field->addAttribute('translate_label', 'false'); - $field->addAttribute('select', 'true'); - $field->addAttribute('new', 'true'); - $field->addAttribute('edit', 'true'); - $field->addAttribute('clear', 'true'); - $field->addAttribute('propagate', 'true'); - } - - $form->load($addform, false); - } - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Is the user allowed to create an on the fly category? - * - * @return boolean - * - * @since 3.6.1 - */ - private function canCreateCategory() - { - return Factory::getUser()->authorise('core.create', 'com_newsfeeds'); - } + use VersionableModelTrait; + + /** + * The type alias for this content type. + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_newsfeeds.newsfeed'; + + /** + * The context used for the associations table + * + * @var string + * @since 3.4.4 + */ + protected $associationsContext = 'com_newsfeeds.item'; + + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_NEWSFEEDS'; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.delete', 'com_newsfeed.category.' . (int) $record->catid); + } + + return parent::canDelete($record); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.edit.state', 'com_newsfeeds.category.' . (int) $record->catid); + } + + return parent::canEditState($record); + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_newsfeeds.newsfeed', 'newsfeed', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + $form->setFieldAttribute('publish_up', 'disabled', 'true'); + $form->setFieldAttribute('publish_down', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + $form->setFieldAttribute('publish_up', 'filter', 'unset'); + $form->setFieldAttribute('publish_down', 'filter', 'unset'); + } + + // Don't allow to change the created_by user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_by', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_newsfeeds.edit.newsfeed.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Prime some default values. + if ($this->getState('newsfeed.id') == 0) { + $app = Factory::getApplication(); + $data->set('catid', $app->input->get('catid', $app->getUserState('com_newsfeeds.newsfeeds.filter.category_id'), 'int')); + } + } + + $this->preprocessData('com_newsfeeds.newsfeed', $data); + + return $data; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 3.0 + */ + public function save($data) + { + $input = Factory::getApplication()->input; + + // Create new category, if needed. + $createCategory = true; + + // If category ID is provided, check if it's valid. + if (is_numeric($data['catid']) && $data['catid']) { + $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_newsfeeds'); + } + + // Save New Category + if ($createCategory && $this->canCreateCategory()) { + $category = [ + // Remove #new# prefix, if exists. + 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], + 'parent_id' => 1, + 'extension' => 'com_newsfeeds', + 'language' => $data['language'], + 'published' => 1, + ]; + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ + $categoryModel = Factory::getApplication()->bootComponent('com_categories') + ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); + + // Create new category. + if (!$categoryModel->save($category)) { + $this->setError($categoryModel->getError()); + + return false; + } + + // Get the Category ID. + $data['catid'] = $categoryModel->getState('category.id'); + } + + // Alter the name for save as copy + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['name'] == $origTable->name) { + list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); + $data['name'] = $name; + $data['alias'] = $alias; + } else { + if ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + } + + $data['published'] = 0; + } + + return parent::save($data); + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + if ($item = parent::getItem($pk)) { + // Convert the params field to an array. + $registry = new Registry($item->metadata); + $item->metadata = $registry->toArray(); + + // Convert the images field to an array. + $registry = new Registry($item->images); + $item->images = $registry->toArray(); + } + + // Load associated newsfeeds items + $assoc = Associations::isEnabled(); + + if ($assoc) { + $item->associations = array(); + + if ($item->id != null) { + $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', $item->id); + + foreach ($associations as $tag => $association) { + $item->associations[$tag] = $association->id; + } + } + } + + if (!empty($item->id)) { + $item->tags = new TagsHelper(); + $item->tags->getTagIds($item->id, 'com_newsfeeds.newsfeed'); + + // @todo: We probably don't need this in any client - but needs careful validation + if (!Factory::getApplication()->isClient('api')) { + $item->metadata['tags'] = $item->tags; + } + } + + return $item; + } + + /** + * Prepare and sanitise the table prior to saving. + * + * @param \Joomla\CMS\Table\Table $table The table object + * + * @return void + */ + protected function prepareTable($table) + { + $date = Factory::getDate(); + $user = Factory::getUser(); + + $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); + $table->alias = ApplicationHelper::stringURLSafe($table->alias, $table->language); + + if (empty($table->alias)) { + $table->alias = ApplicationHelper::stringURLSafe($table->name, $table->language); + } + + if (empty($table->id)) { + // Set the values + $table->created = $date->toSql(); + + // Set ordering to the last item if not set + if (empty($table->ordering)) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('MAX(' . $db->quoteName('ordering') . ')') + ->from($db->quoteName('#__newsfeeds')); + $db->setQuery($query); + $max = $db->loadResult(); + + $table->ordering = $max + 1; + } + } else { + // Set the values + $table->modified = $date->toSql(); + $table->modified_by = $user->get('id'); + } + + // Increment the content version number. + $table->version++; + } + + /** + * Method to change the published state of one or more records. + * + * @param array &$pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function publish(&$pks, $value = 1) + { + $result = parent::publish($pks, $value); + + // Clean extra cache for newsfeeds + $this->cleanCache('feed_parser'); + + return $result; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + return [ + $this->getDatabase()->quoteName('catid') . ' = ' . (int) $table->catid, + ]; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param Form $form The form object. + * @param array $data The data to be injected into the form + * @param string $group The plugin group to process + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + if ($this->canCreateCategory()) { + $form->setFieldAttribute('catid', 'allowAdd', 'true'); + + // Add a prefix for categories created on the fly. + $form->setFieldAttribute('catid', 'customPrefix', '#new#'); + } + + // Association newsfeeds items + if (Associations::isEnabled()) { + $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); + + if (count($languages) > 1) { + $addform = new \SimpleXMLElement(''); + $fields = $addform->addChild('fields'); + $fields->addAttribute('name', 'associations'); + $fieldset = $fields->addChild('fieldset'); + $fieldset->addAttribute('name', 'item_associations'); + + foreach ($languages as $language) { + $field = $fieldset->addChild('field'); + $field->addAttribute('name', $language->lang_code); + $field->addAttribute('type', 'modal_newsfeed'); + $field->addAttribute('language', $language->lang_code); + $field->addAttribute('label', $language->title); + $field->addAttribute('translate_label', 'false'); + $field->addAttribute('select', 'true'); + $field->addAttribute('new', 'true'); + $field->addAttribute('edit', 'true'); + $field->addAttribute('clear', 'true'); + $field->addAttribute('propagate', 'true'); + } + + $form->load($addform, false); + } + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Is the user allowed to create an on the fly category? + * + * @return boolean + * + * @since 3.6.1 + */ + private function canCreateCategory() + { + return Factory::getUser()->authorise('core.create', 'com_newsfeeds'); + } } diff --git a/administrator/components/com_newsfeeds/src/Model/NewsfeedsModel.php b/administrator/components/com_newsfeeds/src/Model/NewsfeedsModel.php index c861401ac04f5..11ac51ee3d84d 100644 --- a/administrator/components/com_newsfeeds/src/Model/NewsfeedsModel.php +++ b/administrator/components/com_newsfeeds/src/Model/NewsfeedsModel.php @@ -1,4 +1,5 @@ input->get('forcedLanguage', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - // Load the parameters. - $params = ComponentHelper::getParams('com_newsfeeds'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language. - if (!empty($forcedLanguage)) - { - $this->setState('filter.language', $forcedLanguage); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.category_id'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . $this->getState('filter.level'); - $id .= ':' . serialize($this->getState('filter.tag')); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.name'), - $db->quoteName('a.alias'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.catid'), - $db->quoteName('a.numarticles'), - $db->quoteName('a.cache_time'), - $db->quoteName('a.created_by'), - $db->quoteName('a.published'), - $db->quoteName('a.access'), - $db->quoteName('a.ordering'), - $db->quoteName('a.language'), - $db->quoteName('a.publish_up'), - $db->quoteName('a.publish_down'), - ] - ) - ) - ->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - $db->quoteName('uc.name', 'editor'), - $db->quoteName('ag.title', 'access_level'), - $db->quoteName('c.title', 'category_title'), - ] - ) - ->from($db->quoteName('#__newsfeeds', 'a')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')); - - // Join over the associations. - if (Associations::isEnabled()) - { - $subQuery = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') - ->from($db->quoteName('#__associations', 'asso1')) - ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) - ->where( - [ - $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), - $db->quoteName('asso1.context') . ' = ' . $db->quote('com_newsfeeds.item'), - ] - ); - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); - } - - // Filter by access level. - if ($access = (int) $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - $query->whereIn($db->quoteName('a.access'), $user->getAuthorisedViewLevels()); - } - - // Filter by published state. - $published = (string) $this->getState('filter.published'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where($db->quoteName('a.published') . ' IN (0, 1)'); - } - - // Filter by category. - $categoryId = $this->getState('filter.category_id'); - - if (is_numeric($categoryId)) - { - $categoryId = (int) $categoryId; - $query->where($db->quoteName('a.catid') . ' = :categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - - // Filter on the level. - if ($level = (int) $this->getState('filter.level')) - { - $query->where($db->quoteName('c.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter by search in title - if ($search = $this->getState('filter.search')) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName('a.name') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - - // Filter by a single or group of tags. - $tag = $this->getState('filter.tag'); - - // Run simplified query when filtering by one tag. - if (\is_array($tag) && \count($tag) === 1) - { - $tag = $tag[0]; - } - - if ($tag && \is_array($tag)) - { - $tag = ArrayHelper::toInteger($tag); - - $subQuery = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('content_item_id')) - ->from($db->quoteName('#__contentitem_tag_map')) - ->where( - [ - $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', - $db->quoteName('type_alias') . ' = ' . $db->quote('com_newsfeeds.newsfeed'), - ] - ); - - $query->join( - 'INNER', - '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ); - } - elseif ($tag = (int) $tag) - { - $query->join( - 'INNER', - $db->quoteName('#__contentitem_tag_map', 'tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ) - ->where( - [ - $db->quoteName('tagmap.tag_id') . ' = :tag', - $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_newsfeeds.newsfeed'), - ] - ) - ->bind(':tag', $tag, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 'a.name'); - $orderDirn = $this->state->get('list.direction', 'ASC'); - - if ($orderCol == 'a.ordering' || $orderCol == 'category_title') - { - $ordering = [ - $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), - $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), - ]; - } - else - { - $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); - } - - $query->order($ordering); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'alias', 'a.alias', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'catid', 'a.catid', 'category_id', 'category_title', + 'published', 'a.published', + 'access', 'a.access', 'access_level', + 'created', 'a.created', + 'created_by', 'a.created_by', + 'ordering', 'a.ordering', + 'language', 'a.language', 'language_title', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'cache_time', 'a.cache_time', + 'numarticles', + 'tag', + 'level', 'c.level', + 'tag', + ); + + if (Associations::isEnabled()) { + $config['filter_fields'][] = 'association'; + } + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.name', $direction = 'asc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + // Load the parameters. + $params = ComponentHelper::getParams('com_newsfeeds'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language. + if (!empty($forcedLanguage)) { + $this->setState('filter.language', $forcedLanguage); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.category_id'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . $this->getState('filter.level'); + $id .= ':' . serialize($this->getState('filter.tag')); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.name'), + $db->quoteName('a.alias'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.catid'), + $db->quoteName('a.numarticles'), + $db->quoteName('a.cache_time'), + $db->quoteName('a.created_by'), + $db->quoteName('a.published'), + $db->quoteName('a.access'), + $db->quoteName('a.ordering'), + $db->quoteName('a.language'), + $db->quoteName('a.publish_up'), + $db->quoteName('a.publish_down'), + ] + ) + ) + ->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + $db->quoteName('uc.name', 'editor'), + $db->quoteName('ag.title', 'access_level'), + $db->quoteName('c.title', 'category_title'), + ] + ) + ->from($db->quoteName('#__newsfeeds', 'a')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')); + + // Join over the associations. + if (Associations::isEnabled()) { + $subQuery = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') + ->from($db->quoteName('#__associations', 'asso1')) + ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) + ->where( + [ + $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), + $db->quoteName('asso1.context') . ' = ' . $db->quote('com_newsfeeds.item'), + ] + ); + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); + } + + // Filter by access level. + if ($access = (int) $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + $query->whereIn($db->quoteName('a.access'), $user->getAuthorisedViewLevels()); + } + + // Filter by published state. + $published = (string) $this->getState('filter.published'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where($db->quoteName('a.published') . ' IN (0, 1)'); + } + + // Filter by category. + $categoryId = $this->getState('filter.category_id'); + + if (is_numeric($categoryId)) { + $categoryId = (int) $categoryId; + $query->where($db->quoteName('a.catid') . ' = :categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + + // Filter on the level. + if ($level = (int) $this->getState('filter.level')) { + $query->where($db->quoteName('c.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter by search in title + if ($search = $this->getState('filter.search')) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName('a.name') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + + // Filter by a single or group of tags. + $tag = $this->getState('filter.tag'); + + // Run simplified query when filtering by one tag. + if (\is_array($tag) && \count($tag) === 1) { + $tag = $tag[0]; + } + + if ($tag && \is_array($tag)) { + $tag = ArrayHelper::toInteger($tag); + + $subQuery = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('content_item_id')) + ->from($db->quoteName('#__contentitem_tag_map')) + ->where( + [ + $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', + $db->quoteName('type_alias') . ' = ' . $db->quote('com_newsfeeds.newsfeed'), + ] + ); + + $query->join( + 'INNER', + '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ); + } elseif ($tag = (int) $tag) { + $query->join( + 'INNER', + $db->quoteName('#__contentitem_tag_map', 'tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ) + ->where( + [ + $db->quoteName('tagmap.tag_id') . ' = :tag', + $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_newsfeeds.newsfeed'), + ] + ) + ->bind(':tag', $tag, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 'a.name'); + $orderDirn = $this->state->get('list.direction', 'ASC'); + + if ($orderCol == 'a.ordering' || $orderCol == 'category_title') { + $ordering = [ + $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), + $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), + ]; + } else { + $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); + } + + $query->order($ordering); + + return $query; + } } diff --git a/administrator/components/com_newsfeeds/src/Service/HTML/AdministratorService.php b/administrator/components/com_newsfeeds/src/Service/HTML/AdministratorService.php index 0b620ba53c22a..e7b349bb69347 100644 --- a/administrator/components/com_newsfeeds/src/Service/HTML/AdministratorService.php +++ b/administrator/components/com_newsfeeds/src/Service/HTML/AdministratorService.php @@ -1,4 +1,5 @@ $associated) - { - $associations[$tag] = (int) $associated->id; - } + // Get the associations + if ($associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', $newsfeedid)) { + foreach ($associations as $tag => $associated) { + $associations[$tag] = (int) $associated->id; + } - // Get the associated newsfeed items - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $query - ->select( - [ - $db->quoteName('c.id'), - $db->quoteName('c.name', 'title'), - $db->quoteName('cat.title', 'category_title'), - $db->quoteName('l.sef', 'lang_sef'), - $db->quoteName('l.lang_code'), - $db->quoteName('l.image'), - $db->quoteName('l.title', 'language_title'), - ] - ) - ->from($db->quoteName('#__newsfeeds', 'c')) - ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) - ->where( - [ - $db->quoteName('c.id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')', - $db->quoteName('c.id') . ' != :id', - ] - ) - ->bind(':id', $newsfeedid, ParameterType::INTEGER); - $db->setQuery($query); + // Get the associated newsfeed items + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query + ->select( + [ + $db->quoteName('c.id'), + $db->quoteName('c.name', 'title'), + $db->quoteName('cat.title', 'category_title'), + $db->quoteName('l.sef', 'lang_sef'), + $db->quoteName('l.lang_code'), + $db->quoteName('l.image'), + $db->quoteName('l.title', 'language_title'), + ] + ) + ->from($db->quoteName('#__newsfeeds', 'c')) + ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) + ->where( + [ + $db->quoteName('c.id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')', + $db->quoteName('c.id') . ' != :id', + ] + ) + ->bind(':id', $newsfeedid, ParameterType::INTEGER); + $db->setQuery($query); - try - { - $items = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500); - } + try { + $items = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500); + } - if ($items) - { - $languages = LanguageHelper::getContentLanguages(array(0, 1)); - $content_languages = array_column($languages, 'lang_code'); + if ($items) { + $languages = LanguageHelper::getContentLanguages(array(0, 1)); + $content_languages = array_column($languages, 'lang_code'); - foreach ($items as &$item) - { - if (in_array($item->lang_code, $content_languages)) - { - $text = $item->lang_code; - $url = Route::_('index.php?option=com_newsfeeds&task=newsfeed.edit&id=' . (int) $item->id); - $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
    ' - . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
    ' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); - $classes = 'badge bg-secondary'; + foreach ($items as &$item) { + if (in_array($item->lang_code, $content_languages)) { + $text = $item->lang_code; + $url = Route::_('index.php?option=com_newsfeeds&task=newsfeed.edit&id=' . (int) $item->id); + $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
    ' + . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
    ' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); + $classes = 'badge bg-secondary'; - $item->link = '' . $text . '' - . ''; - } - else - { - // Display warning if Content Language is trashed or deleted - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); - } - } - } + $item->link = '' . $text . '' + . ''; + } else { + // Display warning if Content Language is trashed or deleted + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); + } + } + } - $html = LayoutHelper::render('joomla.content.associations', $items); - } + $html = LayoutHelper::render('joomla.content.associations', $items); + } - return $html; - } + return $html; + } } diff --git a/administrator/components/com_newsfeeds/src/Table/NewsfeedTable.php b/administrator/components/com_newsfeeds/src/Table/NewsfeedTable.php index 6d54fe1acf1a5..dc04ce72f6023 100644 --- a/administrator/components/com_newsfeeds/src/Table/NewsfeedTable.php +++ b/administrator/components/com_newsfeeds/src/Table/NewsfeedTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_newsfeeds.newsfeed'; - parent::__construct('#__newsfeeds', 'id', $db); - $this->setColumnAlias('title', 'name'); - } - - /** - * Overloaded check method to ensure data integrity. - * - * @return boolean True on success. - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Check for valid name. - if (trim($this->name) == '') - { - $this->setError(Text::_('COM_NEWSFEEDS_WARNING_PROVIDE_VALID_NAME')); - - return false; - } - - if (empty($this->alias)) - { - $this->alias = $this->name; - } - - $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); - - if (trim(str_replace('-', '', $this->alias)) == '') - { - $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); - } - - // Check for a valid category. - if (!$this->catid = (int) $this->catid) - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); - - return false; - } - - // Check the publish down date is not earlier than publish up. - if ((int) $this->publish_down > 0 && $this->publish_down < $this->publish_up) - { - $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); - - return false; - } - - // Clean up description -- eliminate quotes and <> brackets - if (!empty($this->metadesc)) - { - // Only process if not empty - $bad_characters = array("\"", '<', '>'); - $this->metadesc = StringHelper::str_ireplace($bad_characters, '', $this->metadesc); - } - - if (is_null($this->hits)) - { - $this->hits = 0; - } - - return true; - } - - /** - * Overridden \JTable::store to set modified data. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate(); - $user = Factory::getUser(); - - // Set created date if not set. - if (!(int) $this->created) - { - $this->created = $date->toSql(); - } - - if ($this->id) - { - // Existing item - $this->modified_by = $user->get('id'); - $this->modified = $date->toSql(); - } - else - { - // Field created_by can be set by the user, so we don't touch it if it's set. - if (empty($this->created_by)) - { - $this->created_by = $user->get('id'); - } - - if (!(int) $this->modified) - { - $this->modified = $this->created; - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - } - - // Set publish_up, publish_down to null if not set - if (!$this->publish_up) - { - $this->publish_up = null; - } - - if (!$this->publish_down) - { - $this->publish_down = null; - } - - // Verify that the alias is unique - $table = Table::getInstance('NewsfeedTable', __NAMESPACE__ . '\\', array('dbo' => $this->_db)); - - if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) - { - $this->setError(Text::_('COM_NEWSFEEDS_ERROR_UNIQUE_ALIAS')); - - return false; - } - - // Save links as punycode. - $this->link = PunycodeHelper::urlToPunycode($this->link); - - return parent::store($updateNulls); - } - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } + use TaggableTableTrait; + + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Ensure the params, metadata and images are json encoded in the bind method + * + * @var array + * @since 3.3 + */ + protected $_jsonEncode = array('params', 'metadata', 'images'); + + /** + * Constructor + * + * @param DatabaseDriver $db A database connector object + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_newsfeeds.newsfeed'; + parent::__construct('#__newsfeeds', 'id', $db); + $this->setColumnAlias('title', 'name'); + } + + /** + * Overloaded check method to ensure data integrity. + * + * @return boolean True on success. + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Check for valid name. + if (trim($this->name) == '') { + $this->setError(Text::_('COM_NEWSFEEDS_WARNING_PROVIDE_VALID_NAME')); + + return false; + } + + if (empty($this->alias)) { + $this->alias = $this->name; + } + + $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); + + if (trim(str_replace('-', '', $this->alias)) == '') { + $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); + } + + // Check for a valid category. + if (!$this->catid = (int) $this->catid) { + $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); + + return false; + } + + // Check the publish down date is not earlier than publish up. + if ((int) $this->publish_down > 0 && $this->publish_down < $this->publish_up) { + $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); + + return false; + } + + // Clean up description -- eliminate quotes and <> brackets + if (!empty($this->metadesc)) { + // Only process if not empty + $bad_characters = array("\"", '<', '>'); + $this->metadesc = StringHelper::str_ireplace($bad_characters, '', $this->metadesc); + } + + if (is_null($this->hits)) { + $this->hits = 0; + } + + return true; + } + + /** + * Overridden \JTable::store to set modified data. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate(); + $user = Factory::getUser(); + + // Set created date if not set. + if (!(int) $this->created) { + $this->created = $date->toSql(); + } + + if ($this->id) { + // Existing item + $this->modified_by = $user->get('id'); + $this->modified = $date->toSql(); + } else { + // Field created_by can be set by the user, so we don't touch it if it's set. + if (empty($this->created_by)) { + $this->created_by = $user->get('id'); + } + + if (!(int) $this->modified) { + $this->modified = $this->created; + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + } + + // Set publish_up, publish_down to null if not set + if (!$this->publish_up) { + $this->publish_up = null; + } + + if (!$this->publish_down) { + $this->publish_down = null; + } + + // Verify that the alias is unique + $table = Table::getInstance('NewsfeedTable', __NAMESPACE__ . '\\', array('dbo' => $this->_db)); + + if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) { + $this->setError(Text::_('COM_NEWSFEEDS_ERROR_UNIQUE_ALIAS')); + + return false; + } + + // Save links as punycode. + $this->link = PunycodeHelper::urlToPunycode($this->link); + + return parent::store($updateNulls); + } + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } } diff --git a/administrator/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php b/administrator/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php index 612996ed2182a..2b44a81a62cb4 100644 --- a/administrator/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php +++ b/administrator/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // If we are forcing a language in modal (used for associations). - if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) - { - // Set the language field to the forcedLanguage and disable changing it. - $this->form->setValue('language', null, $forcedLanguage); - $this->form->setFieldAttribute('language', 'readonly', 'true'); - - // Only allow to select categories with All language or with the forced language. - $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); - - // Only allow to select tags with All language or with the forced language. - $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); - - // Since we don't track these assets at the item level, use the category id. - $canDo = ContentHelper::getActions('com_newsfeeds', 'category', $this->item->catid); - - $title = $isNew ? Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEED_NEW') : Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEED_EDIT'); - ToolbarHelper::title($title, 'rss newsfeeds'); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if (!$checkedOut && ($canDo->get('core.edit') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0)) - { - ToolbarHelper::apply('newsfeed.apply'); - - $toolbarButtons[] = ['save', 'newsfeed.save']; - } - - if (!$checkedOut && count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) - { - $toolbarButtons[] = ['save2new', 'newsfeed.save2new']; - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'newsfeed.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('newsfeed.cancel'); - } - else - { - ToolbarHelper::cancel('newsfeed.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) - { - ToolbarHelper::versions('com_newsfeeds.newsfeed', $this->item->id); - } - } - - if (!$isNew && Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) - { - ToolbarHelper::custom('newsfeed.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('News_Feeds:_New_or_Edit'); - } + /** + * The item object for the newsfeed + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 1.6 + */ + protected $item; + + /** + * The form object for the newsfeed + * + * @var \Joomla\CMS\Form\Form + * + * @since 1.6 + */ + protected $form; + + /** + * The model state of the newsfeed + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 1.6 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // If we are forcing a language in modal (used for associations). + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) { + // Set the language field to the forcedLanguage and disable changing it. + $this->form->setValue('language', null, $forcedLanguage); + $this->form->setFieldAttribute('language', 'readonly', 'true'); + + // Only allow to select categories with All language or with the forced language. + $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); + + // Only allow to select tags with All language or with the forced language. + $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); + + // Since we don't track these assets at the item level, use the category id. + $canDo = ContentHelper::getActions('com_newsfeeds', 'category', $this->item->catid); + + $title = $isNew ? Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEED_NEW') : Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEED_EDIT'); + ToolbarHelper::title($title, 'rss newsfeeds'); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if (!$checkedOut && ($canDo->get('core.edit') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0)) { + ToolbarHelper::apply('newsfeed.apply'); + + $toolbarButtons[] = ['save', 'newsfeed.save']; + } + + if (!$checkedOut && count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) { + $toolbarButtons[] = ['save2new', 'newsfeed.save2new']; + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'newsfeed.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('newsfeed.cancel'); + } else { + ToolbarHelper::cancel('newsfeed.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) { + ToolbarHelper::versions('com_newsfeeds.newsfeed', $this->item->id); + } + } + + if (!$isNew && Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) { + ToolbarHelper::custom('newsfeed.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('News_Feeds:_New_or_Edit'); + } } diff --git a/administrator/components/com_newsfeeds/src/View/Newsfeeds/HtmlView.php b/administrator/components/com_newsfeeds/src/View/Newsfeeds/HtmlView.php index 0fbe696d4f476..cfe6e8072935c 100644 --- a/administrator/components/com_newsfeeds/src/View/Newsfeeds/HtmlView.php +++ b/administrator/components/com_newsfeeds/src/View/Newsfeeds/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // We don't need toolbar in the modal layout. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - else - { - // In article associations modal we need to remove language filter if forcing a language. - // We also need to change the category filter to show show categories with All or the forced language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. - $languageXml = new \SimpleXMLElement(''); - $this->filterForm->setField($languageXml, 'filter', true); - - // Also, unset the active language filter so the search tools is not open by default with this filter. - unset($this->activeFilters['language']); - - // One last changes needed is to change the category filter to just show categories with All language or with the forced language. - $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $state = $this->get('State'); - $canDo = ContentHelper::getActions('com_newsfeeds', 'category', $state->get('filter.category_id')); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEEDS'), 'rss newsfeeds'); - - if ($canDo->get('core.create') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) - { - $toolbar->addNew('newsfeed.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('newsfeeds.publish')->listCheck(true); - $childBar->unpublish('newsfeeds.unpublish')->listCheck(true); - $childBar->archive('newsfeeds.archive')->listCheck(true); - - if ($user->authorise('core.admin')) - { - $childBar->checkin('newsfeeds.checkin')->listCheck(true); - } - - if ($this->state->get('filter.published') != -2) - { - $childBar->trash('newsfeeds.trash')->listCheck(true); - } - - // Add a batch button - if ($user->authorise('core.create', 'com_newsfeeds') - && $user->authorise('core.edit', 'com_newsfeeds') - && $user->authorise('core.edit.state', 'com_newsfeeds')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && $state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('newsfeeds.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($user->authorise('core.admin', 'com_newsfeeds') || $user->authorise('core.options', 'com_newsfeeds')) - { - $toolbar->preferences('com_newsfeeds'); - } - - $toolbar->help('News_Feeds'); - } + /** + * The list of newsfeeds + * + * @var CMSObject + * + * @since 1.6 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 1.6 + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + * + * @since 1.6 + */ + protected $state; + + /** + * Is this view an Empty State + * + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // We don't need toolbar in the modal layout. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // In article associations modal we need to remove language filter if forcing a language. + // We also need to change the category filter to show show categories with All or the forced language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. + $languageXml = new \SimpleXMLElement(''); + $this->filterForm->setField($languageXml, 'filter', true); + + // Also, unset the active language filter so the search tools is not open by default with this filter. + unset($this->activeFilters['language']); + + // One last changes needed is to change the category filter to just show categories with All language or with the forced language. + $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $state = $this->get('State'); + $canDo = ContentHelper::getActions('com_newsfeeds', 'category', $state->get('filter.category_id')); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEEDS'), 'rss newsfeeds'); + + if ($canDo->get('core.create') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) { + $toolbar->addNew('newsfeed.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('newsfeeds.publish')->listCheck(true); + $childBar->unpublish('newsfeeds.unpublish')->listCheck(true); + $childBar->archive('newsfeeds.archive')->listCheck(true); + + if ($user->authorise('core.admin')) { + $childBar->checkin('newsfeeds.checkin')->listCheck(true); + } + + if ($this->state->get('filter.published') != -2) { + $childBar->trash('newsfeeds.trash')->listCheck(true); + } + + // Add a batch button + if ( + $user->authorise('core.create', 'com_newsfeeds') + && $user->authorise('core.edit', 'com_newsfeeds') + && $user->authorise('core.edit.state', 'com_newsfeeds') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && $state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('newsfeeds.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($user->authorise('core.admin', 'com_newsfeeds') || $user->authorise('core.options', 'com_newsfeeds')) { + $toolbar->preferences('com_newsfeeds'); + } + + $toolbar->help('News_Feeds'); + } } diff --git a/administrator/components/com_newsfeeds/tmpl/newsfeed/edit.php b/administrator/components/com_newsfeeds/tmpl/newsfeed/edit.php index 34dcd86d2be5b..dd09e694e8368 100644 --- a/administrator/components/com_newsfeeds/tmpl/newsfeed/edit.php +++ b/administrator/components/com_newsfeeds/tmpl/newsfeed/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $input = $app->input; @@ -38,82 +39,82 @@ - - -
    - 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - item->id) ? Text::_('COM_NEWSFEEDS_NEW_NEWSFEED') : Text::_('COM_NEWSFEEDS_EDIT_NEWSFEED')); ?> -
    -
    -
    - form->renderField('link'); ?> - form->renderField('description'); ?> -
    -
    -
    - -
    -
    - - - -
    -
    -
    - -
    - form->getGroup('images') as $field) : ?> - renderField(); ?> - -
    -
    -
    -
    - loadTemplate('display'); ?> -
    -
    - - - - - -
    -
    -
    - -
    - -
    -
    -
    -
    -
    - -
    - -
    -
    -
    -
    - - - - -
    - -
    - -
    -
    - - - - - - -
    - - - + + +
    + 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + item->id) ? Text::_('COM_NEWSFEEDS_NEW_NEWSFEED') : Text::_('COM_NEWSFEEDS_EDIT_NEWSFEED')); ?> +
    +
    +
    + form->renderField('link'); ?> + form->renderField('description'); ?> +
    +
    +
    + +
    +
    + + + +
    +
    +
    + +
    + form->getGroup('images') as $field) : ?> + renderField(); ?> + +
    +
    +
    +
    + loadTemplate('display'); ?> +
    +
    + + + + + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + + + + +
    + +
    + +
    +
    + + + + + + +
    + + + diff --git a/administrator/components/com_newsfeeds/tmpl/newsfeed/edit_display.php b/administrator/components/com_newsfeeds/tmpl/newsfeed/edit_display.php index 9ba2e1a89b48f..889b0677275ba 100644 --- a/administrator/components/com_newsfeeds/tmpl/newsfeed/edit_display.php +++ b/administrator/components/com_newsfeeds/tmpl/newsfeed/edit_display.php @@ -1,4 +1,5 @@
    - -
    - -
    + +
    + +
    diff --git a/administrator/components/com_newsfeeds/tmpl/newsfeed/modal.php b/administrator/components/com_newsfeeds/tmpl/newsfeed/modal.php index fcf153534e31c..91f2a029c98ee 100644 --- a/administrator/components/com_newsfeeds/tmpl/newsfeed/modal.php +++ b/administrator/components/com_newsfeeds/tmpl/newsfeed/modal.php @@ -1,4 +1,5 @@
    - setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
    diff --git a/administrator/components/com_newsfeeds/tmpl/newsfeeds/default.php b/administrator/components/com_newsfeeds/tmpl/newsfeeds/default.php index 7611fe76a8789..8fa3553429ed1 100644 --- a/administrator/components/com_newsfeeds/tmpl/newsfeeds/default.php +++ b/administrator/components/com_newsfeeds/tmpl/newsfeeds/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -29,172 +30,172 @@ $saveOrder = $listOrder == 'a.ordering'; $assoc = Associations::isEnabled(); -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_newsfeeds&task=newsfeeds.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_newsfeeds&task=newsfeeds.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
    -
    -
    -
    - $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : - $ordering = ($listOrder == 'a.ordering'); - $canCreate = $user->authorise('core.create', 'com_newsfeeds.category.' . $item->catid); - $canEdit = $user->authorise('core.edit', 'com_newsfeeds.category.' . $item->catid); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canEditOwn = $user->authorise('core.edit.own', 'com_newsfeeds.category.' . $item->catid) && $item->created_by == $user->id; - $canChange = $user->authorise('core.edit.state', 'com_newsfeeds.category.' . $item->catid) && $canCheckin; - ?> - - - - - - - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - - - - - - - -
    - id, false, 'cid', 'cb', $item->name); ?> - - - - - - - - - - published, $i, 'newsfeeds.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> - -
    - checked_out) : ?> - editor, $item->checked_out_time, 'newsfeeds.', $canCheckin); ?> - - - - escape($item->name); ?> - - escape($item->name); ?> - -
    - escape($item->alias)); ?> -
    -
    - escape($item->category_title); ?> -
    -
    -
    - escape($item->access_level); ?> - - numarticles; ?> - - cache_time; ?> - - association) : ?> - id); ?> - - - - - id; ?> -
    +
    +
    +
    + $this)); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : + $ordering = ($listOrder == 'a.ordering'); + $canCreate = $user->authorise('core.create', 'com_newsfeeds.category.' . $item->catid); + $canEdit = $user->authorise('core.edit', 'com_newsfeeds.category.' . $item->catid); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', 'com_newsfeeds.category.' . $item->catid) && $item->created_by == $user->id; + $canChange = $user->authorise('core.edit.state', 'com_newsfeeds.category.' . $item->catid) && $canCheckin; + ?> + + + + + + + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + + + + + + + +
    + id, false, 'cid', 'cb', $item->name); ?> + + + + + + + + + + published, $i, 'newsfeeds.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + +
    + checked_out) : ?> + editor, $item->checked_out_time, 'newsfeeds.', $canCheckin); ?> + + + + escape($item->name); ?> + + escape($item->name); ?> + +
    + escape($item->alias)); ?> +
    +
    + escape($item->category_title); ?> +
    +
    +
    + escape($item->access_level); ?> + + numarticles; ?> + + cache_time; ?> + + association) : ?> + id); ?> + + + + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_newsfeeds') - && $user->authorise('core.edit', 'com_newsfeeds') - && $user->authorise('core.edit.state', 'com_newsfeeds')) : ?> - Text::_('COM_NEWSFEEDS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - - - - -
    -
    -
    + + authorise('core.create', 'com_newsfeeds') + && $user->authorise('core.edit', 'com_newsfeeds') + && $user->authorise('core.edit.state', 'com_newsfeeds') +) : ?> + Text::_('COM_NEWSFEEDS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + + + + +
    +
    +
    diff --git a/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_body.php b/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_body.php index 71e3729811e86..0578eea2932b0 100644 --- a/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_body.php +++ b/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Multilanguage; @@ -15,32 +17,32 @@ ?>
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    - = 0) : ?> -
    -
    - 'com_newsfeeds']); ?> -
    -
    - -
    -
    - -
    -
    -
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + = 0) : ?> +
    +
    + 'com_newsfeeds']); ?> +
    +
    + +
    +
    + +
    +
    +
    diff --git a/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_footer.php b/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_footer.php index b8394dfdf6527..f43ed8b7b5b8b 100644 --- a/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_footer.php +++ b/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_footer.php @@ -1,4 +1,5 @@ diff --git a/administrator/components/com_newsfeeds/tmpl/newsfeeds/emptystate.php b/administrator/components/com_newsfeeds/tmpl/newsfeeds/emptystate.php index 64c1fd1b22b52..234282c353b0e 100644 --- a/administrator/components/com_newsfeeds/tmpl/newsfeeds/emptystate.php +++ b/administrator/components/com_newsfeeds/tmpl/newsfeeds/emptystate.php @@ -1,4 +1,5 @@ 'COM_NEWSFEEDS', - 'formURL' => 'index.php?option=com_newsfeeds&view=newsfeeds', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:News_Feeds', - 'icon' => 'icon-rss newsfeeds', + 'textPrefix' => 'COM_NEWSFEEDS', + 'formURL' => 'index.php?option=com_newsfeeds&view=newsfeeds', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:News_Feeds', + 'icon' => 'icon-rss newsfeeds', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_newsfeeds') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_newsfeeds&task=newsfeed.add'; +if ($user->authorise('core.create', 'com_newsfeeds') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_newsfeeds&task=newsfeed.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_newsfeeds/tmpl/newsfeeds/modal.php b/administrator/components/com_newsfeeds/tmpl/newsfeeds/modal.php index 6ab5819394f2a..c99d6372b0724 100644 --- a/administrator/components/com_newsfeeds/tmpl/newsfeeds/modal.php +++ b/administrator/components/com_newsfeeds/tmpl/newsfeeds/modal.php @@ -1,4 +1,5 @@
    -
    + - $this)); ?> + $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', - ); - ?> - items as $i => $item) : ?> - language && $multilang) - { - $tag = strlen($item->language); - if ($tag == 5) - { - $lang = substr($item->language, 0, 2); - } - elseif ($tag == 6) - { - $lang = substr($item->language, 0, 3); - } - else { - $lang = ''; - } - } - elseif (!$multilang) - { - $lang = ''; - } - ?> - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - -
    - - - - - - escape($item->name); ?> -
    - escape($item->category_title); ?> -
    -
    - escape($item->access_level); ?> - - - - id; ?> -
    + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', + ); + ?> + items as $i => $item) : ?> + language && $multilang) { + $tag = strlen($item->language); + if ($tag == 5) { + $lang = substr($item->language, 0, 2); + } elseif ($tag == 6) { + $lang = substr($item->language, 0, 3); + } else { + $lang = ''; + } + } elseif (!$multilang) { + $lang = ''; + } + ?> + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + +
    + + + + + + escape($item->name); ?> +
    + escape($item->category_title); ?> +
    +
    + escape($item->access_level); ?> + + + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - + + + + -
    +
    diff --git a/administrator/components/com_plugins/helpers/plugins.php b/administrator/components/com_plugins/helpers/plugins.php index a2da98baa77f8..ff6a4aca55a47 100644 --- a/administrator/components/com_plugins/helpers/plugins.php +++ b/administrator/components/com_plugins/helpers/plugins.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Plugins')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Plugins')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Plugins')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Plugins')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_plugins/src/Controller/DisplayController.php b/administrator/components/com_plugins/src/Controller/DisplayController.php index bf6cbc9179085..a6af18afb94cb 100644 --- a/administrator/components/com_plugins/src/Controller/DisplayController.php +++ b/administrator/components/com_plugins/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'plugins'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('extension_id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', 'plugins'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('extension_id'); - // Check for edit form. - if ($view == 'plugin' && $layout == 'edit' && !$this->checkEditId('com_plugins.edit.plugin', $id)) - { - // 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', $id), 'error'); - } + // Check for edit form. + if ($view == 'plugin' && $layout == 'edit' && !$this->checkEditId('com_plugins.edit.plugin', $id)) { + // 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', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_plugins&view=plugins', false)); + $this->setRedirect(Route::_('index.php?option=com_plugins&view=plugins', false)); - return false; - } + return false; + } - parent::display(); - } + parent::display(); + } } diff --git a/administrator/components/com_plugins/src/Controller/PluginController.php b/administrator/components/com_plugins/src/Controller/PluginController.php index 8585648f24198..57454ef0bf8e6 100644 --- a/administrator/components/com_plugins/src/Controller/PluginController.php +++ b/administrator/components/com_plugins/src/Controller/PluginController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Plugins\Administrator\Controller; \defined('_JEXEC') or die; diff --git a/administrator/components/com_plugins/src/Controller/PluginsController.php b/administrator/components/com_plugins/src/Controller/PluginsController.php index aa0883e13569d..fd37e3758d915 100644 --- a/administrator/components/com_plugins/src/Controller/PluginsController.php +++ b/administrator/components/com_plugins/src/Controller/PluginsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Plugin', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } - /** - * Method to get the number of activated plugins - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('Plugins'); + /** + * Method to get the number of activated plugins + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Plugins'); - $model->setState('filter.enabled', 1); + $model->setState('filter.enabled', 1); - $amount = (int) $model->getTotal(); + $amount = (int) $model->getTotal(); - $result = []; + $result = []; - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_PLUGINS_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_PLUGINS_N_QUICKICON', $amount); + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_PLUGINS_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_PLUGINS_N_QUICKICON', $amount); - echo new JsonResponse($result); - } + echo new JsonResponse($result); + } } diff --git a/administrator/components/com_plugins/src/Field/PluginElementField.php b/administrator/components/com_plugins/src/Field/PluginElementField.php index 4f50bc7a0a48f..94b3efeaabef2 100644 --- a/administrator/components/com_plugins/src/Field/PluginElementField.php +++ b/administrator/components/com_plugins/src/Field/PluginElementField.php @@ -1,4 +1,5 @@ getDatabase(); - $folder = $this->form->getValue('folder'); + /** + * Builds the query for the ordering list. + * + * @return \Joomla\Database\DatabaseQuery The query for the ordering form field. + */ + protected function getQuery() + { + $db = $this->getDatabase(); + $folder = $this->form->getValue('folder'); - // Build the query for the ordering list. - $query = $db->getQuery(true) - ->select( - array( - $db->quoteName('ordering', 'value'), - $db->quoteName('name', 'text'), - $db->quoteName('type'), - $db->quote('folder'), - $db->quote('extension_id') - ) - ) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = :folder') - ->order($db->quoteName('ordering')) - ->bind(':folder', $folder); + // Build the query for the ordering list. + $query = $db->getQuery(true) + ->select( + array( + $db->quoteName('ordering', 'value'), + $db->quoteName('name', 'text'), + $db->quoteName('type'), + $db->quote('folder'), + $db->quote('extension_id') + ) + ) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = :folder') + ->order($db->quoteName('ordering')) + ->bind(':folder', $folder); - return $query; - } + return $query; + } - /** - * Retrieves the current Item's Id. - * - * @return integer The current item ID. - */ - protected function getItemId() - { - return (int) $this->form->getValue('extension_id'); - } + /** + * Retrieves the current Item's Id. + * + * @return integer The current item ID. + */ + protected function getItemId() + { + return (int) $this->form->getValue('extension_id'); + } } diff --git a/administrator/components/com_plugins/src/Helper/PluginsHelper.php b/administrator/components/com_plugins/src/Helper/PluginsHelper.php index 39bcf135b691c..6a14e659f6e09 100644 --- a/administrator/components/com_plugins/src/Helper/PluginsHelper.php +++ b/administrator/components/com_plugins/src/Helper/PluginsHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('DISTINCT(folder) AS value, folder AS text') - ->from('#__extensions') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->order('folder'); - - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - return $options; - } - - /** - * Returns a list of elements filter options. - * - * @return string The HTML code for the select tag - */ - public static function elementOptions() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('DISTINCT(element) AS value, element AS text') - ->from('#__extensions') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->order('element'); - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - return $options; - } - - /** - * Parse the template file. - * - * @param string $templateBaseDir Base path to the template directory. - * @param string $templateDir Template directory. - * - * @return CMSObject|bool - */ - public function parseXMLTemplateFile($templateBaseDir, $templateDir) - { - $data = new CMSObject; - - // Check of the xml file exists. - $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); - - if (is_file($filePath)) - { - $xml = Installer::parseXMLInstallFile($filePath); - - if ($xml['type'] != 'template') - { - return false; - } - - foreach ($xml as $key => $value) - { - $data->set($key, $value); - } - } - - return $data; - } + public static $extension = 'com_plugins'; + + /** + * Returns an array of standard published state filter options. + * + * @return array The HTML code for the select tag + */ + public static function publishedOptions() + { + // Build the active state filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', 'JENABLED'); + $options[] = HTMLHelper::_('select.option', '0', 'JDISABLED'); + + return $options; + } + + /** + * Returns a list of folders filter options. + * + * @return string The HTML code for the select tag + */ + public static function folderOptions() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT(folder) AS value, folder AS text') + ->from('#__extensions') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->order('folder'); + + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + return $options; + } + + /** + * Returns a list of elements filter options. + * + * @return string The HTML code for the select tag + */ + public static function elementOptions() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT(element) AS value, element AS text') + ->from('#__extensions') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->order('element'); + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + return $options; + } + + /** + * Parse the template file. + * + * @param string $templateBaseDir Base path to the template directory. + * @param string $templateDir Template directory. + * + * @return CMSObject|bool + */ + public function parseXMLTemplateFile($templateBaseDir, $templateDir) + { + $data = new CMSObject(); + + // Check of the xml file exists. + $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); + + if (is_file($filePath)) { + $xml = Installer::parseXMLInstallFile($filePath); + + if ($xml['type'] != 'template') { + return false; + } + + foreach ($xml as $key => $value) { + $data->set($key, $value); + } + } + + return $data; + } } diff --git a/administrator/components/com_plugins/src/Model/PluginModel.php b/administrator/components/com_plugins/src/Model/PluginModel.php index 834c2b077e4ca..fa32e83621c0d 100644 --- a/administrator/components/com_plugins/src/Model/PluginModel.php +++ b/administrator/components/com_plugins/src/Model/PluginModel.php @@ -1,4 +1,5 @@ 'onExtensionAfterSave', - 'event_before_save' => 'onExtensionBeforeSave', - 'events_map' => array( - 'save' => 'extension' - ) - ), $config - ); - - parent::__construct($config, $factory); - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure. - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // The folder and element vars are passed when saving the form. - if (empty($data)) - { - $item = $this->getItem(); - $folder = $item->folder; - $element = $item->element; - } - else - { - $folder = ArrayHelper::getValue($data, 'folder', '', 'cmd'); - $element = ArrayHelper::getValue($data, 'element', '', 'cmd'); - } - - // Add the default fields directory - Form::addFieldPath(JPATH_PLUGINS . '/' . $folder . '/' . $element . '/field'); - - // These variables are used to add data from the plugin XML files. - $this->setState('item.folder', $folder); - $this->setState('item.element', $element); - - // Get the form. - $form = $this->loadForm('com_plugins.plugin', 'plugin', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('enabled', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('enabled', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_plugins.edit.plugin.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_plugins.plugin', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - */ - public function getItem($pk = null) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('plugin.id'); - - $cacheId = $pk; - - if (\is_array($cacheId)) - { - $cacheId = serialize($cacheId); - } - - if (!isset($this->_cache[$cacheId])) - { - // Get a row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load(\is_array($pk) ? $pk : ['extension_id' => $pk, 'type' => 'plugin']); - - // Check for a table object error. - if ($return === false) - { - return false; - } - - // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. - $properties = $table->getProperties(1); - $this->_cache[$cacheId] = ArrayHelper::toObject($properties, CMSObject::class); - - // Convert the params field to an array. - $registry = new Registry($table->params); - $this->_cache[$cacheId]->params = $registry->toArray(); - - // Get the plugin XML. - $path = Path::clean(JPATH_PLUGINS . '/' . $table->folder . '/' . $table->element . '/' . $table->element . '.xml'); - - if (file_exists($path)) - { - $this->_cache[$cacheId]->xml = simplexml_load_file($path); - } - else - { - $this->_cache[$cacheId]->xml = null; - } - } - - return $this->_cache[$cacheId]; - } - - /** - * Returns a reference to the Table object, always creating it. - * - * @param string $type The table type to instantiate. - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - */ - public function getTable($type = 'Extension', $prefix = 'JTable', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - - /** - * Auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - // Execute the parent method. - parent::populateState(); - - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('extension_id'); - $this->setState('plugin.id', $pk); - } - - /** - * Preprocess the form. - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group Cache group name. - * - * @return mixed True if successful. - * - * @since 1.6 - * - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $folder = $this->getState('item.folder'); - $element = $this->getState('item.element'); - $lang = Factory::getLanguage(); - - // Load the core and/or local language sys file(s) for the ordering field. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('element')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = :folder') - ->bind(':folder', $folder); - $db->setQuery($query); - $elements = $db->loadColumn(); - - foreach ($elements as $elementa) - { - $lang->load('plg_' . $folder . '_' . $elementa . '.sys', JPATH_ADMINISTRATOR) - || $lang->load('plg_' . $folder . '_' . $elementa . '.sys', JPATH_PLUGINS . '/' . $folder . '/' . $elementa); - } - - if (empty($folder) || empty($element)) - { - $app = Factory::getApplication(); - $app->redirect(Route::_('index.php?option=com_plugins&view=plugins', false)); - } - - $formFile = Path::clean(JPATH_PLUGINS . '/' . $folder . '/' . $element . '/' . $element . '.xml'); - - if (!file_exists($formFile)) - { - throw new \Exception(Text::sprintf('COM_PLUGINS_ERROR_FILE_NOT_FOUND', $element . '.xml')); - } - - // Load the core and/or local language file(s). - $lang->load('plg_' . $folder . '_' . $element, JPATH_ADMINISTRATOR) - || $lang->load('plg_' . $folder . '_' . $element, JPATH_PLUGINS . '/' . $folder . '/' . $element); - - if (file_exists($formFile)) - { - // Get the plugin form. - if (!$form->loadFile($formFile, false, '//config')) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Get the help data from the XML file if present. - $help = $xml->xpath('/extension/help'); - - if (!empty($help)) - { - $helpKey = trim((string) $help[0]['key']); - $helpURL = trim((string) $help[0]['url']); - - $this->helpKey = $helpKey ?: $this->helpKey; - $this->helpURL = $helpURL ?: $this->helpURL; - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - $db = $this->getDatabase(); - - return [ - $db->quoteName('type') . ' = ' . $db->quote($table->type), - $db->quoteName('folder') . ' = ' . $db->quote($table->folder), - ]; - } - - /** - * Override method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - // Setup type. - $data['type'] = 'plugin'; - - return parent::save($data); - } - - /** - * Get the necessary data to load an item help screen. - * - * @return object An object with key, url, and local properties for loading the item help screen. - * - * @since 1.6 - */ - public function getHelp() - { - return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); - } - - /** - * Custom clean cache method, plugins are cached in 2 places for different clients. - * - * @param string $group Cache group name. - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_plugins'); - } + /** + * @var string The help screen key for the module. + * @since 1.6 + */ + protected $helpKey = 'Plugins:_Name_of_Plugin'; + + /** + * @var string The help screen base URL for the module. + * @since 1.6 + */ + protected $helpURL; + + /** + * @var array An array of cached plugin items. + * @since 1.6 + */ + protected $_cache; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $config = array_merge( + array( + 'event_after_save' => 'onExtensionAfterSave', + 'event_before_save' => 'onExtensionBeforeSave', + 'events_map' => array( + 'save' => 'extension' + ) + ), + $config + ); + + parent::__construct($config, $factory); + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure. + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // The folder and element vars are passed when saving the form. + if (empty($data)) { + $item = $this->getItem(); + $folder = $item->folder; + $element = $item->element; + } else { + $folder = ArrayHelper::getValue($data, 'folder', '', 'cmd'); + $element = ArrayHelper::getValue($data, 'element', '', 'cmd'); + } + + // Add the default fields directory + Form::addFieldPath(JPATH_PLUGINS . '/' . $folder . '/' . $element . '/field'); + + // These variables are used to add data from the plugin XML files. + $this->setState('item.folder', $folder); + $this->setState('item.element', $element); + + // Get the form. + $form = $this->loadForm('com_plugins.plugin', 'plugin', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('enabled', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('enabled', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_plugins.edit.plugin.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_plugins.plugin', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + */ + public function getItem($pk = null) + { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('plugin.id'); + + $cacheId = $pk; + + if (\is_array($cacheId)) { + $cacheId = serialize($cacheId); + } + + if (!isset($this->_cache[$cacheId])) { + // Get a row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load(\is_array($pk) ? $pk : ['extension_id' => $pk, 'type' => 'plugin']); + + // Check for a table object error. + if ($return === false) { + return false; + } + + // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. + $properties = $table->getProperties(1); + $this->_cache[$cacheId] = ArrayHelper::toObject($properties, CMSObject::class); + + // Convert the params field to an array. + $registry = new Registry($table->params); + $this->_cache[$cacheId]->params = $registry->toArray(); + + // Get the plugin XML. + $path = Path::clean(JPATH_PLUGINS . '/' . $table->folder . '/' . $table->element . '/' . $table->element . '.xml'); + + if (file_exists($path)) { + $this->_cache[$cacheId]->xml = simplexml_load_file($path); + } else { + $this->_cache[$cacheId]->xml = null; + } + } + + return $this->_cache[$cacheId]; + } + + /** + * Returns a reference to the Table object, always creating it. + * + * @param string $type The table type to instantiate. + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + */ + public function getTable($type = 'Extension', $prefix = 'JTable', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + // Execute the parent method. + parent::populateState(); + + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('extension_id'); + $this->setState('plugin.id', $pk); + } + + /** + * Preprocess the form. + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group Cache group name. + * + * @return mixed True if successful. + * + * @since 1.6 + * + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $folder = $this->getState('item.folder'); + $element = $this->getState('item.element'); + $lang = Factory::getLanguage(); + + // Load the core and/or local language sys file(s) for the ordering field. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('element')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = :folder') + ->bind(':folder', $folder); + $db->setQuery($query); + $elements = $db->loadColumn(); + + foreach ($elements as $elementa) { + $lang->load('plg_' . $folder . '_' . $elementa . '.sys', JPATH_ADMINISTRATOR) + || $lang->load('plg_' . $folder . '_' . $elementa . '.sys', JPATH_PLUGINS . '/' . $folder . '/' . $elementa); + } + + if (empty($folder) || empty($element)) { + $app = Factory::getApplication(); + $app->redirect(Route::_('index.php?option=com_plugins&view=plugins', false)); + } + + $formFile = Path::clean(JPATH_PLUGINS . '/' . $folder . '/' . $element . '/' . $element . '.xml'); + + if (!file_exists($formFile)) { + throw new \Exception(Text::sprintf('COM_PLUGINS_ERROR_FILE_NOT_FOUND', $element . '.xml')); + } + + // Load the core and/or local language file(s). + $lang->load('plg_' . $folder . '_' . $element, JPATH_ADMINISTRATOR) + || $lang->load('plg_' . $folder . '_' . $element, JPATH_PLUGINS . '/' . $folder . '/' . $element); + + if (file_exists($formFile)) { + // Get the plugin form. + if (!$form->loadFile($formFile, false, '//config')) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Get the help data from the XML file if present. + $help = $xml->xpath('/extension/help'); + + if (!empty($help)) { + $helpKey = trim((string) $help[0]['key']); + $helpURL = trim((string) $help[0]['url']); + + $this->helpKey = $helpKey ?: $this->helpKey; + $this->helpURL = $helpURL ?: $this->helpURL; + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('type') . ' = ' . $db->quote($table->type), + $db->quoteName('folder') . ' = ' . $db->quote($table->folder), + ]; + } + + /** + * Override method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + // Setup type. + $data['type'] = 'plugin'; + + return parent::save($data); + } + + /** + * Get the necessary data to load an item help screen. + * + * @return object An object with key, url, and local properties for loading the item help screen. + * + * @since 1.6 + */ + public function getHelp() + { + return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); + } + + /** + * Custom clean cache method, plugins are cached in 2 places for different clients. + * + * @param string $group Cache group name. + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_plugins'); + } } diff --git a/administrator/components/com_plugins/src/Model/PluginsModel.php b/administrator/components/com_plugins/src/Model/PluginsModel.php index 6672c88654f9e..6dfb496eb5746 100644 --- a/administrator/components/com_plugins/src/Model/PluginsModel.php +++ b/administrator/components/com_plugins/src/Model/PluginsModel.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Plugins\Administrator\Model; \defined('_JEXEC') or die; @@ -25,282 +27,262 @@ */ class PluginsModel extends ListModel { - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * - * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel - * @since 3.2 - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null) - { - if (empty($config['filter_fields'])) - { - $config['filter_fields'] = array( - 'extension_id', 'a.extension_id', - 'name', 'a.name', - 'folder', 'a.folder', - 'element', 'a.element', - 'checked_out', 'a.checked_out', - 'checked_out_time', 'a.checked_out_time', - 'state', 'a.state', - 'enabled', 'a.enabled', - 'access', 'a.access', 'access_level', - 'ordering', 'a.ordering', - 'client_id', 'a.client_id', - ); - } - - parent::__construct($config, $factory); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'folder', $direction = 'asc') - { - // Load the parameters. - $params = ComponentHelper::getParams('com_plugins'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.enabled'); - $id .= ':' . $this->getState('filter.folder'); - $id .= ':' . $this->getState('filter.element'); - - return parent::getStoreId($id); - } - - /** - * Returns an object list. - * - * @param \Joomla\Database\DatabaseQuery $query A database query object. - * @param integer $limitstart Offset. - * @param integer $limit The number of records. - * - * @return array - */ - protected function _getList($query, $limitstart = 0, $limit = 0) - { - $search = $this->getState('filter.search'); - $ordering = $this->getState('list.ordering', 'ordering'); - - // If "Sort Table By:" is not set, set ordering to name - if ($ordering == '') - { - $ordering = 'name'; - } - - $db = $this->getDatabase(); - - if ($ordering == 'name' || (!empty($search) && stripos($search, 'id:') !== 0)) - { - $db->setQuery($query); - $result = $db->loadObjectList(); - $this->translate($result); - - if (!empty($search)) - { - $escapedSearchString = $this->refineSearchStringToRegex($search, '/'); - - foreach ($result as $i => $item) - { - if (!preg_match("/$escapedSearchString/i", $item->name)) - { - unset($result[$i]); - } - } - } - - $orderingDirection = strtolower($this->getState('list.direction')); - $direction = ($orderingDirection == 'desc') ? -1 : 1; - $result = ArrayHelper::sortObjects($result, $ordering, $direction, true, true); - - $total = count($result); - $this->cache[$this->getStoreId('getTotal')] = $total; - - if ($total < $limitstart) - { - $limitstart = 0; - } - - $this->cache[$this->getStoreId('getStart')] = $limitstart; - - return array_slice($result, $limitstart, $limit ?: null); - } - else - { - if ($ordering == 'ordering') - { - $query->order('a.folder ASC'); - $ordering = 'a.ordering'; - } - - $query->order($db->quoteName($ordering) . ' ' . $this->getState('list.direction')); - - if ($ordering == 'folder') - { - $query->order('a.ordering ASC'); - } - - $result = parent::_getList($query, $limitstart, $limit); - $this->translate($result); - - return $result; - } - } - - /** - * Translate a list of objects. - * - * @param array &$items The array of objects. - * - * @return array The array of translated objects. - */ - protected function translate(&$items) - { - $lang = Factory::getLanguage(); - - foreach ($items as &$item) - { - $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; - $extension = 'plg_' . $item->folder . '_' . $item->element; - $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($extension . '.sys', $source); - $item->name = Text::_($item->name); - } - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.extension_id , a.name, a.element, a.folder, a.checked_out, a.checked_out_time,' . - ' a.enabled, a.access, a.ordering, a.note' - ) - ) - ->from($db->quoteName('#__extensions') . ' AS a') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')); - - // Join over the users for the checked out user. - $query->select('uc.name AS editor') - ->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); - - // Join over the asset groups. - $query->select('ag.title AS access_level') - ->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - $access = (int) $access; - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Filter by published state. - $published = (string) $this->getState('filter.enabled'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.enabled') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->whereIn($db->quoteName('a.enabled'), [0, 1]); - } - - // Filter by state. - $query->where('a.state >= 0'); - - // Filter by folder. - if ($folder = $this->getState('filter.folder')) - { - $query->where($db->quoteName('a.folder') . ' = :folder') - ->bind(':folder', $folder); - } - - // Filter by element. - if ($element = $this->getState('filter.element')) - { - $query->where($db->quoteName('a.element') . ' = :element') - ->bind(':element', $element); - } - - // Filter by search in name or id. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.extension_id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - } - - return $query; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 3.5 - */ - protected function loadFormData() - { - $data = parent::loadFormData(); - - // Set the selected filter values for pages that use the Layouts for filtering - $data->list['sortTable'] = $this->state->get('list.ordering'); - $data->list['directionTable'] = $this->state->get('list.direction'); - - return $data; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'extension_id', 'a.extension_id', + 'name', 'a.name', + 'folder', 'a.folder', + 'element', 'a.element', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'state', 'a.state', + 'enabled', 'a.enabled', + 'access', 'a.access', 'access_level', + 'ordering', 'a.ordering', + 'client_id', 'a.client_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'folder', $direction = 'asc') + { + // Load the parameters. + $params = ComponentHelper::getParams('com_plugins'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.enabled'); + $id .= ':' . $this->getState('filter.folder'); + $id .= ':' . $this->getState('filter.element'); + + return parent::getStoreId($id); + } + + /** + * Returns an object list. + * + * @param \Joomla\Database\DatabaseQuery $query A database query object. + * @param integer $limitstart Offset. + * @param integer $limit The number of records. + * + * @return array + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $search = $this->getState('filter.search'); + $ordering = $this->getState('list.ordering', 'ordering'); + + // If "Sort Table By:" is not set, set ordering to name + if ($ordering == '') { + $ordering = 'name'; + } + + $db = $this->getDatabase(); + + if ($ordering == 'name' || (!empty($search) && stripos($search, 'id:') !== 0)) { + $db->setQuery($query); + $result = $db->loadObjectList(); + $this->translate($result); + + if (!empty($search)) { + $escapedSearchString = $this->refineSearchStringToRegex($search, '/'); + + foreach ($result as $i => $item) { + if (!preg_match("/$escapedSearchString/i", $item->name)) { + unset($result[$i]); + } + } + } + + $orderingDirection = strtolower($this->getState('list.direction')); + $direction = ($orderingDirection == 'desc') ? -1 : 1; + $result = ArrayHelper::sortObjects($result, $ordering, $direction, true, true); + + $total = count($result); + $this->cache[$this->getStoreId('getTotal')] = $total; + + if ($total < $limitstart) { + $limitstart = 0; + } + + $this->cache[$this->getStoreId('getStart')] = $limitstart; + + return array_slice($result, $limitstart, $limit ?: null); + } else { + if ($ordering == 'ordering') { + $query->order('a.folder ASC'); + $ordering = 'a.ordering'; + } + + $query->order($db->quoteName($ordering) . ' ' . $this->getState('list.direction')); + + if ($ordering == 'folder') { + $query->order('a.ordering ASC'); + } + + $result = parent::_getList($query, $limitstart, $limit); + $this->translate($result); + + return $result; + } + } + + /** + * Translate a list of objects. + * + * @param array &$items The array of objects. + * + * @return array The array of translated objects. + */ + protected function translate(&$items) + { + $lang = Factory::getLanguage(); + + foreach ($items as &$item) { + $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; + $extension = 'plg_' . $item->folder . '_' . $item->element; + $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($extension . '.sys', $source); + $item->name = Text::_($item->name); + } + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.extension_id , a.name, a.element, a.folder, a.checked_out, a.checked_out_time,' . + ' a.enabled, a.access, a.ordering, a.note' + ) + ) + ->from($db->quoteName('#__extensions') . ' AS a') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')); + + // Join over the users for the checked out user. + $query->select('uc.name AS editor') + ->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); + + // Join over the asset groups. + $query->select('ag.title AS access_level') + ->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + $access = (int) $access; + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Filter by published state. + $published = (string) $this->getState('filter.enabled'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.enabled') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->whereIn($db->quoteName('a.enabled'), [0, 1]); + } + + // Filter by state. + $query->where('a.state >= 0'); + + // Filter by folder. + if ($folder = $this->getState('filter.folder')) { + $query->where($db->quoteName('a.folder') . ' = :folder') + ->bind(':folder', $folder); + } + + // Filter by element. + if ($element = $this->getState('filter.element')) { + $query->where($db->quoteName('a.element') . ' = :element') + ->bind(':element', $element); + } + + // Filter by search in name or id. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.extension_id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } + } + + return $query; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 3.5 + */ + protected function loadFormData() + { + $data = parent::loadFormData(); + + // Set the selected filter values for pages that use the Layouts for filtering + $data->list['sortTable'] = $this->state->get('list.ordering'); + $data->list['directionTable'] = $this->state->get('list.direction'); + + return $data; + } } diff --git a/administrator/components/com_plugins/src/View/Plugin/HtmlView.php b/administrator/components/com_plugins/src/View/Plugin/HtmlView.php index 888a2303002ab..0251176dd3f40 100644 --- a/administrator/components/com_plugins/src/View/Plugin/HtmlView.php +++ b/administrator/components/com_plugins/src/View/Plugin/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $canDo = ContentHelper::getActions('com_plugins'); - - ToolbarHelper::title(Text::sprintf('COM_PLUGINS_MANAGER_PLUGIN', Text::_($this->item->name)), 'plug plugin'); - - // If not checked out, can save the item. - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('plugin.apply'); - - ToolbarHelper::save('plugin.save'); - } - - ToolbarHelper::cancel('plugin.cancel', 'JTOOLBAR_CLOSE'); - ToolbarHelper::divider(); - - // Get the help information for the plugin item. - $lang = Factory::getLanguage(); - - $help = $this->get('Help'); - - if ($help->url && $lang->hasKey($help->url)) - { - $debug = $lang->setDebug(false); - $url = Text::_($help->url); - $lang->setDebug($debug); - } - else - { - $url = null; - } - - ToolbarHelper::inlinehelp(); - ToolbarHelper::help($help->key, false, $url); - } + /** + * The item object for the newsfeed + * + * @var CMSObject + */ + protected $item; + + /** + * The form object for the newsfeed + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The model state of the newsfeed + * + * @var CMSObject + */ + protected $state; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $canDo = ContentHelper::getActions('com_plugins'); + + ToolbarHelper::title(Text::sprintf('COM_PLUGINS_MANAGER_PLUGIN', Text::_($this->item->name)), 'plug plugin'); + + // If not checked out, can save the item. + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('plugin.apply'); + + ToolbarHelper::save('plugin.save'); + } + + ToolbarHelper::cancel('plugin.cancel', 'JTOOLBAR_CLOSE'); + ToolbarHelper::divider(); + + // Get the help information for the plugin item. + $lang = Factory::getLanguage(); + + $help = $this->get('Help'); + + if ($help->url && $lang->hasKey($help->url)) { + $debug = $lang->setDebug(false); + $url = Text::_($help->url); + $lang->setDebug($debug); + } else { + $url = null; + } + + ToolbarHelper::inlinehelp(); + ToolbarHelper::help($help->key, false, $url); + } } diff --git a/administrator/components/com_plugins/src/View/Plugins/HtmlView.php b/administrator/components/com_plugins/src/View/Plugins/HtmlView.php index dae68430e1421..ae782b343c04f 100644 --- a/administrator/components/com_plugins/src/View/Plugins/HtmlView.php +++ b/administrator/components/com_plugins/src/View/Plugins/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_plugins'); - - ToolbarHelper::title(Text::_('COM_PLUGINS_MANAGER_PLUGINS'), 'plug plugin'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.edit.state')) - { - $toolbar->publish('plugins.publish', 'JTOOLBAR_ENABLE')->listCheck(true); - $toolbar->unpublish('plugins.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); - $toolbar->checkin('plugins.checkin')->listCheck(true); - } - - if ($canDo->get('core.admin')) - { - $toolbar->preferences('com_plugins'); - } - - $toolbar->help('Plugins'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_plugins'); + + ToolbarHelper::title(Text::_('COM_PLUGINS_MANAGER_PLUGINS'), 'plug plugin'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.edit.state')) { + $toolbar->publish('plugins.publish', 'JTOOLBAR_ENABLE')->listCheck(true); + $toolbar->unpublish('plugins.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); + $toolbar->checkin('plugins.checkin')->listCheck(true); + } + + if ($canDo->get('core.admin')) { + $toolbar->preferences('com_plugins'); + } + + $toolbar->help('Plugins'); + } } diff --git a/administrator/components/com_plugins/tmpl/plugin/edit.php b/administrator/components/com_plugins/tmpl/plugin/edit.php index 06dae096c9dd4..c97f8d19a7afd 100644 --- a/administrator/components/com_plugins/tmpl/plugin/edit.php +++ b/administrator/components/com_plugins/tmpl/plugin/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $this->fieldsets = $this->form->getFieldsets('params'); $this->useCoreUI = true; @@ -32,111 +33,105 @@ ?>
    -
    - - 'general', 'recall' => true, 'breakpoint' => 768]); ?> - - - -
    -
    - item->xml) : ?> - item->xml->description) : ?> -

    - item->xml) - { - echo ($text = (string) $this->item->xml->name) ? Text::_($text) : $this->item->name; - } - else - { - echo Text::_('COM_PLUGINS_XML_ERR'); - } - ?> -

    -
    - - form->getValue('folder'); ?> - / - - form->getValue('element'); ?> - -
    -
    - fieldset = 'description'; - $short_description = Text::_($this->item->xml->description); - $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); - - if (!$long_description) - { - $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); - - if (strlen($truncated) > 500) - { - $long_description = $short_description; - $short_description = HTMLHelper::_('string.truncate', $truncated, 250); - - if ($short_description == $long_description) - { - $long_description = ''; - } - } - } - ?> -

    - -

    - - - -

    - -
    - - -
    - - -
    - - fieldset = 'basic'; - $html = LayoutHelper::render('joomla.edit.fieldset', $this); - echo $html ? '
    ' . $html : ''; - ?> -
    -
    - fields = array( - 'enabled', - 'access', - 'ordering', - 'folder', - 'element', - 'note', - ); ?> - -
    -
    - - - - - - - - - fieldsets = array(); - $this->ignore_fieldsets = array('basic', 'description'); - echo LayoutHelper::render('joomla.edit.params', $this); - ?> - - -
    - - - +
    + + 'general', 'recall' => true, 'breakpoint' => 768]); ?> + + + +
    +
    + item->xml) : ?> + item->xml->description) : ?> +

    + item->xml) { + echo ($text = (string) $this->item->xml->name) ? Text::_($text) : $this->item->name; + } else { + echo Text::_('COM_PLUGINS_XML_ERR'); + } + ?> +

    +
    + + form->getValue('folder'); ?> + / + + form->getValue('element'); ?> + +
    +
    + fieldset = 'description'; + $short_description = Text::_($this->item->xml->description); + $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); + + if (!$long_description) { + $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); + + if (strlen($truncated) > 500) { + $long_description = $short_description; + $short_description = HTMLHelper::_('string.truncate', $truncated, 250); + + if ($short_description == $long_description) { + $long_description = ''; + } + } + } + ?> +

    + +

    + + + +

    + +
    + + +
    + + +
    + + fieldset = 'basic'; + $html = LayoutHelper::render('joomla.edit.fieldset', $this); + echo $html ? '
    ' . $html : ''; + ?> +
    +
    + fields = array( + 'enabled', + 'access', + 'ordering', + 'folder', + 'element', + 'note', + ); ?> + +
    +
    + + + + + + + + + fieldsets = array(); + $this->ignore_fieldsets = array('basic', 'description'); + echo LayoutHelper::render('joomla.edit.params', $this); + ?> + + +
    + + +
    diff --git a/administrator/components/com_plugins/tmpl/plugin/modal.php b/administrator/components/com_plugins/tmpl/plugin/modal.php index 4c6534c5c401d..aeff088dc4fb9 100644 --- a/administrator/components/com_plugins/tmpl/plugin/modal.php +++ b/administrator/components/com_plugins/tmpl/plugin/modal.php @@ -1,4 +1,5 @@
    - setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
    diff --git a/administrator/components/com_plugins/tmpl/plugins/default.php b/administrator/components/com_plugins/tmpl/plugins/default.php index 541fd8e0f351f..296766df2ccb3 100644 --- a/administrator/components/com_plugins/tmpl/plugins/default.php +++ b/administrator/components/com_plugins/tmpl/plugins/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'ordering'; -if ($saveOrder) -{ - $saveOrderingUrl = 'index.php?option=com_plugins&task=plugins.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder) { + $saveOrderingUrl = 'index.php?option=com_plugins&task=plugins.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
    -
    - $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : - $ordering = ($listOrder == 'ordering'); - $canEdit = $user->authorise('core.edit', 'com_plugins'); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_plugins') && $canCheckin; - ?> - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - - - -
    - extension_id, false, 'cid', 'cb', $item->name); ?> - - - - - - - - - - enabled, $i, 'plugins.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'plugins.', $canCheckin); ?> - - - - name; ?> - note)) : ?> -
    - escape($item->note)); ?> -
    - - - name; ?> - -
    - escape($item->folder); ?> - - escape($item->element); ?> - - escape($item->access_level); ?> - - extension_id; ?> -
    +
    + $this)); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : + $ordering = ($listOrder == 'ordering'); + $canEdit = $user->authorise('core.edit', 'com_plugins'); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_plugins') && $canCheckin; + ?> + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + + + +
    + extension_id, false, 'cid', 'cb', $item->name); ?> + + + + + + + + + + enabled, $i, 'plugins.', $canChange); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'plugins.', $canCheckin); ?> + + + + name; ?> + note)) : ?> +
    + escape($item->note)); ?> +
    + + + name; ?> + +
    + escape($item->folder); ?> + + escape($item->element); ?> + + escape($item->access_level); ?> + + extension_id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
    + + + +
    diff --git a/administrator/components/com_postinstall/services/provider.php b/administrator/components/com_postinstall/services/provider.php index eeb3f3a013f91..d0995305ee02e 100644 --- a/administrator/components/com_postinstall/services/provider.php +++ b/administrator/components/com_postinstall/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Postinstall')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Postinstall')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Postinstall')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Postinstall')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_postinstall/src/Controller/DisplayController.php b/administrator/components/com_postinstall/src/Controller/DisplayController.php index e9a6be0e6d11e..1eacf0c5b6e4f 100644 --- a/administrator/components/com_postinstall/src/Controller/DisplayController.php +++ b/administrator/components/com_postinstall/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getIdentity()->authorise('core.manage', 'com_postinstall')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Messages'); - - echo new JsonResponse($model->getItemsCount()); - } + /** + * @var string The default view. + * @since 1.6 + */ + protected $default_view = 'messages'; + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_postinstall')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Messages'); + + echo new JsonResponse($model->getItemsCount()); + } } diff --git a/administrator/components/com_postinstall/src/Controller/MessageController.php b/administrator/components/com_postinstall/src/Controller/MessageController.php index 1342187f6713c..95806525465f9 100644 --- a/administrator/components/com_postinstall/src/Controller/MessageController.php +++ b/administrator/components/com_postinstall/src/Controller/MessageController.php @@ -1,4 +1,5 @@ checkToken('get'); - - /** @var MessagesModel $model */ - $model = $this->getModel('Messages', '', ['ignore_request' => true]); - $eid = $this->input->getInt('eid'); - - if (empty($eid)) - { - $eid = $model->getJoomlaFilesExtensionId(); - } - - $model->resetMessages($eid); - - $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); - } - - /** - * Unpublishes post-installation message of the specified extension. - * - * @return void - * - * @since 3.2 - */ - public function unpublish() - { - $model = $this->getModel('Messages', '', ['ignore_request' => true]); - - $id = $this->input->get('id'); - - $eid = (int) $model->getState('eid', $model->getJoomlaFilesExtensionId()); - - if (empty($eid)) - { - $eid = $model->getJoomlaFilesExtensionId(); - } - - $model->setState('published', 0); - $model->unpublishMessage($id); - - $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); - } - - /** - * Re-Publishes an archived post-installation message of the specified extension. - * - * @return void - * - * @since 4.2.0 - */ - public function republish() - { - $model = $this->getModel('Messages', '', ['ignore_request' => true]); - - $id = $this->input->get('id'); - - $eid = (int) $model->getState('eid', $model->getJoomlaFilesExtensionId()); - - if (empty($eid)) - { - $eid = $model->getJoomlaFilesExtensionId(); - } - - $model->setState('published', 1); - $model->republishMessage($id); - - $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); - } - - /** - * Archives a published post-installation message of the specified extension. - * - * @return void - * - * @since 4.2.0 - */ - public function archive() - { - $model = $this->getModel('Messages', '', ['ignore_request' => true]); - - $id = $this->input->get('id'); - - $eid = (int) $model->getState('eid', $model->getJoomlaFilesExtensionId()); - - if (empty($eid)) - { - $eid = $model->getJoomlaFilesExtensionId(); - } - - $model->setState('published', 2); - $model->archiveMessage($id); - - $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); - } - - /** - * Executes the action associated with an item. - * - * @return void - * - * @since 3.2 - */ - public function action() - { - $this->checkToken('get'); - - $model = $this->getModel('Messages', '', ['ignore_request' => true]); - - $id = $this->input->get('id'); - - $item = $model->getItem($id); - - switch ($item->type) - { - case 'link': - $this->setRedirect($item->action); - - return; - - case 'action': - $helper = new PostinstallHelper; - $file = $helper->parsePath($item->action_file); - - if (File::exists($file)) - { - require_once $file; - - call_user_func($item->action); - } - break; - } - - $this->setRedirect('index.php?option=com_postinstall'); - } - - /** - * Hides all post-installation messages of the specified extension. - * - * @return void - * - * @since 3.8.7 - */ - public function hideAll() - { - $this->checkToken(); - - /** @var MessagesModel $model */ - $model = $this->getModel('Messages', '', ['ignore_request' => true]); - $eid = $this->input->getInt('eid'); - - if (empty($eid)) - { - $eid = $model->getJoomlaFilesExtensionId(); - } - - $model->hideMessages($eid); - $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); - } + /** + * Resets all post-installation messages of the specified extension. + * + * @return void + * + * @since 3.2 + */ + public function reset() + { + $this->checkToken('get'); + + /** @var MessagesModel $model */ + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + $eid = $this->input->getInt('eid'); + + if (empty($eid)) { + $eid = $model->getJoomlaFilesExtensionId(); + } + + $model->resetMessages($eid); + + $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); + } + + /** + * Unpublishes post-installation message of the specified extension. + * + * @return void + * + * @since 3.2 + */ + public function unpublish() + { + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + + $id = $this->input->get('id'); + + $eid = (int) $model->getState('eid', $model->getJoomlaFilesExtensionId()); + + if (empty($eid)) { + $eid = $model->getJoomlaFilesExtensionId(); + } + + $model->setState('published', 0); + $model->unpublishMessage($id); + + $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); + } + + /** + * Re-Publishes an archived post-installation message of the specified extension. + * + * @return void + * + * @since 4.2.0 + */ + public function republish() + { + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + + $id = $this->input->get('id'); + + $eid = (int) $model->getState('eid', $model->getJoomlaFilesExtensionId()); + + if (empty($eid)) { + $eid = $model->getJoomlaFilesExtensionId(); + } + + $model->setState('published', 1); + $model->republishMessage($id); + + $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); + } + + /** + * Archives a published post-installation message of the specified extension. + * + * @return void + * + * @since 4.2.0 + */ + public function archive() + { + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + + $id = $this->input->get('id'); + + $eid = (int) $model->getState('eid', $model->getJoomlaFilesExtensionId()); + + if (empty($eid)) { + $eid = $model->getJoomlaFilesExtensionId(); + } + + $model->setState('published', 2); + $model->archiveMessage($id); + + $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); + } + + /** + * Executes the action associated with an item. + * + * @return void + * + * @since 3.2 + */ + public function action() + { + $this->checkToken('get'); + + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + + $id = $this->input->get('id'); + + $item = $model->getItem($id); + + switch ($item->type) { + case 'link': + $this->setRedirect($item->action); + + return; + + case 'action': + $helper = new PostinstallHelper(); + $file = $helper->parsePath($item->action_file); + + if (File::exists($file)) { + require_once $file; + + call_user_func($item->action); + } + break; + } + + $this->setRedirect('index.php?option=com_postinstall'); + } + + /** + * Hides all post-installation messages of the specified extension. + * + * @return void + * + * @since 3.8.7 + */ + public function hideAll() + { + $this->checkToken(); + + /** @var MessagesModel $model */ + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + $eid = $this->input->getInt('eid'); + + if (empty($eid)) { + $eid = $model->getJoomlaFilesExtensionId(); + } + + $model->hideMessages($eid); + $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); + } } diff --git a/administrator/components/com_postinstall/src/Helper/PostinstallHelper.php b/administrator/components/com_postinstall/src/Helper/PostinstallHelper.php index 8b10b66df68be..7cdd7f882c8aa 100644 --- a/administrator/components/com_postinstall/src/Helper/PostinstallHelper.php +++ b/administrator/components/com_postinstall/src/Helper/PostinstallHelper.php @@ -1,4 +1,5 @@ input->getInt('eid'); - - if ($eid) - { - $this->setState('eid', $eid); - } - } - - /** - * Gets an item with the given id from the database - * - * @param integer $id The item id - * - * @return Object - * - * @since 3.2 - */ - public function getItem($id) - { - $db = $this->getDatabase(); - $id = (int) $id; - - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('postinstall_message_id'), - $db->quoteName('extension_id'), - $db->quoteName('title_key'), - $db->quoteName('description_key'), - $db->quoteName('action_key'), - $db->quoteName('language_extension'), - $db->quoteName('language_client_id'), - $db->quoteName('type'), - $db->quoteName('action_file'), - $db->quoteName('action'), - $db->quoteName('condition_file'), - $db->quoteName('condition_method'), - $db->quoteName('version_introduced'), - $db->quoteName('enabled'), - ] - ) - ->from($db->quoteName('#__postinstall_messages')) - ->where($db->quoteName('postinstall_message_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - $result = $db->loadObject(); - - return $result; - } - - /** - * Unpublishes specified post-install message - * - * @param integer $id The message id - * - * @return void - */ - public function unpublishMessage($id) - { - $db = $this->getDatabase(); - $id = (int) $id; - - $query = $db->getQuery(true); - $query - ->update($db->quoteName('#__postinstall_messages')) - ->set($db->quoteName('enabled') . ' = 0') - ->where($db->quoteName('postinstall_message_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - Factory::getCache()->clean('com_postinstall'); - } - - /** - * Archives specified post-install message - * - * @param integer $id The message id - * - * @return void - * - * @since 4.2.0 - */ - public function archiveMessage($id) - { - $db = $this->getDatabase(); - $id = (int) $id; - - $query = $db->getQuery(true); - $query - ->update($db->quoteName('#__postinstall_messages')) - ->set($db->quoteName('enabled') . ' = 2') - ->where($db->quoteName('postinstall_message_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - Factory::getCache()->clean('com_postinstall'); - } - - /** - * Republishes specified post-install message - * - * @param integer $id The message id - * - * @return void - * - * @since 4.2.0 - */ - public function republishMessage($id) - { - $db = $this->getDatabase(); - $id = (int) $id; - - $query = $db->getQuery(true); - $query - ->update($db->quoteName('#__postinstall_messages')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('postinstall_message_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - Factory::getCache()->clean('com_postinstall'); - } - - /** - * Returns a list of messages from the #__postinstall_messages table - * - * @return array - * - * @since 3.2 - */ - public function getItems() - { - // Add a forced extension filtering to the list - $eid = (int) $this->getState('eid', $this->getJoomlaFilesExtensionId()); - - // Build a cache ID for the resulting data object - $cacheId = 'postinstall_messages.' . $eid; - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('postinstall_message_id'), - $db->quoteName('extension_id'), - $db->quoteName('title_key'), - $db->quoteName('description_key'), - $db->quoteName('action_key'), - $db->quoteName('language_extension'), - $db->quoteName('language_client_id'), - $db->quoteName('type'), - $db->quoteName('action_file'), - $db->quoteName('action'), - $db->quoteName('condition_file'), - $db->quoteName('condition_method'), - $db->quoteName('version_introduced'), - $db->quoteName('enabled'), - ] - ) - ->from($db->quoteName('#__postinstall_messages')); - $query->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $eid, ParameterType::INTEGER); - - // Force filter only enabled messages - $query->whereIn($db->quoteName('enabled'), [1, 2]); - $db->setQuery($query); - - try - { - /** @var CallbackController $cache */ - $cache = $this->getCacheControllerFactory()->createCacheController('callback', ['defaultgroup' => 'com_postinstall']); - - $result = $cache->get(array($db, 'loadObjectList'), array(), md5($cacheId), false); - } - catch (\RuntimeException $e) - { - $app = Factory::getApplication(); - $app->getLogger()->warning( - Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), - array('category' => 'jerror') - ); - - return array(); - } - - $this->onProcessList($result); - - return $result; - } - - /** - * Returns a count of all enabled messages from the #__postinstall_messages table - * - * @return integer - * - * @since 4.0.0 - */ - public function getItemsCount() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('language_extension'), - $db->quoteName('language_client_id'), - $db->quoteName('condition_file'), - $db->quoteName('condition_method'), - ] - ) - ->from($db->quoteName('#__postinstall_messages')); - - // Force filter only enabled messages - $query->where($db->quoteName('enabled') . ' = 1'); - $db->setQuery($query); - - try - { - /** @var CallbackController $cache */ - $cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class) - ->createCacheController('callback', ['defaultgroup' => 'com_postinstall']); - - // Get the resulting data object for cache ID 'all.1' from com_postinstall group. - $result = $cache->get(array($db, 'loadObjectList'), array(), md5('all.1'), false); - } - catch (\RuntimeException $e) - { - $app = Factory::getApplication(); - $app->getLogger()->warning( - Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), - array('category' => 'jerror') - ); - - return 0; - } - - $this->onProcessList($result); - - return \count($result); - } - - /** - * Returns the name of an extension, as registered in the #__extensions table - * - * @param integer $eid The extension ID - * - * @return string The extension name - * - * @since 3.2 - */ - public function getExtensionName($eid) - { - // Load the extension's information from the database - $db = $this->getDatabase(); - $eid = (int) $eid; - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('name'), - $db->quoteName('element'), - $db->quoteName('client_id'), - ] - ) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $eid, ParameterType::INTEGER) - ->setLimit(1); - - $db->setQuery($query); - - $extension = $db->loadObject(); - - if (!is_object($extension)) - { - return ''; - } - - // Load language files - $basePath = JPATH_ADMINISTRATOR; - - if ($extension->client_id == 0) - { - $basePath = JPATH_SITE; - } - - $lang = Factory::getApplication()->getLanguage(); - $lang->load($extension->element, $basePath); - - // Return the localised name - return Text::_(strtoupper($extension->name)); - } - - /** - * Resets all messages for an extension - * - * @param integer $eid The extension ID whose messages we'll reset - * - * @return mixed False if we fail, a db cursor otherwise - * - * @since 3.2 - */ - public function resetMessages($eid) - { - $db = $this->getDatabase(); - $eid = (int) $eid; - - $query = $db->getQuery(true) - ->update($db->quoteName('#__postinstall_messages')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $eid, ParameterType::INTEGER); - $db->setQuery($query); - - $result = $db->execute(); - Factory::getCache()->clean('com_postinstall'); - - return $result; - } - - /** - * Hides all messages for an extension - * - * @param integer $eid The extension ID whose messages we'll hide - * - * @return mixed False if we fail, a db cursor otherwise - * - * @since 3.8.7 - */ - public function hideMessages($eid) - { - $db = $this->getDatabase(); - $eid = (int) $eid; - - $query = $db->getQuery(true) - ->update($db->quoteName('#__postinstall_messages')) - ->set($db->quoteName('enabled') . ' = 0') - ->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $eid, ParameterType::INTEGER); - $db->setQuery($query); - - $result = $db->execute(); - Factory::getCache()->clean('com_postinstall'); - - return $result; - } - - /** - * List post-processing. This is used to run the programmatic display - * conditions against each list item and decide if we have to show it or - * not. - * - * Do note that this a core method of the RAD Layer which operates directly - * on the list it's being fed. A little touch of modern magic. - * - * @param array &$resultArray A list of items to process - * - * @return void - * - * @since 3.2 - */ - protected function onProcessList(&$resultArray) - { - $unset_keys = array(); - $language_extensions = array(); - - // Order the results DESC so the newest is on the top. - $resultArray = array_reverse($resultArray); - - foreach ($resultArray as $key => $item) - { - // Filter out messages based on dynamically loaded programmatic conditions. - if (!empty($item->condition_file) && !empty($item->condition_method)) - { - $helper = new PostinstallHelper; - $file = $helper->parsePath($item->condition_file); - - if (File::exists($file)) - { - require_once $file; - - $result = call_user_func($item->condition_method); - - if ($result === false) - { - $unset_keys[] = $key; - } - } - } - - // Load the necessary language files. - if (!empty($item->language_extension)) - { - $hash = $item->language_client_id . '-' . $item->language_extension; - - if (!in_array($hash, $language_extensions)) - { - $language_extensions[] = $hash; - Factory::getApplication()->getLanguage()->load($item->language_extension, $item->language_client_id == 0 ? JPATH_SITE : JPATH_ADMINISTRATOR); - } - } - } - - if (!empty($unset_keys)) - { - foreach ($unset_keys as $key) - { - unset($resultArray[$key]); - } - } - } - - /** - * Get the dropdown options for the list of component with post-installation messages - * - * @since 3.4 - * - * @return array Compatible with JHtmlSelect::genericList - */ - public function getComponentOptions() - { - $db = $this->getDatabase(); - - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__postinstall_messages')) - ->group($db->quoteName('extension_id')); - $db->setQuery($query); - $extension_ids = $db->loadColumn(); - - $options = array(); - - Factory::getApplication()->getLanguage()->load('files_joomla.sys', JPATH_SITE, null, false, false); - - foreach ($extension_ids as $eid) - { - $options[] = HTMLHelper::_('select.option', $eid, $this->getExtensionName($eid)); - } - - return $options; - } - - /** - * Adds or updates a post-installation message (PIM) definition. You can use this in your post-installation script using this code: - * - * require_once JPATH_LIBRARIES . '/fof/include.php'; - * FOFModel::getTmpInstance('Messages', 'PostinstallModel')->addPostInstallationMessage($options); - * - * The $options array contains the following mandatory keys: - * - * extension_id The numeric ID of the extension this message is for (see the #__extensions table) - * - * type One of message, link or action. Their meaning is: - * message Informative message. The user can dismiss it. - * link The action button links to a URL. The URL is defined in the action parameter. - * action A PHP action takes place when the action button is clicked. You need to specify the action_file - * (RAD path to the PHP file) and action (PHP function name) keys. See below for more information. - * - * title_key The Text language key for the title of this PIM. - * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_TITLE - * - * description_key The Text language key for the main body (description) of this PIM - * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_DESCRIPTION - * - * action_key The Text language key for the action button. Ignored and not required when type=message - * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_ACTION - * - * language_extension The extension name which holds the language keys used above. - * For example, com_foobar, mod_something, plg_system_whatever, tpl_mytemplate - * - * language_client_id Should we load the frontend (0) or backend (1) language keys? - * - * version_introduced Which was the version of your extension where this message appeared for the first time? - * Example: 3.2.1 - * - * enabled Must be 1 for this message to be enabled. If you omit it, it defaults to 1. - * - * condition_file The RAD path to a PHP file containing a PHP function which determines whether this message should be shown to - * the user. @see FOFTemplateUtils::parsePath() for RAD path format. Joomla! will include this file before calling - * the condition_method. - * Example: admin://components/com_foobar/helpers/postinstall.php - * - * condition_method The name of a PHP function which will be used to determine whether to show this message to the user. This must be - * a simple PHP user function (not a class method, static method etc) which returns true to show the message and false - * to hide it. This function is defined in the condition_file. - * Example: com_foobar_postinstall_messageone_condition - * - * When type=message no additional keys are required. - * - * When type=link the following additional keys are required: - * - * action The URL which will open when the user clicks on the PIM's action button - * Example: index.php?option=com_foobar&view=tools&task=installSampleData - * - * When type=action the following additional keys are required: - * - * action_file The RAD path to a PHP file containing a PHP function which performs the action of this PIM. @see FOFTemplateUtils::parsePath() - * for RAD path format. Joomla! will include this file before calling the function defined in the action key below. - * Example: admin://components/com_foobar/helpers/postinstall.php - * - * action The name of a PHP function which will be used to run the action of this PIM. This must be a simple PHP user function - * (not a class method, static method etc) which returns no result. - * Example: com_foobar_postinstall_messageone_action - * - * @param array $options See description - * - * @return $this - * - * @throws \Exception - */ - public function addPostInstallationMessage(array $options) - { - // Make sure there are options set - if (!is_array($options)) - { - throw new \Exception('Post-installation message definitions must be of type array', 500); - } - - // Initialise array keys - $defaultOptions = array( - 'extension_id' => '', - 'type' => '', - 'title_key' => '', - 'description_key' => '', - 'action_key' => '', - 'language_extension' => '', - 'language_client_id' => '', - 'action_file' => '', - 'action' => '', - 'condition_file' => '', - 'condition_method' => '', - 'version_introduced' => '', - 'enabled' => '1', - ); - - $options = array_merge($defaultOptions, $options); - - // Array normalisation. Removes array keys not belonging to a definition. - $defaultKeys = array_keys($defaultOptions); - $allKeys = array_keys($options); - $extraKeys = array_diff($allKeys, $defaultKeys); - - if (!empty($extraKeys)) - { - foreach ($extraKeys as $key) - { - unset($options[$key]); - } - } - - // Normalisation of integer values - $options['extension_id'] = (int) $options['extension_id']; - $options['language_client_id'] = (int) $options['language_client_id']; - $options['enabled'] = (int) $options['enabled']; - - // Normalisation of 0/1 values - foreach (array('language_client_id', 'enabled') as $key) - { - $options[$key] = $options[$key] ? 1 : 0; - } - - // Make sure there's an extension_id - if (!(int) $options['extension_id']) - { - throw new \Exception('Post-installation message definitions need an extension_id', 500); - } - - // Make sure there's a valid type - if (!in_array($options['type'], array('message', 'link', 'action'))) - { - throw new \Exception('Post-installation message definitions need to declare a type of message, link or action', 500); - } - - // Make sure there's a title key - if (empty($options['title_key'])) - { - throw new \Exception('Post-installation message definitions need a title key', 500); - } - - // Make sure there's a description key - if (empty($options['description_key'])) - { - throw new \Exception('Post-installation message definitions need a description key', 500); - } - - // If the type is anything other than message you need an action key - if (($options['type'] != 'message') && empty($options['action_key'])) - { - throw new \Exception('Post-installation message definitions need an action key when they are of type "' . $options['type'] . '"', 500); - } - - // You must specify the language extension - if (empty($options['language_extension'])) - { - throw new \Exception('Post-installation message definitions need to specify which extension contains their language keys', 500); - } - - // The action file and method are only required for the "action" type - if ($options['type'] == 'action') - { - if (empty($options['action_file'])) - { - throw new \Exception('Post-installation message definitions need an action file when they are of type "action"', 500); - } - - $helper = new PostinstallHelper; - $file_path = $helper->parsePath($options['action_file']); - - if (!@is_file($file_path)) - { - throw new \Exception('The action file ' . $options['action_file'] . ' of your post-installation message definition does not exist', 500); - } - - if (empty($options['action'])) - { - throw new \Exception('Post-installation message definitions need an action (function name) when they are of type "action"', 500); - } - } - - if ($options['type'] == 'link') - { - if (empty($options['link'])) - { - throw new \Exception('Post-installation message definitions need an action (URL) when they are of type "link"', 500); - } - } - - // The condition file and method are only required when the type is not "message" - if ($options['type'] != 'message') - { - if (empty($options['condition_file'])) - { - throw new \Exception('Post-installation message definitions need a condition file when they are of type "' . $options['type'] . '"', 500); - } - - $helper = new PostinstallHelper; - $file_path = $helper->parsePath($options['condition_file']); - - if (!@is_file($file_path)) - { - throw new \Exception('The condition file ' . $options['condition_file'] . ' of your post-installation message definition does not exist', 500); - } - - if (empty($options['condition_method'])) - { - throw new \Exception( - 'Post-installation message definitions need a condition method (function name) when they are of type "' - . $options['type'] . '"', - 500 - ); - } - } - - // Check if the definition exists - $table = $this->getTable(); - $tableName = $table->getTableName(); - $extensionId = (int) $options['extension_id']; - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName($tableName)) - ->where( - [ - $db->quoteName('extension_id') . ' = :extensionId', - $db->quoteName('type') . ' = :type', - $db->quoteName('title_key') . ' = :titleKey', - ] - ) - ->bind(':extensionId', $extensionId, ParameterType::INTEGER) - ->bind(':type', $options['type']) - ->bind(':titleKey', $options['title_key']); - - $existingRow = $db->setQuery($query)->loadAssoc(); - - // Is the existing definition the same as the one we're trying to save? - if (!empty($existingRow)) - { - $same = true; - - foreach ($options as $k => $v) - { - if ($existingRow[$k] != $v) - { - $same = false; - break; - } - } - - // Trying to add the same row as the existing one; quit - if ($same) - { - return $this; - } - - // Otherwise it's not the same row. Remove the old row before insert a new one. - $query = $db->getQuery(true) - ->delete($db->quoteName($tableName)) - ->where( - [ - $db->quoteName('extension_id') . ' = :extensionId', - $db->quoteName('type') . ' = :type', - $db->quoteName('title_key') . ' = :titleKey', - ] - ) - ->bind(':extensionId', $extensionId, ParameterType::INTEGER) - ->bind(':type', $options['type']) - ->bind(':titleKey', $options['title_key']); - - $db->setQuery($query)->execute(); - } - - // Insert the new row - $options = (object) $options; - $db->insertObject($tableName, $options); - Factory::getCache()->clean('com_postinstall'); - - return $this; - } - - /** - * Returns the library extension ID. - * - * @return integer - * - * @since 4.0.0 - */ - public function getJoomlaFilesExtensionId() - { - return ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - } + /** + * Method to auto-populate the state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the + * configuration flag to ignore the request is set. + * + * @return void + * + * @note Calling getState in this method will result in recursion. + * @since 4.0.0 + */ + protected function populateState() + { + parent::populateState(); + + $eid = (int) Factory::getApplication()->input->getInt('eid'); + + if ($eid) { + $this->setState('eid', $eid); + } + } + + /** + * Gets an item with the given id from the database + * + * @param integer $id The item id + * + * @return Object + * + * @since 3.2 + */ + public function getItem($id) + { + $db = $this->getDatabase(); + $id = (int) $id; + + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('postinstall_message_id'), + $db->quoteName('extension_id'), + $db->quoteName('title_key'), + $db->quoteName('description_key'), + $db->quoteName('action_key'), + $db->quoteName('language_extension'), + $db->quoteName('language_client_id'), + $db->quoteName('type'), + $db->quoteName('action_file'), + $db->quoteName('action'), + $db->quoteName('condition_file'), + $db->quoteName('condition_method'), + $db->quoteName('version_introduced'), + $db->quoteName('enabled'), + ] + ) + ->from($db->quoteName('#__postinstall_messages')) + ->where($db->quoteName('postinstall_message_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + $result = $db->loadObject(); + + return $result; + } + + /** + * Unpublishes specified post-install message + * + * @param integer $id The message id + * + * @return void + */ + public function unpublishMessage($id) + { + $db = $this->getDatabase(); + $id = (int) $id; + + $query = $db->getQuery(true); + $query + ->update($db->quoteName('#__postinstall_messages')) + ->set($db->quoteName('enabled') . ' = 0') + ->where($db->quoteName('postinstall_message_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + Factory::getCache()->clean('com_postinstall'); + } + + /** + * Archives specified post-install message + * + * @param integer $id The message id + * + * @return void + * + * @since 4.2.0 + */ + public function archiveMessage($id) + { + $db = $this->getDatabase(); + $id = (int) $id; + + $query = $db->getQuery(true); + $query + ->update($db->quoteName('#__postinstall_messages')) + ->set($db->quoteName('enabled') . ' = 2') + ->where($db->quoteName('postinstall_message_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + Factory::getCache()->clean('com_postinstall'); + } + + /** + * Republishes specified post-install message + * + * @param integer $id The message id + * + * @return void + * + * @since 4.2.0 + */ + public function republishMessage($id) + { + $db = $this->getDatabase(); + $id = (int) $id; + + $query = $db->getQuery(true); + $query + ->update($db->quoteName('#__postinstall_messages')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('postinstall_message_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + Factory::getCache()->clean('com_postinstall'); + } + + /** + * Returns a list of messages from the #__postinstall_messages table + * + * @return array + * + * @since 3.2 + */ + public function getItems() + { + // Add a forced extension filtering to the list + $eid = (int) $this->getState('eid', $this->getJoomlaFilesExtensionId()); + + // Build a cache ID for the resulting data object + $cacheId = 'postinstall_messages.' . $eid; + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('postinstall_message_id'), + $db->quoteName('extension_id'), + $db->quoteName('title_key'), + $db->quoteName('description_key'), + $db->quoteName('action_key'), + $db->quoteName('language_extension'), + $db->quoteName('language_client_id'), + $db->quoteName('type'), + $db->quoteName('action_file'), + $db->quoteName('action'), + $db->quoteName('condition_file'), + $db->quoteName('condition_method'), + $db->quoteName('version_introduced'), + $db->quoteName('enabled'), + ] + ) + ->from($db->quoteName('#__postinstall_messages')); + $query->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $eid, ParameterType::INTEGER); + + // Force filter only enabled messages + $query->whereIn($db->quoteName('enabled'), [1, 2]); + $db->setQuery($query); + + try { + /** @var CallbackController $cache */ + $cache = $this->getCacheControllerFactory()->createCacheController('callback', ['defaultgroup' => 'com_postinstall']); + + $result = $cache->get(array($db, 'loadObjectList'), array(), md5($cacheId), false); + } catch (\RuntimeException $e) { + $app = Factory::getApplication(); + $app->getLogger()->warning( + Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), + array('category' => 'jerror') + ); + + return array(); + } + + $this->onProcessList($result); + + return $result; + } + + /** + * Returns a count of all enabled messages from the #__postinstall_messages table + * + * @return integer + * + * @since 4.0.0 + */ + public function getItemsCount() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('language_extension'), + $db->quoteName('language_client_id'), + $db->quoteName('condition_file'), + $db->quoteName('condition_method'), + ] + ) + ->from($db->quoteName('#__postinstall_messages')); + + // Force filter only enabled messages + $query->where($db->quoteName('enabled') . ' = 1'); + $db->setQuery($query); + + try { + /** @var CallbackController $cache */ + $cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class) + ->createCacheController('callback', ['defaultgroup' => 'com_postinstall']); + + // Get the resulting data object for cache ID 'all.1' from com_postinstall group. + $result = $cache->get(array($db, 'loadObjectList'), array(), md5('all.1'), false); + } catch (\RuntimeException $e) { + $app = Factory::getApplication(); + $app->getLogger()->warning( + Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), + array('category' => 'jerror') + ); + + return 0; + } + + $this->onProcessList($result); + + return \count($result); + } + + /** + * Returns the name of an extension, as registered in the #__extensions table + * + * @param integer $eid The extension ID + * + * @return string The extension name + * + * @since 3.2 + */ + public function getExtensionName($eid) + { + // Load the extension's information from the database + $db = $this->getDatabase(); + $eid = (int) $eid; + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('name'), + $db->quoteName('element'), + $db->quoteName('client_id'), + ] + ) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $eid, ParameterType::INTEGER) + ->setLimit(1); + + $db->setQuery($query); + + $extension = $db->loadObject(); + + if (!is_object($extension)) { + return ''; + } + + // Load language files + $basePath = JPATH_ADMINISTRATOR; + + if ($extension->client_id == 0) { + $basePath = JPATH_SITE; + } + + $lang = Factory::getApplication()->getLanguage(); + $lang->load($extension->element, $basePath); + + // Return the localised name + return Text::_(strtoupper($extension->name)); + } + + /** + * Resets all messages for an extension + * + * @param integer $eid The extension ID whose messages we'll reset + * + * @return mixed False if we fail, a db cursor otherwise + * + * @since 3.2 + */ + public function resetMessages($eid) + { + $db = $this->getDatabase(); + $eid = (int) $eid; + + $query = $db->getQuery(true) + ->update($db->quoteName('#__postinstall_messages')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $eid, ParameterType::INTEGER); + $db->setQuery($query); + + $result = $db->execute(); + Factory::getCache()->clean('com_postinstall'); + + return $result; + } + + /** + * Hides all messages for an extension + * + * @param integer $eid The extension ID whose messages we'll hide + * + * @return mixed False if we fail, a db cursor otherwise + * + * @since 3.8.7 + */ + public function hideMessages($eid) + { + $db = $this->getDatabase(); + $eid = (int) $eid; + + $query = $db->getQuery(true) + ->update($db->quoteName('#__postinstall_messages')) + ->set($db->quoteName('enabled') . ' = 0') + ->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $eid, ParameterType::INTEGER); + $db->setQuery($query); + + $result = $db->execute(); + Factory::getCache()->clean('com_postinstall'); + + return $result; + } + + /** + * List post-processing. This is used to run the programmatic display + * conditions against each list item and decide if we have to show it or + * not. + * + * Do note that this a core method of the RAD Layer which operates directly + * on the list it's being fed. A little touch of modern magic. + * + * @param array &$resultArray A list of items to process + * + * @return void + * + * @since 3.2 + */ + protected function onProcessList(&$resultArray) + { + $unset_keys = array(); + $language_extensions = array(); + + // Order the results DESC so the newest is on the top. + $resultArray = array_reverse($resultArray); + + foreach ($resultArray as $key => $item) { + // Filter out messages based on dynamically loaded programmatic conditions. + if (!empty($item->condition_file) && !empty($item->condition_method)) { + $helper = new PostinstallHelper(); + $file = $helper->parsePath($item->condition_file); + + if (File::exists($file)) { + require_once $file; + + $result = call_user_func($item->condition_method); + + if ($result === false) { + $unset_keys[] = $key; + } + } + } + + // Load the necessary language files. + if (!empty($item->language_extension)) { + $hash = $item->language_client_id . '-' . $item->language_extension; + + if (!in_array($hash, $language_extensions)) { + $language_extensions[] = $hash; + Factory::getApplication()->getLanguage()->load($item->language_extension, $item->language_client_id == 0 ? JPATH_SITE : JPATH_ADMINISTRATOR); + } + } + } + + if (!empty($unset_keys)) { + foreach ($unset_keys as $key) { + unset($resultArray[$key]); + } + } + } + + /** + * Get the dropdown options for the list of component with post-installation messages + * + * @since 3.4 + * + * @return array Compatible with JHtmlSelect::genericList + */ + public function getComponentOptions() + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__postinstall_messages')) + ->group($db->quoteName('extension_id')); + $db->setQuery($query); + $extension_ids = $db->loadColumn(); + + $options = array(); + + Factory::getApplication()->getLanguage()->load('files_joomla.sys', JPATH_SITE, null, false, false); + + foreach ($extension_ids as $eid) { + $options[] = HTMLHelper::_('select.option', $eid, $this->getExtensionName($eid)); + } + + return $options; + } + + /** + * Adds or updates a post-installation message (PIM) definition. You can use this in your post-installation script using this code: + * + * require_once JPATH_LIBRARIES . '/fof/include.php'; + * FOFModel::getTmpInstance('Messages', 'PostinstallModel')->addPostInstallationMessage($options); + * + * The $options array contains the following mandatory keys: + * + * extension_id The numeric ID of the extension this message is for (see the #__extensions table) + * + * type One of message, link or action. Their meaning is: + * message Informative message. The user can dismiss it. + * link The action button links to a URL. The URL is defined in the action parameter. + * action A PHP action takes place when the action button is clicked. You need to specify the action_file + * (RAD path to the PHP file) and action (PHP function name) keys. See below for more information. + * + * title_key The Text language key for the title of this PIM. + * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_TITLE + * + * description_key The Text language key for the main body (description) of this PIM + * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_DESCRIPTION + * + * action_key The Text language key for the action button. Ignored and not required when type=message + * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_ACTION + * + * language_extension The extension name which holds the language keys used above. + * For example, com_foobar, mod_something, plg_system_whatever, tpl_mytemplate + * + * language_client_id Should we load the frontend (0) or backend (1) language keys? + * + * version_introduced Which was the version of your extension where this message appeared for the first time? + * Example: 3.2.1 + * + * enabled Must be 1 for this message to be enabled. If you omit it, it defaults to 1. + * + * condition_file The RAD path to a PHP file containing a PHP function which determines whether this message should be shown to + * the user. @see FOFTemplateUtils::parsePath() for RAD path format. Joomla! will include this file before calling + * the condition_method. + * Example: admin://components/com_foobar/helpers/postinstall.php + * + * condition_method The name of a PHP function which will be used to determine whether to show this message to the user. This must be + * a simple PHP user function (not a class method, static method etc) which returns true to show the message and false + * to hide it. This function is defined in the condition_file. + * Example: com_foobar_postinstall_messageone_condition + * + * When type=message no additional keys are required. + * + * When type=link the following additional keys are required: + * + * action The URL which will open when the user clicks on the PIM's action button + * Example: index.php?option=com_foobar&view=tools&task=installSampleData + * + * When type=action the following additional keys are required: + * + * action_file The RAD path to a PHP file containing a PHP function which performs the action of this PIM. @see FOFTemplateUtils::parsePath() + * for RAD path format. Joomla! will include this file before calling the function defined in the action key below. + * Example: admin://components/com_foobar/helpers/postinstall.php + * + * action The name of a PHP function which will be used to run the action of this PIM. This must be a simple PHP user function + * (not a class method, static method etc) which returns no result. + * Example: com_foobar_postinstall_messageone_action + * + * @param array $options See description + * + * @return $this + * + * @throws \Exception + */ + public function addPostInstallationMessage(array $options) + { + // Make sure there are options set + if (!is_array($options)) { + throw new \Exception('Post-installation message definitions must be of type array', 500); + } + + // Initialise array keys + $defaultOptions = array( + 'extension_id' => '', + 'type' => '', + 'title_key' => '', + 'description_key' => '', + 'action_key' => '', + 'language_extension' => '', + 'language_client_id' => '', + 'action_file' => '', + 'action' => '', + 'condition_file' => '', + 'condition_method' => '', + 'version_introduced' => '', + 'enabled' => '1', + ); + + $options = array_merge($defaultOptions, $options); + + // Array normalisation. Removes array keys not belonging to a definition. + $defaultKeys = array_keys($defaultOptions); + $allKeys = array_keys($options); + $extraKeys = array_diff($allKeys, $defaultKeys); + + if (!empty($extraKeys)) { + foreach ($extraKeys as $key) { + unset($options[$key]); + } + } + + // Normalisation of integer values + $options['extension_id'] = (int) $options['extension_id']; + $options['language_client_id'] = (int) $options['language_client_id']; + $options['enabled'] = (int) $options['enabled']; + + // Normalisation of 0/1 values + foreach (array('language_client_id', 'enabled') as $key) { + $options[$key] = $options[$key] ? 1 : 0; + } + + // Make sure there's an extension_id + if (!(int) $options['extension_id']) { + throw new \Exception('Post-installation message definitions need an extension_id', 500); + } + + // Make sure there's a valid type + if (!in_array($options['type'], array('message', 'link', 'action'))) { + throw new \Exception('Post-installation message definitions need to declare a type of message, link or action', 500); + } + + // Make sure there's a title key + if (empty($options['title_key'])) { + throw new \Exception('Post-installation message definitions need a title key', 500); + } + + // Make sure there's a description key + if (empty($options['description_key'])) { + throw new \Exception('Post-installation message definitions need a description key', 500); + } + + // If the type is anything other than message you need an action key + if (($options['type'] != 'message') && empty($options['action_key'])) { + throw new \Exception('Post-installation message definitions need an action key when they are of type "' . $options['type'] . '"', 500); + } + + // You must specify the language extension + if (empty($options['language_extension'])) { + throw new \Exception('Post-installation message definitions need to specify which extension contains their language keys', 500); + } + + // The action file and method are only required for the "action" type + if ($options['type'] == 'action') { + if (empty($options['action_file'])) { + throw new \Exception('Post-installation message definitions need an action file when they are of type "action"', 500); + } + + $helper = new PostinstallHelper(); + $file_path = $helper->parsePath($options['action_file']); + + if (!@is_file($file_path)) { + throw new \Exception('The action file ' . $options['action_file'] . ' of your post-installation message definition does not exist', 500); + } + + if (empty($options['action'])) { + throw new \Exception('Post-installation message definitions need an action (function name) when they are of type "action"', 500); + } + } + + if ($options['type'] == 'link') { + if (empty($options['link'])) { + throw new \Exception('Post-installation message definitions need an action (URL) when they are of type "link"', 500); + } + } + + // The condition file and method are only required when the type is not "message" + if ($options['type'] != 'message') { + if (empty($options['condition_file'])) { + throw new \Exception('Post-installation message definitions need a condition file when they are of type "' . $options['type'] . '"', 500); + } + + $helper = new PostinstallHelper(); + $file_path = $helper->parsePath($options['condition_file']); + + if (!@is_file($file_path)) { + throw new \Exception('The condition file ' . $options['condition_file'] . ' of your post-installation message definition does not exist', 500); + } + + if (empty($options['condition_method'])) { + throw new \Exception( + 'Post-installation message definitions need a condition method (function name) when they are of type "' + . $options['type'] . '"', + 500 + ); + } + } + + // Check if the definition exists + $table = $this->getTable(); + $tableName = $table->getTableName(); + $extensionId = (int) $options['extension_id']; + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName($tableName)) + ->where( + [ + $db->quoteName('extension_id') . ' = :extensionId', + $db->quoteName('type') . ' = :type', + $db->quoteName('title_key') . ' = :titleKey', + ] + ) + ->bind(':extensionId', $extensionId, ParameterType::INTEGER) + ->bind(':type', $options['type']) + ->bind(':titleKey', $options['title_key']); + + $existingRow = $db->setQuery($query)->loadAssoc(); + + // Is the existing definition the same as the one we're trying to save? + if (!empty($existingRow)) { + $same = true; + + foreach ($options as $k => $v) { + if ($existingRow[$k] != $v) { + $same = false; + break; + } + } + + // Trying to add the same row as the existing one; quit + if ($same) { + return $this; + } + + // Otherwise it's not the same row. Remove the old row before insert a new one. + $query = $db->getQuery(true) + ->delete($db->quoteName($tableName)) + ->where( + [ + $db->quoteName('extension_id') . ' = :extensionId', + $db->quoteName('type') . ' = :type', + $db->quoteName('title_key') . ' = :titleKey', + ] + ) + ->bind(':extensionId', $extensionId, ParameterType::INTEGER) + ->bind(':type', $options['type']) + ->bind(':titleKey', $options['title_key']); + + $db->setQuery($query)->execute(); + } + + // Insert the new row + $options = (object) $options; + $db->insertObject($tableName, $options); + Factory::getCache()->clean('com_postinstall'); + + return $this; + } + + /** + * Returns the library extension ID. + * + * @return integer + * + * @since 4.0.0 + */ + public function getJoomlaFilesExtensionId() + { + return ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; + } } diff --git a/administrator/components/com_postinstall/src/View/Messages/HtmlView.php b/administrator/components/com_postinstall/src/View/Messages/HtmlView.php index fb5190ec072b1..831db3097794a 100644 --- a/administrator/components/com_postinstall/src/View/Messages/HtmlView.php +++ b/administrator/components/com_postinstall/src/View/Messages/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - - $this->items = $model->getItems(); - - if (!\count($this->items)) - { - $this->setLayout('emptystate'); - } - - $this->joomlaFilesExtensionId = $model->getJoomlaFilesExtensionId(); - $this->eid = (int) $model->getState('eid', $this->joomlaFilesExtensionId); - - if (empty($this->eid)) - { - $this->eid = $this->joomlaFilesExtensionId; - } - - $this->toolbar(); - - $this->token = Factory::getSession()->getFormToken(); - $this->extension_options = $model->getComponentOptions(); - - ToolbarHelper::title(Text::sprintf('COM_POSTINSTALL_MESSAGES_TITLE', $model->getExtensionName($this->eid)), 'bell'); - - parent::display($tpl); - } - - /** - * displays the toolbar - * - * @return void - * - * @since 3.6 - */ - private function toolbar() - { - $toolbar = Toolbar::getInstance('toolbar'); - - if (!empty($this->items)) - { - $toolbar->unpublish('message.hideAll', 'COM_POSTINSTALL_HIDE_ALL_MESSAGES'); - } - - // Options button. - if ($this->getCurrentUser()->authorise('core.admin', 'com_postinstall')) - { - $toolbar->preferences('com_postinstall'); - $toolbar->help('Post-installation_Messages_for_Joomla_CMS'); - } - } + /** + * Executes before rendering the page for the Browse task. + * + * @param string $tpl Subtemplate to use + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + /** @var MessagesModel $model */ + $model = $this->getModel(); + + $this->items = $model->getItems(); + + if (!\count($this->items)) { + $this->setLayout('emptystate'); + } + + $this->joomlaFilesExtensionId = $model->getJoomlaFilesExtensionId(); + $this->eid = (int) $model->getState('eid', $this->joomlaFilesExtensionId); + + if (empty($this->eid)) { + $this->eid = $this->joomlaFilesExtensionId; + } + + $this->toolbar(); + + $this->token = Factory::getSession()->getFormToken(); + $this->extension_options = $model->getComponentOptions(); + + ToolbarHelper::title(Text::sprintf('COM_POSTINSTALL_MESSAGES_TITLE', $model->getExtensionName($this->eid)), 'bell'); + + parent::display($tpl); + } + + /** + * displays the toolbar + * + * @return void + * + * @since 3.6 + */ + private function toolbar() + { + $toolbar = Toolbar::getInstance('toolbar'); + + if (!empty($this->items)) { + $toolbar->unpublish('message.hideAll', 'COM_POSTINSTALL_HIDE_ALL_MESSAGES'); + } + + // Options button. + if ($this->getCurrentUser()->authorise('core.admin', 'com_postinstall')) { + $toolbar->preferences('com_postinstall'); + $toolbar->help('Post-installation_Messages_for_Joomla_CMS'); + } + } } diff --git a/administrator/components/com_postinstall/tmpl/messages/default.php b/administrator/components/com_postinstall/tmpl/messages/default.php index cb7fd12316a7c..84cf2892442b2 100644 --- a/administrator/components/com_postinstall/tmpl/messages/default.php +++ b/administrator/components/com_postinstall/tmpl/messages/default.php @@ -1,4 +1,5 @@
    - - - - - extension_options, 'eid', array('onchange' => 'this.form.submit()', 'class' => 'form-select'), 'value', 'text', $this->eid, 'eid'); ?> + + + + + extension_options, 'eid', array('onchange' => 'this.form.submit()', 'class' => 'form-select'), 'value', 'text', $this->eid, 'eid'); ?>
    items as $item) : ?> - enabled === 1) : ?> -
    -
    -

    title_key); ?>

    -

    - version_introduced); ?> -

    -
    - description_key); ?> - type !== 'message') : ?> - - action_key); ?> - - - getIdentity()->authorise('core.edit.state', 'com_postinstall')) : ?> - - - - - - - -
    -
    -
    - enabled === 2) : ?> -
    -
    -

    title_key); ?>

    -
    - getIdentity()->authorise('core.edit.state', 'com_postinstall')) : ?> - - - - - - - -
    -
    -
    - + enabled === 1) : ?> +
    +
    +

    title_key); ?>

    +

    + version_introduced); ?> +

    +
    + description_key); ?> + type !== 'message') : ?> + + action_key); ?> + + + getIdentity()->authorise('core.edit.state', 'com_postinstall')) : ?> + + + + + + + +
    +
    +
    + enabled === 2) : ?> +
    +
    +

    title_key); ?>

    +
    + getIdentity()->authorise('core.edit.state', 'com_postinstall')) : ?> + + + + + + + +
    +
    +
    + diff --git a/administrator/components/com_postinstall/tmpl/messages/emptystate.php b/administrator/components/com_postinstall/tmpl/messages/emptystate.php index f58a1d4c0ed00..4c2b43aca30f3 100644 --- a/administrator/components/com_postinstall/tmpl/messages/emptystate.php +++ b/administrator/components/com_postinstall/tmpl/messages/emptystate.php @@ -1,4 +1,5 @@
    - - - - - extension_options, 'eid', array('onchange' => 'this.form.submit()', 'class' => 'form-select'), 'value', 'text', $this->eid, 'eid'); ?> + + + + + extension_options, 'eid', array('onchange' => 'this.form.submit()', 'class' => 'form-select'), 'value', 'text', $this->eid, 'eid'); ?>
    'COM_POSTINSTALL', - 'formURL' => 'index.php?option=com_postinstall', - 'icon' => 'icon-bell', - 'createURL' => 'index.php?option=com_postinstall&view=messages&task=message.reset&eid=' . $this->eid . '&' . $this->token . '=1', + 'textPrefix' => 'COM_POSTINSTALL', + 'formURL' => 'index.php?option=com_postinstall', + 'icon' => 'icon-bell', + 'createURL' => 'index.php?option=com_postinstall&view=messages&task=message.reset&eid=' . $this->eid . '&' . $this->token . '=1', ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_privacy/services/provider.php b/administrator/components/com_privacy/services/provider.php index fc62cfaf46de2..d4c329ecda62c 100644 --- a/administrator/components/com_privacy/services/provider.php +++ b/administrator/components/com_privacy/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Privacy')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Privacy')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Privacy')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Privacy')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Privacy')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Privacy')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new PrivacyComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new PrivacyComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_privacy/src/Controller/ConsentsController.php b/administrator/components/com_privacy/src/Controller/ConsentsController.php index 10976f3d2c2a3..1d5301c13ce26 100644 --- a/administrator/components/com_privacy/src/Controller/ConsentsController.php +++ b/administrator/components/com_privacy/src/Controller/ConsentsController.php @@ -1,4 +1,5 @@ checkToken(); - - $ids = (array) $this->input->get('cid', [], 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), CMSApplication::MSG_ERROR); - } - else - { - /** @var ConsentsModel $model */ - $model = $this->getModel(); - - if (!$model->invalidate($ids)) - { - $this->setMessage($model->getError()); - } - else - { - $this->setMessage(Text::plural('COM_PRIVACY_N_CONSENTS_INVALIDATED', count($ids))); - } - } - - $this->setRedirect(Route::_('index.php?option=com_privacy&view=consents', false)); - } - - /** - * Method to invalidate all consents of a specific subject. - * - * @return void - * - * @since 3.9.0 - */ - public function invalidateAll() - { - // Check for request forgeries - $this->checkToken(); - - $filters = $this->input->get('filter', [], 'array'); - - $this->setRedirect(Route::_('index.php?option=com_privacy&view=consents', false)); - - if (isset($filters['subject']) && $filters['subject'] != '') - { - $subject = $filters['subject']; - } - else - { - $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED')); - - return; - } - - /** @var ConsentsModel $model */ - $model = $this->getModel(); - - if (!$model->invalidateAll($subject)) - { - $this->setMessage($model->getError()); - } - - $this->setMessage(Text::_('COM_PRIVACY_CONSENTS_INVALIDATED_ALL')); - } + /** + * Method to invalidate specific consents. + * + * @return void + * + * @since 3.9.0 + */ + public function invalidate() + { + // Check for request forgeries + $this->checkToken(); + + $ids = (array) $this->input->get('cid', [], 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), CMSApplication::MSG_ERROR); + } else { + /** @var ConsentsModel $model */ + $model = $this->getModel(); + + if (!$model->invalidate($ids)) { + $this->setMessage($model->getError()); + } else { + $this->setMessage(Text::plural('COM_PRIVACY_N_CONSENTS_INVALIDATED', count($ids))); + } + } + + $this->setRedirect(Route::_('index.php?option=com_privacy&view=consents', false)); + } + + /** + * Method to invalidate all consents of a specific subject. + * + * @return void + * + * @since 3.9.0 + */ + public function invalidateAll() + { + // Check for request forgeries + $this->checkToken(); + + $filters = $this->input->get('filter', [], 'array'); + + $this->setRedirect(Route::_('index.php?option=com_privacy&view=consents', false)); + + if (isset($filters['subject']) && $filters['subject'] != '') { + $subject = $filters['subject']; + } else { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED')); + + return; + } + + /** @var ConsentsModel $model */ + $model = $this->getModel(); + + if (!$model->invalidateAll($subject)) { + $this->setMessage($model->getError()); + } + + $this->setMessage(Text::_('COM_PRIVACY_CONSENTS_INVALIDATED_ALL')); + } } diff --git a/administrator/components/com_privacy/src/Controller/DisplayController.php b/administrator/components/com_privacy/src/Controller/DisplayController.php index 33d3fbc8dd4d3..f73258c65b7e1 100644 --- a/administrator/components/com_privacy/src/Controller/DisplayController.php +++ b/administrator/components/com_privacy/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', $this->default_view); - $vFormat = $document->getType(); - $lName = $this->input->get('layout', 'default', 'string'); - - // Get and render the view. - if ($view = $this->getView($vName, $vFormat)) - { - $model = $this->getModel($vName); - $view->setModel($model, true); - - if ($vName === 'request') - { - // For the default layout, we need to also push the action logs model into the view - if ($lName === 'default') - { - $logsModel = $this->app->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlogs', 'Administrator', ['ignore_request' => true]); - - // Set default ordering for the context - $logsModel->setState('list.fullordering', 'a.log_date DESC'); - - // And push the model into the view - $view->setModel($logsModel, false); - } - - // For the edit layout, if mail sending is disabled then redirect back to the list view as the form is unusable in this state - if ($lName === 'edit' && !$this->app->get('mailonline', 1)) - { - $this->setRedirect( - Route::_('index.php?option=com_privacy&view=requests', false), - Text::_('COM_PRIVACY_WARNING_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED'), - 'warning' - ); - - return $this; - } - } - - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - - $view->display(); - } - - return $this; - } - - /** - * Fetch and report number urgent privacy requests in JSON format, for AJAX requests - * - * @return void - * - * @since 3.9.0 - */ - public function getNumberUrgentRequests() - { - // Check for a valid token. If invalid, send a 403 with the error message. - if (!Session::checkToken('get')) - { - $this->app->setHeader('status', 403, true); - $this->app->sendHeaders(); - echo new JsonResponse(new \Exception(Text::_('JINVALID_TOKEN'), 403)); - $this->app->close(); - } - - /** @var RequestsModel $model */ - $model = $this->getModel('requests'); - $numberUrgentRequests = $model->getNumberUrgentRequests(); - - echo new JsonResponse(['number_urgent_requests' => $numberUrgentRequests]); - } + /** + * The default view. + * + * @var string + * @since 3.9.0 + */ + protected $default_view = 'requests'; + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return $this + * + * @since 3.9.0 + */ + public function display($cachable = false, $urlparams = []) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', $this->default_view); + $vFormat = $document->getType(); + $lName = $this->input->get('layout', 'default', 'string'); + + // Get and render the view. + if ($view = $this->getView($vName, $vFormat)) { + $model = $this->getModel($vName); + $view->setModel($model, true); + + if ($vName === 'request') { + // For the default layout, we need to also push the action logs model into the view + if ($lName === 'default') { + $logsModel = $this->app->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlogs', 'Administrator', ['ignore_request' => true]); + + // Set default ordering for the context + $logsModel->setState('list.fullordering', 'a.log_date DESC'); + + // And push the model into the view + $view->setModel($logsModel, false); + } + + // For the edit layout, if mail sending is disabled then redirect back to the list view as the form is unusable in this state + if ($lName === 'edit' && !$this->app->get('mailonline', 1)) { + $this->setRedirect( + Route::_('index.php?option=com_privacy&view=requests', false), + Text::_('COM_PRIVACY_WARNING_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED'), + 'warning' + ); + + return $this; + } + } + + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + + $view->display(); + } + + return $this; + } + + /** + * Fetch and report number urgent privacy requests in JSON format, for AJAX requests + * + * @return void + * + * @since 3.9.0 + */ + public function getNumberUrgentRequests() + { + // Check for a valid token. If invalid, send a 403 with the error message. + if (!Session::checkToken('get')) { + $this->app->setHeader('status', 403, true); + $this->app->sendHeaders(); + echo new JsonResponse(new \Exception(Text::_('JINVALID_TOKEN'), 403)); + $this->app->close(); + } + + /** @var RequestsModel $model */ + $model = $this->getModel('requests'); + $numberUrgentRequests = $model->getNumberUrgentRequests(); + + echo new JsonResponse(['number_urgent_requests' => $numberUrgentRequests]); + } } diff --git a/administrator/components/com_privacy/src/Controller/RequestController.php b/administrator/components/com_privacy/src/Controller/RequestController.php index 2f4c55c3ec1fb..67544806b2cbf 100644 --- a/administrator/components/com_privacy/src/Controller/RequestController.php +++ b/administrator/components/com_privacy/src/Controller/RequestController.php @@ -1,4 +1,5 @@ checkToken(); - - /** @var RequestModel $model */ - $model = $this->getModel(); - - /** @var RequestTable $table */ - $table = $model->getTable(); - - // Determine the name of the primary key for the data. - if (empty($key)) - { - $key = $table->getKeyName(); - } - - // To avoid data collisions the urlVar may be different from the primary key. - if (empty($urlVar)) - { - $urlVar = $key; - } - - $recordId = $this->input->getInt($urlVar); - - $item = $model->getItem($recordId); - - // Ensure this record can transition to the requested state - if (!$this->canTransition($item, '2')) - { - $this->setMessage(Text::_('COM_PRIVACY_ERROR_COMPLETE_TRANSITION_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Build the data array for the update - $data = [ - $key => $recordId, - 'status' => '2', - ]; - - // Access check. - if (!$this->allowSave($data, $key)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Attempt to save the data. - if (!$model->save($data)) - { - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Log the request completed - $model->logRequestCompleted($recordId); - - $this->setMessage(Text::_('COM_PRIVACY_REQUEST_COMPLETED')); - - $url = 'index.php?option=com_privacy&view=requests'; - - // Check if there is a return value - $return = $this->input->get('return', null, 'base64'); - - if (!is_null($return) && Uri::isInternal(base64_decode($return))) - { - $url = base64_decode($return); - } - - // Redirect to the list screen. - $this->setRedirect(Route::_($url, false)); - - return true; - } - - /** - * Method to email the data export for a request. - * - * @return boolean - * - * @since 3.9.0 - */ - public function emailexport() - { - // Check for request forgeries. - $this->checkToken('get'); - - /** @var ExportModel $model */ - $model = $this->getModel('Export'); - - $recordId = $this->input->getUint('id'); - - if (!$model->emailDataExport($recordId)) - { - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('COM_PRIVACY_ERROR_EXPORT_EMAIL_FAILED', $model->getError()), 'error'); - } - else - { - $this->setMessage(Text::_('COM_PRIVACY_EXPORT_EMAILED')); - } - - $url = 'index.php?option=com_privacy&view=requests'; - - // Check if there is a return value - $return = $this->input->get('return', null, 'base64'); - - if (!is_null($return) && Uri::isInternal(base64_decode($return))) - { - $url = base64_decode($return); - } - - // Redirect to the list screen. - $this->setRedirect(Route::_($url, false)); - - return true; - } - - /** - * Method to export the data for a request. - * - * @return $this - * - * @since 3.9.0 - */ - public function export() - { - $this->input->set('view', 'export'); - - return $this->display(); - } - - /** - * Method to invalidate a request. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean - * - * @since 3.9.0 - */ - public function invalidate($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - /** @var RequestModel $model */ - $model = $this->getModel(); - - /** @var RequestTable $table */ - $table = $model->getTable(); - - // Determine the name of the primary key for the data. - if (empty($key)) - { - $key = $table->getKeyName(); - } - - // To avoid data collisions the urlVar may be different from the primary key. - if (empty($urlVar)) - { - $urlVar = $key; - } - - $recordId = $this->input->getInt($urlVar); - - $item = $model->getItem($recordId); - - // Ensure this record can transition to the requested state - if (!$this->canTransition($item, '-1')) - { - $this->setMessage(Text::_('COM_PRIVACY_ERROR_INVALID_TRANSITION_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Build the data array for the update - $data = [ - $key => $recordId, - 'status' => '-1', - ]; - - // Access check. - if (!$this->allowSave($data, $key)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Attempt to save the data. - if (!$model->save($data)) - { - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Log the request invalidated - $model->logRequestInvalidated($recordId); - - $this->setMessage(Text::_('COM_PRIVACY_REQUEST_INVALIDATED')); - - $url = 'index.php?option=com_privacy&view=requests'; - - // Check if there is a return value - $return = $this->input->get('return', null, 'base64'); - - if (!is_null($return) && Uri::isInternal(base64_decode($return))) - { - $url = base64_decode($return); - } - - // Redirect to the list screen. - $this->setRedirect(Route::_($url, false)); - - return true; - } - - /** - * Method to remove the user data for a privacy remove request. - * - * @return boolean - * - * @since 3.9.0 - */ - public function remove() - { - // Check for request forgeries. - $this->checkToken('request'); - - /** @var RemoveModel $model */ - $model = $this->getModel('Remove'); - - $recordId = $this->input->getUint('id'); - - if (!$model->removeDataForRequest($recordId)) - { - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('COM_PRIVACY_ERROR_REMOVE_DATA_FAILED', $model->getError()), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - $this->setMessage(Text::_('COM_PRIVACY_DATA_REMOVED')); - - $url = 'index.php?option=com_privacy&view=requests'; - - // Check if there is a return value - $return = $this->input->get('return', null, 'base64'); - - if (!is_null($return) && Uri::isInternal(base64_decode($return))) - { - $url = base64_decode($return); - } - - // Redirect to the list screen. - $this->setRedirect(Route::_($url, false)); - - return true; - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 3.9.0 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = []) - { - // This hook only processes new items - if (!$model->getState($model->getName() . '.new', false)) - { - return; - } - - if (!$model->logRequestCreated($model->getState($model->getName() . '.id'))) - { - if ($error = $model->getError()) - { - $this->app->enqueueMessage($error, 'warning'); - } - } - - if (!$model->notifyUserAdminCreatedRequest($model->getState($model->getName() . '.id'))) - { - if ($error = $model->getError()) - { - $this->app->enqueueMessage($error, 'warning'); - } - } - else - { - $this->app->enqueueMessage(Text::_('COM_PRIVACY_MSG_CONFIRM_EMAIL_SENT_TO_USER')); - } - } - - /** - * Method to determine if an item can transition to the specified status. - * - * @param object $item The item being updated. - * @param string $newStatus The new status of the item. - * - * @return boolean - * - * @since 3.9.0 - */ - private function canTransition($item, $newStatus) - { - switch ($item->status) - { - case '0': - // A pending item can only move to invalid through this controller due to the requirement for a user to confirm the request - return $newStatus === '-1'; - - case '1': - // A confirmed item can be marked completed or invalid - return in_array($newStatus, ['-1', '2'], true); - - // An item which is already in an invalid or complete state cannot transition, likewise if we don't know the state don't change anything - case '-1': - case '2': - default: - return false; - } - } + /** + * Method to complete a request. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean + * + * @since 3.9.0 + */ + public function complete($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + /** @var RequestModel $model */ + $model = $this->getModel(); + + /** @var RequestTable $table */ + $table = $model->getTable(); + + // Determine the name of the primary key for the data. + if (empty($key)) { + $key = $table->getKeyName(); + } + + // To avoid data collisions the urlVar may be different from the primary key. + if (empty($urlVar)) { + $urlVar = $key; + } + + $recordId = $this->input->getInt($urlVar); + + $item = $model->getItem($recordId); + + // Ensure this record can transition to the requested state + if (!$this->canTransition($item, '2')) { + $this->setMessage(Text::_('COM_PRIVACY_ERROR_COMPLETE_TRANSITION_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Build the data array for the update + $data = [ + $key => $recordId, + 'status' => '2', + ]; + + // Access check. + if (!$this->allowSave($data, $key)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Attempt to save the data. + if (!$model->save($data)) { + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Log the request completed + $model->logRequestCompleted($recordId); + + $this->setMessage(Text::_('COM_PRIVACY_REQUEST_COMPLETED')); + + $url = 'index.php?option=com_privacy&view=requests'; + + // Check if there is a return value + $return = $this->input->get('return', null, 'base64'); + + if (!is_null($return) && Uri::isInternal(base64_decode($return))) { + $url = base64_decode($return); + } + + // Redirect to the list screen. + $this->setRedirect(Route::_($url, false)); + + return true; + } + + /** + * Method to email the data export for a request. + * + * @return boolean + * + * @since 3.9.0 + */ + public function emailexport() + { + // Check for request forgeries. + $this->checkToken('get'); + + /** @var ExportModel $model */ + $model = $this->getModel('Export'); + + $recordId = $this->input->getUint('id'); + + if (!$model->emailDataExport($recordId)) { + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('COM_PRIVACY_ERROR_EXPORT_EMAIL_FAILED', $model->getError()), 'error'); + } else { + $this->setMessage(Text::_('COM_PRIVACY_EXPORT_EMAILED')); + } + + $url = 'index.php?option=com_privacy&view=requests'; + + // Check if there is a return value + $return = $this->input->get('return', null, 'base64'); + + if (!is_null($return) && Uri::isInternal(base64_decode($return))) { + $url = base64_decode($return); + } + + // Redirect to the list screen. + $this->setRedirect(Route::_($url, false)); + + return true; + } + + /** + * Method to export the data for a request. + * + * @return $this + * + * @since 3.9.0 + */ + public function export() + { + $this->input->set('view', 'export'); + + return $this->display(); + } + + /** + * Method to invalidate a request. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean + * + * @since 3.9.0 + */ + public function invalidate($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + /** @var RequestModel $model */ + $model = $this->getModel(); + + /** @var RequestTable $table */ + $table = $model->getTable(); + + // Determine the name of the primary key for the data. + if (empty($key)) { + $key = $table->getKeyName(); + } + + // To avoid data collisions the urlVar may be different from the primary key. + if (empty($urlVar)) { + $urlVar = $key; + } + + $recordId = $this->input->getInt($urlVar); + + $item = $model->getItem($recordId); + + // Ensure this record can transition to the requested state + if (!$this->canTransition($item, '-1')) { + $this->setMessage(Text::_('COM_PRIVACY_ERROR_INVALID_TRANSITION_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Build the data array for the update + $data = [ + $key => $recordId, + 'status' => '-1', + ]; + + // Access check. + if (!$this->allowSave($data, $key)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Attempt to save the data. + if (!$model->save($data)) { + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Log the request invalidated + $model->logRequestInvalidated($recordId); + + $this->setMessage(Text::_('COM_PRIVACY_REQUEST_INVALIDATED')); + + $url = 'index.php?option=com_privacy&view=requests'; + + // Check if there is a return value + $return = $this->input->get('return', null, 'base64'); + + if (!is_null($return) && Uri::isInternal(base64_decode($return))) { + $url = base64_decode($return); + } + + // Redirect to the list screen. + $this->setRedirect(Route::_($url, false)); + + return true; + } + + /** + * Method to remove the user data for a privacy remove request. + * + * @return boolean + * + * @since 3.9.0 + */ + public function remove() + { + // Check for request forgeries. + $this->checkToken('request'); + + /** @var RemoveModel $model */ + $model = $this->getModel('Remove'); + + $recordId = $this->input->getUint('id'); + + if (!$model->removeDataForRequest($recordId)) { + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('COM_PRIVACY_ERROR_REMOVE_DATA_FAILED', $model->getError()), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + $this->setMessage(Text::_('COM_PRIVACY_DATA_REMOVED')); + + $url = 'index.php?option=com_privacy&view=requests'; + + // Check if there is a return value + $return = $this->input->get('return', null, 'base64'); + + if (!is_null($return) && Uri::isInternal(base64_decode($return))) { + $url = base64_decode($return); + } + + // Redirect to the list screen. + $this->setRedirect(Route::_($url, false)); + + return true; + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 3.9.0 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = []) + { + // This hook only processes new items + if (!$model->getState($model->getName() . '.new', false)) { + return; + } + + if (!$model->logRequestCreated($model->getState($model->getName() . '.id'))) { + if ($error = $model->getError()) { + $this->app->enqueueMessage($error, 'warning'); + } + } + + if (!$model->notifyUserAdminCreatedRequest($model->getState($model->getName() . '.id'))) { + if ($error = $model->getError()) { + $this->app->enqueueMessage($error, 'warning'); + } + } else { + $this->app->enqueueMessage(Text::_('COM_PRIVACY_MSG_CONFIRM_EMAIL_SENT_TO_USER')); + } + } + + /** + * Method to determine if an item can transition to the specified status. + * + * @param object $item The item being updated. + * @param string $newStatus The new status of the item. + * + * @return boolean + * + * @since 3.9.0 + */ + private function canTransition($item, $newStatus) + { + switch ($item->status) { + case '0': + // A pending item can only move to invalid through this controller due to the requirement for a user to confirm the request + return $newStatus === '-1'; + + case '1': + // A confirmed item can be marked completed or invalid + return in_array($newStatus, ['-1', '2'], true); + + // An item which is already in an invalid or complete state cannot transition, likewise if we don't know the state don't change anything + case '-1': + case '2': + default: + return false; + } + } } diff --git a/administrator/components/com_privacy/src/Controller/RequestsController.php b/administrator/components/com_privacy/src/Controller/RequestsController.php index 4b70cfab10129..04d05f016220b 100644 --- a/administrator/components/com_privacy/src/Controller/RequestsController.php +++ b/administrator/components/com_privacy/src/Controller/RequestsController.php @@ -1,4 +1,5 @@ true]) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return BaseDatabaseModel|boolean Model object on success; otherwise false on failure. + * + * @since 3.9.0 + */ + public function getModel($name = 'Request', $prefix = 'Administrator', $config = ['ignore_request' => true]) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_privacy/src/Dispatcher/Dispatcher.php b/administrator/components/com_privacy/src/Dispatcher/Dispatcher.php index ea1e2921de28d..25fd84a2963b4 100644 --- a/administrator/components/com_privacy/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_privacy/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + /** + * Method to check component access permission + * + * @return void + */ + protected function checkAccess() + { + // Check the user has permission to access this component if in the backend + if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_privacy/src/Export/Domain.php b/administrator/components/com_privacy/src/Export/Domain.php index 5ed379483007e..b3f2af814858d 100644 --- a/administrator/components/com_privacy/src/Export/Domain.php +++ b/administrator/components/com_privacy/src/Export/Domain.php @@ -1,4 +1,5 @@ items[] = $item; - } + /** + * Add an item to the domain + * + * @param Item $item The item to add + * + * @return void + * + * @since 3.9.0 + */ + public function addItem(Item $item) + { + $this->items[] = $item; + } - /** - * Get the domain's items - * - * @return Item[] - * - * @since 3.9.0 - */ - public function getItems() - { - return $this->items; - } + /** + * Get the domain's items + * + * @return Item[] + * + * @since 3.9.0 + */ + public function getItems() + { + return $this->items; + } } diff --git a/administrator/components/com_privacy/src/Export/Field.php b/administrator/components/com_privacy/src/Export/Field.php index 81c94dec78783..bc160104b0a5b 100644 --- a/administrator/components/com_privacy/src/Export/Field.php +++ b/administrator/components/com_privacy/src/Export/Field.php @@ -1,4 +1,5 @@ fields[] = $field; - } + /** + * Add a field to the item + * + * @param Field $field The field to add + * + * @return void + * + * @since 3.9.0 + */ + public function addField(Field $field) + { + $this->fields[] = $field; + } - /** - * Get the item's fields - * - * @return Field[] - * - * @since 3.9.0 - */ - public function getFields() - { - return $this->fields; - } + /** + * Get the item's fields + * + * @return Field[] + * + * @since 3.9.0 + */ + public function getFields() + { + return $this->fields; + } } diff --git a/administrator/components/com_privacy/src/Extension/PrivacyComponent.php b/administrator/components/com_privacy/src/Extension/PrivacyComponent.php index d270d19b49cc0..fbdef2c0bfaf7 100644 --- a/administrator/components/com_privacy/src/Extension/PrivacyComponent.php +++ b/administrator/components/com_privacy/src/Extension/PrivacyComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('privacy', new Privacy); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('privacy', new Privacy()); + } } diff --git a/administrator/components/com_privacy/src/Field/RequeststatusField.php b/administrator/components/com_privacy/src/Field/RequeststatusField.php index 56bb22bdeb6f1..b77ecba9c5e8e 100644 --- a/administrator/components/com_privacy/src/Field/RequeststatusField.php +++ b/administrator/components/com_privacy/src/Field/RequeststatusField.php @@ -1,4 +1,5 @@ 'COM_PRIVACY_STATUS_INVALID', - '0' => 'COM_PRIVACY_STATUS_PENDING', - '1' => 'COM_PRIVACY_STATUS_CONFIRMED', - '2' => 'COM_PRIVACY_STATUS_COMPLETED', - ]; + /** + * Available statuses + * + * @var array + * @since 3.9.0 + */ + protected $predefinedOptions = [ + '-1' => 'COM_PRIVACY_STATUS_INVALID', + '0' => 'COM_PRIVACY_STATUS_PENDING', + '1' => 'COM_PRIVACY_STATUS_CONFIRMED', + '2' => 'COM_PRIVACY_STATUS_COMPLETED', + ]; } diff --git a/administrator/components/com_privacy/src/Field/RequesttypeField.php b/administrator/components/com_privacy/src/Field/RequesttypeField.php index 9d0cbacf65acc..2da4242d13b27 100644 --- a/administrator/components/com_privacy/src/Field/RequesttypeField.php +++ b/administrator/components/com_privacy/src/Field/RequesttypeField.php @@ -1,4 +1,5 @@ 'COM_PRIVACY_HEADING_REQUEST_TYPE_TYPE_EXPORT', - 'remove' => 'COM_PRIVACY_HEADING_REQUEST_TYPE_TYPE_REMOVE', - ]; + /** + * Available types + * + * @var array + * @since 3.9.0 + */ + protected $predefinedOptions = [ + 'export' => 'COM_PRIVACY_HEADING_REQUEST_TYPE_TYPE_EXPORT', + 'remove' => 'COM_PRIVACY_HEADING_REQUEST_TYPE_TYPE_REMOVE', + ]; } diff --git a/administrator/components/com_privacy/src/Helper/PrivacyHelper.php b/administrator/components/com_privacy/src/Helper/PrivacyHelper.php index a8c0f9fabaadc..bd5f13b7d2e4e 100644 --- a/administrator/components/com_privacy/src/Helper/PrivacyHelper.php +++ b/administrator/components/com_privacy/src/Helper/PrivacyHelper.php @@ -1,4 +1,5 @@ '); + /** + * Render the data request as a XML document. + * + * @param Domain[] $exportData The data to be exported. + * + * @return string + * + * @since 3.9.0 + */ + public static function renderDataAsXml(array $exportData) + { + $export = new \SimpleXMLElement(''); - foreach ($exportData as $domain) - { - $xmlDomain = $export->addChild('domain'); - $xmlDomain->addAttribute('name', $domain->name); - $xmlDomain->addAttribute('description', $domain->description); + foreach ($exportData as $domain) { + $xmlDomain = $export->addChild('domain'); + $xmlDomain->addAttribute('name', $domain->name); + $xmlDomain->addAttribute('description', $domain->description); - foreach ($domain->getItems() as $item) - { - $xmlItem = $xmlDomain->addChild('item'); + foreach ($domain->getItems() as $item) { + $xmlItem = $xmlDomain->addChild('item'); - if ($item->id) - { - $xmlItem->addAttribute('id', $item->id); - } + if ($item->id) { + $xmlItem->addAttribute('id', $item->id); + } - foreach ($item->getFields() as $field) - { - $xmlItem->{$field->name} = $field->value; - } - } - } + foreach ($item->getFields() as $field) { + $xmlItem->{$field->name} = $field->value; + } + } + } - $dom = new \DOMDocument; - $dom->loadXML($export->asXML()); - $dom->formatOutput = true; + $dom = new \DOMDocument(); + $dom->loadXML($export->asXML()); + $dom->formatOutput = true; - return $dom->saveXML(); - } + return $dom->saveXML(); + } - /** - * Gets the privacyconsent system plugin extension id. - * - * @return integer The privacyconsent system plugin extension id. - * - * @since 3.9.2 - */ - public static function getPrivacyConsentPluginId() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('privacyconsent')); + /** + * Gets the privacyconsent system plugin extension id. + * + * @return integer The privacyconsent system plugin extension id. + * + * @since 3.9.2 + */ + public static function getPrivacyConsentPluginId() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('privacyconsent')); - $db->setQuery($query); + $db->setQuery($query); - return (int) $db->loadResult(); - } + return (int) $db->loadResult(); + } } diff --git a/administrator/components/com_privacy/src/Model/CapabilitiesModel.php b/administrator/components/com_privacy/src/Model/CapabilitiesModel.php index 98296574a4751..7936a52deba15 100644 --- a/administrator/components/com_privacy/src/Model/CapabilitiesModel.php +++ b/administrator/components/com_privacy/src/Model/CapabilitiesModel.php @@ -1,4 +1,5 @@ [ - Text::_('COM_PRIVACY_CORE_CAPABILITY_SESSION_IP_ADDRESS_AND_COOKIE'), - Text::sprintf('COM_PRIVACY_CORE_CAPABILITY_LOGGING_IP_ADDRESS', $app->get('log_path', JPATH_ADMINISTRATOR . '/logs')), - Text::_('COM_PRIVACY_CORE_CAPABILITY_COMMUNICATION_WITH_JOOMLA_ORG'), - ], - ]; + $coreCapabilities = [ + Text::_('COM_PRIVACY_HEADING_CORE_CAPABILITIES') => [ + Text::_('COM_PRIVACY_CORE_CAPABILITY_SESSION_IP_ADDRESS_AND_COOKIE'), + Text::sprintf('COM_PRIVACY_CORE_CAPABILITY_LOGGING_IP_ADDRESS', $app->get('log_path', JPATH_ADMINISTRATOR . '/logs')), + Text::_('COM_PRIVACY_CORE_CAPABILITY_COMMUNICATION_WITH_JOOMLA_ORG'), + ], + ]; - /* - * We will search for capabilities from the following plugin groups: - * - * - Authentication: These plugins by design process user information and may have capabilities such as creating cookies - * - Captcha: These plugins may communicate information to third party systems - * - Installer: These plugins can add additional install capabilities to the Extension Manager, such as the Install from Web service - * - Privacy: These plugins are the primary integration point into this component - * - User: These plugins are intended to extend the user management system - * - * This is in addition to plugin groups which are imported before this method is triggered, generally this is the system group. - */ + /* + * We will search for capabilities from the following plugin groups: + * + * - Authentication: These plugins by design process user information and may have capabilities such as creating cookies + * - Captcha: These plugins may communicate information to third party systems + * - Installer: These plugins can add additional install capabilities to the Extension Manager, such as the Install from Web service + * - Privacy: These plugins are the primary integration point into this component + * - User: These plugins are intended to extend the user management system + * + * This is in addition to plugin groups which are imported before this method is triggered, generally this is the system group. + */ - PluginHelper::importPlugin('authentication'); - PluginHelper::importPlugin('captcha'); - PluginHelper::importPlugin('installer'); - PluginHelper::importPlugin('privacy'); - PluginHelper::importPlugin('user'); + PluginHelper::importPlugin('authentication'); + PluginHelper::importPlugin('captcha'); + PluginHelper::importPlugin('installer'); + PluginHelper::importPlugin('privacy'); + PluginHelper::importPlugin('user'); - $pluginResults = $app->triggerEvent('onPrivacyCollectAdminCapabilities'); + $pluginResults = $app->triggerEvent('onPrivacyCollectAdminCapabilities'); - // We are going to "cheat" here and include this component's capabilities without using a plugin - $extensionCapabilities = [ - Text::_('COM_PRIVACY') => [ - Text::_('COM_PRIVACY_EXTENSION_CAPABILITY_PERSONAL_INFO'), - ], - ]; + // We are going to "cheat" here and include this component's capabilities without using a plugin + $extensionCapabilities = [ + Text::_('COM_PRIVACY') => [ + Text::_('COM_PRIVACY_EXTENSION_CAPABILITY_PERSONAL_INFO'), + ], + ]; - foreach ($pluginResults as $pluginResult) - { - $extensionCapabilities += $pluginResult; - } + foreach ($pluginResults as $pluginResult) { + $extensionCapabilities += $pluginResult; + } - // Sort the extension list alphabetically - ksort($extensionCapabilities); + // Sort the extension list alphabetically + ksort($extensionCapabilities); - // Always prepend the core capabilities to the array - return $coreCapabilities + $extensionCapabilities; - } + // Always prepend the core capabilities to the array + return $coreCapabilities + $extensionCapabilities; + } - /** - * Method to auto-populate the model state. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_privacy')); - } + /** + * Method to auto-populate the model state. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_privacy')); + } } diff --git a/administrator/components/com_privacy/src/Model/ConsentsModel.php b/administrator/components/com_privacy/src/Model/ConsentsModel.php index b50b1992fea73..e77c45125ff26 100644 --- a/administrator/components/com_privacy/src/Model/ConsentsModel.php +++ b/administrator/components/com_privacy/src/Model/ConsentsModel.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select($this->getState('list.select', 'a.*')); - $query->from($db->quoteName('#__privacy_consents', 'a')); - - // Join over the users for the username and name. - $query->select($db->quoteName('u.username', 'username')) - ->select($db->quoteName('u.name', 'name')); - $query->join('LEFT', $db->quoteName('#__users', 'u') . ' ON u.id = a.user_id'); - - // Filter by search in email - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $ids, ParameterType::INTEGER); - } - elseif (stripos($search, 'uid:') === 0) - { - $uid = (int) substr($search, 4); - $query->where($db->quoteName('a.user_id') . ' = :uid') - ->bind(':uid', $uid, ParameterType::INTEGER); - } - elseif (stripos($search, 'name:') === 0) - { - $search = '%' . substr($search, 5) . '%'; - $query->where($db->quoteName('u.name') . ' LIKE :search') - ->bind(':search', $search); - } - else - { - $search = '%' . $search . '%'; - $query->where('(' . $db->quoteName('u.username') . ' LIKE :search)') - ->bind(':search', $search); - } - } - - $state = $this->getState('filter.state'); - - if ($state != '') - { - $state = (int) $state; - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - - // Handle the list ordering. - $ordering = $this->getState('list.ordering'); - $direction = $this->getState('list.direction'); - - if (!empty($ordering)) - { - $query->order($db->escape($ordering) . ' ' . $db->escape($direction)); - } - - return $query; - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string - * - * @since 3.9.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState($ordering = 'a.id', $direction = 'desc') - { - // Load the filter state. - $this->setState( - 'filter.search', - $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search') - ); - - $this->setState( - 'filter.subject', - $this->getUserStateFromRequest($this->context . '.filter.subject', 'filter_subject') - ); - - $this->setState( - 'filter.state', - $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state') - ); - - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_privacy')); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to invalidate specific consents. - * - * @param array $pks The ids of the consents to invalidate. - * - * @return boolean True on success. - */ - public function invalidate($pks) - { - // Sanitize the ids. - $pks = (array) $pks; - $pks = ArrayHelper::toInteger($pks); - - try - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->update($db->quoteName('#__privacy_consents')) - ->set($db->quoteName('state') . ' = -1') - ->whereIn($db->quoteName('id'), $pks) - ->where($db->quoteName('state') . ' = 1'); - $db->setQuery($query); - $db->execute(); - } - catch (ExecutionFailureException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return true; - } - - /** - * Method to invalidate a group of specific consents. - * - * @param array $subject The subject of the consents to invalidate. - * - * @return boolean True on success. - */ - public function invalidateAll($subject) - { - try - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->update($db->quoteName('#__privacy_consents')) - ->set($db->quoteName('state') . ' = -1') - ->where($db->quoteName('subject') . ' = :subject') - ->where($db->quoteName('state') . ' = 1') - ->bind(':subject', $subject); - $db->setQuery($query); - $db->execute(); - } - catch (ExecutionFailureException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return true; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 3.9.0 + */ + public function __construct($config = []) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = [ + 'id', 'a.id', + 'user_id', 'a.user_id', + 'subject', 'a.subject', + 'created', 'a.created', + 'username', 'u.username', + 'name', 'u.name', + 'state', 'a.state', + ]; + } + + parent::__construct($config); + } + + /** + * Method to get a DatabaseQuery object for retrieving the data set from a database. + * + * @return DatabaseQuery + * + * @since 3.9.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select($this->getState('list.select', 'a.*')); + $query->from($db->quoteName('#__privacy_consents', 'a')); + + // Join over the users for the username and name. + $query->select($db->quoteName('u.username', 'username')) + ->select($db->quoteName('u.name', 'name')); + $query->join('LEFT', $db->quoteName('#__users', 'u') . ' ON u.id = a.user_id'); + + // Filter by search in email + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $ids, ParameterType::INTEGER); + } elseif (stripos($search, 'uid:') === 0) { + $uid = (int) substr($search, 4); + $query->where($db->quoteName('a.user_id') . ' = :uid') + ->bind(':uid', $uid, ParameterType::INTEGER); + } elseif (stripos($search, 'name:') === 0) { + $search = '%' . substr($search, 5) . '%'; + $query->where($db->quoteName('u.name') . ' LIKE :search') + ->bind(':search', $search); + } else { + $search = '%' . $search . '%'; + $query->where('(' . $db->quoteName('u.username') . ' LIKE :search)') + ->bind(':search', $search); + } + } + + $state = $this->getState('filter.state'); + + if ($state != '') { + $state = (int) $state; + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } + + // Handle the list ordering. + $ordering = $this->getState('list.ordering'); + $direction = $this->getState('list.direction'); + + if (!empty($ordering)) { + $query->order($db->escape($ordering) . ' ' . $db->escape($direction)); + } + + return $query; + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string + * + * @since 3.9.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState($ordering = 'a.id', $direction = 'desc') + { + // Load the filter state. + $this->setState( + 'filter.search', + $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search') + ); + + $this->setState( + 'filter.subject', + $this->getUserStateFromRequest($this->context . '.filter.subject', 'filter_subject') + ); + + $this->setState( + 'filter.state', + $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state') + ); + + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_privacy')); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to invalidate specific consents. + * + * @param array $pks The ids of the consents to invalidate. + * + * @return boolean True on success. + */ + public function invalidate($pks) + { + // Sanitize the ids. + $pks = (array) $pks; + $pks = ArrayHelper::toInteger($pks); + + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__privacy_consents')) + ->set($db->quoteName('state') . ' = -1') + ->whereIn($db->quoteName('id'), $pks) + ->where($db->quoteName('state') . ' = 1'); + $db->setQuery($query); + $db->execute(); + } catch (ExecutionFailureException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return true; + } + + /** + * Method to invalidate a group of specific consents. + * + * @param array $subject The subject of the consents to invalidate. + * + * @return boolean True on success. + */ + public function invalidateAll($subject) + { + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__privacy_consents')) + ->set($db->quoteName('state') . ' = -1') + ->where($db->quoteName('subject') . ' = :subject') + ->where($db->quoteName('state') . ' = 1') + ->bind(':subject', $subject); + $db->setQuery($query); + $db->execute(); + } catch (ExecutionFailureException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return true; + } } diff --git a/administrator/components/com_privacy/src/Model/ExportModel.php b/administrator/components/com_privacy/src/Model/ExportModel.php index b190bcdf3162e..716b60f2d4a78 100644 --- a/administrator/components/com_privacy/src/Model/ExportModel.php +++ b/administrator/components/com_privacy/src/Model/ExportModel.php @@ -1,4 +1,5 @@ getState($this->getName() . '.request_id'); - - if (!$id) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_EXPORT')); - - return false; - } - - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); + /** + * Create the export document for an information request. + * + * @param integer $id The request ID to process + * + * @return Domain[]|boolean A SimpleXMLElement object for a successful export or boolean false on an error + * + * @since 3.9.0 + */ + public function collectDataForExportRequest($id = null) + { + $id = !empty($id) ? $id : (int) $this->getState($this->getName() . '.request_id'); + + if (!$id) { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_EXPORT')); + + return false; + } + + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + if ($table->request_type !== 'export') { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_EXPORT')); + + return false; + } + + if ($table->status != 1) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_EXPORT_UNCONFIRMED_REQUEST')); + + return false; + } + + // If there is a user account associated with the email address, load it here for use in the plugins + $db = $this->getDatabase(); + + $userId = (int) $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $table->email) + ->setLimit(1) + )->loadResult(); + + $user = $userId ? User::getInstance($userId) : null; + + // Log the export + $this->logExport($table); + + PluginHelper::importPlugin('privacy'); + + $pluginResults = Factory::getApplication()->triggerEvent('onPrivacyExportRequest', [$table, $user]); + + $domains = []; + + foreach ($pluginResults as $pluginDomains) { + $domains = array_merge($domains, $pluginDomains); + } + + return $domains; + } + + /** + * Email the data export to the user. + * + * @param integer $id The request ID to process + * + * @return boolean + * + * @since 3.9.0 + */ + public function emailDataExport($id = null) + { + $id = !empty($id) ? $id : (int) $this->getState($this->getName() . '.request_id'); + + if (!$id) { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_EXPORT')); + + return false; + } + + $exportData = $this->collectDataForExportRequest($id); + + if ($exportData === false) { + // Error is already set, we just need to bail + return false; + } + + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + if ($table->request_type !== 'export') { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_EXPORT')); - return false; - } + return false; + } - if ($table->request_type !== 'export') - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_EXPORT')); - - return false; - } - - if ($table->status != 1) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_EXPORT_UNCONFIRMED_REQUEST')); - - return false; - } - - // If there is a user account associated with the email address, load it here for use in the plugins - $db = $this->getDatabase(); - - $userId = (int) $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $table->email) - ->setLimit(1) - )->loadResult(); - - $user = $userId ? User::getInstance($userId) : null; - - // Log the export - $this->logExport($table); - - PluginHelper::importPlugin('privacy'); - - $pluginResults = Factory::getApplication()->triggerEvent('onPrivacyExportRequest', [$table, $user]); - - $domains = []; - - foreach ($pluginResults as $pluginDomains) - { - $domains = array_merge($domains, $pluginDomains); - } - - return $domains; - } - - /** - * Email the data export to the user. - * - * @param integer $id The request ID to process - * - * @return boolean - * - * @since 3.9.0 - */ - public function emailDataExport($id = null) - { - $id = !empty($id) ? $id : (int) $this->getState($this->getName() . '.request_id'); - - if (!$id) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_EXPORT')); - - return false; - } - - $exportData = $this->collectDataForExportRequest($id); - - if ($exportData === false) - { - // Error is already set, we just need to bail - return false; - } - - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - if ($table->request_type !== 'export') - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_EXPORT')); - - return false; - } - - if ($table->status != 1) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_EXPORT_UNCONFIRMED_REQUEST')); - - return false; - } - - // Log the email - $this->logExportEmailed($table); - - /* - * If there is an associated user account, we will attempt to send this email in the user's preferred language. - * Because of this, it is expected that Language::_() is directly called and that the Text class is NOT used - * for translating all messages. - * - * Error messages will still be displayed to the administrator, so those messages should continue to use the Text class. - */ - - $lang = Factory::getLanguage(); - - $db = $this->getDatabase(); - - $userId = (int) $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $table->email), - 0, - 1 - )->loadResult(); - - if ($userId) - { - $receiver = User::getInstance($userId); - - /* - * We don't know if the user has admin access, so we will check if they have an admin language in their parameters, - * falling back to the site language, falling back to the currently active language - */ - - $langCode = $receiver->getParam('admin_language', ''); - - if (!$langCode) - { - $langCode = $receiver->getParam('language', $lang->getTag()); - } - - $lang = Language::getInstance($langCode, $lang->getDebug()); - } - - // Ensure the right language files have been loaded - $lang->load('com_privacy', JPATH_ADMINISTRATOR) - || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); - - // The mailer can be set to either throw Exceptions or return boolean false, account for both - try - { - $app = Factory::getApplication(); - $mailer = new MailTemplate('com_privacy.userdataexport', $app->getLanguage()->getTag()); - - $templateData = [ - 'sitename' => $app->get('sitename'), - 'url' => Uri::root(), - ]; - - $mailer->addRecipient($table->email); - $mailer->addTemplateData($templateData); - $mailer->addAttachment('user-data_' . Uri::getInstance()->toString(['host']) . '.xml', PrivacyHelper::renderDataAsXml($exportData)); - - if ($mailer->send() === false) - { - $this->setError($mailer->ErrorInfo); - - return false; - } - - return true; - } - catch (phpmailerException $exception) - { - $this->setError($exception->getMessage()); - - return false; - } - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @throws \Exception - * @since 3.9.0 - */ - public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Log the data export to the action log system. - * - * @param RequestTable $request The request record being processed - * - * @return void - * - * @since 3.9.0 - */ - public function logExport(RequestTable $request) - { - $user = Factory::getUser(); - - $message = [ - 'action' => 'export', - 'id' => $request->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_EXPORT', 'com_privacy.request', $user->id); - } - - /** - * Log the data export email to the action log system. - * - * @param RequestTable $request The request record being processed - * - * @return void - * - * @since 3.9.0 - */ - public function logExportEmailed(RequestTable $request) - { - $user = Factory::getUser(); - - $message = [ - 'action' => 'export_emailed', - 'id' => $request->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_EXPORT_EMAILED', 'com_privacy.request', $user->id); - } - - /** - * Method to auto-populate the model state. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Get the pk of the record from the request. - $this->setState($this->getName() . '.request_id', Factory::getApplication()->input->getUint('id')); - - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_privacy')); - } - - /** - * Method to fetch an instance of the action log model. - * - * @return ActionlogModel - * - * @since 4.0.0 - */ - private function getActionlogModel(): ActionlogModel - { - return Factory::getApplication()->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - } + if ($table->status != 1) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_EXPORT_UNCONFIRMED_REQUEST')); + + return false; + } + + // Log the email + $this->logExportEmailed($table); + + /* + * If there is an associated user account, we will attempt to send this email in the user's preferred language. + * Because of this, it is expected that Language::_() is directly called and that the Text class is NOT used + * for translating all messages. + * + * Error messages will still be displayed to the administrator, so those messages should continue to use the Text class. + */ + + $lang = Factory::getLanguage(); + + $db = $this->getDatabase(); + + $userId = (int) $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $table->email), + 0, + 1 + )->loadResult(); + + if ($userId) { + $receiver = User::getInstance($userId); + + /* + * We don't know if the user has admin access, so we will check if they have an admin language in their parameters, + * falling back to the site language, falling back to the currently active language + */ + + $langCode = $receiver->getParam('admin_language', ''); + + if (!$langCode) { + $langCode = $receiver->getParam('language', $lang->getTag()); + } + + $lang = Language::getInstance($langCode, $lang->getDebug()); + } + + // Ensure the right language files have been loaded + $lang->load('com_privacy', JPATH_ADMINISTRATOR) + || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); + + // The mailer can be set to either throw Exceptions or return boolean false, account for both + try { + $app = Factory::getApplication(); + $mailer = new MailTemplate('com_privacy.userdataexport', $app->getLanguage()->getTag()); + + $templateData = [ + 'sitename' => $app->get('sitename'), + 'url' => Uri::root(), + ]; + + $mailer->addRecipient($table->email); + $mailer->addTemplateData($templateData); + $mailer->addAttachment('user-data_' . Uri::getInstance()->toString(['host']) . '.xml', PrivacyHelper::renderDataAsXml($exportData)); + + if ($mailer->send() === false) { + $this->setError($mailer->ErrorInfo); + + return false; + } + + return true; + } catch (phpmailerException $exception) { + $this->setError($exception->getMessage()); + + return false; + } + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @throws \Exception + * @since 3.9.0 + */ + public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Log the data export to the action log system. + * + * @param RequestTable $request The request record being processed + * + * @return void + * + * @since 3.9.0 + */ + public function logExport(RequestTable $request) + { + $user = Factory::getUser(); + + $message = [ + 'action' => 'export', + 'id' => $request->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_EXPORT', 'com_privacy.request', $user->id); + } + + /** + * Log the data export email to the action log system. + * + * @param RequestTable $request The request record being processed + * + * @return void + * + * @since 3.9.0 + */ + public function logExportEmailed(RequestTable $request) + { + $user = Factory::getUser(); + + $message = [ + 'action' => 'export_emailed', + 'id' => $request->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_EXPORT_EMAILED', 'com_privacy.request', $user->id); + } + + /** + * Method to auto-populate the model state. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Get the pk of the record from the request. + $this->setState($this->getName() . '.request_id', Factory::getApplication()->input->getUint('id')); + + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_privacy')); + } + + /** + * Method to fetch an instance of the action log model. + * + * @return ActionlogModel + * + * @since 4.0.0 + */ + private function getActionlogModel(): ActionlogModel + { + return Factory::getApplication()->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + } } diff --git a/administrator/components/com_privacy/src/Model/RemoveModel.php b/administrator/components/com_privacy/src/Model/RemoveModel.php index ffaff9a83d628..29f0990a09db4 100644 --- a/administrator/components/com_privacy/src/Model/RemoveModel.php +++ b/administrator/components/com_privacy/src/Model/RemoveModel.php @@ -1,4 +1,5 @@ getState($this->getName() . '.request_id'); - - if (!$id) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_REMOVE')); - - return false; - } - - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - if ($table->request_type !== 'remove') - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_REMOVE')); - - return false; - } - - if ($table->status != 1) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_REMOVE_UNCONFIRMED_REQUEST')); - - return false; - } - - // If there is a user account associated with the email address, load it here for use in the plugins - $db = $this->getDatabase(); - - $userId = (int) $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $table->email) - ->setLimit(1) - )->loadResult(); - - $user = $userId ? User::getInstance($userId) : null; - - $canRemove = true; - - PluginHelper::importPlugin('privacy'); - - /** @var Status[] $pluginResults */ - $pluginResults = Factory::getApplication()->triggerEvent('onPrivacyCanRemoveData', [$table, $user]); - - foreach ($pluginResults as $status) - { - if (!$status->canRemove) - { - $this->setError($status->reason ?: Text::_('COM_PRIVACY_ERROR_CANNOT_REMOVE_DATA')); - - $canRemove = false; - } - } - - if (!$canRemove) - { - $this->logRemoveBlocked($table, $this->getErrors()); - - return false; - } - - // Log the removal - $this->logRemove($table); - - Factory::getApplication()->triggerEvent('onPrivacyRemoveData', [$table, $user]); - - return true; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @throws \Exception - * @since 3.9.0 - */ - public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Log the data removal to the action log system. - * - * @param RequestTable $request The request record being processed - * - * @return void - * - * @since 3.9.0 - */ - public function logRemove(RequestTable $request) - { - $user = Factory::getUser(); - - $message = [ - 'action' => 'remove', - 'id' => $request->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_REMOVE', 'com_privacy.request', $user->id); - } - - /** - * Log the data removal being blocked to the action log system. - * - * @param RequestTable $request The request record being processed - * @param string[] $reasons The reasons given why the record could not be removed. - * - * @return void - * - * @since 3.9.0 - */ - public function logRemoveBlocked(RequestTable $request, array $reasons) - { - $user = Factory::getUser(); - - $message = [ - 'action' => 'remove-blocked', - 'id' => $request->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - 'reasons' => implode('; ', $reasons), - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_REMOVE_BLOCKED', 'com_privacy.request', $user->id); - } - - /** - * Method to auto-populate the model state. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Get the pk of the record from the request. - $this->setState($this->getName() . '.request_id', Factory::getApplication()->input->getUint('id')); - - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_privacy')); - } - - /** - * Method to fetch an instance of the action log model. - * - * @return ActionlogModel - * - * @since 4.0.0 - */ - private function getActionlogModel(): ActionlogModel - { - return Factory::getApplication()->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - } + /** + * Remove the user data. + * + * @param integer $id The request ID to process + * + * @return boolean + * + * @since 3.9.0 + */ + public function removeDataForRequest($id = null) + { + $id = !empty($id) ? $id : (int) $this->getState($this->getName() . '.request_id'); + + if (!$id) { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_REMOVE')); + + return false; + } + + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + if ($table->request_type !== 'remove') { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_REMOVE')); + + return false; + } + + if ($table->status != 1) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_REMOVE_UNCONFIRMED_REQUEST')); + + return false; + } + + // If there is a user account associated with the email address, load it here for use in the plugins + $db = $this->getDatabase(); + + $userId = (int) $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $table->email) + ->setLimit(1) + )->loadResult(); + + $user = $userId ? User::getInstance($userId) : null; + + $canRemove = true; + + PluginHelper::importPlugin('privacy'); + + /** @var Status[] $pluginResults */ + $pluginResults = Factory::getApplication()->triggerEvent('onPrivacyCanRemoveData', [$table, $user]); + + foreach ($pluginResults as $status) { + if (!$status->canRemove) { + $this->setError($status->reason ?: Text::_('COM_PRIVACY_ERROR_CANNOT_REMOVE_DATA')); + + $canRemove = false; + } + } + + if (!$canRemove) { + $this->logRemoveBlocked($table, $this->getErrors()); + + return false; + } + + // Log the removal + $this->logRemove($table); + + Factory::getApplication()->triggerEvent('onPrivacyRemoveData', [$table, $user]); + + return true; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @throws \Exception + * @since 3.9.0 + */ + public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Log the data removal to the action log system. + * + * @param RequestTable $request The request record being processed + * + * @return void + * + * @since 3.9.0 + */ + public function logRemove(RequestTable $request) + { + $user = Factory::getUser(); + + $message = [ + 'action' => 'remove', + 'id' => $request->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_REMOVE', 'com_privacy.request', $user->id); + } + + /** + * Log the data removal being blocked to the action log system. + * + * @param RequestTable $request The request record being processed + * @param string[] $reasons The reasons given why the record could not be removed. + * + * @return void + * + * @since 3.9.0 + */ + public function logRemoveBlocked(RequestTable $request, array $reasons) + { + $user = Factory::getUser(); + + $message = [ + 'action' => 'remove-blocked', + 'id' => $request->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + 'reasons' => implode('; ', $reasons), + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_REMOVE_BLOCKED', 'com_privacy.request', $user->id); + } + + /** + * Method to auto-populate the model state. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Get the pk of the record from the request. + $this->setState($this->getName() . '.request_id', Factory::getApplication()->input->getUint('id')); + + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_privacy')); + } + + /** + * Method to fetch an instance of the action log model. + * + * @return ActionlogModel + * + * @since 4.0.0 + */ + private function getActionlogModel(): ActionlogModel + { + return Factory::getApplication()->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + } } diff --git a/administrator/components/com_privacy/src/Model/RequestModel.php b/administrator/components/com_privacy/src/Model/RequestModel.php index 6dde5737a1912..c1c1e4eb23b11 100644 --- a/administrator/components/com_privacy/src/Model/RequestModel.php +++ b/administrator/components/com_privacy/src/Model/RequestModel.php @@ -1,4 +1,5 @@ loadForm('com_privacy.request', 'request', ['control' => 'jform', 'load_data' => $loadData]); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @throws \Exception - * @since 3.9.0 - */ - public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 3.9.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_privacy.edit.request.data', []); - - if (empty($data)) - { - $data = $this->getItem(); - } - - return $data; - } - - /** - * Log the completion of a request to the action log system. - * - * @param integer $id The ID of the request to process. - * - * @return boolean - * - * @since 3.9.0 - */ - public function logRequestCompleted($id) - { - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - $user = Factory::getUser(); - - $message = [ - 'action' => 'request-completed', - 'requesttype' => $table->request_type, - 'subjectemail' => $table->email, - 'id' => $table->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_COMPLETED_REQUEST', 'com_privacy.request', $user->id); - - return true; - } - - /** - * Log the creation of a request to the action log system. - * - * @param integer $id The ID of the request to process. - * - * @return boolean - * - * @since 3.9.0 - */ - public function logRequestCreated($id) - { - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - $user = Factory::getUser(); - - $message = [ - 'action' => 'request-created', - 'requesttype' => $table->request_type, - 'subjectemail' => $table->email, - 'id' => $table->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_CREATED_REQUEST', 'com_privacy.request', $user->id); - - return true; - } - - /** - * Log the invalidation of a request to the action log system. - * - * @param integer $id The ID of the request to process. - * - * @return boolean - * - * @since 3.9.0 - */ - public function logRequestInvalidated($id) - { - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - $user = Factory::getUser(); - - $message = [ - 'action' => 'request-invalidated', - 'requesttype' => $table->request_type, - 'subjectemail' => $table->email, - 'id' => $table->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_INVALIDATED_REQUEST', 'com_privacy.request', $user->id); - - return true; - } - - /** - * Notifies the user that an information request has been created by a site administrator. - * - * Because confirmation tokens are stored in the database as a hashed value, this method will generate a new confirmation token - * for the request. - * - * @param integer $id The ID of the request to process. - * - * @return boolean - * - * @since 3.9.0 - */ - public function notifyUserAdminCreatedRequest($id) - { - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - /* - * If there is an associated user account, we will attempt to send this email in the user's preferred language. - * Because of this, it is expected that Language::_() is directly called and that the Text class is NOT used - * for translating all messages. - * - * Error messages will still be displayed to the administrator, so those messages should continue to use the Text class. - */ - - $lang = Factory::getLanguage(); - - $db = $this->getDatabase(); - - $userId = (int) $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $table->email) - ->setLimit(1) - )->loadResult(); - - if ($userId) - { - $receiver = User::getInstance($userId); - - /* - * We don't know if the user has admin access, so we will check if they have an admin language in their parameters, - * falling back to the site language, falling back to the currently active language - */ - - $langCode = $receiver->getParam('admin_language', ''); - - if (!$langCode) - { - $langCode = $receiver->getParam('language', $lang->getTag()); - } - - $lang = Language::getInstance($langCode, $lang->getDebug()); - } - - // Ensure the right language files have been loaded - $lang->load('com_privacy', JPATH_ADMINISTRATOR) - || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); - - // Regenerate the confirmation token - $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); - $hashedToken = UserHelper::hashPassword($token); - - $table->confirm_token = $hashedToken; - $table->confirm_token_created_at = Factory::getDate()->toSql(); - - try - { - $table->store(); - } - catch (ExecutionFailureException $exception) - { - $this->setError($exception->getMessage()); - - return false; - } - - // The mailer can be set to either throw Exceptions or return boolean false, account for both - try - { - $app = Factory::getApplication(); - - $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - $templateData = [ - 'sitename' => $app->get('sitename'), - 'url' => Uri::root(), - 'tokenurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm&confirm_token=' . $token, false, $linkMode, true), - 'formurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm', false, $linkMode, true), - 'token' => $token, - ]; - - switch ($table->request_type) - { - case 'export': - $mailer = new MailTemplate('com_privacy.notification.admin.export', $app->getLanguage()->getTag()); - - break; - - case 'remove': - $mailer = new MailTemplate('com_privacy.notification.admin.remove', $app->getLanguage()->getTag()); - - break; - - default: - $this->setError(Text::_('COM_PRIVACY_ERROR_UNKNOWN_REQUEST_TYPE')); - - return false; - } - - $mailer->addTemplateData($templateData); - $mailer->addRecipient($table->email); - - $mailer->send(); - - return true; - } - catch (MailDisabledException | phpmailerException $exception) - { - $this->setError($exception->getMessage()); - - return false; - } - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success, False on error. - * - * @since 3.9.0 - */ - public function save($data) - { - $table = $this->getTable(); - $key = $table->getKeyName(); - $pk = !empty($data[$key]) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); - - if (!$pk && !Factory::getApplication()->get('mailonline', 1)) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED')); - - return false; - } - - return parent::save($data); - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see \Joomla\CMS\Form\FormRule - * @see JFilterInput - * @since 3.9.0 - */ - public function validate($form, $data, $group = null) - { - $validatedData = parent::validate($form, $data, $group); - - // If parent validation failed there's no point in doing our extended validation - if ($validatedData === false) - { - return false; - } - - // Make sure the status is always 0 - $validatedData['status'] = 0; - - // The user cannot create a request for their own account - if (strtolower(Factory::getUser()->email) === strtolower($validatedData['email'])) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_FOR_SELF')); - - return false; - } - - // Check for an active request for this email address - $db = $this->getDatabase(); - - $query = $db->getQuery(true) - ->select('COUNT(id)') - ->from($db->quoteName('#__privacy_requests')) - ->where($db->quoteName('email') . ' = :email') - ->where($db->quoteName('request_type') . ' = :requesttype') - ->whereIn($db->quoteName('status'), [0, 1]) - ->bind(':email', $validatedData['email']) - ->bind(':requesttype', $validatedData['request_type']); - - $activeRequestCount = (int) $db->setQuery($query)->loadResult(); - - if ($activeRequestCount > 0) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_ACTIVE_REQUEST_FOR_EMAIL')); - - return false; - } - - return $validatedData; - } - - /** - * Method to fetch an instance of the action log model. - * - * @return ActionlogModel - * - * @since 4.0.0 - */ - private function getActionlogModel(): ActionlogModel - { - return Factory::getApplication()->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - } + /** + * Clean the cache + * + * @param string $group The cache group + * + * @return void + * + * @since 3.9.0 + */ + protected function cleanCache($group = 'com_privacy') + { + parent::cleanCache('com_privacy'); + } + + /** + * Method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 3.9.0 + */ + public function getForm($data = [], $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_privacy.request', 'request', ['control' => 'jform', 'load_data' => $loadData]); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @throws \Exception + * @since 3.9.0 + */ + public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 3.9.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_privacy.edit.request.data', []); + + if (empty($data)) { + $data = $this->getItem(); + } + + return $data; + } + + /** + * Log the completion of a request to the action log system. + * + * @param integer $id The ID of the request to process. + * + * @return boolean + * + * @since 3.9.0 + */ + public function logRequestCompleted($id) + { + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + $user = Factory::getUser(); + + $message = [ + 'action' => 'request-completed', + 'requesttype' => $table->request_type, + 'subjectemail' => $table->email, + 'id' => $table->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_COMPLETED_REQUEST', 'com_privacy.request', $user->id); + + return true; + } + + /** + * Log the creation of a request to the action log system. + * + * @param integer $id The ID of the request to process. + * + * @return boolean + * + * @since 3.9.0 + */ + public function logRequestCreated($id) + { + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + $user = Factory::getUser(); + + $message = [ + 'action' => 'request-created', + 'requesttype' => $table->request_type, + 'subjectemail' => $table->email, + 'id' => $table->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_CREATED_REQUEST', 'com_privacy.request', $user->id); + + return true; + } + + /** + * Log the invalidation of a request to the action log system. + * + * @param integer $id The ID of the request to process. + * + * @return boolean + * + * @since 3.9.0 + */ + public function logRequestInvalidated($id) + { + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + $user = Factory::getUser(); + + $message = [ + 'action' => 'request-invalidated', + 'requesttype' => $table->request_type, + 'subjectemail' => $table->email, + 'id' => $table->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_INVALIDATED_REQUEST', 'com_privacy.request', $user->id); + + return true; + } + + /** + * Notifies the user that an information request has been created by a site administrator. + * + * Because confirmation tokens are stored in the database as a hashed value, this method will generate a new confirmation token + * for the request. + * + * @param integer $id The ID of the request to process. + * + * @return boolean + * + * @since 3.9.0 + */ + public function notifyUserAdminCreatedRequest($id) + { + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + /* + * If there is an associated user account, we will attempt to send this email in the user's preferred language. + * Because of this, it is expected that Language::_() is directly called and that the Text class is NOT used + * for translating all messages. + * + * Error messages will still be displayed to the administrator, so those messages should continue to use the Text class. + */ + + $lang = Factory::getLanguage(); + + $db = $this->getDatabase(); + + $userId = (int) $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $table->email) + ->setLimit(1) + )->loadResult(); + + if ($userId) { + $receiver = User::getInstance($userId); + + /* + * We don't know if the user has admin access, so we will check if they have an admin language in their parameters, + * falling back to the site language, falling back to the currently active language + */ + + $langCode = $receiver->getParam('admin_language', ''); + + if (!$langCode) { + $langCode = $receiver->getParam('language', $lang->getTag()); + } + + $lang = Language::getInstance($langCode, $lang->getDebug()); + } + + // Ensure the right language files have been loaded + $lang->load('com_privacy', JPATH_ADMINISTRATOR) + || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); + + // Regenerate the confirmation token + $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); + $hashedToken = UserHelper::hashPassword($token); + + $table->confirm_token = $hashedToken; + $table->confirm_token_created_at = Factory::getDate()->toSql(); + + try { + $table->store(); + } catch (ExecutionFailureException $exception) { + $this->setError($exception->getMessage()); + + return false; + } + + // The mailer can be set to either throw Exceptions or return boolean false, account for both + try { + $app = Factory::getApplication(); + + $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + $templateData = [ + 'sitename' => $app->get('sitename'), + 'url' => Uri::root(), + 'tokenurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm&confirm_token=' . $token, false, $linkMode, true), + 'formurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm', false, $linkMode, true), + 'token' => $token, + ]; + + switch ($table->request_type) { + case 'export': + $mailer = new MailTemplate('com_privacy.notification.admin.export', $app->getLanguage()->getTag()); + + break; + + case 'remove': + $mailer = new MailTemplate('com_privacy.notification.admin.remove', $app->getLanguage()->getTag()); + + break; + + default: + $this->setError(Text::_('COM_PRIVACY_ERROR_UNKNOWN_REQUEST_TYPE')); + + return false; + } + + $mailer->addTemplateData($templateData); + $mailer->addRecipient($table->email); + + $mailer->send(); + + return true; + } catch (MailDisabledException | phpmailerException $exception) { + $this->setError($exception->getMessage()); + + return false; + } + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success, False on error. + * + * @since 3.9.0 + */ + public function save($data) + { + $table = $this->getTable(); + $key = $table->getKeyName(); + $pk = !empty($data[$key]) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); + + if (!$pk && !Factory::getApplication()->get('mailonline', 1)) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED')); + + return false; + } + + return parent::save($data); + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see \Joomla\CMS\Form\FormRule + * @see JFilterInput + * @since 3.9.0 + */ + public function validate($form, $data, $group = null) + { + $validatedData = parent::validate($form, $data, $group); + + // If parent validation failed there's no point in doing our extended validation + if ($validatedData === false) { + return false; + } + + // Make sure the status is always 0 + $validatedData['status'] = 0; + + // The user cannot create a request for their own account + if (strtolower(Factory::getUser()->email) === strtolower($validatedData['email'])) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_FOR_SELF')); + + return false; + } + + // Check for an active request for this email address + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('COUNT(id)') + ->from($db->quoteName('#__privacy_requests')) + ->where($db->quoteName('email') . ' = :email') + ->where($db->quoteName('request_type') . ' = :requesttype') + ->whereIn($db->quoteName('status'), [0, 1]) + ->bind(':email', $validatedData['email']) + ->bind(':requesttype', $validatedData['request_type']); + + $activeRequestCount = (int) $db->setQuery($query)->loadResult(); + + if ($activeRequestCount > 0) { + $this->setError(Text::_('COM_PRIVACY_ERROR_ACTIVE_REQUEST_FOR_EMAIL')); + + return false; + } + + return $validatedData; + } + + /** + * Method to fetch an instance of the action log model. + * + * @return ActionlogModel + * + * @since 4.0.0 + */ + private function getActionlogModel(): ActionlogModel + { + return Factory::getApplication()->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + } } diff --git a/administrator/components/com_privacy/src/Model/RequestsModel.php b/administrator/components/com_privacy/src/Model/RequestsModel.php index c7d542d66437d..af4b3dce59406 100644 --- a/administrator/components/com_privacy/src/Model/RequestsModel.php +++ b/administrator/components/com_privacy/src/Model/RequestsModel.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select($this->getState('list.select', 'a.*')); - $query->from($db->quoteName('#__privacy_requests', 'a')); - - // Filter by status - $status = $this->getState('filter.status'); - - if (is_numeric($status)) - { - $status = (int) $status; - $query->where($db->quoteName('a.status') . ' = :status') - ->bind(':status', $status, ParameterType::INTEGER); - } - - // Filter by request type - $requestType = $this->getState('filter.request_type', ''); - - if ($requestType) - { - $query->where($db->quoteName('a.request_type') . ' = :requesttype') - ->bind(':requesttype', $requestType); - } - - // Filter by search in email - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . $search . '%'; - $query->where('(' . $db->quoteName('a.email') . ' LIKE :search)') - ->bind(':search', $search); - } - } - - // Handle the list ordering. - $ordering = $this->getState('list.ordering'); - $direction = $this->getState('list.direction'); - - if (!empty($ordering)) - { - $query->order($db->escape($ordering) . ' ' . $db->escape($direction)); - } - - return $query; - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string - * - * @since 3.9.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.status'); - $id .= ':' . $this->getState('filter.request_type'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState($ordering = 'a.id', $direction = 'desc') - { - // Load the filter state. - $this->setState( - 'filter.search', - $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search') - ); - - $this->setState( - 'filter.status', - $this->getUserStateFromRequest($this->context . '.filter.status', 'filter_status', '', 'int') - ); - - $this->setState( - 'filter.request_type', - $this->getUserStateFromRequest($this->context . '.filter.request_type', 'filter_request_type', '', 'string') - ); - - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_privacy')); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to return number privacy requests older than X days. - * - * @return integer - * - * @since 3.9.0 - */ - public function getNumberUrgentRequests() - { - // Load the parameters. - $params = ComponentHelper::getComponent('com_privacy')->getParams(); - $notify = (int) $params->get('notify', 14); - $now = Factory::getDate()->toSql(); - $period = '-' . $notify; - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('COUNT(*)'); - $query->from($db->quoteName('#__privacy_requests')); - $query->where($db->quoteName('status') . ' = 1 '); - $query->where($query->dateAdd($db->quote($now), $period, 'DAY') . ' > ' . $db->quoteName('requested_at')); - $db->setQuery($query); - - return (int) $db->loadResult(); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 3.9.0 + */ + public function __construct($config = []) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = [ + 'id', 'a.id', + 'email', 'a.email', + 'requested_at', 'a.requested_at', + 'request_type', 'a.request_type', + 'status', 'a.status', + ]; + } + + parent::__construct($config); + } + + /** + * Method to get a DatabaseQuery object for retrieving the data set from a database. + * + * @return DatabaseQuery + * + * @since 3.9.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select($this->getState('list.select', 'a.*')); + $query->from($db->quoteName('#__privacy_requests', 'a')); + + // Filter by status + $status = $this->getState('filter.status'); + + if (is_numeric($status)) { + $status = (int) $status; + $query->where($db->quoteName('a.status') . ' = :status') + ->bind(':status', $status, ParameterType::INTEGER); + } + + // Filter by request type + $requestType = $this->getState('filter.request_type', ''); + + if ($requestType) { + $query->where($db->quoteName('a.request_type') . ' = :requesttype') + ->bind(':requesttype', $requestType); + } + + // Filter by search in email + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . $search . '%'; + $query->where('(' . $db->quoteName('a.email') . ' LIKE :search)') + ->bind(':search', $search); + } + } + + // Handle the list ordering. + $ordering = $this->getState('list.ordering'); + $direction = $this->getState('list.direction'); + + if (!empty($ordering)) { + $query->order($db->escape($ordering) . ' ' . $db->escape($direction)); + } + + return $query; + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string + * + * @since 3.9.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.status'); + $id .= ':' . $this->getState('filter.request_type'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState($ordering = 'a.id', $direction = 'desc') + { + // Load the filter state. + $this->setState( + 'filter.search', + $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search') + ); + + $this->setState( + 'filter.status', + $this->getUserStateFromRequest($this->context . '.filter.status', 'filter_status', '', 'int') + ); + + $this->setState( + 'filter.request_type', + $this->getUserStateFromRequest($this->context . '.filter.request_type', 'filter_request_type', '', 'string') + ); + + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_privacy')); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to return number privacy requests older than X days. + * + * @return integer + * + * @since 3.9.0 + */ + public function getNumberUrgentRequests() + { + // Load the parameters. + $params = ComponentHelper::getComponent('com_privacy')->getParams(); + $notify = (int) $params->get('notify', 14); + $now = Factory::getDate()->toSql(); + $period = '-' . $notify; + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('COUNT(*)'); + $query->from($db->quoteName('#__privacy_requests')); + $query->where($db->quoteName('status') . ' = 1 '); + $query->where($query->dateAdd($db->quote($now), $period, 'DAY') . ' > ' . $db->quoteName('requested_at')); + $db->setQuery($query); + + return (int) $db->loadResult(); + } } diff --git a/administrator/components/com_privacy/src/Plugin/PrivacyPlugin.php b/administrator/components/com_privacy/src/Plugin/PrivacyPlugin.php index 0679a47148290..06245129e9f95 100644 --- a/administrator/components/com_privacy/src/Plugin/PrivacyPlugin.php +++ b/administrator/components/com_privacy/src/Plugin/PrivacyPlugin.php @@ -1,4 +1,5 @@ name = $name; - $domain->description = $description; - - return $domain; - } - - /** - * Create an item object for an array - * - * @param array $data The array data to convert - * @param integer|null $itemId The ID of this item - * - * @return Item - * - * @since 3.9.0 - */ - protected function createItemFromArray(array $data, $itemId = null) - { - $item = new Item; - $item->id = $itemId; - - foreach ($data as $key => $value) - { - if (is_object($value)) - { - $value = (array) $value; - } - - if (is_array($value)) - { - $value = print_r($value, true); - } - - $field = new Field; - $field->name = $key; - $field->value = $value; - - $item->addField($field); - } - - return $item; - } - - /** - * Create an item object for a Table object - * - * @param Table $table The Table object to convert - * - * @return Item - * - * @since 3.9.0 - */ - protected function createItemForTable($table) - { - $data = []; - - foreach (array_keys($table->getFields()) as $fieldName) - { - $data[$fieldName] = $table->$fieldName; - } - - return $this->createItemFromArray($data, $table->{$table->getKeyName(false)}); - } - - /** - * Helper function to create the domain for the items custom fields. - * - * @param string $context The context - * @param array $items The items - * - * @return Domain - * - * @since 3.9.0 - */ - protected function createCustomFieldsDomain($context, $items = array()) - { - if (!is_array($items)) - { - $items = [$items]; - } - - $parts = FieldsHelper::extract($context); - - if (!$parts) - { - return []; - } - - $type = str_replace('com_', '', $parts[0]); - - $domain = $this->createDomain($type . '_' . $parts[1] . '_custom_fields', 'joomla_' . $type . '_' . $parts[1] . '_custom_fields_data'); - - foreach ($items as $item) - { - // Get item's fields, also preparing their value property for manual display - $fields = FieldsHelper::getFields($parts[0] . '.' . $parts[1], $item); - - foreach ($fields as $field) - { - $fieldValue = is_array($field->value) ? implode(', ', $field->value) : $field->value; - - $data = [ - $type . '_id' => $item->id, - 'field_name' => $field->name, - 'field_title' => $field->title, - 'field_value' => $fieldValue, - ]; - - $domain->addItem($this->createItemFromArray($data)); - } - } - - return $domain; - } + /** + * Database object + * + * @var \Joomla\Database\DatabaseDriver + * @since 3.9.0 + */ + protected $db; + + /** + * Affects constructor behaviour. If true, language files will be loaded automatically. + * + * @var boolean + * @since 3.9.0 + */ + protected $autoloadLanguage = true; + + /** + * Create a new domain object + * + * @param string $name The domain's name + * @param string $description The domain's description + * + * @return Domain + * + * @since 3.9.0 + */ + protected function createDomain($name, $description = '') + { + $domain = new Domain(); + $domain->name = $name; + $domain->description = $description; + + return $domain; + } + + /** + * Create an item object for an array + * + * @param array $data The array data to convert + * @param integer|null $itemId The ID of this item + * + * @return Item + * + * @since 3.9.0 + */ + protected function createItemFromArray(array $data, $itemId = null) + { + $item = new Item(); + $item->id = $itemId; + + foreach ($data as $key => $value) { + if (is_object($value)) { + $value = (array) $value; + } + + if (is_array($value)) { + $value = print_r($value, true); + } + + $field = new Field(); + $field->name = $key; + $field->value = $value; + + $item->addField($field); + } + + return $item; + } + + /** + * Create an item object for a Table object + * + * @param Table $table The Table object to convert + * + * @return Item + * + * @since 3.9.0 + */ + protected function createItemForTable($table) + { + $data = []; + + foreach (array_keys($table->getFields()) as $fieldName) { + $data[$fieldName] = $table->$fieldName; + } + + return $this->createItemFromArray($data, $table->{$table->getKeyName(false)}); + } + + /** + * Helper function to create the domain for the items custom fields. + * + * @param string $context The context + * @param array $items The items + * + * @return Domain + * + * @since 3.9.0 + */ + protected function createCustomFieldsDomain($context, $items = array()) + { + if (!is_array($items)) { + $items = [$items]; + } + + $parts = FieldsHelper::extract($context); + + if (!$parts) { + return []; + } + + $type = str_replace('com_', '', $parts[0]); + + $domain = $this->createDomain($type . '_' . $parts[1] . '_custom_fields', 'joomla_' . $type . '_' . $parts[1] . '_custom_fields_data'); + + foreach ($items as $item) { + // Get item's fields, also preparing their value property for manual display + $fields = FieldsHelper::getFields($parts[0] . '.' . $parts[1], $item); + + foreach ($fields as $field) { + $fieldValue = is_array($field->value) ? implode(', ', $field->value) : $field->value; + + $data = [ + $type . '_id' => $item->id, + 'field_name' => $field->name, + 'field_title' => $field->title, + 'field_value' => $fieldValue, + ]; + + $domain->addItem($this->createItemFromArray($data)); + } + } + + return $domain; + } } diff --git a/administrator/components/com_privacy/src/Removal/Status.php b/administrator/components/com_privacy/src/Removal/Status.php index 03b698de5b24e..cccc7fb7955ab 100644 --- a/administrator/components/com_privacy/src/Removal/Status.php +++ b/administrator/components/com_privacy/src/Removal/Status.php @@ -1,4 +1,5 @@ ' . Text::_('COM_PRIVACY_STATUS_COMPLETED') . ''; - - case 1: - return '' . Text::_('COM_PRIVACY_STATUS_CONFIRMED') . ''; - - case -1: - return '' . Text::_('COM_PRIVACY_STATUS_INVALID') . ''; - - default: - case 0: - return '' . Text::_('COM_PRIVACY_STATUS_PENDING') . ''; - } - } + /** + * Render a status badge + * + * @param integer $status The item status + * + * @return string + * + * @since 3.9.0 + */ + public function statusLabel($status) + { + switch ($status) { + case 2: + return '' . Text::_('COM_PRIVACY_STATUS_COMPLETED') . ''; + + case 1: + return '' . Text::_('COM_PRIVACY_STATUS_CONFIRMED') . ''; + + case -1: + return '' . Text::_('COM_PRIVACY_STATUS_INVALID') . ''; + + default: + case 0: + return '' . Text::_('COM_PRIVACY_STATUS_PENDING') . ''; + } + } } diff --git a/administrator/components/com_privacy/src/Table/ConsentTable.php b/administrator/components/com_privacy/src/Table/ConsentTable.php index 56330ce800cbf..33f364d403b30 100644 --- a/administrator/components/com_privacy/src/Table/ConsentTable.php +++ b/administrator/components/com_privacy/src/Table/ConsentTable.php @@ -1,4 +1,5 @@ id) - { - if (!$this->remind) - { - $this->remind = '0'; - } + // Set default values for new records + if (!$this->id) { + if (!$this->remind) { + $this->remind = '0'; + } - if (!$this->created) - { - $this->created = $date->toSql(); - } - } + if (!$this->created) { + $this->created = $date->toSql(); + } + } - return parent::store($updateNulls); - } + return parent::store($updateNulls); + } } diff --git a/administrator/components/com_privacy/src/Table/RequestTable.php b/administrator/components/com_privacy/src/Table/RequestTable.php index 8028385bffbbb..41399485c95e5 100644 --- a/administrator/components/com_privacy/src/Table/RequestTable.php +++ b/administrator/components/com_privacy/src/Table/RequestTable.php @@ -1,4 +1,5 @@ id) - { - if (!$this->status) - { - $this->status = '0'; - } + // Set default values for new records + if (!$this->id) { + if (!$this->status) { + $this->status = '0'; + } - if (!$this->requested_at) - { - $this->requested_at = $date->toSql(); - } + if (!$this->requested_at) { + $this->requested_at = $date->toSql(); + } - if (!$this->confirm_token_created_at) - { - $this->confirm_token_created_at = null; - } - } + if (!$this->confirm_token_created_at) { + $this->confirm_token_created_at = null; + } + } - return parent::store($updateNulls); - } + return parent::store($updateNulls); + } } diff --git a/administrator/components/com_privacy/src/View/Capabilities/HtmlView.php b/administrator/components/com_privacy/src/View/Capabilities/HtmlView.php index ca86b2fc1b540..7183bca88f1f8 100644 --- a/administrator/components/com_privacy/src/View/Capabilities/HtmlView.php +++ b/administrator/components/com_privacy/src/View/Capabilities/HtmlView.php @@ -1,4 +1,5 @@ capabilities = $this->get('Capabilities'); - $this->state = $this->get('State'); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + // Initialise variables + $this->capabilities = $this->get('Capabilities'); + $this->state = $this->get('State'); - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new Genericdataexception(implode("\n", $errors), 500); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new Genericdataexception(implode("\n", $errors), 500); + } - $this->addToolbar(); + $this->addToolbar(); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.9.0 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_CAPABILITIES'), 'lock'); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.9.0 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_CAPABILITIES'), 'lock'); - ToolbarHelper::preferences('com_privacy'); + ToolbarHelper::preferences('com_privacy'); - ToolbarHelper::help('Privacy:_Extension_Capabilities'); - } + ToolbarHelper::help('Privacy:_Extension_Capabilities'); + } } diff --git a/administrator/components/com_privacy/src/View/Consents/HtmlView.php b/administrator/components/com_privacy/src/View/Consents/HtmlView.php index 13be2edb3c840..a7082cd3e008b 100644 --- a/administrator/components/com_privacy/src/View/Consents/HtmlView.php +++ b/administrator/components/com_privacy/src/View/Consents/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->state = $model->getState(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new Genericdataexception(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.9.0 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_CONSENTS'), 'lock'); - - $bar = Toolbar::getInstance('toolbar'); - - // Add a button to invalidate a consent - if (!$this->isEmptyState) - { - $bar->appendButton( - 'Confirm', - 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_CONFIRM_MSG', - 'trash', - 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE', - 'consents.invalidate', - true - ); - } - - // If the filter is restricted to a specific subject, show the "Invalidate all" button - if ($this->state->get('filter.subject') != '') - { - $bar->appendButton( - 'Confirm', - 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_ALL_CONFIRM_MSG', - 'cancel', - 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_ALL', - 'consents.invalidateAll', - false - ); - } - - ToolbarHelper::preferences('com_privacy'); - - ToolbarHelper::help('Privacy:_Consents'); - } + /** + * The active search tools filters + * + * @var array + * @since 3.9.0 + * @note Must be public to be accessed from the search tools layout + */ + public $activeFilters; + + /** + * Form instance containing the search tools filter form + * + * @var Form + * @since 3.9.0 + * @note Must be public to be accessed from the search tools layout + */ + public $filterForm; + + /** + * The items to display + * + * @var array + * @since 3.9.0 + */ + protected $items; + + /** + * The pagination object + * + * @var Pagination + * @since 3.9.0 + */ + protected $pagination; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + /** @var ConsentsModel $model */ + $model = $this->getModel(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->state = $model->getState(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new Genericdataexception(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.9.0 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_CONSENTS'), 'lock'); + + $bar = Toolbar::getInstance('toolbar'); + + // Add a button to invalidate a consent + if (!$this->isEmptyState) { + $bar->appendButton( + 'Confirm', + 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_CONFIRM_MSG', + 'trash', + 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE', + 'consents.invalidate', + true + ); + } + + // If the filter is restricted to a specific subject, show the "Invalidate all" button + if ($this->state->get('filter.subject') != '') { + $bar->appendButton( + 'Confirm', + 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_ALL_CONFIRM_MSG', + 'cancel', + 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_ALL', + 'consents.invalidateAll', + false + ); + } + + ToolbarHelper::preferences('com_privacy'); + + ToolbarHelper::help('Privacy:_Consents'); + } } diff --git a/administrator/components/com_privacy/src/View/Export/XmlView.php b/administrator/components/com_privacy/src/View/Export/XmlView.php index 8fdee62f5a3c1..2fe95d3b0fa69 100644 --- a/administrator/components/com_privacy/src/View/Export/XmlView.php +++ b/administrator/components/com_privacy/src/View/Export/XmlView.php @@ -1,4 +1,5 @@ getModel(); - - $exportData = $model->collectDataForExportRequest(); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $requestId = $model->getState($model->getName() . '.request_id'); - - // This document should always be downloaded - $this->document->setDownload(true); - $this->document->setName('export-request-' . $requestId); - - echo PrivacyHelper::renderDataAsXml($exportData); - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + * + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + /** @var ExportModel $model */ + $model = $this->getModel(); + + $exportData = $model->collectDataForExportRequest(); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $requestId = $model->getState($model->getName() . '.request_id'); + + // This document should always be downloaded + $this->document->setDownload(true); + $this->document->setName('export-request-' . $requestId); + + echo PrivacyHelper::renderDataAsXml($exportData); + } } diff --git a/administrator/components/com_privacy/src/View/Request/HtmlView.php b/administrator/components/com_privacy/src/View/Request/HtmlView.php index 0ca8c3de68cfc..868f3b1f12f4b 100644 --- a/administrator/components/com_privacy/src/View/Request/HtmlView.php +++ b/administrator/components/com_privacy/src/View/Request/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->item = $model->getItem(); - $this->state = $model->getState(); - - // Variables only required for the default layout - if ($this->getLayout() === 'default') - { - /** @var \Joomla\Component\Actionlogs\Administrator\Model\ActionlogsModel $logsModel */ - $logsModel = $this->getModel('actionlogs'); - - $this->actionlogs = $logsModel->getLogsForItem('com_privacy.request', $this->item->id); - - // Load the com_actionlogs language strings for use in the layout - $lang = Factory::getLanguage(); - $lang->load('com_actionlogs', JPATH_ADMINISTRATOR) - || $lang->load('com_actionlogs', JPATH_ADMINISTRATOR . '/components/com_actionlogs'); - } - - // Variables only required for the edit layout - if ($this->getLayout() === 'edit') - { - $this->form = $this->get('Form'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.9.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - // Set the title and toolbar based on the layout - if ($this->getLayout() === 'edit') - { - ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUEST_ADD_REQUEST'), 'lock'); - - ToolbarHelper::save('request.save'); - ToolbarHelper::cancel('request.cancel'); - ToolbarHelper::help('Privacy:_New_Information_Request'); - } - else - { - ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUEST_SHOW_REQUEST'), 'lock'); - - $bar = Toolbar::getInstance('toolbar'); - - // Add transition and action buttons based on item status - switch ($this->item->status) - { - case '0': - $bar->appendButton('Standard', 'cancel-circle', 'COM_PRIVACY_TOOLBAR_INVALIDATE', 'request.invalidate', false); - - break; - - case '1': - $return = '&return=' . base64_encode('index.php?option=com_privacy&view=request&id=' . (int) $this->item->id); - - $bar->appendButton('Standard', 'apply', 'COM_PRIVACY_TOOLBAR_COMPLETE', 'request.complete', false); - $bar->appendButton('Standard', 'cancel-circle', 'COM_PRIVACY_TOOLBAR_INVALIDATE', 'request.invalidate', false); - - if ($this->item->request_type === 'export') - { - ToolbarHelper::link( - Route::_('index.php?option=com_privacy&task=request.export&format=xml&id=' . (int) $this->item->id . $return), - 'COM_PRIVACY_ACTION_EXPORT_DATA', - 'download' - ); - - if (Factory::getApplication()->get('mailonline', 1)) - { - ToolbarHelper::link( - Route::_( - 'index.php?option=com_privacy&task=request.emailexport&id=' . (int) $this->item->id . $return - . '&' . Session::getFormToken() . '=1' - ), - 'COM_PRIVACY_ACTION_EMAIL_EXPORT_DATA', - 'mail' - ); - } - } - - if ($this->item->request_type === 'remove') - { - $bar->appendButton('Standard', 'delete', 'COM_PRIVACY_ACTION_DELETE_DATA', 'request.remove', false); - } - - break; - - // Item is in a "locked" state and cannot transition - default: - break; - } - - ToolbarHelper::cancel('request.cancel', 'JTOOLBAR_CLOSE'); - ToolbarHelper::help('Privacy:_Review_Information_Request'); - } - } + /** + * The action logs for the item + * + * @var array + * @since 3.9.0 + */ + protected $actionlogs; + + /** + * The form object + * + * @var Form + * @since 3.9.0 + */ + protected $form; + + /** + * The item record + * + * @var CMSObject + * @since 3.9.0 + */ + protected $item; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + /** @var RequestsModel $model */ + $model = $this->getModel(); + $this->item = $model->getItem(); + $this->state = $model->getState(); + + // Variables only required for the default layout + if ($this->getLayout() === 'default') { + /** @var \Joomla\Component\Actionlogs\Administrator\Model\ActionlogsModel $logsModel */ + $logsModel = $this->getModel('actionlogs'); + + $this->actionlogs = $logsModel->getLogsForItem('com_privacy.request', $this->item->id); + + // Load the com_actionlogs language strings for use in the layout + $lang = Factory::getLanguage(); + $lang->load('com_actionlogs', JPATH_ADMINISTRATOR) + || $lang->load('com_actionlogs', JPATH_ADMINISTRATOR . '/components/com_actionlogs'); + } + + // Variables only required for the edit layout + if ($this->getLayout() === 'edit') { + $this->form = $this->get('Form'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.9.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + // Set the title and toolbar based on the layout + if ($this->getLayout() === 'edit') { + ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUEST_ADD_REQUEST'), 'lock'); + + ToolbarHelper::save('request.save'); + ToolbarHelper::cancel('request.cancel'); + ToolbarHelper::help('Privacy:_New_Information_Request'); + } else { + ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUEST_SHOW_REQUEST'), 'lock'); + + $bar = Toolbar::getInstance('toolbar'); + + // Add transition and action buttons based on item status + switch ($this->item->status) { + case '0': + $bar->appendButton('Standard', 'cancel-circle', 'COM_PRIVACY_TOOLBAR_INVALIDATE', 'request.invalidate', false); + + break; + + case '1': + $return = '&return=' . base64_encode('index.php?option=com_privacy&view=request&id=' . (int) $this->item->id); + + $bar->appendButton('Standard', 'apply', 'COM_PRIVACY_TOOLBAR_COMPLETE', 'request.complete', false); + $bar->appendButton('Standard', 'cancel-circle', 'COM_PRIVACY_TOOLBAR_INVALIDATE', 'request.invalidate', false); + + if ($this->item->request_type === 'export') { + ToolbarHelper::link( + Route::_('index.php?option=com_privacy&task=request.export&format=xml&id=' . (int) $this->item->id . $return), + 'COM_PRIVACY_ACTION_EXPORT_DATA', + 'download' + ); + + if (Factory::getApplication()->get('mailonline', 1)) { + ToolbarHelper::link( + Route::_( + 'index.php?option=com_privacy&task=request.emailexport&id=' . (int) $this->item->id . $return + . '&' . Session::getFormToken() . '=1' + ), + 'COM_PRIVACY_ACTION_EMAIL_EXPORT_DATA', + 'mail' + ); + } + } + + if ($this->item->request_type === 'remove') { + $bar->appendButton('Standard', 'delete', 'COM_PRIVACY_ACTION_DELETE_DATA', 'request.remove', false); + } + + break; + + // Item is in a "locked" state and cannot transition + default: + break; + } + + ToolbarHelper::cancel('request.cancel', 'JTOOLBAR_CLOSE'); + ToolbarHelper::help('Privacy:_Review_Information_Request'); + } + } } diff --git a/administrator/components/com_privacy/src/View/Requests/HtmlView.php b/administrator/components/com_privacy/src/View/Requests/HtmlView.php index 6e23cc33c2df6..c164ec3ad434a 100644 --- a/administrator/components/com_privacy/src/View/Requests/HtmlView.php +++ b/administrator/components/com_privacy/src/View/Requests/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->state = $model->getState(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - $this->urgentRequestAge = (int) ComponentHelper::getParams('com_privacy')->get('notify', 14); - $this->sendMailEnabled = (bool) Factory::getApplication()->get('mailonline', 1); - - if (!count($this->items) && $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new Genericdataexception(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.9.0 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUESTS'), 'lock'); - - // Requests can only be created if mail sending is enabled - if (Factory::getApplication()->get('mailonline', 1)) - { - ToolbarHelper::addNew('request.add'); - } - - ToolbarHelper::preferences('com_privacy'); - ToolbarHelper::help('Privacy:_Information_Requests'); - } + /** + * The active search tools filters + * + * @var array + * @since 3.9.0 + * @note Must be public to be accessed from the search tools layout + */ + public $activeFilters; + + /** + * Form instance containing the search tools filter form + * + * @var Form + * @since 3.9.0 + * @note Must be public to be accessed from the search tools layout + */ + public $filterForm; + + /** + * The items to display + * + * @var array + * @since 3.9.0 + */ + protected $items; + + /** + * The pagination object + * + * @var Pagination + * @since 3.9.0 + */ + protected $pagination; + + /** + * Flag indicating the site supports sending email + * + * @var boolean + * @since 3.9.0 + */ + protected $sendMailEnabled; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * The age of urgent requests + * + * @var integer + * @since 3.9.0 + */ + protected $urgentRequestAge; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + /** @var RequestsModel $model */ + $model = $this->getModel(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->state = $model->getState(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + $this->urgentRequestAge = (int) ComponentHelper::getParams('com_privacy')->get('notify', 14); + $this->sendMailEnabled = (bool) Factory::getApplication()->get('mailonline', 1); + + if (!count($this->items) && $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new Genericdataexception(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.9.0 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUESTS'), 'lock'); + + // Requests can only be created if mail sending is enabled + if (Factory::getApplication()->get('mailonline', 1)) { + ToolbarHelper::addNew('request.add'); + } + + ToolbarHelper::preferences('com_privacy'); + ToolbarHelper::help('Privacy:_Information_Requests'); + } } diff --git a/administrator/components/com_privacy/tmpl/capabilities/default.php b/administrator/components/com_privacy/tmpl/capabilities/default.php index fe0bfc66ecc47..137ed9db13173 100644 --- a/administrator/components/com_privacy/tmpl/capabilities/default.php +++ b/administrator/components/com_privacy/tmpl/capabilities/default.php @@ -1,4 +1,5 @@
    -
    -

    - -
    - capabilities)) : ?> -
    - - -
    - - capabilities as $extension => $capabilities) : ?> -
    - - -
    - - -
    - -
      - -
    • - -
    - -
    - - +
    +

    + +
    + capabilities)) : ?> +
    + + +
    + + capabilities as $extension => $capabilities) : ?> +
    + + +
    + + +
    + +
      + +
    • + +
    + +
    + +
    diff --git a/administrator/components/com_privacy/tmpl/consents/default.php b/administrator/components/com_privacy/tmpl/consents/default.php index f4240c1933ea2..0878d0b277689 100644 --- a/administrator/components/com_privacy/tmpl/consents/default.php +++ b/administrator/components/com_privacy/tmpl/consents/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -29,100 +30,100 @@ $now = Factory::getDate(); $stateIcons = array(-1 => 'delete', 0 => 'archive', 1 => 'publish'); $stateMsgs = array( - -1 => Text::_('COM_PRIVACY_CONSENTS_STATE_INVALIDATED'), - 0 => Text::_('COM_PRIVACY_CONSENTS_STATE_OBSOLETE'), - 1 => Text::_('COM_PRIVACY_CONSENTS_STATE_VALID') + -1 => Text::_('COM_PRIVACY_CONSENTS_STATE_INVALIDATED'), + 0 => Text::_('COM_PRIVACY_CONSENTS_STATE_OBSOLETE'), + 1 => Text::_('COM_PRIVACY_CONSENTS_STATE_VALID') ); ?>
    -
    - $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - items as $i => $item) : ?> - - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - - - - - -
    - id, false, 'cid', 'cb', $item->username); ?> - - - state]; ?>"> - - username; ?> - - name; ?> - - user_id; ?> - - subject); ?> - - body; ?> - - created), null, $now); ?> -
    - created, Text::_('DATE_FORMAT_LC6')); ?> -
    -
    - id; ?> -
    - +
    + $this)); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + + + + + +
    + id, false, 'cid', 'cb', $item->username); ?> + + + state]; ?>"> + + username; ?> + + name; ?> + + user_id; ?> + + subject); ?> + + body; ?> + + created), null, $now); ?> +
    + created, Text::_('DATE_FORMAT_LC6')); ?> +
    +
    + id; ?> +
    + - - - -
    + + + +
    diff --git a/administrator/components/com_privacy/tmpl/consents/emptystate.php b/administrator/components/com_privacy/tmpl/consents/emptystate.php index 4527f0f230941..a76086b554fc5 100644 --- a/administrator/components/com_privacy/tmpl/consents/emptystate.php +++ b/administrator/components/com_privacy/tmpl/consents/emptystate.php @@ -1,4 +1,5 @@ 'COM_PRIVACY_CONSENTS', - 'formURL' => 'index.php?option=com_privacy&view=consents', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Privacy:_Consents', - 'icon' => 'icon-lock', + 'textPrefix' => 'COM_PRIVACY_CONSENTS', + 'formURL' => 'index.php?option=com_privacy&view=consents', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Privacy:_Consents', + 'icon' => 'icon-lock', ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_privacy/tmpl/request/default.php b/administrator/components/com_privacy/tmpl/request/default.php index 8fa51ae5ad271..95ae325ba8ab9 100644 --- a/administrator/components/com_privacy/tmpl/request/default.php +++ b/administrator/components/com_privacy/tmpl/request/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
    -
    -
    -
    -

    -
    -
    -
    :
    -
    item->email; ?>
    +
    +
    +
    +

    +
    +
    +
    :
    +
    item->email; ?>
    -
    :
    -
    item->status); ?>
    +
    :
    +
    item->status); ?>
    -
    :
    -
    item->request_type); ?>
    +
    :
    +
    item->request_type); ?>
    -
    :
    -
    item->requested_at, Text::_('DATE_FORMAT_LC6')); ?>
    -
    -
    -
    -
    -
    -
    -

    -
    - actionlogs)) : ?> -
    - - -
    - - - - - - - - - actionlogs as $i => $item) : ?> - - - - - - - -
    - - - - - -
    - - - log_date, Text::_('DATE_FORMAT_LC6')); ?> - - name; ?> -
    - -
    -
    -
    -
    +
    :
    +
    item->requested_at, Text::_('DATE_FORMAT_LC6')); ?>
    +
    +
    +
    +
    +
    +
    +

    +
    + actionlogs)) : ?> +
    + + +
    + + + + + + + + + actionlogs as $i => $item) : ?> + + + + + + + +
    + + + + + +
    + + + log_date, Text::_('DATE_FORMAT_LC6')); ?> + + name; ?> +
    + +
    +
    +
    +
    - - + +
    diff --git a/administrator/components/com_privacy/tmpl/request/edit.php b/administrator/components/com_privacy/tmpl/request/edit.php index d38658acf3345..682e79313dd4d 100644 --- a/administrator/components/com_privacy/tmpl/request/edit.php +++ b/administrator/components/com_privacy/tmpl/request/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
    -
    -
    -
    -
    - form->renderField('email'); ?> - form->renderField('status'); ?> - form->renderField('request_type'); ?> -
    -
    -
    - - - -
    +
    +
    +
    +
    + form->renderField('email'); ?> + form->renderField('status'); ?> + form->renderField('request_type'); ?> +
    +
    +
    + + + +
    diff --git a/administrator/components/com_privacy/tmpl/requests/default.php b/administrator/components/com_privacy/tmpl/requests/default.php index 9c7a9fa19fcd6..5f8c0c0a59f08 100644 --- a/administrator/components/com_privacy/tmpl/requests/default.php +++ b/administrator/components/com_privacy/tmpl/requests/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -34,96 +35,96 @@ ?>
    -
    - $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - items as $i => $item) : ?> - requested_at); - ?> - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - -
    -
    - status == 1 && $item->request_type === 'export') : ?> - - sendMailEnabled) : ?> - - - - status == 1 && $item->request_type === 'remove') : ?> - - -
    -
    - status); ?> - - status == 1 && $urgentRequestDate >= $itemRequestedAt) : ?> - - - - escape($item->email)); ?> - - - request_type); ?> - - -
    - requested_at, Text::_('DATE_FORMAT_LC6')); ?> -
    -
    - id; ?> -
    +
    + $this)); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + items as $i => $item) : ?> + requested_at); + ?> + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + +
    +
    + status == 1 && $item->request_type === 'export') : ?> + + sendMailEnabled) : ?> + + + + status == 1 && $item->request_type === 'remove') : ?> + + +
    +
    + status); ?> + + status == 1 && $urgentRequestDate >= $itemRequestedAt) : ?> + + + + escape($item->email)); ?> + + + request_type); ?> + + +
    + requested_at, Text::_('DATE_FORMAT_LC6')); ?> +
    +
    + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
    + + + +
    diff --git a/administrator/components/com_privacy/tmpl/requests/emptystate.php b/administrator/components/com_privacy/tmpl/requests/emptystate.php index c345a7be9d0f1..14eb521edf0ca 100644 --- a/administrator/components/com_privacy/tmpl/requests/emptystate.php +++ b/administrator/components/com_privacy/tmpl/requests/emptystate.php @@ -1,4 +1,5 @@ 'COM_PRIVACY_REQUESTS', - 'formURL' => 'index.php?option=com_privacy&view=requests', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Privacy:_Information_Requests', - 'icon' => 'icon-lock', + 'textPrefix' => 'COM_PRIVACY_REQUESTS', + 'formURL' => 'index.php?option=com_privacy&view=requests', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Privacy:_Information_Requests', + 'icon' => 'icon-lock', ]; -if (Factory::getApplication()->get('mailonline', 1)) -{ - $displayData['createURL'] = 'index.php?option=com_privacy&task=request.add'; +if (Factory::getApplication()->get('mailonline', 1)) { + $displayData['createURL'] = 'index.php?option=com_privacy&task=request.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_redirect/helpers/redirect.php b/administrator/components/com_redirect/helpers/redirect.php index 0f6c31e9f6dae..494b7c015f3be 100644 --- a/administrator/components/com_redirect/helpers/redirect.php +++ b/administrator/components/com_redirect/helpers/redirect.php @@ -1,4 +1,5 @@ diff --git a/administrator/components/com_redirect/services/provider.php b/administrator/components/com_redirect/services/provider.php index 54db6ae48a2a7..d52d7e8230510 100644 --- a/administrator/components/com_redirect/services/provider.php +++ b/administrator/components/com_redirect/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Redirect')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Redirect')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Redirect')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Redirect')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new RedirectComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new RedirectComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_redirect/src/Controller/DisplayController.php b/administrator/components/com_redirect/src/Controller/DisplayController.php index 541446f8dd621..e509c938807cc 100644 --- a/administrator/components/com_redirect/src/Controller/DisplayController.php +++ b/administrator/components/com_redirect/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'links'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached. + * @param mixed $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', 'links'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - if ($view === 'links') - { - $pluginEnabled = PluginHelper::isEnabled('system', 'redirect'); - $collectUrlsEnabled = RedirectHelper::collectUrlsEnabled(); + if ($view === 'links') { + $pluginEnabled = PluginHelper::isEnabled('system', 'redirect'); + $collectUrlsEnabled = RedirectHelper::collectUrlsEnabled(); - // Show messages about the enabled plugin and if the plugin should collect URLs - if ($pluginEnabled && $collectUrlsEnabled) - { - $this->app->enqueueMessage(Text::sprintf('COM_REDIRECT_COLLECT_URLS_ENABLED', Text::_('COM_REDIRECT_PLUGIN_ENABLED')), 'notice'); - } - else - { - $redirectPluginId = RedirectHelper::getRedirectPluginId(); - $link = HTMLHelper::_( - 'link', - '#plugin' . $redirectPluginId . 'Modal', - Text::_('COM_REDIRECT_SYSTEM_PLUGIN'), - 'class="alert-link" data-bs-toggle="modal" id="title-' . $redirectPluginId . '"' - ); + // Show messages about the enabled plugin and if the plugin should collect URLs + if ($pluginEnabled && $collectUrlsEnabled) { + $this->app->enqueueMessage(Text::sprintf('COM_REDIRECT_COLLECT_URLS_ENABLED', Text::_('COM_REDIRECT_PLUGIN_ENABLED')), 'notice'); + } else { + $redirectPluginId = RedirectHelper::getRedirectPluginId(); + $link = HTMLHelper::_( + 'link', + '#plugin' . $redirectPluginId . 'Modal', + Text::_('COM_REDIRECT_SYSTEM_PLUGIN'), + 'class="alert-link" data-bs-toggle="modal" id="title-' . $redirectPluginId . '"' + ); - if ($pluginEnabled && !$collectUrlsEnabled) - { - $this->app->enqueueMessage( - Text::sprintf('COM_REDIRECT_COLLECT_MODAL_URLS_DISABLED', Text::_('COM_REDIRECT_PLUGIN_ENABLED'), $link), - 'notice' - ); - } - else - { - $this->app->enqueueMessage(Text::sprintf('COM_REDIRECT_PLUGIN_MODAL_DISABLED', $link), 'error'); - } - } - } + if ($pluginEnabled && !$collectUrlsEnabled) { + $this->app->enqueueMessage( + Text::sprintf('COM_REDIRECT_COLLECT_MODAL_URLS_DISABLED', Text::_('COM_REDIRECT_PLUGIN_ENABLED'), $link), + 'notice' + ); + } else { + $this->app->enqueueMessage(Text::sprintf('COM_REDIRECT_PLUGIN_MODAL_DISABLED', $link), 'error'); + } + } + } - // Check for edit form. - if ($view == 'link' && $layout == 'edit' && !$this->checkEditId('com_redirect.edit.link', $id)) - { - // 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', $id), 'error'); - } + // Check for edit form. + if ($view == 'link' && $layout == 'edit' && !$this->checkEditId('com_redirect.edit.link', $id)) { + // 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', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_redirect&view=links', false)); + $this->setRedirect(Route::_('index.php?option=com_redirect&view=links', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/administrator/components/com_redirect/src/Controller/LinkController.php b/administrator/components/com_redirect/src/Controller/LinkController.php index e6b38f58b2e46..d4aaf87e13c4a 100644 --- a/administrator/components/com_redirect/src/Controller/LinkController.php +++ b/administrator/components/com_redirect/src/Controller/LinkController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Redirect\Administrator\Controller; \defined('_JEXEC') or die; @@ -19,5 +21,5 @@ */ class LinkController extends FormController { - // Parent class access checks are sufficient for this controller. + // Parent class access checks are sufficient for this controller. } diff --git a/administrator/components/com_redirect/src/Controller/LinksController.php b/administrator/components/com_redirect/src/Controller/LinksController.php index f18202fe86e99..7c810f3cf83e2 100644 --- a/administrator/components/com_redirect/src/Controller/LinksController.php +++ b/administrator/components/com_redirect/src/Controller/LinksController.php @@ -1,4 +1,5 @@ checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('COM_REDIRECT_NO_ITEM_SELECTED'), 'warning'); - } - else - { - $newUrl = $this->input->getString('new_url'); - $comment = $this->input->getString('comment'); - - // Get the model. - $model = $this->getModel(); - - // Remove the items. - if (!$model->activate($ids, $newUrl, $comment)) - { - $this->app->enqueueMessage($model->getError(), 'warning'); - } - else - { - $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_UPDATED', count($ids))); - } - } - - $this->setRedirect('index.php?option=com_redirect&view=links'); - } - - /** - * Method to duplicate URLs in records. - * - * @return void - * - * @since 3.6.0 - */ - public function duplicateUrls() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('COM_REDIRECT_NO_ITEM_SELECTED'), 'warning'); - } - else - { - $newUrl = $this->input->getString('new_url'); - $comment = $this->input->getString('comment'); - - // Get the model. - $model = $this->getModel(); - - // Remove the items. - if (!$model->duplicateUrls($ids, $newUrl, $comment)) - { - $this->app->enqueueMessage($model->getError(), 'warning'); - } - else - { - $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_UPDATED', count($ids))); - } - } - - $this->setRedirect('index.php?option=com_redirect&view=links'); - } - - /** - * Proxy for getModel. - * - * @param string $name The name of the model. - * @param string $prefix The prefix of the model. - * @param array $config An array of settings. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model instance - * - * @since 1.6 - */ - public function getModel($name = 'Link', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Executes the batch process to add URLs to the database - * - * @return void - */ - public function batch() - { - // Check for request forgeries. - $this->checkToken(); - - $batch_urls_request = $this->input->post->get('batch_urls', array(), 'array'); - $batch_urls_lines = array_map('trim', explode("\n", $batch_urls_request[0])); - - $batch_urls = array(); - - foreach ($batch_urls_lines as $batch_urls_line) - { - if (!empty($batch_urls_line)) - { - $params = ComponentHelper::getParams('com_redirect'); - $separator = $params->get('separator', '|'); - - // Basic check to make sure the correct separator is being used - if (!\Joomla\String\StringHelper::strpos($batch_urls_line, $separator)) - { - $this->setMessage(Text::sprintf('COM_REDIRECT_NO_SEPARATOR_FOUND', $separator), 'error'); - $this->setRedirect('index.php?option=com_redirect&view=links'); - - return; - } - - $batch_urls[] = array_map('trim', explode($separator, $batch_urls_line)); - } - } - - // Set default message on error - overwrite if successful - $this->setMessage(Text::_('COM_REDIRECT_NO_ITEM_ADDED'), 'error'); - - if (!empty($batch_urls)) - { - $model = $this->getModel('Links'); - - // Execute the batch process - if ($model->batchProcess($batch_urls)) - { - $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_ADDED', count($batch_urls))); - } - } - - $this->setRedirect('index.php?option=com_redirect&view=links'); - } - - /** - * Clean out the unpublished links. - * - * @return void - * - * @since 3.5 - */ - public function purge() - { - // Check for request forgeries. - $this->checkToken(); - - $model = $this->getModel('Links'); - - if ($model->purge()) - { - $message = Text::_('COM_REDIRECT_CLEAR_SUCCESS'); - } - else - { - $message = Text::_('COM_REDIRECT_CLEAR_FAIL'); - } - - $this->setRedirect('index.php?option=com_redirect&view=links', $message); - } + /** + * Method to update a record. + * + * @return void + * + * @since 1.6 + */ + public function activate() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('COM_REDIRECT_NO_ITEM_SELECTED'), 'warning'); + } else { + $newUrl = $this->input->getString('new_url'); + $comment = $this->input->getString('comment'); + + // Get the model. + $model = $this->getModel(); + + // Remove the items. + if (!$model->activate($ids, $newUrl, $comment)) { + $this->app->enqueueMessage($model->getError(), 'warning'); + } else { + $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_UPDATED', count($ids))); + } + } + + $this->setRedirect('index.php?option=com_redirect&view=links'); + } + + /** + * Method to duplicate URLs in records. + * + * @return void + * + * @since 3.6.0 + */ + public function duplicateUrls() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('COM_REDIRECT_NO_ITEM_SELECTED'), 'warning'); + } else { + $newUrl = $this->input->getString('new_url'); + $comment = $this->input->getString('comment'); + + // Get the model. + $model = $this->getModel(); + + // Remove the items. + if (!$model->duplicateUrls($ids, $newUrl, $comment)) { + $this->app->enqueueMessage($model->getError(), 'warning'); + } else { + $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_UPDATED', count($ids))); + } + } + + $this->setRedirect('index.php?option=com_redirect&view=links'); + } + + /** + * Proxy for getModel. + * + * @param string $name The name of the model. + * @param string $prefix The prefix of the model. + * @param array $config An array of settings. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model instance + * + * @since 1.6 + */ + public function getModel($name = 'Link', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Executes the batch process to add URLs to the database + * + * @return void + */ + public function batch() + { + // Check for request forgeries. + $this->checkToken(); + + $batch_urls_request = $this->input->post->get('batch_urls', array(), 'array'); + $batch_urls_lines = array_map('trim', explode("\n", $batch_urls_request[0])); + + $batch_urls = array(); + + foreach ($batch_urls_lines as $batch_urls_line) { + if (!empty($batch_urls_line)) { + $params = ComponentHelper::getParams('com_redirect'); + $separator = $params->get('separator', '|'); + + // Basic check to make sure the correct separator is being used + if (!\Joomla\String\StringHelper::strpos($batch_urls_line, $separator)) { + $this->setMessage(Text::sprintf('COM_REDIRECT_NO_SEPARATOR_FOUND', $separator), 'error'); + $this->setRedirect('index.php?option=com_redirect&view=links'); + + return; + } + + $batch_urls[] = array_map('trim', explode($separator, $batch_urls_line)); + } + } + + // Set default message on error - overwrite if successful + $this->setMessage(Text::_('COM_REDIRECT_NO_ITEM_ADDED'), 'error'); + + if (!empty($batch_urls)) { + $model = $this->getModel('Links'); + + // Execute the batch process + if ($model->batchProcess($batch_urls)) { + $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_ADDED', count($batch_urls))); + } + } + + $this->setRedirect('index.php?option=com_redirect&view=links'); + } + + /** + * Clean out the unpublished links. + * + * @return void + * + * @since 3.5 + */ + public function purge() + { + // Check for request forgeries. + $this->checkToken(); + + $model = $this->getModel('Links'); + + if ($model->purge()) { + $message = Text::_('COM_REDIRECT_CLEAR_SUCCESS'); + } else { + $message = Text::_('COM_REDIRECT_CLEAR_FAIL'); + } + + $this->setRedirect('index.php?option=com_redirect&view=links', $message); + } } diff --git a/administrator/components/com_redirect/src/Extension/RedirectComponent.php b/administrator/components/com_redirect/src/Extension/RedirectComponent.php index 2f155999b68b6..dcbbdf93e5139 100644 --- a/administrator/components/com_redirect/src/Extension/RedirectComponent.php +++ b/administrator/components/com_redirect/src/Extension/RedirectComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('redirect', new Redirect); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('redirect', new Redirect()); + } } diff --git a/administrator/components/com_redirect/src/Field/RedirectField.php b/administrator/components/com_redirect/src/Field/RedirectField.php index 719c7b3c79c2a..2628551ed062f 100644 --- a/administrator/components/com_redirect/src/Field/RedirectField.php +++ b/administrator/components/com_redirect/src/Field/RedirectField.php @@ -1,4 +1,5 @@ 'HTTP/1.1 100 Continue', - 101 => 'HTTP/1.1 101 Switching Protocols', - 102 => 'HTTP/1.1 102 Processing', - 103 => 'HTTP/1.1 103 Early Hints', - 200 => 'HTTP/1.1 200 OK', - 201 => 'HTTP/1.1 201 Created', - 202 => 'HTTP/1.1 202 Accepted', - 203 => 'HTTP/1.1 203 Non-Authoritative Information', - 204 => 'HTTP/1.1 204 No Content', - 205 => 'HTTP/1.1 205 Reset Content', - 206 => 'HTTP/1.1 206 Partial Content', - 207 => 'HTTP/1.1 207 Multi-Status', - 208 => 'HTTP/1.1 208 Already Reported', - 226 => 'HTTP/1.1 226 IM Used', - 300 => 'HTTP/1.1 300 Multiple Choices', - 301 => 'HTTP/1.1 301 Moved Permanently', - 302 => 'HTTP/1.1 302 Found', - 303 => 'HTTP/1.1 303 See other', - 304 => 'HTTP/1.1 304 Not Modified', - 305 => 'HTTP/1.1 305 Use Proxy', - 306 => 'HTTP/1.1 306 (Unused)', - 307 => 'HTTP/1.1 307 Temporary Redirect', - 308 => 'HTTP/1.1 308 Permanent Redirect', - 400 => 'HTTP/1.1 400 Bad Request', - 401 => 'HTTP/1.1 401 Unauthorized', - 402 => 'HTTP/1.1 402 Payment Required', - 403 => 'HTTP/1.1 403 Forbidden', - 404 => 'HTTP/1.1 404 Not Found', - 405 => 'HTTP/1.1 405 Method Not Allowed', - 406 => 'HTTP/1.1 406 Not Acceptable', - 407 => 'HTTP/1.1 407 Proxy Authentication Required', - 408 => 'HTTP/1.1 408 Request Timeout', - 409 => 'HTTP/1.1 409 Conflict', - 410 => 'HTTP/1.1 410 Gone', - 411 => 'HTTP/1.1 411 Length Required', - 412 => 'HTTP/1.1 412 Precondition Failed', - 413 => 'HTTP/1.1 413 Payload Too Large', - 414 => 'HTTP/1.1 414 URI Too Long', - 415 => 'HTTP/1.1 415 Unsupported Media Type', - 416 => 'HTTP/1.1 416 Requested Range Not Satisfiable', - 417 => 'HTTP/1.1 417 Expectation Failed', - 418 => 'HTTP/1.1 418 I\'m a teapot', - 421 => 'HTTP/1.1 421 Misdirected Request', - 422 => 'HTTP/1.1 422 Unprocessable Entity', - 423 => 'HTTP/1.1 423 Locked', - 424 => 'HTTP/1.1 424 Failed Dependency', - 425 => 'HTTP/1.1 425 Reserved for WebDAV advanced collections expired proposal', - 426 => 'HTTP/1.1 426 Upgrade Required', - 428 => 'HTTP/1.1 428 Precondition Required', - 429 => 'HTTP/1.1 429 Too Many Requests', - 431 => 'HTTP/1.1 431 Request Header Fields Too Large', - 451 => 'HTTP/1.1 451 Unavailable For Legal Reasons', - 500 => 'HTTP/1.1 500 Internal Server Error', - 501 => 'HTTP/1.1 501 Not Implemented', - 502 => 'HTTP/1.1 502 Bad Gateway', - 503 => 'HTTP/1.1 503 Service Unavailable', - 504 => 'HTTP/1.1 504 Gateway Timeout', - 505 => 'HTTP/1.1 505 HTTP Version Not Supported', - 506 => 'HTTP/1.1 506 Variant Also Negotiates (Experimental)', - 507 => 'HTTP/1.1 507 Insufficient Storage', - 508 => 'HTTP/1.1 508 Loop Detected', - 510 => 'HTTP/1.1 510 Not Extended', - 511 => 'HTTP/1.1 511 Network Authentication Required', - ); + /** + * A map of integer HTTP 1.1 response codes to the full HTTP Status for the headers. + * + * @var object + * @since 3.4 + * @link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + */ + protected $responseMap = array( + 100 => 'HTTP/1.1 100 Continue', + 101 => 'HTTP/1.1 101 Switching Protocols', + 102 => 'HTTP/1.1 102 Processing', + 103 => 'HTTP/1.1 103 Early Hints', + 200 => 'HTTP/1.1 200 OK', + 201 => 'HTTP/1.1 201 Created', + 202 => 'HTTP/1.1 202 Accepted', + 203 => 'HTTP/1.1 203 Non-Authoritative Information', + 204 => 'HTTP/1.1 204 No Content', + 205 => 'HTTP/1.1 205 Reset Content', + 206 => 'HTTP/1.1 206 Partial Content', + 207 => 'HTTP/1.1 207 Multi-Status', + 208 => 'HTTP/1.1 208 Already Reported', + 226 => 'HTTP/1.1 226 IM Used', + 300 => 'HTTP/1.1 300 Multiple Choices', + 301 => 'HTTP/1.1 301 Moved Permanently', + 302 => 'HTTP/1.1 302 Found', + 303 => 'HTTP/1.1 303 See other', + 304 => 'HTTP/1.1 304 Not Modified', + 305 => 'HTTP/1.1 305 Use Proxy', + 306 => 'HTTP/1.1 306 (Unused)', + 307 => 'HTTP/1.1 307 Temporary Redirect', + 308 => 'HTTP/1.1 308 Permanent Redirect', + 400 => 'HTTP/1.1 400 Bad Request', + 401 => 'HTTP/1.1 401 Unauthorized', + 402 => 'HTTP/1.1 402 Payment Required', + 403 => 'HTTP/1.1 403 Forbidden', + 404 => 'HTTP/1.1 404 Not Found', + 405 => 'HTTP/1.1 405 Method Not Allowed', + 406 => 'HTTP/1.1 406 Not Acceptable', + 407 => 'HTTP/1.1 407 Proxy Authentication Required', + 408 => 'HTTP/1.1 408 Request Timeout', + 409 => 'HTTP/1.1 409 Conflict', + 410 => 'HTTP/1.1 410 Gone', + 411 => 'HTTP/1.1 411 Length Required', + 412 => 'HTTP/1.1 412 Precondition Failed', + 413 => 'HTTP/1.1 413 Payload Too Large', + 414 => 'HTTP/1.1 414 URI Too Long', + 415 => 'HTTP/1.1 415 Unsupported Media Type', + 416 => 'HTTP/1.1 416 Requested Range Not Satisfiable', + 417 => 'HTTP/1.1 417 Expectation Failed', + 418 => 'HTTP/1.1 418 I\'m a teapot', + 421 => 'HTTP/1.1 421 Misdirected Request', + 422 => 'HTTP/1.1 422 Unprocessable Entity', + 423 => 'HTTP/1.1 423 Locked', + 424 => 'HTTP/1.1 424 Failed Dependency', + 425 => 'HTTP/1.1 425 Reserved for WebDAV advanced collections expired proposal', + 426 => 'HTTP/1.1 426 Upgrade Required', + 428 => 'HTTP/1.1 428 Precondition Required', + 429 => 'HTTP/1.1 429 Too Many Requests', + 431 => 'HTTP/1.1 431 Request Header Fields Too Large', + 451 => 'HTTP/1.1 451 Unavailable For Legal Reasons', + 500 => 'HTTP/1.1 500 Internal Server Error', + 501 => 'HTTP/1.1 501 Not Implemented', + 502 => 'HTTP/1.1 502 Bad Gateway', + 503 => 'HTTP/1.1 503 Service Unavailable', + 504 => 'HTTP/1.1 504 Gateway Timeout', + 505 => 'HTTP/1.1 505 HTTP Version Not Supported', + 506 => 'HTTP/1.1 506 Variant Also Negotiates (Experimental)', + 507 => 'HTTP/1.1 507 Insufficient Storage', + 508 => 'HTTP/1.1 508 Loop Detected', + 510 => 'HTTP/1.1 510 Not Extended', + 511 => 'HTTP/1.1 511 Network Authentication Required', + ); - /** - * Method to get the field input markup. - * - * @return array The field input markup. - * - * @since 3.4 - */ - protected function getOptions() - { - $options = array(); + /** + * Method to get the field input markup. + * + * @return array The field input markup. + * + * @since 3.4 + */ + protected function getOptions() + { + $options = array(); - foreach ($this->responseMap as $key => $value) - { - $options[] = HTMLHelper::_('select.option', $key, $value); - } + foreach ($this->responseMap as $key => $value) { + $options[] = HTMLHelper::_('select.option', $key, $value); + } - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); - return $options; - } + return $options; + } } diff --git a/administrator/components/com_redirect/src/Helper/RedirectHelper.php b/administrator/components/com_redirect/src/Helper/RedirectHelper.php index f09310abd0b4d..667163239daec 100644 --- a/administrator/components/com_redirect/src/Helper/RedirectHelper.php +++ b/administrator/components/com_redirect/src/Helper/RedirectHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('redirect')); - $db->setQuery($query); + /** + * Gets the redirect system plugin extension id. + * + * @return integer The redirect system plugin extension id. + * + * @since 3.6.0 + */ + public static function getRedirectPluginId() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('redirect')); + $db->setQuery($query); - try - { - $result = (int) $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } + try { + $result = (int) $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } - return $result; - } + return $result; + } - /** - * Checks whether the option "Collect URLs" is enabled for the output message - * - * @return boolean - * - * @since 3.4 - */ - public static function collectUrlsEnabled() - { - $collect_urls = false; + /** + * Checks whether the option "Collect URLs" is enabled for the output message + * + * @return boolean + * + * @since 3.4 + */ + public static function collectUrlsEnabled() + { + $collect_urls = false; - if (PluginHelper::isEnabled('system', 'redirect')) - { - $params = new Registry(PluginHelper::getPlugin('system', 'redirect')->params); - $collect_urls = (bool) $params->get('collect_urls', 1); - } + if (PluginHelper::isEnabled('system', 'redirect')) { + $params = new Registry(PluginHelper::getPlugin('system', 'redirect')->params); + $collect_urls = (bool) $params->get('collect_urls', 1); + } - return $collect_urls; - } + return $collect_urls; + } } diff --git a/administrator/components/com_redirect/src/Model/LinkModel.php b/administrator/components/com_redirect/src/Model/LinkModel.php index 7eff8659f7aa6..f0823fd633d54 100644 --- a/administrator/components/com_redirect/src/Model/LinkModel.php +++ b/administrator/components/com_redirect/src/Model/LinkModel.php @@ -1,4 +1,5 @@ published != -2) - { - return false; - } - - return parent::canDelete($record); - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return \Joomla\CMS\Form\Form A JForm object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_redirect.link', 'link', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if ($this->canEditState((object) $data) != true) - { - // Disable fields for display. - $form->setFieldAttribute('published', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - // If in advanced mode then we make sure the new URL field is not compulsory and the header - // field compulsory in case people select non-3xx redirects - if (ComponentHelper::getParams('com_redirect')->get('mode', 0) == true) - { - $form->setFieldAttribute('new_url', 'required', 'false'); - $form->setFieldAttribute('header', 'required', 'true'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_redirect.edit.link.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_redirect.link', $data); - - return $data; - } - - /** - * Method to activate links. - * - * @param array &$pks An array of link ids. - * @param string $url The new URL to set for the redirect. - * @param string $comment A comment for the redirect links. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - */ - public function activate(&$pks, $url, $comment = null) - { - $user = Factory::getUser(); - $db = $this->getDatabase(); - - // Sanitize the ids. - $pks = (array) $pks; - $pks = ArrayHelper::toInteger($pks); - - // Populate default comment if necessary. - $comment = (!empty($comment)) ? $comment : Text::sprintf('COM_REDIRECT_REDIRECTED_ON', HTMLHelper::_('date', time())); - - // Access checks. - if (!$user->authorise('core.edit', 'com_redirect')) - { - $pks = array(); - $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED')); - - return false; - } - - if (!empty($pks)) - { - // Update the link rows. - $query = $db->getQuery(true) - ->update($db->quoteName('#__redirect_links')) - ->set($db->quoteName('new_url') . ' = :url') - ->set($db->quoteName('published') . ' = 1') - ->set($db->quoteName('comment') . ' = :comment') - ->whereIn($db->quoteName('id'), $pks) - ->bind(':url', $url) - ->bind(':comment', $comment); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - return true; - } - - /** - * Method to batch update URLs to have new redirect urls and comments. Note will publish any unpublished URLs. - * - * @param array &$pks An array of link ids. - * @param string $url The new URL to set for the redirect. - * @param string $comment A comment for the redirect links. - * - * @return boolean Returns true on success, false on failure. - * - * @since 3.6.0 - */ - public function duplicateUrls(&$pks, $url, $comment = null) - { - $user = Factory::getUser(); - $db = $this->getDatabase(); - - // Sanitize the ids. - $pks = (array) $pks; - $pks = ArrayHelper::toInteger($pks); - - // Access checks. - if (!$user->authorise('core.edit', 'com_redirect')) - { - $pks = array(); - $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED')); - - return false; - } - - if (!empty($pks)) - { - $date = Factory::getDate()->toSql(); - - // Update the link rows. - $query = $db->getQuery(true) - ->update($db->quoteName('#__redirect_links')) - ->set($db->quoteName('new_url') . ' = :url') - ->set($db->quoteName('modified_date') . ' = :date') - ->set($db->quoteName('published') . ' = 1') - ->whereIn($db->quoteName('id'), $pks) - ->bind(':url', $url) - ->bind(':date', $date); - - if (!empty($comment)) - { - $query->set($db->quoteName('comment') . ' = ' . $db->quote($comment)); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - return true; - } + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_REDIRECT'; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if ($record->published != -2) { + return false; + } + + return parent::canDelete($record); + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form A JForm object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_redirect.link', 'link', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if ($this->canEditState((object) $data) != true) { + // Disable fields for display. + $form->setFieldAttribute('published', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + // If in advanced mode then we make sure the new URL field is not compulsory and the header + // field compulsory in case people select non-3xx redirects + if (ComponentHelper::getParams('com_redirect')->get('mode', 0) == true) { + $form->setFieldAttribute('new_url', 'required', 'false'); + $form->setFieldAttribute('header', 'required', 'true'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_redirect.edit.link.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_redirect.link', $data); + + return $data; + } + + /** + * Method to activate links. + * + * @param array &$pks An array of link ids. + * @param string $url The new URL to set for the redirect. + * @param string $comment A comment for the redirect links. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + */ + public function activate(&$pks, $url, $comment = null) + { + $user = Factory::getUser(); + $db = $this->getDatabase(); + + // Sanitize the ids. + $pks = (array) $pks; + $pks = ArrayHelper::toInteger($pks); + + // Populate default comment if necessary. + $comment = (!empty($comment)) ? $comment : Text::sprintf('COM_REDIRECT_REDIRECTED_ON', HTMLHelper::_('date', time())); + + // Access checks. + if (!$user->authorise('core.edit', 'com_redirect')) { + $pks = array(); + $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED')); + + return false; + } + + if (!empty($pks)) { + // Update the link rows. + $query = $db->getQuery(true) + ->update($db->quoteName('#__redirect_links')) + ->set($db->quoteName('new_url') . ' = :url') + ->set($db->quoteName('published') . ' = 1') + ->set($db->quoteName('comment') . ' = :comment') + ->whereIn($db->quoteName('id'), $pks) + ->bind(':url', $url) + ->bind(':comment', $comment); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + return true; + } + + /** + * Method to batch update URLs to have new redirect urls and comments. Note will publish any unpublished URLs. + * + * @param array &$pks An array of link ids. + * @param string $url The new URL to set for the redirect. + * @param string $comment A comment for the redirect links. + * + * @return boolean Returns true on success, false on failure. + * + * @since 3.6.0 + */ + public function duplicateUrls(&$pks, $url, $comment = null) + { + $user = Factory::getUser(); + $db = $this->getDatabase(); + + // Sanitize the ids. + $pks = (array) $pks; + $pks = ArrayHelper::toInteger($pks); + + // Access checks. + if (!$user->authorise('core.edit', 'com_redirect')) { + $pks = array(); + $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED')); + + return false; + } + + if (!empty($pks)) { + $date = Factory::getDate()->toSql(); + + // Update the link rows. + $query = $db->getQuery(true) + ->update($db->quoteName('#__redirect_links')) + ->set($db->quoteName('new_url') . ' = :url') + ->set($db->quoteName('modified_date') . ' = :date') + ->set($db->quoteName('published') . ' = 1') + ->whereIn($db->quoteName('id'), $pks) + ->bind(':url', $url) + ->bind(':date', $date); + + if (!empty($comment)) { + $query->set($db->quoteName('comment') . ' = ' . $db->quote($comment)); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + return true; + } } diff --git a/administrator/components/com_redirect/src/Model/LinksModel.php b/administrator/components/com_redirect/src/Model/LinksModel.php index de012229f9b7d..486e35e246aa9 100644 --- a/administrator/components/com_redirect/src/Model/LinksModel.php +++ b/administrator/components/com_redirect/src/Model/LinksModel.php @@ -1,4 +1,5 @@ getDatabase(); - - $query = $db->getQuery(true); - - $query->delete('#__redirect_links')->where($db->quoteName('published') . '= 0'); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\Exception $e) - { - return false; - } - - return true; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.old_url', $direction = 'asc') - { - // Load the parameters. - $params = ComponentHelper::getParams('com_redirect'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.http_status'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.*' - ) - ); - $query->from($db->quoteName('#__redirect_links', 'a')); - - // Filter by published state - $state = (string) $this->getState('filter.state'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.published') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif ($state === '') - { - $query->whereIn($db->quoteName('a.published'), [0,1]); - } - - // Filter the items over the HTTP status code header. - if ($httpStatusCode = $this->getState('filter.http_status')) - { - $httpStatusCode = (int) $httpStatusCode; - $query->where($db->quoteName('a.header') . ' = :header') - ->bind(':header', $httpStatusCode, ParameterType::INTEGER); - } - - // Filter the items over the search string if set. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%'); - $query->where( - '(' . $db->quoteName('old_url') . ' LIKE :oldurl' - . ' OR ' . $db->quoteName('new_url') . ' LIKE :newurl' - . ' OR ' . $db->quoteName('comment') . ' LIKE :comment' - . ' OR ' . $db->quoteName('referer') . ' LIKE :referer)' - ) - ->bind(':oldurl', $search) - ->bind(':newurl', $search) - ->bind(':comment', $search) - ->bind(':referer', $search); - } - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.old_url')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Add the entered URLs into the database - * - * @param array $batchUrls Array of URLs to enter into the database - * - * @return boolean - */ - public function batchProcess($batchUrls) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $params = ComponentHelper::getParams('com_redirect'); - $state = (int) $params->get('defaultImportState', 0); - $created = Factory::getDate()->toSql(); - - $columns = [ - 'old_url', - 'new_url', - 'referer', - 'comment', - 'hits', - 'published', - 'created_date', - 'modified_date', - ]; - - $values = [ - ':oldurl', - ':newurl', - $db->quote(''), - $db->quote(''), - 0, - ':state', - ':created', - ':modified', - ]; - - $query - ->insert($db->quoteName('#__redirect_links'), false) - ->columns($db->quoteName($columns)) - ->values(implode(', ', $values)) - ->bind(':oldurl', $old_url) - ->bind(':newurl', $new_url) - ->bind(':state', $state, ParameterType::INTEGER) - ->bind(':created', $created) - ->bind(':modified', $created); - - $db->setQuery($query); - - foreach ($batchUrls as $batch_url) - { - $old_url = $batch_url[0]; - - // Destination URL can also be an external URL - if (!empty($batch_url[1])) - { - $new_url = $batch_url[1]; - } - else - { - $new_url = ''; - } - - $db->execute(); - } - - return true; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'state', 'a.state', + 'old_url', 'a.old_url', + 'new_url', 'a.new_url', + 'referer', 'a.referer', + 'hits', 'a.hits', + 'created_date', 'a.created_date', + 'published', 'a.published', + 'header', 'a.header', 'http_status', + ); + } + + parent::__construct($config, $factory); + } + /** + * Removes all of the unpublished redirects from the table. + * + * @return boolean result of operation + * + * @since 3.5 + */ + public function purge() + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true); + + $query->delete('#__redirect_links')->where($db->quoteName('published') . '= 0'); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\Exception $e) { + return false; + } + + return true; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.old_url', $direction = 'asc') + { + // Load the parameters. + $params = ComponentHelper::getParams('com_redirect'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.http_status'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.*' + ) + ); + $query->from($db->quoteName('#__redirect_links', 'a')); + + // Filter by published state + $state = (string) $this->getState('filter.state'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.published') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif ($state === '') { + $query->whereIn($db->quoteName('a.published'), [0,1]); + } + + // Filter the items over the HTTP status code header. + if ($httpStatusCode = $this->getState('filter.http_status')) { + $httpStatusCode = (int) $httpStatusCode; + $query->where($db->quoteName('a.header') . ' = :header') + ->bind(':header', $httpStatusCode, ParameterType::INTEGER); + } + + // Filter the items over the search string if set. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%'); + $query->where( + '(' . $db->quoteName('old_url') . ' LIKE :oldurl' + . ' OR ' . $db->quoteName('new_url') . ' LIKE :newurl' + . ' OR ' . $db->quoteName('comment') . ' LIKE :comment' + . ' OR ' . $db->quoteName('referer') . ' LIKE :referer)' + ) + ->bind(':oldurl', $search) + ->bind(':newurl', $search) + ->bind(':comment', $search) + ->bind(':referer', $search); + } + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.old_url')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Add the entered URLs into the database + * + * @param array $batchUrls Array of URLs to enter into the database + * + * @return boolean + */ + public function batchProcess($batchUrls) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $params = ComponentHelper::getParams('com_redirect'); + $state = (int) $params->get('defaultImportState', 0); + $created = Factory::getDate()->toSql(); + + $columns = [ + 'old_url', + 'new_url', + 'referer', + 'comment', + 'hits', + 'published', + 'created_date', + 'modified_date', + ]; + + $values = [ + ':oldurl', + ':newurl', + $db->quote(''), + $db->quote(''), + 0, + ':state', + ':created', + ':modified', + ]; + + $query + ->insert($db->quoteName('#__redirect_links'), false) + ->columns($db->quoteName($columns)) + ->values(implode(', ', $values)) + ->bind(':oldurl', $old_url) + ->bind(':newurl', $new_url) + ->bind(':state', $state, ParameterType::INTEGER) + ->bind(':created', $created) + ->bind(':modified', $created); + + $db->setQuery($query); + + foreach ($batchUrls as $batch_url) { + $old_url = $batch_url[0]; + + // Destination URL can also be an external URL + if (!empty($batch_url[1])) { + $new_url = $batch_url[1]; + } else { + $new_url = ''; + } + + $db->execute(); + } + + return true; + } } diff --git a/administrator/components/com_redirect/src/Service/HTML/Redirect.php b/administrator/components/com_redirect/src/Service/HTML/Redirect.php index c7dfcb0da4690..4c38772af6063 100644 --- a/administrator/components/com_redirect/src/Service/HTML/Redirect.php +++ b/administrator/components/com_redirect/src/Service/HTML/Redirect.php @@ -1,4 +1,5 @@ array('publish', 'links.unpublish', 'JENABLED', 'COM_REDIRECT_DISABLE_LINK'), - 0 => array('unpublish', 'links.publish', 'JDISABLED', 'COM_REDIRECT_ENABLE_LINK'), - 2 => array('archive', 'links.unpublish', 'JARCHIVED', 'JUNARCHIVE'), - -2 => array('trash', 'links.publish', 'JTRASHED', 'COM_REDIRECT_ENABLE_LINK'), - ); + // Array of image, task, title, action + $states = array( + 1 => array('publish', 'links.unpublish', 'JENABLED', 'COM_REDIRECT_DISABLE_LINK'), + 0 => array('unpublish', 'links.publish', 'JDISABLED', 'COM_REDIRECT_ENABLE_LINK'), + 2 => array('archive', 'links.unpublish', 'JARCHIVED', 'JUNARCHIVE'), + -2 => array('trash', 'links.publish', 'JTRASHED', 'COM_REDIRECT_ENABLE_LINK'), + ); - $state = ArrayHelper::getValue($states, (int) $value, $states[0]); - $icon = $state[0]; + $state = ArrayHelper::getValue($states, (int) $value, $states[0]); + $icon = $state[0]; - if ($canChange) - { - $html = '' - . ''; - } + if ($canChange) { + $html = '' + . ''; + } - return $html; - } + return $html; + } } diff --git a/administrator/components/com_redirect/src/Table/LinkTable.php b/administrator/components/com_redirect/src/Table/LinkTable.php index fdc4ced7c4015..fe6f26aa957f4 100644 --- a/administrator/components/com_redirect/src/Table/LinkTable.php +++ b/administrator/components/com_redirect/src/Table/LinkTable.php @@ -1,4 +1,5 @@ setError($e->getMessage()); - - return false; - } - - $this->old_url = trim(rawurldecode($this->old_url)); - $this->new_url = trim(rawurldecode($this->new_url)); - - // Check for valid name. - if (empty($this->old_url)) - { - $this->setError(Text::_('COM_REDIRECT_ERROR_SOURCE_URL_REQUIRED')); - - return false; - } - - // Check for NOT NULL. - if (empty($this->referer)) - { - $this->referer = ''; - } - - // Check for valid name if not in advanced mode. - if (empty($this->new_url) && ComponentHelper::getParams('com_redirect')->get('mode', 0) == false) - { - $this->setError(Text::_('COM_REDIRECT_ERROR_DESTINATION_URL_REQUIRED')); - - return false; - } - elseif (empty($this->new_url) && ComponentHelper::getParams('com_redirect')->get('mode', 0) == true) - { - // Else if an empty URL and in redirect mode only throw the same error if the code is a 3xx status code - if ($this->header < 400 && $this->header >= 300) - { - $this->setError(Text::_('COM_REDIRECT_ERROR_DESTINATION_URL_REQUIRED')); - - return false; - } - } - - // Check for duplicates - if ($this->old_url == $this->new_url) - { - $this->setError(Text::_('COM_REDIRECT_ERROR_DUPLICATE_URLS')); - - return false; - } - - $db = $this->getDbo(); - - // Check for existing name - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->select($db->quoteName('old_url')) - ->from($db->quoteName('#__redirect_links')) - ->where($db->quoteName('old_url') . ' = :url') - ->bind(':url', $this->old_url); - $db->setQuery($query); - $urls = $db->loadAssocList(); - - foreach ($urls as $url) - { - if ($url['old_url'] === $this->old_url && (int) $url['id'] != (int) $this->id) - { - $this->setError(Text::_('COM_REDIRECT_ERROR_DUPLICATE_OLD_URL')); - - return false; - } - } - - return true; - } - - /** - * Overridden store method to set dates. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function store($updateNulls = false) - { - $date = Factory::getDate()->toSql(); - - if (!$this->id) - { - // New record. - $this->created_date = $date; - $this->modified_date = $date; - } - - if (empty($this->modified_date)) - { - $this->modified_date = $this->created_date; - } - - return parent::store($updateNulls); - } + /** + * Constructor + * + * @param DatabaseDriver $db Database object. + * + * @since 1.6 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__redirect_links', 'id', $db); + } + + /** + * Overloaded check function + * + * @return boolean + * + * @since 1.6 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $this->old_url = trim(rawurldecode($this->old_url)); + $this->new_url = trim(rawurldecode($this->new_url)); + + // Check for valid name. + if (empty($this->old_url)) { + $this->setError(Text::_('COM_REDIRECT_ERROR_SOURCE_URL_REQUIRED')); + + return false; + } + + // Check for NOT NULL. + if (empty($this->referer)) { + $this->referer = ''; + } + + // Check for valid name if not in advanced mode. + if (empty($this->new_url) && ComponentHelper::getParams('com_redirect')->get('mode', 0) == false) { + $this->setError(Text::_('COM_REDIRECT_ERROR_DESTINATION_URL_REQUIRED')); + + return false; + } elseif (empty($this->new_url) && ComponentHelper::getParams('com_redirect')->get('mode', 0) == true) { + // Else if an empty URL and in redirect mode only throw the same error if the code is a 3xx status code + if ($this->header < 400 && $this->header >= 300) { + $this->setError(Text::_('COM_REDIRECT_ERROR_DESTINATION_URL_REQUIRED')); + + return false; + } + } + + // Check for duplicates + if ($this->old_url == $this->new_url) { + $this->setError(Text::_('COM_REDIRECT_ERROR_DUPLICATE_URLS')); + + return false; + } + + $db = $this->getDbo(); + + // Check for existing name + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->select($db->quoteName('old_url')) + ->from($db->quoteName('#__redirect_links')) + ->where($db->quoteName('old_url') . ' = :url') + ->bind(':url', $this->old_url); + $db->setQuery($query); + $urls = $db->loadAssocList(); + + foreach ($urls as $url) { + if ($url['old_url'] === $this->old_url && (int) $url['id'] != (int) $this->id) { + $this->setError(Text::_('COM_REDIRECT_ERROR_DUPLICATE_OLD_URL')); + + return false; + } + } + + return true; + } + + /** + * Overridden store method to set dates. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function store($updateNulls = false) + { + $date = Factory::getDate()->toSql(); + + if (!$this->id) { + // New record. + $this->created_date = $date; + $this->modified_date = $date; + } + + if (empty($this->modified_date)) { + $this->modified_date = $this->created_date; + } + + return parent::store($updateNulls); + } } diff --git a/administrator/components/com_redirect/src/View/Link/HtmlView.php b/administrator/components/com_redirect/src/View/Link/HtmlView.php index c20f83e059c50..9d0c3a8b0355a 100644 --- a/administrator/components/com_redirect/src/View/Link/HtmlView.php +++ b/administrator/components/com_redirect/src/View/Link/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $isNew = ($this->item->id == 0); - $canDo = ContentHelper::getActions('com_redirect'); - - ToolbarHelper::title($isNew ? Text::_('COM_REDIRECT_MANAGER_LINK_NEW') : Text::_('COM_REDIRECT_MANAGER_LINK_EDIT'), 'map-signs redirect'); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('link.apply'); - $toolbarButtons[] = ['save', 'link.save']; - } - - /** - * This component does not support Save as Copy due to uniqueness checks. - * While it can be done, it causes too much confusion if the user does - * not change the Old URL. - */ - if ($canDo->get('core.edit') && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'link.save2new']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('link.cancel'); - } - else - { - ToolbarHelper::cancel('link.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::help('Redirects:_New_or_Edit'); - } + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed False if unsuccessful, otherwise void. + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $isNew = ($this->item->id == 0); + $canDo = ContentHelper::getActions('com_redirect'); + + ToolbarHelper::title($isNew ? Text::_('COM_REDIRECT_MANAGER_LINK_NEW') : Text::_('COM_REDIRECT_MANAGER_LINK_EDIT'), 'map-signs redirect'); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('link.apply'); + $toolbarButtons[] = ['save', 'link.save']; + } + + /** + * This component does not support Save as Copy due to uniqueness checks. + * While it can be done, it causes too much confusion if the user does + * not change the Old URL. + */ + if ($canDo->get('core.edit') && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'link.save2new']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('link.cancel'); + } else { + ToolbarHelper::cancel('link.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::help('Redirects:_New_or_Edit'); + } } diff --git a/administrator/components/com_redirect/src/View/Links/HtmlView.php b/administrator/components/com_redirect/src/View/Links/HtmlView.php index f04a0b8c8f45d..30c74178ec129 100644 --- a/administrator/components/com_redirect/src/View/Links/HtmlView.php +++ b/administrator/components/com_redirect/src/View/Links/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->params = ComponentHelper::getParams('com_redirect'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - if (!(PluginHelper::isEnabled('system', 'redirect') && RedirectHelper::collectUrlsEnabled())) - { - $this->redirectPluginId = RedirectHelper::getRedirectPluginId(); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $state = $this->get('State'); - $canDo = ContentHelper::getActions('com_redirect'); - - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_REDIRECT_MANAGER_LINKS'), 'map-signs redirect'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('link.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($state->get('filter.state') != 2) - { - $childBar->publish('links.publish', 'JTOOLBAR_ENABLE')->listCheck(true); - $childBar->unpublish('links.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); - } - - if ($state->get('filter.state') != -1) - { - if ($state->get('filter.state') != 2) - { - $childBar->archive('links.archive')->listCheck(true); - } - elseif ($state->get('filter.state') == 2) - { - $childBar->unarchive('links.unarchive')->listCheck(true); - } - } - - if (!$state->get('filter.state') == -2) - { - $childBar->trash('links.trash')->listCheck(true); - } - } - - if ($state->get('filter.state') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('links.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if (!$this->isEmptyState && (!$state->get('filter.state') == -2 && $canDo->get('core.delete'))) - { - $toolbar->confirmButton('delete') - ->text('COM_REDIRECT_TOOLBAR_PURGE') - ->message('COM_REDIRECT_CONFIRM_PURGE') - ->task('links.purge'); - } - - if ($canDo->get('core.create')) - { - $toolbar->popupButton('batch') - ->text('JTOOLBAR_BULK_IMPORT') - ->selector('collapseModal') - ->listCheck(false); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_redirect'); - } - - $toolbar->help('Redirects:_Links'); - } + /** + * True if "System - Redirect Plugin" is enabled + * + * @var boolean + */ + protected $enabled; + + /** + * True if "Collect URLs" is enabled + * + * @var boolean + */ + protected $collect_urls_enabled; + + /** + * The id of the redirect plugin in mysql + * + * @var integer + * @since 3.8.0 + */ + protected $redirectPluginId = 0; + + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * The model state + * + * @var \Joomla\Registry\Registry + */ + protected $params; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @throws GenericDataException + * @since 1.6 + */ + public function display($tpl = null) + { + // Set variables + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->params = ComponentHelper::getParams('com_redirect'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + if (!(PluginHelper::isEnabled('system', 'redirect') && RedirectHelper::collectUrlsEnabled())) { + $this->redirectPluginId = RedirectHelper::getRedirectPluginId(); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $state = $this->get('State'); + $canDo = ContentHelper::getActions('com_redirect'); + + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_REDIRECT_MANAGER_LINKS'), 'map-signs redirect'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('link.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($state->get('filter.state') != 2) { + $childBar->publish('links.publish', 'JTOOLBAR_ENABLE')->listCheck(true); + $childBar->unpublish('links.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); + } + + if ($state->get('filter.state') != -1) { + if ($state->get('filter.state') != 2) { + $childBar->archive('links.archive')->listCheck(true); + } elseif ($state->get('filter.state') == 2) { + $childBar->unarchive('links.unarchive')->listCheck(true); + } + } + + if (!$state->get('filter.state') == -2) { + $childBar->trash('links.trash')->listCheck(true); + } + } + + if ($state->get('filter.state') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('links.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if (!$this->isEmptyState && (!$state->get('filter.state') == -2 && $canDo->get('core.delete'))) { + $toolbar->confirmButton('delete') + ->text('COM_REDIRECT_TOOLBAR_PURGE') + ->message('COM_REDIRECT_CONFIRM_PURGE') + ->task('links.purge'); + } + + if ($canDo->get('core.create')) { + $toolbar->popupButton('batch') + ->text('JTOOLBAR_BULK_IMPORT') + ->selector('collapseModal') + ->listCheck(false); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_redirect'); + } + + $toolbar->help('Redirects:_Links'); + } } diff --git a/administrator/components/com_redirect/tmpl/link/edit.php b/administrator/components/com_redirect/tmpl/link/edit.php index 9f3320aabdcb1..9117e79dbd40e 100644 --- a/administrator/components/com_redirect/tmpl/link/edit.php +++ b/administrator/components/com_redirect/tmpl/link/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?> diff --git a/administrator/components/com_redirect/tmpl/links/default.php b/administrator/components/com_redirect/tmpl/links/default.php index 8be568281c1e4..c681dae183adb 100644 --- a/administrator/components/com_redirect/tmpl/links/default.php +++ b/administrator/components/com_redirect/tmpl/links/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?>
    -
    - $this)); ?> - redirectPluginId) : ?> - redirectPluginId . '&tmpl=component&layout=modal'); ?> - redirectPluginId . 'Modal', - array( - 'url' => $link, - 'title' => Text::_('COM_REDIRECT_EDIT_PLUGIN_SETTINGS'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'closeButton' => false, - 'backdrop' => 'static', - 'keyboard' => false, - 'footer' => '' - . '' - . '' - ) - ); ?> - +
    + $this)); ?> + redirectPluginId) : ?> + redirectPluginId . '&tmpl=component&layout=modal'); ?> + redirectPluginId . 'Modal', + array( + 'url' => $link, + 'title' => Text::_('COM_REDIRECT_EDIT_PLUGIN_SETTINGS'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'closeButton' => false, + 'backdrop' => 'static', + 'keyboard' => false, + 'footer' => '' + . '' + . '' + ) + ); ?> + - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - - - items as $i => $item) : - $canEdit = $user->authorise('core.edit', 'com_redirect'); - $canChange = $user->authorise('core.edit.state', 'com_redirect'); - ?> - - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - - - - - -
    - id, false, 'cid', 'cb', $item->old_url); ?> - - published, $i); ?> - - - - escape(str_replace(Uri::root(), '', rawurldecode($item->old_url))); ?> - - - escape(str_replace(Uri::root(), '', rawurldecode($item->old_url))); ?> - - - escape(rawurldecode($item->new_url)); ?> - - escape($item->referer); ?> - - created_date, Text::_('DATE_FORMAT_LC4')); ?> - - hits; ?> - - header; ?> - - id; ?> -
    + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + + + items as $i => $item) : + $canEdit = $user->authorise('core.edit', 'com_redirect'); + $canChange = $user->authorise('core.edit.state', 'com_redirect'); + ?> + + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + + + + + +
    + id, false, 'cid', 'cb', $item->old_url); ?> + + published, $i); ?> + + + + escape(str_replace(Uri::root(), '', rawurldecode($item->old_url))); ?> + + + escape(str_replace(Uri::root(), '', rawurldecode($item->old_url))); ?> + + + escape(rawurldecode($item->new_url)); ?> + + escape($item->referer); ?> + + created_date, Text::_('DATE_FORMAT_LC4')); ?> + + hits; ?> + + header; ?> + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - items)) : ?> - loadTemplate('addform'); ?> - - - authorise('core.create', 'com_redirect') - && $user->authorise('core.edit', 'com_redirect') - && $user->authorise('core.edit.state', 'com_redirect')) : ?> - Text::_('COM_REDIRECT_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - + items)) : ?> + loadTemplate('addform'); ?> + + + authorise('core.create', 'com_redirect') + && $user->authorise('core.edit', 'com_redirect') + && $user->authorise('core.edit.state', 'com_redirect') +) : ?> + Text::_('COM_REDIRECT_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + - - - -
    + + + +
    diff --git a/administrator/components/com_redirect/tmpl/links/default_addform.php b/administrator/components/com_redirect/tmpl/links/default_addform.php index 186b46583bd8d..6510ef5016108 100644 --- a/administrator/components/com_redirect/tmpl/links/default_addform.php +++ b/administrator/components/com_redirect/tmpl/links/default_addform.php @@ -1,4 +1,5 @@
    -
    - -
    -
    -
    -
    -
    -
    - -
    -
    - - - - -
    -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    + + + + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    diff --git a/administrator/components/com_redirect/tmpl/links/default_batch_body.php b/administrator/components/com_redirect/tmpl/links/default_batch_body.php index 58ff121d215a0..052eef41afe42 100644 --- a/administrator/components/com_redirect/tmpl/links/default_batch_body.php +++ b/administrator/components/com_redirect/tmpl/links/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; @@ -16,12 +18,12 @@ ?>
    -
    -
    - -
    - -
    -
    -
    +
    +
    + +
    + +
    +
    +
    diff --git a/administrator/components/com_redirect/tmpl/links/default_batch_footer.php b/administrator/components/com_redirect/tmpl/links/default_batch_footer.php index 08b76e82053da..acd5826500778 100644 --- a/administrator/components/com_redirect/tmpl/links/default_batch_footer.php +++ b/administrator/components/com_redirect/tmpl/links/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/administrator/components/com_redirect/tmpl/links/emptystate.php b/administrator/components/com_redirect/tmpl/links/emptystate.php index 705387dd82823..752d956a46dc2 100644 --- a/administrator/components/com_redirect/tmpl/links/emptystate.php +++ b/administrator/components/com_redirect/tmpl/links/emptystate.php @@ -1,4 +1,5 @@ 'COM_REDIRECT', - 'formURL' => 'index.php?option=com_redirect&view=links', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Redirects:_Links', - 'icon' => 'icon-map-signs redirect', + 'textPrefix' => 'COM_REDIRECT', + 'formURL' => 'index.php?option=com_redirect&view=links', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Redirects:_Links', + 'icon' => 'icon-map-signs redirect', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_redirect')) -{ - $displayData['createURL'] = 'index.php?option=com_redirect&task=link.add'; +if ($user->authorise('core.create', 'com_redirect')) { + $displayData['createURL'] = 'index.php?option=com_redirect&task=link.add'; } -if ($user->authorise('core.create', 'com_redirect') - && $user->authorise('core.edit', 'com_redirect') - && $user->authorise('core.edit.state', 'com_redirect')) -{ - $displayData['formAppend'] = HTMLHelper::_( - 'bootstrap.renderModal', - 'collapseModal', - [ - 'title' => Text::_('COM_REDIRECT_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ], - $this->loadTemplate('batch_body') - ); +if ( + $user->authorise('core.create', 'com_redirect') + && $user->authorise('core.edit', 'com_redirect') + && $user->authorise('core.edit.state', 'com_redirect') +) { + $displayData['formAppend'] = HTMLHelper::_( + 'bootstrap.renderModal', + 'collapseModal', + [ + 'title' => Text::_('COM_REDIRECT_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ], + $this->loadTemplate('batch_body') + ); } ?> redirectPluginId) : ?> - redirectPluginId . '&tmpl=component&layout=modal'); ?> - redirectPluginId . 'Modal', - array( - 'url' => $link, - 'title' => Text::_('COM_REDIRECT_EDIT_PLUGIN_SETTINGS'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'closeButton' => false, - 'backdrop' => 'static', - 'keyboard' => false, - 'footer' => '' - . '' - . '' - ) - ); ?> + redirectPluginId . '&tmpl=component&layout=modal'); ?> + redirectPluginId . 'Modal', + array( + 'url' => $link, + 'title' => Text::_('COM_REDIRECT_EDIT_PLUGIN_SETTINGS'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'closeButton' => false, + 'backdrop' => 'static', + 'keyboard' => false, + 'footer' => '' + . '' + . '' + ) + ); ?>
    - - + +
    diff --git a/administrator/components/com_scheduler/services/provider.php b/administrator/components/com_scheduler/services/provider.php index 4511f0be44ed0..ded0221067825 100644 --- a/administrator/components/com_scheduler/services/provider.php +++ b/administrator/components/com_scheduler/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Scheduler')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Scheduler')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.1.0 + */ + public function register(Container $container) + { + /** + * Register the MVCFactory and ComponentDispatcherFactory providers to map + * 'MVCFactoryInterface' and 'ComponentDispatcherFactoryInterface' to their + * initializers and register them with the component's DI container. + */ + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Scheduler')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Scheduler')); - $container->set( - ComponentInterface::class, - function (Container $container) { - $component = new SchedulerComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new SchedulerComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_scheduler/src/Controller/DisplayController.php b/administrator/components/com_scheduler/src/Controller/DisplayController.php index eb79b7f7837a9..848c16d478321 100644 --- a/administrator/components/com_scheduler/src/Controller/DisplayController.php +++ b/administrator/components/com_scheduler/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('layout', 'default'); + /** + * @var string + * @since 4.1.0 + */ + protected $default_view = 'tasks'; - // Check for edit form. - if ($layout === 'edit') - { - if (!$this->validateEntry()) - { - $tasksViewUrl = Route::_('index.php?option=com_scheduler&view=tasks', false); - $this->setRedirect($tasksViewUrl); + /** + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe url parameters and their variable types, for valid values see + * {@link InputFilter::clean()}. + * + * @return BaseController|boolean Returns either a BaseController object to support chaining, or false on failure + * + * @since 4.1.0 + * @throws \Exception + */ + public function display($cachable = false, $urlparams = array()) + { + $layout = $this->input->get('layout', 'default'); - return false; - } - } + // Check for edit form. + if ($layout === 'edit') { + if (!$this->validateEntry()) { + $tasksViewUrl = Route::_('index.php?option=com_scheduler&view=tasks', false); + $this->setRedirect($tasksViewUrl); - // Let the parent method take over - return parent::display($cachable, $urlparams); - } + return false; + } + } - /** - * Validates entry to the view - * - * @param string $layout The layout to validate entry for (defaults to 'edit') - * - * @return boolean True is entry is valid - * - * @since 4.1.0 - */ - private function validateEntry(string $layout = 'edit'): bool - { - $context = 'com_scheduler'; - $id = $this->input->getInt('id'); - $isValid = true; + // Let the parent method take over + return parent::display($cachable, $urlparams); + } - switch ($layout) - { - case 'edit': + /** + * Validates entry to the view + * + * @param string $layout The layout to validate entry for (defaults to 'edit') + * + * @return boolean True is entry is valid + * + * @since 4.1.0 + */ + private function validateEntry(string $layout = 'edit'): bool + { + $context = 'com_scheduler'; + $id = $this->input->getInt('id'); + $isValid = true; - // True if controller was called and verified permissions - $inEditList = $this->checkEditId("$context.edit.task", $id); - $isNew = ($id == 0); + switch ($layout) { + case 'edit': + // True if controller was called and verified permissions + $inEditList = $this->checkEditId("$context.edit.task", $id); + $isNew = ($id == 0); - // For new item, entry is invalid if task type was not selected through SelectView - if ($isNew && !$this->app->getUserState("$context.add.task.task_type")) - { - $this->setMessage((Text::_('COM_SCHEDULER_ERROR_FORBIDDEN_JUMP_TO_ADD_VIEW')), 'error'); - $isValid = false; - } - // For existing item, entry is invalid if TaskController has not granted access - elseif (!$inEditList) - { - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // For new item, entry is invalid if task type was not selected through SelectView + if ($isNew && !$this->app->getUserState("$context.add.task.task_type")) { + $this->setMessage((Text::_('COM_SCHEDULER_ERROR_FORBIDDEN_JUMP_TO_ADD_VIEW')), 'error'); + $isValid = false; + } + // For existing item, entry is invalid if TaskController has not granted access + elseif (!$inEditList) { + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $isValid = false; - } - break; - default: - break; - } + $isValid = false; + } + break; + default: + break; + } - return $isValid; - } + return $isValid; + } } diff --git a/administrator/components/com_scheduler/src/Controller/TaskController.php b/administrator/components/com_scheduler/src/Controller/TaskController.php index 50e3409abaf76..cad465b60271b 100644 --- a/administrator/components/com_scheduler/src/Controller/TaskController.php +++ b/administrator/components/com_scheduler/src/Controller/TaskController.php @@ -1,4 +1,5 @@ app; - $input = $app->getInput(); - $validTaskOptions = SchedulerHelper::getTaskOptions(); - - $canAdd = parent::add(); - - if ($canAdd !== true) - { - return false; - } - - $taskType = $input->get('type'); - $taskOption = $validTaskOptions->findOption($taskType) ?: null; - - if (!$taskOption) - { - // ? : Is this the right redirect [review] - $redirectUrl = 'index.php?option=' . $this->option . '&view=select&layout=edit'; - $this->setRedirect(Route::_($redirectUrl, false)); - $app->enqueueMessage(Text::_('COM_SCHEDULER_ERROR_INVALID_TASK_TYPE'), 'warning'); - $canAdd = false; - } - - $app->setUserState('com_scheduler.add.task.task_type', $taskType); - $app->setUserState('com_scheduler.add.task.task_option', $taskOption); - - // @todo : Parameter array handling below? - - return $canAdd; - } - - /** - * Override parent cancel method to reset the add task state - * - * @param ?string $key Primary key from the URL param - * - * @return boolean True if access level checks pass - * - * @since 4.1.0 - */ - public function cancel($key = null): bool - { - $result = parent::cancel($key); - - $this->app->setUserState('com_scheduler.add.task.task_type', null); - $this->app->setUserState('com_scheduler.add.task.task_option', null); - - // ? Do we need to redirect based on URL's 'return' param? {@see ModuleController} - - return $result; - } - - /** - * Check if user has the authority to edit an asset - * - * @param array $data Array of input data - * @param string $key Name of key for primary key, defaults to 'id' - * - * @return boolean True if user is allowed to edit record - * - * @since 4.1.0 - */ - protected function allowEdit($data = array(), $key = 'id'): bool - { - // Extract the recordId from $data, will come in handy - $recordId = (int) $data[$key] ?? 0; - - /** - * Zero record (id:0), return component edit permission by calling parent controller method - * ?: Is this the right way to do this? - */ - if ($recordId === 0) - { - return parent::allowEdit($data, $key); - } - - // @todo : Check if this works as expected - return $this->app->getIdentity()->authorise('core.edit', 'com_scheduler.task.' . $recordId); - - } + /** + * Add a new record + * + * @return boolean + * @since 4.1.0 + * @throws \Exception + */ + public function add(): bool + { + /** @var AdministratorApplication $app */ + $app = $this->app; + $input = $app->getInput(); + $validTaskOptions = SchedulerHelper::getTaskOptions(); + + $canAdd = parent::add(); + + if ($canAdd !== true) { + return false; + } + + $taskType = $input->get('type'); + $taskOption = $validTaskOptions->findOption($taskType) ?: null; + + if (!$taskOption) { + // ? : Is this the right redirect [review] + $redirectUrl = 'index.php?option=' . $this->option . '&view=select&layout=edit'; + $this->setRedirect(Route::_($redirectUrl, false)); + $app->enqueueMessage(Text::_('COM_SCHEDULER_ERROR_INVALID_TASK_TYPE'), 'warning'); + $canAdd = false; + } + + $app->setUserState('com_scheduler.add.task.task_type', $taskType); + $app->setUserState('com_scheduler.add.task.task_option', $taskOption); + + // @todo : Parameter array handling below? + + return $canAdd; + } + + /** + * Override parent cancel method to reset the add task state + * + * @param ?string $key Primary key from the URL param + * + * @return boolean True if access level checks pass + * + * @since 4.1.0 + */ + public function cancel($key = null): bool + { + $result = parent::cancel($key); + + $this->app->setUserState('com_scheduler.add.task.task_type', null); + $this->app->setUserState('com_scheduler.add.task.task_option', null); + + // ? Do we need to redirect based on URL's 'return' param? {@see ModuleController} + + return $result; + } + + /** + * Check if user has the authority to edit an asset + * + * @param array $data Array of input data + * @param string $key Name of key for primary key, defaults to 'id' + * + * @return boolean True if user is allowed to edit record + * + * @since 4.1.0 + */ + protected function allowEdit($data = array(), $key = 'id'): bool + { + // Extract the recordId from $data, will come in handy + $recordId = (int) $data[$key] ?? 0; + + /** + * Zero record (id:0), return component edit permission by calling parent controller method + * ?: Is this the right way to do this? + */ + if ($recordId === 0) { + return parent::allowEdit($data, $key); + } + + // @todo : Check if this works as expected + return $this->app->getIdentity()->authorise('core.edit', 'com_scheduler.task.' . $recordId); + } } diff --git a/administrator/components/com_scheduler/src/Controller/TasksController.php b/administrator/components/com_scheduler/src/Controller/TasksController.php index ba2252dddf16c..d2f8a22ff2305 100644 --- a/administrator/components/com_scheduler/src/Controller/TasksController.php +++ b/administrator/components/com_scheduler/src/Controller/TasksController.php @@ -1,4 +1,5 @@ true]): BaseDatabaseModel - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for the parent method. + * + * @param string $name The name of the model. + * @param string $prefix The prefix for the PHP class name. + * @param array $config Array of configuration parameters. + * + * @return BaseDatabaseModel + * + * @since 4.1.0 + */ + public function getModel($name = 'Task', $prefix = 'Administrator', $config = ['ignore_request' => true]): BaseDatabaseModel + { + return parent::getModel($name, $prefix, $config); + } - /** - * Unlock a locked task, i.e., a task that is presumably still running but might have crashed and got stuck in the - * "locked" state. - * - * @return void - * - * @since 4.1.0 - */ - public function unlock(): void - { - // Check for request forgeries - $this->checkToken(); + /** + * Unlock a locked task, i.e., a task that is presumably still running but might have crashed and got stuck in the + * "locked" state. + * + * @return void + * + * @since 4.1.0 + */ + public function unlock(): void + { + // Check for request forgeries + $this->checkToken(); - /** @var integer[] $cid Items to publish (from request parameters). */ - $cid = (array) $this->input->get('cid', [], 'int'); + /** @var integer[] $cid Items to publish (from request parameters). */ + $cid = (array) $this->input->get('cid', [], 'int'); - // Remove zero values resulting from input filter - $cid = array_filter($cid); + // Remove zero values resulting from input filter + $cid = array_filter($cid); - if (empty($cid)) - { - $this->app->getLogger() - ->warning(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), array('category' => 'jerror')); - } - else - { - /** @var TaskModel $model */ - $model = $this->getModel(); + if (empty($cid)) { + $this->app->getLogger() + ->warning(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), array('category' => 'jerror')); + } else { + /** @var TaskModel $model */ + $model = $this->getModel(); - // Make sure the item IDs are integers - $cid = ArrayHelper::toInteger($cid); + // Make sure the item IDs are integers + $cid = ArrayHelper::toInteger($cid); - // Unlock the items. - try - { - $model->unlock($cid); - $errors = $model->getErrors(); - $noticeText = null; + // Unlock the items. + try { + $model->unlock($cid); + $errors = $model->getErrors(); + $noticeText = null; - if ($errors) - { - $this->app->enqueueMessage(Text::plural($this->text_prefix . '_N_ITEMS_FAILED_UNLOCKING', \count($cid)), 'error'); - } - else - { - $noticeText = $this->text_prefix . '_N_ITEMS_UNLOCKED'; - } + if ($errors) { + $this->app->enqueueMessage(Text::plural($this->text_prefix . '_N_ITEMS_FAILED_UNLOCKING', \count($cid)), 'error'); + } else { + $noticeText = $this->text_prefix . '_N_ITEMS_UNLOCKED'; + } - if (\count($cid)) - { - $this->setMessage(Text::plural($noticeText, \count($cid))); - } - } - catch (\Exception $e) - { - $this->setMessage($e->getMessage(), 'error'); - } - } + if (\count($cid)) { + $this->setMessage(Text::plural($noticeText, \count($cid))); + } + } catch (\Exception $e) { + $this->setMessage($e->getMessage(), 'error'); + } + } - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), - false - ) - ); - } + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(), + false + ) + ); + } } diff --git a/administrator/components/com_scheduler/src/Event/ExecuteTaskEvent.php b/administrator/components/com_scheduler/src/Event/ExecuteTaskEvent.php index 31fb65508cd09..0c4b338495412 100644 --- a/administrator/components/com_scheduler/src/Event/ExecuteTaskEvent.php +++ b/administrator/components/com_scheduler/src/Event/ExecuteTaskEvent.php @@ -1,4 +1,5 @@ arguments['resultSnapshot'] = $snapshot; + /** + * Sets the task result snapshot and stops event propagation. + * + * @param array $snapshot The task snapshot. + * + * @return void + * + * @since 4.1.0 + */ + public function setResult(array $snapshot = []): void + { + $this->arguments['resultSnapshot'] = $snapshot; - if (!empty($snapshot)) - { - $this->stopPropagation(); - } - } + if (!empty($snapshot)) { + $this->stopPropagation(); + } + } - /** - * @return integer The task's taskId. - * - * @since 4.1.0 - */ - public function getTaskId(): int - { - return $this->arguments['subject']->get('id'); - } + /** + * @return integer The task's taskId. + * + * @since 4.1.0 + */ + public function getTaskId(): int + { + return $this->arguments['subject']->get('id'); + } - /** - * @return string The task's 'type'. - * - * @since 4.1.0 - */ - public function getRoutineId(): string - { - return $this->arguments['subject']->get('type'); - } + /** + * @return string The task's 'type'. + * + * @since 4.1.0 + */ + public function getRoutineId(): string + { + return $this->arguments['subject']->get('type'); + } - /** - * Returns the snapshot of the triggered task if available, else an empty array - * - * @return array The task snapshot if available, else null - * - * @since 4.1.0 - */ - public function getResultSnapshot(): array - { - return $this->arguments['resultSnapshot'] ?? []; - } + /** + * Returns the snapshot of the triggered task if available, else an empty array + * + * @return array The task snapshot if available, else null + * + * @since 4.1.0 + */ + public function getResultSnapshot(): array + { + return $this->arguments['resultSnapshot'] ?? []; + } } diff --git a/administrator/components/com_scheduler/src/Extension/SchedulerComponent.php b/administrator/components/com_scheduler/src/Extension/SchedulerComponent.php index 9f0b58278f3be..b813d22ea723a 100644 --- a/administrator/components/com_scheduler/src/Extension/SchedulerComponent.php +++ b/administrator/components/com_scheduler/src/Extension/SchedulerComponent.php @@ -1,4 +1,5 @@ [0, 59], - 'hours' => [0, 23], - 'days_week' => [1, 7], - 'days_month' => [1, 31], - 'months' => [1, 12], - ]; - - /** - * Response labels for the 'month' and 'days_week' subtypes. - * The labels are language constants translated when needed. - * - * @var string[][] - * @since 4.1.0 - */ - private const PREPARED_RESPONSE_LABELS = [ - 'months' => [ - 'JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', - 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER', - ], - 'days_week' => [ - 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', - 'FRIDAY', 'SATURDAY', 'SUNDAY', - ], - ]; - - /** - * The form field type. - * - * @var string - * - * @since 4.1.0 - */ - protected $type = 'cronIntervals'; - - /** - * The subtype of the CronIntervals field - * - * @var string - * @since 4.1.0 - */ - private $subtype; - - /** - * If true, field options will include a wildcard - * - * @var boolean - * @since 4.1.0 - */ - private $wildcard; - - /** - * If true, field will only have numeric labels (for days_week and months) - * - * @var boolean - * @since 4.1.0 - */ - private $onlyNumericLabels; - - /** - * Override the parent method to set deal with subtypes. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form - * field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for - * the field. For example if the field has `name="foo"` and the group value is - * set to "bar" then the full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.1.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null): bool - { - $parentResult = parent::setup($element, $value, $group); - - $subtype = ((string) $element['subtype'] ?? '') ?: null; - $wildcard = ((string) $element['wildcard'] ?? '') === 'true'; - $onlyNumericLabels = ((string) $element['onlyNumericLabels']) === 'true'; - - if (!($subtype && \in_array($subtype, self::SUBTYPES))) - { - return false; - } - - $this->subtype = $subtype; - $this->wildcard = $wildcard; - $this->onlyNumericLabels = $onlyNumericLabels; - - return $parentResult; - } - - /** - * Method to get field options - * - * @return array Array of objects representing options in the options list - * - * @since 4.1.0 - */ - protected function getOptions(): array - { - $subtype = $this->subtype; - $options = parent::getOptions(); - - if (!\in_array($subtype, self::SUBTYPES)) - { - return $options; - } - - if ($this->wildcard) - { - try - { - $options[] = HTMLHelper::_('select.option', '*', '*'); - } - catch (\InvalidArgumentException $e) - { - } - } - - [$optionLower, $optionUpper] = self::OPTIONS_RANGE[$subtype]; - - // If we need text labels, we translate them first - if (\array_key_exists($subtype, self::PREPARED_RESPONSE_LABELS) && !$this->onlyNumericLabels) - { - $labels = array_map( - static function (string $string): string { - return Text::_($string); - }, - self::PREPARED_RESPONSE_LABELS[$subtype] - ); - } - else - { - $labels = range(...self::OPTIONS_RANGE[$subtype]); - } - - for ([$i, $l] = [$optionLower, 0]; $i <= $optionUpper; $i++, $l++) - { - try - { - $options[] = HTMLHelper::_('select.option', (string) ($i), $labels[$l]); - } - catch (\InvalidArgumentException $e) - { - } - } - - return $options; - } + /** + * The subtypes supported by this field type. + * + * @var string[] + * + * @since 4.1.0 + */ + private const SUBTYPES = [ + 'minutes', + 'hours', + 'days_month', + 'months', + 'days_week', + ]; + + /** + * Count of predefined options for each subtype + * + * @var int[][] + * + * @since 4.1.0 + */ + private const OPTIONS_RANGE = [ + 'minutes' => [0, 59], + 'hours' => [0, 23], + 'days_week' => [1, 7], + 'days_month' => [1, 31], + 'months' => [1, 12], + ]; + + /** + * Response labels for the 'month' and 'days_week' subtypes. + * The labels are language constants translated when needed. + * + * @var string[][] + * @since 4.1.0 + */ + private const PREPARED_RESPONSE_LABELS = [ + 'months' => [ + 'JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', + 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER', + ], + 'days_week' => [ + 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', + 'FRIDAY', 'SATURDAY', 'SUNDAY', + ], + ]; + + /** + * The form field type. + * + * @var string + * + * @since 4.1.0 + */ + protected $type = 'cronIntervals'; + + /** + * The subtype of the CronIntervals field + * + * @var string + * @since 4.1.0 + */ + private $subtype; + + /** + * If true, field options will include a wildcard + * + * @var boolean + * @since 4.1.0 + */ + private $wildcard; + + /** + * If true, field will only have numeric labels (for days_week and months) + * + * @var boolean + * @since 4.1.0 + */ + private $onlyNumericLabels; + + /** + * Override the parent method to set deal with subtypes. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form + * field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for + * the field. For example if the field has `name="foo"` and the group value is + * set to "bar" then the full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.1.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null): bool + { + $parentResult = parent::setup($element, $value, $group); + + $subtype = ((string) $element['subtype'] ?? '') ?: null; + $wildcard = ((string) $element['wildcard'] ?? '') === 'true'; + $onlyNumericLabels = ((string) $element['onlyNumericLabels']) === 'true'; + + if (!($subtype && \in_array($subtype, self::SUBTYPES))) { + return false; + } + + $this->subtype = $subtype; + $this->wildcard = $wildcard; + $this->onlyNumericLabels = $onlyNumericLabels; + + return $parentResult; + } + + /** + * Method to get field options + * + * @return array Array of objects representing options in the options list + * + * @since 4.1.0 + */ + protected function getOptions(): array + { + $subtype = $this->subtype; + $options = parent::getOptions(); + + if (!\in_array($subtype, self::SUBTYPES)) { + return $options; + } + + if ($this->wildcard) { + try { + $options[] = HTMLHelper::_('select.option', '*', '*'); + } catch (\InvalidArgumentException $e) { + } + } + + [$optionLower, $optionUpper] = self::OPTIONS_RANGE[$subtype]; + + // If we need text labels, we translate them first + if (\array_key_exists($subtype, self::PREPARED_RESPONSE_LABELS) && !$this->onlyNumericLabels) { + $labels = array_map( + static function (string $string): string { + return Text::_($string); + }, + self::PREPARED_RESPONSE_LABELS[$subtype] + ); + } else { + $labels = range(...self::OPTIONS_RANGE[$subtype]); + } + + for ([$i, $l] = [$optionLower, 0]; $i <= $optionUpper; $i++, $l++) { + try { + $options[] = HTMLHelper::_('select.option', (string) ($i), $labels[$l]); + } catch (\InvalidArgumentException $e) { + } + } + + return $options; + } } diff --git a/administrator/components/com_scheduler/src/Field/ExecutionRuleField.php b/administrator/components/com_scheduler/src/Field/ExecutionRuleField.php index 6f877357d2099..ea6ea06b71988 100644 --- a/administrator/components/com_scheduler/src/Field/ExecutionRuleField.php +++ b/administrator/components/com_scheduler/src/Field/ExecutionRuleField.php @@ -1,4 +1,5 @@ 'COM_SCHEDULER_EXECUTION_INTERVAL_MINUTES', - 'interval-hours' => 'COM_SCHEDULER_EXECUTION_INTERVAL_HOURS', - 'interval-days' => 'COM_SCHEDULER_EXECUTION_INTERVAL_DAYS', - 'interval-months' => 'COM_SCHEDULER_EXECUTION_INTERVAL_MONTHS', - 'cron-expression' => 'COM_SCHEDULER_EXECUTION_CRON_EXPRESSION', - 'manual' => 'COM_SCHEDULER_OPTION_EXECUTION_MANUAL_LABEL', - ]; + /** + * Available execution rules. + * + * @var string[] + * @since 4.1.0 + */ + protected $predefinedOptions = [ + 'interval-minutes' => 'COM_SCHEDULER_EXECUTION_INTERVAL_MINUTES', + 'interval-hours' => 'COM_SCHEDULER_EXECUTION_INTERVAL_HOURS', + 'interval-days' => 'COM_SCHEDULER_EXECUTION_INTERVAL_DAYS', + 'interval-months' => 'COM_SCHEDULER_EXECUTION_INTERVAL_MONTHS', + 'cron-expression' => 'COM_SCHEDULER_EXECUTION_CRON_EXPRESSION', + 'manual' => 'COM_SCHEDULER_OPTION_EXECUTION_MANUAL_LABEL', + ]; } diff --git a/administrator/components/com_scheduler/src/Field/IntervalField.php b/administrator/components/com_scheduler/src/Field/IntervalField.php index 7a7202e66f33d..d085d7318ff14 100644 --- a/administrator/components/com_scheduler/src/Field/IntervalField.php +++ b/administrator/components/com_scheduler/src/Field/IntervalField.php @@ -1,4 +1,5 @@ [minVal, maxVal] - * - * @var string[] - * @since 4.1.0 - */ - private const SUBTYPES = [ - 'minutes' => [1, 59], - 'hours' => [1, 23], - 'days' => [1, 30], - 'months' => [1, 12], - ]; + /** + * The subtypes supported by this field type => [minVal, maxVal] + * + * @var string[] + * @since 4.1.0 + */ + private const SUBTYPES = [ + 'minutes' => [1, 59], + 'hours' => [1, 23], + 'days' => [1, 30], + 'months' => [1, 12], + ]; - /** - * The allowable maximum value of the field. - * - * @var float - * @since 4.1.0 - */ - protected $max; + /** + * The allowable maximum value of the field. + * + * @var float + * @since 4.1.0 + */ + protected $max; - /** - * The allowable minimum value of the field. - * - * @var float - * @since 4.1.0 - */ - protected $min; + /** + * The allowable minimum value of the field. + * + * @var float + * @since 4.1.0 + */ + protected $min; - /** - * The step by which value of the field increased or decreased. - * - * @var float - * @since 4.1.0 - */ - protected $step = 1; + /** + * The step by which value of the field increased or decreased. + * + * @var float + * @since 4.1.0 + */ + protected $step = 1; - /** - * Override the parent method to set deal with subtypes. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form - * field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for - * the field. For example if the field has `name="foo"` and the group value is - * set to "bar" then the full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.1.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null): bool - { - $parentResult = FormField::setup($element, $value, $group); - $subtype = ((string) $element['subtype'] ?? '') ?: null; + /** + * Override the parent method to set deal with subtypes. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form + * field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for + * the field. For example if the field has `name="foo"` and the group value is + * set to "bar" then the full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.1.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null): bool + { + $parentResult = FormField::setup($element, $value, $group); + $subtype = ((string) $element['subtype'] ?? '') ?: null; - if (empty($subtype) || !\array_key_exists($subtype, self::SUBTYPES)) - { - return false; - } + if (empty($subtype) || !\array_key_exists($subtype, self::SUBTYPES)) { + return false; + } - [$this->min, $this->max] = self::SUBTYPES[$subtype]; + [$this->min, $this->max] = self::SUBTYPES[$subtype]; - return $parentResult; - } + return $parentResult; + } } diff --git a/administrator/components/com_scheduler/src/Field/TaskStateField.php b/administrator/components/com_scheduler/src/Field/TaskStateField.php index 4ea8ba93d8a6e..21f2e7de2694b 100644 --- a/administrator/components/com_scheduler/src/Field/TaskStateField.php +++ b/administrator/components/com_scheduler/src/Field/TaskStateField.php @@ -1,4 +1,5 @@ 'JTRASHED', - 0 => 'JDISABLED', - 1 => 'JENABLED', - '*' => 'JALL', - ]; + /** + * Available states + * + * @var string[] + * @since 4.1.0 + */ + protected $predefinedOptions = [ + -2 => 'JTRASHED', + 0 => 'JDISABLED', + 1 => 'JENABLED', + '*' => 'JALL', + ]; } diff --git a/administrator/components/com_scheduler/src/Field/TaskTypeField.php b/administrator/components/com_scheduler/src/Field/TaskTypeField.php index 86d2e23702a0a..01aa820097a72 100644 --- a/administrator/components/com_scheduler/src/Field/TaskTypeField.php +++ b/administrator/components/com_scheduler/src/Field/TaskTypeField.php @@ -1,4 +1,5 @@ options, - 'title', - 1 - ); + // Get all available task types and sort by title + $types = ArrayHelper::sortObjects( + SchedulerHelper::getTaskOptions()->options, + 'title', + 1 + ); - // Closure to add a TaskOption as a option in $options: array + $addTypeAsOption = function (TaskOption $type) use (&$options) { + try { + $options[] = HTMLHelper::_('select.option', $type->id, $type->title); + } catch (\InvalidArgumentException $e) { + } + }; - // Call $addTypeAsOption on each type - array_map($addTypeAsOption, $types); + // Call $addTypeAsOption on each type + array_map($addTypeAsOption, $types); - return $options; - } + return $options; + } } diff --git a/administrator/components/com_scheduler/src/Field/WebcronLinkField.php b/administrator/components/com_scheduler/src/Field/WebcronLinkField.php index 597afee3e6428..31795e17a1b18 100644 --- a/administrator/components/com_scheduler/src/Field/WebcronLinkField.php +++ b/administrator/components/com_scheduler/src/Field/WebcronLinkField.php @@ -1,4 +1,5 @@ task = \is_array($task) ? $task : ArrayHelper::fromObject($task); - $rule = $this->getFromTask('cron_rules'); - $this->rule = \is_string($rule) - ? (object) json_decode($rule) - : (\is_array($rule) ? (object) $rule : $rule); - $this->type = $this->rule->type; - } + /** + * @param array|object $task A task entry + * + * @since 4.1.0 + */ + public function __construct($task) + { + $this->task = \is_array($task) ? $task : ArrayHelper::fromObject($task); + $rule = $this->getFromTask('cron_rules'); + $this->rule = \is_string($rule) + ? (object) json_decode($rule) + : (\is_array($rule) ? (object) $rule : $rule); + $this->type = $this->rule->type; + } - /** - * Get a property from the task array - * - * @param string $property The property to get - * @param mixed $default The default value returned if property does not exist - * - * @return mixed - * - * @since 4.1.0 - */ - private function getFromTask(string $property, $default = null) - { - $property = ArrayHelper::getValue($this->task, $property); + /** + * Get a property from the task array + * + * @param string $property The property to get + * @param mixed $default The default value returned if property does not exist + * + * @return mixed + * + * @since 4.1.0 + */ + private function getFromTask(string $property, $default = null) + { + $property = ArrayHelper::getValue($this->task, $property); - return $property ?? $default; - } + return $property ?? $default; + } - /** - * @param boolean $string If true, an SQL formatted string is returned. - * @param boolean $basisNow If true, the current date-time is used as the basis for projecting the next - * execution. - * - * @return ?Date|string - * - * @since 4.1.0 - * @throws \Exception - */ - public function nextExec(bool $string = true, bool $basisNow = false) - { - // Exception handling here - switch ($this->type) - { - case 'interval': - $lastExec = Factory::getDate($basisNow ? 'now' : $this->getFromTask('last_execution'), 'UTC'); - $interval = new \DateInterval($this->rule->exp); - $nextExec = $lastExec->add($interval); - $nextExec = $string ? $nextExec->toSql() : $nextExec; - break; - case 'cron-expression': - // @todo: testing - $cExp = new CronExpression((string) $this->rule->exp); - $nextExec = $cExp->getNextRunDate('now', 0, false, 'UTC'); - $nextExec = $string ? $this->dateTimeToSql($nextExec) : $nextExec; - break; - default: - // 'manual' execution is handled here. - $nextExec = null; - } + /** + * @param boolean $string If true, an SQL formatted string is returned. + * @param boolean $basisNow If true, the current date-time is used as the basis for projecting the next + * execution. + * + * @return ?Date|string + * + * @since 4.1.0 + * @throws \Exception + */ + public function nextExec(bool $string = true, bool $basisNow = false) + { + // Exception handling here + switch ($this->type) { + case 'interval': + $lastExec = Factory::getDate($basisNow ? 'now' : $this->getFromTask('last_execution'), 'UTC'); + $interval = new \DateInterval($this->rule->exp); + $nextExec = $lastExec->add($interval); + $nextExec = $string ? $nextExec->toSql() : $nextExec; + break; + case 'cron-expression': + // @todo: testing + $cExp = new CronExpression((string) $this->rule->exp); + $nextExec = $cExp->getNextRunDate('now', 0, false, 'UTC'); + $nextExec = $string ? $this->dateTimeToSql($nextExec) : $nextExec; + break; + default: + // 'manual' execution is handled here. + $nextExec = null; + } - return $nextExec; - } + return $nextExec; + } - /** - * Returns a sql-formatted string for a DateTime object. - * Only needed for DateTime objects returned by CronExpression, JDate supports this as class method. - * - * @param \DateTime $dateTime A DateTime object to format - * - * @return string - * - * @since 4.1.0 - */ - private function dateTimeToSql(\DateTime $dateTime): string - { - static $db; - $db = $db ?? Factory::getContainer()->get(DatabaseDriver::class); + /** + * Returns a sql-formatted string for a DateTime object. + * Only needed for DateTime objects returned by CronExpression, JDate supports this as class method. + * + * @param \DateTime $dateTime A DateTime object to format + * + * @return string + * + * @since 4.1.0 + */ + private function dateTimeToSql(\DateTime $dateTime): string + { + static $db; + $db = $db ?? Factory::getContainer()->get(DatabaseDriver::class); - return $dateTime->format($db->getDateFormat()); - } + return $dateTime->format($db->getDateFormat()); + } } diff --git a/administrator/components/com_scheduler/src/Helper/SchedulerHelper.php b/administrator/components/com_scheduler/src/Helper/SchedulerHelper.php index 3e0f13a2cefe2..72a4aef18361c 100644 --- a/administrator/components/com_scheduler/src/Helper/SchedulerHelper.php +++ b/administrator/components/com_scheduler/src/Helper/SchedulerHelper.php @@ -1,4 +1,5 @@ $options, - ] - ); + /** @var AdministratorApplication $app */ + $app = Factory::getApplication(); + $options = new TaskOptions(); + $event = AbstractEvent::create( + 'onTaskOptionsList', + [ + 'subject' => $options, + ] + ); - PluginHelper::importPlugin('task'); - $app->getDispatcher()->dispatch('onTaskOptionsList', $event); + PluginHelper::importPlugin('task'); + $app->getDispatcher()->dispatch('onTaskOptionsList', $event); - self::$taskOptionsCache = $options; + self::$taskOptionsCache = $options; - return $options; - } + return $options; + } } diff --git a/administrator/components/com_scheduler/src/Model/SelectModel.php b/administrator/components/com_scheduler/src/Model/SelectModel.php index ea92d6c816e90..c683a68207554 100644 --- a/administrator/components/com_scheduler/src/Model/SelectModel.php +++ b/administrator/components/com_scheduler/src/Model/SelectModel.php @@ -1,4 +1,5 @@ app = Factory::getApplication(); - - parent::__construct($config, $factory); - } - - /** - * @return TaskOption[] An array of TaskOption objects - * - * @throws \Exception - * @since 4.1.0 - */ - public function getItems(): array - { - return SchedulerHelper::getTaskOptions()->options; - } + /** + * The Application object, due removal. + * + * @var AdministratorApplication + * @since 4.1.0 + */ + protected $app; + + /** + * SelectModel constructor. + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param ?MVCFactoryInterface $factory The factory. + * + * @throws \Exception + * @since 4.1.0 + */ + public function __construct($config = array(), ?MVCFactoryInterface $factory = null) + { + $this->app = Factory::getApplication(); + + parent::__construct($config, $factory); + } + + /** + * @return TaskOption[] An array of TaskOption objects + * + * @throws \Exception + * @since 4.1.0 + */ + public function getItems(): array + { + return SchedulerHelper::getTaskOptions()->options; + } } diff --git a/administrator/components/com_scheduler/src/Model/TaskModel.php b/administrator/components/com_scheduler/src/Model/TaskModel.php index 5bdb8fe9be1fe..484a29ecfe8f8 100644 --- a/administrator/components/com_scheduler/src/Model/TaskModel.php +++ b/administrator/components/com_scheduler/src/Model/TaskModel.php @@ -1,4 +1,5 @@ 1, - 'disabled' => 0, - 'trashed' => -2, - ]; - - /** - * The name of the database table with task records. - * - * @var string - * @since 4.1.0 - */ - public const TASK_TABLE = '#__scheduler_tasks'; - - /** - * Prefix used with controller messages - * - * @var string - * @since 4.1.0 - */ - protected $text_prefix = 'COM_SCHEDULER'; - - /** - * Type alias for content type - * - * @var string - * @since 4.1.0 - */ - public $typeAlias = 'com_scheduler.task'; - - /** - * The Application object, for convenience - * - * @var AdministratorApplication $app - * @since 4.1.0 - */ - protected $app; - - /** - * The event to trigger before unlocking the data. - * - * @var string - * @since 4.1.0 - */ - protected $event_before_unlock = null; - - /** - * The event to trigger after unlocking the data. - * - * @var string - * @since 4.1.0 - */ - protected $event_unlock = null; - - /** - * TaskModel constructor. Needed just to set $app - * - * @param array $config An array of configuration options - * @param MVCFactoryInterface|null $factory The factory - * @param FormFactoryInterface|null $formFactory The form factory - * - * @since 4.1.0 - * @throws \Exception - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null) - { - $config['events_map'] = $config['events_map'] ?? []; - - $config['events_map'] = array_merge( - [ - 'save' => 'task', - 'validate' => 'task', - 'unlock' => 'task', - ], - $config['events_map'] - ); - - if (isset($config['event_before_unlock'])) - { - $this->event_before_unlock = $config['event_before_unlock']; - } - elseif (empty($this->event_before_unlock)) - { - $this->event_before_unlock = 'onContentBeforeUnlock'; - } - - if (isset($config['event_unlock'])) - { - $this->event_unlock = $config['event_unlock']; - } - elseif (empty($this->event_unlock)) - { - $this->event_unlock = 'onContentUnlock'; - } - - $this->app = Factory::getApplication(); - - parent::__construct($config, $factory, $formFactory); - } - - /** - * Fetches the form object associated with this model. By default, - * loads the corresponding data from the DB and binds it with the form. - * - * @param array $data Data that needs to go into the form - * @param bool $loadData Should the form load its data from the DB? - * - * @return Form|boolean A JForm object on success, false on failure. - * - * @since 4.1.0 - * @throws \Exception - */ - public function getForm($data = array(), $loadData = true) - { - Form::addFieldPath(JPATH_ADMINISTRATOR . 'components/com_scheduler/src/Field'); - - /** - * loadForm() (defined by FormBehaviourTrait) also loads the form data by calling - * loadFormData() : $data [implemented here] and binds it to the form by calling - * $form->bind($data). - */ - $form = $this->loadForm('com_scheduler.task', 'task', ['control' => 'jform', 'load_data' => $loadData]); - - if (empty($form)) - { - return false; - } - - $user = $this->app->getIdentity(); - - // If new entry, set task type from state - if ($this->getState('task.id', 0) === 0 && $this->getState('task.type') !== null) - { - $form->setValue('type', null, $this->getState('task.type')); - } - - // @todo : Check if this is working as expected for new items (id == 0) - if (!$user->authorise('core.edit.state', 'com_scheduler.task.' . $this->getState('task.id'))) - { - // Disable fields - $form->setFieldAttribute('state', 'disabled', 'true'); - - // No "hacking" ._. - $form->setFieldAttribute('state', 'filter', 'unset'); - } - - return $form; - } - - /** - * Determine whether a record may be deleted taking into consideration - * the user's permissions over the record. - * - * @param object $record The database row/record in question - * - * @return boolean True if the record may be deleted - * - * @since 4.1.0 - * @throws \Exception - */ - protected function canDelete($record): bool - { - // Record doesn't exist, can't delete - if (empty($record->id)) - { - return false; - } - - return $this->app->getIdentity()->authorise('core.delete', 'com_scheduler.task.' . $record->id); - } - - /** - * Populate the model state, we use these instead of toying with input or the global state - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - protected function populateState(): void - { - $app = $this->app; - - $taskId = $app->getInput()->getInt('id'); - $taskType = $app->getUserState('com_scheduler.add.task.task_type'); - - // @todo: Remove this. Get the option through a helper call. - $taskOption = $app->getUserState('com_scheduler.add.task.task_option'); - - $this->setState('task.id', $taskId); - $this->setState('task.type', $taskType); - $this->setState('task.option', $taskOption); - - // Load component params, though com_scheduler does not (yet) have any params - $cParams = ComponentHelper::getParams($this->option); - $this->setState('params', $cParams); - } - - /** - * Don't need to define this method since the parent getTable() - * implicitly deduces $name and $prefix anyways. This makes the object - * more transparent though. - * - * @param string $name Name of the table - * @param string $prefix Class prefix - * @param array $options Model config array - * - * @return Table - * - * @since 4.1.0 - * @throws \Exception - */ - public function getTable($name = 'Task', $prefix = 'Table', $options = array()): Table - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Fetches the data to be injected into the form - * - * @return object Associative array of form data. - * - * @since 4.1.0 - * @throws \Exception - */ - protected function loadFormData() - { - $data = $this->app->getUserState('com_scheduler.edit.task.data', array()); - - // If the data from UserState is empty, we fetch it with getItem() - if (empty($data)) - { - /** @var CMSObject $data */ - $data = $this->getItem(); - - // @todo : further data processing goes here - - // For a fresh object, set exec-day and exec-time - if (!($data->id ?? 0)) - { - $data->execution_rules['exec-day'] = gmdate('d'); - $data->execution_rules['exec-time'] = gmdate('H:i'); - } - } - - // Let plugins manipulate the data - $this->preprocessData('com_scheduler.task', $data, 'task'); - - return $data; - } - - /** - * Overloads the parent getItem() method. - * - * @param integer $pk Primary key - * - * @return object|boolean Object on success, false on failure - * - * @since 4.1.0 - * @throws \Exception - */ - public function getItem($pk = null) - { - $item = parent::getItem($pk); - - if (!\is_object($item)) - { - return false; - } - - // Parent call leaves `execution_rules` and `cron_rules` JSON encoded - $item->set('execution_rules', json_decode($item->get('execution_rules', ''))); - $item->set('cron_rules', json_decode($item->get('cron_rules', ''))); - - $taskOption = SchedulerHelper::getTaskOptions()->findOption( - ($item->id ?? 0) ? ($item->type ?? 0) : $this->getState('task.type') - ); - - $item->set('taskOption', $taskOption); - - return $item; - } - - /** - * Get a task from the database, only if an exclusive "lock" on the task can be acquired. - * The method supports options to customise the limitations on the fetch. - * - * @param array $options Array with options to fetch the task: - * 1. `id`: Optional id of the task to fetch. - * 2. `allowDisabled`: If true, disabled tasks can also be fetched. - * (default: false) - * 3. `bypassScheduling`: If true, tasks that are not due can also be - * fetched. Should only be true if an `id` is targeted instead of the - * task queue. (default: false) - * 4. `allowConcurrent`: If true, fetches even when another task is - * running ('locked'). (default: false) - * 5. `includeCliExclusive`: If true, can also fetch CLI exclusive tasks. (default: true) - * - * @return ?\stdClass Task entry as in the database. - * - * @since 4.1.0 - * @throws UndefinedOptionsException|InvalidOptionsException - * @throws \RuntimeException - */ - public function getTask(array $options = []): ?\stdClass - { - $resolver = new OptionsResolver; - - try - { - $this->configureTaskGetterOptions($resolver); - } - catch (\Exception $e) - { - } - - try - { - $options = $resolver->resolve($options); - } - catch (\Exception $e) - { - if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) - { - throw $e; - } - } - - $db = $this->getDatabase(); - $now = Factory::getDate()->toSql(); - - // Get lock on the table to help with concurrency issues - $db->lockTable(self::TASK_TABLE); - - // If concurrency is not allowed, we only get a task if another one does not have a "lock" - if (!$options['allowConcurrent']) - { - // Get count of locked (presumed running) tasks - $lockCountQuery = $db->getQuery(true) - ->from($db->quoteName(self::TASK_TABLE)) - ->select('COUNT(id)') - ->where($db->quoteName('locked') . ' IS NOT NULL'); - - try - { - $runningCount = $db->setQuery($lockCountQuery)->loadResult(); - } - catch (\RuntimeException $e) - { - $db->unlockTables(); - - return null; - } - - if ($runningCount !== 0) - { - $db->unlockTables(); - - return null; - } - } - - $lockQuery = $db->getQuery(true); - - $lockQuery->update($db->quoteName(self::TASK_TABLE)) - ->set($db->quoteName('locked') . ' = :now1') - ->bind(':now1', $now); - - // Array of all active routine ids - $activeRoutines = array_map( - static function (TaskOption $taskOption): string - { - return $taskOption->id; - }, - SchedulerHelper::getTaskOptions()->options - ); - - // "Orphaned" tasks are not a part of the task queue! - $lockQuery->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); - - // If directed, exclude CLI exclusive tasks - if (!$options['includeCliExclusive']) - { - $lockQuery->where($db->quoteName('cli_exclusive') . ' = 0'); - } - - if (!$options['bypassScheduling']) - { - $lockQuery->where($db->quoteName('next_execution') . ' <= :now2') - ->bind(':now2', $now); - } - - if ($options['allowDisabled']) - { - $lockQuery->whereIn($db->quoteName('state'), [0, 1]); - } - else - { - $lockQuery->where($db->quoteName('state') . ' = 1'); - } - - if ($options['id'] > 0) - { - $lockQuery->where($db->quoteName('id') . ' = :taskId') - ->bind(':taskId', $options['id'], ParameterType::INTEGER); - } - // Pick from the front of the task queue if no 'id' is specified - else - { - // Get the id of the next task in the task queue - $idQuery = $db->getQuery(true) - ->from($db->quoteName(self::TASK_TABLE)) - ->select($db->quoteName('id')) - ->where($db->quoteName('state') . ' = 1') - ->order($db->quoteName('priority') . ' DESC') - ->order($db->quoteName('next_execution') . ' ASC') - ->setLimit(1); - - try - { - $ids = $db->setQuery($idQuery)->loadColumn(); - } - catch (\RuntimeException $e) - { - $db->unlockTables(); - - return null; - } - - if (count($ids) === 0) - { - $db->unlockTables(); - - return null; - } - - $lockQuery->whereIn($db->quoteName('id'), $ids); - } - - try - { - $db->setQuery($lockQuery)->execute(); - } - catch (\RuntimeException $e) - { - } - finally - { - $affectedRows = $db->getAffectedRows(); - - $db->unlockTables(); - } - - if ($affectedRows != 1) - { - /* - // @todo - // ? Fatal failure handling here? - // ! Question is, how? If we check for tasks running beyond there time here, we have no way of - // ! what's already been notified (since we're not auto-unlocking/recovering tasks anymore). - // The solution __may__ be in a "last_successful_finish" (or something) column. - */ - - return null; - } - - $getQuery = $db->getQuery(true); - - $getQuery->select('*') - ->from($db->quoteName(self::TASK_TABLE)) - ->where($db->quoteName('locked') . ' = :now') - ->bind(':now', $now); - - $task = $db->setQuery($getQuery)->loadObject(); - - $task->execution_rules = json_decode($task->execution_rules); - $task->cron_rules = json_decode($task->cron_rules); - - $task->taskOption = SchedulerHelper::getTaskOptions()->findOption($task->type); - - return $task; - } - - /** - * Set up an {@see OptionsResolver} to resolve options compatible with the {@see GetTask()} method. - * - * @param OptionsResolver $resolver The {@see OptionsResolver} instance to set up. - * - * @return OptionsResolver - * - * @since 4.1.0 - * @throws AccessException - */ - public static function configureTaskGetterOptions(OptionsResolver $resolver): OptionsResolver - { - $resolver->setDefaults( - [ - 'id' => 0, - 'allowDisabled' => false, - 'bypassScheduling' => false, - 'allowConcurrent' => false, - 'includeCliExclusive' => true, - ] - ) - ->setAllowedTypes('id', 'numeric') - ->setAllowedTypes('allowDisabled', 'bool') - ->setAllowedTypes('bypassScheduling', 'bool') - ->setAllowedTypes('allowConcurrent', 'bool') - ->setAllowedTypes('includeCliExclusive', 'bool'); - - return $resolver; - } - - /** - * @param array $data The form data - * - * @return boolean True on success, false on failure - * - * @since 4.1.0 - * @throws \Exception - */ - public function save($data): bool - { - $id = (int) ($data['id'] ?? $this->getState('task.id')); - $isNew = $id === 0; - - // Clean up execution rules - $data['execution_rules'] = $this->processExecutionRules($data['execution_rules']); - - // If a new entry, we'll have to put in place a pseudo-last_execution - if ($isNew) - { - $basisDayOfMonth = $data['execution_rules']['exec-day']; - [$basisHour, $basisMinute] = explode(':', $data['execution_rules']['exec-time']); - - $data['last_execution'] = Factory::getDate('now', 'GMT')->format('Y-m') - . "-$basisDayOfMonth $basisHour:$basisMinute:00"; - } - else - { - $data['last_execution'] = $this->getItem($id)->last_execution; - } - - // Build the `cron_rules` column from `execution_rules` - $data['cron_rules'] = $this->buildExecutionRules($data['execution_rules']); - - // `next_execution` would be null if scheduling is disabled with the "manual" rule! - $data['next_execution'] = (new ExecRuleHelper($data))->nextExec(); - - if ($isNew) - { - $data['last_execution'] = null; - } - - // If no params, we set as empty array. - // ? Is this the right place to do this - $data['params'] = $data['params'] ?? []; - - // Parent method takes care of saving to the table - return parent::save($data); - } - - /** - * Clean up and standardise execution rules - * - * @param array $unprocessedRules The form data [? can just replace with execution_interval] - * - * @return array Processed rules - * - * @since 4.1.0 - */ - private function processExecutionRules(array $unprocessedRules): array - { - $executionRules = $unprocessedRules; - - $ruleType = $executionRules['rule-type']; - $retainKeys = ['rule-type', $ruleType, 'exec-day', 'exec-time']; - $executionRules = array_intersect_key($executionRules, array_flip($retainKeys)); - - // Default to current date-time in UTC/GMT as the basis - $executionRules['exec-day'] = $executionRules['exec-day'] ?: (string) gmdate('d'); - $executionRules['exec-time'] = $executionRules['exec-time'] ?: (string) gmdate('H:i'); - - // If custom ruleset, sort it - // ? Is this necessary - if ($ruleType === 'cron-expression') - { - foreach ($executionRules['cron-expression'] as &$values) - { - sort($values); - } - } - - return $executionRules; - } - - /** - * Private method to build execution expression from input execution rules. - * This expression is used internally to determine execution times/conditions. - * - * @param array $executionRules Execution rules from the Task form, post-processing. - * - * @return array - * - * @since 4.1.0 - * @throws \Exception - */ - private function buildExecutionRules(array $executionRules): array - { - // Maps interval strings, use with sprintf($map[intType], $interval) - $intervalStringMap = [ - 'minutes' => 'PT%dM', - 'hours' => 'PT%dH', - 'days' => 'P%dD', - 'months' => 'P%dM', - 'years' => 'P%dY', - ]; - - $ruleType = $executionRules['rule-type']; - $ruleClass = strpos($ruleType, 'interval') === 0 ? 'interval' : $ruleType; - $buildExpression = ''; - - if ($ruleClass === 'interval') - { - // Rule type for intervals interval- - $intervalType = explode('-', $ruleType)[1]; - $interval = $executionRules["interval-$intervalType"]; - $buildExpression = sprintf($intervalStringMap[$intervalType], $interval); - } - - if ($ruleClass === 'cron-expression') - { - // ! custom matches are disabled in the form - $matches = $executionRules['cron-expression']; - $buildExpression .= $this->wildcardIfMatch($matches['minutes'], range(0, 59), true); - $buildExpression .= ' ' . $this->wildcardIfMatch($matches['hours'], range(0, 23), true); - $buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_month'], range(1, 31), true); - $buildExpression .= ' ' . $this->wildcardIfMatch($matches['months'], range(1, 12), true); - $buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_week'], range(0, 6), true); - } - - return [ - 'type' => $ruleClass, - 'exp' => $buildExpression, - ]; - } - - /** - * This method releases "locks" on a set of tasks from the database. - * These locks are pseudo-locks that are used to keep a track of running tasks. However, they require require manual - * intervention to release these locks in cases such as when a task process crashes, leaving the task "locked". - * - * @param array $pks A list of the primary keys to unlock. - * - * @return boolean True on success. - * - * @since 4.1.0 - * @throws \RuntimeException|\UnexpectedValueException|\BadMethodCallException - */ - public function unlock(array &$pks): bool - { - /** @var TaskTable $table */ - $table = $this->getTable(); - - $user = Factory::getApplication()->getIdentity(); - - $context = $this->option . '.' . $this->name; - - // Include the plugins for the change of state event. - PluginHelper::importPlugin($this->events_map['unlock']); - - // Access checks. - foreach ($pks as $i => $pk) - { - $table->reset(); - - if ($table->load($pk)) - { - if (!$this->canEditState($table)) - { - // Prune items that you can't change. - unset($pks[$i]); - Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - - return false; - } - - // Prune items that are already at the given state. - $lockedColumnName = $table->getColumnAlias('locked'); - - if (property_exists($table, $lockedColumnName) && \is_null($table->get($lockedColumnName))) - { - unset($pks[$i]); - } - } - } - - // Check if there are items to change. - if (!\count($pks)) - { - return true; - } - - $event = AbstractEvent::create( - $this->event_before_unlock, - [ - 'subject' => $this, - 'context' => $context, - 'pks' => $pks, - ] - ); - - try - { - Factory::getApplication()->getDispatcher()->dispatch($this->event_before_unlock, $event); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Attempt to unlock the records. - if (!$table->unlock($pks, $user->id)) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after unlock event - $event = AbstractEvent::create( - $this->event_unlock, - [ - 'subject' => $this, - 'context' => $context, - 'pks' => $pks, - ] - ); - - try - { - Factory::getApplication()->getDispatcher()->dispatch($this->event_unlock, $event); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Determine if an array is populated by all its possible values by comparison to a reference array, if found a - * match a wildcard '*' is returned. - * - * @param array $target The target array - * @param array $reference The reference array, populated by the complete set of possible values in $target - * @param bool $targetToInt If true, converts $target array values to integers before comparing - * - * @return string A wildcard string if $target is fully populated, else $target itself. - * - * @since 4.1.0 - */ - private function wildcardIfMatch(array $target, array $reference, bool $targetToInt = false): string - { - if ($targetToInt) - { - $target = array_map( - static function (string $x): int { - return (int) $x; - }, - $target - ); - } - - $isMatch = array_diff($reference, $target) === []; - - return $isMatch ? "*" : implode(',', $target); - } - - /** - * Method to allow derived classes to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 4.1.0 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content'): void - { - // Load the 'task' plugin group - PluginHelper::importPlugin('task'); - - // Let the parent method take over - parent::preprocessForm($form, $data, $group); - } + /** + * Maps logical states to their values in the DB + * ? Do we end up using this? + * + * @var array + * @since 4.1.0 + */ + protected const TASK_STATES = [ + 'enabled' => 1, + 'disabled' => 0, + 'trashed' => -2, + ]; + + /** + * The name of the database table with task records. + * + * @var string + * @since 4.1.0 + */ + public const TASK_TABLE = '#__scheduler_tasks'; + + /** + * Prefix used with controller messages + * + * @var string + * @since 4.1.0 + */ + protected $text_prefix = 'COM_SCHEDULER'; + + /** + * Type alias for content type + * + * @var string + * @since 4.1.0 + */ + public $typeAlias = 'com_scheduler.task'; + + /** + * The Application object, for convenience + * + * @var AdministratorApplication $app + * @since 4.1.0 + */ + protected $app; + + /** + * The event to trigger before unlocking the data. + * + * @var string + * @since 4.1.0 + */ + protected $event_before_unlock = null; + + /** + * The event to trigger after unlocking the data. + * + * @var string + * @since 4.1.0 + */ + protected $event_unlock = null; + + /** + * TaskModel constructor. Needed just to set $app + * + * @param array $config An array of configuration options + * @param MVCFactoryInterface|null $factory The factory + * @param FormFactoryInterface|null $formFactory The form factory + * + * @since 4.1.0 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null) + { + $config['events_map'] = $config['events_map'] ?? []; + + $config['events_map'] = array_merge( + [ + 'save' => 'task', + 'validate' => 'task', + 'unlock' => 'task', + ], + $config['events_map'] + ); + + if (isset($config['event_before_unlock'])) { + $this->event_before_unlock = $config['event_before_unlock']; + } elseif (empty($this->event_before_unlock)) { + $this->event_before_unlock = 'onContentBeforeUnlock'; + } + + if (isset($config['event_unlock'])) { + $this->event_unlock = $config['event_unlock']; + } elseif (empty($this->event_unlock)) { + $this->event_unlock = 'onContentUnlock'; + } + + $this->app = Factory::getApplication(); + + parent::__construct($config, $factory, $formFactory); + } + + /** + * Fetches the form object associated with this model. By default, + * loads the corresponding data from the DB and binds it with the form. + * + * @param array $data Data that needs to go into the form + * @param bool $loadData Should the form load its data from the DB? + * + * @return Form|boolean A JForm object on success, false on failure. + * + * @since 4.1.0 + * @throws \Exception + */ + public function getForm($data = array(), $loadData = true) + { + Form::addFieldPath(JPATH_ADMINISTRATOR . 'components/com_scheduler/src/Field'); + + /** + * loadForm() (defined by FormBehaviourTrait) also loads the form data by calling + * loadFormData() : $data [implemented here] and binds it to the form by calling + * $form->bind($data). + */ + $form = $this->loadForm('com_scheduler.task', 'task', ['control' => 'jform', 'load_data' => $loadData]); + + if (empty($form)) { + return false; + } + + $user = $this->app->getIdentity(); + + // If new entry, set task type from state + if ($this->getState('task.id', 0) === 0 && $this->getState('task.type') !== null) { + $form->setValue('type', null, $this->getState('task.type')); + } + + // @todo : Check if this is working as expected for new items (id == 0) + if (!$user->authorise('core.edit.state', 'com_scheduler.task.' . $this->getState('task.id'))) { + // Disable fields + $form->setFieldAttribute('state', 'disabled', 'true'); + + // No "hacking" ._. + $form->setFieldAttribute('state', 'filter', 'unset'); + } + + return $form; + } + + /** + * Determine whether a record may be deleted taking into consideration + * the user's permissions over the record. + * + * @param object $record The database row/record in question + * + * @return boolean True if the record may be deleted + * + * @since 4.1.0 + * @throws \Exception + */ + protected function canDelete($record): bool + { + // Record doesn't exist, can't delete + if (empty($record->id)) { + return false; + } + + return $this->app->getIdentity()->authorise('core.delete', 'com_scheduler.task.' . $record->id); + } + + /** + * Populate the model state, we use these instead of toying with input or the global state + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + protected function populateState(): void + { + $app = $this->app; + + $taskId = $app->getInput()->getInt('id'); + $taskType = $app->getUserState('com_scheduler.add.task.task_type'); + + // @todo: Remove this. Get the option through a helper call. + $taskOption = $app->getUserState('com_scheduler.add.task.task_option'); + + $this->setState('task.id', $taskId); + $this->setState('task.type', $taskType); + $this->setState('task.option', $taskOption); + + // Load component params, though com_scheduler does not (yet) have any params + $cParams = ComponentHelper::getParams($this->option); + $this->setState('params', $cParams); + } + + /** + * Don't need to define this method since the parent getTable() + * implicitly deduces $name and $prefix anyways. This makes the object + * more transparent though. + * + * @param string $name Name of the table + * @param string $prefix Class prefix + * @param array $options Model config array + * + * @return Table + * + * @since 4.1.0 + * @throws \Exception + */ + public function getTable($name = 'Task', $prefix = 'Table', $options = array()): Table + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Fetches the data to be injected into the form + * + * @return object Associative array of form data. + * + * @since 4.1.0 + * @throws \Exception + */ + protected function loadFormData() + { + $data = $this->app->getUserState('com_scheduler.edit.task.data', array()); + + // If the data from UserState is empty, we fetch it with getItem() + if (empty($data)) { + /** @var CMSObject $data */ + $data = $this->getItem(); + + // @todo : further data processing goes here + + // For a fresh object, set exec-day and exec-time + if (!($data->id ?? 0)) { + $data->execution_rules['exec-day'] = gmdate('d'); + $data->execution_rules['exec-time'] = gmdate('H:i'); + } + } + + // Let plugins manipulate the data + $this->preprocessData('com_scheduler.task', $data, 'task'); + + return $data; + } + + /** + * Overloads the parent getItem() method. + * + * @param integer $pk Primary key + * + * @return object|boolean Object on success, false on failure + * + * @since 4.1.0 + * @throws \Exception + */ + public function getItem($pk = null) + { + $item = parent::getItem($pk); + + if (!\is_object($item)) { + return false; + } + + // Parent call leaves `execution_rules` and `cron_rules` JSON encoded + $item->set('execution_rules', json_decode($item->get('execution_rules', ''))); + $item->set('cron_rules', json_decode($item->get('cron_rules', ''))); + + $taskOption = SchedulerHelper::getTaskOptions()->findOption( + ($item->id ?? 0) ? ($item->type ?? 0) : $this->getState('task.type') + ); + + $item->set('taskOption', $taskOption); + + return $item; + } + + /** + * Get a task from the database, only if an exclusive "lock" on the task can be acquired. + * The method supports options to customise the limitations on the fetch. + * + * @param array $options Array with options to fetch the task: + * 1. `id`: Optional id of the task to fetch. + * 2. `allowDisabled`: If true, disabled tasks can also be fetched. + * (default: false) + * 3. `bypassScheduling`: If true, tasks that are not due can also be + * fetched. Should only be true if an `id` is targeted instead of the + * task queue. (default: false) + * 4. `allowConcurrent`: If true, fetches even when another task is + * running ('locked'). (default: false) + * 5. `includeCliExclusive`: If true, can also fetch CLI exclusive tasks. (default: true) + * + * @return ?\stdClass Task entry as in the database. + * + * @since 4.1.0 + * @throws UndefinedOptionsException|InvalidOptionsException + * @throws \RuntimeException + */ + public function getTask(array $options = []): ?\stdClass + { + $resolver = new OptionsResolver(); + + try { + $this->configureTaskGetterOptions($resolver); + } catch (\Exception $e) { + } + + try { + $options = $resolver->resolve($options); + } catch (\Exception $e) { + if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) { + throw $e; + } + } + + $db = $this->getDatabase(); + $now = Factory::getDate()->toSql(); + + // Get lock on the table to help with concurrency issues + $db->lockTable(self::TASK_TABLE); + + // If concurrency is not allowed, we only get a task if another one does not have a "lock" + if (!$options['allowConcurrent']) { + // Get count of locked (presumed running) tasks + $lockCountQuery = $db->getQuery(true) + ->from($db->quoteName(self::TASK_TABLE)) + ->select('COUNT(id)') + ->where($db->quoteName('locked') . ' IS NOT NULL'); + + try { + $runningCount = $db->setQuery($lockCountQuery)->loadResult(); + } catch (\RuntimeException $e) { + $db->unlockTables(); + + return null; + } + + if ($runningCount !== 0) { + $db->unlockTables(); + + return null; + } + } + + $lockQuery = $db->getQuery(true); + + $lockQuery->update($db->quoteName(self::TASK_TABLE)) + ->set($db->quoteName('locked') . ' = :now1') + ->bind(':now1', $now); + + // Array of all active routine ids + $activeRoutines = array_map( + static function (TaskOption $taskOption): string { + return $taskOption->id; + }, + SchedulerHelper::getTaskOptions()->options + ); + + // "Orphaned" tasks are not a part of the task queue! + $lockQuery->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); + + // If directed, exclude CLI exclusive tasks + if (!$options['includeCliExclusive']) { + $lockQuery->where($db->quoteName('cli_exclusive') . ' = 0'); + } + + if (!$options['bypassScheduling']) { + $lockQuery->where($db->quoteName('next_execution') . ' <= :now2') + ->bind(':now2', $now); + } + + if ($options['allowDisabled']) { + $lockQuery->whereIn($db->quoteName('state'), [0, 1]); + } else { + $lockQuery->where($db->quoteName('state') . ' = 1'); + } + + if ($options['id'] > 0) { + $lockQuery->where($db->quoteName('id') . ' = :taskId') + ->bind(':taskId', $options['id'], ParameterType::INTEGER); + } + // Pick from the front of the task queue if no 'id' is specified + else { + // Get the id of the next task in the task queue + $idQuery = $db->getQuery(true) + ->from($db->quoteName(self::TASK_TABLE)) + ->select($db->quoteName('id')) + ->where($db->quoteName('state') . ' = 1') + ->order($db->quoteName('priority') . ' DESC') + ->order($db->quoteName('next_execution') . ' ASC') + ->setLimit(1); + + try { + $ids = $db->setQuery($idQuery)->loadColumn(); + } catch (\RuntimeException $e) { + $db->unlockTables(); + + return null; + } + + if (count($ids) === 0) { + $db->unlockTables(); + + return null; + } + + $lockQuery->whereIn($db->quoteName('id'), $ids); + } + + try { + $db->setQuery($lockQuery)->execute(); + } catch (\RuntimeException $e) { + } finally { + $affectedRows = $db->getAffectedRows(); + + $db->unlockTables(); + } + + if ($affectedRows != 1) { + /* + // @todo + // ? Fatal failure handling here? + // ! Question is, how? If we check for tasks running beyond there time here, we have no way of + // ! what's already been notified (since we're not auto-unlocking/recovering tasks anymore). + // The solution __may__ be in a "last_successful_finish" (or something) column. + */ + + return null; + } + + $getQuery = $db->getQuery(true); + + $getQuery->select('*') + ->from($db->quoteName(self::TASK_TABLE)) + ->where($db->quoteName('locked') . ' = :now') + ->bind(':now', $now); + + $task = $db->setQuery($getQuery)->loadObject(); + + $task->execution_rules = json_decode($task->execution_rules); + $task->cron_rules = json_decode($task->cron_rules); + + $task->taskOption = SchedulerHelper::getTaskOptions()->findOption($task->type); + + return $task; + } + + /** + * Set up an {@see OptionsResolver} to resolve options compatible with the {@see GetTask()} method. + * + * @param OptionsResolver $resolver The {@see OptionsResolver} instance to set up. + * + * @return OptionsResolver + * + * @since 4.1.0 + * @throws AccessException + */ + public static function configureTaskGetterOptions(OptionsResolver $resolver): OptionsResolver + { + $resolver->setDefaults( + [ + 'id' => 0, + 'allowDisabled' => false, + 'bypassScheduling' => false, + 'allowConcurrent' => false, + 'includeCliExclusive' => true, + ] + ) + ->setAllowedTypes('id', 'numeric') + ->setAllowedTypes('allowDisabled', 'bool') + ->setAllowedTypes('bypassScheduling', 'bool') + ->setAllowedTypes('allowConcurrent', 'bool') + ->setAllowedTypes('includeCliExclusive', 'bool'); + + return $resolver; + } + + /** + * @param array $data The form data + * + * @return boolean True on success, false on failure + * + * @since 4.1.0 + * @throws \Exception + */ + public function save($data): bool + { + $id = (int) ($data['id'] ?? $this->getState('task.id')); + $isNew = $id === 0; + + // Clean up execution rules + $data['execution_rules'] = $this->processExecutionRules($data['execution_rules']); + + // If a new entry, we'll have to put in place a pseudo-last_execution + if ($isNew) { + $basisDayOfMonth = $data['execution_rules']['exec-day']; + [$basisHour, $basisMinute] = explode(':', $data['execution_rules']['exec-time']); + + $data['last_execution'] = Factory::getDate('now', 'GMT')->format('Y-m') + . "-$basisDayOfMonth $basisHour:$basisMinute:00"; + } else { + $data['last_execution'] = $this->getItem($id)->last_execution; + } + + // Build the `cron_rules` column from `execution_rules` + $data['cron_rules'] = $this->buildExecutionRules($data['execution_rules']); + + // `next_execution` would be null if scheduling is disabled with the "manual" rule! + $data['next_execution'] = (new ExecRuleHelper($data))->nextExec(); + + if ($isNew) { + $data['last_execution'] = null; + } + + // If no params, we set as empty array. + // ? Is this the right place to do this + $data['params'] = $data['params'] ?? []; + + // Parent method takes care of saving to the table + return parent::save($data); + } + + /** + * Clean up and standardise execution rules + * + * @param array $unprocessedRules The form data [? can just replace with execution_interval] + * + * @return array Processed rules + * + * @since 4.1.0 + */ + private function processExecutionRules(array $unprocessedRules): array + { + $executionRules = $unprocessedRules; + + $ruleType = $executionRules['rule-type']; + $retainKeys = ['rule-type', $ruleType, 'exec-day', 'exec-time']; + $executionRules = array_intersect_key($executionRules, array_flip($retainKeys)); + + // Default to current date-time in UTC/GMT as the basis + $executionRules['exec-day'] = $executionRules['exec-day'] ?: (string) gmdate('d'); + $executionRules['exec-time'] = $executionRules['exec-time'] ?: (string) gmdate('H:i'); + + // If custom ruleset, sort it + // ? Is this necessary + if ($ruleType === 'cron-expression') { + foreach ($executionRules['cron-expression'] as &$values) { + sort($values); + } + } + + return $executionRules; + } + + /** + * Private method to build execution expression from input execution rules. + * This expression is used internally to determine execution times/conditions. + * + * @param array $executionRules Execution rules from the Task form, post-processing. + * + * @return array + * + * @since 4.1.0 + * @throws \Exception + */ + private function buildExecutionRules(array $executionRules): array + { + // Maps interval strings, use with sprintf($map[intType], $interval) + $intervalStringMap = [ + 'minutes' => 'PT%dM', + 'hours' => 'PT%dH', + 'days' => 'P%dD', + 'months' => 'P%dM', + 'years' => 'P%dY', + ]; + + $ruleType = $executionRules['rule-type']; + $ruleClass = strpos($ruleType, 'interval') === 0 ? 'interval' : $ruleType; + $buildExpression = ''; + + if ($ruleClass === 'interval') { + // Rule type for intervals interval- + $intervalType = explode('-', $ruleType)[1]; + $interval = $executionRules["interval-$intervalType"]; + $buildExpression = sprintf($intervalStringMap[$intervalType], $interval); + } + + if ($ruleClass === 'cron-expression') { + // ! custom matches are disabled in the form + $matches = $executionRules['cron-expression']; + $buildExpression .= $this->wildcardIfMatch($matches['minutes'], range(0, 59), true); + $buildExpression .= ' ' . $this->wildcardIfMatch($matches['hours'], range(0, 23), true); + $buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_month'], range(1, 31), true); + $buildExpression .= ' ' . $this->wildcardIfMatch($matches['months'], range(1, 12), true); + $buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_week'], range(0, 6), true); + } + + return [ + 'type' => $ruleClass, + 'exp' => $buildExpression, + ]; + } + + /** + * This method releases "locks" on a set of tasks from the database. + * These locks are pseudo-locks that are used to keep a track of running tasks. However, they require require manual + * intervention to release these locks in cases such as when a task process crashes, leaving the task "locked". + * + * @param array $pks A list of the primary keys to unlock. + * + * @return boolean True on success. + * + * @since 4.1.0 + * @throws \RuntimeException|\UnexpectedValueException|\BadMethodCallException + */ + public function unlock(array &$pks): bool + { + /** @var TaskTable $table */ + $table = $this->getTable(); + + $user = Factory::getApplication()->getIdentity(); + + $context = $this->option . '.' . $this->name; + + // Include the plugins for the change of state event. + PluginHelper::importPlugin($this->events_map['unlock']); + + // Access checks. + foreach ($pks as $i => $pk) { + $table->reset(); + + if ($table->load($pk)) { + if (!$this->canEditState($table)) { + // Prune items that you can't change. + unset($pks[$i]); + Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + + return false; + } + + // Prune items that are already at the given state. + $lockedColumnName = $table->getColumnAlias('locked'); + + if (property_exists($table, $lockedColumnName) && \is_null($table->get($lockedColumnName))) { + unset($pks[$i]); + } + } + } + + // Check if there are items to change. + if (!\count($pks)) { + return true; + } + + $event = AbstractEvent::create( + $this->event_before_unlock, + [ + 'subject' => $this, + 'context' => $context, + 'pks' => $pks, + ] + ); + + try { + Factory::getApplication()->getDispatcher()->dispatch($this->event_before_unlock, $event); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Attempt to unlock the records. + if (!$table->unlock($pks, $user->id)) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after unlock event + $event = AbstractEvent::create( + $this->event_unlock, + [ + 'subject' => $this, + 'context' => $context, + 'pks' => $pks, + ] + ); + + try { + Factory::getApplication()->getDispatcher()->dispatch($this->event_unlock, $event); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Determine if an array is populated by all its possible values by comparison to a reference array, if found a + * match a wildcard '*' is returned. + * + * @param array $target The target array + * @param array $reference The reference array, populated by the complete set of possible values in $target + * @param bool $targetToInt If true, converts $target array values to integers before comparing + * + * @return string A wildcard string if $target is fully populated, else $target itself. + * + * @since 4.1.0 + */ + private function wildcardIfMatch(array $target, array $reference, bool $targetToInt = false): string + { + if ($targetToInt) { + $target = array_map( + static function (string $x): int { + return (int) $x; + }, + $target + ); + } + + $isMatch = array_diff($reference, $target) === []; + + return $isMatch ? "*" : implode(',', $target); + } + + /** + * Method to allow derived classes to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 4.1.0 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content'): void + { + // Load the 'task' plugin group + PluginHelper::importPlugin('task'); + + // Let the parent method take over + parent::preprocessForm($form, $data, $group); + } } diff --git a/administrator/components/com_scheduler/src/Model/TasksModel.php b/administrator/components/com_scheduler/src/Model/TasksModel.php index e8f635d636830..477df8f5c405a 100644 --- a/administrator/components/com_scheduler/src/Model/TasksModel.php +++ b/administrator/components/com_scheduler/src/Model/TasksModel.php @@ -1,4 +1,5 @@ getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.type'); - $id .= ':' . $this->getState('filter.orphaned'); - $id .= ':' . $this->getState('filter.due'); - $id .= ':' . $this->getState('filter.locked'); - $id .= ':' . $this->getState('filter.trigger'); - $id .= ':' . $this->getState('list.select'); - - return parent::getStoreId($id); - } - - /** - * Method to create a query for a list of items. - * - * @return QueryInterface - * - * @since 4.1.0 - * @throws \Exception - */ - protected function getListQuery(): QueryInterface - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - /** - * Select the required fields from the table. - * ? Do we need all these defaults ? - * ? Does 'list.select' exist ? - */ - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.asset_id'), - $db->quoteName('a.title'), - $db->quoteName('a.type'), - $db->quoteName('a.execution_rules'), - $db->quoteName('a.state'), - $db->quoteName('a.last_exit_code'), - $db->quoteName('a.locked'), - $db->quoteName('a.last_execution'), - $db->quoteName('a.next_execution'), - $db->quoteName('a.times_executed'), - $db->quoteName('a.times_failed'), - $db->quoteName('a.priority'), - $db->quoteName('a.ordering'), - $db->quoteName('a.note'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - ] - ) - ) - ->select( - [ - $db->quoteName('uc.name', 'editor'), - ] - ) - ->from($db->quoteName('#__scheduler_tasks', 'a')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Filters go below - $filterCount = 0; - - /** - * Extends query if already filtered. - * - * @param string $outerGlue - * @param array $conditions - * @param string $innerGlue - * - * @since 4.1.0 - */ - $extendWhereIfFiltered = static function ( - string $outerGlue, - array $conditions, - string $innerGlue - ) use ($query, &$filterCount) { - if ($filterCount++) - { - $query->extendWhere($outerGlue, $conditions, $innerGlue); - } - else - { - $query->where($conditions, $innerGlue); - } - - }; - - // Filter over ID, title (redundant to search, but) --- - if (is_numeric($id = $this->getState('filter.id'))) - { - $filterCount++; - $id = (int) $id; - $query->where($db->qn('a.id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - } - elseif ($title = $this->getState('filter.title')) - { - $filterCount++; - $match = "%$title%"; - $query->where($db->qn('a.title') . ' LIKE :match') - ->bind(':match', $match); - } - - // Filter orphaned (-1: exclude, 0: include, 1: only) ---- - $filterOrphaned = (int) $this->getState('filter.orphaned'); - - if ($filterOrphaned !== 0) - { - $filterCount++; - $taskOptions = SchedulerHelper::getTaskOptions(); - - // Array of all active routine ids - $activeRoutines = array_map( - static function (TaskOption $taskOption): string - { - return $taskOption->id; - }, - $taskOptions->options - ); - - if ($filterOrphaned === -1) - { - $query->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); - } - else - { - $query->whereNotIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); - } - } - - // Filter over state ---- - $state = $this->getState('filter.state'); - - if ($state !== '*') - { - $filterCount++; - - if (is_numeric($state)) - { - $state = (int) $state; - - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - else - { - $query->whereIn($db->quoteName('a.state'), [0, 1]); - } - } - - // Filter over type ---- - $typeFilter = $this->getState('filter.type'); - - if ($typeFilter) - { - $filterCount++; - $query->where($db->quotename('a.type') . '= :type') - ->bind(':type', $typeFilter); - } - - // Filter over exit code ---- - $exitCode = $this->getState('filter.last_exit_code'); - - if (is_numeric($exitCode)) - { - $filterCount++; - $exitCode = (int) $exitCode; - $query->where($db->quoteName('a.last_exit_code') . '= :last_exit_code') - ->bind(':last_exit_code', $exitCode, ParameterType::INTEGER); - } - - // Filter due (-1: exclude, 0: include, 1: only) ---- - $due = $this->getState('filter.due'); - - if (is_numeric($due) && $due != 0) - { - $now = Factory::getDate('now', 'GMT')->toSql(); - $operator = $due == 1 ? ' <= ' : ' > '; - $filterCount++; - $query->where($db->qn('a.next_execution') . $operator . ':now') - ->bind(':now', $now); - } - - /* - * Filter locked --- - * Locks can be either hard locks or soft locks. Locks that have expired (exceeded the task timeout) are soft - * locks. Hard-locked tasks are assumed to be running. Soft-locked tasks are assumed to have suffered a fatal - * failure. - * {-2: exclude-all, -1: exclude-hard-locked, 0: include, 1: include-only-locked, 2: include-only-soft-locked} - */ - $locked = $this->getState('filter.locked'); - - if (is_numeric($locked) && $locked != 0) - { - $now = Factory::getDate('now', 'GMT'); - $timeout = ComponentHelper::getParams('com_scheduler')->get('timeout', 300); - $timeout = new \DateInterval(sprintf('PT%dS', $timeout)); - $timeoutThreshold = (clone $now)->sub($timeout)->toSql(); - $now = $now->toSql(); - - switch ($locked) - { - case -2: - $query->where($db->qn('a.locked') . 'IS NULL'); - break; - case -1: - $extendWhereIfFiltered( - 'AND', - [ - $db->qn('a.locked') . ' IS NULL', - $db->qn('a.locked') . ' < :threshold', - ], - 'OR' - ); - $query->bind(':threshold', $timeoutThreshold); - break; - case 1: - $query->where($db->qn('a.locked') . ' IS NOT NULL'); - break; - case 2: - $query->where($db->qn('a.locked') . ' < :threshold') - ->bind(':threshold', $timeoutThreshold); - } - } - - // Filter over search string if set (title, type title, note, id) ---- - $searchStr = $this->getState('filter.search'); - - if (!empty($searchStr)) - { - // Allow search by ID - if (stripos($searchStr, 'id:') === 0) - { - // Add array support [?] - $id = (int) substr($searchStr, 3); - $query->where($db->quoteName('a.id') . '= :id') - ->bind(':id', $id, ParameterType::INTEGER); - } - // Search by type is handled exceptionally in _getList() [@todo: remove refs] - elseif (stripos($searchStr, 'type:') !== 0) - { - $searchStr = "%$searchStr%"; - - // Bind keys to query - $query->bind(':title', $searchStr) - ->bind(':note', $searchStr); - $conditions = [ - $db->quoteName('a.title') . ' LIKE :title', - $db->quoteName('a.note') . ' LIKE :note', - ]; - $extendWhereIfFiltered('AND', $conditions, 'OR'); - } - } - - // Add list ordering clause. ---- - // @todo implement multi-column ordering someway - $multiOrdering = $this->state->get('list.multi_ordering'); - - if (!$multiOrdering || !\is_array($multiOrdering)) - { - $orderCol = $this->state->get('list.ordering', 'a.title'); - $orderDir = $this->state->get('list.direction', 'asc'); - - // Type title ordering is handled exceptionally in _getList() - if ($orderCol !== 'j.type_title') - { - $query->order($db->quoteName($orderCol) . ' ' . $orderDir); - - // If ordering by type or state, also order by title. - if (\in_array($orderCol, ['a.type', 'a.state', 'a.priority'])) - { - // @todo : Test if things are working as expected - $query->order($db->quoteName('a.title') . ' ' . $orderDir); - } - } - } - else - { - // @todo Should add quoting here - $query->order($multiOrdering); - } - - return $query; - } - - /** - * Overloads the parent _getList() method. - * Takes care of attaching TaskOption objects and sorting by type titles. - * - * @param DatabaseQuery $query The database query to get the list with - * @param int $limitstart The list offset - * @param int $limit Number of list items to fetch - * - * @return object[] - * - * @since 4.1.0 - * @throws \Exception - */ - protected function _getList($query, $limitstart = 0, $limit = 0): array - { - // Get stuff from the model state - $listOrder = $this->getState('list.ordering', 'a.title'); - $listDirectionN = strtolower($this->getState('list.direction', 'asc')) == 'desc' ? -1 : 1; - - // Set limit parameters and get object list - $query->setLimit($limit, $limitstart); - $this->getDatabase()->setQuery($query); - - // Return optionally an extended class. - // @todo: Use something other than CMSObject.. - if ($this->getState('list.customClass')) - { - $responseList = array_map( - static function (array $arr) { - $o = new CMSObject; - - foreach ($arr as $k => $v) - { - $o->{$k} = $v; - } - - return $o; - }, - $this->getDatabase()->loadAssocList() ?: [] - ); - } - else - { - $responseList = $this->getDatabase()->loadObjectList(); - } - - // Attach TaskOptions objects and a safe type title - $this->attachTaskOptions($responseList); - - // If ordering by non-db fields, we need to sort here in code - if ($listOrder == 'j.type_title') - { - $responseList = ArrayHelper::sortObjects($responseList, 'safeTypeTitle', $listDirectionN, true, false); - } - - return $responseList; - } - - /** - * For an array of items, attaches TaskOption objects and (safe) type titles to each. - * - * @param array $items Array of items, passed by reference - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - private function attachTaskOptions(array $items): void - { - $taskOptions = SchedulerHelper::getTaskOptions(); - - foreach ($items as $item) - { - $item->taskOption = $taskOptions->findOption($item->type); - $item->safeTypeTitle = $item->taskOption->title ?? Text::_('JGLOBAL_NONAPPLICABLE'); - } - } - - /** - * Proxy for the parent method. - * Sets ordering defaults. - * - * @param string $ordering Field to order/sort list by - * @param string $direction Direction in which to sort list - * - * @return void - * @since 4.1.0 - */ - protected function populateState($ordering = 'a.title', $direction = 'ASC'): void - { - // Call the parent method - parent::populateState($ordering, $direction); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface|null $factory The factory. + * + * @since 4.1.0 + * @throws \Exception + * @see \JControllerLegacy + */ + public function __construct($config = [], MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = [ + 'id', 'a.id', + 'asset_id', 'a.asset_id', + 'title', 'a.title', + 'type', 'a.type', + 'type_title', 'j.type_title', + 'state', 'a.state', + 'last_exit_code', 'a.last_exit_code', + 'last_execution', 'a.last_execution', + 'next_execution', 'a.next_execution', + 'times_executed', 'a.times_executed', + 'times_failed', 'a.times_failed', + 'ordering', 'a.ordering', + 'priority', 'a.priority', + 'note', 'a.note', + 'created', 'a.created', + 'created_by', 'a.created_by', + ]; + } + + parent::__construct($config, $factory); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 4.1.0 + */ + protected function getStoreId($id = ''): string + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.type'); + $id .= ':' . $this->getState('filter.orphaned'); + $id .= ':' . $this->getState('filter.due'); + $id .= ':' . $this->getState('filter.locked'); + $id .= ':' . $this->getState('filter.trigger'); + $id .= ':' . $this->getState('list.select'); + + return parent::getStoreId($id); + } + + /** + * Method to create a query for a list of items. + * + * @return QueryInterface + * + * @since 4.1.0 + * @throws \Exception + */ + protected function getListQuery(): QueryInterface + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + /** + * Select the required fields from the table. + * ? Do we need all these defaults ? + * ? Does 'list.select' exist ? + */ + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.asset_id'), + $db->quoteName('a.title'), + $db->quoteName('a.type'), + $db->quoteName('a.execution_rules'), + $db->quoteName('a.state'), + $db->quoteName('a.last_exit_code'), + $db->quoteName('a.locked'), + $db->quoteName('a.last_execution'), + $db->quoteName('a.next_execution'), + $db->quoteName('a.times_executed'), + $db->quoteName('a.times_failed'), + $db->quoteName('a.priority'), + $db->quoteName('a.ordering'), + $db->quoteName('a.note'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + ] + ) + ) + ->select( + [ + $db->quoteName('uc.name', 'editor'), + ] + ) + ->from($db->quoteName('#__scheduler_tasks', 'a')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Filters go below + $filterCount = 0; + + /** + * Extends query if already filtered. + * + * @param string $outerGlue + * @param array $conditions + * @param string $innerGlue + * + * @since 4.1.0 + */ + $extendWhereIfFiltered = static function ( + string $outerGlue, + array $conditions, + string $innerGlue + ) use ( + $query, + &$filterCount +) { + if ($filterCount++) { + $query->extendWhere($outerGlue, $conditions, $innerGlue); + } else { + $query->where($conditions, $innerGlue); + } + }; + + // Filter over ID, title (redundant to search, but) --- + if (is_numeric($id = $this->getState('filter.id'))) { + $filterCount++; + $id = (int) $id; + $query->where($db->qn('a.id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + } elseif ($title = $this->getState('filter.title')) { + $filterCount++; + $match = "%$title%"; + $query->where($db->qn('a.title') . ' LIKE :match') + ->bind(':match', $match); + } + + // Filter orphaned (-1: exclude, 0: include, 1: only) ---- + $filterOrphaned = (int) $this->getState('filter.orphaned'); + + if ($filterOrphaned !== 0) { + $filterCount++; + $taskOptions = SchedulerHelper::getTaskOptions(); + + // Array of all active routine ids + $activeRoutines = array_map( + static function (TaskOption $taskOption): string { + return $taskOption->id; + }, + $taskOptions->options + ); + + if ($filterOrphaned === -1) { + $query->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); + } else { + $query->whereNotIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); + } + } + + // Filter over state ---- + $state = $this->getState('filter.state'); + + if ($state !== '*') { + $filterCount++; + + if (is_numeric($state)) { + $state = (int) $state; + + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } else { + $query->whereIn($db->quoteName('a.state'), [0, 1]); + } + } + + // Filter over type ---- + $typeFilter = $this->getState('filter.type'); + + if ($typeFilter) { + $filterCount++; + $query->where($db->quotename('a.type') . '= :type') + ->bind(':type', $typeFilter); + } + + // Filter over exit code ---- + $exitCode = $this->getState('filter.last_exit_code'); + + if (is_numeric($exitCode)) { + $filterCount++; + $exitCode = (int) $exitCode; + $query->where($db->quoteName('a.last_exit_code') . '= :last_exit_code') + ->bind(':last_exit_code', $exitCode, ParameterType::INTEGER); + } + + // Filter due (-1: exclude, 0: include, 1: only) ---- + $due = $this->getState('filter.due'); + + if (is_numeric($due) && $due != 0) { + $now = Factory::getDate('now', 'GMT')->toSql(); + $operator = $due == 1 ? ' <= ' : ' > '; + $filterCount++; + $query->where($db->qn('a.next_execution') . $operator . ':now') + ->bind(':now', $now); + } + + /* + * Filter locked --- + * Locks can be either hard locks or soft locks. Locks that have expired (exceeded the task timeout) are soft + * locks. Hard-locked tasks are assumed to be running. Soft-locked tasks are assumed to have suffered a fatal + * failure. + * {-2: exclude-all, -1: exclude-hard-locked, 0: include, 1: include-only-locked, 2: include-only-soft-locked} + */ + $locked = $this->getState('filter.locked'); + + if (is_numeric($locked) && $locked != 0) { + $now = Factory::getDate('now', 'GMT'); + $timeout = ComponentHelper::getParams('com_scheduler')->get('timeout', 300); + $timeout = new \DateInterval(sprintf('PT%dS', $timeout)); + $timeoutThreshold = (clone $now)->sub($timeout)->toSql(); + $now = $now->toSql(); + + switch ($locked) { + case -2: + $query->where($db->qn('a.locked') . 'IS NULL'); + break; + case -1: + $extendWhereIfFiltered( + 'AND', + [ + $db->qn('a.locked') . ' IS NULL', + $db->qn('a.locked') . ' < :threshold', + ], + 'OR' + ); + $query->bind(':threshold', $timeoutThreshold); + break; + case 1: + $query->where($db->qn('a.locked') . ' IS NOT NULL'); + break; + case 2: + $query->where($db->qn('a.locked') . ' < :threshold') + ->bind(':threshold', $timeoutThreshold); + } + } + + // Filter over search string if set (title, type title, note, id) ---- + $searchStr = $this->getState('filter.search'); + + if (!empty($searchStr)) { + // Allow search by ID + if (stripos($searchStr, 'id:') === 0) { + // Add array support [?] + $id = (int) substr($searchStr, 3); + $query->where($db->quoteName('a.id') . '= :id') + ->bind(':id', $id, ParameterType::INTEGER); + } + // Search by type is handled exceptionally in _getList() [@todo: remove refs] + elseif (stripos($searchStr, 'type:') !== 0) { + $searchStr = "%$searchStr%"; + + // Bind keys to query + $query->bind(':title', $searchStr) + ->bind(':note', $searchStr); + $conditions = [ + $db->quoteName('a.title') . ' LIKE :title', + $db->quoteName('a.note') . ' LIKE :note', + ]; + $extendWhereIfFiltered('AND', $conditions, 'OR'); + } + } + + // Add list ordering clause. ---- + // @todo implement multi-column ordering someway + $multiOrdering = $this->state->get('list.multi_ordering'); + + if (!$multiOrdering || !\is_array($multiOrdering)) { + $orderCol = $this->state->get('list.ordering', 'a.title'); + $orderDir = $this->state->get('list.direction', 'asc'); + + // Type title ordering is handled exceptionally in _getList() + if ($orderCol !== 'j.type_title') { + $query->order($db->quoteName($orderCol) . ' ' . $orderDir); + + // If ordering by type or state, also order by title. + if (\in_array($orderCol, ['a.type', 'a.state', 'a.priority'])) { + // @todo : Test if things are working as expected + $query->order($db->quoteName('a.title') . ' ' . $orderDir); + } + } + } else { + // @todo Should add quoting here + $query->order($multiOrdering); + } + + return $query; + } + + /** + * Overloads the parent _getList() method. + * Takes care of attaching TaskOption objects and sorting by type titles. + * + * @param DatabaseQuery $query The database query to get the list with + * @param int $limitstart The list offset + * @param int $limit Number of list items to fetch + * + * @return object[] + * + * @since 4.1.0 + * @throws \Exception + */ + protected function _getList($query, $limitstart = 0, $limit = 0): array + { + // Get stuff from the model state + $listOrder = $this->getState('list.ordering', 'a.title'); + $listDirectionN = strtolower($this->getState('list.direction', 'asc')) == 'desc' ? -1 : 1; + + // Set limit parameters and get object list + $query->setLimit($limit, $limitstart); + $this->getDatabase()->setQuery($query); + + // Return optionally an extended class. + // @todo: Use something other than CMSObject.. + if ($this->getState('list.customClass')) { + $responseList = array_map( + static function (array $arr) { + $o = new CMSObject(); + + foreach ($arr as $k => $v) { + $o->{$k} = $v; + } + + return $o; + }, + $this->getDatabase()->loadAssocList() ?: [] + ); + } else { + $responseList = $this->getDatabase()->loadObjectList(); + } + + // Attach TaskOptions objects and a safe type title + $this->attachTaskOptions($responseList); + + // If ordering by non-db fields, we need to sort here in code + if ($listOrder == 'j.type_title') { + $responseList = ArrayHelper::sortObjects($responseList, 'safeTypeTitle', $listDirectionN, true, false); + } + + return $responseList; + } + + /** + * For an array of items, attaches TaskOption objects and (safe) type titles to each. + * + * @param array $items Array of items, passed by reference + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + private function attachTaskOptions(array $items): void + { + $taskOptions = SchedulerHelper::getTaskOptions(); + + foreach ($items as $item) { + $item->taskOption = $taskOptions->findOption($item->type); + $item->safeTypeTitle = $item->taskOption->title ?? Text::_('JGLOBAL_NONAPPLICABLE'); + } + } + + /** + * Proxy for the parent method. + * Sets ordering defaults. + * + * @param string $ordering Field to order/sort list by + * @param string $direction Direction in which to sort list + * + * @return void + * @since 4.1.0 + */ + protected function populateState($ordering = 'a.title', $direction = 'ASC'): void + { + // Call the parent method + parent::populateState($ordering, $direction); + } } diff --git a/administrator/components/com_scheduler/src/Rule/ExecutionRulesRule.php b/administrator/components/com_scheduler/src/Rule/ExecutionRulesRule.php index 56f149993cb3a..3cfcc4391677d 100644 --- a/administrator/components/com_scheduler/src/Rule/ExecutionRulesRule.php +++ b/administrator/components/com_scheduler/src/Rule/ExecutionRulesRule.php @@ -1,4 +1,5 @@ ` tag for the form - * field object. - * @param mixed $value The form field value to validate. - * @param ?string $group The field name group control value. This acts as an array container for the - * field. For example if the field has `name="foo"` and the group value is set - * to "bar" then the full field name would end up being "bar[foo]". - * @param ?Registry $input An optional Registry object with the entire data set to validate against - * the entire form. - * @param ?Form $form The form object for which the field is being tested. - * - * @return boolean - * - * @since 4.1.0 - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null): bool - { - $fieldName = (string) $element['name']; - $ruleType = $input->get(self::RULE_TYPE_FIELD); + /** + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form + * field object. + * @param mixed $value The form field value to validate. + * @param ?string $group The field name group control value. This acts as an array container for the + * field. For example if the field has `name="foo"` and the group value is set + * to "bar" then the full field name would end up being "bar[foo]". + * @param ?Registry $input An optional Registry object with the entire data set to validate against + * the entire form. + * @param ?Form $form The form object for which the field is being tested. + * + * @return boolean + * + * @since 4.1.0 + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null): bool + { + $fieldName = (string) $element['name']; + $ruleType = $input->get(self::RULE_TYPE_FIELD); - if ($ruleType === $fieldName || ($ruleType === 'custom' && $group === self::CUSTOM_RULE_GROUP)) - { - return $this->validateField($element, $value, $group, $form); - } + if ($ruleType === $fieldName || ($ruleType === 'custom' && $group === self::CUSTOM_RULE_GROUP)) { + return $this->validateField($element, $value, $group, $form); + } - return true; - } + return true; + } - /** - * @param \SimpleXMLElement $element The SimpleXMLElement for the field. - * @param mixed $value The field value. - * @param ?string $group The form field group the element belongs to. - * @param Form|null $form The Form object against which the field is tested/ - * - * @return boolean True if field is valid - * - * @since 4.1.0 - */ - private function validateField(\SimpleXMLElement $element, $value, ?string $group = null, ?Form $form = null): bool - { - $elementType = (string) $element['type']; + /** + * @param \SimpleXMLElement $element The SimpleXMLElement for the field. + * @param mixed $value The field value. + * @param ?string $group The form field group the element belongs to. + * @param Form|null $form The Form object against which the field is tested/ + * + * @return boolean True if field is valid + * + * @since 4.1.0 + */ + private function validateField(\SimpleXMLElement $element, $value, ?string $group = null, ?Form $form = null): bool + { + $elementType = (string) $element['type']; - // If element is of cron type, we test against options and return - if ($elementType === 'cron') - { - return (new OptionsRule)->test($element, $value, $group, null, $form); - } + // If element is of cron type, we test against options and return + if ($elementType === 'cron') { + return (new OptionsRule())->test($element, $value, $group, null, $form); + } - // Test for a positive integer value and return - return filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); - } + // Test for a positive integer value and return + return filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); + } } diff --git a/administrator/components/com_scheduler/src/Scheduler/Scheduler.php b/administrator/components/com_scheduler/src/Scheduler/Scheduler.php index ea1c68f8dc4f3..b21ee01b088bd 100644 --- a/administrator/components/com_scheduler/src/Scheduler/Scheduler.php +++ b/administrator/components/com_scheduler/src/Scheduler/Scheduler.php @@ -1,4 +1,5 @@ 'COM_SCHEDULER_SCHEDULER_TASK_COMPLETE', - Status::WILL_RESUME => 'COM_SCHEDULER_SCHEDULER_TASK_WILL_RESUME', - Status::NO_LOCK => 'COM_SCHEDULER_SCHEDULER_TASK_LOCKED', - Status::NO_RUN => 'COM_SCHEDULER_SCHEDULER_TASK_UNLOCKED', - Status::NO_ROUTINE => 'COM_SCHEDULER_SCHEDULER_TASK_ROUTINE_NA', - ]; - - /** - * Filters for the task queue. Can be used with fetchTaskRecords(). - * - * @since 4.1.0 - * @todo remove? - */ - public const TASK_QUEUE_FILTERS = [ - 'due' => 1, - 'locked' => -1, - ]; - - /** - * List config for the task queue. Can be used with fetchTaskRecords(). - * - * @since 4.1.0 - * @todo remove? - */ - public const TASK_QUEUE_LIST_CONFIG = [ - 'multi_ordering' => ['a.priority DESC ', 'a.next_execution ASC'], - ]; - - /** - * Run a scheduled task. - * Runs a single due task from the task queue by default if $id and $title are not passed. - * - * @param array $options Array with options to configure the method's behavior. Supports: - * 1. `id`: (Optional) ID of the task to run. - * 2. `allowDisabled`: Allow running disabled tasks. - * 3. `allowConcurrent`: Allow concurrent execution, i.e., running the task when another - * task may be running. - * - * @return ?Task The task executed or null if not exists - * - * @since 4.1.0 - * @throws \RuntimeException - */ - public function runTask(array $options): ?Task - { - $resolver = new OptionsResolver; - - try - { - $this->configureTaskRunnerOptions($resolver); - } - catch (\Exception $e) - { - } - - try - { - $options = $resolver->resolve($options); - } - catch (\Exception $e) - { - if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) - { - throw $e; - } - } - - /** @var CMSApplication $app */ - $app = Factory::getApplication(); - - // ? Sure about inferring scheduling bypass? - $task = $this->getTask( - [ - 'id' => (int) $options['id'], - 'allowDisabled' => $options['allowDisabled'], - 'bypassScheduling' => (int) $options['id'] !== 0, - 'allowConcurrent' => $options['allowConcurrent'], - 'includeCliExclusive' => ($app->isClient('cli')), - ] - ); - - // ? Should this be logged? (probably, if an ID is passed?) - if (empty($task)) - { - return null; - } - - $app->getLanguage()->load('com_scheduler', JPATH_ADMINISTRATOR); - - $options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}'; - $options['text_file'] = 'joomla_scheduler.php'; - Log::addLogger($options, Log::ALL, $task->logCategory); - - $taskId = $task->get('id'); - $taskTitle = $task->get('title'); - - $task->log(Text::sprintf('COM_SCHEDULER_SCHEDULER_TASK_START', $taskId, $taskTitle), 'info'); - - // Let's try to avoid time-outs - if (\function_exists('set_time_limit')) - { - set_time_limit(0); - } - - try - { - $task->run(); - } - catch (\Exception $e) - { - // We suppress the exception here, it's still accessible with `$task->getContent()['exception']`. - } - - $executionSnapshot = $task->getContent(); - $exitCode = $executionSnapshot['status'] ?? Status::NO_EXIT; - $netDuration = $executionSnapshot['netDuration'] ?? 0; - $duration = $executionSnapshot['duration'] ?? 0; - - if (\array_key_exists($exitCode, self::LOG_TEXT)) - { - $level = in_array($exitCode, [Status::OK, Status::WILL_RESUME]) ? 'info' : 'warning'; - $task->log(Text::sprintf(self::LOG_TEXT[$exitCode], $taskId, $duration, $netDuration), $level); - - return $task; - } - - $task->log( - Text::sprintf('COM_SCHEDULER_SCHEDULER_TASK_UNKNOWN_EXIT', $taskId, $duration, $netDuration, $exitCode), - 'warning' - ); - - return $task; - } - - /** - * Set up an {@see OptionsResolver} to resolve options compatible with {@see runTask}. - * - * @param OptionsResolver $resolver The {@see OptionsResolver} instance to set up. - * - * @return void - * - * @since 4.1.0 - * @throws AccessException - */ - protected function configureTaskRunnerOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults( - [ - 'id' => 0, - 'allowDisabled' => false, - 'allowConcurrent' => false, - ] - ) - ->setAllowedTypes('id', 'numeric') - ->setAllowedTypes('allowDisabled', 'bool') - ->setAllowedTypes('allowConcurrent', 'bool'); - } - - /** - * Get the next task which is due to run, limit to a specific task when ID is given - * - * @param array $options Options for the getter, see {@see TaskModel::getTask()}. - * ! should probably also support a non-locking getter. - * - * @return Task $task The task to execute - * - * @since 4.1.0 - * @throws \RuntimeException - */ - public function getTask(array $options = []): ?Task - { - $resolver = new OptionsResolver; - - try - { - TaskModel::configureTaskGetterOptions($resolver); - } - catch (\Exception $e) - { - } - - try - { - $options = $resolver->resolve($options); - } - catch (\Exception $e) - { - if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) - { - throw $e; - } - } - - try - { - /** @var SchedulerComponent $component */ - $component = Factory::getApplication()->bootComponent('com_scheduler'); - - /** @var TaskModel $model */ - $model = $component->getMVCFactory()->createModel('Task', 'Administrator', ['ignore_request' => true]); - } - catch (\Exception $e) - { - } - - if (!isset($model)) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $task = $model->getTask($options); - - if (empty($task)) - { - return null; - } - - return new Task($task); - } - - /** - * Fetches a single scheduled task in a Task instance. - * If no id or title is specified, a due task is returned. - * - * @param int $id The task ID. - * @param bool $allowDisabled Allow disabled/trashed tasks? - * - * @return ?object A matching task record, if it exists - * - * @since 4.1.0 - * @throws \RuntimeException - */ - public function fetchTaskRecord(int $id = 0, bool $allowDisabled = false): ?object - { - $filters = []; - $listConfig = ['limit' => 1]; - - if ($id > 0) - { - $filters['id'] = $id; - } - else - { - // Filters and list config for scheduled task queue - $filters['due'] = 1; - $filters['locked'] = -1; - $listConfig['multi_ordering'] = [ - 'a.priority DESC', - 'a.next_execution ASC', - ]; - } - - if ($allowDisabled) - { - $filters['state'] = ''; - } - - return $this->fetchTaskRecords($filters, $listConfig)[0] ?? null; - } - - /** - * @param array $filters The filters to set to the model - * @param array $listConfig The list config (ordering, etc.) to set to the model - * - * @return array - * - * @since 4.1.0 - * @throws \RunTimeException - */ - public function fetchTaskRecords(array $filters, array $listConfig): array - { - $model = null; - - try - { - /** @var SchedulerComponent $component */ - $component = Factory::getApplication()->bootComponent('com_scheduler'); - - /** @var TasksModel $model */ - $model = $component->getMVCFactory() - ->createModel('Tasks', 'Administrator', ['ignore_request' => true]); - } - catch (\Exception $e) - { - } - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $model->setState('list.select', 'a.*'); - - // Default to only enabled tasks - if (!isset($filters['state'])) - { - $model->setState('filter.state', 1); - } - - // Default to including orphaned tasks - $model->setState('filter.orphaned', 0); - - // Default to ordering by ID - $model->setState('list.ordering', 'a.id'); - $model->setState('list.direction', 'ASC'); - - // List options - foreach ($listConfig as $key => $value) - { - $model->setState('list.' . $key, $value); - } - - // Filter options - foreach ($filters as $type => $filter) - { - $model->setState('filter.' . $type, $filter); - } - - return $model->getItems() ?: []; - } + private const LOG_TEXT = [ + Status::OK => 'COM_SCHEDULER_SCHEDULER_TASK_COMPLETE', + Status::WILL_RESUME => 'COM_SCHEDULER_SCHEDULER_TASK_WILL_RESUME', + Status::NO_LOCK => 'COM_SCHEDULER_SCHEDULER_TASK_LOCKED', + Status::NO_RUN => 'COM_SCHEDULER_SCHEDULER_TASK_UNLOCKED', + Status::NO_ROUTINE => 'COM_SCHEDULER_SCHEDULER_TASK_ROUTINE_NA', + ]; + + /** + * Filters for the task queue. Can be used with fetchTaskRecords(). + * + * @since 4.1.0 + * @todo remove? + */ + public const TASK_QUEUE_FILTERS = [ + 'due' => 1, + 'locked' => -1, + ]; + + /** + * List config for the task queue. Can be used with fetchTaskRecords(). + * + * @since 4.1.0 + * @todo remove? + */ + public const TASK_QUEUE_LIST_CONFIG = [ + 'multi_ordering' => ['a.priority DESC ', 'a.next_execution ASC'], + ]; + + /** + * Run a scheduled task. + * Runs a single due task from the task queue by default if $id and $title are not passed. + * + * @param array $options Array with options to configure the method's behavior. Supports: + * 1. `id`: (Optional) ID of the task to run. + * 2. `allowDisabled`: Allow running disabled tasks. + * 3. `allowConcurrent`: Allow concurrent execution, i.e., running the task when another + * task may be running. + * + * @return ?Task The task executed or null if not exists + * + * @since 4.1.0 + * @throws \RuntimeException + */ + public function runTask(array $options): ?Task + { + $resolver = new OptionsResolver(); + + try { + $this->configureTaskRunnerOptions($resolver); + } catch (\Exception $e) { + } + + try { + $options = $resolver->resolve($options); + } catch (\Exception $e) { + if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) { + throw $e; + } + } + + /** @var CMSApplication $app */ + $app = Factory::getApplication(); + + // ? Sure about inferring scheduling bypass? + $task = $this->getTask( + [ + 'id' => (int) $options['id'], + 'allowDisabled' => $options['allowDisabled'], + 'bypassScheduling' => (int) $options['id'] !== 0, + 'allowConcurrent' => $options['allowConcurrent'], + 'includeCliExclusive' => ($app->isClient('cli')), + ] + ); + + // ? Should this be logged? (probably, if an ID is passed?) + if (empty($task)) { + return null; + } + + $app->getLanguage()->load('com_scheduler', JPATH_ADMINISTRATOR); + + $options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}'; + $options['text_file'] = 'joomla_scheduler.php'; + Log::addLogger($options, Log::ALL, $task->logCategory); + + $taskId = $task->get('id'); + $taskTitle = $task->get('title'); + + $task->log(Text::sprintf('COM_SCHEDULER_SCHEDULER_TASK_START', $taskId, $taskTitle), 'info'); + + // Let's try to avoid time-outs + if (\function_exists('set_time_limit')) { + set_time_limit(0); + } + + try { + $task->run(); + } catch (\Exception $e) { + // We suppress the exception here, it's still accessible with `$task->getContent()['exception']`. + } + + $executionSnapshot = $task->getContent(); + $exitCode = $executionSnapshot['status'] ?? Status::NO_EXIT; + $netDuration = $executionSnapshot['netDuration'] ?? 0; + $duration = $executionSnapshot['duration'] ?? 0; + + if (\array_key_exists($exitCode, self::LOG_TEXT)) { + $level = in_array($exitCode, [Status::OK, Status::WILL_RESUME]) ? 'info' : 'warning'; + $task->log(Text::sprintf(self::LOG_TEXT[$exitCode], $taskId, $duration, $netDuration), $level); + + return $task; + } + + $task->log( + Text::sprintf('COM_SCHEDULER_SCHEDULER_TASK_UNKNOWN_EXIT', $taskId, $duration, $netDuration, $exitCode), + 'warning' + ); + + return $task; + } + + /** + * Set up an {@see OptionsResolver} to resolve options compatible with {@see runTask}. + * + * @param OptionsResolver $resolver The {@see OptionsResolver} instance to set up. + * + * @return void + * + * @since 4.1.0 + * @throws AccessException + */ + protected function configureTaskRunnerOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults( + [ + 'id' => 0, + 'allowDisabled' => false, + 'allowConcurrent' => false, + ] + ) + ->setAllowedTypes('id', 'numeric') + ->setAllowedTypes('allowDisabled', 'bool') + ->setAllowedTypes('allowConcurrent', 'bool'); + } + + /** + * Get the next task which is due to run, limit to a specific task when ID is given + * + * @param array $options Options for the getter, see {@see TaskModel::getTask()}. + * ! should probably also support a non-locking getter. + * + * @return Task $task The task to execute + * + * @since 4.1.0 + * @throws \RuntimeException + */ + public function getTask(array $options = []): ?Task + { + $resolver = new OptionsResolver(); + + try { + TaskModel::configureTaskGetterOptions($resolver); + } catch (\Exception $e) { + } + + try { + $options = $resolver->resolve($options); + } catch (\Exception $e) { + if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) { + throw $e; + } + } + + try { + /** @var SchedulerComponent $component */ + $component = Factory::getApplication()->bootComponent('com_scheduler'); + + /** @var TaskModel $model */ + $model = $component->getMVCFactory()->createModel('Task', 'Administrator', ['ignore_request' => true]); + } catch (\Exception $e) { + } + + if (!isset($model)) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $task = $model->getTask($options); + + if (empty($task)) { + return null; + } + + return new Task($task); + } + + /** + * Fetches a single scheduled task in a Task instance. + * If no id or title is specified, a due task is returned. + * + * @param int $id The task ID. + * @param bool $allowDisabled Allow disabled/trashed tasks? + * + * @return ?object A matching task record, if it exists + * + * @since 4.1.0 + * @throws \RuntimeException + */ + public function fetchTaskRecord(int $id = 0, bool $allowDisabled = false): ?object + { + $filters = []; + $listConfig = ['limit' => 1]; + + if ($id > 0) { + $filters['id'] = $id; + } else { + // Filters and list config for scheduled task queue + $filters['due'] = 1; + $filters['locked'] = -1; + $listConfig['multi_ordering'] = [ + 'a.priority DESC', + 'a.next_execution ASC', + ]; + } + + if ($allowDisabled) { + $filters['state'] = ''; + } + + return $this->fetchTaskRecords($filters, $listConfig)[0] ?? null; + } + + /** + * @param array $filters The filters to set to the model + * @param array $listConfig The list config (ordering, etc.) to set to the model + * + * @return array + * + * @since 4.1.0 + * @throws \RunTimeException + */ + public function fetchTaskRecords(array $filters, array $listConfig): array + { + $model = null; + + try { + /** @var SchedulerComponent $component */ + $component = Factory::getApplication()->bootComponent('com_scheduler'); + + /** @var TasksModel $model */ + $model = $component->getMVCFactory() + ->createModel('Tasks', 'Administrator', ['ignore_request' => true]); + } catch (\Exception $e) { + } + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $model->setState('list.select', 'a.*'); + + // Default to only enabled tasks + if (!isset($filters['state'])) { + $model->setState('filter.state', 1); + } + + // Default to including orphaned tasks + $model->setState('filter.orphaned', 0); + + // Default to ordering by ID + $model->setState('list.ordering', 'a.id'); + $model->setState('list.direction', 'ASC'); + + // List options + foreach ($listConfig as $key => $value) { + $model->setState('list.' . $key, $value); + } + + // Filter options + foreach ($filters as $type => $filter) { + $model->setState('filter.' . $type, $filter); + } + + return $model->getItems() ?: []; + } } diff --git a/administrator/components/com_scheduler/src/Table/TaskTable.php b/administrator/components/com_scheduler/src/Table/TaskTable.php index d06b91ec0b516..023977f421415 100644 --- a/administrator/components/com_scheduler/src/Table/TaskTable.php +++ b/administrator/components/com_scheduler/src/Table/TaskTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - - parent::__construct('#__scheduler_tasks', 'id', $db); - } - - /** - * Overloads {@see Table::check()} to perform sanity checks on properties and make sure they're - * safe to store. - * - * @return boolean True if checks pass. - * - * @since 4.1.0 - * @throws \Exception - */ - public function check(): bool - { - try - { - parent::check(); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage()); - - return false; - } - - $this->title = htmlspecialchars_decode($this->title, ENT_QUOTES); - - // Set created date if not set. - // ? Might not need since the constructor already sets this - if (!(int) $this->created) - { - $this->created = Factory::getDate()->toSql(); - } - - // @todo : Add more checks if needed - - return true; - } - - /** - * Override {@see Table::store()} to update null fields as a default, which is needed when DATETIME - * fields need to be updated to NULL. This override is needed because {@see AdminModel::save()} does not - * expose an option to pass true to Table::store(). Also ensures the `created` and `created_by` fields are - * set. - * - * @param boolean $updateNulls True to update fields even if they're null. - * - * @return boolean True if successful. - * - * @since 4.1.0 - * @throws \Exception - */ - public function store($updateNulls = true): bool - { - $isNew = empty($this->getId()); - - // Set creation date if not set for a new item. - if ($isNew && empty($this->created)) - { - $this->created = Factory::getDate()->toSql(); - } - - // Set `created_by` if not set for a new item. - if ($isNew && empty($this->created_by)) - { - $this->created_by = Factory::getApplication()->getIdentity()->id; - } - - // @todo : Should we add modified, modified_by fields? [ ] - - return parent::store($updateNulls); - } - - /** - * Returns the asset name of the entry as it appears in the {@see Asset} table. - * - * @return string The asset name. - * - * @since 4.1.0 - */ - protected function _getAssetName(): string - { - $k = $this->_tbl_key; - - return 'com_scheduler.task.' . (int) $this->$k; - } - - /** - * Override {@see Table::bind()} to bind some fields even if they're null given they're present in $src. - * This override is needed specifically for DATETIME fields, of which the `next_execution` field is updated to - * null if a task is configured to execute only on manual trigger. - * - * @param array|object $src An associative array or object to bind to the Table instance. - * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean - * - * @since 4.1.0 - */ - public function bind($src, $ignore = array()): bool - { - $fields = ['next_execution']; - - foreach ($fields as $field) - { - if (\array_key_exists($field, $src) && \is_null($src[$field])) - { - $this->$field = $src[$field]; - } - } - - return parent::bind($src, $ignore); - } - - /** - * Release pseudo-locks on a set of task records. If an empty set is passed, this method releases lock on its - * instance primary key, if available. - * - * @param integer[] $pks An optional array of primary key values to update. If not set the instance property - * value is used. - * @param ?int $userId ID of the user unlocking the tasks. - * - * @return boolean True on success; false if $pks is empty. - * - * @since 4.1.0 - * @throws QueryTypeAlreadyDefinedException|\UnexpectedValueException|\BadMethodCallException - */ - public function unlock(array $pks = [], ?int $userId = null): bool - { - // Pre-processing by observers - $event = AbstractEvent::create( - 'onTaskBeforeUnlock', - [ - 'subject' => $this, - 'pks' => $pks, - 'userId' => $userId, - ] - ); - - $this->getDispatcher()->dispatch('onTaskBeforeUnlock', $event); - - // Some pre-processing before we can work with the keys. - if (!empty($pks)) - { - foreach ($pks as $key => $pk) - { - if (!\is_array($pk)) - { - $pks[$key] = array($this->_tbl_key => $pk); - } - } - } - - // If there are no primary keys set check to see if the instance key is set and use that. - if (empty($pks)) - { - $pk = []; - - foreach ($this->_tbl_keys as $key) - { - if ($this->$key) - { - $pk[$key] = $this->$key; - } - // We don't have a full primary key - return false. - else - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED')); - - return false; - } - } - - $pks = [$pk]; - } - - $lockedField = $this->getColumnAlias('locked'); - - foreach ($pks as $pk) - { - // Update the publishing state for rows with the given primary keys. - $query = $this->_db->getQuery(true) - ->update($this->_tbl) - ->set($this->_db->quoteName($lockedField) . ' = NULL'); - - // Build the WHERE clause for the primary keys. - $this->appendPrimaryKeys($query, $pk); - - $this->_db->setQuery($query); - - try - { - $this->_db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // If the Table instance value is in the list of primary keys that were set, set the instance. - $ours = true; - - foreach ($this->_tbl_keys as $key) - { - if ($this->$key != $pk[$key]) - { - $ours = false; - } - } - - if ($ours) - { - $this->$lockedField = null; - } - } - - // Pre-processing by observers - $event = AbstractEvent::create( - 'onTaskAfterUnlock', - [ - 'subject' => $this, - 'pks' => $pks, - 'userId' => $userId, - ] - ); - - $this->getDispatcher()->dispatch('onTaskAfterUnlock', $event); - - return true; - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.1.1 + */ + protected $_supportNullValue = true; + + /** + * Ensure params are json encoded by the bind method. + * + * @var string[] + * @since 4.1.0 + */ + protected $_jsonEncode = ['params', 'execution_rules', 'cron_rules']; + + /** + * The 'created' column. + * + * @var string + * @since 4.1.0 + */ + public $created; + + /** + * The 'title' column. + * + * @var string + * @since 4.1.0 + */ + public $title; + + /** + * @var string + * @since 4.1.0 + */ + public $typeAlias = 'com_scheduler.task'; + + /** + * TaskTable constructor override, needed to pass the DB table name and primary key to {@see Table::__construct()}. + * + * @param DatabaseDriver $db A database connector object. + * + * @since 4.1.0 + */ + public function __construct(DatabaseDriver $db) + { + $this->setColumnAlias('published', 'state'); + + parent::__construct('#__scheduler_tasks', 'id', $db); + } + + /** + * Overloads {@see Table::check()} to perform sanity checks on properties and make sure they're + * safe to store. + * + * @return boolean True if checks pass. + * + * @since 4.1.0 + * @throws \Exception + */ + public function check(): bool + { + try { + parent::check(); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage()); + + return false; + } + + $this->title = htmlspecialchars_decode($this->title, ENT_QUOTES); + + // Set created date if not set. + // ? Might not need since the constructor already sets this + if (!(int) $this->created) { + $this->created = Factory::getDate()->toSql(); + } + + // @todo : Add more checks if needed + + return true; + } + + /** + * Override {@see Table::store()} to update null fields as a default, which is needed when DATETIME + * fields need to be updated to NULL. This override is needed because {@see AdminModel::save()} does not + * expose an option to pass true to Table::store(). Also ensures the `created` and `created_by` fields are + * set. + * + * @param boolean $updateNulls True to update fields even if they're null. + * + * @return boolean True if successful. + * + * @since 4.1.0 + * @throws \Exception + */ + public function store($updateNulls = true): bool + { + $isNew = empty($this->getId()); + + // Set creation date if not set for a new item. + if ($isNew && empty($this->created)) { + $this->created = Factory::getDate()->toSql(); + } + + // Set `created_by` if not set for a new item. + if ($isNew && empty($this->created_by)) { + $this->created_by = Factory::getApplication()->getIdentity()->id; + } + + // @todo : Should we add modified, modified_by fields? [ ] + + return parent::store($updateNulls); + } + + /** + * Returns the asset name of the entry as it appears in the {@see Asset} table. + * + * @return string The asset name. + * + * @since 4.1.0 + */ + protected function _getAssetName(): string + { + $k = $this->_tbl_key; + + return 'com_scheduler.task.' . (int) $this->$k; + } + + /** + * Override {@see Table::bind()} to bind some fields even if they're null given they're present in $src. + * This override is needed specifically for DATETIME fields, of which the `next_execution` field is updated to + * null if a task is configured to execute only on manual trigger. + * + * @param array|object $src An associative array or object to bind to the Table instance. + * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean + * + * @since 4.1.0 + */ + public function bind($src, $ignore = array()): bool + { + $fields = ['next_execution']; + + foreach ($fields as $field) { + if (\array_key_exists($field, $src) && \is_null($src[$field])) { + $this->$field = $src[$field]; + } + } + + return parent::bind($src, $ignore); + } + + /** + * Release pseudo-locks on a set of task records. If an empty set is passed, this method releases lock on its + * instance primary key, if available. + * + * @param integer[] $pks An optional array of primary key values to update. If not set the instance property + * value is used. + * @param ?int $userId ID of the user unlocking the tasks. + * + * @return boolean True on success; false if $pks is empty. + * + * @since 4.1.0 + * @throws QueryTypeAlreadyDefinedException|\UnexpectedValueException|\BadMethodCallException + */ + public function unlock(array $pks = [], ?int $userId = null): bool + { + // Pre-processing by observers + $event = AbstractEvent::create( + 'onTaskBeforeUnlock', + [ + 'subject' => $this, + 'pks' => $pks, + 'userId' => $userId, + ] + ); + + $this->getDispatcher()->dispatch('onTaskBeforeUnlock', $event); + + // Some pre-processing before we can work with the keys. + if (!empty($pks)) { + foreach ($pks as $key => $pk) { + if (!\is_array($pk)) { + $pks[$key] = array($this->_tbl_key => $pk); + } + } + } + + // If there are no primary keys set check to see if the instance key is set and use that. + if (empty($pks)) { + $pk = []; + + foreach ($this->_tbl_keys as $key) { + if ($this->$key) { + $pk[$key] = $this->$key; + } + // We don't have a full primary key - return false. + else { + $this->setError(Text::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED')); + + return false; + } + } + + $pks = [$pk]; + } + + $lockedField = $this->getColumnAlias('locked'); + + foreach ($pks as $pk) { + // Update the publishing state for rows with the given primary keys. + $query = $this->_db->getQuery(true) + ->update($this->_tbl) + ->set($this->_db->quoteName($lockedField) . ' = NULL'); + + // Build the WHERE clause for the primary keys. + $this->appendPrimaryKeys($query, $pk); + + $this->_db->setQuery($query); + + try { + $this->_db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // If the Table instance value is in the list of primary keys that were set, set the instance. + $ours = true; + + foreach ($this->_tbl_keys as $key) { + if ($this->$key != $pk[$key]) { + $ours = false; + } + } + + if ($ours) { + $this->$lockedField = null; + } + } + + // Pre-processing by observers + $event = AbstractEvent::create( + 'onTaskAfterUnlock', + [ + 'subject' => $this, + 'pks' => $pks, + 'userId' => $userId, + ] + ); + + $this->getDispatcher()->dispatch('onTaskAfterUnlock', $event); + + return true; + } } diff --git a/administrator/components/com_scheduler/src/Task/Status.php b/administrator/components/com_scheduler/src/Task/Status.php index 50f4673577deb..da35c8479c86d 100644 --- a/administrator/components/com_scheduler/src/Task/Status.php +++ b/administrator/components/com_scheduler/src/Task/Status.php @@ -1,4 +1,5 @@ 'trashed', - self::STATE_DISABLED => 'disabled', - self::STATE_ENABLED => 'enabled', - ]; - - /** - * The task snapshot - * - * @var array - * @since 4.1.0 - */ - protected $snapshot = []; - - /** - * @var Registry - * @since 4.1.0 - */ - protected $taskRegistry; - - /** - * @var string - * @since 4.1.0 - */ - public $logCategory; - - /** - * @var CMSApplication - * @since 4.1.0 - */ - protected $app; - - /** - * @var DatabaseInterface - * @since 4.1.0 - */ - protected $db; - - /** - * Maps task exit codes to events which should be dispatched when the task finishes. - * 'NA' maps to the event for general task failures. - * - * @var string[] - * @since 4.1.0 - */ - protected const EVENTS_MAP = [ - Status::OK => 'onTaskExecuteSuccess', - Status::NO_ROUTINE => 'onTaskRoutineNotFound', - Status::WILL_RESUME => 'onTaskRoutineWillResume', - 'NA' => 'onTaskExecuteFailure', - ]; - - /** - * Constructor for {@see Task}. - * - * @param object $record A task from {@see TaskTable}. - * - * @since 4.1.0 - * @throws \Exception - */ - public function __construct(object $record) - { - // Workaround because Registry dumps private properties otherwise. - $taskOption = $record->taskOption; - $record->params = json_decode($record->params, true); - - $this->taskRegistry = new Registry($record); - - $this->set('taskOption', $taskOption); - $this->app = Factory::getApplication(); - $this->db = Factory::getContainer()->get(DatabaseDriver::class); - $this->setLogger(Log::createDelegatedLogger()); - $this->logCategory = 'task' . $this->get('id'); - - if ($this->get('params.individual_log')) - { - $logFile = $this->get('params.log_file') ?? 'task_' . $this->get('id') . '.log.php'; - - $options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}'; - $options['text_file'] = $logFile; - Log::addLogger($options, Log::ALL, [$this->logCategory]); - } - } - - /** - * Get the task as a data object that can be stored back in the database. - * ! This method should be removed or changed as part of a better API implementation for the driver. - * - * @return object - * - * @since 4.1.0 - */ - public function getRecord(): object - { - // ! Probably, an array instead - $recObject = $this->taskRegistry->toObject(); - - $recObject->cron_rules = (array) $recObject->cron_rules; - - return $recObject; - } - - /** - * Execute the task. - * - * @return boolean True if success - * - * @since 4.1.0 - * @throws \Exception - */ - public function run(): bool - { - /** - * We try to acquire the lock here, only if we don't already have one. - * We do this, so we can support two ways of running tasks: - * 1. Directly through {@see Scheduler}, which optimises acquiring a lock while fetching from the task queue. - * 2. Running a task without a pre-acquired lock. - * ! This needs some more thought, for whether it should be allowed or if the single-query optimisation - * should be used everywhere, although it doesn't make sense in the context of fetching - * a task when it doesn't need to be run. This might be solved if we force a re-fetch - * with the lock or do it here ourselves (using acquireLock as a proxy to the model's - * getter). - */ - if ($this->get('locked') === null) - { - $this->acquireLock(); - } - - // Exit early if task routine is not available - if (!SchedulerHelper::getTaskOptions()->findOption($this->get('type'))) - { - $this->snapshot['status'] = Status::NO_ROUTINE; - $this->skipExecution(); - $this->dispatchExitEvent(); - - return $this->isSuccess(); - } - - $this->snapshot['status'] = Status::RUNNING; - $this->snapshot['taskStart'] = $this->snapshot['taskStart'] ?? microtime(true); - $this->snapshot['netDuration'] = 0; - - /** @var ExecuteTaskEvent $event */ - $event = AbstractEvent::create( - 'onExecuteTask', - [ - 'eventClass' => ExecuteTaskEvent::class, - 'subject' => $this, - 'routineId' => $this->get('type'), - 'langConstPrefix' => $this->get('taskOption')->langConstPrefix, - 'params' => $this->get('params'), - ] - ); - - PluginHelper::importPlugin('task'); - - try - { - $this->app->getDispatcher()->dispatch('onExecuteTask', $event); - } - catch (\Exception $e) - { - // Suppress the exception for now, we'll throw it again once it's safe - $this->log(Text::sprintf('COM_SCHEDULER_TASK_ROUTINE_EXCEPTION', $e->getMessage()), 'error'); - $this->snapshot['exception'] = $e; - $this->snapshot['status'] = Status::KNOCKOUT; - } - - $resultSnapshot = $event->getResultSnapshot(); - - $this->snapshot['taskEnd'] = microtime(true); - $this->snapshot['netDuration'] = $this->snapshot['taskEnd'] - $this->snapshot['taskStart']; - $this->snapshot = array_merge($this->snapshot, $resultSnapshot); - - // @todo make the ExecRuleHelper usage less ugly, perhaps it should be composed into Task - // Update object state. - $this->set('last_execution', Factory::getDate('@' . (int) $this->snapshot['taskStart'])->toSql()); - $this->set('last_exit_code', $this->snapshot['status']); - - if ($this->snapshot['status'] !== Status::WILL_RESUME) - { - $this->set('next_execution', (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec()); - $this->set('times_executed', $this->get('times_executed') + 1); - } - else - { - /** - * Resumable tasks need special handling. - * - * They are rescheduled as soon as possible to let their next step to be executed without - * a very large temporal gap to the previous step. - * - * Moreover, the times executed does NOT increase for each step. It will increase once, - * after the last step, when they return Status::OK. - */ - $this->set('next_execution', Factory::getDate('now', 'UTC')->sub(new \DateInterval('PT1M'))->toSql()); - } - - // The only acceptable "successful" statuses are either clean exit or resuming execution. - if (!in_array($this->snapshot['status'], [Status::WILL_RESUME, Status::OK])) - { - $this->set('times_failed', $this->get('times_failed') + 1); - } - - if (!$this->releaseLock()) - { - $this->snapshot['status'] = Status::NO_RELEASE; - } - - $this->dispatchExitEvent(); - - if (!empty($this->snapshot['exception'])) - { - throw $this->snapshot['exception']; - } - - return $this->isSuccess(); - } - - /** - * Get the task execution snapshot. - * ! Access locations will need updates once a more robust Snapshot container is implemented. - * - * @return array - * - * @since 4.1.0 - */ - public function getContent(): array - { - return $this->snapshot; - } - - /** - * Acquire a pseudo-lock on the task record. - * ! At the moment, this method is not used anywhere as task locks are already - * acquired when they're fetched. As such this method is not functional and should - * not be reviewed until it is updated. - * - * @return boolean - * - * @since 4.1.0 - * @throws \Exception - */ - public function acquireLock(): bool - { - $db = $this->db; - $query = $db->getQuery(true); - $id = $this->get('id'); - $now = Factory::getDate('now', 'GMT'); - - $timeout = ComponentHelper::getParams('com_scheduler')->get('timeout', 300); - $timeout = new \DateInterval(sprintf('PT%dS', $timeout)); - $timeoutThreshold = (clone $now)->sub($timeout)->toSql(); - $now = $now->toSql(); - - // @todo update or remove this method - $query->update($db->qn('#__scheduler_tasks')) - ->set('locked = :now') - ->where($db->qn('id') . ' = :taskId') - ->extendWhere( - 'AND', - [ - $db->qn('locked') . ' < :threshold', - $db->qn('locked') . 'IS NULL', - ], - 'OR' - ) - ->bind(':taskId', $id, ParameterType::INTEGER) - ->bind(':now', $now) - ->bind(':threshold', $timeoutThreshold); - - try - { - $db->lockTable('#__scheduler_tasks'); - $db->setQuery($query)->execute(); - } - catch (\RuntimeException $e) - { - return false; - } - finally - { - $db->unlockTables(); - } - - if ($db->getAffectedRows() === 0) - { - return false; - } - - $this->set('locked', $now); - - return true; - } - - /** - * Remove the pseudo-lock and optionally update the task record. - * - * @param bool $update If true, the record is updated with the snapshot - * - * @return boolean - * - * @since 4.1.0 - * @throws \Exception - */ - public function releaseLock(bool $update = true): bool - { - $db = $this->db; - $query = $db->getQuery(true); - $id = $this->get('id'); - - $query->update($db->qn('#__scheduler_tasks', 't')) - ->set('locked = NULL') - ->where($db->qn('id') . ' = :taskId') - ->where($db->qn('locked') . ' IS NOT NULL') - ->bind(':taskId', $id, ParameterType::INTEGER); - - if ($update) - { - $exitCode = $this->get('last_exit_code'); - $lastExec = $this->get('last_execution'); - $nextExec = $this->get('next_execution'); - $timesFailed = $this->get('times_failed'); - $timesExecuted = $this->get('times_executed'); - - $query->set( - [ - 'last_exit_code = :exitCode', - 'last_execution = :lastExec', - 'next_execution = :nextExec', - 'times_executed = :times_executed', - 'times_failed = :times_failed', - ] - ) - ->bind(':exitCode', $exitCode, ParameterType::INTEGER) - ->bind(':lastExec', $lastExec) - ->bind(':nextExec', $nextExec) - ->bind(':times_executed', $timesExecuted) - ->bind(':times_failed', $timesFailed); - } - - try - { - $db->setQuery($query)->execute(); - } - catch (\RuntimeException $e) - { - return false; - } - - if (!$db->getAffectedRows()) - { - return false; - } - - $this->set('locked', null); - - return true; - } - - /** - * @param string $message Log message - * @param string $priority Log level, defaults to 'info' - * - * @return void - * - * @since 4.1.0 - * @throws InvalidArgumentException - */ - public function log(string $message, string $priority = 'info'): void - { - $this->logger->log($priority, $message, ['category' => $this->logCategory]); - } - - /** - * Advance the task entry's next calculated execution, effectively skipping the current execution. - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - public function skipExecution(): void - { - $db = $this->db; - $query = $db->getQuery(true); - - $id = $this->get('id'); - $nextExec = (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec(true, true); - - $query->update($db->qn('#__scheduler_tasks', 't')) - ->set('t.next_execution = :nextExec') - ->where('t.id = :id') - ->bind(':nextExec', $nextExec) - ->bind(':id', $id); - - try - { - $db->setQuery($query)->execute(); - } - catch (\RuntimeException $e) - { - } - - $this->set('next_execution', $nextExec); - } - - /** - * Handles task exit (dispatch event). - * - * @return void - * - * @since 4.1.0 - * - * @throws \UnexpectedValueException|\BadMethodCallException - */ - protected function dispatchExitEvent(): void - { - $exitCode = $this->snapshot['status'] ?? 'NA'; - $eventName = self::EVENTS_MAP[$exitCode] ?? self::EVENTS_MAP['NA']; - - $event = AbstractEvent::create( - $eventName, - [ - 'subject' => $this, - ] - ); - - $this->app->getDispatcher()->dispatch($eventName, $event); - } - - /** - * Was the task successful? - * - * @return boolean True if the task was successful. - * @since 4.1.0 - */ - public function isSuccess(): bool - { - return in_array(($this->snapshot['status'] ?? null), [Status::OK, Status::WILL_RESUME]); - } - - /** - * Set a task property. This method is a proxy to {@see Registry::set()}. - * - * @param string $path Registry path of the task property. - * @param mixed $value The value to set to the property. - * @param ?string $separator The key separator. - * - * @return mixed|null - * - * @since 4.1.0 - */ - protected function set(string $path, $value, string $separator = null) - { - return $this->taskRegistry->set($path, $value, $separator); - } - - /** - * Get a task property. This method is a proxy to {@see Registry::get()}. - * - * @param string $path Registry path of the task property. - * @param mixed $default Default property to return, if the actual value is null. - * - * @return mixed The task property. - * - * @since 4.1.0 - */ - public function get(string $path, $default = null) - { - return $this->taskRegistry->get($path, $default); - } - - /** - * Static method to determine whether an enumerated task state (as a string) is valid. - * - * @param string $state The task state (enumerated, as a string). - * - * @return boolean - * - * @since 4.1.0 - */ - public static function isValidState(string $state): bool - { - if (!is_numeric($state)) - { - return false; - } - - // Takes care of interpreting as float/int - $state = $state + 0; - - return ArrayHelper::getValue(self::STATE_MAP, $state) !== null; - } - - /** - * Static method to determine whether a task id is valid. Note that this does not - * validate ids against the database, but only verifies that an id may exist. - * - * @param string $id The task id (as a string). - * - * @return boolean - * - * @since 4.1.0 - */ - public static function isValidId(string $id): bool - { - $id = is_numeric($id) ? ($id + 0) : $id; - - if (!\is_int($id) || $id <= 0) - { - return false; - } - - return true; - } + use LoggerAwareTrait; + + /** + * Enumerated state for enabled tasks. + * + * @since 4.1.0 + */ + const STATE_ENABLED = 1; + + /** + * Enumerated state for disabled tasks. + * + * @since 4.1.0 + */ + public const STATE_DISABLED = 0; + + /** + * Enumerated state for trashed tasks. + * + * @since 4.1.0 + */ + public const STATE_TRASHED = -2; + + /** + * Map state enumerations to logical language adjectives. + * + * @since 4.1.0 + */ + public const STATE_MAP = [ + self::STATE_TRASHED => 'trashed', + self::STATE_DISABLED => 'disabled', + self::STATE_ENABLED => 'enabled', + ]; + + /** + * The task snapshot + * + * @var array + * @since 4.1.0 + */ + protected $snapshot = []; + + /** + * @var Registry + * @since 4.1.0 + */ + protected $taskRegistry; + + /** + * @var string + * @since 4.1.0 + */ + public $logCategory; + + /** + * @var CMSApplication + * @since 4.1.0 + */ + protected $app; + + /** + * @var DatabaseInterface + * @since 4.1.0 + */ + protected $db; + + /** + * Maps task exit codes to events which should be dispatched when the task finishes. + * 'NA' maps to the event for general task failures. + * + * @var string[] + * @since 4.1.0 + */ + protected const EVENTS_MAP = [ + Status::OK => 'onTaskExecuteSuccess', + Status::NO_ROUTINE => 'onTaskRoutineNotFound', + Status::WILL_RESUME => 'onTaskRoutineWillResume', + 'NA' => 'onTaskExecuteFailure', + ]; + + /** + * Constructor for {@see Task}. + * + * @param object $record A task from {@see TaskTable}. + * + * @since 4.1.0 + * @throws \Exception + */ + public function __construct(object $record) + { + // Workaround because Registry dumps private properties otherwise. + $taskOption = $record->taskOption; + $record->params = json_decode($record->params, true); + + $this->taskRegistry = new Registry($record); + + $this->set('taskOption', $taskOption); + $this->app = Factory::getApplication(); + $this->db = Factory::getContainer()->get(DatabaseDriver::class); + $this->setLogger(Log::createDelegatedLogger()); + $this->logCategory = 'task' . $this->get('id'); + + if ($this->get('params.individual_log')) { + $logFile = $this->get('params.log_file') ?? 'task_' . $this->get('id') . '.log.php'; + + $options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}'; + $options['text_file'] = $logFile; + Log::addLogger($options, Log::ALL, [$this->logCategory]); + } + } + + /** + * Get the task as a data object that can be stored back in the database. + * ! This method should be removed or changed as part of a better API implementation for the driver. + * + * @return object + * + * @since 4.1.0 + */ + public function getRecord(): object + { + // ! Probably, an array instead + $recObject = $this->taskRegistry->toObject(); + + $recObject->cron_rules = (array) $recObject->cron_rules; + + return $recObject; + } + + /** + * Execute the task. + * + * @return boolean True if success + * + * @since 4.1.0 + * @throws \Exception + */ + public function run(): bool + { + /** + * We try to acquire the lock here, only if we don't already have one. + * We do this, so we can support two ways of running tasks: + * 1. Directly through {@see Scheduler}, which optimises acquiring a lock while fetching from the task queue. + * 2. Running a task without a pre-acquired lock. + * ! This needs some more thought, for whether it should be allowed or if the single-query optimisation + * should be used everywhere, although it doesn't make sense in the context of fetching + * a task when it doesn't need to be run. This might be solved if we force a re-fetch + * with the lock or do it here ourselves (using acquireLock as a proxy to the model's + * getter). + */ + if ($this->get('locked') === null) { + $this->acquireLock(); + } + + // Exit early if task routine is not available + if (!SchedulerHelper::getTaskOptions()->findOption($this->get('type'))) { + $this->snapshot['status'] = Status::NO_ROUTINE; + $this->skipExecution(); + $this->dispatchExitEvent(); + + return $this->isSuccess(); + } + + $this->snapshot['status'] = Status::RUNNING; + $this->snapshot['taskStart'] = $this->snapshot['taskStart'] ?? microtime(true); + $this->snapshot['netDuration'] = 0; + + /** @var ExecuteTaskEvent $event */ + $event = AbstractEvent::create( + 'onExecuteTask', + [ + 'eventClass' => ExecuteTaskEvent::class, + 'subject' => $this, + 'routineId' => $this->get('type'), + 'langConstPrefix' => $this->get('taskOption')->langConstPrefix, + 'params' => $this->get('params'), + ] + ); + + PluginHelper::importPlugin('task'); + + try { + $this->app->getDispatcher()->dispatch('onExecuteTask', $event); + } catch (\Exception $e) { + // Suppress the exception for now, we'll throw it again once it's safe + $this->log(Text::sprintf('COM_SCHEDULER_TASK_ROUTINE_EXCEPTION', $e->getMessage()), 'error'); + $this->snapshot['exception'] = $e; + $this->snapshot['status'] = Status::KNOCKOUT; + } + + $resultSnapshot = $event->getResultSnapshot(); + + $this->snapshot['taskEnd'] = microtime(true); + $this->snapshot['netDuration'] = $this->snapshot['taskEnd'] - $this->snapshot['taskStart']; + $this->snapshot = array_merge($this->snapshot, $resultSnapshot); + + // @todo make the ExecRuleHelper usage less ugly, perhaps it should be composed into Task + // Update object state. + $this->set('last_execution', Factory::getDate('@' . (int) $this->snapshot['taskStart'])->toSql()); + $this->set('last_exit_code', $this->snapshot['status']); + + if ($this->snapshot['status'] !== Status::WILL_RESUME) { + $this->set('next_execution', (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec()); + $this->set('times_executed', $this->get('times_executed') + 1); + } else { + /** + * Resumable tasks need special handling. + * + * They are rescheduled as soon as possible to let their next step to be executed without + * a very large temporal gap to the previous step. + * + * Moreover, the times executed does NOT increase for each step. It will increase once, + * after the last step, when they return Status::OK. + */ + $this->set('next_execution', Factory::getDate('now', 'UTC')->sub(new \DateInterval('PT1M'))->toSql()); + } + + // The only acceptable "successful" statuses are either clean exit or resuming execution. + if (!in_array($this->snapshot['status'], [Status::WILL_RESUME, Status::OK])) { + $this->set('times_failed', $this->get('times_failed') + 1); + } + + if (!$this->releaseLock()) { + $this->snapshot['status'] = Status::NO_RELEASE; + } + + $this->dispatchExitEvent(); + + if (!empty($this->snapshot['exception'])) { + throw $this->snapshot['exception']; + } + + return $this->isSuccess(); + } + + /** + * Get the task execution snapshot. + * ! Access locations will need updates once a more robust Snapshot container is implemented. + * + * @return array + * + * @since 4.1.0 + */ + public function getContent(): array + { + return $this->snapshot; + } + + /** + * Acquire a pseudo-lock on the task record. + * ! At the moment, this method is not used anywhere as task locks are already + * acquired when they're fetched. As such this method is not functional and should + * not be reviewed until it is updated. + * + * @return boolean + * + * @since 4.1.0 + * @throws \Exception + */ + public function acquireLock(): bool + { + $db = $this->db; + $query = $db->getQuery(true); + $id = $this->get('id'); + $now = Factory::getDate('now', 'GMT'); + + $timeout = ComponentHelper::getParams('com_scheduler')->get('timeout', 300); + $timeout = new \DateInterval(sprintf('PT%dS', $timeout)); + $timeoutThreshold = (clone $now)->sub($timeout)->toSql(); + $now = $now->toSql(); + + // @todo update or remove this method + $query->update($db->qn('#__scheduler_tasks')) + ->set('locked = :now') + ->where($db->qn('id') . ' = :taskId') + ->extendWhere( + 'AND', + [ + $db->qn('locked') . ' < :threshold', + $db->qn('locked') . 'IS NULL', + ], + 'OR' + ) + ->bind(':taskId', $id, ParameterType::INTEGER) + ->bind(':now', $now) + ->bind(':threshold', $timeoutThreshold); + + try { + $db->lockTable('#__scheduler_tasks'); + $db->setQuery($query)->execute(); + } catch (\RuntimeException $e) { + return false; + } finally { + $db->unlockTables(); + } + + if ($db->getAffectedRows() === 0) { + return false; + } + + $this->set('locked', $now); + + return true; + } + + /** + * Remove the pseudo-lock and optionally update the task record. + * + * @param bool $update If true, the record is updated with the snapshot + * + * @return boolean + * + * @since 4.1.0 + * @throws \Exception + */ + public function releaseLock(bool $update = true): bool + { + $db = $this->db; + $query = $db->getQuery(true); + $id = $this->get('id'); + + $query->update($db->qn('#__scheduler_tasks', 't')) + ->set('locked = NULL') + ->where($db->qn('id') . ' = :taskId') + ->where($db->qn('locked') . ' IS NOT NULL') + ->bind(':taskId', $id, ParameterType::INTEGER); + + if ($update) { + $exitCode = $this->get('last_exit_code'); + $lastExec = $this->get('last_execution'); + $nextExec = $this->get('next_execution'); + $timesFailed = $this->get('times_failed'); + $timesExecuted = $this->get('times_executed'); + + $query->set( + [ + 'last_exit_code = :exitCode', + 'last_execution = :lastExec', + 'next_execution = :nextExec', + 'times_executed = :times_executed', + 'times_failed = :times_failed', + ] + ) + ->bind(':exitCode', $exitCode, ParameterType::INTEGER) + ->bind(':lastExec', $lastExec) + ->bind(':nextExec', $nextExec) + ->bind(':times_executed', $timesExecuted) + ->bind(':times_failed', $timesFailed); + } + + try { + $db->setQuery($query)->execute(); + } catch (\RuntimeException $e) { + return false; + } + + if (!$db->getAffectedRows()) { + return false; + } + + $this->set('locked', null); + + return true; + } + + /** + * @param string $message Log message + * @param string $priority Log level, defaults to 'info' + * + * @return void + * + * @since 4.1.0 + * @throws InvalidArgumentException + */ + public function log(string $message, string $priority = 'info'): void + { + $this->logger->log($priority, $message, ['category' => $this->logCategory]); + } + + /** + * Advance the task entry's next calculated execution, effectively skipping the current execution. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + public function skipExecution(): void + { + $db = $this->db; + $query = $db->getQuery(true); + + $id = $this->get('id'); + $nextExec = (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec(true, true); + + $query->update($db->qn('#__scheduler_tasks', 't')) + ->set('t.next_execution = :nextExec') + ->where('t.id = :id') + ->bind(':nextExec', $nextExec) + ->bind(':id', $id); + + try { + $db->setQuery($query)->execute(); + } catch (\RuntimeException $e) { + } + + $this->set('next_execution', $nextExec); + } + + /** + * Handles task exit (dispatch event). + * + * @return void + * + * @since 4.1.0 + * + * @throws \UnexpectedValueException|\BadMethodCallException + */ + protected function dispatchExitEvent(): void + { + $exitCode = $this->snapshot['status'] ?? 'NA'; + $eventName = self::EVENTS_MAP[$exitCode] ?? self::EVENTS_MAP['NA']; + + $event = AbstractEvent::create( + $eventName, + [ + 'subject' => $this, + ] + ); + + $this->app->getDispatcher()->dispatch($eventName, $event); + } + + /** + * Was the task successful? + * + * @return boolean True if the task was successful. + * @since 4.1.0 + */ + public function isSuccess(): bool + { + return in_array(($this->snapshot['status'] ?? null), [Status::OK, Status::WILL_RESUME]); + } + + /** + * Set a task property. This method is a proxy to {@see Registry::set()}. + * + * @param string $path Registry path of the task property. + * @param mixed $value The value to set to the property. + * @param ?string $separator The key separator. + * + * @return mixed|null + * + * @since 4.1.0 + */ + protected function set(string $path, $value, string $separator = null) + { + return $this->taskRegistry->set($path, $value, $separator); + } + + /** + * Get a task property. This method is a proxy to {@see Registry::get()}. + * + * @param string $path Registry path of the task property. + * @param mixed $default Default property to return, if the actual value is null. + * + * @return mixed The task property. + * + * @since 4.1.0 + */ + public function get(string $path, $default = null) + { + return $this->taskRegistry->get($path, $default); + } + + /** + * Static method to determine whether an enumerated task state (as a string) is valid. + * + * @param string $state The task state (enumerated, as a string). + * + * @return boolean + * + * @since 4.1.0 + */ + public static function isValidState(string $state): bool + { + if (!is_numeric($state)) { + return false; + } + + // Takes care of interpreting as float/int + $state = $state + 0; + + return ArrayHelper::getValue(self::STATE_MAP, $state) !== null; + } + + /** + * Static method to determine whether a task id is valid. Note that this does not + * validate ids against the database, but only verifies that an id may exist. + * + * @param string $id The task id (as a string). + * + * @return boolean + * + * @since 4.1.0 + */ + public static function isValidId(string $id): bool + { + $id = is_numeric($id) ? ($id + 0) : $id; + + if (!\is_int($id) || $id <= 0) { + return false; + } + + return true; + } } diff --git a/administrator/components/com_scheduler/src/Task/TaskOption.php b/administrator/components/com_scheduler/src/Task/TaskOption.php index 05fc2ecdd05d8..48049a09e37bb 100644 --- a/administrator/components/com_scheduler/src/Task/TaskOption.php +++ b/administrator/components/com_scheduler/src/Task/TaskOption.php @@ -1,4 +1,5 @@ id = $type; - $this->title = Text::_("${langConstPrefix}_TITLE"); - $this->desc = Text::_("${langConstPrefix}_DESC"); - $this->langConstPrefix = $langConstPrefix; - } + /** + * TaskOption constructor. + * + * @param string $type A unique ID string for a plugin task routine. + * @param string $langConstPrefix The Language constant prefix $p. Expects $p . _TITLE and $p . _DESC to exist. + * + * @since 4.1.0 + */ + public function __construct(string $type, string $langConstPrefix) + { + $this->id = $type; + $this->title = Text::_("${langConstPrefix}_TITLE"); + $this->desc = Text::_("${langConstPrefix}_DESC"); + $this->langConstPrefix = $langConstPrefix; + } - /** - * Magic method to allow read-only access to private properties. - * - * @param string $name The object property requested. - * - * @return ?string - * - * @since 4.1.0 - */ - public function __get(string $name) - { - if (property_exists($this, $name)) - { - return $this->$name; - } + /** + * Magic method to allow read-only access to private properties. + * + * @param string $name The object property requested. + * + * @return ?string + * + * @since 4.1.0 + */ + public function __get(string $name) + { + if (property_exists($this, $name)) { + return $this->$name; + } - // Trigger a deprecation for the 'type' property (replaced with {@see id}). - if ($name === 'type') - { - try - { - Log::add( - sprintf( - 'The %1$s property is deprecated. Use %2$s instead.', - $name, - 'id' - ), - Log::WARNING, - 'deprecated' - ); - } - catch (\RuntimeException $e) - { - // Pass - } + // Trigger a deprecation for the 'type' property (replaced with {@see id}). + if ($name === 'type') { + try { + Log::add( + sprintf( + 'The %1$s property is deprecated. Use %2$s instead.', + $name, + 'id' + ), + Log::WARNING, + 'deprecated' + ); + } catch (\RuntimeException $e) { + // Pass + } - return $this->id; - } + return $this->id; + } - return null; - } + return null; + } } diff --git a/administrator/components/com_scheduler/src/Task/TaskOptions.php b/administrator/components/com_scheduler/src/Task/TaskOptions.php index 4488aab494d7d..3cab8d5d3ee3d 100644 --- a/administrator/components/com_scheduler/src/Task/TaskOptions.php +++ b/administrator/components/com_scheduler/src/Task/TaskOptions.php @@ -1,4 +1,5 @@ 'languageConstantPrefix', ... ] - * - * @return void - * - * @since 4.1.0 - */ - public function addOptions(array $taskRoutines): void - { - foreach ($taskRoutines as $routineId => $langConstPrefix) - { - $this->options[] = new TaskOption($routineId, $langConstPrefix); - } - } + /** + * A plugin can support several task routines + * This method is used by a plugin's onTaskOptionsList subscriber to advertise supported routines. + * + * @param array $taskRoutines An associative array of {@var TaskOption} constructor argument pairs: + * [ 'routineId' => 'languageConstantPrefix', ... ] + * + * @return void + * + * @since 4.1.0 + */ + public function addOptions(array $taskRoutines): void + { + foreach ($taskRoutines as $routineId => $langConstPrefix) { + $this->options[] = new TaskOption($routineId, $langConstPrefix); + } + } - /** - * @param ?string $routineId A unique identifier for a plugin task routine - * - * @return ?TaskOption A matching TaskOption if available, null otherwise - * - * @since 4.1.0 - */ - public function findOption(?string $routineId): ?TaskOption - { - if ($routineId === null) - { - return null; - } + /** + * @param ?string $routineId A unique identifier for a plugin task routine + * + * @return ?TaskOption A matching TaskOption if available, null otherwise + * + * @since 4.1.0 + */ + public function findOption(?string $routineId): ?TaskOption + { + if ($routineId === null) { + return null; + } - foreach ($this->options as $option) - { - if ($option->id === $routineId) - { - return $option; - } - } + foreach ($this->options as $option) { + if ($option->id === $routineId) { + return $option; + } + } - return null; - } + return null; + } } diff --git a/administrator/components/com_scheduler/src/Traits/TaskPluginTrait.php b/administrator/components/com_scheduler/src/Traits/TaskPluginTrait.php index 16f3ac1481542..8acf05506860a 100644 --- a/administrator/components/com_scheduler/src/Traits/TaskPluginTrait.php +++ b/administrator/components/com_scheduler/src/Traits/TaskPluginTrait.php @@ -1,4 +1,5 @@ snapshot['logCategory'] = $event->getArgument('subject')->logCategory; - $this->snapshot['plugin'] = $this->_name; - $this->snapshot['startTime'] = microtime(true); - $this->snapshot['status'] = Status::RUNNING; - } - - /** - * Set information to {@see $snapshot} when ending a routine. This information includes the routine exit code and - * timing information. - * - * @param ExecuteTaskEvent $event The event - * @param ?int $exitCode The task exit code - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - protected function endRoutine(ExecuteTaskEvent $event, int $exitCode): void - { - if (!$this instanceof CMSPlugin) - { - return; - } - - $this->snapshot['endTime'] = $endTime = microtime(true); - $this->snapshot['duration'] = $endTime - $this->snapshot['startTime']; - $this->snapshot['status'] = $exitCode ?? Status::OK; - $event->setResult($this->snapshot); - } - - /** - * Enhance the task form with routine-specific fields from an XML file declared through the TASKS_MAP constant. - * If a plugin only supports the task form and does not need additional logic, this method can be mapped to the - * `onContentPrepareForm` event through {@see SubscriberInterface::getSubscribedEvents()} and will take care - * of injecting the fields without additional logic in the plugin class. - * - * @param EventInterface|Form $context The onContentPrepareForm event or the Form object. - * @param mixed $data The form data, required when $context is a {@see Form} instance. - * - * @return boolean True if the form was successfully enhanced or the context was not relevant. - * - * @since 4.1.0 - * @throws \Exception - */ - public function enhanceTaskItemForm($context, $data = null): bool - { - if ($context instanceof EventInterface) - { - /** @var Form $form */ - $form = $context->getArgument('0'); - $data = $context->getArgument('1'); - } - elseif ($context instanceof Form) - { - $form = $context; - } - else - { - throw new \InvalidArgumentException( - sprintf( - 'Argument 0 of %1$s must be an instance of %2$s or %3$s', - __METHOD__, - EventInterface::class, - Form::class - ) - ); - } - - if ($form->getName() !== 'com_scheduler.task') - { - return true; - } - - $routineId = $this->getRoutineId($form, $data); - $isSupported = \array_key_exists($routineId, self::TASKS_MAP); - $enhancementFormName = self::TASKS_MAP[$routineId]['form'] ?? ''; - - // Return if routine is not supported by the plugin or the routine does not have a form linked in TASKS_MAP. - if (!$isSupported || \strlen($enhancementFormName) === 0) - { - return true; - } - - // We expect the form XML in "{PLUGIN_PATH}/forms/{FORM_NAME}.xml" - $path = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name; - $enhancementFormFile = $path . '/forms/' . $enhancementFormName . '.xml'; - - try - { - $enhancementFormFile = Path::check($enhancementFormFile); - } - catch (\Exception $e) - { - return false; - } - - if (is_file($enhancementFormFile)) - { - return $form->loadFile($enhancementFormFile); - } - - return false; - } - - /** - * Advertise the task routines supported by the plugin. This method should be mapped to the `onTaskOptionsList`, - * enabling the plugin to advertise its routines without any custom logic.
    - * **Note:** This method expects the `TASKS_MAP` class constant to have relevant information. - * - * @param EventInterface $event onTaskOptionsList Event - * - * @return void - * - * @since 4.1.0 - */ - public function advertiseRoutines(EventInterface $event): void - { - $options = []; - - foreach (self::TASKS_MAP as $routineId => $details) - { - // Sanity check against non-compliant plugins - if (isset($details['langConstPrefix'])) - { - $options[$routineId] = $details['langConstPrefix']; - } - } - - $subject = $event->getArgument('subject'); - $subject->addOptions($options); - } - - /** - * Get the relevant task routine ID in the context of a form event, e.g., the `onContentPrepareForm` event. - * - * @param Form $form The form - * @param mixed $data The data - * - * @return string - * - * @since 4.1.0 - * @throws \Exception - */ - protected function getRoutineId(Form $form, $data): string - { - /* - * Depending on when the form is loaded, the ID may either be in $data or the data already bound to the form. - * $data can also either be an object or an array. - */ - $routineId = $data->taskOption->id ?? $data->type ?? $data['type'] ?? $form->getValue('type') ?? $data['taskOption']->id ?? ''; - - // If we're unable to find a routineId, it might be in the form input. - if (empty($routineId)) - { - $app = $this->getApplication() ?? ($this->app ?? Factory::getApplication()); - $form = $app->getInput()->get('jform', []); - $routineId = ArrayHelper::getValue($form, 'type', '', 'STRING'); - } - - return $routineId; - } - - /** - * Add a log message to the task log. - * - * @param string $message The log message - * @param string $priority The log message priority - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - * @todo : use dependency injection here (starting from the Task & Scheduler classes). - */ - protected function logTask(string $message, string $priority = 'info'): void - { - static $langLoaded; - static $priorityMap = [ - 'debug' => Log::DEBUG, - 'error' => Log::ERROR, - 'info' => Log::INFO, - 'notice' => Log::NOTICE, - 'warning' => Log::WARNING, - ]; - - if (!$langLoaded) - { - $app = $this->getApplication() ?? ($this->app ?? Factory::getApplication()); - $app->getLanguage()->load('com_scheduler', JPATH_ADMINISTRATOR); - $langLoaded = true; - } - - $category = $this->snapshot['logCategory']; - - Log::add(Text::_('COM_SCHEDULER_ROUTINE_LOG_PREFIX') . $message, $priorityMap[$priority] ?? Log::INFO, $category); - } - - /** - * Handler for *standard* task routines. Standard routines are mapped to valid class methods 'method' through - * `static::TASKS_MAP`. These methods are expected to take a single argument (the Event) and return an integer - * return status (see {@see Status}). For a plugin that maps each of its task routines to valid methods and does - * not need non-standard handling, this method can be mapped to the `onExecuteTask` event through - * {@see SubscriberInterface::getSubscribedEvents()}, which would allow it to then check if the event wants to - * execute a routine offered by the parent plugin, call the routine and do some other housework without any code - * in the parent classes.
    - * **Compatible routine method signature:**   ({@see ExecuteTaskEvent::class}, ...): int - * - * @param ExecuteTaskEvent $event The `onExecuteTask` event. - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - public function standardRoutineHandler(ExecuteTaskEvent $event): void - { - if (!\array_key_exists($event->getRoutineId(), self::TASKS_MAP)) - { - return; - } - - $this->startRoutine($event); - $routineId = $event->getRoutineId(); - $methodName = (string) self::TASKS_MAP[$routineId]['method'] ?? ''; - $exitCode = Status::NO_EXIT; - - // We call the mapped method if it exists and confirms to the ($event) -> int signature. - if (!empty($methodName) && ($staticReflection = new \ReflectionClass($this))->hasMethod($methodName)) - { - $method = $staticReflection->getMethod($methodName); - - // Might need adjustments here for PHP8 named parameters. - if (!($method->getNumberOfRequiredParameters() === 1) - || !$method->getParameters()[0]->hasType() - || $method->getParameters()[0]->getType()->getName() !== ExecuteTaskEvent::class - || !$method->hasReturnType() - || $method->getReturnType()->getName() !== 'int') - { - $this->logTask( - sprintf( - 'Incorrect routine method signature for %1$s(). See checks in %2$s()', - $method->getName(), - __METHOD__ - ), - 'error' - ); - - return; - } - - try - { - // Enable invocation of private/protected methods. - $method->setAccessible(true); - $exitCode = $method->invoke($this, $event); - } - catch (\ReflectionException $e) - { - // @todo replace with language string (?) - $this->logTask('Exception when calling routine: ' . $e->getMessage(), 'error'); - $exitCode = Status::NO_RUN; - } - } - else - { - $this->logTask( - sprintf( - 'Incorrectly configured TASKS_MAP in class %s. Missing valid method for `routine_id` %s', - static::class, - $routineId - ), - 'error' - ); - } - - /** - * Closure to validate a status against {@see Status} - * - * @since 4.1.0 - */ - $validateStatus = static function (int $statusCode): bool { - return \in_array( - $statusCode, - (new \ReflectionClass(Status::class))->getConstants() - ); - }; - - // Validate the exit code. - if (!\is_int($exitCode) || !$validateStatus($exitCode)) - { - $exitCode = Status::INVALID_EXIT; - } - - $this->endRoutine($event, $exitCode); - } + /** + * A snapshot of the routine state. + * + * @var array + * @since 4.1.0 + */ + protected $snapshot = []; + + /** + * Set information to {@see $snapshot} when initializing a routine. + * + * @param ExecuteTaskEvent $event The onExecuteTask event. + * + * @return void + * + * @since 4.1.0 + */ + protected function startRoutine(ExecuteTaskEvent $event): void + { + if (!$this instanceof CMSPlugin) { + return; + } + + $this->snapshot['logCategory'] = $event->getArgument('subject')->logCategory; + $this->snapshot['plugin'] = $this->_name; + $this->snapshot['startTime'] = microtime(true); + $this->snapshot['status'] = Status::RUNNING; + } + + /** + * Set information to {@see $snapshot} when ending a routine. This information includes the routine exit code and + * timing information. + * + * @param ExecuteTaskEvent $event The event + * @param ?int $exitCode The task exit code + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + protected function endRoutine(ExecuteTaskEvent $event, int $exitCode): void + { + if (!$this instanceof CMSPlugin) { + return; + } + + $this->snapshot['endTime'] = $endTime = microtime(true); + $this->snapshot['duration'] = $endTime - $this->snapshot['startTime']; + $this->snapshot['status'] = $exitCode ?? Status::OK; + $event->setResult($this->snapshot); + } + + /** + * Enhance the task form with routine-specific fields from an XML file declared through the TASKS_MAP constant. + * If a plugin only supports the task form and does not need additional logic, this method can be mapped to the + * `onContentPrepareForm` event through {@see SubscriberInterface::getSubscribedEvents()} and will take care + * of injecting the fields without additional logic in the plugin class. + * + * @param EventInterface|Form $context The onContentPrepareForm event or the Form object. + * @param mixed $data The form data, required when $context is a {@see Form} instance. + * + * @return boolean True if the form was successfully enhanced or the context was not relevant. + * + * @since 4.1.0 + * @throws \Exception + */ + public function enhanceTaskItemForm($context, $data = null): bool + { + if ($context instanceof EventInterface) { + /** @var Form $form */ + $form = $context->getArgument('0'); + $data = $context->getArgument('1'); + } elseif ($context instanceof Form) { + $form = $context; + } else { + throw new \InvalidArgumentException( + sprintf( + 'Argument 0 of %1$s must be an instance of %2$s or %3$s', + __METHOD__, + EventInterface::class, + Form::class + ) + ); + } + + if ($form->getName() !== 'com_scheduler.task') { + return true; + } + + $routineId = $this->getRoutineId($form, $data); + $isSupported = \array_key_exists($routineId, self::TASKS_MAP); + $enhancementFormName = self::TASKS_MAP[$routineId]['form'] ?? ''; + + // Return if routine is not supported by the plugin or the routine does not have a form linked in TASKS_MAP. + if (!$isSupported || \strlen($enhancementFormName) === 0) { + return true; + } + + // We expect the form XML in "{PLUGIN_PATH}/forms/{FORM_NAME}.xml" + $path = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name; + $enhancementFormFile = $path . '/forms/' . $enhancementFormName . '.xml'; + + try { + $enhancementFormFile = Path::check($enhancementFormFile); + } catch (\Exception $e) { + return false; + } + + if (is_file($enhancementFormFile)) { + return $form->loadFile($enhancementFormFile); + } + + return false; + } + + /** + * Advertise the task routines supported by the plugin. This method should be mapped to the `onTaskOptionsList`, + * enabling the plugin to advertise its routines without any custom logic.
    + * **Note:** This method expects the `TASKS_MAP` class constant to have relevant information. + * + * @param EventInterface $event onTaskOptionsList Event + * + * @return void + * + * @since 4.1.0 + */ + public function advertiseRoutines(EventInterface $event): void + { + $options = []; + + foreach (self::TASKS_MAP as $routineId => $details) { + // Sanity check against non-compliant plugins + if (isset($details['langConstPrefix'])) { + $options[$routineId] = $details['langConstPrefix']; + } + } + + $subject = $event->getArgument('subject'); + $subject->addOptions($options); + } + + /** + * Get the relevant task routine ID in the context of a form event, e.g., the `onContentPrepareForm` event. + * + * @param Form $form The form + * @param mixed $data The data + * + * @return string + * + * @since 4.1.0 + * @throws \Exception + */ + protected function getRoutineId(Form $form, $data): string + { + /* + * Depending on when the form is loaded, the ID may either be in $data or the data already bound to the form. + * $data can also either be an object or an array. + */ + $routineId = $data->taskOption->id ?? $data->type ?? $data['type'] ?? $form->getValue('type') ?? $data['taskOption']->id ?? ''; + + // If we're unable to find a routineId, it might be in the form input. + if (empty($routineId)) { + $app = $this->getApplication() ?? ($this->app ?? Factory::getApplication()); + $form = $app->getInput()->get('jform', []); + $routineId = ArrayHelper::getValue($form, 'type', '', 'STRING'); + } + + return $routineId; + } + + /** + * Add a log message to the task log. + * + * @param string $message The log message + * @param string $priority The log message priority + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + * @todo : use dependency injection here (starting from the Task & Scheduler classes). + */ + protected function logTask(string $message, string $priority = 'info'): void + { + static $langLoaded; + static $priorityMap = [ + 'debug' => Log::DEBUG, + 'error' => Log::ERROR, + 'info' => Log::INFO, + 'notice' => Log::NOTICE, + 'warning' => Log::WARNING, + ]; + + if (!$langLoaded) { + $app = $this->getApplication() ?? ($this->app ?? Factory::getApplication()); + $app->getLanguage()->load('com_scheduler', JPATH_ADMINISTRATOR); + $langLoaded = true; + } + + $category = $this->snapshot['logCategory']; + + Log::add(Text::_('COM_SCHEDULER_ROUTINE_LOG_PREFIX') . $message, $priorityMap[$priority] ?? Log::INFO, $category); + } + + /** + * Handler for *standard* task routines. Standard routines are mapped to valid class methods 'method' through + * `static::TASKS_MAP`. These methods are expected to take a single argument (the Event) and return an integer + * return status (see {@see Status}). For a plugin that maps each of its task routines to valid methods and does + * not need non-standard handling, this method can be mapped to the `onExecuteTask` event through + * {@see SubscriberInterface::getSubscribedEvents()}, which would allow it to then check if the event wants to + * execute a routine offered by the parent plugin, call the routine and do some other housework without any code + * in the parent classes.
    + * **Compatible routine method signature:**   ({@see ExecuteTaskEvent::class}, ...): int + * + * @param ExecuteTaskEvent $event The `onExecuteTask` event. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + public function standardRoutineHandler(ExecuteTaskEvent $event): void + { + if (!\array_key_exists($event->getRoutineId(), self::TASKS_MAP)) { + return; + } + + $this->startRoutine($event); + $routineId = $event->getRoutineId(); + $methodName = (string) self::TASKS_MAP[$routineId]['method'] ?? ''; + $exitCode = Status::NO_EXIT; + + // We call the mapped method if it exists and confirms to the ($event) -> int signature. + if (!empty($methodName) && ($staticReflection = new \ReflectionClass($this))->hasMethod($methodName)) { + $method = $staticReflection->getMethod($methodName); + + // Might need adjustments here for PHP8 named parameters. + if ( + !($method->getNumberOfRequiredParameters() === 1) + || !$method->getParameters()[0]->hasType() + || $method->getParameters()[0]->getType()->getName() !== ExecuteTaskEvent::class + || !$method->hasReturnType() + || $method->getReturnType()->getName() !== 'int' + ) { + $this->logTask( + sprintf( + 'Incorrect routine method signature for %1$s(). See checks in %2$s()', + $method->getName(), + __METHOD__ + ), + 'error' + ); + + return; + } + + try { + // Enable invocation of private/protected methods. + $method->setAccessible(true); + $exitCode = $method->invoke($this, $event); + } catch (\ReflectionException $e) { + // @todo replace with language string (?) + $this->logTask('Exception when calling routine: ' . $e->getMessage(), 'error'); + $exitCode = Status::NO_RUN; + } + } else { + $this->logTask( + sprintf( + 'Incorrectly configured TASKS_MAP in class %s. Missing valid method for `routine_id` %s', + static::class, + $routineId + ), + 'error' + ); + } + + /** + * Closure to validate a status against {@see Status} + * + * @since 4.1.0 + */ + $validateStatus = static function (int $statusCode): bool { + return \in_array( + $statusCode, + (new \ReflectionClass(Status::class))->getConstants() + ); + }; + + // Validate the exit code. + if (!\is_int($exitCode) || !$validateStatus($exitCode)) { + $exitCode = Status::INVALID_EXIT; + } + + $this->endRoutine($event, $exitCode); + } } diff --git a/administrator/components/com_scheduler/src/View/Select/HtmlView.php b/administrator/components/com_scheduler/src/View/Select/HtmlView.php index c2d35d3a44b12..0eaaa40711cf7 100644 --- a/administrator/components/com_scheduler/src/View/Select/HtmlView.php +++ b/administrator/components/com_scheduler/src/View/Select/HtmlView.php @@ -1,4 +1,5 @@ app = Factory::getApplication(); - - parent::__construct($config); - } - - /** - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - public function display($tpl = null): void - { - $this->state = $this->get('State'); - $this->items = $this->get('Items'); - $this->modalLink = ''; - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.1.0 - */ - protected function addToolbar(): void - { - /* - * Get the global Toolbar instance - * @todo : Replace usage with ToolbarFactoryInterface. but how? - * Probably some changes in the core, since mod_menu calls and renders the getInstance() toolbar - */ - $toolbar = Toolbar::getInstance(); - - // Add page title - ToolbarHelper::title(Text::_('COM_SCHEDULER_MANAGER_TASKS'), 'clock'); - - $toolbar->linkButton('cancel') - ->url('index.php?option=com_scheduler') - ->buttonClass('btn btn-danger') - ->icon('icon-times') - ->text(Text::_('JCANCEL')); - } + /** + * @var AdministratorApplication + * @since 4.1.0 + */ + protected $app; + + /** + * The model state + * + * @var CMSObject + * @since 4.1.0 + */ + protected $state; + + /** + * An array of items + * + * @var TaskOption[] + * @since 4.1.0 + */ + protected $items; + + /** + * A suffix for links for modal use [?] + * + * @var string + * @since 4.1.0 + */ + protected $modalLink; + + /** + * HtmlView constructor. + * + * @param array $config A named configuration array for object construction. + * name: the name (optional) of the view (defaults to the view class name suffix). + * charset: the character set to use for display + * escape: the name (optional) of the function to use for escaping strings + * base_path: the parent path (optional) of the `views` directory (defaults to the component + * folder) template_plath: the path (optional) of the layout directory (defaults to + * base_path + /views/ + view name helper_path: the path (optional) of the helper files + * (defaults to base_path + /helpers/) layout: the layout (optional) to use to display the + * view + * + * @since 4.1.0 + * @throws \Exception + */ + public function __construct($config = []) + { + $this->app = Factory::getApplication(); + + parent::__construct($config); + } + + /** + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + public function display($tpl = null): void + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->modalLink = ''; + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.1.0 + */ + protected function addToolbar(): void + { + /* + * Get the global Toolbar instance + * @todo : Replace usage with ToolbarFactoryInterface. but how? + * Probably some changes in the core, since mod_menu calls and renders the getInstance() toolbar + */ + $toolbar = Toolbar::getInstance(); + + // Add page title + ToolbarHelper::title(Text::_('COM_SCHEDULER_MANAGER_TASKS'), 'clock'); + + $toolbar->linkButton('cancel') + ->url('index.php?option=com_scheduler') + ->buttonClass('btn btn-danger') + ->icon('icon-times') + ->text(Text::_('JCANCEL')); + } } diff --git a/administrator/components/com_scheduler/src/View/Task/HtmlView.php b/administrator/components/com_scheduler/src/View/Task/HtmlView.php index 6023fc9715f3b..052e6953adef6 100644 --- a/administrator/components/com_scheduler/src/View/Task/HtmlView.php +++ b/administrator/components/com_scheduler/src/View/Task/HtmlView.php @@ -1,4 +1,5 @@ app = Factory::getApplication(); - parent::__construct($config); - } - - /** - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - public function display($tpl = null): void - { - /* - * Will call the getForm() method of TaskModel - */ - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - $this->canDo = ContentHelper::getActions('com_scheduler', 'task', $this->item->id); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Adds the page title and toolbar - * - * @return void - * - * @since 4.1.0 - */ - protected function addToolbar(): void - { - $app = $this->app; - - $app->getInput()->set('hidemainmenu', true); - $isNew = ($this->item->id == 0); - $canDo = $this->canDo; - - /* - * Get the toolbar object instance - * !! @todo : Replace usage with ToolbarFactoryInterface - */ - $toolbar = Toolbar::getInstance(); - - ToolbarHelper::title($isNew ? Text::_('COM_SCHEDULER_MANAGER_TASK_NEW') : Text::_('COM_SCHEDULER_MANAGER_TASK_EDIT'), 'clock'); - - if (($isNew && $canDo->get('core.create')) || (!$isNew && $canDo->get('core.edit'))) - { - $toolbar->apply('task.apply'); - $toolbar->save('task.save'); - } - - // @todo | ? : Do we need save2new, save2copy? - - $toolbar->cancel('task.cancel', $isNew ? 'JTOOLBAR_CANCEL' : 'JTOOLBAR_CLOSE'); - $toolbar->help('Scheduled_Tasks:_Edit'); - } + /** + * @var AdministratorApplication $app + * @since 4.1.0 + */ + protected $app; + + /** + * The Form object + * + * @var Form + * @since 4.1.0 + */ + protected $form; + + /** + * The active item + * + * @var object + * @since 4.1.0 + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + * @since 4.1.0 + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * @since 4.1.0 + */ + protected $canDo; + + /** + * Overloads the parent constructor. + * Just needed to fetch the Application object. + * + * @param array $config A named configuration array for object construction. + * name: the name (optional) of the view (defaults to the view class name suffix). + * charset: the character set to use for display + * escape: the name (optional) of the function to use for escaping strings + * base_path: the parent path (optional) of the `views` directory (defaults to the + * component folder) template_plath: the path (optional) of the layout directory (defaults + * to base_path + /views/ + view name helper_path: the path (optional) of the helper files + * (defaults to base_path + /helpers/) layout: the layout (optional) to use to display the + * view + * + * @since 4.1.0 + * @throws \Exception + */ + public function __construct($config = array()) + { + $this->app = Factory::getApplication(); + parent::__construct($config); + } + + /** + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + public function display($tpl = null): void + { + /* + * Will call the getForm() method of TaskModel + */ + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + $this->canDo = ContentHelper::getActions('com_scheduler', 'task', $this->item->id); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Adds the page title and toolbar + * + * @return void + * + * @since 4.1.0 + */ + protected function addToolbar(): void + { + $app = $this->app; + + $app->getInput()->set('hidemainmenu', true); + $isNew = ($this->item->id == 0); + $canDo = $this->canDo; + + /* + * Get the toolbar object instance + * !! @todo : Replace usage with ToolbarFactoryInterface + */ + $toolbar = Toolbar::getInstance(); + + ToolbarHelper::title($isNew ? Text::_('COM_SCHEDULER_MANAGER_TASK_NEW') : Text::_('COM_SCHEDULER_MANAGER_TASK_EDIT'), 'clock'); + + if (($isNew && $canDo->get('core.create')) || (!$isNew && $canDo->get('core.edit'))) { + $toolbar->apply('task.apply'); + $toolbar->save('task.save'); + } + + // @todo | ? : Do we need save2new, save2copy? + + $toolbar->cancel('task.cancel', $isNew ? 'JTOOLBAR_CANCEL' : 'JTOOLBAR_CLOSE'); + $toolbar->help('Scheduled_Tasks:_Edit'); + } } diff --git a/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php b/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php index a7fd01bde0670..f709026c3fd78 100644 --- a/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php +++ b/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('empty_state'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_scheduler'); - $user = Factory::getApplication()->getIdentity(); - - /* - * Get the toolbar object instance - * !! @todo : Replace usage with ToolbarFactoryInterface - */ - $toolbar = Toolbar::getInstance(); - - ToolbarHelper::title(Text::_('COM_SCHEDULER_MANAGER_TASKS'), 'clock'); - - if ($canDo->get('core.create')) - { - $toolbar->linkButton('new', 'JTOOLBAR_NEW') - ->url('index.php?option=com_scheduler&view=select&layout=default') - ->buttonClass('btn btn-success') - ->icon('icon-new'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) - { - /** @var DropdownButton $dropdown */ - $dropdown = $toolbar->dropdownButton('status-group') - ->toggleSplit(false) - ->text('JTOOLBAR_CHANGE_STATUS') - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - // Add the batch Enable, Disable and Trash buttons if privileged - if ($canDo->get('core.edit.state')) - { - $childBar->publish('tasks.publish', 'JTOOLBAR_ENABLE')->listCheck(true); - $childBar->unpublish('tasks.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); - - if ($canDo->get('core.admin')) - { - $childBar->checkin('tasks.checkin')->listCheck(true); - } - - $childBar->checkin('tasks.unlock', 'COM_SCHEDULER_TOOLBAR_UNLOCK')->listCheck(true)->icon('icon-unlock'); - - // We don't want the batch Trash button if displayed entries are all trashed - if ($this->state->get('filter.state') != -2) - { - $childBar->trash('tasks.trash')->listCheck(true); - } - } - } - - // Add "Empty Trash" button if filtering by trashed. - if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('tasks.delete') - ->message('JGLOBAL_CONFIRM_DELETE') - ->text('JTOOLBAR_EMPTY_TRASH') - ->listCheck(true); - } - - // Link to component preferences if user has admin privileges - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_scheduler'); - } - - $toolbar->help('Scheduled_Tasks'); - } + /** + * Array of task items. + * + * @var array + * @since 4.1.0 + */ + protected $items; + + /** + * The pagination object. + * + * @var Pagination + * @since 4.1.0 + * @todo Test pagination. + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 4.1.0 + */ + protected $state; + + /** + * A Form object for search filters. + * + * @var Form + * @since 4.1.0 + */ + public $filterForm; + + /** + * The active search filters. + * + * @var array + * @since 4.1.0 + */ + public $activeFilters; + + /** + * Is this view in an empty state? + * + * @var boolean + * @since 4.1.0 + */ + private $isEmptyState = false; + + /** + * @inheritDoc + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + public function display($tpl = null): void + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('empty_state'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_scheduler'); + $user = Factory::getApplication()->getIdentity(); + + /* + * Get the toolbar object instance + * !! @todo : Replace usage with ToolbarFactoryInterface + */ + $toolbar = Toolbar::getInstance(); + + ToolbarHelper::title(Text::_('COM_SCHEDULER_MANAGER_TASKS'), 'clock'); + + if ($canDo->get('core.create')) { + $toolbar->linkButton('new', 'JTOOLBAR_NEW') + ->url('index.php?option=com_scheduler&view=select&layout=default') + ->buttonClass('btn btn-success') + ->icon('icon-new'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) { + /** @var DropdownButton $dropdown */ + $dropdown = $toolbar->dropdownButton('status-group') + ->toggleSplit(false) + ->text('JTOOLBAR_CHANGE_STATUS') + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + // Add the batch Enable, Disable and Trash buttons if privileged + if ($canDo->get('core.edit.state')) { + $childBar->publish('tasks.publish', 'JTOOLBAR_ENABLE')->listCheck(true); + $childBar->unpublish('tasks.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); + + if ($canDo->get('core.admin')) { + $childBar->checkin('tasks.checkin')->listCheck(true); + } + + $childBar->checkin('tasks.unlock', 'COM_SCHEDULER_TOOLBAR_UNLOCK')->listCheck(true)->icon('icon-unlock'); + + // We don't want the batch Trash button if displayed entries are all trashed + if ($this->state->get('filter.state') != -2) { + $childBar->trash('tasks.trash')->listCheck(true); + } + } + } + + // Add "Empty Trash" button if filtering by trashed. + if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('tasks.delete') + ->message('JGLOBAL_CONFIRM_DELETE') + ->text('JTOOLBAR_EMPTY_TRASH') + ->listCheck(true); + } + + // Link to component preferences if user has admin privileges + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_scheduler'); + } + + $toolbar->help('Scheduled_Tasks'); + } } diff --git a/administrator/components/com_scheduler/tmpl/select/default.php b/administrator/components/com_scheduler/tmpl/select/default.php index efffafd4f13ef..680860340fed5 100644 --- a/administrator/components/com_scheduler/tmpl/select/default.php +++ b/administrator/components/com_scheduler/tmpl/select/default.php @@ -1,4 +1,5 @@
    -
    -
    - -
    - -
    - -
    -
    -
    -
    +
    +
    + +
    + +
    + +
    +
    +
    +
    -
    - -
    - - -
    -

    - -

    +
    + +
    + + +
    +

    + +

    - -
    + +
    - - items as $item) : ?> - - id; ?> - escape($item->title); ?> - escape(strip_tags($item->desc)), 200); ?> - - -
    -

    -

    - -

    -
    - - - -
    - - -
    -
    + + items as $item) : ?> + + id; ?> + escape($item->title); ?> + escape(strip_tags($item->desc)), 200); ?> + + +
    +

    +

    + +

    +
    + + + +
    + + +
    +
    diff --git a/administrator/components/com_scheduler/tmpl/select/modal.php b/administrator/components/com_scheduler/tmpl/select/modal.php index 745adeeef782e..5c2d100569e8a 100644 --- a/administrator/components/com_scheduler/tmpl/select/modal.php +++ b/administrator/components/com_scheduler/tmpl/select/modal.php @@ -1,4 +1,5 @@
    - setLayout('default'); ?> - - loadTemplate(); - } - catch (Exception $e) - { - die('Exception while loading template..'); - } - ?> + setLayout('default'); ?> + + loadTemplate(); + } catch (Exception $e) { + die('Exception while loading template..'); + } + ?>
    diff --git a/administrator/components/com_scheduler/tmpl/task/edit.php b/administrator/components/com_scheduler/tmpl/task/edit.php index 184fcb1dfb47c..7d91554d3a53f 100644 --- a/administrator/components/com_scheduler/tmpl/task/edit.php +++ b/administrator/components/com_scheduler/tmpl/task/edit.php @@ -1,4 +1,5 @@ $fieldset) : - if ($name === 'task_params') : - unset($advancedFieldsets[$name]); - continue; - endif; + if ($name === 'task_params') : + unset($advancedFieldsets[$name]); + continue; + endif; - $this->ignore_fieldsets[] = $fieldset->name; + $this->ignore_fieldsets[] = $fieldset->name; endforeach; ?>
    - - - - - -
    - 'general')); ?> - - - item->id) ? Text::_('COM_SCHEDULER_NEW_TASK') : Text::_('COM_SCHEDULER_EDIT_TASK') - ); - ?> -
    -
    - - item->taskOption): - /** @var TaskOption $taskOption */ - $taskOption = $this->item->taskOption; ?> -
    -

    - title ?> -

    - fieldset = 'description'; - $short_description = Text::_($taskOption->desc); - $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); - - if (!$long_description) - { - $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); - - if (strlen($truncated) > 500) - { - $long_description = $short_description; - $short_description = HTMLHelper::_('string.truncate', $truncated, 250); - - if ($short_description == $long_description) - { - $long_description = ''; - } - } - } - ?> -

    - -

    - - - -

    - -
    - - enqueueMessage(Text::_('COM_SCHEDULER_WARNING_EXISTING_TASK_TYPE_NOT_FOUND'), 'warning'); - ?> - -
    - - form->renderFieldset('basic'); ?> -
    - -
    - - form->renderFieldset('custom-cron-rules'); ?> -
    - -
    - -
    - form->renderFieldset('aside'); ?> -
    -
    - - - -
    -
    - -
    -
    - - - - -
    -
    -
    - - form->renderFieldset('priority') ?> -
    - -
    - label ?: 'COM_SCHEDULER_FIELDSET_' . $fieldset->name) ?> - form->renderFieldset($fieldset->name) ?> -
    - -
    -
    - - - - -
    -
    -
    - - form->renderFieldset('exec_hist'); ?> -
    -
    -
    - - - - -
    -
    -
    - - form->renderFieldset('details'); ?> -
    -
    -
    - - - - canDo->get('core.admin')) : ?> - -
    - -
    - form->getInput('rules'); ?> -
    -
    - - - - form->getInput('context'); ?> - - -
    + method="post" name="adminForm" id="task-form" + aria-label="item->id === 0 ? 'NEW' : 'EDIT'), true); ?>" + class="form-validate"> + + + + + +
    + 'general')); ?> + + + item->id) ? Text::_('COM_SCHEDULER_NEW_TASK') : Text::_('COM_SCHEDULER_EDIT_TASK') + ); + ?> +
    +
    + + item->taskOption) : + /** @var TaskOption $taskOption */ + $taskOption = $this->item->taskOption; ?> +
    +

    + title ?> +

    + fieldset = 'description'; + $short_description = Text::_($taskOption->desc); + $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); + + if (!$long_description) { + $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); + + if (strlen($truncated) > 500) { + $long_description = $short_description; + $short_description = HTMLHelper::_('string.truncate', $truncated, 250); + + if ($short_description == $long_description) { + $long_description = ''; + } + } + } + ?> +

    + +

    + + + +

    + +
    + + enqueueMessage(Text::_('COM_SCHEDULER_WARNING_EXISTING_TASK_TYPE_NOT_FOUND'), 'warning'); + ?> + +
    + + form->renderFieldset('basic'); ?> +
    + +
    + + form->renderFieldset('custom-cron-rules'); ?> +
    + +
    + +
    + form->renderFieldset('aside'); ?> +
    +
    + + + +
    +
    + +
    +
    + + + + +
    +
    +
    + + form->renderFieldset('priority') ?> +
    + +
    + label ?: 'COM_SCHEDULER_FIELDSET_' . $fieldset->name) ?> + form->renderFieldset($fieldset->name) ?> +
    + +
    +
    + + + + +
    +
    +
    + + form->renderFieldset('exec_hist'); ?> +
    +
    +
    + + + + +
    +
    +
    + + form->renderFieldset('details'); ?> +
    +
    +
    + + + + canDo->get('core.admin')) : ?> + +
    + +
    + form->getInput('rules'); ?> +
    +
    + + + + form->getInput('context'); ?> + + +
    diff --git a/administrator/components/com_scheduler/tmpl/tasks/default.php b/administrator/components/com_scheduler/tmpl/tasks/default.php index ec6f238b2a889..c33990606550a 100644 --- a/administrator/components/com_scheduler/tmpl/tasks/default.php +++ b/administrator/components/com_scheduler/tmpl/tasks/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect') - ->useScript('com_scheduler.test-task') - ->useStyle('com_scheduler.admin-view-tasks-css'); + ->useScript('multiselect') + ->useScript('com_scheduler.test-task') + ->useStyle('com_scheduler.admin-view-tasks-css'); Text::script('COM_SCHEDULER_TEST_RUN_TITLE'); Text::script('COM_SCHEDULER_TEST_RUN_TASK'); @@ -42,14 +43,11 @@ Text::script('JLIB_JS_AJAX_ERROR_NO_CONTENT'); Text::script('JLIB_JS_AJAX_ERROR_PARSE'); -try -{ - /** @var CMSWebApplicationInterface $app */ - $app = Factory::getApplication(); -} -catch (Exception $e) -{ - die('Failed to get app'); +try { + /** @var CMSWebApplicationInterface $app */ + $app = Factory::getApplication(); +} catch (Exception $e) { + die('Failed to get app'); } $user = $app->getIdentity(); @@ -60,237 +58,235 @@ $section = null; $mode = false; -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_scheduler&task=tasks.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_scheduler&task=tasks.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $this->document->addScriptOptions('com_scheduler.test-task.token', Session::getFormToken()); ?>
    -
    - $this)); - ?> - - - items)): ?> - -
    - - -
    - - - - items)): ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true" > - items as $i => $item): - $canCreate = $user->authorise('core.create', 'com_scheduler'); - $canEdit = $user->authorise('core.edit', 'com_scheduler'); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_scheduler') && $canCheckin; - ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - - - - - - -
    - id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - - - state, $i, 'tasks.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'tasks.', $canCheckin); ?> - - locked) : ?> - $canChange, 'prefix' => 'tasks.', - 'active_class' => 'none fa fa-running border-dark text-body', - 'inactive_class' => 'none fa fa-running', 'tip' => true, 'translate' => false, - 'active_title' => Text::sprintf('COM_SCHEDULER_RUNNING_SINCE', HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5')), - 'inactive_title' => Text::sprintf('COM_SCHEDULER_RUNNING_SINCE', HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5')), - ]); ?> - - - - escape($item->title); ?> - - - escape($item->title); ?> - - last_exit_code, [Status::OK, Status::WILL_RESUME])): ?> - -
    - last_exit_code); ?> -
    - -
    - - note): ?> - - escape($item->note)); ?> - - -
    - escape($item->safeTypeTitle); ?> - - last_execution ? HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5') : '-'; ?> - - - - priority === -1) : ?> - - priority === 0) : ?> - - priority === 1) : ?> - - - - id; ?> -
    - - pagination->getListFooter(); - - // Modal for test runs - $modalparams = [ - 'title' => '', - ]; - - $modalbody = '
    '; - - echo HTMLHelper::_('bootstrap.renderModal', 'scheduler-test-modal', $modalparams, $modalbody); - - ?> - - - - - - -
    + id="adminForm"> +
    + $this)); + ?> + + + items)) : ?> + +
    + + +
    + + + + items)) : ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true" > + items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_scheduler'); + $canEdit = $user->authorise('core.edit', 'com_scheduler'); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_scheduler') && $canCheckin; + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + + + + + + +
    + id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + + state, $i, 'tasks.', $canChange); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'tasks.', $canCheckin); ?> + + locked) : ?> + $canChange, 'prefix' => 'tasks.', + 'active_class' => 'none fa fa-running border-dark text-body', + 'inactive_class' => 'none fa fa-running', 'tip' => true, 'translate' => false, + 'active_title' => Text::sprintf('COM_SCHEDULER_RUNNING_SINCE', HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5')), + 'inactive_title' => Text::sprintf('COM_SCHEDULER_RUNNING_SINCE', HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5')), + ]); ?> + + + + escape($item->title); ?> + + + escape($item->title); ?> + + last_exit_code, [Status::OK, Status::WILL_RESUME])) : ?> + +
    + last_exit_code); ?> +
    + +
    + + note) : ?> + + escape($item->note)); ?> + + +
    + escape($item->safeTypeTitle); ?> + + last_execution ? HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5') : '-'; ?> + + + + priority === -1) : ?> + + priority === 0) : ?> + + priority === 1) : ?> + + + + id; ?> +
    + + pagination->getListFooter(); + + // Modal for test runs + $modalparams = [ + 'title' => '', + ]; + + $modalbody = '
    '; + + echo HTMLHelper::_('bootstrap.renderModal', 'scheduler-test-modal', $modalparams, $modalbody); + + ?> + + + + + + +
    diff --git a/administrator/components/com_scheduler/tmpl/tasks/empty_state.php b/administrator/components/com_scheduler/tmpl/tasks/empty_state.php index adbcc38835678..96e2eb0958ca4 100644 --- a/administrator/components/com_scheduler/tmpl/tasks/empty_state.php +++ b/administrator/components/com_scheduler/tmpl/tasks/empty_state.php @@ -1,4 +1,5 @@ 'COM_SCHEDULER', - 'formURL' => 'index.php?option=com_scheduler&task=task.add', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/J4.x:Task_Scheduler', - 'icon' => 'icon-clock clock', + 'textPrefix' => 'COM_SCHEDULER', + 'formURL' => 'index.php?option=com_scheduler&task=task.add', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/J4.x:Task_Scheduler', + 'icon' => 'icon-clock clock', ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_scheduler')) -{ - $displayData['createURL'] = 'index.php?option=com_scheduler&view=select&layout=default'; +if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_scheduler')) { + $displayData['createURL'] = 'index.php?option=com_scheduler&view=select&layout=default'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_tags/services/provider.php b/administrator/components/com_tags/services/provider.php index 669a7013d295c..6352e87ae35fa 100644 --- a/administrator/components/com_tags/services/provider.php +++ b/administrator/components/com_tags/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Tags')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Tags')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Tags')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new TagsComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Tags')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Tags')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Tags')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new TagsComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_tags/src/Controller/DisplayController.php b/administrator/components/com_tags/src/Controller/DisplayController.php index 366c926efd126..ac474722e7a40 100644 --- a/administrator/components/com_tags/src/Controller/DisplayController.php +++ b/administrator/components/com_tags/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'tags'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 3.1 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', 'tags'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($view == 'tag' && $layout == 'edit' && !$this->checkEditId('com_tags.edit.tag', $id)) - { - // 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', $id), 'error'); - } + // Check for edit form. + if ($view == 'tag' && $layout == 'edit' && !$this->checkEditId('com_tags.edit.tag', $id)) { + // 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', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_tags&view=tags', false)); + $this->setRedirect(Route::_('index.php?option=com_tags&view=tags', false)); - return false; - } + return false; + } - parent::display(); + parent::display(); - return $this; - } + return $this; + } } diff --git a/administrator/components/com_tags/src/Controller/TagController.php b/administrator/components/com_tags/src/Controller/TagController.php index 1abe0e5f90aba..67965f9923422 100644 --- a/administrator/components/com_tags/src/Controller/TagController.php +++ b/administrator/components/com_tags/src/Controller/TagController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Tags\Administrator\Controller; \defined('_JEXEC') or die; @@ -20,57 +22,57 @@ */ class TagController extends FormController { - use VersionableControllerTrait; + use VersionableControllerTrait; - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 3.1 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', 'com_tags'); - } + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 3.1 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', 'com_tags'); + } - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 3.1 - */ - protected function allowEdit($data = array(), $key = 'id') - { - // Since there is no asset tracking and no categories, revert to the component permissions. - return parent::allowEdit($data, $key); - } + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 3.1 + */ + protected function allowEdit($data = array(), $key = 'id') + { + // Since there is no asset tracking and no categories, revert to the component permissions. + return parent::allowEdit($data, $key); + } - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 3.1 - */ - public function batch($model = null) - { - $this->checkToken(); + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 3.1 + */ + public function batch($model = null) + { + $this->checkToken(); - // Set the model - $model = $this->getModel('Tag'); + // Set the model + $model = $this->getModel('Tag'); - // Preset the redirect - $this->setRedirect('index.php?option=com_tags&view=tags'); + // Preset the redirect + $this->setRedirect('index.php?option=com_tags&view=tags'); - return parent::batch($model); - } + return parent::batch($model); + } } diff --git a/administrator/components/com_tags/src/Controller/TagsController.php b/administrator/components/com_tags/src/Controller/TagsController.php index 78c4fefd802cb..66142589dfcdc 100644 --- a/administrator/components/com_tags/src/Controller/TagsController.php +++ b/administrator/components/com_tags/src/Controller/TagsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Rebuild the nested set tree. - * - * @return boolean False on failure or error, true on success. - * - * @since 3.1 - */ - public function rebuild() - { - $this->checkToken(); - - $this->setRedirect(Route::_('index.php?option=com_tags&view=tags', false)); - - /** @var \Joomla\Component\Tags\Administrator\Model\TagModel $model */ - $model = $this->getModel(); - - if ($model->rebuild()) - { - // Rebuild succeeded. - $this->setMessage(Text::_('COM_TAGS_REBUILD_SUCCESS')); - - return true; - } - else - { - // Rebuild failed. - $this->setMessage(Text::_('COM_TAGS_REBUILD_FAILURE')); - - return false; - } - } - - /** - * Method to get the JSON-encoded amount of published tags for quickicons - * - * @return void - * - * @since 4.1.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('tags'); - - $model->setState('filter.published', 1); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_TAGS_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_TAGS_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } + /** + * Proxy for getModel + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config An optional associative array of configuration settings. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 3.1 + */ + public function getModel($name = 'Tag', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Rebuild the nested set tree. + * + * @return boolean False on failure or error, true on success. + * + * @since 3.1 + */ + public function rebuild() + { + $this->checkToken(); + + $this->setRedirect(Route::_('index.php?option=com_tags&view=tags', false)); + + /** @var \Joomla\Component\Tags\Administrator\Model\TagModel $model */ + $model = $this->getModel(); + + if ($model->rebuild()) { + // Rebuild succeeded. + $this->setMessage(Text::_('COM_TAGS_REBUILD_SUCCESS')); + + return true; + } else { + // Rebuild failed. + $this->setMessage(Text::_('COM_TAGS_REBUILD_FAILURE')); + + return false; + } + } + + /** + * Method to get the JSON-encoded amount of published tags for quickicons + * + * @return void + * + * @since 4.1.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('tags'); + + $model->setState('filter.published', 1); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_TAGS_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_TAGS_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } } diff --git a/administrator/components/com_tags/src/Extension/TagsComponent.php b/administrator/components/com_tags/src/Extension/TagsComponent.php index 4ca170b8e0082..e6387b0a604fe 100644 --- a/administrator/components/com_tags/src/Extension/TagsComponent.php +++ b/administrator/components/com_tags/src/Extension/TagsComponent.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage', - ); - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 3.1 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - return parent::canDelete($record); - } - - /** - * Auto-populate the model state. - * - * @note Calling getState in this method will result in recursion. - * - * @return void - * - * @since 3.1 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - $parentId = $app->input->getInt('parent_id'); - $this->setState('tag.parent_id', $parentId); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState($this->getName() . '.id', $pk); - - // Load the parameters. - $params = ComponentHelper::getParams('com_tags'); - $this->setState('params', $params); - } - - /** - * Method to get a tag. - * - * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. - * - * @return mixed Tag data object on success, false on failure. - * - * @since 3.1 - */ - public function getItem($pk = null) - { - if ($result = parent::getItem($pk)) - { - // Prime required properties. - if (empty($result->id)) - { - $result->parent_id = $this->getState('tag.parent_id'); - } - - // Convert the metadata field to an array. - $registry = new Registry($result->metadata); - $result->metadata = $registry->toArray(); - - // Convert the images field to an array. - $registry = new Registry($result->images); - $result->images = $registry->toArray(); - - // Convert the urls field to an array. - $registry = new Registry($result->urls); - $result->urls = $registry->toArray(); - - // Convert the modified date to local user time for display in the form. - $tz = new \DateTimeZone(Factory::getApplication()->get('offset')); - - if ((int) $result->modified_time) - { - $date = new Date($result->modified_time); - $date->setTimezone($tz); - $result->modified_time = $date->toSql(true); - } - else - { - $result->modified_time = null; - } - } - - return $result; - } - - /** - * Method to get the row form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return bool|\Joomla\CMS\Form\Form A Form object on success, false on failure - * - * @since 3.1 - */ - public function getForm($data = array(), $loadData = true) - { - $jinput = Factory::getApplication()->input; - - // Get the form. - $form = $this->loadForm('com_tags.tag', 'tag', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - $user = Factory::getUser(); - - if (!$user->authorise('core.edit.state', 'com_tags' . $jinput->get('id'))) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 3.1 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_tags.edit.tag.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_tags.tag', $data); - - return $data; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function save($data) - { - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - $table = $this->getTable(); - $input = Factory::getApplication()->input; - $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState($this->getName() . '.id'); - $isNew = true; - $context = $this->option . '.' . $this->name; - - // Include the plugins for the save events. - PluginHelper::importPlugin($this->events_map['save']); - - try - { - // Load the row if saving an existing tag. - if ($pk > 0) - { - $table->load($pk); - $isNew = false; - } - - // Set the new parent id if parent id not matched OR while New/Save as Copy . - if ($table->parent_id != $data['parent_id'] || $data['id'] == 0) - { - $table->setLocation($data['parent_id'], 'last-child'); - } - - // Alter the title for save as copy - if ($input->get('task') == 'save2copy') - { - $origTable = $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['title'] == $origTable->title) - { - list($title, $alias) = $this->generateNewTitle($data['parent_id'], $data['alias'], $data['title']); - $data['title'] = $title; - $data['alias'] = $alias; - } - elseif ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - - $data['published'] = 0; - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Prepare the row for saving - $this->prepareTable($table); - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, $table, $isNew, $data)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, $table, $isNew)); - - // Rebuild the path for the tag: - if (!$table->rebuildPath($table->id)) - { - $this->setError($table->getError()); - - return false; - } - - // Rebuild the paths of the tag's children: - if (!$table->rebuild($table->id, $table->lft, $table->level, $table->path)) - { - $this->setError($table->getError()); - - return false; - } - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $this->setState($this->getName() . '.id', $table->id); - $this->setState($this->getName() . '.new', $isNew); - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Prepare and sanitise the table data prior to saving. - * - * @param \Joomla\CMS\Table\Table $table A Table object. - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - // Increment the content version number. - $table->version++; - } - - /** - * Method rebuild the entire nested set tree. - * - * @return boolean False on failure or error, true otherwise. - * - * @since 3.1 - */ - public function rebuild() - { - // Get an instance of the table object. - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - - $table = $this->getTable(); - - if (!$table->rebuild()) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to save the reordered nested set tree. - * First we save the new order values in the lft values of the changed ids. - * Then we invoke the table rebuild to implement the new ordering. - * - * @param array $idArray An array of primary key ids. - * @param integer $lftArray The lft value - * - * @return boolean False on failure or error, True otherwise - * - * @since 3.1 - */ - public function saveorder($idArray = null, $lftArray = null) - { - // Get an instance of the table object. - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - - $table = $this->getTable(); - - if (!$table->saveorder($idArray, $lftArray)) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the title & alias. - * - * @param integer $parentId The id of the parent. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 3.1 - */ - protected function generateNewTitle($parentId, $alias, $title) - { - // Alter the title & alias - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - - $table = $this->getTable(); - - while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) - { - $title = ($table->title != $title) ? $title : StringHelper::increment($title); - $alias = StringHelper::increment($alias, 'dash'); - } - - return array($title, $alias); - } + use VersionableModelTrait; + + /** + * @var string The prefix to use with controller messages. + * @since 3.1 + */ + protected $text_prefix = 'COM_TAGS'; + + /** + * @var string The type alias for this content type. + * @since 3.2 + */ + public $typeAlias = 'com_tags.tag'; + + /** + * Allowed batch commands + * + * @var array + * @since 3.7.0 + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage', + ); + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 3.1 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + return parent::canDelete($record); + } + + /** + * Auto-populate the model state. + * + * @note Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.1 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + $parentId = $app->input->getInt('parent_id'); + $this->setState('tag.parent_id', $parentId); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState($this->getName() . '.id', $pk); + + // Load the parameters. + $params = ComponentHelper::getParams('com_tags'); + $this->setState('params', $params); + } + + /** + * Method to get a tag. + * + * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. + * + * @return mixed Tag data object on success, false on failure. + * + * @since 3.1 + */ + public function getItem($pk = null) + { + if ($result = parent::getItem($pk)) { + // Prime required properties. + if (empty($result->id)) { + $result->parent_id = $this->getState('tag.parent_id'); + } + + // Convert the metadata field to an array. + $registry = new Registry($result->metadata); + $result->metadata = $registry->toArray(); + + // Convert the images field to an array. + $registry = new Registry($result->images); + $result->images = $registry->toArray(); + + // Convert the urls field to an array. + $registry = new Registry($result->urls); + $result->urls = $registry->toArray(); + + // Convert the modified date to local user time for display in the form. + $tz = new \DateTimeZone(Factory::getApplication()->get('offset')); + + if ((int) $result->modified_time) { + $date = new Date($result->modified_time); + $date->setTimezone($tz); + $result->modified_time = $date->toSql(true); + } else { + $result->modified_time = null; + } + } + + return $result; + } + + /** + * Method to get the row form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return bool|\Joomla\CMS\Form\Form A Form object on success, false on failure + * + * @since 3.1 + */ + public function getForm($data = array(), $loadData = true) + { + $jinput = Factory::getApplication()->input; + + // Get the form. + $form = $this->loadForm('com_tags.tag', 'tag', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + $user = Factory::getUser(); + + if (!$user->authorise('core.edit.state', 'com_tags' . $jinput->get('id'))) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 3.1 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_tags.edit.tag.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_tags.tag', $data); + + return $data; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function save($data) + { + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + $table = $this->getTable(); + $input = Factory::getApplication()->input; + $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState($this->getName() . '.id'); + $isNew = true; + $context = $this->option . '.' . $this->name; + + // Include the plugins for the save events. + PluginHelper::importPlugin($this->events_map['save']); + + try { + // Load the row if saving an existing tag. + if ($pk > 0) { + $table->load($pk); + $isNew = false; + } + + // Set the new parent id if parent id not matched OR while New/Save as Copy . + if ($table->parent_id != $data['parent_id'] || $data['id'] == 0) { + $table->setLocation($data['parent_id'], 'last-child'); + } + + // Alter the title for save as copy + if ($input->get('task') == 'save2copy') { + $origTable = $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['title'] == $origTable->title) { + list($title, $alias) = $this->generateNewTitle($data['parent_id'], $data['alias'], $data['title']); + $data['title'] = $title; + $data['alias'] = $alias; + } elseif ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + + $data['published'] = 0; + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Prepare the row for saving + $this->prepareTable($table); + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, $table, $isNew, $data)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, $table, $isNew)); + + // Rebuild the path for the tag: + if (!$table->rebuildPath($table->id)) { + $this->setError($table->getError()); + + return false; + } + + // Rebuild the paths of the tag's children: + if (!$table->rebuild($table->id, $table->lft, $table->level, $table->path)) { + $this->setError($table->getError()); + + return false; + } + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $this->setState($this->getName() . '.id', $table->id); + $this->setState($this->getName() . '.new', $isNew); + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Prepare and sanitise the table data prior to saving. + * + * @param \Joomla\CMS\Table\Table $table A Table object. + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + // Increment the content version number. + $table->version++; + } + + /** + * Method rebuild the entire nested set tree. + * + * @return boolean False on failure or error, true otherwise. + * + * @since 3.1 + */ + public function rebuild() + { + // Get an instance of the table object. + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + + $table = $this->getTable(); + + if (!$table->rebuild()) { + $this->setError($table->getError()); + + return false; + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to save the reordered nested set tree. + * First we save the new order values in the lft values of the changed ids. + * Then we invoke the table rebuild to implement the new ordering. + * + * @param array $idArray An array of primary key ids. + * @param integer $lftArray The lft value + * + * @return boolean False on failure or error, True otherwise + * + * @since 3.1 + */ + public function saveorder($idArray = null, $lftArray = null) + { + // Get an instance of the table object. + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + + $table = $this->getTable(); + + if (!$table->saveorder($idArray, $lftArray)) { + $this->setError($table->getError()); + + return false; + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the title & alias. + * + * @param integer $parentId The id of the parent. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 3.1 + */ + protected function generateNewTitle($parentId, $alias, $title) + { + // Alter the title & alias + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + + $table = $this->getTable(); + + while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) { + $title = ($table->title != $title) ? $title : StringHelper::increment($title); + $alias = StringHelper::increment($alias, 'dash'); + } + + return array($title, $alias); + } } diff --git a/administrator/components/com_tags/src/Model/TagsModel.php b/administrator/components/com_tags/src/Model/TagsModel.php index 254075adcd502..80f1a96f813e8 100644 --- a/administrator/components/com_tags/src/Model/TagsModel.php +++ b/administrator/components/com_tags/src/Model/TagsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.filter.extension', 'extension', 'com_content', 'cmd'); - - $this->setState('filter.extension', $extension); - $parts = explode('.', $extension); - - // Extract the component name - $this->setState('filter.component', $parts[0]); - - // Extract the optional section name - $this->setState('filter.section', (count($parts) > 1) ? $parts[1] : null); - - // Load the parameters. - $params = ComponentHelper::getParams('com_tags'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 3.1 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.extension'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.level'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.language'); - - return parent::getStoreId($id); - } - - /** - * Method to create a query for a list of items. - * - * @return string - * - * @since 3.1 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.id, a.title, a.alias, a.note, a.published, a.access, a.description' . - ', a.checked_out, a.checked_out_time, a.created_user_id' . - ', a.path, a.parent_id, a.level, a.lft, a.rgt' . - ', a.language' - ) - ); - $query->from($db->quoteName('#__tags', 'a')) - ->where($db->quoteName('a.alias') . ' <> ' . $db->quote('root')); - - // Join over the language - $query->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - ] - ) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); - - // Join over the users for the checked out user. - $query->select($db->quoteName('uc.name', 'editor')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Join over the users for the author. - $query->select($db->quoteName('ua.name', 'author_name')) - ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_user_id')) - ->select($db->quoteName('ug.title', 'access_title')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ug'), $db->quoteName('ug.id') . ' = ' . $db->quoteName('a.access')); - - // Count Items - $subQueryCountTaggedItems = $db->getQuery(true); - $subQueryCountTaggedItems - ->select('COUNT(' . $db->quoteName('tag_map.content_item_id') . ')') - ->from($db->quoteName('#__contentitem_tag_map', 'tag_map')) - ->where($db->quoteName('tag_map.tag_id') . ' = ' . $db->quoteName('a.id')); - $query->select('(' . (string) $subQueryCountTaggedItems . ') AS ' . $db->quoteName('countTaggedItems')); - - // Filter on the level. - if ($level = (int) $this->getState('filter.level')) - { - $query->where($db->quoteName('a.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter by access level. - if ($access = (int) $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - } - - // Filter by published state - $published = (string) $this->getState('filter.published'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->whereIn($db->quoteName('a.published'), [0, 1]); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.title') . ' LIKE :title', - $db->quoteName('a.alias') . ' LIKE :alias', - $db->quoteName('a.note') . ' LIKE :note', - - ], - 'OR' - ); - $query->bind(':title', $search) - ->bind(':alias', $search) - ->bind(':note', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - - // Add the list ordering clause - $listOrdering = $this->getState('list.ordering', 'a.lft'); - $listDirn = $db->escape($this->getState('list.direction', 'ASC')); - - if ($listOrdering == 'a.access') - { - $query->order('a.access ' . $listDirn . ', a.lft ' . $listDirn); - } - else - { - $query->order($db->escape($listOrdering) . ' ' . $listDirn); - } - - return $query; - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.0.1 - */ - public function getItems() - { - $items = parent::getItems(); - - if ($items != false) - { - $extension = $this->getState('filter.extension'); - - $this->countItems($items, $extension); - } - - return $items; - } - - /** - * Method to load the countItems method from the extensions - * - * @param \stdClass[] &$items The category items - * @param string $extension The category extension - * - * @return void - * - * @since 3.5 - */ - public function countItems(&$items, $extension) - { - $parts = explode('.', $extension); - - if (count($parts) < 2) - { - return; - } - - $component = Factory::getApplication()->bootComponent($parts[0]); - - if ($component instanceof TagServiceInterface) - { - $component->countTagItems($items, $extension); - } - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - $db = $this->getDatabase(); - - $query->where($db->quoteName('alias') . ' != ' . $db->quote('root')); - - return $query; - } + /** + * Constructor. + * + * @param MVCFactoryInterface $factory The factory. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', + 'a.id', + 'title', + 'a.title', + 'alias', + 'a.alias', + 'published', + 'a.published', + 'access', + 'a.access', + 'access_level', + 'language', + 'a.language', + 'checked_out', + 'a.checked_out', + 'checked_out_time', + 'a.checked_out_time', + 'created_time', + 'a.created_time', + 'created_user_id', + 'a.created_user_id', + 'lft', + 'a.lft', + 'rgt', + 'a.rgt', + 'level', + 'a.level', + 'path', + 'a.path', + 'countTaggedItems', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.1 + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + $extension = $this->getUserStateFromRequest($this->context . '.filter.extension', 'extension', 'com_content', 'cmd'); + + $this->setState('filter.extension', $extension); + $parts = explode('.', $extension); + + // Extract the component name + $this->setState('filter.component', $parts[0]); + + // Extract the optional section name + $this->setState('filter.section', (count($parts) > 1) ? $parts[1] : null); + + // Load the parameters. + $params = ComponentHelper::getParams('com_tags'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 3.1 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.extension'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.level'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.language'); + + return parent::getStoreId($id); + } + + /** + * Method to create a query for a list of items. + * + * @return string + * + * @since 3.1 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.title, a.alias, a.note, a.published, a.access, a.description' . + ', a.checked_out, a.checked_out_time, a.created_user_id' . + ', a.path, a.parent_id, a.level, a.lft, a.rgt' . + ', a.language' + ) + ); + $query->from($db->quoteName('#__tags', 'a')) + ->where($db->quoteName('a.alias') . ' <> ' . $db->quote('root')); + + // Join over the language + $query->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + ] + ) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); + + // Join over the users for the checked out user. + $query->select($db->quoteName('uc.name', 'editor')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Join over the users for the author. + $query->select($db->quoteName('ua.name', 'author_name')) + ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_user_id')) + ->select($db->quoteName('ug.title', 'access_title')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ug'), $db->quoteName('ug.id') . ' = ' . $db->quoteName('a.access')); + + // Count Items + $subQueryCountTaggedItems = $db->getQuery(true); + $subQueryCountTaggedItems + ->select('COUNT(' . $db->quoteName('tag_map.content_item_id') . ')') + ->from($db->quoteName('#__contentitem_tag_map', 'tag_map')) + ->where($db->quoteName('tag_map.tag_id') . ' = ' . $db->quoteName('a.id')); + $query->select('(' . (string) $subQueryCountTaggedItems . ') AS ' . $db->quoteName('countTaggedItems')); + + // Filter on the level. + if ($level = (int) $this->getState('filter.level')) { + $query->where($db->quoteName('a.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter by access level. + if ($access = (int) $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + } + + // Filter by published state + $published = (string) $this->getState('filter.published'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->whereIn($db->quoteName('a.published'), [0, 1]); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.title') . ' LIKE :title', + $db->quoteName('a.alias') . ' LIKE :alias', + $db->quoteName('a.note') . ' LIKE :note', + + ], + 'OR' + ); + $query->bind(':title', $search) + ->bind(':alias', $search) + ->bind(':note', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + + // Add the list ordering clause + $listOrdering = $this->getState('list.ordering', 'a.lft'); + $listDirn = $db->escape($this->getState('list.direction', 'ASC')); + + if ($listOrdering == 'a.access') { + $query->order('a.access ' . $listDirn . ', a.lft ' . $listDirn); + } else { + $query->order($db->escape($listOrdering) . ' ' . $listDirn); + } + + return $query; + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.0.1 + */ + public function getItems() + { + $items = parent::getItems(); + + if ($items != false) { + $extension = $this->getState('filter.extension'); + + $this->countItems($items, $extension); + } + + return $items; + } + + /** + * Method to load the countItems method from the extensions + * + * @param \stdClass[] &$items The category items + * @param string $extension The category extension + * + * @return void + * + * @since 3.5 + */ + public function countItems(&$items, $extension) + { + $parts = explode('.', $extension); + + if (count($parts) < 2) { + return; + } + + $component = Factory::getApplication()->bootComponent($parts[0]); + + if ($component instanceof TagServiceInterface) { + $component->countTagItems($items, $extension); + } + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + $db = $this->getDatabase(); + + $query->where($db->quoteName('alias') . ' != ' . $db->quote('root')); + + return $query; + } } diff --git a/administrator/components/com_tags/src/Table/TagTable.php b/administrator/components/com_tags/src/Table/TagTable.php index 5e65a182664ae..3ae680808a66d 100644 --- a/administrator/components/com_tags/src/Table/TagTable.php +++ b/administrator/components/com_tags/src/Table/TagTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_tags.tag'; - - parent::__construct('#__tags', 'id', $db); - } - - /** - * Overloaded check method to ensure data integrity. - * - * @return boolean True on success. - * - * @since 3.1 - * @throws \UnexpectedValueException - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Check for valid name. - if (trim($this->title) == '') - { - throw new \UnexpectedValueException('The title is empty'); - } - - if (empty($this->alias)) - { - $this->alias = $this->title; - } - - $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); - - if (trim(str_replace('-', '', $this->alias)) == '') - { - $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); - } - - // Check the publish down date is not earlier than publish up. - if (!empty($this->publish_down) && !empty($this->publish_up) && $this->publish_down < $this->publish_up) - { - throw new \UnexpectedValueException('End publish date is before start publish date.'); - } - - // Clean up description -- eliminate quotes and <> brackets - if (!empty($this->metadesc)) - { - // Only process if not empty - $bad_characters = array("\"", '<', '>'); - $this->metadesc = StringHelper::str_ireplace($bad_characters, '', $this->metadesc); - } - - if (empty($this->path)) - { - $this->path = ''; - } - - if (empty($this->hits)) - { - $this->hits = 0; - } - - if (empty($this->params)) - { - $this->params = '{}'; - } - - if (empty($this->metadesc)) - { - $this->metadesc = ''; - } - - if (empty($this->metakey)) - { - $this->metakey = ''; - } - - if (empty($this->metadata)) - { - $this->metadata = '{}'; - } - - if (empty($this->urls)) - { - $this->urls = '{}'; - } - - if (empty($this->images)) - { - $this->images = '{}'; - } - - if (!(int) $this->checked_out_time) - { - $this->checked_out_time = null; - } - - if (!(int) $this->publish_up) - { - $this->publish_up = null; - } - - if (!(int) $this->publish_down) - { - $this->publish_down = null; - } - - return true; - } - - /** - * Overridden \JTable::store to set modified data and user id. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate(); - $user = Factory::getUser(); - - if ($this->id) - { - // Existing item - $this->modified_user_id = $user->get('id'); - $this->modified_time = $date->toSql(); - } - else - { - // New tag. A tag created and created_by field can be set by the user, - // so we don't touch either of these if they are set. - if (!(int) $this->created_time) - { - $this->created_time = $date->toSql(); - } - - if (empty($this->created_user_id)) - { - $this->created_user_id = $user->get('id'); - } - - if (!(int) $this->modified_time) - { - $this->modified_time = $this->created_time; - } - - if (empty($this->modified_user_id)) - { - $this->modified_user_id = $this->created_user_id; - } - } - - // Verify that the alias is unique - $table = new static($this->getDbo()); - - if ($table->load(array('alias' => $this->alias)) && ($table->id != $this->id || $this->id == 0)) - { - $this->setError(Text::_('COM_TAGS_ERROR_UNIQUE_ALIAS')); - - return false; - } - - return parent::store($updateNulls); - } - - /** - * Method to delete a node and, optionally, its child nodes from the table. - * - * @param integer $pk The primary key of the node to delete. - * @param boolean $children True to delete child nodes, false to move them up a level. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function delete($pk = null, $children = false) - { - $return = parent::delete($pk, $children); - - if ($return) - { - $helper = new TagsHelper; - $helper->tagDeleteInstances($pk); - } - - return $return; - } - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } + /** + * An array of key names to be json encoded in the bind function + * + * @var array + * @since 4.0.0 + */ + protected $_jsonEncode = ['params', 'metadata', 'urls', 'images']; + + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Constructor + * + * @param DatabaseDriver $db A database connector object + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_tags.tag'; + + parent::__construct('#__tags', 'id', $db); + } + + /** + * Overloaded check method to ensure data integrity. + * + * @return boolean True on success. + * + * @since 3.1 + * @throws \UnexpectedValueException + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Check for valid name. + if (trim($this->title) == '') { + throw new \UnexpectedValueException('The title is empty'); + } + + if (empty($this->alias)) { + $this->alias = $this->title; + } + + $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); + + if (trim(str_replace('-', '', $this->alias)) == '') { + $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); + } + + // Check the publish down date is not earlier than publish up. + if (!empty($this->publish_down) && !empty($this->publish_up) && $this->publish_down < $this->publish_up) { + throw new \UnexpectedValueException('End publish date is before start publish date.'); + } + + // Clean up description -- eliminate quotes and <> brackets + if (!empty($this->metadesc)) { + // Only process if not empty + $bad_characters = array("\"", '<', '>'); + $this->metadesc = StringHelper::str_ireplace($bad_characters, '', $this->metadesc); + } + + if (empty($this->path)) { + $this->path = ''; + } + + if (empty($this->hits)) { + $this->hits = 0; + } + + if (empty($this->params)) { + $this->params = '{}'; + } + + if (empty($this->metadesc)) { + $this->metadesc = ''; + } + + if (empty($this->metakey)) { + $this->metakey = ''; + } + + if (empty($this->metadata)) { + $this->metadata = '{}'; + } + + if (empty($this->urls)) { + $this->urls = '{}'; + } + + if (empty($this->images)) { + $this->images = '{}'; + } + + if (!(int) $this->checked_out_time) { + $this->checked_out_time = null; + } + + if (!(int) $this->publish_up) { + $this->publish_up = null; + } + + if (!(int) $this->publish_down) { + $this->publish_down = null; + } + + return true; + } + + /** + * Overridden \JTable::store to set modified data and user id. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate(); + $user = Factory::getUser(); + + if ($this->id) { + // Existing item + $this->modified_user_id = $user->get('id'); + $this->modified_time = $date->toSql(); + } else { + // New tag. A tag created and created_by field can be set by the user, + // so we don't touch either of these if they are set. + if (!(int) $this->created_time) { + $this->created_time = $date->toSql(); + } + + if (empty($this->created_user_id)) { + $this->created_user_id = $user->get('id'); + } + + if (!(int) $this->modified_time) { + $this->modified_time = $this->created_time; + } + + if (empty($this->modified_user_id)) { + $this->modified_user_id = $this->created_user_id; + } + } + + // Verify that the alias is unique + $table = new static($this->getDbo()); + + if ($table->load(array('alias' => $this->alias)) && ($table->id != $this->id || $this->id == 0)) { + $this->setError(Text::_('COM_TAGS_ERROR_UNIQUE_ALIAS')); + + return false; + } + + return parent::store($updateNulls); + } + + /** + * Method to delete a node and, optionally, its child nodes from the table. + * + * @param integer $pk The primary key of the node to delete. + * @param boolean $children True to delete child nodes, false to move them up a level. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function delete($pk = null, $children = false) + { + $return = parent::delete($pk, $children); + + if ($return) { + $helper = new TagsHelper(); + $helper->tagDeleteInstances($pk); + } + + return $return; + } + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } } diff --git a/administrator/components/com_tags/src/View/Tag/HtmlView.php b/administrator/components/com_tags/src/View/Tag/HtmlView.php index 80a2745b25dfc..4b8773693aea9 100644 --- a/administrator/components/com_tags/src/View/Tag/HtmlView.php +++ b/administrator/components/com_tags/src/View/Tag/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @since 3.1 - * - * @return void - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $userId = $user->get('id'); - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - $canDo = ContentHelper::getActions('com_tags'); - - ToolbarHelper::title($isNew ? Text::_('COM_TAGS_MANAGER_TAG_NEW') : Text::_('COM_TAGS_MANAGER_TAG_EDIT'), 'tag'); - - // Build the actions for new and existing records. - if ($isNew) - { - ToolbarHelper::apply('tag.apply'); - ToolbarHelper::saveGroup( - [ - ['save', 'tag.save'], - ['save2new', 'tag.save2new'] - ], - 'btn-success' - ); - - ToolbarHelper::cancel('tag.cancel'); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_user_id == $userId); - - $toolbarButtons = []; - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - ToolbarHelper::apply('tag.apply'); - $toolbarButtons[] = ['save', 'tag.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'tag.save2new']; - } - } - - // If checked out, we can still save - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'tag.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('tag.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) - { - ToolbarHelper::versions('com_tags.tag', $this->item->id); - } - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Tags:_New_or_Edit'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * Flag if an association exists + * + * @var boolean + */ + protected $assoc; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * + * @since 4.0.0 + */ + protected $canDo; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @since 3.1 + * + * @return void + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->get('id'); + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + $canDo = ContentHelper::getActions('com_tags'); + + ToolbarHelper::title($isNew ? Text::_('COM_TAGS_MANAGER_TAG_NEW') : Text::_('COM_TAGS_MANAGER_TAG_EDIT'), 'tag'); + + // Build the actions for new and existing records. + if ($isNew) { + ToolbarHelper::apply('tag.apply'); + ToolbarHelper::saveGroup( + [ + ['save', 'tag.save'], + ['save2new', 'tag.save2new'] + ], + 'btn-success' + ); + + ToolbarHelper::cancel('tag.cancel'); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_user_id == $userId); + + $toolbarButtons = []; + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + ToolbarHelper::apply('tag.apply'); + $toolbarButtons[] = ['save', 'tag.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'tag.save2new']; + } + } + + // If checked out, we can still save + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'tag.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('tag.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) { + ToolbarHelper::versions('com_tags.tag', $this->item->id); + } + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Tags:_New_or_Edit'); + } } diff --git a/administrator/components/com_tags/src/View/Tags/HtmlView.php b/administrator/components/com_tags/src/View/Tags/HtmlView.php index 3616b1fbcaf5e..549ac09f21524 100644 --- a/administrator/components/com_tags/src/View/Tags/HtmlView.php +++ b/administrator/components/com_tags/src/View/Tags/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Preprocess the list of items to find ordering divisions. - foreach ($this->items as &$item) - { - $this->ordering[$item->parent_id][] = $item->id; - } - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.1 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_tags'); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_TAGS_MANAGER_TAGS'), 'tags'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('tag.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('tags.publish')->listCheck(true); - $childBar->unpublish('tags.unpublish')->listCheck(true); - $childBar->archive('tags.archive')->listCheck(true); - } - - if ($user->authorise('core.admin')) - { - $childBar->checkin('tags.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) - { - $childBar->trash('tags.trash')->listCheck(true); - } - - // Add a batch button - if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('tags.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_tags'); - } - - $toolbar->help('Tags'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Preprocess the list of items to find ordering divisions. + foreach ($this->items as &$item) { + $this->ordering[$item->parent_id][] = $item->id; + } + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.1 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_tags'); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_TAGS_MANAGER_TAGS'), 'tags'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('tag.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('tags.publish')->listCheck(true); + $childBar->unpublish('tags.unpublish')->listCheck(true); + $childBar->archive('tags.archive')->listCheck(true); + } + + if ($user->authorise('core.admin')) { + $childBar->checkin('tags.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) { + $childBar->trash('tags.trash')->listCheck(true); + } + + // Add a batch button + if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('tags.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_tags'); + } + + $toolbar->help('Tags'); + } } diff --git a/administrator/components/com_tags/tmpl/tag/edit.php b/administrator/components/com_tags/tmpl/tag/edit.php index d4cd2270515e3..f451c3cb0f425 100644 --- a/administrator/components/com_tags/tmpl/tag/edit.php +++ b/administrator/components/com_tags/tmpl/tag/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); // Fieldsets to not automatically render by /layouts/joomla/edit/params.php $this->ignore_fieldsets = ['jmetadata']; @@ -27,50 +28,50 @@
    - + -
    - 'details', 'recall' => true, 'breakpoint' => 768]); ?> +
    + 'details', 'recall' => true, 'breakpoint' => 768]); ?> - -
    -
    -
    - form->getLabel('description'); ?> - form->getInput('description'); ?> -
    -
    -
    - -
    -
    - + +
    +
    +
    + form->getLabel('description'); ?> + form->getInput('description'); ?> +
    +
    +
    + +
    +
    + - + - -
    -
    -
    - -
    - -
    -
    -
    -
    -
    - -
    - -
    -
    -
    -
    - + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + - -
    - - + +
    + +
    diff --git a/administrator/components/com_tags/tmpl/tags/default.php b/administrator/components/com_tags/tmpl/tags/default.php index c3e455a2b66dc..b7799fd6df4ba 100644 --- a/administrator/components/com_tags/tmpl/tags/default.php +++ b/administrator/components/com_tags/tmpl/tags/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $app = Factory::getApplication(); $user = Factory::getUser(); @@ -35,249 +36,240 @@ $section = null; $mode = false; -if (count($parts) > 1) -{ - $section = $parts[1]; - $inflector = Inflector::getInstance(); +if (count($parts) > 1) { + $section = $parts[1]; + $inflector = Inflector::getInstance(); - if (!$inflector->isPlural($section)) - { - $section = $inflector->toPlural($section); - } + if (!$inflector->isPlural($section)) { + $section = $inflector->toPlural($section); + } } -if ($section === 'categories') -{ - $mode = true; - $section = $component; - $component = 'com_categories'; +if ($section === 'categories') { + $mode = true; + $section = $component; + $component = 'com_categories'; } -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_tags&task=tags.saveOrderAjax&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_tags&task=tags.saveOrderAjax&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
    -
    - $this)); - ?> - items)) : ?> -
    - - -
    - - - - - - - - - +
    + $this)); + ?> + items)) : ?> +
    + + +
    + +
    - , - , - -
    - - - - - - - -
    + + + + + + + - items[0]) && property_exists($this->items[0], 'count_published')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_archived')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> - - + items[0]) && property_exists($this->items[0], 'count_published')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_archived')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> + + - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : - $orderkey = array_search($item->id, $this->ordering[$item->parent_id]); - $canCreate = $user->authorise('core.create', 'com_tags'); - $canEdit = $user->authorise('core.edit', 'com_tags'); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_tags') && $canCheckin; + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : + $orderkey = array_search($item->id, $this->ordering[$item->parent_id]); + $canCreate = $user->authorise('core.create', 'com_tags'); + $canEdit = $user->authorise('core.edit', 'com_tags'); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_tags') && $canCheckin; - // Get the parents of item for sorting - if ($item->level > 1) - { - $parentsStr = ''; - $_currentParentId = $item->parent_id; - $parentsStr = ' ' . $_currentParentId; - for ($j = 0; $j < $item->level; $j++) - { - foreach ($this->ordering as $k => $v) - { - $v = implode('-', $v); - $v = '-' . $v . '-'; - if (strpos($v, '-' . $_currentParentId . '-') !== false) - { - $parentsStr .= ' ' . $k; - $_currentParentId = $k; - break; - } - } - } - } - else - { - $parentsStr = ''; - } - ?> - - - - - + // Get the parents of item for sorting + if ($item->level > 1) { + $parentsStr = ''; + $_currentParentId = $item->parent_id; + $parentsStr = ' ' . $_currentParentId; + for ($j = 0; $j < $item->level; $j++) { + foreach ($this->ordering as $k => $v) { + $v = implode('-', $v); + $v = '-' . $v . '-'; + if (strpos($v, '-' . $_currentParentId . '-') !== false) { + $parentsStr .= ' ' . $k; + $_currentParentId = $k; + break; + } + } + } + } else { + $parentsStr = ''; + } + ?> + + + + + - items[0]) && property_exists($this->items[0], 'count_published')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_archived')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> - - - - - - - - - - - -
    + , + , + +
    + + + + + + + + - - - - - - - - + + + + + + + + - - - state->get('list.direction'), $this->state->get('list.ordering')); ?> - - - - -
    + + + state->get('list.direction'), $this->state->get('list.ordering')); ?> + + + + +
    - id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - published, $i, 'tags.', $canChange); ?> - - $item->level)); ?> - checked_out) : ?> - editor, $item->checked_out_time, 'tags.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
    - note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - -
    -
    + id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + published, $i, 'tags.', $canChange); ?> + + $item->level)); ?> + checked_out) : ?> + editor, $item->checked_out_time, 'tags.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
    + note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
    +
    - - count_published; ?> - - - count_unpublished; ?> - - - count_archived; ?> - - - count_trashed; ?> - - escape($item->access_title); ?> - - - - - countTaggedItems; ?> - - - id; ?> -
    + items[0]) && property_exists($this->items[0], 'count_published')) : ?> + + + count_published; ?> + + + items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> + + + count_unpublished; ?> + + + items[0]) && property_exists($this->items[0], 'count_archived')) : ?> + + + count_archived; ?> + + + items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> + + + count_trashed; ?> + + + + escape($item->access_title); ?> + + + + + + + + + countTaggedItems; ?> + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_tags') - && $user->authorise('core.edit', 'com_tags') - && $user->authorise('core.edit.state', 'com_tags')) : ?> - Text::_('COM_TAGS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - + + authorise('core.create', 'com_tags') + && $user->authorise('core.edit', 'com_tags') + && $user->authorise('core.edit.state', 'com_tags') +) : ?> + Text::_('COM_TAGS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + - - - -
    + + + +
    diff --git a/administrator/components/com_tags/tmpl/tags/default_batch_body.php b/administrator/components/com_tags/tmpl/tags/default_batch_body.php index bc82733e4def3..cffa2753fb27e 100644 --- a/administrator/components/com_tags/tmpl/tags/default_batch_body.php +++ b/administrator/components/com_tags/tmpl/tags/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Multilanguage; @@ -15,18 +17,18 @@ ?>
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    diff --git a/administrator/components/com_tags/tmpl/tags/default_batch_footer.php b/administrator/components/com_tags/tmpl/tags/default_batch_footer.php index a30292feb9cd3..c448ec1ba0dd8 100644 --- a/administrator/components/com_tags/tmpl/tags/default_batch_footer.php +++ b/administrator/components/com_tags/tmpl/tags/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/administrator/components/com_tags/tmpl/tags/emptystate.php b/administrator/components/com_tags/tmpl/tags/emptystate.php index 04eb96caa97c5..2b49d1b63bcda 100644 --- a/administrator/components/com_tags/tmpl/tags/emptystate.php +++ b/administrator/components/com_tags/tmpl/tags/emptystate.php @@ -1,4 +1,5 @@ 'COM_TAGS', - 'formURL' => 'index.php?option=com_tags&task=tag.add', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/J3.x:How_To_Use_Content_Tags_in_Joomla!', - 'icon' => 'icon-tags tags', + 'textPrefix' => 'COM_TAGS', + 'formURL' => 'index.php?option=com_tags&task=tag.add', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/J3.x:How_To_Use_Content_Tags_in_Joomla!', + 'icon' => 'icon-tags tags', ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_tags')) -{ - $displayData['createURL'] = 'index.php?option=com_tags&task=tag.add'; +if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_tags')) { + $displayData['createURL'] = 'index.php?option=com_tags&task=tag.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_templates/helpers/template.php b/administrator/components/com_templates/helpers/template.php index 38115180eef74..ca8ee1762b81a 100644 --- a/administrator/components/com_templates/helpers/template.php +++ b/administrator/components/com_templates/helpers/template.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Templates')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Templates')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Templates')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Templates')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new TemplatesComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new TemplatesComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_templates/src/Controller/DisplayController.php b/administrator/components/com_templates/src/Controller/DisplayController.php index d7eec744efa42..5a319d3080f2f 100644 --- a/administrator/components/com_templates/src/Controller/DisplayController.php +++ b/administrator/components/com_templates/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'styles'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', 'styles'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - // For JSON requests - if ($this->app->getDocument()->getType() == 'json') - { - return parent::display(); - } + // For JSON requests + if ($this->app->getDocument()->getType() == 'json') { + return parent::display(); + } - // Check for edit form. - if ($view == 'style' && $layout == 'edit' && !$this->checkEditId('com_templates.edit.style', $id)) - { - // 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', $id), 'error'); - } + // Check for edit form. + if ($view == 'style' && $layout == 'edit' && !$this->checkEditId('com_templates.edit.style', $id)) { + // 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', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_templates&view=styles', false)); + $this->setRedirect(Route::_('index.php?option=com_templates&view=styles', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/administrator/components/com_templates/src/Controller/StyleController.php b/administrator/components/com_templates/src/Controller/StyleController.php index 748af6bcb6e7b..bda9b2548034f 100644 --- a/administrator/components/com_templates/src/Controller/StyleController.php +++ b/administrator/components/com_templates/src/Controller/StyleController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Templates\Administrator\Controller; \defined('_JEXEC') or die; @@ -21,135 +23,124 @@ */ class StyleController extends FormController { - /** - * The prefix to use with controller messages. - * - * @var string - * @since 1.6 - */ - protected $text_prefix = 'COM_TEMPLATES_STYLE'; - - /** - * Method to save a template style. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 1.6 - */ - public function save($key = null, $urlVar = null) - { - $this->checkToken(); - - if ($this->app->getDocument()->getType() === 'json') - { - $model = $this->getModel('Style', 'Administrator'); - $table = $model->getTable(); - $data = $this->input->post->get('params', array(), 'array'); - $checkin = $table->hasField('checked_out'); - $context = $this->option . '.edit.' . $this->context; - - $item = $model->getItem($this->app->getTemplate(true)->id); - - // Setting received params - $item->set('params', $data); - - $data = $item->getProperties(); - unset($data['xml']); - - $key = $table->getKeyName(); - - // Access check. - if (!$this->allowSave($data, $key)) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return false; - } - - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_templates/forms'); - - // Validate the posted data. - // Sometimes the form needs some posted data, such as for plugins and modules. - $form = $model->getForm($data, false); - - if (!$form) - { - $this->app->enqueueMessage($model->getError(), 'error'); - - return false; - } - - // Test whether the data is valid. - $validData = $model->validate($form, $data); - - if ($validData === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $this->app->setUserState($context . '.data', $data); - - return false; - } - - if (!isset($validData['tags'])) - { - $validData['tags'] = null; - } - - // Attempt to save the data. - if (!$model->save($validData)) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $validData); - - $this->app->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - - return false; - } - - // Save succeeded, so check-in the record. - if ($checkin && $model->checkin($validData[$key]) === false) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $validData); - - // Check-in failed, so go back to the record and display a notice. - $this->app->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); - - return false; - } - - // Redirect the user and adjust session state - // Set the record data in the session. - $recordId = $model->getState($this->context . '.id'); - $this->holdEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - $model->checkout($recordId); - - // Invoke the postSave method to allow for the child class to access the model. - $this->postSaveHook($model, $validData); + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_TEMPLATES_STYLE'; + + /** + * Method to save a template style. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 1.6 + */ + public function save($key = null, $urlVar = null) + { + $this->checkToken(); + + if ($this->app->getDocument()->getType() === 'json') { + $model = $this->getModel('Style', 'Administrator'); + $table = $model->getTable(); + $data = $this->input->post->get('params', array(), 'array'); + $checkin = $table->hasField('checked_out'); + $context = $this->option . '.edit.' . $this->context; + + $item = $model->getItem($this->app->getTemplate(true)->id); + + // Setting received params + $item->set('params', $data); + + $data = $item->getProperties(); + unset($data['xml']); + + $key = $table->getKeyName(); + + // Access check. + if (!$this->allowSave($data, $key)) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return false; + } + + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_templates/forms'); + + // Validate the posted data. + // Sometimes the form needs some posted data, such as for plugins and modules. + $form = $model->getForm($data, false); + + if (!$form) { + $this->app->enqueueMessage($model->getError(), 'error'); + + return false; + } + + // Test whether the data is valid. + $validData = $model->validate($form, $data); + + if ($validData === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $this->app->setUserState($context . '.data', $data); + + return false; + } + + if (!isset($validData['tags'])) { + $validData['tags'] = null; + } + + // Attempt to save the data. + if (!$model->save($validData)) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $validData); + + $this->app->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + + return false; + } + + // Save succeeded, so check-in the record. + if ($checkin && $model->checkin($validData[$key]) === false) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $validData); + + // Check-in failed, so go back to the record and display a notice. + $this->app->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); + + return false; + } + + // Redirect the user and adjust session state + // Set the record data in the session. + $recordId = $model->getState($this->context . '.id'); + $this->holdEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + $model->checkout($recordId); + + // Invoke the postSave method to allow for the child class to access the model. + $this->postSaveHook($model, $validData); - return true; - } + return true; + } - return parent::save($key, $urlVar); - } + return parent::save($key, $urlVar); + } } diff --git a/administrator/components/com_templates/src/Controller/StylesController.php b/administrator/components/com_templates/src/Controller/StylesController.php index 2a3afaa530ed2..c0d597d253dad 100644 --- a/administrator/components/com_templates/src/Controller/StylesController.php +++ b/administrator/components/com_templates/src/Controller/StylesController.php @@ -1,4 +1,5 @@ checkToken(); - - $pks = (array) $this->input->post->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $pks = array_filter($pks); - - try - { - if (empty($pks)) - { - throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); - } - - $model = $this->getModel(); - $model->duplicate($pks); - $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_DUPLICATED')); - } - catch (\Exception $e) - { - $this->app->enqueueMessage($e->getMessage(), 'error'); - } - - $this->setRedirect('index.php?option=com_templates&view=styles'); - } - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return BaseDatabaseModel - * - * @since 1.6 - */ - public function getModel($name = 'Style', $prefix = 'Administrator', $config = array()) - { - return parent::getModel($name, $prefix, array('ignore_request' => true)); - } - - /** - * Method to set the home template for a client. - * - * @return void - * - * @since 1.6 - */ - public function setDefault() - { - // Check for request forgeries - $this->checkToken(); - - $pks = (array) $this->input->post->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $pks = array_filter($pks); - - try - { - if (empty($pks)) - { - throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); - } - - // Pop off the first element. - $id = array_shift($pks); - - /** @var \Joomla\Component\Templates\Administrator\Model\StyleModel $model */ - $model = $this->getModel(); - $model->setHome($id); - $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_HOME_SET')); - } - catch (\Exception $e) - { - $this->setMessage($e->getMessage(), 'warning'); - } - - $this->setRedirect('index.php?option=com_templates&view=styles'); - } - - /** - * Method to unset the default template for a client and for a language - * - * @return void - * - * @since 1.6 - */ - public function unsetDefault() - { - // Check for request forgeries - $this->checkToken('request'); - - $pks = (array) $this->input->get->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $pks = array_filter($pks); - - try - { - if (empty($pks)) - { - throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); - } - - // Pop off the first element. - $id = array_shift($pks); - - /** @var \Joomla\Component\Templates\Administrator\Model\StyleModel $model */ - $model = $this->getModel(); - $model->unsetHome($id); - $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_HOME_UNSET')); - } - catch (\Exception $e) - { - $this->setMessage($e->getMessage(), 'warning'); - } - - $this->setRedirect('index.php?option=com_templates&view=styles'); - } + /** + * Method to clone and existing template style. + * + * @return void + */ + public function duplicate() + { + // Check for request forgeries + $this->checkToken(); + + $pks = (array) $this->input->post->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $pks = array_filter($pks); + + try { + if (empty($pks)) { + throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); + } + + $model = $this->getModel(); + $model->duplicate($pks); + $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_DUPLICATED')); + } catch (\Exception $e) { + $this->app->enqueueMessage($e->getMessage(), 'error'); + } + + $this->setRedirect('index.php?option=com_templates&view=styles'); + } + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return BaseDatabaseModel + * + * @since 1.6 + */ + public function getModel($name = 'Style', $prefix = 'Administrator', $config = array()) + { + return parent::getModel($name, $prefix, array('ignore_request' => true)); + } + + /** + * Method to set the home template for a client. + * + * @return void + * + * @since 1.6 + */ + public function setDefault() + { + // Check for request forgeries + $this->checkToken(); + + $pks = (array) $this->input->post->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $pks = array_filter($pks); + + try { + if (empty($pks)) { + throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); + } + + // Pop off the first element. + $id = array_shift($pks); + + /** @var \Joomla\Component\Templates\Administrator\Model\StyleModel $model */ + $model = $this->getModel(); + $model->setHome($id); + $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_HOME_SET')); + } catch (\Exception $e) { + $this->setMessage($e->getMessage(), 'warning'); + } + + $this->setRedirect('index.php?option=com_templates&view=styles'); + } + + /** + * Method to unset the default template for a client and for a language + * + * @return void + * + * @since 1.6 + */ + public function unsetDefault() + { + // Check for request forgeries + $this->checkToken('request'); + + $pks = (array) $this->input->get->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $pks = array_filter($pks); + + try { + if (empty($pks)) { + throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); + } + + // Pop off the first element. + $id = array_shift($pks); + + /** @var \Joomla\Component\Templates\Administrator\Model\StyleModel $model */ + $model = $this->getModel(); + $model->unsetHome($id); + $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_HOME_UNSET')); + } catch (\Exception $e) { + $this->setMessage($e->getMessage(), 'warning'); + } + + $this->setRedirect('index.php?option=com_templates&view=styles'); + } } diff --git a/administrator/components/com_templates/src/Controller/TemplateController.php b/administrator/components/com_templates/src/Controller/TemplateController.php index 15643a5cfe776..f8406fdbc9c89 100644 --- a/administrator/components/com_templates/src/Controller/TemplateController.php +++ b/administrator/components/com_templates/src/Controller/TemplateController.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - $this->registerTask('unpublish', 'publish'); - $this->registerTask('publish', 'publish'); - $this->registerTask('deleteOverrideHistory', 'publish'); - } - - /** - * Method for closing the template. - * - * @return void - * - * @since 3.2 - */ - public function cancel() - { - $this->setRedirect(Route::_('index.php?option=com_templates&view=templates', false)); - } - - /** - * Method for closing a file. - * - * @return void - * - * @since 3.2 - */ - public function close() - { - $file = base64_encode('home'); - $id = (int) $this->input->get('id', 0, 'int'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . - $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - - /** - * Marked as Checked/Unchecked of override history. - * - * @return void - * - * @since 4.0.0 - */ - public function publish() - { - // Check for request forgeries. - $this->checkToken(); - - $file = $this->input->get('file'); - $id = $this->input->get('id'); - - $ids = (array) $this->input->get('cid', array(), 'string'); - $values = array('publish' => 1, 'unpublish' => 0, 'deleteOverrideHistory' => -3); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - - if (empty($ids)) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_NO_FILE_SELECTED'), 'warning'); - } - else - { - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - // Change the state of the records. - if (!$model->publish($ids, $value, $id)) - { - $this->setMessage(implode('
    ', $model->getErrors()), 'warning'); - } - else - { - if ($value === 1) - { - $ntext = 'COM_TEMPLATES_N_OVERRIDE_CHECKED'; - } - elseif ($value === 0) - { - $ntext = 'COM_TEMPLATES_N_OVERRIDE_UNCHECKED'; - } - elseif ($value === -3) - { - $ntext = 'COM_TEMPLATES_N_OVERRIDE_DELETED'; - } - - $this->setMessage(Text::plural($ntext, count($ids))); - } - } - - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . - $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - - /** - * Method for copying the template. - * - * @return boolean true on success, false otherwise - * - * @since 3.2 - */ - public function copy() - { - // Check for request forgeries - $this->checkToken(); - - $app = $this->app; - $this->input->set('installtype', 'folder'); - $newNameRaw = $this->input->get('new_name', null, 'string'); - // Only accept letters, numbers and underscore for template name - $newName = preg_replace('/[^a-zA-Z0-9_]/', '', $newNameRaw); - $templateID = (int) $this->input->getInt('id', 0); - $file = (string) $this->input->get('file', '', 'cmd'); - - // Access check. - if (!$this->allowEdit()) - { - $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return false; - } - - $this->setRedirect('index.php?option=com_templates&view=template&id=' . $templateID . '&file=' . $file); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel('Template', 'Administrator'); - $model->setState('new_name', $newName); - $model->setState('tmp_prefix', uniqid('template_copy_')); - $model->setState('to_path', $app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); - - // Process only if we have a new name entered - if (strlen($newName) > 0) - { - if (!$this->app->getIdentity()->authorise('core.create', 'com_templates')) - { - // User is not authorised to delete - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_CREATE_NOT_PERMITTED'), 'error'); - - return false; - } - - // Check that new name is valid - if (($newNameRaw !== null) && ($newName !== $newNameRaw)) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); - - return false; - } - - // Check that new name doesn't already exist - if (!$model->checkNewName()) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_DUPLICATE_TEMPLATE_NAME'), 'error'); - - return false; - } - - // Check that from name does exist and get the folder name - $fromName = $model->getFromName(); - - if (!$fromName) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); - - return false; - } - - // Call model's copy method - if (!$model->copy()) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_COPY'), 'error'); - - return false; - } - - // Call installation model - $this->input->set('install_directory', $app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); - - /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $installModel */ - $installModel = $this->app->bootComponent('com_installer') - ->getMVCFactory()->createModel('Install', 'Administrator'); - $this->app->getLanguage()->load('com_installer'); - - if (!$installModel->install()) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_INSTALL'), 'error'); - - return false; - } - - $this->setMessage(Text::sprintf('COM_TEMPLATES_COPY_SUCCESS', $newName)); - $model->cleanup(); - - return true; - } - - $this->setMessage(Text::sprintf('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); - - return false; - } - - /** - * Method to get a model object, loading it if required. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional (note, the empty array is atypical compared to other models). - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 3.2 - */ - public function getModel($name = 'Template', $prefix = 'Administrator', $config = array()) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to check if you can add a new record. - * - * @return boolean - * - * @since 3.2 - */ - protected function allowEdit() - { - return $this->app->getIdentity()->authorise('core.admin'); - } - - /** - * Saves a template source file. - * - * @return void - * - * @since 3.2 - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - $data = $this->input->post->get('jform', array(), 'array'); - $task = $this->getTask(); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $fileName = (string) $this->input->getCmd('file', ''); - $explodeArray = explode(':', str_replace('//', '/', base64_decode($fileName))); - - // Access check. - if (!$this->allowEdit()) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - // Match the stored id's with the submitted. - if (empty($data['extension_id']) || empty($data['filename'])) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); - - return; - } - elseif ($data['extension_id'] != $model->getState('extension.id')) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); - - return; - } - elseif (str_ends_with(end($explodeArray), Path::clean($data['filename'], '/'))) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); - - return; - } - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - $this->setMessage($model->getError(), 'error'); - - return; - } - - $data = $model->validate($form, $data); - - // Check for validation errors. - if ($data === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Redirect back to the edit screen. - $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - - return; - } - - // Attempt to save the data. - if (!$model->save($data)) - { - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED', $model->getError()), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - - return; - } - - $this->setMessage(Text::_('COM_TEMPLATES_FILE_SAVE_SUCCESS')); - - // Redirect the user based on the chosen task. - switch ($task) - { - case 'apply': - // Redirect back to the edit screen. - $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - break; - - default: - // Redirect to the list screen. - $file = base64_encode('home'); - $id = (int) $this->input->get('id', 0, 'int'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - break; - } - } - - /** - * Method for creating override. - * - * @return void - * - * @since 3.2 - */ - public function overrides() - { - // Check for request forgeries. - $this->checkToken('get'); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $file = (string) $this->input->getCmd('file', ''); - $override = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('folder', '')), 'path'); - $id = (int) $this->input->get('id', 0, 'int'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - $model->createOverride($override); - - // Redirect back to the edit screen. - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - - /** - * Method for deleting a file. - * - * @return void - * - * @since 3.2 - */ - public function delete() - { - // Check for request forgeries - $this->checkToken(); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (base64_decode(urldecode($file)) == '/index.php') - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INDEX_DELETE'), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif (base64_decode(urldecode($file)) == '/joomla.asset.json') - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_ASSET_FILE_DELETE'), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->deleteFile($file)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_DELETE_SUCCESS')); - $file = base64_encode('home'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_DELETE'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for creating a new file. - * - * @return void - * - * @since 3.2 - */ - public function createFile() - { - // Check for request forgeries - $this->checkToken(); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->get('file', '', 'cmd'); - $name = (string) $this->input->get('name', '', 'cmd'); - $location = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); - $type = (string) $this->input->get('type', '', 'cmd'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if ($type == 'null') - { - $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_TYPE'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $name)) - { - $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->createFile($name, $type, $location)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_CREATE_SUCCESS')); - $file = urlencode(base64_encode($location . '/' . $name . '.' . $type)); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_CREATE'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for uploading a file. - * - * @return void - * - * @since 3.2 - */ - public function uploadFile() - { - // Check for request forgeries - $this->checkToken(); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $upload = $this->input->files->get('files'); - $location = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if ($return = $model->uploadFile($upload, $location)) - { - $this->setMessage(Text::sprintf('COM_TEMPLATES_FILE_UPLOAD_SUCCESS', $upload['name'])); - $redirect = base64_encode($return); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $redirect . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_UPLOAD'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for creating a new folder. - * - * @return void - * - * @since 3.2 - */ - public function createFolder() - { - // Check for request forgeries - $this->checkToken(); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $name = $this->input->get('name'); - $location = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (!preg_match('/^[a-zA-Z0-9-_.]+$/', $name)) - { - $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FOLDER_NAME'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->createFolder($name, $location)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_CREATE_SUCCESS')); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FOLDER_CREATE'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for deleting a folder. - * - * @return void - * - * @since 3.2 - */ - public function deleteFolder() - { - // Check for request forgeries - $this->checkToken(); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $isMedia = (int) $this->input->get('isMedia', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $location = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (empty($location)) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_ROOT_DELETE'), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->deleteFolder($location)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_SUCCESS')); - - if (stristr(base64_decode($file), $location) != false) - { - $file = base64_encode('home'); - } - - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_ERROR'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for renaming a file. - * - * @return void - * - * @since 3.2 - */ - public function renameFile() - { - // Check for request forgeries - $this->checkToken(); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $isMedia = (int) $this->input->get('isMedia', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $newName = $this->input->get('new_name'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (base64_decode(urldecode($file)) == '/index.php') - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_RENAME_INDEX'), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - elseif (base64_decode(urldecode($file)) == '/joomla.asset.json') - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_RENAME_ASSET_FILE'), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $newName)) - { - $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - elseif ($rename = $model->renameFile($file, $newName)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_RENAME_SUCCESS')); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $rename . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_RENAME'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for cropping an image. - * - * @return void - * - * @since 3.2 - */ - public function cropImage() - { - // Check for request forgeries - $this->checkToken(); - - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->get('file', '', 'cmd'); - $x = $this->input->get('x'); - $y = $this->input->get('y'); - $w = $this->input->get('w'); - $h = $this->input->get('h'); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (empty($w) && empty($h) && empty($x) && empty($y)) - { - $this->setMessage(Text::_('COM_TEMPLATES_CROP_AREA_ERROR'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->cropImage($file, $w, $h, $x, $y)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_CROP_SUCCESS')); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_CROP_ERROR'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for resizing an image. - * - * @return void - * - * @since 3.2 - */ - public function resizeImage() - { - // Check for request forgeries - $this->checkToken(); - - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $width = $this->input->get('width'); - $height = $this->input->get('height'); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if ($model->resizeImage($file, $width, $height)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_RESIZE_SUCCESS')); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_RESIZE_ERROR'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for copying a file. - * - * @return void - * - * @since 3.2 - */ - public function copyFile() - { - // Check for request forgeries - $this->checkToken(); - - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $newName = $this->input->get('new_name'); - $location = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (!preg_match('/^[a-zA-Z0-9-_]+$/', $newName)) - { - $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->copyFile($newName, $location, $file)) - { - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_COPY_FAIL'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for extracting an archive file. - * - * @return void - * - * @since 3.2 - */ - public function extractArchive() - { - // Check for request forgeries - $this->checkToken(); - - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if ($model->extractArchive($file)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXTRACT_SUCCESS')); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file; - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXTRACT_FAIL'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file; - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Fetch and report updates in \JSON format, for AJAX requests - * - * @return void - * - * @since 4.0.0 - */ - public function ajax() - { - $app = $this->app; - - if (!Session::checkToken('get')) - { - $app->setHeader('status', 403, true); - $app->sendHeaders(); - echo Text::_('JINVALID_TOKEN_NOTICE'); - $app->close(); - } - - // Checks status of installer override plugin. - if (!PluginHelper::isEnabled('installer', 'override')) - { - $error = array('installerOverride' => 'disabled'); - - echo json_encode($error); - - $app->close(); - } - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - $result = $model->getUpdatedList(true, true); - - echo json_encode($result); - - $app->close(); - } - - - /** - * Method for creating a child template. - * - * @return boolean true on success, false otherwise - * - * @since 4.1.0 - */ - public function child() - { - // Check for request forgeries - $this->checkToken(); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return false; - } - - $this->input->set('installtype', 'folder'); - $newNameRaw = $this->input->get('new_name', null, 'string'); - - // Only accept letters, numbers and underscore for template name - $newName = preg_replace('/[^a-zA-Z0-9_]/', '', $newNameRaw); - $templateID = (int) $this->input->getInt('id', 0); - $file = (string) $this->input->get('file', '', 'cmd'); - $extraStyles = (array) $this->input->get('style_ids', [], 'array'); - - $this->setRedirect('index.php?option=com_templates&view=template&id=' . $templateID . '&file=' . $file); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel('Template', 'Administrator'); - $model->setState('new_name', $newName); - $model->setState('tmp_prefix', uniqid('template_child_')); - $model->setState('to_path', $this->app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); - - // Process only if we have a new name entered - if (!strlen($newName)) { - $this->setMessage(Text::sprintf('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); - - return false; - } - - // Process only if user is allowed to create child template - if (!$this->app->getIdentity()->authorise('core.create', 'com_templates')) { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_CREATE_NOT_PERMITTED'), 'error'); - - return false; - } - - // Check that new name is valid - if (($newNameRaw !== null) && ($newName !== $newNameRaw)) { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); - - return false; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 1.6 + * @see BaseController + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('apply', 'save'); + $this->registerTask('unpublish', 'publish'); + $this->registerTask('publish', 'publish'); + $this->registerTask('deleteOverrideHistory', 'publish'); + } + + /** + * Method for closing the template. + * + * @return void + * + * @since 3.2 + */ + public function cancel() + { + $this->setRedirect(Route::_('index.php?option=com_templates&view=templates', false)); + } + + /** + * Method for closing a file. + * + * @return void + * + * @since 3.2 + */ + public function close() + { + $file = base64_encode('home'); + $id = (int) $this->input->get('id', 0, 'int'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . + $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + + /** + * Marked as Checked/Unchecked of override history. + * + * @return void + * + * @since 4.0.0 + */ + public function publish() + { + // Check for request forgeries. + $this->checkToken(); + + $file = $this->input->get('file'); + $id = $this->input->get('id'); + + $ids = (array) $this->input->get('cid', array(), 'string'); + $values = array('publish' => 1, 'unpublish' => 0, 'deleteOverrideHistory' => -3); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + + if (empty($ids)) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_NO_FILE_SELECTED'), 'warning'); + } else { + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + // Change the state of the records. + if (!$model->publish($ids, $value, $id)) { + $this->setMessage(implode('
    ', $model->getErrors()), 'warning'); + } else { + if ($value === 1) { + $ntext = 'COM_TEMPLATES_N_OVERRIDE_CHECKED'; + } elseif ($value === 0) { + $ntext = 'COM_TEMPLATES_N_OVERRIDE_UNCHECKED'; + } elseif ($value === -3) { + $ntext = 'COM_TEMPLATES_N_OVERRIDE_DELETED'; + } + + $this->setMessage(Text::plural($ntext, count($ids))); + } + } + + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . + $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + + /** + * Method for copying the template. + * + * @return boolean true on success, false otherwise + * + * @since 3.2 + */ + public function copy() + { + // Check for request forgeries + $this->checkToken(); + + $app = $this->app; + $this->input->set('installtype', 'folder'); + $newNameRaw = $this->input->get('new_name', null, 'string'); + // Only accept letters, numbers and underscore for template name + $newName = preg_replace('/[^a-zA-Z0-9_]/', '', $newNameRaw); + $templateID = (int) $this->input->getInt('id', 0); + $file = (string) $this->input->get('file', '', 'cmd'); + + // Access check. + if (!$this->allowEdit()) { + $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return false; + } + + $this->setRedirect('index.php?option=com_templates&view=template&id=' . $templateID . '&file=' . $file); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel('Template', 'Administrator'); + $model->setState('new_name', $newName); + $model->setState('tmp_prefix', uniqid('template_copy_')); + $model->setState('to_path', $app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); + + // Process only if we have a new name entered + if (strlen($newName) > 0) { + if (!$this->app->getIdentity()->authorise('core.create', 'com_templates')) { + // User is not authorised to delete + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_CREATE_NOT_PERMITTED'), 'error'); + + return false; + } + + // Check that new name is valid + if (($newNameRaw !== null) && ($newName !== $newNameRaw)) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); + + return false; + } + + // Check that new name doesn't already exist + if (!$model->checkNewName()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_DUPLICATE_TEMPLATE_NAME'), 'error'); + + return false; + } + + // Check that from name does exist and get the folder name + $fromName = $model->getFromName(); + + if (!$fromName) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); + + return false; + } + + // Call model's copy method + if (!$model->copy()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_COPY'), 'error'); + + return false; + } + + // Call installation model + $this->input->set('install_directory', $app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); + + /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $installModel */ + $installModel = $this->app->bootComponent('com_installer') + ->getMVCFactory()->createModel('Install', 'Administrator'); + $this->app->getLanguage()->load('com_installer'); + + if (!$installModel->install()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_INSTALL'), 'error'); + + return false; + } + + $this->setMessage(Text::sprintf('COM_TEMPLATES_COPY_SUCCESS', $newName)); + $model->cleanup(); + + return true; + } + + $this->setMessage(Text::sprintf('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); + + return false; + } + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional (note, the empty array is atypical compared to other models). + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 3.2 + */ + public function getModel($name = 'Template', $prefix = 'Administrator', $config = array()) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to check if you can add a new record. + * + * @return boolean + * + * @since 3.2 + */ + protected function allowEdit() + { + return $this->app->getIdentity()->authorise('core.admin'); + } + + /** + * Saves a template source file. + * + * @return void + * + * @since 3.2 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + $data = $this->input->post->get('jform', array(), 'array'); + $task = $this->getTask(); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $fileName = (string) $this->input->getCmd('file', ''); + $explodeArray = explode(':', str_replace('//', '/', base64_decode($fileName))); + + // Access check. + if (!$this->allowEdit()) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + // Match the stored id's with the submitted. + if (empty($data['extension_id']) || empty($data['filename'])) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); + + return; + } elseif ($data['extension_id'] != $model->getState('extension.id')) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); + + return; + } elseif (str_ends_with(end($explodeArray), Path::clean($data['filename'], '/'))) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); + + return; + } + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + $this->setMessage($model->getError(), 'error'); + + return; + } + + $data = $model->validate($form, $data); + + // Check for validation errors. + if ($data === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Redirect back to the edit screen. + $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + + return; + } + + // Attempt to save the data. + if (!$model->save($data)) { + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED', $model->getError()), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + + return; + } + + $this->setMessage(Text::_('COM_TEMPLATES_FILE_SAVE_SUCCESS')); + + // Redirect the user based on the chosen task. + switch ($task) { + case 'apply': + // Redirect back to the edit screen. + $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + break; + + default: + // Redirect to the list screen. + $file = base64_encode('home'); + $id = (int) $this->input->get('id', 0, 'int'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + break; + } + } + + /** + * Method for creating override. + * + * @return void + * + * @since 3.2 + */ + public function overrides() + { + // Check for request forgeries. + $this->checkToken('get'); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $file = (string) $this->input->getCmd('file', ''); + $override = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('folder', '')), 'path'); + $id = (int) $this->input->get('id', 0, 'int'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + $model->createOverride($override); + + // Redirect back to the edit screen. + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + + /** + * Method for deleting a file. + * + * @return void + * + * @since 3.2 + */ + public function delete() + { + // Check for request forgeries + $this->checkToken(); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (base64_decode(urldecode($file)) == '/index.php') { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INDEX_DELETE'), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif (base64_decode(urldecode($file)) == '/joomla.asset.json') { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_ASSET_FILE_DELETE'), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif ($model->deleteFile($file)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_DELETE_SUCCESS')); + $file = base64_encode('home'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_DELETE'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for creating a new file. + * + * @return void + * + * @since 3.2 + */ + public function createFile() + { + // Check for request forgeries + $this->checkToken(); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->get('file', '', 'cmd'); + $name = (string) $this->input->get('name', '', 'cmd'); + $location = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); + $type = (string) $this->input->get('type', '', 'cmd'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if ($type == 'null') { + $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_TYPE'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $name)) { + $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif ($model->createFile($name, $type, $location)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_CREATE_SUCCESS')); + $file = urlencode(base64_encode($location . '/' . $name . '.' . $type)); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_CREATE'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for uploading a file. + * + * @return void + * + * @since 3.2 + */ + public function uploadFile() + { + // Check for request forgeries + $this->checkToken(); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $upload = $this->input->files->get('files'); + $location = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if ($return = $model->uploadFile($upload, $location)) { + $this->setMessage(Text::sprintf('COM_TEMPLATES_FILE_UPLOAD_SUCCESS', $upload['name'])); + $redirect = base64_encode($return); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $redirect . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_UPLOAD'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for creating a new folder. + * + * @return void + * + * @since 3.2 + */ + public function createFolder() + { + // Check for request forgeries + $this->checkToken(); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $name = $this->input->get('name'); + $location = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (!preg_match('/^[a-zA-Z0-9-_.]+$/', $name)) { + $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FOLDER_NAME'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif ($model->createFolder($name, $location)) { + $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_CREATE_SUCCESS')); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FOLDER_CREATE'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for deleting a folder. + * + * @return void + * + * @since 3.2 + */ + public function deleteFolder() + { + // Check for request forgeries + $this->checkToken(); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $isMedia = (int) $this->input->get('isMedia', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $location = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (empty($location)) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_ROOT_DELETE'), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } elseif ($model->deleteFolder($location)) { + $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_SUCCESS')); + + if (stristr(base64_decode($file), $location) != false) { + $file = base64_encode('home'); + } + + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_ERROR'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for renaming a file. + * + * @return void + * + * @since 3.2 + */ + public function renameFile() + { + // Check for request forgeries + $this->checkToken(); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $isMedia = (int) $this->input->get('isMedia', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $newName = $this->input->get('new_name'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (base64_decode(urldecode($file)) == '/index.php') { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_RENAME_INDEX'), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } elseif (base64_decode(urldecode($file)) == '/joomla.asset.json') { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_RENAME_ASSET_FILE'), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $newName)) { + $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } elseif ($rename = $model->renameFile($file, $newName)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_RENAME_SUCCESS')); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $rename . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_RENAME'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for cropping an image. + * + * @return void + * + * @since 3.2 + */ + public function cropImage() + { + // Check for request forgeries + $this->checkToken(); + + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->get('file', '', 'cmd'); + $x = $this->input->get('x'); + $y = $this->input->get('y'); + $w = $this->input->get('w'); + $h = $this->input->get('h'); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (empty($w) && empty($h) && empty($x) && empty($y)) { + $this->setMessage(Text::_('COM_TEMPLATES_CROP_AREA_ERROR'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif ($model->cropImage($file, $w, $h, $x, $y)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_CROP_SUCCESS')); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_CROP_ERROR'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for resizing an image. + * + * @return void + * + * @since 3.2 + */ + public function resizeImage() + { + // Check for request forgeries + $this->checkToken(); + + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $width = $this->input->get('width'); + $height = $this->input->get('height'); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if ($model->resizeImage($file, $width, $height)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_RESIZE_SUCCESS')); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_RESIZE_ERROR'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for copying a file. + * + * @return void + * + * @since 3.2 + */ + public function copyFile() + { + // Check for request forgeries + $this->checkToken(); + + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $newName = $this->input->get('new_name'); + $location = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (!preg_match('/^[a-zA-Z0-9-_]+$/', $newName)) { + $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif ($model->copyFile($newName, $location, $file)) { + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_COPY_FAIL'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for extracting an archive file. + * + * @return void + * + * @since 3.2 + */ + public function extractArchive() + { + // Check for request forgeries + $this->checkToken(); + + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if ($model->extractArchive($file)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXTRACT_SUCCESS')); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file; + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXTRACT_FAIL'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file; + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Fetch and report updates in \JSON format, for AJAX requests + * + * @return void + * + * @since 4.0.0 + */ + public function ajax() + { + $app = $this->app; + + if (!Session::checkToken('get')) { + $app->setHeader('status', 403, true); + $app->sendHeaders(); + echo Text::_('JINVALID_TOKEN_NOTICE'); + $app->close(); + } + + // Checks status of installer override plugin. + if (!PluginHelper::isEnabled('installer', 'override')) { + $error = array('installerOverride' => 'disabled'); + + echo json_encode($error); + + $app->close(); + } + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + $result = $model->getUpdatedList(true, true); + + echo json_encode($result); + + $app->close(); + } + + + /** + * Method for creating a child template. + * + * @return boolean true on success, false otherwise + * + * @since 4.1.0 + */ + public function child() + { + // Check for request forgeries + $this->checkToken(); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return false; + } + + $this->input->set('installtype', 'folder'); + $newNameRaw = $this->input->get('new_name', null, 'string'); + + // Only accept letters, numbers and underscore for template name + $newName = preg_replace('/[^a-zA-Z0-9_]/', '', $newNameRaw); + $templateID = (int) $this->input->getInt('id', 0); + $file = (string) $this->input->get('file', '', 'cmd'); + $extraStyles = (array) $this->input->get('style_ids', [], 'array'); + + $this->setRedirect('index.php?option=com_templates&view=template&id=' . $templateID . '&file=' . $file); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel('Template', 'Administrator'); + $model->setState('new_name', $newName); + $model->setState('tmp_prefix', uniqid('template_child_')); + $model->setState('to_path', $this->app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); + + // Process only if we have a new name entered + if (!strlen($newName)) { + $this->setMessage(Text::sprintf('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); + + return false; + } + + // Process only if user is allowed to create child template + if (!$this->app->getIdentity()->authorise('core.create', 'com_templates')) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_CREATE_NOT_PERMITTED'), 'error'); + + return false; + } + + // Check that new name is valid + if (($newNameRaw !== null) && ($newName !== $newNameRaw)) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); + + return false; + } - // Check that new name doesn't already exist - if (!$model->checkNewName()) { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_DUPLICATE_TEMPLATE_NAME'), 'error'); + // Check that new name doesn't already exist + if (!$model->checkNewName()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_DUPLICATE_TEMPLATE_NAME'), 'error'); - return false; - } + return false; + } - // Check that from name does exist and get the folder name - $fromName = $model->getFromName(); + // Check that from name does exist and get the folder name + $fromName = $model->getFromName(); - if (!$fromName) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); + if (!$fromName) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); - return false; - } + return false; + } - // Call model's copy method - if (!$model->child()) { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_COPY'), 'error'); + // Call model's copy method + if (!$model->child()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_COPY'), 'error'); - return false; - } + return false; + } - // Call installation model - $this->input->set('install_directory', $this->app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); + // Call installation model + $this->input->set('install_directory', $this->app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); - /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $installModel */ - $installModel = $this->app->bootComponent('com_installer') - ->getMVCFactory()->createModel('Install', 'Administrator'); - $this->app->getLanguage()->load('com_installer'); + /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $installModel */ + $installModel = $this->app->bootComponent('com_installer') + ->getMVCFactory()->createModel('Install', 'Administrator'); + $this->app->getLanguage()->load('com_installer'); - if (!$installModel->install()) { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_INSTALL'), 'error'); + if (!$installModel->install()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_INSTALL'), 'error'); - return false; - } + return false; + } - $this->setMessage(Text::sprintf('COM_TEMPLATES_CHILD_SUCCESS', $newName)); - $model->cleanup(); + $this->setMessage(Text::sprintf('COM_TEMPLATES_CHILD_SUCCESS', $newName)); + $model->cleanup(); - if (\count($extraStyles) > 0) - { - $model->setState('stylesToCopy', $extraStyles); - $model->copyStyles(); - } + if (\count($extraStyles) > 0) { + $model->setState('stylesToCopy', $extraStyles); + $model->copyStyles(); + } - return true; - } + return true; + } } diff --git a/administrator/components/com_templates/src/Extension/TemplatesComponent.php b/administrator/components/com_templates/src/Extension/TemplatesComponent.php index e0736517413f1..db2c46a27360e 100644 --- a/administrator/components/com_templates/src/Extension/TemplatesComponent.php +++ b/administrator/components/com_templates/src/Extension/TemplatesComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('templates', new Templates); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('templates', new Templates()); + } } diff --git a/administrator/components/com_templates/src/Field/TemplatelocationField.php b/administrator/components/com_templates/src/Field/TemplatelocationField.php index cf4793eb5506c..f5eb0e8b5d16f 100644 --- a/administrator/components/com_templates/src/Field/TemplatelocationField.php +++ b/administrator/components/com_templates/src/Field/TemplatelocationField.php @@ -1,4 +1,5 @@ getUserStateFromRequest('com_templates.styles.client_id', 'client_id', '0', 'string'); - - // Get the templates for the selected client_id. - $options = TemplatesHelper::getTemplateOptions($clientId); - - // Merge into the parent options. - return array_merge(parent::getOptions(), $options); - } + /** + * The form field type. + * + * @var string + * @since 3.5 + */ + protected $type = 'TemplateName'; + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 1.6 + */ + public function getOptions() + { + // Get the client_id filter from the user state. + $clientId = Factory::getApplication()->getUserStateFromRequest('com_templates.styles.client_id', 'client_id', '0', 'string'); + + // Get the templates for the selected client_id. + $options = TemplatesHelper::getTemplateOptions($clientId); + + // Merge into the parent options. + return array_merge(parent::getOptions(), $options); + } } diff --git a/administrator/components/com_templates/src/Helper/TemplateHelper.php b/administrator/components/com_templates/src/Helper/TemplateHelper.php index dbbfd061ff811..c436321f0a1e9 100644 --- a/administrator/components/com_templates/src/Helper/TemplateHelper.php +++ b/administrator/components/com_templates/src/Helper/TemplateHelper.php @@ -1,4 +1,5 @@ enqueueMessage(Text::_('COM_TEMPLATES_ERROR_UPLOAD_INPUT'), 'error'); - - return false; - } - - // Media file names should never have executable extensions buried in them. - $executable = array( - 'exe', 'phtml','java', 'perl', 'py', 'asp','dll', 'go', 'jar', - 'ade', 'adp', 'bat', 'chm', 'cmd', 'com', 'cpl', 'hta', 'ins', 'isp', - 'jse', 'lib', 'mde', 'msc', 'msp', 'mst', 'pif', 'scr', 'sct', 'shb', - 'sys', 'vb', 'vbe', 'vbs', 'vxd', 'wsc', 'wsf', 'wsh' - ); - $explodedFileName = explode('.', $file['name']); - - if (count($explodedFileName) > 2) - { - foreach ($executable as $extensionName) - { - if (in_array($extensionName, $explodedFileName)) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXECUTABLE'), 'error'); - - return false; - } - } - } - - if ($file['name'] !== File::makeSafe($file['name']) || preg_match('/\s/', File::makeSafe($file['name']))) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILENAME'), 'error'); - - return false; - } - - $format = strtolower(File::getExt($file['name'])); - - $imageTypes = explode(',', $params->get('image_formats')); - $sourceTypes = explode(',', $params->get('source_formats')); - $fontTypes = explode(',', $params->get('font_formats')); - $archiveTypes = explode(',', $params->get('compressed_formats')); - - $allowable = array_merge($imageTypes, $sourceTypes, $fontTypes, $archiveTypes); - - if ($format == '' || $format == false || (!in_array($format, $allowable))) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILETYPE'), 'error'); - - return false; - } - - if (in_array($format, $archiveTypes)) - { - $zip = new \ZipArchive; - - if ($zip->open($file['tmp_name']) === true) - { - for ($i = 0; $i < $zip->numFiles; $i++) - { - $entry = $zip->getNameIndex($i); - $endString = substr($entry, -1); - - if ($endString != DIRECTORY_SEPARATOR) - { - $explodeArray = explode('.', $entry); - $ext = end($explodeArray); - - if (!in_array($ext, $allowable)) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_UNSUPPORTED_ARCHIVE'), 'error'); - - return false; - } - } - } - } - else - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); - - return false; - } - } - - // Max upload size set to 2 MB for Template Manager - $maxSize = (int) ($params->get('upload_limit') * 1024 * 1024); - - if ($maxSize > 0 && (int) $file['size'] > $maxSize) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILETOOLARGE'), 'error'); - - return false; - } - - $xss_check = file_get_contents($file['tmp_name'], false, null, -1, 256); - $html_tags = array( - 'abbr', 'acronym', 'address', 'applet', 'area', 'audioscope', 'base', 'basefont', 'bdo', 'bgsound', 'big', 'blackface', 'blink', 'blockquote', - 'body', 'bq', 'br', 'button', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'comment', 'custom', 'dd', 'del', 'dfn', 'dir', 'div', - 'dl', 'dt', 'em', 'embed', 'fieldset', 'fn', 'font', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', - 'iframe', 'ilayer', 'img', 'input', 'ins', 'isindex', 'keygen', 'kbd', 'label', 'layer', 'legend', 'li', 'limittext', 'link', 'listing', - 'map', 'marquee', 'menu', 'meta', 'multicol', 'nobr', 'noembed', 'noframes', 'noscript', 'nosmartquotes', 'object', 'ol', 'optgroup', 'option', - 'param', 'plaintext', 'pre', 'rt', 'ruby', 's', 'samp', 'script', 'select', 'server', 'shadow', 'sidebar', 'small', 'spacer', 'span', 'strike', - 'strong', 'style', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr', 'tt', 'ul', 'var', 'wbr', 'xml', - 'xmp', '!DOCTYPE', '!--' - ); - - foreach ($html_tags as $tag) - { - // A tag is '' - if (stristr($xss_check, '<' . $tag . ' ') || stristr($xss_check, '<' . $tag . '>')) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNIEXSS'), 'error'); - - return false; - } - } - - return true; - } + /** + * Checks if the file is an image + * + * @param string $fileName The filename + * + * @return boolean + * + * @since 3.2 + */ + public static function getTypeIcon($fileName) + { + // Get file extension + return strtolower(substr($fileName, strrpos($fileName, '.') + 1)); + } + + /** + * Checks if the file can be uploaded + * + * @param array $file File information + * @param string $err An error message to be returned + * + * @return boolean + * + * @since 3.2 + */ + public static function canUpload($file, $err = '') + { + $params = ComponentHelper::getParams('com_templates'); + + if (empty($file['name'])) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_UPLOAD_INPUT'), 'error'); + + return false; + } + + // Media file names should never have executable extensions buried in them. + $executable = array( + 'exe', 'phtml','java', 'perl', 'py', 'asp','dll', 'go', 'jar', + 'ade', 'adp', 'bat', 'chm', 'cmd', 'com', 'cpl', 'hta', 'ins', 'isp', + 'jse', 'lib', 'mde', 'msc', 'msp', 'mst', 'pif', 'scr', 'sct', 'shb', + 'sys', 'vb', 'vbe', 'vbs', 'vxd', 'wsc', 'wsf', 'wsh' + ); + $explodedFileName = explode('.', $file['name']); + + if (count($explodedFileName) > 2) { + foreach ($executable as $extensionName) { + if (in_array($extensionName, $explodedFileName)) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXECUTABLE'), 'error'); + + return false; + } + } + } + + if ($file['name'] !== File::makeSafe($file['name']) || preg_match('/\s/', File::makeSafe($file['name']))) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILENAME'), 'error'); + + return false; + } + + $format = strtolower(File::getExt($file['name'])); + + $imageTypes = explode(',', $params->get('image_formats')); + $sourceTypes = explode(',', $params->get('source_formats')); + $fontTypes = explode(',', $params->get('font_formats')); + $archiveTypes = explode(',', $params->get('compressed_formats')); + + $allowable = array_merge($imageTypes, $sourceTypes, $fontTypes, $archiveTypes); + + if ($format == '' || $format == false || (!in_array($format, $allowable))) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILETYPE'), 'error'); + + return false; + } + + if (in_array($format, $archiveTypes)) { + $zip = new \ZipArchive(); + + if ($zip->open($file['tmp_name']) === true) { + for ($i = 0; $i < $zip->numFiles; $i++) { + $entry = $zip->getNameIndex($i); + $endString = substr($entry, -1); + + if ($endString != DIRECTORY_SEPARATOR) { + $explodeArray = explode('.', $entry); + $ext = end($explodeArray); + + if (!in_array($ext, $allowable)) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_UNSUPPORTED_ARCHIVE'), 'error'); + + return false; + } + } + } + } else { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); + + return false; + } + } + + // Max upload size set to 2 MB for Template Manager + $maxSize = (int) ($params->get('upload_limit') * 1024 * 1024); + + if ($maxSize > 0 && (int) $file['size'] > $maxSize) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILETOOLARGE'), 'error'); + + return false; + } + + $xss_check = file_get_contents($file['tmp_name'], false, null, -1, 256); + $html_tags = array( + 'abbr', 'acronym', 'address', 'applet', 'area', 'audioscope', 'base', 'basefont', 'bdo', 'bgsound', 'big', 'blackface', 'blink', 'blockquote', + 'body', 'bq', 'br', 'button', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'comment', 'custom', 'dd', 'del', 'dfn', 'dir', 'div', + 'dl', 'dt', 'em', 'embed', 'fieldset', 'fn', 'font', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', + 'iframe', 'ilayer', 'img', 'input', 'ins', 'isindex', 'keygen', 'kbd', 'label', 'layer', 'legend', 'li', 'limittext', 'link', 'listing', + 'map', 'marquee', 'menu', 'meta', 'multicol', 'nobr', 'noembed', 'noframes', 'noscript', 'nosmartquotes', 'object', 'ol', 'optgroup', 'option', + 'param', 'plaintext', 'pre', 'rt', 'ruby', 's', 'samp', 'script', 'select', 'server', 'shadow', 'sidebar', 'small', 'spacer', 'span', 'strike', + 'strong', 'style', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr', 'tt', 'ul', 'var', 'wbr', 'xml', + 'xmp', '!DOCTYPE', '!--' + ); + + foreach ($html_tags as $tag) { + // A tag is '' + if (stristr($xss_check, '<' . $tag . ' ') || stristr($xss_check, '<' . $tag . '>')) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNIEXSS'), 'error'); + + return false; + } + } + + return true; + } } diff --git a/administrator/components/com_templates/src/Helper/TemplatesHelper.php b/administrator/components/com_templates/src/Helper/TemplatesHelper.php index 53a50bdb8d0a1..5c97f240437b1 100644 --- a/administrator/components/com_templates/src/Helper/TemplatesHelper.php +++ b/administrator/components/com_templates/src/Helper/TemplatesHelper.php @@ -1,4 +1,5 @@ getQuery(true); - - $query->select($db->quoteName('element', 'value')) - ->select($db->quoteName('name', 'text')) - ->select($db->quoteName('extension_id', 'e_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('template')) - ->where($db->quoteName('enabled') . ' = 1') - ->order($db->quoteName('client_id') . ' ASC') - ->order($db->quoteName('name') . ' ASC'); - - if ($clientId != '*') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - } - - $db->setQuery($query); - $options = $db->loadObjectList(); - - return $options; - } - - /** - * @param string $templateBaseDir - * @param string $templateDir - * - * @return boolean|CMSObject - */ - public static function parseXMLTemplateFile($templateBaseDir, $templateDir) - { - $data = new CMSObject; - - // Check of the xml file exists - $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); - - if (is_file($filePath)) - { - $xml = Installer::parseXMLInstallFile($filePath); - - if ($xml['type'] != 'template') - { - return false; - } - - foreach ($xml as $key => $value) - { - $data->set($key, $value); - } - } - - return $data; - } - - /** - * @param integer $clientId - * @param string $templateDir - * - * @return boolean|array - * - * @since 3.0 - */ - public static function getPositions($clientId, $templateDir) - { - $positions = array(); - - $templateBaseDir = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; - $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); - - if (is_file($filePath)) - { - // Read the file to see if it's a valid component XML file - $xml = simplexml_load_file($filePath); - - if (!$xml) - { - return false; - } - - // Check for a valid XML root tag. - - // Extensions use 'extension' as the root tag. Languages use 'metafile' instead - - if ($xml->getName() != 'extension' && $xml->getName() != 'metafile') - { - unset($xml); - - return false; - } - - $positions = (array) $xml->positions; - - if (isset($positions['position'])) - { - $positions = (array) $positions['position']; - } - else - { - $positions = array(); - } - } - - return $positions; - } + /** + * Get a list of filter options for the application clients. + * + * @return array An array of HtmlOption elements. + */ + public static function getClientOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JSITE')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JADMINISTRATOR')); + + return $options; + } + + /** + * Get a list of filter options for the templates with styles. + * + * @param mixed $clientId The CMS client id (0:site | 1:administrator) or '*' for all. + * + * @return array + */ + public static function getTemplateOptions($clientId = '*') + { + // Build the filter options. + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->select($db->quoteName('element', 'value')) + ->select($db->quoteName('name', 'text')) + ->select($db->quoteName('extension_id', 'e_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('template')) + ->where($db->quoteName('enabled') . ' = 1') + ->order($db->quoteName('client_id') . ' ASC') + ->order($db->quoteName('name') . ' ASC'); + + if ($clientId != '*') { + $clientId = (int) $clientId; + $query->where($db->quoteName('client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + } + + $db->setQuery($query); + $options = $db->loadObjectList(); + + return $options; + } + + /** + * @param string $templateBaseDir + * @param string $templateDir + * + * @return boolean|CMSObject + */ + public static function parseXMLTemplateFile($templateBaseDir, $templateDir) + { + $data = new CMSObject(); + + // Check of the xml file exists + $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); + + if (is_file($filePath)) { + $xml = Installer::parseXMLInstallFile($filePath); + + if ($xml['type'] != 'template') { + return false; + } + + foreach ($xml as $key => $value) { + $data->set($key, $value); + } + } + + return $data; + } + + /** + * @param integer $clientId + * @param string $templateDir + * + * @return boolean|array + * + * @since 3.0 + */ + public static function getPositions($clientId, $templateDir) + { + $positions = array(); + + $templateBaseDir = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; + $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); + + if (is_file($filePath)) { + // Read the file to see if it's a valid component XML file + $xml = simplexml_load_file($filePath); + + if (!$xml) { + return false; + } + + // Check for a valid XML root tag. + + // Extensions use 'extension' as the root tag. Languages use 'metafile' instead + + if ($xml->getName() != 'extension' && $xml->getName() != 'metafile') { + unset($xml); + + return false; + } + + $positions = (array) $xml->positions; + + if (isset($positions['position'])) { + $positions = (array) $positions['position']; + } else { + $positions = array(); + } + } + + return $positions; + } } diff --git a/administrator/components/com_templates/src/Model/StyleModel.php b/administrator/components/com_templates/src/Model/StyleModel.php index 427101683fa8a..fdd016186ecf8 100644 --- a/administrator/components/com_templates/src/Model/StyleModel.php +++ b/administrator/components/com_templates/src/Model/StyleModel.php @@ -1,4 +1,5 @@ 'onExtensionBeforeDelete', - 'event_after_delete' => 'onExtensionAfterDelete', - 'event_before_save' => 'onExtensionBeforeSave', - 'event_after_save' => 'onExtensionAfterSave', - 'events_map' => array('delete' => 'extension', 'save' => 'extension') - ), $config - ); - - parent::__construct($config, $factory); - } - - /** - * Method to auto-populate the model state. - * - * @note Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState('style.id', $pk); - - // Load the parameters. - $params = ComponentHelper::getParams('com_templates'); - $this->setState('params', $params); - } - - /** - * Method to delete rows. - * - * @param array &$pks An array of item ids. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function delete(&$pks) - { - $pks = (array) $pks; - $user = Factory::getUser(); - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - - PluginHelper::importPlugin($this->events_map['delete']); - - // Iterate the items to delete each one. - foreach ($pks as $pk) - { - if ($table->load($pk)) - { - // Access checks. - if (!$user->authorise('core.delete', 'com_templates')) - { - throw new \Exception(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED')); - } - - // You should not delete a default style - if ($table->home != '0') - { - Factory::getApplication()->enqueueMessage(Text::_('COM_TEMPLATES_STYLE_CANNOT_DELETE_DEFAULT_STYLE'), 'error'); - - return false; - } - - // Trigger the before delete event. - $result = Factory::getApplication()->triggerEvent($this->event_before_delete, array($context, $table)); - - if (in_array(false, $result, true) || !$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after delete event. - Factory::getApplication()->triggerEvent($this->event_after_delete, array($context, $table)); - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - // Clean cache - $this->cleanCache(); - - return true; - } - - /** - * Method to duplicate styles. - * - * @param array &$pks An array of primary key IDs. - * - * @return boolean True if successful. - * - * @throws \Exception - */ - public function duplicate(&$pks) - { - $user = Factory::getUser(); - - // Access checks. - if (!$user->authorise('core.create', 'com_templates')) - { - throw new \Exception(Text::_('JERROR_CORE_CREATE_NOT_PERMITTED')); - } - - $context = $this->option . '.' . $this->name; - - // Include the plugins for the save events. - PluginHelper::importPlugin($this->events_map['save']); - - $table = $this->getTable(); - - foreach ($pks as $pk) - { - if ($table->load($pk, true)) - { - // Reset the id to create a new record. - $table->id = 0; - - // Reset the home (don't want dupes of that field). - $table->home = 0; - - // Alter the title. - $m = null; - $table->title = $this->generateNewTitle(null, null, $table->title); - - if (!$table->check()) - { - throw new \Exception($table->getError()); - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, true)); - - if (in_array(false, $result, true) || !$table->store()) - { - throw new \Exception($table->getError()); - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, true)); - } - else - { - throw new \Exception($table->getError()); - } - } - - // Clean cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the title. - * - * @param integer $categoryId The id of the category. - * @param string $alias The alias. - * @param string $title The title. - * - * @return string New title. - * - * @since 1.7.1 - */ - protected function generateNewTitle($categoryId, $alias, $title) - { - // Alter the title - $table = $this->getTable(); - - while ($table->load(array('title' => $title))) - { - $title = StringHelper::increment($title); - } - - return $title; - } - - /** - * Method to get the record form. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // The folder and element vars are passed when saving the form. - if (empty($data)) - { - $item = $this->getItem(); - $clientId = $item->client_id; - $template = $item->template; - } - else - { - $clientId = ArrayHelper::getValue($data, 'client_id'); - $template = ArrayHelper::getValue($data, 'template'); - } - - // Add the default fields directory - $baseFolder = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; - Form::addFieldPath($baseFolder . '/templates/' . $template . '/field'); - - // These variables are used to add data from the plugin XML files. - $this->setState('item.client_id', $clientId); - $this->setState('item.template', $template); - - // Get the form. - $form = $this->loadForm('com_templates.style', 'style', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('home', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('home', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_templates.edit.style.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_templates.style', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - */ - public function getItem($pk = null) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('style.id'); - - if (!isset($this->_cache[$pk])) - { - // Get a row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load($pk); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - - // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. - $properties = $table->getProperties(1); - $this->_cache[$pk] = ArrayHelper::toObject($properties, CMSObject::class); - - // Convert the params field to an array. - $registry = new Registry($table->params); - $this->_cache[$pk]->params = $registry->toArray(); - - // Get the template XML. - $client = ApplicationHelper::getClientInfo($table->client_id); - $path = Path::clean($client->path . '/templates/' . $table->template . '/templateDetails.xml'); - - if (file_exists($path)) - { - $this->_cache[$pk]->xml = simplexml_load_file($path); - } - else - { - $this->_cache[$pk]->xml = null; - } - } - - return $this->_cache[$pk]; - } - - /** - * Method to allow derived classes to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $clientId = $this->getState('item.client_id'); - $template = $this->getState('item.template'); - $lang = Factory::getLanguage(); - $client = ApplicationHelper::getClientInfo($clientId); - - if (!$form->loadFile('style_' . $client->name, true)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - $formFile = Path::clean($client->path . '/templates/' . $template . '/templateDetails.xml'); - - // Load the core and/or local language file(s). - $lang->load('tpl_' . $template, $client->path) - || (!empty($data->parent) && $lang->load('tpl_' . $data->parent, $client->path)) - || (!empty($data->parent) && $lang->load('tpl_' . $data->parent, $client->path . '/templates/' . $data->parent)) - || $lang->load('tpl_' . $template, $client->path . '/templates/' . $template); - - if (file_exists($formFile)) - { - // Get the template form. - if (!$form->loadFile($formFile, false, '//config')) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - - // Disable home field if it is default style - - if ((is_array($data) && array_key_exists('home', $data) && $data['home'] == '1') - || (is_object($data) && isset($data->home) && $data->home == '1')) - { - $form->setFieldAttribute('home', 'readonly', 'true'); - } - - if ($client->name === 'site' && !Multilanguage::isEnabled()) - { - $form->setFieldAttribute('home', 'type', 'radio'); - $form->setFieldAttribute('home', 'layout', 'joomla.form.field.radio.switcher'); - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Get the help data from the XML file if present. - $help = $xml->xpath('/extension/help'); - - if (!empty($help)) - { - $helpKey = trim((string) $help[0]['key']); - $helpURL = trim((string) $help[0]['url']); - - $this->helpKey = $helpKey ?: $this->helpKey; - $this->helpURL = $helpURL ?: $this->helpURL; - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - */ - public function save($data) - { - // Detect disabled extension - $extension = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\'); - - if ($extension->load(array('enabled' => 0, 'type' => 'template', 'element' => $data['template'], 'client_id' => $data['client_id']))) - { - $this->setError(Text::_('COM_TEMPLATES_ERROR_SAVE_DISABLED_TEMPLATE')); - - return false; - } - - $app = Factory::getApplication(); - $table = $this->getTable(); - $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('style.id'); - $isNew = true; - - // Include the extension plugins for the save events. - PluginHelper::importPlugin($this->events_map['save']); - - // Load the row if saving an existing record. - if ($pk > 0) - { - $table->load($pk); - $isNew = false; - } - - if ($app->input->get('task') == 'save2copy') - { - $data['title'] = $this->generateNewTitle(null, null, $data['title']); - $data['home'] = 0; - $data['assigned'] = ''; - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Prepare the row for saving - $this->prepareTable($table); - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array('com_templates.style', &$table, $isNew)); - - // Store the data. - if (in_array(false, $result, true) || !$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - $user = Factory::getUser(); - - if ($user->authorise('core.edit', 'com_menus') && $table->client_id == 0) - { - $n = 0; - $db = $this->getDatabase(); - $user = Factory::getUser(); - $tableId = (int) $table->id; - $userId = (int) $user->id; - - if (!empty($data['assigned']) && is_array($data['assigned'])) - { - $data['assigned'] = ArrayHelper::toInteger($data['assigned']); - - // Update the mapping for menu items that this style IS assigned to. - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('template_style_id') . ' = :newtsid') - ->whereIn($db->quoteName('id'), $data['assigned']) - ->where($db->quoteName('template_style_id') . ' != :tsid') - ->where('(' . $db->quoteName('checked_out') . ' IS NULL OR ' . $db->quoteName('checked_out') . ' = :userid)') - ->bind(':userid', $userId, ParameterType::INTEGER) - ->bind(':newtsid', $tableId, ParameterType::INTEGER) - ->bind(':tsid', $tableId, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - $n += $db->getAffectedRows(); - } - - // Remove style mappings for menu items this style is NOT assigned to. - // If unassigned then all existing maps will be removed. - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('template_style_id') . ' = 0'); - - if (!empty($data['assigned'])) - { - $query->whereNotIn($db->quoteName('id'), $data['assigned']); - } - - $query->where($db->quoteName('template_style_id') . ' = :templatestyleid') - ->where('(' . $db->quoteName('checked_out') . ' IS NULL OR ' . $db->quoteName('checked_out') . ' = :userid)') - ->bind(':userid', $userId, ParameterType::INTEGER) - ->bind(':templatestyleid', $tableId, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - $n += $db->getAffectedRows(); - - if ($n > 0) - { - $app->enqueueMessage(Text::plural('COM_TEMPLATES_MENU_CHANGED', $n)); - } - } - - // Clean the cache. - $this->cleanCache(); - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array('com_templates.style', &$table, $isNew)); - - $this->setState('style.id', $table->id); - - return true; - } - - /** - * Method to set a template style as home. - * - * @param integer $id The primary key ID for the style. - * - * @return boolean True if successful. - * - * @throws \Exception - */ - public function setHome($id = 0) - { - $user = Factory::getUser(); - $db = $this->getDatabase(); - - // Access checks. - if (!$user->authorise('core.edit.state', 'com_templates')) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); - } - - $style = $this->getTable(); - - if (!$style->load((int) $id)) - { - throw new \Exception(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND')); - } - - // Detect disabled extension - $extension = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\'); - - if ($extension->load(array('enabled' => 0, 'type' => 'template', 'element' => $style->template, 'client_id' => $style->client_id))) - { - throw new \Exception(Text::_('COM_TEMPLATES_ERROR_SAVE_DISABLED_TEMPLATE')); - } - - $clientId = (int) $style->client_id; - $id = (int) $id; - - // Reset the home fields for the client_id. - $query = $db->getQuery(true) - ->update($db->quoteName('#__template_styles')) - ->set($db->quoteName('home') . ' = ' . $db->quote('0')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->where($db->quoteName('home') . ' = ' . $db->quote('1')) - ->bind(':clientid', $clientId, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - // Set the new home style. - $query = $db->getQuery(true) - ->update($db->quoteName('#__template_styles')) - ->set($db->quoteName('home') . ' = ' . $db->quote('1')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - // Clean the cache. - $this->cleanCache(); - - return true; - } - - /** - * Method to unset a template style as default for a language. - * - * @param integer $id The primary key ID for the style. - * - * @return boolean True if successful. - * - * @throws \Exception - */ - public function unsetHome($id = 0) - { - $user = Factory::getUser(); - $db = $this->getDatabase(); - - // Access checks. - if (!$user->authorise('core.edit.state', 'com_templates')) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); - } - - $id = (int) $id; - - // Lookup the client_id. - $query = $db->getQuery(true) - ->select($db->quoteName(['client_id', 'home'])) - ->from($db->quoteName('#__template_styles')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $style = $db->loadObject(); - - if (!is_numeric($style->client_id)) - { - throw new \Exception(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND')); - } - elseif ($style->home == '1') - { - throw new \Exception(Text::_('COM_TEMPLATES_ERROR_CANNOT_UNSET_DEFAULT_STYLE')); - } - - // Set the new home style. - $query = $db->getQuery(true) - ->update($db->quoteName('#__template_styles')) - ->set($db->quoteName('home') . ' = ' . $db->quote('0')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - // Clean the cache. - $this->cleanCache(); - - return true; - } - - /** - * Get the necessary data to load an item help screen. - * - * @return object An object with key, url, and local properties for loading the item help screen. - * - * @since 1.6 - */ - public function getHelp() - { - return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); - } - - /** - * Returns the back end template for the given style. - * - * @param int $styleId The style id - * - * @return stdClass - * - * @since 4.2.0 - */ - public function getAdminTemplate(int $styleId): stdClass - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['s.template', 's.params', 's.inheritable', 's.parent'])) - ->from($db->quoteName('#__template_styles', 's')) - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.type') . ' = ' . $db->quote('template') - . ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('s.template') - . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('s.client_id') - ) - ->where( - [ - $db->quoteName('s.client_id') . ' = 1', - $db->quoteName('s.home') . ' = ' . $db->quote('1'), - ] - ); - - if ($styleId) - { - $query->extendWhere( - 'OR', - [ - $db->quoteName('s.client_id') . ' = 1', - $db->quoteName('s.id') . ' = :style', - $db->quoteName('e.enabled') . ' = 1', - ] - ) - ->bind(':style', $styleId, ParameterType::INTEGER); - } - - $query->order($db->quoteName('s.home')); - $db->setQuery($query); - - return $db->loadObject(); - } - - /** - * Returns the front end templates. - * - * @return array - * - * @since 4.2.0 - */ - public function getSiteTemplates(): array - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['id', 'home', 'template', 's.params', 'inheritable', 'parent'])) - ->from($db->quoteName('#__template_styles', 's')) - ->where( - [ - $db->quoteName('s.client_id') . ' = 0', - $db->quoteName('e.enabled') . ' = 1', - ] - ) - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.element') . ' = ' . $db->quoteName('s.template') - . ' AND ' . $db->quoteName('e.type') . ' = ' . $db->quote('template') - . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('s.client_id') - ); - - $db->setQuery($query); - - return $db->loadObjectList('id'); - } - - /** - * Custom clean cache method - * - * @param string $group The cache group - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_templates'); - parent::cleanCache('_system'); - } + /** + * The help screen key for the module. + * + * @var string + * @since 1.6 + */ + protected $helpKey = 'Templates:_Edit_Style'; + + /** + * The help screen base URL for the module. + * + * @var string + * @since 1.6 + */ + protected $helpURL; + + /** + * Item cache. + * + * @var array + * @since 1.6 + */ + private $_cache = array(); + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $config = array_merge( + array( + 'event_before_delete' => 'onExtensionBeforeDelete', + 'event_after_delete' => 'onExtensionAfterDelete', + 'event_before_save' => 'onExtensionBeforeSave', + 'event_after_save' => 'onExtensionAfterSave', + 'events_map' => array('delete' => 'extension', 'save' => 'extension') + ), + $config + ); + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * @note Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState('style.id', $pk); + + // Load the parameters. + $params = ComponentHelper::getParams('com_templates'); + $this->setState('params', $params); + } + + /** + * Method to delete rows. + * + * @param array &$pks An array of item ids. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function delete(&$pks) + { + $pks = (array) $pks; + $user = Factory::getUser(); + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + + PluginHelper::importPlugin($this->events_map['delete']); + + // Iterate the items to delete each one. + foreach ($pks as $pk) { + if ($table->load($pk)) { + // Access checks. + if (!$user->authorise('core.delete', 'com_templates')) { + throw new \Exception(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED')); + } + + // You should not delete a default style + if ($table->home != '0') { + Factory::getApplication()->enqueueMessage(Text::_('COM_TEMPLATES_STYLE_CANNOT_DELETE_DEFAULT_STYLE'), 'error'); + + return false; + } + + // Trigger the before delete event. + $result = Factory::getApplication()->triggerEvent($this->event_before_delete, array($context, $table)); + + if (in_array(false, $result, true) || !$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after delete event. + Factory::getApplication()->triggerEvent($this->event_after_delete, array($context, $table)); + } else { + $this->setError($table->getError()); + + return false; + } + } + + // Clean cache + $this->cleanCache(); + + return true; + } + + /** + * Method to duplicate styles. + * + * @param array &$pks An array of primary key IDs. + * + * @return boolean True if successful. + * + * @throws \Exception + */ + public function duplicate(&$pks) + { + $user = Factory::getUser(); + + // Access checks. + if (!$user->authorise('core.create', 'com_templates')) { + throw new \Exception(Text::_('JERROR_CORE_CREATE_NOT_PERMITTED')); + } + + $context = $this->option . '.' . $this->name; + + // Include the plugins for the save events. + PluginHelper::importPlugin($this->events_map['save']); + + $table = $this->getTable(); + + foreach ($pks as $pk) { + if ($table->load($pk, true)) { + // Reset the id to create a new record. + $table->id = 0; + + // Reset the home (don't want dupes of that field). + $table->home = 0; + + // Alter the title. + $m = null; + $table->title = $this->generateNewTitle(null, null, $table->title); + + if (!$table->check()) { + throw new \Exception($table->getError()); + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, true)); + + if (in_array(false, $result, true) || !$table->store()) { + throw new \Exception($table->getError()); + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, true)); + } else { + throw new \Exception($table->getError()); + } + } + + // Clean cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the title. + * + * @param integer $categoryId The id of the category. + * @param string $alias The alias. + * @param string $title The title. + * + * @return string New title. + * + * @since 1.7.1 + */ + protected function generateNewTitle($categoryId, $alias, $title) + { + // Alter the title + $table = $this->getTable(); + + while ($table->load(array('title' => $title))) { + $title = StringHelper::increment($title); + } + + return $title; + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // The folder and element vars are passed when saving the form. + if (empty($data)) { + $item = $this->getItem(); + $clientId = $item->client_id; + $template = $item->template; + } else { + $clientId = ArrayHelper::getValue($data, 'client_id'); + $template = ArrayHelper::getValue($data, 'template'); + } + + // Add the default fields directory + $baseFolder = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; + Form::addFieldPath($baseFolder . '/templates/' . $template . '/field'); + + // These variables are used to add data from the plugin XML files. + $this->setState('item.client_id', $clientId); + $this->setState('item.template', $template); + + // Get the form. + $form = $this->loadForm('com_templates.style', 'style', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('home', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('home', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_templates.edit.style.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_templates.style', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + */ + public function getItem($pk = null) + { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('style.id'); + + if (!isset($this->_cache[$pk])) { + // Get a row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load($pk); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + + // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. + $properties = $table->getProperties(1); + $this->_cache[$pk] = ArrayHelper::toObject($properties, CMSObject::class); + + // Convert the params field to an array. + $registry = new Registry($table->params); + $this->_cache[$pk]->params = $registry->toArray(); + + // Get the template XML. + $client = ApplicationHelper::getClientInfo($table->client_id); + $path = Path::clean($client->path . '/templates/' . $table->template . '/templateDetails.xml'); + + if (file_exists($path)) { + $this->_cache[$pk]->xml = simplexml_load_file($path); + } else { + $this->_cache[$pk]->xml = null; + } + } + + return $this->_cache[$pk]; + } + + /** + * Method to allow derived classes to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $clientId = $this->getState('item.client_id'); + $template = $this->getState('item.template'); + $lang = Factory::getLanguage(); + $client = ApplicationHelper::getClientInfo($clientId); + + if (!$form->loadFile('style_' . $client->name, true)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + $formFile = Path::clean($client->path . '/templates/' . $template . '/templateDetails.xml'); + + // Load the core and/or local language file(s). + $lang->load('tpl_' . $template, $client->path) + || (!empty($data->parent) && $lang->load('tpl_' . $data->parent, $client->path)) + || (!empty($data->parent) && $lang->load('tpl_' . $data->parent, $client->path . '/templates/' . $data->parent)) + || $lang->load('tpl_' . $template, $client->path . '/templates/' . $template); + + if (file_exists($formFile)) { + // Get the template form. + if (!$form->loadFile($formFile, false, '//config')) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + + // Disable home field if it is default style + + if ( + (is_array($data) && array_key_exists('home', $data) && $data['home'] == '1') + || (is_object($data) && isset($data->home) && $data->home == '1') + ) { + $form->setFieldAttribute('home', 'readonly', 'true'); + } + + if ($client->name === 'site' && !Multilanguage::isEnabled()) { + $form->setFieldAttribute('home', 'type', 'radio'); + $form->setFieldAttribute('home', 'layout', 'joomla.form.field.radio.switcher'); + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Get the help data from the XML file if present. + $help = $xml->xpath('/extension/help'); + + if (!empty($help)) { + $helpKey = trim((string) $help[0]['key']); + $helpURL = trim((string) $help[0]['url']); + + $this->helpKey = $helpKey ?: $this->helpKey; + $this->helpURL = $helpURL ?: $this->helpURL; + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + */ + public function save($data) + { + // Detect disabled extension + $extension = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\'); + + if ($extension->load(array('enabled' => 0, 'type' => 'template', 'element' => $data['template'], 'client_id' => $data['client_id']))) { + $this->setError(Text::_('COM_TEMPLATES_ERROR_SAVE_DISABLED_TEMPLATE')); + + return false; + } + + $app = Factory::getApplication(); + $table = $this->getTable(); + $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('style.id'); + $isNew = true; + + // Include the extension plugins for the save events. + PluginHelper::importPlugin($this->events_map['save']); + + // Load the row if saving an existing record. + if ($pk > 0) { + $table->load($pk); + $isNew = false; + } + + if ($app->input->get('task') == 'save2copy') { + $data['title'] = $this->generateNewTitle(null, null, $data['title']); + $data['home'] = 0; + $data['assigned'] = ''; + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Prepare the row for saving + $this->prepareTable($table); + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array('com_templates.style', &$table, $isNew)); + + // Store the data. + if (in_array(false, $result, true) || !$table->store()) { + $this->setError($table->getError()); + + return false; + } + + $user = Factory::getUser(); + + if ($user->authorise('core.edit', 'com_menus') && $table->client_id == 0) { + $n = 0; + $db = $this->getDatabase(); + $user = Factory::getUser(); + $tableId = (int) $table->id; + $userId = (int) $user->id; + + if (!empty($data['assigned']) && is_array($data['assigned'])) { + $data['assigned'] = ArrayHelper::toInteger($data['assigned']); + + // Update the mapping for menu items that this style IS assigned to. + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('template_style_id') . ' = :newtsid') + ->whereIn($db->quoteName('id'), $data['assigned']) + ->where($db->quoteName('template_style_id') . ' != :tsid') + ->where('(' . $db->quoteName('checked_out') . ' IS NULL OR ' . $db->quoteName('checked_out') . ' = :userid)') + ->bind(':userid', $userId, ParameterType::INTEGER) + ->bind(':newtsid', $tableId, ParameterType::INTEGER) + ->bind(':tsid', $tableId, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + $n += $db->getAffectedRows(); + } + + // Remove style mappings for menu items this style is NOT assigned to. + // If unassigned then all existing maps will be removed. + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('template_style_id') . ' = 0'); + + if (!empty($data['assigned'])) { + $query->whereNotIn($db->quoteName('id'), $data['assigned']); + } + + $query->where($db->quoteName('template_style_id') . ' = :templatestyleid') + ->where('(' . $db->quoteName('checked_out') . ' IS NULL OR ' . $db->quoteName('checked_out') . ' = :userid)') + ->bind(':userid', $userId, ParameterType::INTEGER) + ->bind(':templatestyleid', $tableId, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + $n += $db->getAffectedRows(); + + if ($n > 0) { + $app->enqueueMessage(Text::plural('COM_TEMPLATES_MENU_CHANGED', $n)); + } + } + + // Clean the cache. + $this->cleanCache(); + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array('com_templates.style', &$table, $isNew)); + + $this->setState('style.id', $table->id); + + return true; + } + + /** + * Method to set a template style as home. + * + * @param integer $id The primary key ID for the style. + * + * @return boolean True if successful. + * + * @throws \Exception + */ + public function setHome($id = 0) + { + $user = Factory::getUser(); + $db = $this->getDatabase(); + + // Access checks. + if (!$user->authorise('core.edit.state', 'com_templates')) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); + } + + $style = $this->getTable(); + + if (!$style->load((int) $id)) { + throw new \Exception(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND')); + } + + // Detect disabled extension + $extension = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\'); + + if ($extension->load(array('enabled' => 0, 'type' => 'template', 'element' => $style->template, 'client_id' => $style->client_id))) { + throw new \Exception(Text::_('COM_TEMPLATES_ERROR_SAVE_DISABLED_TEMPLATE')); + } + + $clientId = (int) $style->client_id; + $id = (int) $id; + + // Reset the home fields for the client_id. + $query = $db->getQuery(true) + ->update($db->quoteName('#__template_styles')) + ->set($db->quoteName('home') . ' = ' . $db->quote('0')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->where($db->quoteName('home') . ' = ' . $db->quote('1')) + ->bind(':clientid', $clientId, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + // Set the new home style. + $query = $db->getQuery(true) + ->update($db->quoteName('#__template_styles')) + ->set($db->quoteName('home') . ' = ' . $db->quote('1')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + // Clean the cache. + $this->cleanCache(); + + return true; + } + + /** + * Method to unset a template style as default for a language. + * + * @param integer $id The primary key ID for the style. + * + * @return boolean True if successful. + * + * @throws \Exception + */ + public function unsetHome($id = 0) + { + $user = Factory::getUser(); + $db = $this->getDatabase(); + + // Access checks. + if (!$user->authorise('core.edit.state', 'com_templates')) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); + } + + $id = (int) $id; + + // Lookup the client_id. + $query = $db->getQuery(true) + ->select($db->quoteName(['client_id', 'home'])) + ->from($db->quoteName('#__template_styles')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $style = $db->loadObject(); + + if (!is_numeric($style->client_id)) { + throw new \Exception(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND')); + } elseif ($style->home == '1') { + throw new \Exception(Text::_('COM_TEMPLATES_ERROR_CANNOT_UNSET_DEFAULT_STYLE')); + } + + // Set the new home style. + $query = $db->getQuery(true) + ->update($db->quoteName('#__template_styles')) + ->set($db->quoteName('home') . ' = ' . $db->quote('0')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + // Clean the cache. + $this->cleanCache(); + + return true; + } + + /** + * Get the necessary data to load an item help screen. + * + * @return object An object with key, url, and local properties for loading the item help screen. + * + * @since 1.6 + */ + public function getHelp() + { + return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); + } + + /** + * Returns the back end template for the given style. + * + * @param int $styleId The style id + * + * @return stdClass + * + * @since 4.2.0 + */ + public function getAdminTemplate(int $styleId): stdClass + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['s.template', 's.params', 's.inheritable', 's.parent'])) + ->from($db->quoteName('#__template_styles', 's')) + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.type') . ' = ' . $db->quote('template') + . ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('s.template') + . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('s.client_id') + ) + ->where( + [ + $db->quoteName('s.client_id') . ' = 1', + $db->quoteName('s.home') . ' = ' . $db->quote('1'), + ] + ); + + if ($styleId) { + $query->extendWhere( + 'OR', + [ + $db->quoteName('s.client_id') . ' = 1', + $db->quoteName('s.id') . ' = :style', + $db->quoteName('e.enabled') . ' = 1', + ] + ) + ->bind(':style', $styleId, ParameterType::INTEGER); + } + + $query->order($db->quoteName('s.home')); + $db->setQuery($query); + + return $db->loadObject(); + } + + /** + * Returns the front end templates. + * + * @return array + * + * @since 4.2.0 + */ + public function getSiteTemplates(): array + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['id', 'home', 'template', 's.params', 'inheritable', 'parent'])) + ->from($db->quoteName('#__template_styles', 's')) + ->where( + [ + $db->quoteName('s.client_id') . ' = 0', + $db->quoteName('e.enabled') . ' = 1', + ] + ) + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.element') . ' = ' . $db->quoteName('s.template') + . ' AND ' . $db->quoteName('e.type') . ' = ' . $db->quote('template') + . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('s.client_id') + ); + + $db->setQuery($query); + + return $db->loadObjectList('id'); + } + + /** + * Custom clean cache method + * + * @param string $group The cache group + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_templates'); + parent::cleanCache('_system'); + } } diff --git a/administrator/components/com_templates/src/Model/StylesModel.php b/administrator/components/com_templates/src/Model/StylesModel.php index c635eaabb4614..a22e1ce91c401 100644 --- a/administrator/components/com_templates/src/Model/StylesModel.php +++ b/administrator/components/com_templates/src/Model/StylesModel.php @@ -1,4 +1,5 @@ isClient('api')) - { - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.template', $this->getUserStateFromRequest($this->context . '.filter.template', 'filter_template', '', 'string')); - $this->setState('filter.menuitem', $this->getUserStateFromRequest($this->context . '.filter.menuitem', 'filter_menuitem', '', 'cmd')); + if (!$app->isClient('api')) { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.template', $this->getUserStateFromRequest($this->context . '.filter.template', 'filter_template', '', 'string')); + $this->setState('filter.menuitem', $this->getUserStateFromRequest($this->context . '.filter.menuitem', 'filter_menuitem', '', 'cmd')); - // Special case for the client id. - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $clientId = !in_array($clientId, [0, 1]) ? 0 : $clientId; - $this->setState('client_id', $clientId); - } + // Special case for the client id. + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $clientId = !in_array($clientId, [0, 1]) ? 0 : $clientId; + $this->setState('client_id', $clientId); + } - // Load the parameters. - $params = ComponentHelper::getParams('com_templates'); - $this->setState('params', $params); + // Load the parameters. + $params = ComponentHelper::getParams('com_templates'); + $this->setState('params', $params); - // List state information. - parent::populateState($ordering, $direction); - } + // List state information. + parent::populateState($ordering, $direction); + } - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('client_id'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.template'); - $id .= ':' . $this->getState('filter.menuitem'); + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('client_id'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.template'); + $id .= ':' . $this->getState('filter.menuitem'); - return parent::getStoreId($id); - } + return parent::getStoreId($id); + } - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - $clientId = (int) $this->getState('client_id'); + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + $clientId = (int) $this->getState('client_id'); - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.template'), - $db->quoteName('a.title'), - $db->quoteName('a.home'), - $db->quoteName('a.client_id'), - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image'), - $db->quoteName('l.sef', 'language_sef'), - ] - ) - ) - ->select( - [ - 'COUNT(' . $db->quoteName('m.template_style_id') . ') AS assigned', - $db->quoteName('extension_id', 'e_id'), - ] - ) - ->from($db->quoteName('#__template_styles', 'a')) - ->where($db->quoteName('a.client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.template'), + $db->quoteName('a.title'), + $db->quoteName('a.home'), + $db->quoteName('a.client_id'), + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image'), + $db->quoteName('l.sef', 'language_sef'), + ] + ) + ) + ->select( + [ + 'COUNT(' . $db->quoteName('m.template_style_id') . ') AS assigned', + $db->quoteName('extension_id', 'e_id'), + ] + ) + ->from($db->quoteName('#__template_styles', 'a')) + ->where($db->quoteName('a.client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); - // Join on menus. - $query->join('LEFT', $db->quoteName('#__menu', 'm'), $db->quoteName('m.template_style_id') . ' = ' . $db->quoteName('a.id')) - ->group( - [ - $db->quoteName('a.id'), - $db->quoteName('a.template'), - $db->quoteName('a.title'), - $db->quoteName('a.home'), - $db->quoteName('a.client_id'), - $db->quoteName('l.title'), - $db->quoteName('l.image'), - $db->quoteName('l.sef'), - $db->quoteName('e.extension_id'), - ] - ); + // Join on menus. + $query->join('LEFT', $db->quoteName('#__menu', 'm'), $db->quoteName('m.template_style_id') . ' = ' . $db->quoteName('a.id')) + ->group( + [ + $db->quoteName('a.id'), + $db->quoteName('a.template'), + $db->quoteName('a.title'), + $db->quoteName('a.home'), + $db->quoteName('a.client_id'), + $db->quoteName('l.title'), + $db->quoteName('l.image'), + $db->quoteName('l.sef'), + $db->quoteName('e.extension_id'), + ] + ); - // Join over the language. - $query->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.home')); + // Join over the language. + $query->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.home')); - // Filter by extension enabled. - $query->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.element') . ' = ' . $db->quoteName('a.template') - . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('a.client_id') - ) - ->where( - [ - $db->quoteName('e.enabled') . ' = 1', - $db->quoteName('e.type') . ' = ' . $db->quote('template'), - ] - ); + // Filter by extension enabled. + $query->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.element') . ' = ' . $db->quoteName('a.template') + . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('a.client_id') + ) + ->where( + [ + $db->quoteName('e.enabled') . ' = 1', + $db->quoteName('e.type') . ' = ' . $db->quote('template'), + ] + ); - // Filter by template. - if ($template = $this->getState('filter.template')) - { - $query->where($db->quoteName('a.template') . ' = :template') - ->bind(':template', $template); - } + // Filter by template. + if ($template = $this->getState('filter.template')) { + $query->where($db->quoteName('a.template') . ' = :template') + ->bind(':template', $template); + } - // Filter by menuitem. - $menuItemId = $this->getState('filter.menuitem'); + // Filter by menuitem. + $menuItemId = $this->getState('filter.menuitem'); - if ($clientId === 0 && is_numeric($menuItemId)) - { - // If user selected the templates styles that are not assigned to any page. - if ((int) $menuItemId === -1) - { - // Only custom template styles overrides not assigned to any menu item. - $query->where( - [ - $db->quoteName('a.home') . ' = ' . $db->quote('0'), - $db->quoteName('m.id') . ' IS NULL', - ] - ); - } - // If user selected the templates styles assigned to particular pages. - else - { - // Subquery to get the language of the selected menu item. - $menuItemId = (int) $menuItemId; - $menuItemLanguageSubQuery = $db->getQuery(true); - $menuItemLanguageSubQuery->select($db->quoteName('language')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('id') . ' = :menuitemid'); - $query->bind(':menuitemid', $menuItemId, ParameterType::INTEGER); + if ($clientId === 0 && is_numeric($menuItemId)) { + // If user selected the templates styles that are not assigned to any page. + if ((int) $menuItemId === -1) { + // Only custom template styles overrides not assigned to any menu item. + $query->where( + [ + $db->quoteName('a.home') . ' = ' . $db->quote('0'), + $db->quoteName('m.id') . ' IS NULL', + ] + ); + } + // If user selected the templates styles assigned to particular pages. + else { + // Subquery to get the language of the selected menu item. + $menuItemId = (int) $menuItemId; + $menuItemLanguageSubQuery = $db->getQuery(true); + $menuItemLanguageSubQuery->select($db->quoteName('language')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('id') . ' = :menuitemid'); + $query->bind(':menuitemid', $menuItemId, ParameterType::INTEGER); - // Subquery to get the language of the selected menu item. - $templateStylesMenuItemsSubQuery = $db->getQuery(true); - $templateStylesMenuItemsSubQuery->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('template_style_id') . ' = ' . $db->quoteName('a.id')); + // Subquery to get the language of the selected menu item. + $templateStylesMenuItemsSubQuery = $db->getQuery(true); + $templateStylesMenuItemsSubQuery->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('template_style_id') . ' = ' . $db->quoteName('a.id')); - // Main query where clause. - $query->where('(' . - // Default template style (fallback template style to all menu items). - $db->quoteName('a.home') . ' = ' . $db->quote('1') . ' OR ' . - // Default template style for specific language (fallback template style to the selected menu item language). - $db->quoteName('a.home') . ' IN (' . $menuItemLanguageSubQuery . ') OR ' . - // Custom template styles override (only if assigned to the selected menu item). - '(' . $db->quoteName('a.home') . ' = ' . $db->quote('0') . ' AND ' . $menuItemId . ' IN (' . $templateStylesMenuItemsSubQuery . '))' . - ')' - ); - } - } + // Main query where clause. + $query->where('(' . + // Default template style (fallback template style to all menu items). + $db->quoteName('a.home') . ' = ' . $db->quote('1') . ' OR ' . + // Default template style for specific language (fallback template style to the selected menu item language). + $db->quoteName('a.home') . ' IN (' . $menuItemLanguageSubQuery . ') OR ' . + // Custom template styles override (only if assigned to the selected menu item). + '(' . $db->quoteName('a.home') . ' = ' . $db->quote('0') . ' AND ' . $menuItemId . ' IN (' . $templateStylesMenuItemsSubQuery . '))' . + ')'); + } + } - // Filter by search in title. - if ($search = $this->getState('filter.search')) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . StringHelper::strtolower($search) . '%'; - $query->extendWhere( - 'AND', - [ - 'LOWER(' . $db->quoteName('a.template') . ') LIKE :template', - 'LOWER(' . $db->quoteName('a.title') . ') LIKE :title', - ], - 'OR' - ) - ->bind(':template', $search) - ->bind(':title', $search); - } - } + // Filter by search in title. + if ($search = $this->getState('filter.search')) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . StringHelper::strtolower($search) . '%'; + $query->extendWhere( + 'AND', + [ + 'LOWER(' . $db->quoteName('a.template') . ') LIKE :template', + 'LOWER(' . $db->quoteName('a.title') . ') LIKE :title', + ], + 'OR' + ) + ->bind(':template', $search) + ->bind(':title', $search); + } + } - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.template')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.template')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - return $query; - } + return $query; + } } diff --git a/administrator/components/com_templates/src/Model/TemplateModel.php b/administrator/components/com_templates/src/Model/TemplateModel.php index 50267364f0195..76233f704e6d6 100644 --- a/administrator/components/com_templates/src/Model/TemplateModel.php +++ b/administrator/components/com_templates/src/Model/TemplateModel.php @@ -1,4 +1,5 @@ getTemplate()) - { - $path = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $path); - $path = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $path); - $temp->name = $name; - $temp->id = urlencode(base64_encode(str_replace('\\', '//', $path))); - - return $temp; - } - } - - /** - * Method to store file information. - * - * @param string $path The base path. - * @param string $name The file name. - * @param stdClass $template The std class object of template. - * - * @return object stdClass object. - * - * @since 4.0.0 - */ - protected function storeFileInfo($path, $name, $template) - { - $temp = new \stdClass; - $temp->id = base64_encode($path . $name); - $temp->client = $template->client_id; - $temp->template = $template->element; - $temp->extension_id = $template->extension_id; - - if ($coreFile = $this->getCoreFile($path . $name, $template->client_id)) - { - $temp->coreFile = md5_file($coreFile); - } - else - { - $temp->coreFile = null; - } - - return $temp; - } - - /** - * Method to get all template list. - * - * @return object stdClass object - * - * @since 4.0.0 - */ - public function getTemplateList() - { - // Get a db connection. - $db = $this->getDatabase(); - - // Create a new query object. - $query = $db->getQuery(true); - - // Select the required fields from the table - $query->select( - $this->getState( - 'list.select', - 'a.extension_id, a.name, a.element, a.client_id' - ) - ); - - $query->from($db->quoteName('#__extensions', 'a')) - ->where($db->quoteName('a.enabled') . ' = 1') - ->where($db->quoteName('a.type') . ' = ' . $db->quote('template')); - - // Reset the query. - $db->setQuery($query); - - // Load the results as a list of stdClass objects. - $results = $db->loadObjectList(); - - return $results; - } - - /** - * Method to get all updated file list. - * - * @param boolean $state The optional parameter if you want unchecked list. - * @param boolean $all The optional parameter if you want all list. - * @param boolean $cleanup The optional parameter if you want to clean record which is no more required. - * - * @return object stdClass object - * - * @since 4.0.0 - */ - public function getUpdatedList($state = false, $all = false, $cleanup = false) - { - // Get a db connection. - $db = $this->getDatabase(); - - // Create a new query object. - $query = $db->getQuery(true); - - // Select the required fields from the table - $query->select( - $this->getState( - 'list.select', - 'a.template, a.hash_id, a.extension_id, a.state, a.action, a.client_id, a.created_date, a.modified_date' - ) - ); - - $template = $this->getTemplate(); - - $query->from($db->quoteName('#__template_overrides', 'a')); - - if (!$all) - { - $teid = (int) $template->extension_id; - $query->where($db->quoteName('extension_id') . ' = :teid') - ->bind(':teid', $teid, ParameterType::INTEGER); - } - - if ($state) - { - $query->where($db->quoteName('state') . ' = 0'); - } - - $query->order($db->quoteName('a.modified_date') . ' DESC'); - - // Reset the query. - $db->setQuery($query); - - // Load the results as a list of stdClass objects. - $pks = $db->loadObjectList(); - - if ($state) - { - return $pks; - } - - $results = array(); - - foreach ($pks as $pk) - { - $client = ApplicationHelper::getClientInfo($pk->client_id); - $path = Path::clean($client->path . '/templates/' . $pk->template . base64_decode($pk->hash_id)); - - if (file_exists($path)) - { - $results[] = $pk; - } - elseif ($cleanup) - { - $cleanupIds = array(); - $cleanupIds[] = $pk->hash_id; - $this->publish($cleanupIds, -3, $pk->extension_id); - } - } - - return $results; - } - - /** - * Method to get a list of all the core files of override files. - * - * @return array An array of all core files. - * - * @since 4.0.0 - */ - public function getCoreList() - { - // Get list of all templates - $templates = $this->getTemplateList(); - - // Initialize the array variable to store core file list. - $this->coreFileList = array(); - - $app = Factory::getApplication(); - - foreach ($templates as $template) - { - $client = ApplicationHelper::getClientInfo($template->client_id); - $element = Path::clean($client->path . '/templates/' . $template->element . '/'); - $path = Path::clean($element . 'html/'); - - if (is_dir($path)) - { - $this->prepareCoreFiles($path, $element, $template); - } - } - - // Sort list of stdClass array. - usort( - $this->coreFileList, - function ($a, $b) - { - return strcmp($a->id, $b->id); - } - ); - - return $this->coreFileList; - } - - /** - * Prepare core files. - * - * @param string $dir The path of the directory to scan. - * @param string $element The path of the template element. - * @param \stdClass $template The stdClass object of template. - * - * @return array - * - * @since 4.0.0 - */ - public function prepareCoreFiles($dir, $element, $template) - { - $dirFiles = scandir($dir); - - foreach ($dirFiles as $key => $value) - { - if (in_array($value, array('.', '..', 'node_modules'))) - { - continue; - } - - if (is_dir($dir . $value)) - { - $relativePath = str_replace($element, '', $dir . $value); - $this->prepareCoreFiles($dir . $value . '/', $element, $template); - } - else - { - $ext = pathinfo($dir . $value, PATHINFO_EXTENSION); - $allowedFormat = $this->checkFormat($ext); - - if ($allowedFormat === true) - { - $relativePath = str_replace($element, '', $dir); - $info = $this->storeFileInfo('/' . $relativePath, $value, $template); - - if ($info) - { - $this->coreFileList[] = $info; - } - } - } - } - } - - /** - * Method to update status of list. - * - * @param array $ids The base path. - * @param array $value The file name. - * @param integer $exid The template extension id. - * - * @return integer Number of files changed. - * - * @since 4.0.0 - */ - public function publish($ids, $value, $exid) - { - $db = $this->getDatabase(); - - foreach ($ids as $id) - { - if ($value === -3) - { - $deleteQuery = $db->getQuery(true) - ->delete($db->quoteName('#__template_overrides')) - ->where($db->quoteName('hash_id') . ' = :hashid') - ->where($db->quoteName('extension_id') . ' = :exid') - ->bind(':hashid', $id) - ->bind(':exid', $exid, ParameterType::INTEGER); - - try - { - // Set the query using our newly populated query object and execute it. - $db->setQuery($deleteQuery); - $result = $db->execute(); - } - catch (\RuntimeException $e) - { - return $e; - } - } - elseif ($value === 1 || $value === 0) - { - $updateQuery = $db->getQuery(true) - ->update($db->quoteName('#__template_overrides')) - ->set($db->quoteName('state') . ' = :state') - ->where($db->quoteName('hash_id') . ' = :hashid') - ->where($db->quoteName('extension_id') . ' = :exid') - ->bind(':state', $value, ParameterType::INTEGER) - ->bind(':hashid', $id) - ->bind(':exid', $exid, ParameterType::INTEGER); - - try - { - // Set the query using our newly populated query object and execute it. - $db->setQuery($updateQuery); - $result = $db->execute(); - } - catch (\RuntimeException $e) - { - return $e; - } - } - } - - return $result; - } - - /** - * Method to get a list of all the files to edit in a template. - * - * @return array A nested array of relevant files. - * - * @since 1.6 - */ - public function getFiles() - { - $result = array(); - - if ($template = $this->getTemplate()) - { - $app = Factory::getApplication(); - $client = ApplicationHelper::getClientInfo($template->client_id); - $path = Path::clean($client->path . '/templates/' . $template->element . '/'); - $lang = Factory::getLanguage(); - - // Load the core and/or local language file(s). - $lang->load('tpl_' . $template->element, $client->path) - || (!empty($template->xmldata->parent) && $lang->load('tpl_' . $template->xmldata->parent, $client->path)) - || $lang->load('tpl_' . $template->element, $client->path . '/templates/' . $template->element) - || (!empty($template->xmldata->parent) && $lang->load('tpl_' . $template->xmldata->parent, $client->path . '/templates/' . $template->xmldata->parent)); - $this->element = $path; - - if (!is_writable($path)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_DIRECTORY_NOT_WRITABLE'), 'error'); - } - - if (is_dir($path)) - { - $result = $this->getDirectoryTree($path); - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_TEMPLATE_FOLDER_NOT_FOUND'), 'error'); - - return false; - } - - // Clean up override history - $this->getUpdatedList(false, true, true); - } - - return $result; - } - - /** - * Get the directory tree. - * - * @param string $dir The path of the directory to scan - * - * @return array - * - * @since 3.2 - */ - public function getDirectoryTree($dir) - { - $result = array(); - - $dirFiles = scandir($dir); - - foreach ($dirFiles as $key => $value) - { - if (!in_array($value, array('.', '..', 'node_modules'))) - { - if (is_dir($dir . $value)) - { - $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $dir . $value); - $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) .'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $relativePath); - $result[str_replace('\\', '//', $relativePath)] = $this->getDirectoryTree($dir . $value . '/'); - } - else - { - $ext = pathinfo($dir . $value, PATHINFO_EXTENSION); - $allowedFormat = $this->checkFormat($ext); - - if ($allowedFormat == true) - { - $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media'. DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $dir . $value); - $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $relativePath); - $result[] = $this->getFile($relativePath, $value); - } - } - } - } - - return $result; - } - - /** - * Method to get the core file of override file - * - * @param string $file Override file - * @param integer $client_id Client Id - * - * @return string $corefile The full path and file name for the target file, or boolean false if the file is not found in any of the paths. - * - * @since 4.0.0 - */ - public function getCoreFile($file, $client_id) - { - $app = Factory::getApplication(); - $filePath = Path::clean($file); - $explodeArray = explode(DIRECTORY_SEPARATOR, $filePath); - - // Only allow html/ folder - if ($explodeArray['1'] !== 'html') - { - return false; - } - - $fileName = basename($filePath); - $type = $explodeArray['2']; - $client = ApplicationHelper::getClientInfo($client_id); - - $componentPath = Path::clean($client->path . '/components/'); - $modulePath = Path::clean($client->path . '/modules/'); - $layoutPath = Path::clean(JPATH_ROOT . '/layouts/'); - - // For modules - if (stristr($type, 'mod_') !== false) - { - $folder = $explodeArray['2']; - $htmlPath = Path::clean($modulePath . $folder . '/tmpl/'); - $fileName = $this->getSafeName($fileName); - $coreFile = Path::find($htmlPath, $fileName); - - return $coreFile; - } - elseif (stristr($type, 'com_') !== false) - { - // For components - $folder = $explodeArray['2']; - $subFolder = $explodeArray['3']; - $fileName = $this->getSafeName($fileName); - - // The new scheme, if a view has a tmpl folder - $newHtmlPath = Path::clean($componentPath . $folder . '/tmpl/' . $subFolder . '/'); - - if (!$coreFile = Path::find($newHtmlPath, $fileName)) - { - // The old scheme, the views are directly in the component/tmpl folder - $oldHtmlPath = Path::clean($componentPath . $folder . '/views/' . $subFolder . '/tmpl/'); - $coreFile = Path::find($oldHtmlPath, $fileName); - - return $coreFile; - } - - return $coreFile; - } - elseif (stristr($type, 'layouts') !== false) - { - // For Layouts - $subtype = $explodeArray['3']; - - if (stristr($subtype, 'com_')) - { - $folder = $explodeArray['3']; - $subFolder = array_slice($explodeArray, 4, -1); - $subFolder = implode(DIRECTORY_SEPARATOR, $subFolder); - $htmlPath = Path::clean($componentPath . $folder . '/layouts/' . $subFolder); - $fileName = $this->getSafeName($fileName); - $coreFile = Path::find($htmlPath, $fileName); - - return $coreFile; - } - elseif (stristr($subtype, 'joomla') || stristr($subtype, 'libraries') || stristr($subtype, 'plugins')) - { - $subFolder = array_slice($explodeArray, 3, -1); - $subFolder = implode(DIRECTORY_SEPARATOR, $subFolder); - $htmlPath = Path::clean($layoutPath . $subFolder); - $fileName = $this->getSafeName($fileName); - $coreFile = Path::find($htmlPath, $fileName); - - return $coreFile; - } - } - - return false; - } - - /** - * Creates a safe file name for the given name. - * - * @param string $name The filename - * - * @return string $fileName The filtered name without Date - * - * @since 4.0.0 - */ - private function getSafeName($name) - { - if (strpos($name, '-') !== false && preg_match('/[0-9]/', $name)) - { - // Get the extension - $extension = File::getExt($name); - - // Remove ( Date ) from file - $explodeArray = explode('-', $name); - $size = count($explodeArray); - $date = $explodeArray[$size - 2] . '-' . str_replace('.' . $extension, '', $explodeArray[$size - 1]); - - if ($this->validateDate($date)) - { - $nameWithoutExtension = implode('-', array_slice($explodeArray, 0, -2)); - - // Filtered name - $name = $nameWithoutExtension . '.' . $extension; - } - } - - return $name; - } - - /** - * Validate Date in file name. - * - * @param string $date Date to validate. - * - * @return boolean Return true if date is valid and false if not. - * - * @since 4.0.0 - */ - private function validateDate($date) - { - $format = 'Ymd-His'; - $valid = Date::createFromFormat($format, $date); - - return $valid && $valid->format($format) === $date; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState('extension.id', $pk); - - // Load the parameters. - $params = ComponentHelper::getParams('com_templates'); - $this->setState('params', $params); - } - - /** - * Method to get the template information. - * - * @return mixed Object if successful, false if not and internal error is set. - * - * @since 1.6 - */ - public function &getTemplate() - { - if (empty($this->template)) - { - $pk = (int) $this->getState('extension.id'); - $db = $this->getDatabase(); - $app = Factory::getApplication(); - - // Get the template information. - $query = $db->getQuery(true) - ->select($db->quoteName(['extension_id', 'client_id', 'element', 'name', 'manifest_cache'])) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('extension_id') . ' = :pk') - ->where($db->quoteName('type') . ' = ' . $db->quote('template')) - ->bind(':pk', $pk, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $result = $db->loadObject(); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage($e->getMessage(), 'warning'); - $this->template = false; - - return false; - } - - if (empty($result)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXTENSION_RECORD_NOT_FOUND'), 'error'); - $this->template = false; - } - else - { - $this->template = $result; - - // Client ID is not always an integer, so enforce here - $this->template->client_id = (int) $this->template->client_id; - - if (!isset($this->template->xmldata)) - { - $this->template->xmldata = TemplatesHelper::parseXMLTemplateFile($this->template->client_id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $this->template->name); - } - } - } - - return $this->template; - } - - /** - * Method to check if new template name already exists - * - * @return boolean true if name is not used, false otherwise - * - * @since 2.5 - */ - public function checkNewName() - { - $db = $this->getDatabase(); - $name = $this->getState('new_name'); - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('name') . ' = :name') - ->bind(':name', $name); - $db->setQuery($query); - - return ($db->loadResult() == 0); - } - - /** - * Method to check if new template name already exists - * - * @return string name of current template - * - * @since 2.5 - */ - public function getFromName() - { - return $this->getTemplate()->element; - } - - /** - * Method to check if new template name already exists - * - * @return boolean true if name is not used, false otherwise - * - * @since 2.5 - */ - public function copy() - { - $app = Factory::getApplication(); - - if ($template = $this->getTemplate()) - { - $client = ApplicationHelper::getClientInfo($template->client_id); - $fromPath = Path::clean($client->path . '/templates/' . $template->element . '/'); - - // Delete new folder if it exists - $toPath = $this->getState('to_path'); - - if (Folder::exists($toPath)) - { - if (!Folder::delete($toPath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); - - return false; - } - } - - // Copy all files from $fromName template to $newName folder - if (!Folder::copy($fromPath, $toPath)) - { - return false; - } - - // Check manifest for additional files - $manifest = simplexml_load_file($toPath . '/templateDetails.xml'); - - // Copy language files from global folder - if ($languages = $manifest->languages) - { - $folder = (string) $languages->attributes()->folder; - $languageFiles = $languages->language; - - Folder::create($toPath . '/' . $folder . '/' . $languageFiles->attributes()->tag); - - foreach ($languageFiles as $languageFile) - { - $src = Path::clean($client->path . '/language/' . $languageFile); - $dst = Path::clean($toPath . '/' . $folder . '/' . $languageFile); - - if (File::exists($src)) - { - File::copy($src, $dst); - } - } - } - - // Copy media files - if ($media = $manifest->media) - { - $folder = (string) $media->attributes()->folder; - $destination = (string) $media->attributes()->destination; - - Folder::copy(JPATH_SITE . '/media/' . $destination, $toPath . '/' . $folder); - } - - // Adjust to new template name - if (!$this->fixTemplateName()) - { - return false; - } - - return true; - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); - - return false; - } - } - - /** - * Method to delete tmp folder - * - * @return boolean true if delete successful, false otherwise - * - * @since 2.5 - */ - public function cleanup() - { - // Clear installation messages - $app = Factory::getApplication(); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - - // Delete temporary directory - return Folder::delete($this->getState('to_path')); - } - - /** - * Method to rename the template in the XML files and rename the language files - * - * @return boolean true if successful, false otherwise - * - * @since 2.5 - */ - protected function fixTemplateName() - { - // Rename Language files - // Get list of language files - $result = true; - $files = Folder::files($this->getState('to_path'), '\.ini$', true, true); - $newName = strtolower($this->getState('new_name')); - $template = $this->getTemplate(); - $oldName = $template->element; - $manifest = json_decode($template->manifest_cache); - - foreach ($files as $file) - { - $newFile = '/' . str_replace($oldName, $newName, basename($file)); - $result = File::move($file, dirname($file) . $newFile) && $result; - } - - // Edit XML file - $xmlFile = $this->getState('to_path') . '/templateDetails.xml'; - - if (File::exists($xmlFile)) - { - $contents = file_get_contents($xmlFile); - $pattern[] = '#\s*' . $manifest->name . '\s*#i'; - $replace[] = '' . $newName . ''; - $pattern[] = '##'; - $replace[] = ''; - $pattern[] = '##'; - $replace[] = ''; - $contents = preg_replace($pattern, $replace, $contents); - $result = File::write($xmlFile, $contents) && $result; - } - - return $result; - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - $app = Factory::getApplication(); - - // Codemirror or Editor None should be enabled - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from('#__extensions as a') - ->where( - '(a.name =' . $db->quote('plg_editors_codemirror') . - ' AND a.enabled = 1) OR (a.name =' . - $db->quote('plg_editors_none') . - ' AND a.enabled = 1)' - ); - $db->setQuery($query); - $state = $db->loadResult(); - - if ((int) $state < 1) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EDITOR_DISABLED'), 'warning'); - } - - // Get the form. - $form = $this->loadForm('com_templates.source', 'source', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $data = $this->getSource(); - - $this->preprocessData('com_templates.source', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function &getSource() - { - $app = Factory::getApplication(); - $item = new \stdClass; - - if (!$this->template) - { - $this->getTemplate(); - } - - if ($this->template) - { - $input = Factory::getApplication()->input; - $fileName = base64_decode($input->get('file')); - $fileName = str_replace('//', '/', $fileName); - $isMedia = $input->getInt('isMedia', 0); - - $fileName = $isMedia ? Path::clean(JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . $fileName) - : Path::clean(JPATH_ROOT . ($this->template->client_id === 0 ? '' : '/administrator') . '/templates/' . $this->template->element . $fileName); - - try - { - $filePath = Path::check($fileName); - } - catch (\Exception $e) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_FOUND'), 'error'); - - return; - } - - if (file_exists($filePath)) - { - $item->extension_id = $this->getState('extension.id'); - $item->filename = Path::clean($fileName); - $item->source = file_get_contents($filePath); - $item->filePath = Path::clean($filePath); - $ds = DIRECTORY_SEPARATOR; - $cleanFileName = str_replace(JPATH_ROOT . ($this->template->client_id === 1 ? $ds . 'administrator' . $ds : $ds) . 'templates' . $ds . $this->template->element, '', $fileName); - - if ($coreFile = $this->getCoreFile($cleanFileName, $this->template->client_id)) - { - $item->coreFile = $coreFile; - $item->core = file_get_contents($coreFile); - } - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_FOUND'), 'error'); - } - } - - return $item; - } - - /** - * Method to store the source file contents. - * - * @param array $data The source data to save. - * - * @return boolean True on success, false otherwise and internal error set. - * - * @since 1.6 - */ - public function save($data) - { - // Get the template. - $template = $this->getTemplate(); - - if (empty($template)) - { - return false; - } - - $app = Factory::getApplication(); - $fileName = base64_decode($app->input->get('file')); - $isMedia = $app->input->getInt('isMedia', 0); - $fileName = $isMedia ? JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . $fileName : - JPATH_ROOT . '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element . $fileName; - - $filePath = Path::clean($fileName); - - // Include the extension plugins for the save events. - PluginHelper::importPlugin('extension'); - - $user = get_current_user(); - chown($filePath, $user); - Path::setPermissions($filePath, '0644'); - - // Try to make the template file writable. - if (!is_writable($filePath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_WRITABLE'), 'warning'); - $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_FILE_PERMISSIONS', Path::getPermissions($filePath)), 'warning'); - - if (!Path::isOwner($filePath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_CHECK_FILE_OWNERSHIP'), 'warning'); - } - - return false; - } - - // Make sure EOL is Unix - $data['source'] = str_replace(array("\r\n", "\r"), "\n", $data['source']); - - // If the asset file for the template ensure we have valid template so we don't instantly destroy it - if ($fileName === '/joomla.asset.json' && json_decode($data['source']) === null) - { - $this->setError(Text::_('COM_TEMPLATES_ERROR_ASSET_FILE_INVALID_JSON')); - - return false; - } - - $return = File::write($filePath, $data['source']); - - if (!$return) - { - $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_ERROR_FAILED_TO_SAVE_FILENAME', $fileName), 'error'); - - return false; - } - - // Get the extension of the changed file. - $explodeArray = explode('.', $fileName); - $ext = end($explodeArray); - - if ($ext == 'less') - { - $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_COMPILE_LESS', $fileName)); - } - - return true; - } - - /** - * Get overrides folder. - * - * @param string $name The name of override. - * @param string $path Location of override. - * - * @return object containing override name and path. - * - * @since 3.2 - */ - public function getOverridesFolder($name,$path) - { - $folder = new \stdClass; - $folder->name = $name; - $folder->path = base64_encode($path . $name); - - return $folder; - } - - /** - * Get a list of overrides. - * - * @return array containing overrides. - * - * @since 3.2 - */ - public function getOverridesList() - { - if ($template = $this->getTemplate()) - { - $client = ApplicationHelper::getClientInfo($template->client_id); - $componentPath = Path::clean($client->path . '/components/'); - $modulePath = Path::clean($client->path . '/modules/'); - $pluginPath = Path::clean(JPATH_ROOT . '/plugins/'); - $layoutPath = Path::clean(JPATH_ROOT . '/layouts/'); - $components = Folder::folders($componentPath); - - foreach ($components as $component) - { - // Collect the folders with views - $folders = Folder::folders($componentPath . '/' . $component, '^view[s]?$', false, true); - $folders = array_merge($folders, Folder::folders($componentPath . '/' . $component, '^tmpl?$', false, true)); - - if (!$folders) - { - continue; - } - - foreach ($folders as $folder) - { - // The subfolders are views - $views = Folder::folders($folder); - - foreach ($views as $view) - { - // The old scheme, if a view has a tmpl folder - $path = $folder . '/' . $view . '/tmpl'; - - // The new scheme, the views are directly in the component/tmpl folder - if (!is_dir($path) && substr($folder, -4) == 'tmpl') - { - $path = $folder . '/' . $view; - } - - // Check if the folder exists - if (!is_dir($path)) - { - continue; - } - - $result['components'][$component][] = $this->getOverridesFolder($view, Path::clean($folder . '/')); - } - } - } - - foreach (Folder::folders($pluginPath) as $pluginGroup) - { - foreach (Folder::folders($pluginPath . '/' . $pluginGroup) as $plugin) - { - if (file_exists($pluginPath . '/' . $pluginGroup . '/' . $plugin . '/tmpl/')) - { - $pluginLayoutPath = Path::clean($pluginPath . '/' . $pluginGroup . '/'); - $result['plugins'][$pluginGroup][] = $this->getOverridesFolder($plugin, $pluginLayoutPath); - } - } - } - - $modules = Folder::folders($modulePath); - - foreach ($modules as $module) - { - $result['modules'][] = $this->getOverridesFolder($module, $modulePath); - } - - $layoutFolders = Folder::folders($layoutPath); - - foreach ($layoutFolders as $layoutFolder) - { - $layoutFolderPath = Path::clean($layoutPath . '/' . $layoutFolder . '/'); - $layouts = Folder::folders($layoutFolderPath); - - foreach ($layouts as $layout) - { - $result['layouts'][$layoutFolder][] = $this->getOverridesFolder($layout, $layoutFolderPath); - } - } - - // Check for layouts in component folders - foreach ($components as $component) - { - if (file_exists($componentPath . '/' . $component . '/layouts/')) - { - $componentLayoutPath = Path::clean($componentPath . '/' . $component . '/layouts/'); - - if ($componentLayoutPath) - { - $layouts = Folder::folders($componentLayoutPath); - - foreach ($layouts as $layout) - { - $result['layouts'][$component][] = $this->getOverridesFolder($layout, $componentLayoutPath); - } - } - } - } - } - - if (!empty($result)) - { - return $result; - } - } - - /** - * Create overrides. - * - * @param string $override The override location. - * - * @return boolean true if override creation is successful, false otherwise - * - * @since 3.2 - */ - public function createOverride($override) - { - if ($template = $this->getTemplate()) - { - $app = Factory::getApplication(); - $explodeArray = explode(DIRECTORY_SEPARATOR, $override); - $name = end($explodeArray); - $client = ApplicationHelper::getClientInfo($template->client_id); - - if (stristr($name, 'mod_') != false) - { - $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $name); - } - elseif (stristr($override, 'com_') != false) - { - $size = count($explodeArray); - - $url = Path::clean($explodeArray[$size - 3] . '/' . $explodeArray[$size - 1]); - - if ($explodeArray[$size - 2] == 'layouts') - { - $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/layouts/' . $url); - } - else - { - $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $url); - } - } - elseif (stripos($override, Path::clean(JPATH_ROOT . '/plugins/')) === 0) - { - $size = count($explodeArray); - $layoutPath = Path::clean('plg_' . $explodeArray[$size - 2] . '_' . $explodeArray[$size - 1]); - $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $layoutPath); - } - else - { - $layoutPath = implode('/', array_slice($explodeArray, -2)); - $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/layouts/' . $layoutPath); - } - - // Check Html folder, create if not exist - if (!Folder::exists($htmlPath)) - { - if (!Folder::create($htmlPath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_ERROR'), 'error'); - - return false; - } - } - - if (stristr($name, 'mod_') != false) - { - $return = $this->createTemplateOverride(Path::clean($override . '/tmpl'), $htmlPath); - } - elseif (stristr($override, 'com_') != false && stristr($override, 'layouts') == false) - { - $path = $override . '/tmpl'; - - // View can also be in the top level folder - if (!is_dir($path)) - { - $path = $override; - } - - $return = $this->createTemplateOverride(Path::clean($path), $htmlPath); - } - elseif (stripos($override, Path::clean(JPATH_ROOT . '/plugins/')) === 0) - { - $return = $this->createTemplateOverride(Path::clean($override . '/tmpl'), $htmlPath); - } - else - { - $return = $this->createTemplateOverride($override, $htmlPath); - } - - if ($return) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_OVERRIDE_CREATED') . str_replace(JPATH_ROOT, '', $htmlPath)); - - return true; - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_OVERRIDE_FAILED'), 'error'); - - return false; - } - } - } - - /** - * Create override folder & file - * - * @param string $overridePath The override location - * @param string $htmlPath The html location - * - * @return boolean True on success. False otherwise. - */ - public function createTemplateOverride($overridePath, $htmlPath) - { - $return = false; - - if (empty($overridePath) || empty($htmlPath)) - { - return $return; - } - - // Get list of template folders - $folders = Folder::folders($overridePath, null, true, true); - - if (!empty($folders)) - { - foreach ($folders as $folder) - { - $htmlFolder = $htmlPath . str_replace($overridePath, '', $folder); - - if (!Folder::exists($htmlFolder)) - { - Folder::create($htmlFolder); - } - } - } - - // Get list of template files (Only get *.php file for template file) - $files = Folder::files($overridePath, '.php', true, true); - - if (empty($files)) - { - return true; - } - - foreach ($files as $file) - { - $overrideFilePath = str_replace($overridePath, '', $file); - $htmlFilePath = $htmlPath . $overrideFilePath; - - if (File::exists($htmlFilePath)) - { - // Generate new unique file name base on current time - $today = Factory::getDate(); - $htmlFilePath = File::stripExt($htmlFilePath) . '-' . $today->format('Ymd-His') . '.' . File::getExt($htmlFilePath); - } - - $return = File::copy($file, $htmlFilePath, '', true); - } - - return $return; - } - - /** - * Delete a particular file. - * - * @param string $file The relative location of the file. - * - * @return boolean True if file deletion is successful, false otherwise - * - * @since 3.2 - */ - public function deleteFile($file) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $filePath = $this->getBasePath() . urldecode(base64_decode($file)); - - $return = File::delete($filePath); - - if (!$return) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_DELETE_ERROR'), 'error'); - - return false; - } - - return true; - } - } - - /** - * Create new file. - * - * @param string $name The name of file. - * @param string $type The extension of the file. - * @param string $location Location for the new file. - * - * @return boolean true if file created successfully, false otherwise - * - * @since 3.2 - */ - public function createFile($name, $type, $location) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $base = $this->getBasePath(); - - if (file_exists(Path::clean($base . '/' . $location . '/' . $name . '.' . $type))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); - - return false; - } - - if (!fopen(Path::clean($base . '/' . $location . '/' . $name . '.' . $type), 'x')) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_CREATE_ERROR'), 'error'); - - return false; - } - - // Check if the format is allowed and will be showed in the backend - $check = $this->checkFormat($type); - - // Add a message if we are not allowed to show this file in the backend. - if (!$check) - { - $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_WARNING_FORMAT_WILL_NOT_BE_VISIBLE', $type), 'warning'); - } - - return true; - } - } - - /** - * Upload new file. - * - * @param array $file The uploaded file array. - * @param string $location Location for the new file. - * - * @return boolean True if file uploaded successfully, false otherwise - * - * @since 3.2 - */ - public function uploadFile($file, $location) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = $this->getBasePath(); - $fileName = File::makeSafe($file['name']); - - $err = null; - - if (!TemplateHelper::canUpload($file, $err)) - { - // Can't upload the file - return false; - } - - if (file_exists(Path::clean($path . '/' . $location . '/' . $file['name']))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); - - return false; - } - - if (!File::upload($file['tmp_name'], Path::clean($path . '/' . $location . '/' . $fileName))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_UPLOAD_ERROR'), 'error'); - - return false; - } - - $url = Path::clean($location . '/' . $fileName); - - return $url; - } - } - - /** - * Create new folder. - * - * @param string $name The name of the new folder. - * @param string $location Location for the new folder. - * - * @return boolean True if override folder is created successfully, false otherwise - * - * @since 3.2 - */ - public function createFolder($name, $location) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = Path::clean($location . '/'); - $base = $this->getBasePath(); - - if (file_exists(Path::clean($base . $path . $name))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_EXISTS'), 'error'); - - return false; - } - - if (!Folder::create(Path::clean($base . $path . $name))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_CREATE_ERROR'), 'error'); - - return false; - } - - return true; - } - } - - /** - * Delete a folder. - * - * @param string $location The name and location of the folder. - * - * @return boolean True if override folder is deleted successfully, false otherwise - * - * @since 3.2 - */ - public function deleteFolder($location) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $base = $this->getBasePath(); - $path = Path::clean($location . '/'); - - if (!file_exists($base . $path)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_NOT_EXISTS'), 'error'); - - return false; - } - - $return = Folder::delete($base . $path); - - if (!$return) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_ERROR'), 'error'); - - return false; - } - - return true; - } - } - - /** - * Rename a file. - * - * @param string $file The name and location of the old file - * @param string $name The new name of the file. - * - * @return string Encoded string containing the new file location. - * - * @since 3.2 - */ - public function renameFile($file, $name) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = $this->getBasePath(); - $fileName = base64_decode($file); - $explodeArray = explode('.', $fileName); - $type = end($explodeArray); - $explodeArray = explode('/', $fileName); - $newName = str_replace(end($explodeArray), $name . '.' . $type, $fileName); - - if (file_exists($path . $newName)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); - - return false; - } - - if (!rename($path . $fileName, $path . $newName)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_RENAME_ERROR'), 'error'); - - return false; - } - - return base64_encode($newName); - } - } - - /** - * Get an image address, height and width. - * - * @return array an associative array containing image address, height and width. - * - * @since 3.2 - */ - public function getImage() - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $fileName = base64_decode($app->input->get('file')); - $path = $this->getBasePath(); - - $uri = Uri::root(false) . ltrim(str_replace(JPATH_ROOT, '', $this->getBasePath()), '/'); - - if (file_exists(Path::clean($path . $fileName))) - { - $JImage = new Image(Path::clean($path . $fileName)); - $image['address'] = $uri . $fileName; - $image['path'] = $fileName; - $image['height'] = $JImage->getHeight(); - $image['width'] = $JImage->getWidth(); - } - - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_IMAGE_FILE_NOT_FOUND'), 'error'); - - return false; - } - - return $image; - } - } - - /** - * Crop an image. - * - * @param string $file The name and location of the file - * @param string $w width. - * @param string $h height. - * @param string $x x-coordinate. - * @param string $y y-coordinate. - * - * @return boolean true if image cropped successfully, false otherwise. - * - * @since 3.2 - */ - public function cropImage($file, $w, $h, $x, $y) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = $this->getBasePath() . base64_decode($file); - - try - { - $image = new Image($path); - $properties = $image->getImageFileProperties($path); - - switch ($properties->mime) - { - case 'image/webp': - $imageType = \IMAGETYPE_WEBP; - break; - case 'image/png': - $imageType = \IMAGETYPE_PNG; - break; - case 'image/gif': - $imageType = \IMAGETYPE_GIF; - break; - default: - $imageType = \IMAGETYPE_JPEG; - } - - $image->crop($w, $h, $x, $y, false); - $image->toFile($path, $imageType); - - return true; - } - catch (\Exception $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - } - } - } - - /** - * Resize an image. - * - * @param string $file The name and location of the file - * @param string $width The new width of the image. - * @param string $height The new height of the image. - * - * @return boolean true if image resize successful, false otherwise. - * - * @since 3.2 - */ - public function resizeImage($file, $width, $height) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = $this->getBasePath() . base64_decode($file); - - try - { - $image = new Image($path); - $properties = $image->getImageFileProperties($path); - - switch ($properties->mime) - { - case 'image/webp': - $imageType = \IMAGETYPE_WEBP; - break; - case 'image/png': - $imageType = \IMAGETYPE_PNG; - break; - case 'image/gif': - $imageType = \IMAGETYPE_GIF; - break; - default: - $imageType = \IMAGETYPE_JPEG; - } - - $image->resize($width, $height, false, Image::SCALE_FILL); - $image->toFile($path, $imageType); - - return true; - } - catch (\Exception $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - } - } - } - - /** - * Template preview. - * - * @return object object containing the id of the template. - * - * @since 3.2 - */ - public function getPreview() - { - $app = Factory::getApplication(); - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select($db->quoteName(['id', 'client_id'])); - $query->from($db->quoteName('#__template_styles')); - $query->where($db->quoteName('template') . ' = :template') - ->bind(':template', $this->template->element); - - $db->setQuery($query); - - try - { - $result = $db->loadObject(); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage($e->getMessage(), 'warning'); - } - - if (empty($result)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXTENSION_RECORD_NOT_FOUND'), 'warning'); - } - else - { - return $result; - } - } - - /** - * Rename a file. - * - * @return mixed array on success, false on failure - * - * @since 3.2 - */ - public function getFont() - { - if ($template = $this->getTemplate()) - { - $app = Factory::getApplication(); - $client = ApplicationHelper::getClientInfo($template->client_id); - $relPath = base64_decode($app->input->get('file')); - $explodeArray = explode('/', $relPath); - $fileName = end($explodeArray); - $path = $this->getBasePath() . base64_decode($app->input->get('file')); - - if (stristr($client->path, 'administrator') == false) - { - $folder = '/templates/'; - } - else - { - $folder = '/administrator/templates/'; - } - - $uri = Uri::root(true) . $folder . $template->element; - - if (file_exists(Path::clean($path))) - { - $font['address'] = $uri . $relPath; - - $font['rel_path'] = $relPath; - - $font['name'] = $fileName; - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_FONT_FILE_NOT_FOUND'), 'error'); - - return false; - } - - return $font; - } - } - - /** - * Copy a file. - * - * @param string $newName The name of the copied file - * @param string $location The final location where the file is to be copied - * @param string $file The name and location of the file - * - * @return boolean true if image resize successful, false otherwise. - * - * @since 3.2 - */ - public function copyFile($newName, $location, $file) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $relPath = base64_decode($file); - $explodeArray = explode('.', $relPath); - $ext = end($explodeArray); - $path = $this->getBasePath(); - $newPath = Path::clean($path . $location . '/' . $newName . '.' . $ext); - - if (file_exists($newPath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); - - return false; - } - - if (File::copy($path . $relPath, $newPath)) - { - $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_FILE_COPY_SUCCESS', $newName . '.' . $ext)); - - return true; - } - else - { - return false; - } - } - } - - /** - * Get the compressed files. - * - * @return array if file exists, false otherwise - * - * @since 3.2 - */ - public function getArchive() - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = $this->getBasePath() . base64_decode($app->input->get('file')); - - if (file_exists(Path::clean($path))) - { - $files = array(); - $zip = new \ZipArchive; - - if ($zip->open($path) === true) - { - for ($i = 0; $i < $zip->numFiles; $i++) - { - $entry = $zip->getNameIndex($i); - $files[] = $entry; - } - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); - - return false; - } - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_FONT_FILE_NOT_FOUND'), 'error'); - - return false; - } - - return $files; - } - } - - /** - * Extract contents of an archive file. - * - * @param string $file The name and location of the file - * - * @return boolean true if image extraction is successful, false otherwise. - * - * @since 3.2 - */ - public function extractArchive($file) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $relPath = base64_decode($file); - $explodeArray = explode('/', $relPath); - $fileName = end($explodeArray); - $path = $this->getBasePath() . base64_decode($file); - - if (file_exists(Path::clean($path . '/' . $fileName))) - { - $zip = new \ZipArchive; - - if ($zip->open(Path::clean($path . '/' . $fileName)) === true) - { - for ($i = 0; $i < $zip->numFiles; $i++) - { - $entry = $zip->getNameIndex($i); - - if (file_exists(Path::clean($path . '/' . $entry))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXISTS'), 'error'); - - return false; - } - } - - $zip->extractTo($path); - - return true; - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); - - return false; - } - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_NOT_FOUND'), 'error'); - - return false; - } - } - } - - /** - * Check if the extension is allowed and will be shown in the template manager - * - * @param string $ext The extension to check if it is allowed - * - * @return boolean true if the extension is allowed false otherwise - * - * @since 3.6.0 - */ - protected function checkFormat($ext) - { - if (!isset($this->allowedFormats)) - { - $params = ComponentHelper::getParams('com_templates'); - $imageTypes = explode(',', $params->get('image_formats')); - $sourceTypes = explode(',', $params->get('source_formats')); - $fontTypes = explode(',', $params->get('font_formats')); - $archiveTypes = explode(',', $params->get('compressed_formats')); - - $this->allowedFormats = array_merge($imageTypes, $sourceTypes, $fontTypes, $archiveTypes); - $this->allowedFormats = array_map('strtolower', $this->allowedFormats); - } - - return in_array(strtolower($ext), $this->allowedFormats); - } - - /** - * Method to get a list of all the files to edit in a template's media folder. - * - * @return array A nested array of relevant files. - * - * @since 4.1.0 - */ - public function getMediaFiles() - { - $result = []; - $template = $this->getTemplate(); - - if (!isset($template->xmldata)) - { - $template->xmldata = TemplatesHelper::parseXMLTemplateFile($template->client_id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); - } - - if (!isset($template->xmldata->inheritable) || (isset($template->xmldata->parent) && $template->xmldata->parent === '')) - { - return $result; - } - - $app = Factory::getApplication(); - $path = Path::clean(JPATH_ROOT . '/media/templates/' . ($template->client_id === 0 ? 'site' : 'administrator') . '/' . $template->element . '/'); - $this->mediaElement = $path; - - if (!is_writable($path)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_DIRECTORY_NOT_WRITABLE'), 'error'); - } - - if (is_dir($path)) - { - $result = $this->getDirectoryTree($path); - } - - return $result; - } - - /** - * Method to resolve the base folder. - * - * @return string The absolute path for the base. - * - * @since 4.1.0 - */ - private function getBasePath() - { - $app = Factory::getApplication(); - $isMedia = $app->input->getInt('isMedia', 0); - - return $isMedia ? JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element : - JPATH_ROOT . '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element; - } - - /** - * Method to create the templateDetails.xml for the child template - * - * @return boolean true if name is not used, false otherwise - * - * @since 4.1.0 - */ - public function child() - { - $app = Factory::getApplication(); - $template = $this->getTemplate(); - - if (!(array) $template) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); - - return false; - } - - $client = ApplicationHelper::getClientInfo($template->client_id); - $fromPath = Path::clean($client->path . '/templates/' . $template->element . '/templateDetails.xml'); - - // Delete new folder if it exists - $toPath = $this->getState('to_path'); - - if (Folder::exists($toPath)) - { - if (!Folder::delete($toPath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); - - return false; - } - } - else - { - if (!Folder::create($toPath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); - - return false; - } - } - - // Copy the template definition from the parent template - if (!File::copy($fromPath, $toPath . '/templateDetails.xml')) - { - return false; - } - - // Check manifest for additional files - $newName = strtolower($this->getState('new_name')); - $template = $this->getTemplate(); - - // Edit XML file - $xmlFile = Path::clean($this->getState('to_path') . '/templateDetails.xml'); - - if (!File::exists($xmlFile)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); - - return false; - } - - try - { - $xml = simplexml_load_string(file_get_contents($xmlFile)); - } - catch (\Exception $e) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_READ'), 'error'); - - return false; - } - - $user = Factory::getUser(); - unset($xml->languages); - unset($xml->media); - unset($xml->files); - unset($xml->parent); - unset($xml->inheritable); - - // Remove the update parts - unset($xml->update); - unset($xml->updateservers); - - if (isset($xml->creationDate)) - { - $xml->creationDate = (new Date('now'))->format('F Y'); - } - else - { - $xml->addChild('creationDate', (new Date('now'))->format('F Y')); - } - - if (isset($xml->author)) - { - $xml->author = $user->name; - } - else - { - $xml->addChild('author', $user->name); - } - - if (isset($xml->authorEmail)) - { - $xml->authorEmail = $user->email; - } - else - { - $xml->addChild('authorEmail', $user->email); - } - - $files = $xml->addChild('files'); - $files->addChild('filename', 'templateDetails.xml'); - - // Media folder - $media = $xml->addChild('media'); - $media->addAttribute('folder', 'media'); - $media->addAttribute('destination', 'templates/' . ($template->client_id === 0 ? 'site/' : 'administrator/') . $template->element . '_' . $newName); - $media->addChild('folder', 'css'); - $media->addChild('folder', 'js'); - $media->addChild('folder', 'images'); - $media->addChild('folder', 'html'); - $media->addChild('folder', 'scss'); - - $xml->name = $template->element . '_' . $newName; - $xml->inheritable = 0; - $files = $xml->addChild('parent', $template->element); - - $dom = new \DOMDocument; - $dom->preserveWhiteSpace = false; - $dom->formatOutput = true; - $dom->loadXML($xml->asXML()); - - $result = File::write($xmlFile, $dom->saveXML()); - - if (!$result) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); - - return false; - } - - // Create an empty media folder structure - if (!Folder::create($toPath . '/media') - || !Folder::create($toPath . '/media/css') - || !Folder::create($toPath . '/media/js') - || !Folder::create($toPath . '/media/images') - || !Folder::create($toPath . '/media/html/tinymce') - || !Folder::create($toPath . '/media/scss')) - { - return false; - } - - return true; - } - - /** - * Method to get the parent template existing styles - * - * @return array array of id,titles of the styles - * - * @since 4.1.3 - */ - public function getAllTemplateStyles() - { - $template = $this->getTemplate(); - - if (empty($template->xmldata->inheritable)) - { - return []; - } - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select($db->quoteName(['id', 'title'])) - ->from($db->quoteName('#__template_styles')) - ->where($db->quoteName('client_id') . ' = :client_id', 'AND') - ->where($db->quoteName('template') . ' = :template') - ->orWhere($db->quoteName('parent') . ' = :parent') - ->bind(':client_id', $template->client_id, ParameterType::INTEGER) - ->bind(':template', $template->element) - ->bind(':parent', $template->element); - - $db->setQuery($query); - - return $db->loadObjectList(); - } - - /** - * Method to copy selected styles to the child template - * - * @return boolean true if name is not used, false otherwise - * - * @since 4.1.3 - */ - public function copyStyles() - { - $app = Factory::getApplication(); - $template = $this->getTemplate(); - $newName = strtolower($this->getState('new_name')); - $applyStyles = $this->getState('stylesToCopy'); - - // Get a db connection. - $db = $this->getDatabase(); - - // Create a new query object. - $query = $db->getQuery(true); - - $query->select($db->quoteName(['title', 'params'])) - ->from($db->quoteName('#__template_styles')) - ->whereIn($db->quoteName('id'), ArrayHelper::toInteger($applyStyles)); - // Reset the query using our newly populated query object. - $db->setQuery($query); - - try - { - $parentStyle = $db->loadObjectList(); - } - catch (\Exception $e) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND'), 'error'); - - return false; - } - - foreach ($parentStyle as $style) - { - $query = $db->getQuery(true); - $styleName = Text::sprintf('COM_TEMPLATES_COPY_CHILD_TEMPLATE_STYLES', ucfirst($template->element . '_' . $newName), $style->title); - - // Insert columns and values - $columns = ['id', 'template', 'client_id', 'home', 'title', 'inheritable', 'parent', 'params']; - $values = [0, $db->quote($template->element . '_' . $newName), (int) $template->client_id, $db->quote('0'), $db->quote($styleName), 0, $db->quote($template->element), $db->quote($style->params)]; - - $query - ->insert($db->quoteName('#__template_styles')) - ->columns($db->quoteName($columns)) - ->values(implode(',', $values)); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\Exception $e) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_READ'), 'error'); - - return false; - } - } - - return true; - } + /** + * The information in a template + * + * @var \stdClass + * @since 1.6 + */ + protected $template = null; + + /** + * The path to the template + * + * @var string + * @since 3.2 + */ + protected $element = null; + + /** + * The path to the static assets + * + * @var string + * @since 4.1.0 + */ + protected $mediaElement = null; + + /** + * Internal method to get file properties. + * + * @param string $path The base path. + * @param string $name The file name. + * + * @return object + * + * @since 1.6 + */ + protected function getFile($path, $name) + { + $temp = new \stdClass(); + + if ($this->getTemplate()) { + $path = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $path); + $path = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $path); + $temp->name = $name; + $temp->id = urlencode(base64_encode(str_replace('\\', '//', $path))); + + return $temp; + } + } + + /** + * Method to store file information. + * + * @param string $path The base path. + * @param string $name The file name. + * @param stdClass $template The std class object of template. + * + * @return object stdClass object. + * + * @since 4.0.0 + */ + protected function storeFileInfo($path, $name, $template) + { + $temp = new \stdClass(); + $temp->id = base64_encode($path . $name); + $temp->client = $template->client_id; + $temp->template = $template->element; + $temp->extension_id = $template->extension_id; + + if ($coreFile = $this->getCoreFile($path . $name, $template->client_id)) { + $temp->coreFile = md5_file($coreFile); + } else { + $temp->coreFile = null; + } + + return $temp; + } + + /** + * Method to get all template list. + * + * @return object stdClass object + * + * @since 4.0.0 + */ + public function getTemplateList() + { + // Get a db connection. + $db = $this->getDatabase(); + + // Create a new query object. + $query = $db->getQuery(true); + + // Select the required fields from the table + $query->select( + $this->getState( + 'list.select', + 'a.extension_id, a.name, a.element, a.client_id' + ) + ); + + $query->from($db->quoteName('#__extensions', 'a')) + ->where($db->quoteName('a.enabled') . ' = 1') + ->where($db->quoteName('a.type') . ' = ' . $db->quote('template')); + + // Reset the query. + $db->setQuery($query); + + // Load the results as a list of stdClass objects. + $results = $db->loadObjectList(); + + return $results; + } + + /** + * Method to get all updated file list. + * + * @param boolean $state The optional parameter if you want unchecked list. + * @param boolean $all The optional parameter if you want all list. + * @param boolean $cleanup The optional parameter if you want to clean record which is no more required. + * + * @return object stdClass object + * + * @since 4.0.0 + */ + public function getUpdatedList($state = false, $all = false, $cleanup = false) + { + // Get a db connection. + $db = $this->getDatabase(); + + // Create a new query object. + $query = $db->getQuery(true); + + // Select the required fields from the table + $query->select( + $this->getState( + 'list.select', + 'a.template, a.hash_id, a.extension_id, a.state, a.action, a.client_id, a.created_date, a.modified_date' + ) + ); + + $template = $this->getTemplate(); + + $query->from($db->quoteName('#__template_overrides', 'a')); + + if (!$all) { + $teid = (int) $template->extension_id; + $query->where($db->quoteName('extension_id') . ' = :teid') + ->bind(':teid', $teid, ParameterType::INTEGER); + } + + if ($state) { + $query->where($db->quoteName('state') . ' = 0'); + } + + $query->order($db->quoteName('a.modified_date') . ' DESC'); + + // Reset the query. + $db->setQuery($query); + + // Load the results as a list of stdClass objects. + $pks = $db->loadObjectList(); + + if ($state) { + return $pks; + } + + $results = array(); + + foreach ($pks as $pk) { + $client = ApplicationHelper::getClientInfo($pk->client_id); + $path = Path::clean($client->path . '/templates/' . $pk->template . base64_decode($pk->hash_id)); + + if (file_exists($path)) { + $results[] = $pk; + } elseif ($cleanup) { + $cleanupIds = array(); + $cleanupIds[] = $pk->hash_id; + $this->publish($cleanupIds, -3, $pk->extension_id); + } + } + + return $results; + } + + /** + * Method to get a list of all the core files of override files. + * + * @return array An array of all core files. + * + * @since 4.0.0 + */ + public function getCoreList() + { + // Get list of all templates + $templates = $this->getTemplateList(); + + // Initialize the array variable to store core file list. + $this->coreFileList = array(); + + $app = Factory::getApplication(); + + foreach ($templates as $template) { + $client = ApplicationHelper::getClientInfo($template->client_id); + $element = Path::clean($client->path . '/templates/' . $template->element . '/'); + $path = Path::clean($element . 'html/'); + + if (is_dir($path)) { + $this->prepareCoreFiles($path, $element, $template); + } + } + + // Sort list of stdClass array. + usort( + $this->coreFileList, + function ($a, $b) { + return strcmp($a->id, $b->id); + } + ); + + return $this->coreFileList; + } + + /** + * Prepare core files. + * + * @param string $dir The path of the directory to scan. + * @param string $element The path of the template element. + * @param \stdClass $template The stdClass object of template. + * + * @return array + * + * @since 4.0.0 + */ + public function prepareCoreFiles($dir, $element, $template) + { + $dirFiles = scandir($dir); + + foreach ($dirFiles as $key => $value) { + if (in_array($value, array('.', '..', 'node_modules'))) { + continue; + } + + if (is_dir($dir . $value)) { + $relativePath = str_replace($element, '', $dir . $value); + $this->prepareCoreFiles($dir . $value . '/', $element, $template); + } else { + $ext = pathinfo($dir . $value, PATHINFO_EXTENSION); + $allowedFormat = $this->checkFormat($ext); + + if ($allowedFormat === true) { + $relativePath = str_replace($element, '', $dir); + $info = $this->storeFileInfo('/' . $relativePath, $value, $template); + + if ($info) { + $this->coreFileList[] = $info; + } + } + } + } + } + + /** + * Method to update status of list. + * + * @param array $ids The base path. + * @param array $value The file name. + * @param integer $exid The template extension id. + * + * @return integer Number of files changed. + * + * @since 4.0.0 + */ + public function publish($ids, $value, $exid) + { + $db = $this->getDatabase(); + + foreach ($ids as $id) { + if ($value === -3) { + $deleteQuery = $db->getQuery(true) + ->delete($db->quoteName('#__template_overrides')) + ->where($db->quoteName('hash_id') . ' = :hashid') + ->where($db->quoteName('extension_id') . ' = :exid') + ->bind(':hashid', $id) + ->bind(':exid', $exid, ParameterType::INTEGER); + + try { + // Set the query using our newly populated query object and execute it. + $db->setQuery($deleteQuery); + $result = $db->execute(); + } catch (\RuntimeException $e) { + return $e; + } + } elseif ($value === 1 || $value === 0) { + $updateQuery = $db->getQuery(true) + ->update($db->quoteName('#__template_overrides')) + ->set($db->quoteName('state') . ' = :state') + ->where($db->quoteName('hash_id') . ' = :hashid') + ->where($db->quoteName('extension_id') . ' = :exid') + ->bind(':state', $value, ParameterType::INTEGER) + ->bind(':hashid', $id) + ->bind(':exid', $exid, ParameterType::INTEGER); + + try { + // Set the query using our newly populated query object and execute it. + $db->setQuery($updateQuery); + $result = $db->execute(); + } catch (\RuntimeException $e) { + return $e; + } + } + } + + return $result; + } + + /** + * Method to get a list of all the files to edit in a template. + * + * @return array A nested array of relevant files. + * + * @since 1.6 + */ + public function getFiles() + { + $result = array(); + + if ($template = $this->getTemplate()) { + $app = Factory::getApplication(); + $client = ApplicationHelper::getClientInfo($template->client_id); + $path = Path::clean($client->path . '/templates/' . $template->element . '/'); + $lang = Factory::getLanguage(); + + // Load the core and/or local language file(s). + $lang->load('tpl_' . $template->element, $client->path) + || (!empty($template->xmldata->parent) && $lang->load('tpl_' . $template->xmldata->parent, $client->path)) + || $lang->load('tpl_' . $template->element, $client->path . '/templates/' . $template->element) + || (!empty($template->xmldata->parent) && $lang->load('tpl_' . $template->xmldata->parent, $client->path . '/templates/' . $template->xmldata->parent)); + $this->element = $path; + + if (!is_writable($path)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_DIRECTORY_NOT_WRITABLE'), 'error'); + } + + if (is_dir($path)) { + $result = $this->getDirectoryTree($path); + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_TEMPLATE_FOLDER_NOT_FOUND'), 'error'); + + return false; + } + + // Clean up override history + $this->getUpdatedList(false, true, true); + } + + return $result; + } + + /** + * Get the directory tree. + * + * @param string $dir The path of the directory to scan + * + * @return array + * + * @since 3.2 + */ + public function getDirectoryTree($dir) + { + $result = array(); + + $dirFiles = scandir($dir); + + foreach ($dirFiles as $key => $value) { + if (!in_array($value, array('.', '..', 'node_modules'))) { + if (is_dir($dir . $value)) { + $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $dir . $value); + $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $relativePath); + $result[str_replace('\\', '//', $relativePath)] = $this->getDirectoryTree($dir . $value . '/'); + } else { + $ext = pathinfo($dir . $value, PATHINFO_EXTENSION); + $allowedFormat = $this->checkFormat($ext); + + if ($allowedFormat == true) { + $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $dir . $value); + $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $relativePath); + $result[] = $this->getFile($relativePath, $value); + } + } + } + } + + return $result; + } + + /** + * Method to get the core file of override file + * + * @param string $file Override file + * @param integer $client_id Client Id + * + * @return string $corefile The full path and file name for the target file, or boolean false if the file is not found in any of the paths. + * + * @since 4.0.0 + */ + public function getCoreFile($file, $client_id) + { + $app = Factory::getApplication(); + $filePath = Path::clean($file); + $explodeArray = explode(DIRECTORY_SEPARATOR, $filePath); + + // Only allow html/ folder + if ($explodeArray['1'] !== 'html') { + return false; + } + + $fileName = basename($filePath); + $type = $explodeArray['2']; + $client = ApplicationHelper::getClientInfo($client_id); + + $componentPath = Path::clean($client->path . '/components/'); + $modulePath = Path::clean($client->path . '/modules/'); + $layoutPath = Path::clean(JPATH_ROOT . '/layouts/'); + + // For modules + if (stristr($type, 'mod_') !== false) { + $folder = $explodeArray['2']; + $htmlPath = Path::clean($modulePath . $folder . '/tmpl/'); + $fileName = $this->getSafeName($fileName); + $coreFile = Path::find($htmlPath, $fileName); + + return $coreFile; + } elseif (stristr($type, 'com_') !== false) { + // For components + $folder = $explodeArray['2']; + $subFolder = $explodeArray['3']; + $fileName = $this->getSafeName($fileName); + + // The new scheme, if a view has a tmpl folder + $newHtmlPath = Path::clean($componentPath . $folder . '/tmpl/' . $subFolder . '/'); + + if (!$coreFile = Path::find($newHtmlPath, $fileName)) { + // The old scheme, the views are directly in the component/tmpl folder + $oldHtmlPath = Path::clean($componentPath . $folder . '/views/' . $subFolder . '/tmpl/'); + $coreFile = Path::find($oldHtmlPath, $fileName); + + return $coreFile; + } + + return $coreFile; + } elseif (stristr($type, 'layouts') !== false) { + // For Layouts + $subtype = $explodeArray['3']; + + if (stristr($subtype, 'com_')) { + $folder = $explodeArray['3']; + $subFolder = array_slice($explodeArray, 4, -1); + $subFolder = implode(DIRECTORY_SEPARATOR, $subFolder); + $htmlPath = Path::clean($componentPath . $folder . '/layouts/' . $subFolder); + $fileName = $this->getSafeName($fileName); + $coreFile = Path::find($htmlPath, $fileName); + + return $coreFile; + } elseif (stristr($subtype, 'joomla') || stristr($subtype, 'libraries') || stristr($subtype, 'plugins')) { + $subFolder = array_slice($explodeArray, 3, -1); + $subFolder = implode(DIRECTORY_SEPARATOR, $subFolder); + $htmlPath = Path::clean($layoutPath . $subFolder); + $fileName = $this->getSafeName($fileName); + $coreFile = Path::find($htmlPath, $fileName); + + return $coreFile; + } + } + + return false; + } + + /** + * Creates a safe file name for the given name. + * + * @param string $name The filename + * + * @return string $fileName The filtered name without Date + * + * @since 4.0.0 + */ + private function getSafeName($name) + { + if (strpos($name, '-') !== false && preg_match('/[0-9]/', $name)) { + // Get the extension + $extension = File::getExt($name); + + // Remove ( Date ) from file + $explodeArray = explode('-', $name); + $size = count($explodeArray); + $date = $explodeArray[$size - 2] . '-' . str_replace('.' . $extension, '', $explodeArray[$size - 1]); + + if ($this->validateDate($date)) { + $nameWithoutExtension = implode('-', array_slice($explodeArray, 0, -2)); + + // Filtered name + $name = $nameWithoutExtension . '.' . $extension; + } + } + + return $name; + } + + /** + * Validate Date in file name. + * + * @param string $date Date to validate. + * + * @return boolean Return true if date is valid and false if not. + * + * @since 4.0.0 + */ + private function validateDate($date) + { + $format = 'Ymd-His'; + $valid = Date::createFromFormat($format, $date); + + return $valid && $valid->format($format) === $date; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState('extension.id', $pk); + + // Load the parameters. + $params = ComponentHelper::getParams('com_templates'); + $this->setState('params', $params); + } + + /** + * Method to get the template information. + * + * @return mixed Object if successful, false if not and internal error is set. + * + * @since 1.6 + */ + public function &getTemplate() + { + if (empty($this->template)) { + $pk = (int) $this->getState('extension.id'); + $db = $this->getDatabase(); + $app = Factory::getApplication(); + + // Get the template information. + $query = $db->getQuery(true) + ->select($db->quoteName(['extension_id', 'client_id', 'element', 'name', 'manifest_cache'])) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('extension_id') . ' = :pk') + ->where($db->quoteName('type') . ' = ' . $db->quote('template')) + ->bind(':pk', $pk, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $result = $db->loadObject(); + } catch (\RuntimeException $e) { + $app->enqueueMessage($e->getMessage(), 'warning'); + $this->template = false; + + return false; + } + + if (empty($result)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXTENSION_RECORD_NOT_FOUND'), 'error'); + $this->template = false; + } else { + $this->template = $result; + + // Client ID is not always an integer, so enforce here + $this->template->client_id = (int) $this->template->client_id; + + if (!isset($this->template->xmldata)) { + $this->template->xmldata = TemplatesHelper::parseXMLTemplateFile($this->template->client_id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $this->template->name); + } + } + } + + return $this->template; + } + + /** + * Method to check if new template name already exists + * + * @return boolean true if name is not used, false otherwise + * + * @since 2.5 + */ + public function checkNewName() + { + $db = $this->getDatabase(); + $name = $this->getState('new_name'); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('name') . ' = :name') + ->bind(':name', $name); + $db->setQuery($query); + + return ($db->loadResult() == 0); + } + + /** + * Method to check if new template name already exists + * + * @return string name of current template + * + * @since 2.5 + */ + public function getFromName() + { + return $this->getTemplate()->element; + } + + /** + * Method to check if new template name already exists + * + * @return boolean true if name is not used, false otherwise + * + * @since 2.5 + */ + public function copy() + { + $app = Factory::getApplication(); + + if ($template = $this->getTemplate()) { + $client = ApplicationHelper::getClientInfo($template->client_id); + $fromPath = Path::clean($client->path . '/templates/' . $template->element . '/'); + + // Delete new folder if it exists + $toPath = $this->getState('to_path'); + + if (Folder::exists($toPath)) { + if (!Folder::delete($toPath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); + + return false; + } + } + + // Copy all files from $fromName template to $newName folder + if (!Folder::copy($fromPath, $toPath)) { + return false; + } + + // Check manifest for additional files + $manifest = simplexml_load_file($toPath . '/templateDetails.xml'); + + // Copy language files from global folder + if ($languages = $manifest->languages) { + $folder = (string) $languages->attributes()->folder; + $languageFiles = $languages->language; + + Folder::create($toPath . '/' . $folder . '/' . $languageFiles->attributes()->tag); + + foreach ($languageFiles as $languageFile) { + $src = Path::clean($client->path . '/language/' . $languageFile); + $dst = Path::clean($toPath . '/' . $folder . '/' . $languageFile); + + if (File::exists($src)) { + File::copy($src, $dst); + } + } + } + + // Copy media files + if ($media = $manifest->media) { + $folder = (string) $media->attributes()->folder; + $destination = (string) $media->attributes()->destination; + + Folder::copy(JPATH_SITE . '/media/' . $destination, $toPath . '/' . $folder); + } + + // Adjust to new template name + if (!$this->fixTemplateName()) { + return false; + } + + return true; + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); + + return false; + } + } + + /** + * Method to delete tmp folder + * + * @return boolean true if delete successful, false otherwise + * + * @since 2.5 + */ + public function cleanup() + { + // Clear installation messages + $app = Factory::getApplication(); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + + // Delete temporary directory + return Folder::delete($this->getState('to_path')); + } + + /** + * Method to rename the template in the XML files and rename the language files + * + * @return boolean true if successful, false otherwise + * + * @since 2.5 + */ + protected function fixTemplateName() + { + // Rename Language files + // Get list of language files + $result = true; + $files = Folder::files($this->getState('to_path'), '\.ini$', true, true); + $newName = strtolower($this->getState('new_name')); + $template = $this->getTemplate(); + $oldName = $template->element; + $manifest = json_decode($template->manifest_cache); + + foreach ($files as $file) { + $newFile = '/' . str_replace($oldName, $newName, basename($file)); + $result = File::move($file, dirname($file) . $newFile) && $result; + } + + // Edit XML file + $xmlFile = $this->getState('to_path') . '/templateDetails.xml'; + + if (File::exists($xmlFile)) { + $contents = file_get_contents($xmlFile); + $pattern[] = '#\s*' . $manifest->name . '\s*#i'; + $replace[] = '' . $newName . ''; + $pattern[] = '##'; + $replace[] = ''; + $pattern[] = '##'; + $replace[] = ''; + $contents = preg_replace($pattern, $replace, $contents); + $result = File::write($xmlFile, $contents) && $result; + } + + return $result; + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + $app = Factory::getApplication(); + + // Codemirror or Editor None should be enabled + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from('#__extensions as a') + ->where( + '(a.name =' . $db->quote('plg_editors_codemirror') . + ' AND a.enabled = 1) OR (a.name =' . + $db->quote('plg_editors_none') . + ' AND a.enabled = 1)' + ); + $db->setQuery($query); + $state = $db->loadResult(); + + if ((int) $state < 1) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EDITOR_DISABLED'), 'warning'); + } + + // Get the form. + $form = $this->loadForm('com_templates.source', 'source', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $data = $this->getSource(); + + $this->preprocessData('com_templates.source', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function &getSource() + { + $app = Factory::getApplication(); + $item = new \stdClass(); + + if (!$this->template) { + $this->getTemplate(); + } + + if ($this->template) { + $input = Factory::getApplication()->input; + $fileName = base64_decode($input->get('file')); + $fileName = str_replace('//', '/', $fileName); + $isMedia = $input->getInt('isMedia', 0); + + $fileName = $isMedia ? Path::clean(JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . $fileName) + : Path::clean(JPATH_ROOT . ($this->template->client_id === 0 ? '' : '/administrator') . '/templates/' . $this->template->element . $fileName); + + try { + $filePath = Path::check($fileName); + } catch (\Exception $e) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_FOUND'), 'error'); + + return; + } + + if (file_exists($filePath)) { + $item->extension_id = $this->getState('extension.id'); + $item->filename = Path::clean($fileName); + $item->source = file_get_contents($filePath); + $item->filePath = Path::clean($filePath); + $ds = DIRECTORY_SEPARATOR; + $cleanFileName = str_replace(JPATH_ROOT . ($this->template->client_id === 1 ? $ds . 'administrator' . $ds : $ds) . 'templates' . $ds . $this->template->element, '', $fileName); + + if ($coreFile = $this->getCoreFile($cleanFileName, $this->template->client_id)) { + $item->coreFile = $coreFile; + $item->core = file_get_contents($coreFile); + } + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_FOUND'), 'error'); + } + } + + return $item; + } + + /** + * Method to store the source file contents. + * + * @param array $data The source data to save. + * + * @return boolean True on success, false otherwise and internal error set. + * + * @since 1.6 + */ + public function save($data) + { + // Get the template. + $template = $this->getTemplate(); + + if (empty($template)) { + return false; + } + + $app = Factory::getApplication(); + $fileName = base64_decode($app->input->get('file')); + $isMedia = $app->input->getInt('isMedia', 0); + $fileName = $isMedia ? JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . $fileName : + JPATH_ROOT . '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element . $fileName; + + $filePath = Path::clean($fileName); + + // Include the extension plugins for the save events. + PluginHelper::importPlugin('extension'); + + $user = get_current_user(); + chown($filePath, $user); + Path::setPermissions($filePath, '0644'); + + // Try to make the template file writable. + if (!is_writable($filePath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_WRITABLE'), 'warning'); + $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_FILE_PERMISSIONS', Path::getPermissions($filePath)), 'warning'); + + if (!Path::isOwner($filePath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_CHECK_FILE_OWNERSHIP'), 'warning'); + } + + return false; + } + + // Make sure EOL is Unix + $data['source'] = str_replace(array("\r\n", "\r"), "\n", $data['source']); + + // If the asset file for the template ensure we have valid template so we don't instantly destroy it + if ($fileName === '/joomla.asset.json' && json_decode($data['source']) === null) { + $this->setError(Text::_('COM_TEMPLATES_ERROR_ASSET_FILE_INVALID_JSON')); + + return false; + } + + $return = File::write($filePath, $data['source']); + + if (!$return) { + $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_ERROR_FAILED_TO_SAVE_FILENAME', $fileName), 'error'); + + return false; + } + + // Get the extension of the changed file. + $explodeArray = explode('.', $fileName); + $ext = end($explodeArray); + + if ($ext == 'less') { + $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_COMPILE_LESS', $fileName)); + } + + return true; + } + + /** + * Get overrides folder. + * + * @param string $name The name of override. + * @param string $path Location of override. + * + * @return object containing override name and path. + * + * @since 3.2 + */ + public function getOverridesFolder($name, $path) + { + $folder = new \stdClass(); + $folder->name = $name; + $folder->path = base64_encode($path . $name); + + return $folder; + } + + /** + * Get a list of overrides. + * + * @return array containing overrides. + * + * @since 3.2 + */ + public function getOverridesList() + { + if ($template = $this->getTemplate()) { + $client = ApplicationHelper::getClientInfo($template->client_id); + $componentPath = Path::clean($client->path . '/components/'); + $modulePath = Path::clean($client->path . '/modules/'); + $pluginPath = Path::clean(JPATH_ROOT . '/plugins/'); + $layoutPath = Path::clean(JPATH_ROOT . '/layouts/'); + $components = Folder::folders($componentPath); + + foreach ($components as $component) { + // Collect the folders with views + $folders = Folder::folders($componentPath . '/' . $component, '^view[s]?$', false, true); + $folders = array_merge($folders, Folder::folders($componentPath . '/' . $component, '^tmpl?$', false, true)); + + if (!$folders) { + continue; + } + + foreach ($folders as $folder) { + // The subfolders are views + $views = Folder::folders($folder); + + foreach ($views as $view) { + // The old scheme, if a view has a tmpl folder + $path = $folder . '/' . $view . '/tmpl'; + + // The new scheme, the views are directly in the component/tmpl folder + if (!is_dir($path) && substr($folder, -4) == 'tmpl') { + $path = $folder . '/' . $view; + } + + // Check if the folder exists + if (!is_dir($path)) { + continue; + } + + $result['components'][$component][] = $this->getOverridesFolder($view, Path::clean($folder . '/')); + } + } + } + + foreach (Folder::folders($pluginPath) as $pluginGroup) { + foreach (Folder::folders($pluginPath . '/' . $pluginGroup) as $plugin) { + if (file_exists($pluginPath . '/' . $pluginGroup . '/' . $plugin . '/tmpl/')) { + $pluginLayoutPath = Path::clean($pluginPath . '/' . $pluginGroup . '/'); + $result['plugins'][$pluginGroup][] = $this->getOverridesFolder($plugin, $pluginLayoutPath); + } + } + } + + $modules = Folder::folders($modulePath); + + foreach ($modules as $module) { + $result['modules'][] = $this->getOverridesFolder($module, $modulePath); + } + + $layoutFolders = Folder::folders($layoutPath); + + foreach ($layoutFolders as $layoutFolder) { + $layoutFolderPath = Path::clean($layoutPath . '/' . $layoutFolder . '/'); + $layouts = Folder::folders($layoutFolderPath); + + foreach ($layouts as $layout) { + $result['layouts'][$layoutFolder][] = $this->getOverridesFolder($layout, $layoutFolderPath); + } + } + + // Check for layouts in component folders + foreach ($components as $component) { + if (file_exists($componentPath . '/' . $component . '/layouts/')) { + $componentLayoutPath = Path::clean($componentPath . '/' . $component . '/layouts/'); + + if ($componentLayoutPath) { + $layouts = Folder::folders($componentLayoutPath); + + foreach ($layouts as $layout) { + $result['layouts'][$component][] = $this->getOverridesFolder($layout, $componentLayoutPath); + } + } + } + } + } + + if (!empty($result)) { + return $result; + } + } + + /** + * Create overrides. + * + * @param string $override The override location. + * + * @return boolean true if override creation is successful, false otherwise + * + * @since 3.2 + */ + public function createOverride($override) + { + if ($template = $this->getTemplate()) { + $app = Factory::getApplication(); + $explodeArray = explode(DIRECTORY_SEPARATOR, $override); + $name = end($explodeArray); + $client = ApplicationHelper::getClientInfo($template->client_id); + + if (stristr($name, 'mod_') != false) { + $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $name); + } elseif (stristr($override, 'com_') != false) { + $size = count($explodeArray); + + $url = Path::clean($explodeArray[$size - 3] . '/' . $explodeArray[$size - 1]); + + if ($explodeArray[$size - 2] == 'layouts') { + $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/layouts/' . $url); + } else { + $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $url); + } + } elseif (stripos($override, Path::clean(JPATH_ROOT . '/plugins/')) === 0) { + $size = count($explodeArray); + $layoutPath = Path::clean('plg_' . $explodeArray[$size - 2] . '_' . $explodeArray[$size - 1]); + $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $layoutPath); + } else { + $layoutPath = implode('/', array_slice($explodeArray, -2)); + $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/layouts/' . $layoutPath); + } + + // Check Html folder, create if not exist + if (!Folder::exists($htmlPath)) { + if (!Folder::create($htmlPath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_ERROR'), 'error'); + + return false; + } + } + + if (stristr($name, 'mod_') != false) { + $return = $this->createTemplateOverride(Path::clean($override . '/tmpl'), $htmlPath); + } elseif (stristr($override, 'com_') != false && stristr($override, 'layouts') == false) { + $path = $override . '/tmpl'; + + // View can also be in the top level folder + if (!is_dir($path)) { + $path = $override; + } + + $return = $this->createTemplateOverride(Path::clean($path), $htmlPath); + } elseif (stripos($override, Path::clean(JPATH_ROOT . '/plugins/')) === 0) { + $return = $this->createTemplateOverride(Path::clean($override . '/tmpl'), $htmlPath); + } else { + $return = $this->createTemplateOverride($override, $htmlPath); + } + + if ($return) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_OVERRIDE_CREATED') . str_replace(JPATH_ROOT, '', $htmlPath)); + + return true; + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_OVERRIDE_FAILED'), 'error'); + + return false; + } + } + } + + /** + * Create override folder & file + * + * @param string $overridePath The override location + * @param string $htmlPath The html location + * + * @return boolean True on success. False otherwise. + */ + public function createTemplateOverride($overridePath, $htmlPath) + { + $return = false; + + if (empty($overridePath) || empty($htmlPath)) { + return $return; + } + + // Get list of template folders + $folders = Folder::folders($overridePath, null, true, true); + + if (!empty($folders)) { + foreach ($folders as $folder) { + $htmlFolder = $htmlPath . str_replace($overridePath, '', $folder); + + if (!Folder::exists($htmlFolder)) { + Folder::create($htmlFolder); + } + } + } + + // Get list of template files (Only get *.php file for template file) + $files = Folder::files($overridePath, '.php', true, true); + + if (empty($files)) { + return true; + } + + foreach ($files as $file) { + $overrideFilePath = str_replace($overridePath, '', $file); + $htmlFilePath = $htmlPath . $overrideFilePath; + + if (File::exists($htmlFilePath)) { + // Generate new unique file name base on current time + $today = Factory::getDate(); + $htmlFilePath = File::stripExt($htmlFilePath) . '-' . $today->format('Ymd-His') . '.' . File::getExt($htmlFilePath); + } + + $return = File::copy($file, $htmlFilePath, '', true); + } + + return $return; + } + + /** + * Delete a particular file. + * + * @param string $file The relative location of the file. + * + * @return boolean True if file deletion is successful, false otherwise + * + * @since 3.2 + */ + public function deleteFile($file) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $filePath = $this->getBasePath() . urldecode(base64_decode($file)); + + $return = File::delete($filePath); + + if (!$return) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_DELETE_ERROR'), 'error'); + + return false; + } + + return true; + } + } + + /** + * Create new file. + * + * @param string $name The name of file. + * @param string $type The extension of the file. + * @param string $location Location for the new file. + * + * @return boolean true if file created successfully, false otherwise + * + * @since 3.2 + */ + public function createFile($name, $type, $location) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $base = $this->getBasePath(); + + if (file_exists(Path::clean($base . '/' . $location . '/' . $name . '.' . $type))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); + + return false; + } + + if (!fopen(Path::clean($base . '/' . $location . '/' . $name . '.' . $type), 'x')) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_CREATE_ERROR'), 'error'); + + return false; + } + + // Check if the format is allowed and will be showed in the backend + $check = $this->checkFormat($type); + + // Add a message if we are not allowed to show this file in the backend. + if (!$check) { + $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_WARNING_FORMAT_WILL_NOT_BE_VISIBLE', $type), 'warning'); + } + + return true; + } + } + + /** + * Upload new file. + * + * @param array $file The uploaded file array. + * @param string $location Location for the new file. + * + * @return boolean True if file uploaded successfully, false otherwise + * + * @since 3.2 + */ + public function uploadFile($file, $location) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = $this->getBasePath(); + $fileName = File::makeSafe($file['name']); + + $err = null; + + if (!TemplateHelper::canUpload($file, $err)) { + // Can't upload the file + return false; + } + + if (file_exists(Path::clean($path . '/' . $location . '/' . $file['name']))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); + + return false; + } + + if (!File::upload($file['tmp_name'], Path::clean($path . '/' . $location . '/' . $fileName))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_UPLOAD_ERROR'), 'error'); + + return false; + } + + $url = Path::clean($location . '/' . $fileName); + + return $url; + } + } + + /** + * Create new folder. + * + * @param string $name The name of the new folder. + * @param string $location Location for the new folder. + * + * @return boolean True if override folder is created successfully, false otherwise + * + * @since 3.2 + */ + public function createFolder($name, $location) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = Path::clean($location . '/'); + $base = $this->getBasePath(); + + if (file_exists(Path::clean($base . $path . $name))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_EXISTS'), 'error'); + + return false; + } + + if (!Folder::create(Path::clean($base . $path . $name))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_CREATE_ERROR'), 'error'); + + return false; + } + + return true; + } + } + + /** + * Delete a folder. + * + * @param string $location The name and location of the folder. + * + * @return boolean True if override folder is deleted successfully, false otherwise + * + * @since 3.2 + */ + public function deleteFolder($location) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $base = $this->getBasePath(); + $path = Path::clean($location . '/'); + + if (!file_exists($base . $path)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_NOT_EXISTS'), 'error'); + + return false; + } + + $return = Folder::delete($base . $path); + + if (!$return) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_ERROR'), 'error'); + + return false; + } + + return true; + } + } + + /** + * Rename a file. + * + * @param string $file The name and location of the old file + * @param string $name The new name of the file. + * + * @return string Encoded string containing the new file location. + * + * @since 3.2 + */ + public function renameFile($file, $name) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = $this->getBasePath(); + $fileName = base64_decode($file); + $explodeArray = explode('.', $fileName); + $type = end($explodeArray); + $explodeArray = explode('/', $fileName); + $newName = str_replace(end($explodeArray), $name . '.' . $type, $fileName); + + if (file_exists($path . $newName)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); + + return false; + } + + if (!rename($path . $fileName, $path . $newName)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_RENAME_ERROR'), 'error'); + + return false; + } + + return base64_encode($newName); + } + } + + /** + * Get an image address, height and width. + * + * @return array an associative array containing image address, height and width. + * + * @since 3.2 + */ + public function getImage() + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $fileName = base64_decode($app->input->get('file')); + $path = $this->getBasePath(); + + $uri = Uri::root(false) . ltrim(str_replace(JPATH_ROOT, '', $this->getBasePath()), '/'); + + if (file_exists(Path::clean($path . $fileName))) { + $JImage = new Image(Path::clean($path . $fileName)); + $image['address'] = $uri . $fileName; + $image['path'] = $fileName; + $image['height'] = $JImage->getHeight(); + $image['width'] = $JImage->getWidth(); + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_IMAGE_FILE_NOT_FOUND'), 'error'); + + return false; + } + + return $image; + } + } + + /** + * Crop an image. + * + * @param string $file The name and location of the file + * @param string $w width. + * @param string $h height. + * @param string $x x-coordinate. + * @param string $y y-coordinate. + * + * @return boolean true if image cropped successfully, false otherwise. + * + * @since 3.2 + */ + public function cropImage($file, $w, $h, $x, $y) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = $this->getBasePath() . base64_decode($file); + + try { + $image = new Image($path); + $properties = $image->getImageFileProperties($path); + + switch ($properties->mime) { + case 'image/webp': + $imageType = \IMAGETYPE_WEBP; + break; + case 'image/png': + $imageType = \IMAGETYPE_PNG; + break; + case 'image/gif': + $imageType = \IMAGETYPE_GIF; + break; + default: + $imageType = \IMAGETYPE_JPEG; + } + + $image->crop($w, $h, $x, $y, false); + $image->toFile($path, $imageType); + + return true; + } catch (\Exception $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + } + } + } + + /** + * Resize an image. + * + * @param string $file The name and location of the file + * @param string $width The new width of the image. + * @param string $height The new height of the image. + * + * @return boolean true if image resize successful, false otherwise. + * + * @since 3.2 + */ + public function resizeImage($file, $width, $height) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = $this->getBasePath() . base64_decode($file); + + try { + $image = new Image($path); + $properties = $image->getImageFileProperties($path); + + switch ($properties->mime) { + case 'image/webp': + $imageType = \IMAGETYPE_WEBP; + break; + case 'image/png': + $imageType = \IMAGETYPE_PNG; + break; + case 'image/gif': + $imageType = \IMAGETYPE_GIF; + break; + default: + $imageType = \IMAGETYPE_JPEG; + } + + $image->resize($width, $height, false, Image::SCALE_FILL); + $image->toFile($path, $imageType); + + return true; + } catch (\Exception $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + } + } + } + + /** + * Template preview. + * + * @return object object containing the id of the template. + * + * @since 3.2 + */ + public function getPreview() + { + $app = Factory::getApplication(); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select($db->quoteName(['id', 'client_id'])); + $query->from($db->quoteName('#__template_styles')); + $query->where($db->quoteName('template') . ' = :template') + ->bind(':template', $this->template->element); + + $db->setQuery($query); + + try { + $result = $db->loadObject(); + } catch (\RuntimeException $e) { + $app->enqueueMessage($e->getMessage(), 'warning'); + } + + if (empty($result)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXTENSION_RECORD_NOT_FOUND'), 'warning'); + } else { + return $result; + } + } + + /** + * Rename a file. + * + * @return mixed array on success, false on failure + * + * @since 3.2 + */ + public function getFont() + { + if ($template = $this->getTemplate()) { + $app = Factory::getApplication(); + $client = ApplicationHelper::getClientInfo($template->client_id); + $relPath = base64_decode($app->input->get('file')); + $explodeArray = explode('/', $relPath); + $fileName = end($explodeArray); + $path = $this->getBasePath() . base64_decode($app->input->get('file')); + + if (stristr($client->path, 'administrator') == false) { + $folder = '/templates/'; + } else { + $folder = '/administrator/templates/'; + } + + $uri = Uri::root(true) . $folder . $template->element; + + if (file_exists(Path::clean($path))) { + $font['address'] = $uri . $relPath; + + $font['rel_path'] = $relPath; + + $font['name'] = $fileName; + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_FONT_FILE_NOT_FOUND'), 'error'); + + return false; + } + + return $font; + } + } + + /** + * Copy a file. + * + * @param string $newName The name of the copied file + * @param string $location The final location where the file is to be copied + * @param string $file The name and location of the file + * + * @return boolean true if image resize successful, false otherwise. + * + * @since 3.2 + */ + public function copyFile($newName, $location, $file) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $relPath = base64_decode($file); + $explodeArray = explode('.', $relPath); + $ext = end($explodeArray); + $path = $this->getBasePath(); + $newPath = Path::clean($path . $location . '/' . $newName . '.' . $ext); + + if (file_exists($newPath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); + + return false; + } + + if (File::copy($path . $relPath, $newPath)) { + $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_FILE_COPY_SUCCESS', $newName . '.' . $ext)); + + return true; + } else { + return false; + } + } + } + + /** + * Get the compressed files. + * + * @return array if file exists, false otherwise + * + * @since 3.2 + */ + public function getArchive() + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = $this->getBasePath() . base64_decode($app->input->get('file')); + + if (file_exists(Path::clean($path))) { + $files = array(); + $zip = new \ZipArchive(); + + if ($zip->open($path) === true) { + for ($i = 0; $i < $zip->numFiles; $i++) { + $entry = $zip->getNameIndex($i); + $files[] = $entry; + } + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); + + return false; + } + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_FONT_FILE_NOT_FOUND'), 'error'); + + return false; + } + + return $files; + } + } + + /** + * Extract contents of an archive file. + * + * @param string $file The name and location of the file + * + * @return boolean true if image extraction is successful, false otherwise. + * + * @since 3.2 + */ + public function extractArchive($file) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $relPath = base64_decode($file); + $explodeArray = explode('/', $relPath); + $fileName = end($explodeArray); + $path = $this->getBasePath() . base64_decode($file); + + if (file_exists(Path::clean($path . '/' . $fileName))) { + $zip = new \ZipArchive(); + + if ($zip->open(Path::clean($path . '/' . $fileName)) === true) { + for ($i = 0; $i < $zip->numFiles; $i++) { + $entry = $zip->getNameIndex($i); + + if (file_exists(Path::clean($path . '/' . $entry))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXISTS'), 'error'); + + return false; + } + } + + $zip->extractTo($path); + + return true; + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); + + return false; + } + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_NOT_FOUND'), 'error'); + + return false; + } + } + } + + /** + * Check if the extension is allowed and will be shown in the template manager + * + * @param string $ext The extension to check if it is allowed + * + * @return boolean true if the extension is allowed false otherwise + * + * @since 3.6.0 + */ + protected function checkFormat($ext) + { + if (!isset($this->allowedFormats)) { + $params = ComponentHelper::getParams('com_templates'); + $imageTypes = explode(',', $params->get('image_formats')); + $sourceTypes = explode(',', $params->get('source_formats')); + $fontTypes = explode(',', $params->get('font_formats')); + $archiveTypes = explode(',', $params->get('compressed_formats')); + + $this->allowedFormats = array_merge($imageTypes, $sourceTypes, $fontTypes, $archiveTypes); + $this->allowedFormats = array_map('strtolower', $this->allowedFormats); + } + + return in_array(strtolower($ext), $this->allowedFormats); + } + + /** + * Method to get a list of all the files to edit in a template's media folder. + * + * @return array A nested array of relevant files. + * + * @since 4.1.0 + */ + public function getMediaFiles() + { + $result = []; + $template = $this->getTemplate(); + + if (!isset($template->xmldata)) { + $template->xmldata = TemplatesHelper::parseXMLTemplateFile($template->client_id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); + } + + if (!isset($template->xmldata->inheritable) || (isset($template->xmldata->parent) && $template->xmldata->parent === '')) { + return $result; + } + + $app = Factory::getApplication(); + $path = Path::clean(JPATH_ROOT . '/media/templates/' . ($template->client_id === 0 ? 'site' : 'administrator') . '/' . $template->element . '/'); + $this->mediaElement = $path; + + if (!is_writable($path)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_DIRECTORY_NOT_WRITABLE'), 'error'); + } + + if (is_dir($path)) { + $result = $this->getDirectoryTree($path); + } + + return $result; + } + + /** + * Method to resolve the base folder. + * + * @return string The absolute path for the base. + * + * @since 4.1.0 + */ + private function getBasePath() + { + $app = Factory::getApplication(); + $isMedia = $app->input->getInt('isMedia', 0); + + return $isMedia ? JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element : + JPATH_ROOT . '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element; + } + + /** + * Method to create the templateDetails.xml for the child template + * + * @return boolean true if name is not used, false otherwise + * + * @since 4.1.0 + */ + public function child() + { + $app = Factory::getApplication(); + $template = $this->getTemplate(); + + if (!(array) $template) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); + + return false; + } + + $client = ApplicationHelper::getClientInfo($template->client_id); + $fromPath = Path::clean($client->path . '/templates/' . $template->element . '/templateDetails.xml'); + + // Delete new folder if it exists + $toPath = $this->getState('to_path'); + + if (Folder::exists($toPath)) { + if (!Folder::delete($toPath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); + + return false; + } + } else { + if (!Folder::create($toPath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); + + return false; + } + } + + // Copy the template definition from the parent template + if (!File::copy($fromPath, $toPath . '/templateDetails.xml')) { + return false; + } + + // Check manifest for additional files + $newName = strtolower($this->getState('new_name')); + $template = $this->getTemplate(); + + // Edit XML file + $xmlFile = Path::clean($this->getState('to_path') . '/templateDetails.xml'); + + if (!File::exists($xmlFile)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); + + return false; + } + + try { + $xml = simplexml_load_string(file_get_contents($xmlFile)); + } catch (\Exception $e) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_READ'), 'error'); + + return false; + } + + $user = Factory::getUser(); + unset($xml->languages); + unset($xml->media); + unset($xml->files); + unset($xml->parent); + unset($xml->inheritable); + + // Remove the update parts + unset($xml->update); + unset($xml->updateservers); + + if (isset($xml->creationDate)) { + $xml->creationDate = (new Date('now'))->format('F Y'); + } else { + $xml->addChild('creationDate', (new Date('now'))->format('F Y')); + } + + if (isset($xml->author)) { + $xml->author = $user->name; + } else { + $xml->addChild('author', $user->name); + } + + if (isset($xml->authorEmail)) { + $xml->authorEmail = $user->email; + } else { + $xml->addChild('authorEmail', $user->email); + } + + $files = $xml->addChild('files'); + $files->addChild('filename', 'templateDetails.xml'); + + // Media folder + $media = $xml->addChild('media'); + $media->addAttribute('folder', 'media'); + $media->addAttribute('destination', 'templates/' . ($template->client_id === 0 ? 'site/' : 'administrator/') . $template->element . '_' . $newName); + $media->addChild('folder', 'css'); + $media->addChild('folder', 'js'); + $media->addChild('folder', 'images'); + $media->addChild('folder', 'html'); + $media->addChild('folder', 'scss'); + + $xml->name = $template->element . '_' . $newName; + $xml->inheritable = 0; + $files = $xml->addChild('parent', $template->element); + + $dom = new \DOMDocument(); + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + $dom->loadXML($xml->asXML()); + + $result = File::write($xmlFile, $dom->saveXML()); + + if (!$result) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); + + return false; + } + + // Create an empty media folder structure + if ( + !Folder::create($toPath . '/media') + || !Folder::create($toPath . '/media/css') + || !Folder::create($toPath . '/media/js') + || !Folder::create($toPath . '/media/images') + || !Folder::create($toPath . '/media/html/tinymce') + || !Folder::create($toPath . '/media/scss') + ) { + return false; + } + + return true; + } + + /** + * Method to get the parent template existing styles + * + * @return array array of id,titles of the styles + * + * @since 4.1.3 + */ + public function getAllTemplateStyles() + { + $template = $this->getTemplate(); + + if (empty($template->xmldata->inheritable)) { + return []; + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select($db->quoteName(['id', 'title'])) + ->from($db->quoteName('#__template_styles')) + ->where($db->quoteName('client_id') . ' = :client_id', 'AND') + ->where($db->quoteName('template') . ' = :template') + ->orWhere($db->quoteName('parent') . ' = :parent') + ->bind(':client_id', $template->client_id, ParameterType::INTEGER) + ->bind(':template', $template->element) + ->bind(':parent', $template->element); + + $db->setQuery($query); + + return $db->loadObjectList(); + } + + /** + * Method to copy selected styles to the child template + * + * @return boolean true if name is not used, false otherwise + * + * @since 4.1.3 + */ + public function copyStyles() + { + $app = Factory::getApplication(); + $template = $this->getTemplate(); + $newName = strtolower($this->getState('new_name')); + $applyStyles = $this->getState('stylesToCopy'); + + // Get a db connection. + $db = $this->getDatabase(); + + // Create a new query object. + $query = $db->getQuery(true); + + $query->select($db->quoteName(['title', 'params'])) + ->from($db->quoteName('#__template_styles')) + ->whereIn($db->quoteName('id'), ArrayHelper::toInteger($applyStyles)); + // Reset the query using our newly populated query object. + $db->setQuery($query); + + try { + $parentStyle = $db->loadObjectList(); + } catch (\Exception $e) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND'), 'error'); + + return false; + } + + foreach ($parentStyle as $style) { + $query = $db->getQuery(true); + $styleName = Text::sprintf('COM_TEMPLATES_COPY_CHILD_TEMPLATE_STYLES', ucfirst($template->element . '_' . $newName), $style->title); + + // Insert columns and values + $columns = ['id', 'template', 'client_id', 'home', 'title', 'inheritable', 'parent', 'params']; + $values = [0, $db->quote($template->element . '_' . $newName), (int) $template->client_id, $db->quote('0'), $db->quote($styleName), 0, $db->quote($template->element), $db->quote($style->params)]; + + $query + ->insert($db->quoteName('#__template_styles')) + ->columns($db->quoteName($columns)) + ->values(implode(',', $values)); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\Exception $e) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_READ'), 'error'); + + return false; + } + } + + return true; + } } diff --git a/administrator/components/com_templates/src/Model/TemplatesModel.php b/administrator/components/com_templates/src/Model/TemplatesModel.php index 13ef6e3644480..f258c5e5508e3 100644 --- a/administrator/components/com_templates/src/Model/TemplatesModel.php +++ b/administrator/components/com_templates/src/Model/TemplatesModel.php @@ -1,4 +1,5 @@ client_id); - $item->xmldata = TemplatesHelper::parseXMLTemplateFile($client->path, $item->element); - $num = $this->updated($item->extension_id); - - if ($num) - { - $item->updated = $num; - } - } - - return $items; - } - - /** - * Check if template extension have any updated override. - * - * @param integer $exid Extension id of template. - * - * @return boolean False if records not found/else integer. - * - * @since 4.0.0 - */ - public function updated($exid) - { - $db = $this->getDatabase(); - - // Select the required fields from the table - $query = $db->getQuery(true) - ->select($db->quoteName('template')) - ->from($db->quoteName('#__template_overrides')) - ->where($db->quoteName('extension_id') . ' = :extensionid') - ->where($db->quoteName('state') . ' = 0') - ->bind(':extensionid', $exid, ParameterType::INTEGER); - - // Reset the query. - $db->setQuery($query); - - // Load the results as a list of stdClass objects. - $num = count($db->loadObjectList()); - - if ($num > 0) - { - return $num; - } - - return false; - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.extension_id, a.name, a.element, a.client_id' - ) - ); - $clientId = (int) $this->getState('client_id'); - $query->from($db->quoteName('#__extensions', 'a')) - ->where($db->quoteName('a.client_id') . ' = :clientid') - ->where($db->quoteName('a.enabled') . ' = 1') - ->where($db->quoteName('a.type') . ' = ' . $db->quote('template')) - ->bind(':clientid', $clientId, ParameterType::INTEGER); - - // Filter by search in title. - if ($search = $this->getState('filter.search')) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . StringHelper::strtolower($search) . '%'; - $query->extendWhere( - 'AND', - [ - 'LOWER(' . $db->quoteName('a.element') . ') LIKE :element', - 'LOWER(' . $db->quoteName('a.name') . ') LIKE :name', - ], - 'OR' - ) - ->bind(':element', $search) - ->bind(':name', $search); - } - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.element')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('client_id'); - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.element', $direction = 'asc') - { - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - - // Special case for the client id. - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $clientId = (!in_array($clientId, array (0, 1))) ? 0 : $clientId; - $this->setState('client_id', $clientId); - - // Load the parameters. - $params = ComponentHelper::getParams('com_templates'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'folder', 'a.folder', + 'element', 'a.element', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'state', 'a.state', + 'enabled', 'a.enabled', + 'ordering', 'a.ordering', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Override parent getItems to add extra XML metadata. + * + * @return array + * + * @since 1.6 + */ + public function getItems() + { + $items = parent::getItems(); + + foreach ($items as &$item) { + $client = ApplicationHelper::getClientInfo($item->client_id); + $item->xmldata = TemplatesHelper::parseXMLTemplateFile($client->path, $item->element); + $num = $this->updated($item->extension_id); + + if ($num) { + $item->updated = $num; + } + } + + return $items; + } + + /** + * Check if template extension have any updated override. + * + * @param integer $exid Extension id of template. + * + * @return boolean False if records not found/else integer. + * + * @since 4.0.0 + */ + public function updated($exid) + { + $db = $this->getDatabase(); + + // Select the required fields from the table + $query = $db->getQuery(true) + ->select($db->quoteName('template')) + ->from($db->quoteName('#__template_overrides')) + ->where($db->quoteName('extension_id') . ' = :extensionid') + ->where($db->quoteName('state') . ' = 0') + ->bind(':extensionid', $exid, ParameterType::INTEGER); + + // Reset the query. + $db->setQuery($query); + + // Load the results as a list of stdClass objects. + $num = count($db->loadObjectList()); + + if ($num > 0) { + return $num; + } + + return false; + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.extension_id, a.name, a.element, a.client_id' + ) + ); + $clientId = (int) $this->getState('client_id'); + $query->from($db->quoteName('#__extensions', 'a')) + ->where($db->quoteName('a.client_id') . ' = :clientid') + ->where($db->quoteName('a.enabled') . ' = 1') + ->where($db->quoteName('a.type') . ' = ' . $db->quote('template')) + ->bind(':clientid', $clientId, ParameterType::INTEGER); + + // Filter by search in title. + if ($search = $this->getState('filter.search')) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . StringHelper::strtolower($search) . '%'; + $query->extendWhere( + 'AND', + [ + 'LOWER(' . $db->quoteName('a.element') . ') LIKE :element', + 'LOWER(' . $db->quoteName('a.name') . ') LIKE :name', + ], + 'OR' + ) + ->bind(':element', $search) + ->bind(':name', $search); + } + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.element')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('client_id'); + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.element', $direction = 'asc') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + + // Special case for the client id. + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $clientId = (!in_array($clientId, array (0, 1))) ? 0 : $clientId; + $this->setState('client_id', $clientId); + + // Load the parameters. + $params = ComponentHelper::getParams('com_templates'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } } diff --git a/administrator/components/com_templates/src/Service/HTML/Templates.php b/administrator/components/com_templates/src/Service/HTML/Templates.php index 6d7fd3632d220..1cb528c11ac67 100644 --- a/administrator/components/com_templates/src/Service/HTML/Templates.php +++ b/administrator/components/com_templates/src/Service/HTML/Templates.php @@ -1,4 +1,5 @@ client_id); - - if (!isset($template->xmldata)) - { - $template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); - } - - if ((isset($template->xmldata->inheritable) && (bool) $template->xmldata->inheritable) || isset($template->xmldata->parent)) - { - if (isset($template->xmldata->parent) && (string) $template->xmldata->parent !== '' && file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png')) - { - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) - { - $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); - $html = ''; - } - elseif ((file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_preview.png'))) - { - $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); - $html = ''; - } - else - { - $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); - } - } - elseif (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) - { - $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); - - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) - { - $html = ''; - } - } - else - { - $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); - } - } - elseif (file_exists($client->path . '/templates/' . $template->element . '/template_thumbnail.png')) - { - $html = HTMLHelper::_('image', (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . '/administrator/') . '/templates/' . $template->element . '/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW'), [], false, -1); - - if (file_exists($client->path . '/templates/' . $template->element . '/template_preview.png')) - { - $html = ''; - } - } - else - { - $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); - } - - return $html; - } - - /** - * Renders the html for the modal linked to thumb. - * - * @param string|object $template The name of the template or the template object. - * @param integer $clientId The application client ID the template applies to - * - * @return string The html string - * - * @since 3.4 - * - * @deprecated 5.0 The argument $template should be object and $clientId will be removed - */ - public function thumbModal($template, $clientId = 0) - { - if (is_string($template)) - { - return HTMLHelper::_('image', 'template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW'), [], true, -1); - } - - $html = ''; - $thumb = ''; - $preview = ''; - $client = ApplicationHelper::getClientInfo($template->client_id); - - if (!isset($template->xmldata)) - { - $template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); - } - - if ((isset($template->xmldata->inheritable) && (bool) $template->xmldata->inheritable) || isset($template->xmldata->parent)) - { - if (isset($template->xmldata->parent) && (string) $template->xmldata->parent !== '') - { - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) - { - $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . 'administrator') . 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png'; - - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element. '/images/template_preview.png')) - { - $preview = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . $template->element. '/images/template_preview.png'; - } - } - else - { - $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . 'administrator') . 'media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png'; - - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent. '/images/template_preview.png')) - { - $preview = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent. '/images/template_preview.png'; - } - } - } - elseif (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) - { - $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png'; - - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) - { - $preview = Uri::root(true) . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png'; - } - } - } - elseif (file_exists($client->path . '/templates/' . $template->element . '/template_thumbnail.png')) - { - $thumb = (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . 'administrator') . '/templates/' . $template->element . '/template_thumbnail.png'; - - if (file_exists($client->path . '/templates/' . $template->element . '/template_preview.png')) - { - $preview = (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . '/administrator') . '/templates/' . $template->element . '/template_preview.png'; - } - } - - if ($thumb !== '' && $preview !== '') - { - $footer = ''; - - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - $template->name . '-Modal', - array( - 'title' => Text::sprintf('COM_TEMPLATES_SCREENSHOT', ucfirst($template->name)), - 'height' => '500px', - 'width' => '800px', - 'footer' => $footer, - ), - '
    ' . $template->name . '
    ' - ); - } - - return $html; - } + /** + * Display the thumb for the template. + * + * @param string|object $template The name of the template or the template object. + * @param integer $clientId The application client ID the template applies to + * + * @return string The html string + * + * @since 1.6 + * + * @deprecated 5.0 The argument $template should be object and $clientId will be removed + */ + public function thumb($template, $clientId = 0) + { + if (is_string($template)) { + return HTMLHelper::_('image', 'template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW'), [], true, -1); + } + + $client = ApplicationHelper::getClientInfo($template->client_id); + + if (!isset($template->xmldata)) { + $template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); + } + + if ((isset($template->xmldata->inheritable) && (bool) $template->xmldata->inheritable) || isset($template->xmldata->parent)) { + if (isset($template->xmldata->parent) && (string) $template->xmldata->parent !== '' && file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png')) { + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) { + $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); + $html = ''; + } elseif ((file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_preview.png'))) { + $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); + $html = ''; + } else { + $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); + } + } elseif (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) { + $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); + + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) { + $html = ''; + } + } else { + $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); + } + } elseif (file_exists($client->path . '/templates/' . $template->element . '/template_thumbnail.png')) { + $html = HTMLHelper::_('image', (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . '/administrator/') . '/templates/' . $template->element . '/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW'), [], false, -1); + + if (file_exists($client->path . '/templates/' . $template->element . '/template_preview.png')) { + $html = ''; + } + } else { + $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); + } + + return $html; + } + + /** + * Renders the html for the modal linked to thumb. + * + * @param string|object $template The name of the template or the template object. + * @param integer $clientId The application client ID the template applies to + * + * @return string The html string + * + * @since 3.4 + * + * @deprecated 5.0 The argument $template should be object and $clientId will be removed + */ + public function thumbModal($template, $clientId = 0) + { + if (is_string($template)) { + return HTMLHelper::_('image', 'template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW'), [], true, -1); + } + + $html = ''; + $thumb = ''; + $preview = ''; + $client = ApplicationHelper::getClientInfo($template->client_id); + + if (!isset($template->xmldata)) { + $template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); + } + + if ((isset($template->xmldata->inheritable) && (bool) $template->xmldata->inheritable) || isset($template->xmldata->parent)) { + if (isset($template->xmldata->parent) && (string) $template->xmldata->parent !== '') { + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) { + $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . 'administrator') . 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png'; + + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) { + $preview = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png'; + } + } else { + $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . 'administrator') . 'media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png'; + + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_preview.png')) { + $preview = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_preview.png'; + } + } + } elseif (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) { + $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png'; + + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) { + $preview = Uri::root(true) . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png'; + } + } + } elseif (file_exists($client->path . '/templates/' . $template->element . '/template_thumbnail.png')) { + $thumb = (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . 'administrator') . '/templates/' . $template->element . '/template_thumbnail.png'; + + if (file_exists($client->path . '/templates/' . $template->element . '/template_preview.png')) { + $preview = (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . '/administrator') . '/templates/' . $template->element . '/template_preview.png'; + } + } + + if ($thumb !== '' && $preview !== '') { + $footer = ''; + + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + $template->name . '-Modal', + array( + 'title' => Text::sprintf('COM_TEMPLATES_SCREENSHOT', ucfirst($template->name)), + 'height' => '500px', + 'width' => '800px', + 'footer' => $footer, + ), + '
    ' . $template->name . '
    ' + ); + } + + return $html; + } } diff --git a/administrator/components/com_templates/src/Table/StyleTable.php b/administrator/components/com_templates/src/Table/StyleTable.php index 857dd4f01f3c4..e793ca058638d 100644 --- a/administrator/components/com_templates/src/Table/StyleTable.php +++ b/administrator/components/com_templates/src/Table/StyleTable.php @@ -1,4 +1,5 @@ home == '1') - { - $this->setError(Text::_('COM_TEMPLATES_ERROR_CANNOT_UNSET_DEFAULT_STYLE')); - - return false; - } - - return parent::bind($array, $ignore); - } - - /** - * Overloaded check method to ensure data integrity. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (empty($this->title)) - { - $this->setError(Text::_('COM_TEMPLATES_ERROR_STYLE_REQUIRES_TITLE')); - - return false; - } - - return true; - } - - /** - * Overloaded store method to ensure unicity of default style. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function store($updateNulls = false) - { - if ($this->home != '0') - { - $clientId = (int) $this->client_id; - $query = $this->_db->getQuery(true) - ->update($this->_db->quoteName('#__template_styles')) - ->set($this->_db->quoteName('home') . ' = ' . $this->_db->quote('0')) - ->where($this->_db->quoteName('client_id') . ' = :clientid') - ->where($this->_db->quoteName('home') . ' = :home') - ->bind(':clientid', $clientId, ParameterType::INTEGER) - ->bind(':home', $this->home); - $this->_db->setQuery($query); - $this->_db->execute(); - } - - return parent::store($updateNulls); - } - - /** - * Overloaded store method to unsure existence of a default style for a template. - * - * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function delete($pk = null) - { - $k = $this->_tbl_key; - $pk = is_null($pk) ? $this->$k : $pk; - - if (!is_null($pk)) - { - $clientId = (int) $this->client_id; - $query = $this->_db->getQuery(true) - ->select($this->_db->quoteName('id')) - ->from($this->_db->quoteName('#__template_styles')) - ->where($this->_db->quoteName('client_id') . ' = :clientid') - ->where($this->_db->quoteName('template') . ' = :template') - ->bind(':template', $this->template) - ->bind(':clientid', $clientId, ParameterType::INTEGER); - $this->_db->setQuery($query); - $results = $this->_db->loadColumn(); - - if (count($results) == 1 && $results[0] == $pk) - { - $this->setError(Text::_('COM_TEMPLATES_ERROR_CANNOT_DELETE_LAST_STYLE')); - - return false; - } - } - - return parent::delete($pk); - } + /** + * Constructor + * + * @param DatabaseDriver $db A database connector object + * + * @since 1.6 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__template_styles', 'id', $db); + } + + /** + * Overloaded bind function to pre-process the params. + * + * @param array $array Named array + * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return null|string null if operation was satisfactory, otherwise returns an error + * + * @since 1.6 + */ + public function bind($array, $ignore = '') + { + if (isset($array['params']) && is_array($array['params'])) { + $registry = new Registry($array['params']); + $array['params'] = (string) $registry; + } + + // Verify that the default style is not unset + if ($array['home'] == '0' && $this->home == '1') { + $this->setError(Text::_('COM_TEMPLATES_ERROR_CANNOT_UNSET_DEFAULT_STYLE')); + + return false; + } + + return parent::bind($array, $ignore); + } + + /** + * Overloaded check method to ensure data integrity. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (empty($this->title)) { + $this->setError(Text::_('COM_TEMPLATES_ERROR_STYLE_REQUIRES_TITLE')); + + return false; + } + + return true; + } + + /** + * Overloaded store method to ensure unicity of default style. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function store($updateNulls = false) + { + if ($this->home != '0') { + $clientId = (int) $this->client_id; + $query = $this->_db->getQuery(true) + ->update($this->_db->quoteName('#__template_styles')) + ->set($this->_db->quoteName('home') . ' = ' . $this->_db->quote('0')) + ->where($this->_db->quoteName('client_id') . ' = :clientid') + ->where($this->_db->quoteName('home') . ' = :home') + ->bind(':clientid', $clientId, ParameterType::INTEGER) + ->bind(':home', $this->home); + $this->_db->setQuery($query); + $this->_db->execute(); + } + + return parent::store($updateNulls); + } + + /** + * Overloaded store method to unsure existence of a default style for a template. + * + * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function delete($pk = null) + { + $k = $this->_tbl_key; + $pk = is_null($pk) ? $this->$k : $pk; + + if (!is_null($pk)) { + $clientId = (int) $this->client_id; + $query = $this->_db->getQuery(true) + ->select($this->_db->quoteName('id')) + ->from($this->_db->quoteName('#__template_styles')) + ->where($this->_db->quoteName('client_id') . ' = :clientid') + ->where($this->_db->quoteName('template') . ' = :template') + ->bind(':template', $this->template) + ->bind(':clientid', $clientId, ParameterType::INTEGER); + $this->_db->setQuery($query); + $results = $this->_db->loadColumn(); + + if (count($results) == 1 && $results[0] == $pk) { + $this->setError(Text::_('COM_TEMPLATES_ERROR_CANNOT_DELETE_LAST_STYLE')); + + return false; + } + } + + return parent::delete($pk); + } } diff --git a/administrator/components/com_templates/src/View/Style/HtmlView.php b/administrator/components/com_templates/src/View/Style/HtmlView.php index 8f315cd13deed..75aefc9766627 100644 --- a/administrator/components/com_templates/src/View/Style/HtmlView.php +++ b/administrator/components/com_templates/src/View/Style/HtmlView.php @@ -1,4 +1,5 @@ item = $this->get('Item'); - $this->state = $this->get('State'); - $this->form = $this->get('Form'); - $this->canDo = ContentHelper::getActions('com_templates'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $isNew = ($this->item->id == 0); - $canDo = $this->canDo; - - ToolbarHelper::title( - $isNew ? Text::_('COM_TEMPLATES_MANAGER_ADD_STYLE') - : Text::_('COM_TEMPLATES_MANAGER_EDIT_STYLE'), 'paint-brush thememanager' - ); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('style.apply'); - $toolbarButtons[] = ['save', 'style.save']; - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'style.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('style.cancel'); - } - else - { - ToolbarHelper::cancel('style.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - - // Get the help information for the template item. - $lang = Factory::getLanguage(); - $help = $this->get('Help'); - - if ($lang->hasKey($help->url)) - { - $debug = $lang->setDebug(false); - $url = Text::_($help->url); - $lang->setDebug($debug); - } - else - { - $url = null; - } - - ToolbarHelper::help($help->key, false, $url); - } + /** + * The CMSObject (on success, false on failure) + * + * @var CMSObject + */ + protected $item; + + /** + * The form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * + * @since 4.0.0 + */ + protected $canDo; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + $this->form = $this->get('Form'); + $this->canDo = ContentHelper::getActions('com_templates'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $isNew = ($this->item->id == 0); + $canDo = $this->canDo; + + ToolbarHelper::title( + $isNew ? Text::_('COM_TEMPLATES_MANAGER_ADD_STYLE') + : Text::_('COM_TEMPLATES_MANAGER_EDIT_STYLE'), + 'paint-brush thememanager' + ); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('style.apply'); + $toolbarButtons[] = ['save', 'style.save']; + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'style.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('style.cancel'); + } else { + ToolbarHelper::cancel('style.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + + // Get the help information for the template item. + $lang = Factory::getLanguage(); + $help = $this->get('Help'); + + if ($lang->hasKey($help->url)) { + $debug = $lang->setDebug(false); + $url = Text::_($help->url); + $lang->setDebug($debug); + } else { + $url = null; + } + + ToolbarHelper::help($help->key, false, $url); + } } diff --git a/administrator/components/com_templates/src/View/Style/JsonView.php b/administrator/components/com_templates/src/View/Style/JsonView.php index edc7dd229f897..64025223d5dc1 100644 --- a/administrator/components/com_templates/src/View/Style/JsonView.php +++ b/administrator/components/com_templates/src/View/Style/JsonView.php @@ -1,4 +1,5 @@ item = $this->get('Item'); - } - catch (\Exception $e) - { - $app = Factory::getApplication(); - $app->enqueueMessage($e->getMessage(), 'error'); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + * + * @since 1.6 + */ + public function display($tpl = null) + { + try { + $this->item = $this->get('Item'); + } catch (\Exception $e) { + $app = Factory::getApplication(); + $app->enqueueMessage($e->getMessage(), 'error'); - return false; - } + return false; + } - $paramsList = $this->item->getProperties(); + $paramsList = $this->item->getProperties(); - unset($paramsList['xml']); + unset($paramsList['xml']); - $paramsList = json_encode($paramsList); + $paramsList = json_encode($paramsList); - return $paramsList; - } + return $paramsList; + } } diff --git a/administrator/components/com_templates/src/View/Styles/HtmlView.php b/administrator/components/com_templates/src/View/Styles/HtmlView.php index 15783e618dfa4..b4df510fcb7a8 100644 --- a/administrator/components/com_templates/src/View/Styles/HtmlView.php +++ b/administrator/components/com_templates/src/View/Styles/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->total = $this->get('Total'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->preview = ComponentHelper::getParams('com_templates')->get('template_positions_display'); - - // Remove the menu item filter for administrator styles. - if ((int) $this->state->get('client_id') !== 0) - { - unset($this->activeFilters['menuitem']); - $this->filterForm->removeField('menuitem', 'filter'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_templates'); - $clientId = (int) $this->get('State')->get('client_id'); - - // Add a shortcut to the templates list view. - ToolbarHelper::link('index.php?option=com_templates&view=templates&client_id=' . $clientId, 'COM_TEMPLATES_MANAGER_TEMPLATES', 'icon-code thememanager'); - - // Set the title. - if ($clientId === 1) - { - ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_STYLES_ADMIN'), 'paint-brush thememanager'); - } - else - { - ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_STYLES_SITE'), 'paint-brush thememanager'); - } - - if ($canDo->get('core.edit.state')) - { - ToolbarHelper::makeDefault('styles.setDefault', 'COM_TEMPLATES_TOOLBAR_SET_HOME'); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.create')) - { - ToolbarHelper::custom('styles.duplicate', 'copy', '', 'JTOOLBAR_DUPLICATE', true); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.delete')) - { - ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'styles.delete', 'JTOOLBAR_DELETE'); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_templates'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Templates:_Styles'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is the parameter enabled to show template positions in the frontend? + * + * @var boolean + * @since 4.0.0 + */ + public $preview; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->total = $this->get('Total'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->preview = ComponentHelper::getParams('com_templates')->get('template_positions_display'); + + // Remove the menu item filter for administrator styles. + if ((int) $this->state->get('client_id') !== 0) { + unset($this->activeFilters['menuitem']); + $this->filterForm->removeField('menuitem', 'filter'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_templates'); + $clientId = (int) $this->get('State')->get('client_id'); + + // Add a shortcut to the templates list view. + ToolbarHelper::link('index.php?option=com_templates&view=templates&client_id=' . $clientId, 'COM_TEMPLATES_MANAGER_TEMPLATES', 'icon-code thememanager'); + + // Set the title. + if ($clientId === 1) { + ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_STYLES_ADMIN'), 'paint-brush thememanager'); + } else { + ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_STYLES_SITE'), 'paint-brush thememanager'); + } + + if ($canDo->get('core.edit.state')) { + ToolbarHelper::makeDefault('styles.setDefault', 'COM_TEMPLATES_TOOLBAR_SET_HOME'); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.create')) { + ToolbarHelper::custom('styles.duplicate', 'copy', '', 'JTOOLBAR_DUPLICATE', true); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.delete')) { + ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'styles.delete', 'JTOOLBAR_DELETE'); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_templates'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Templates:_Styles'); + } } diff --git a/administrator/components/com_templates/src/View/Template/HtmlView.php b/administrator/components/com_templates/src/View/Template/HtmlView.php index 84992c390493c..792b5a99a84e0 100644 --- a/administrator/components/com_templates/src/View/Template/HtmlView.php +++ b/administrator/components/com_templates/src/View/Template/HtmlView.php @@ -1,4 +1,5 @@ file = $app->input->get('file'); - $this->fileName = InputFilter::getInstance()->clean(base64_decode($this->file), 'string'); - $explodeArray = explode('.', $this->fileName); - $ext = end($explodeArray); - $this->files = $this->get('Files'); - $this->mediaFiles = $this->get('MediaFiles'); - $this->state = $this->get('State'); - $this->template = $this->get('Template'); - $this->preview = $this->get('Preview'); - $this->pluginState = PluginHelper::isEnabled('installer', 'override'); - $this->updatedList = $this->get('UpdatedList'); - $this->styles = $this->get('AllTemplateStyles'); - $this->stylesHTML = ''; - - $params = ComponentHelper::getParams('com_templates'); - $imageTypes = explode(',', $params->get('image_formats')); - $sourceTypes = explode(',', $params->get('source_formats')); - $fontTypes = explode(',', $params->get('font_formats')); - $archiveTypes = explode(',', $params->get('compressed_formats')); - - if (in_array($ext, $sourceTypes)) - { - $this->form = $this->get('Form'); - $this->form->setFieldAttribute('source', 'syntax', $ext); - $this->source = $this->get('Source'); - $this->type = 'file'; - } - elseif (in_array($ext, $imageTypes)) - { - try - { - $this->image = $this->get('Image'); - $this->type = 'image'; - } - catch (\RuntimeException $exception) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_GD_EXTENSION_NOT_AVAILABLE')); - $this->type = 'home'; - } - } - elseif (in_array($ext, $fontTypes)) - { - $this->font = $this->get('Font'); - $this->type = 'font'; - } - elseif (in_array($ext, $archiveTypes)) - { - $this->archive = $this->get('Archive'); - $this->type = 'archive'; - } - else - { - $this->type = 'home'; - } - - $this->overridesList = $this->get('OverridesList'); - $this->id = $this->state->get('extension.id'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - $app->enqueueMessage(implode("\n", $errors)); - - return false; - } - - $this->addToolbar(); - - if (!$this->getCurrentUser()->authorise('core.admin')) - { - $this->setLayout('readonly'); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @since 1.6 - * - * @return void - */ - protected function addToolbar() - { - $app = Factory::getApplication(); - $user = $this->getCurrentUser(); - $app->input->set('hidemainmenu', true); - - // User is global SuperUser - $isSuperUser = $user->authorise('core.admin'); - - // Get the toolbar object instance - $bar = Toolbar::getInstance('toolbar'); - $explodeArray = explode('.', $this->fileName); - $ext = end($explodeArray); - - ToolbarHelper::title(Text::sprintf('COM_TEMPLATES_MANAGER_VIEW_TEMPLATE', ucfirst($this->template->name)), 'icon-code thememanager'); - - // Only show file edit buttons for global SuperUser - if ($isSuperUser) - { - // Add an Apply and save button - if ($this->type === 'file') - { - ToolbarHelper::apply('template.apply'); - ToolbarHelper::save('template.save'); - } - // Add a Crop and Resize button - elseif ($this->type === 'image') - { - ToolbarHelper::custom('template.cropImage', 'icon-crop', '', 'COM_TEMPLATES_BUTTON_CROP', false); - ToolbarHelper::modal('resizeModal', 'icon-expand', 'COM_TEMPLATES_BUTTON_RESIZE'); - } - // Add an extract button - elseif ($this->type === 'archive') - { - ToolbarHelper::custom('template.extractArchive', 'chevron-down', '', 'COM_TEMPLATES_BUTTON_EXTRACT_ARCHIVE', false); - } - // Add a copy/child template button - elseif ($this->type === 'home') - { - if (isset($this->template->xmldata->inheritable) && (string) $this->template->xmldata->inheritable === '1') - { - ToolbarHelper::modal('childModal', 'icon-copy', 'COM_TEMPLATES_BUTTON_TEMPLATE_CHILD', false); - } - elseif (!isset($this->template->xmldata->parent) || $this->template->xmldata->parent == '') - { - ToolbarHelper::modal('copyModal', 'icon-copy', 'COM_TEMPLATES_BUTTON_COPY_TEMPLATE', false); - } - } - } - - // Add a Template preview button - if ($this->type === 'home') - { - $client = (int) $this->preview->client_id === 1 ? 'administrator/' : ''; - $bar->linkButton('preview') - ->icon('icon-image') - ->text('COM_TEMPLATES_BUTTON_PREVIEW') - ->url(Uri::root() . $client . 'index.php?tp=1&templateStyle=' . $this->preview->id) - ->attributes(['target' => '_new']); - } - - // Only show file manage buttons for global SuperUser - if ($isSuperUser) - { - if ($this->type === 'home') - { - // Add Manage folders button - ToolbarHelper::modal('folderModal', 'icon-folder icon white', 'COM_TEMPLATES_BUTTON_FOLDERS'); - - // Add a new file button - ToolbarHelper::modal('fileModal', 'icon-file', 'COM_TEMPLATES_BUTTON_FILE'); - } - else - { - // Add a Rename file Button - ToolbarHelper::modal('renameModal', 'icon-sync', 'COM_TEMPLATES_BUTTON_RENAME_FILE'); - - // Add a Delete file Button - ToolbarHelper::modal('deleteModal', 'icon-times', 'COM_TEMPLATES_BUTTON_DELETE_FILE', 'btn-danger'); - } - } - - if (count($this->updatedList) !== 0 && $this->pluginState) - { - ToolbarHelper::custom('template.deleteOverrideHistory', 'times', '', 'COM_TEMPLATES_BUTTON_DELETE_LIST_ENTRY', true, 'updateForm'); - } - - if ($this->type === 'home') - { - ToolbarHelper::cancel('template.cancel', 'JTOOLBAR_CLOSE'); - } - else - { - ToolbarHelper::cancel('template.close', 'COM_TEMPLATES_BUTTON_CLOSE_FILE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Templates:_Customise'); - } - - /** - * Method for creating the collapsible tree. - * - * @param array $array The value of the present node for recursion - * - * @return string - * - * @note Uses recursion - * @since 3.2 - */ - protected function directoryTree($array) - { - $temp = $this->files; - $this->files = $array; - $txt = $this->loadTemplate('tree'); - $this->files = $temp; - - return $txt; - } - - /** - * Method for listing the folder tree in modals. - * - * @param array $array The value of the present node for recursion - * - * @return string - * - * @note Uses recursion - * @since 3.2 - */ - protected function folderTree($array) - { - $temp = $this->files; - $this->files = $array; - $txt = $this->loadTemplate('folders'); - $this->files = $temp; - - return $txt; - } - - /** - * Method for creating the collapsible tree. - * - * @param array $array The value of the present node for recursion - * - * @return string - * - * @note Uses recursion - * @since 4.1.0 - */ - protected function mediaTree($array) - { - $temp = $this->mediaFiles; - $this->mediaFiles = $array; - $txt = $this->loadTemplate('tree_media'); - $this->mediaFiles = $temp; - - return $txt; - } - - /** - * Method for listing the folder tree in modals. - * - * @param array $array The value of the present node for recursion - * - * @return string - * - * @note Uses recursion - * @since 4.1.0 - */ - protected function mediaFolderTree($array) - { - $temp = $this->mediaFiles; - $this->mediaFiles = $array; - $txt = $this->loadTemplate('media_folders'); - $this->mediaFiles = $temp; - - return $txt; - } + /** + * The Model state + * + * @var CMSObject + */ + protected $state; + + /** + * The template details + * + * @var \stdClass|false + */ + protected $template; + + /** + * For loading the source form + * + * @var Form + */ + protected $form; + + /** + * For loading source file contents + * + * @var array + */ + protected $source; + + /** + * Extension id + * + * @var integer + */ + protected $id; + + /** + * Encrypted file path + * + * @var string + */ + protected $file; + + /** + * List of available overrides + * + * @var array + */ + protected $overridesList; + + /** + * Name of the present file + * + * @var string + */ + protected $fileName; + + /** + * Type of the file - image, source, font + * + * @var string + */ + protected $type; + + /** + * For loading image information + * + * @var array + */ + protected $image; + + /** + * Template id for showing preview button + * + * @var \stdClass + */ + protected $preview; + + /** + * For loading font information + * + * @var array + */ + protected $font; + + /** + * A nested array containing list of files and folders + * + * @var array + */ + protected $files; + + /** + * An array containing a list of compressed files + * + * @var array + */ + protected $archive; + + /** + * The state of installer override plugin. + * + * @var array + * + * @since 4.0.0 + */ + protected $pluginState; + + /** + * A nested array containing list of files and folders in the media folder + * + * @var array + * + * @since 4.1.0 + */ + protected $mediaFiles; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void|boolean + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $this->file = $app->input->get('file'); + $this->fileName = InputFilter::getInstance()->clean(base64_decode($this->file), 'string'); + $explodeArray = explode('.', $this->fileName); + $ext = end($explodeArray); + $this->files = $this->get('Files'); + $this->mediaFiles = $this->get('MediaFiles'); + $this->state = $this->get('State'); + $this->template = $this->get('Template'); + $this->preview = $this->get('Preview'); + $this->pluginState = PluginHelper::isEnabled('installer', 'override'); + $this->updatedList = $this->get('UpdatedList'); + $this->styles = $this->get('AllTemplateStyles'); + $this->stylesHTML = ''; + + $params = ComponentHelper::getParams('com_templates'); + $imageTypes = explode(',', $params->get('image_formats')); + $sourceTypes = explode(',', $params->get('source_formats')); + $fontTypes = explode(',', $params->get('font_formats')); + $archiveTypes = explode(',', $params->get('compressed_formats')); + + if (in_array($ext, $sourceTypes)) { + $this->form = $this->get('Form'); + $this->form->setFieldAttribute('source', 'syntax', $ext); + $this->source = $this->get('Source'); + $this->type = 'file'; + } elseif (in_array($ext, $imageTypes)) { + try { + $this->image = $this->get('Image'); + $this->type = 'image'; + } catch (\RuntimeException $exception) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_GD_EXTENSION_NOT_AVAILABLE')); + $this->type = 'home'; + } + } elseif (in_array($ext, $fontTypes)) { + $this->font = $this->get('Font'); + $this->type = 'font'; + } elseif (in_array($ext, $archiveTypes)) { + $this->archive = $this->get('Archive'); + $this->type = 'archive'; + } else { + $this->type = 'home'; + } + + $this->overridesList = $this->get('OverridesList'); + $this->id = $this->state->get('extension.id'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + $app->enqueueMessage(implode("\n", $errors)); + + return false; + } + + $this->addToolbar(); + + if (!$this->getCurrentUser()->authorise('core.admin')) { + $this->setLayout('readonly'); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @since 1.6 + * + * @return void + */ + protected function addToolbar() + { + $app = Factory::getApplication(); + $user = $this->getCurrentUser(); + $app->input->set('hidemainmenu', true); + + // User is global SuperUser + $isSuperUser = $user->authorise('core.admin'); + + // Get the toolbar object instance + $bar = Toolbar::getInstance('toolbar'); + $explodeArray = explode('.', $this->fileName); + $ext = end($explodeArray); + + ToolbarHelper::title(Text::sprintf('COM_TEMPLATES_MANAGER_VIEW_TEMPLATE', ucfirst($this->template->name)), 'icon-code thememanager'); + + // Only show file edit buttons for global SuperUser + if ($isSuperUser) { + // Add an Apply and save button + if ($this->type === 'file') { + ToolbarHelper::apply('template.apply'); + ToolbarHelper::save('template.save'); + } + // Add a Crop and Resize button + elseif ($this->type === 'image') { + ToolbarHelper::custom('template.cropImage', 'icon-crop', '', 'COM_TEMPLATES_BUTTON_CROP', false); + ToolbarHelper::modal('resizeModal', 'icon-expand', 'COM_TEMPLATES_BUTTON_RESIZE'); + } + // Add an extract button + elseif ($this->type === 'archive') { + ToolbarHelper::custom('template.extractArchive', 'chevron-down', '', 'COM_TEMPLATES_BUTTON_EXTRACT_ARCHIVE', false); + } + // Add a copy/child template button + elseif ($this->type === 'home') { + if (isset($this->template->xmldata->inheritable) && (string) $this->template->xmldata->inheritable === '1') { + ToolbarHelper::modal('childModal', 'icon-copy', 'COM_TEMPLATES_BUTTON_TEMPLATE_CHILD', false); + } elseif (!isset($this->template->xmldata->parent) || $this->template->xmldata->parent == '') { + ToolbarHelper::modal('copyModal', 'icon-copy', 'COM_TEMPLATES_BUTTON_COPY_TEMPLATE', false); + } + } + } + + // Add a Template preview button + if ($this->type === 'home') { + $client = (int) $this->preview->client_id === 1 ? 'administrator/' : ''; + $bar->linkButton('preview') + ->icon('icon-image') + ->text('COM_TEMPLATES_BUTTON_PREVIEW') + ->url(Uri::root() . $client . 'index.php?tp=1&templateStyle=' . $this->preview->id) + ->attributes(['target' => '_new']); + } + + // Only show file manage buttons for global SuperUser + if ($isSuperUser) { + if ($this->type === 'home') { + // Add Manage folders button + ToolbarHelper::modal('folderModal', 'icon-folder icon white', 'COM_TEMPLATES_BUTTON_FOLDERS'); + + // Add a new file button + ToolbarHelper::modal('fileModal', 'icon-file', 'COM_TEMPLATES_BUTTON_FILE'); + } else { + // Add a Rename file Button + ToolbarHelper::modal('renameModal', 'icon-sync', 'COM_TEMPLATES_BUTTON_RENAME_FILE'); + + // Add a Delete file Button + ToolbarHelper::modal('deleteModal', 'icon-times', 'COM_TEMPLATES_BUTTON_DELETE_FILE', 'btn-danger'); + } + } + + if (count($this->updatedList) !== 0 && $this->pluginState) { + ToolbarHelper::custom('template.deleteOverrideHistory', 'times', '', 'COM_TEMPLATES_BUTTON_DELETE_LIST_ENTRY', true, 'updateForm'); + } + + if ($this->type === 'home') { + ToolbarHelper::cancel('template.cancel', 'JTOOLBAR_CLOSE'); + } else { + ToolbarHelper::cancel('template.close', 'COM_TEMPLATES_BUTTON_CLOSE_FILE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Templates:_Customise'); + } + + /** + * Method for creating the collapsible tree. + * + * @param array $array The value of the present node for recursion + * + * @return string + * + * @note Uses recursion + * @since 3.2 + */ + protected function directoryTree($array) + { + $temp = $this->files; + $this->files = $array; + $txt = $this->loadTemplate('tree'); + $this->files = $temp; + + return $txt; + } + + /** + * Method for listing the folder tree in modals. + * + * @param array $array The value of the present node for recursion + * + * @return string + * + * @note Uses recursion + * @since 3.2 + */ + protected function folderTree($array) + { + $temp = $this->files; + $this->files = $array; + $txt = $this->loadTemplate('folders'); + $this->files = $temp; + + return $txt; + } + + /** + * Method for creating the collapsible tree. + * + * @param array $array The value of the present node for recursion + * + * @return string + * + * @note Uses recursion + * @since 4.1.0 + */ + protected function mediaTree($array) + { + $temp = $this->mediaFiles; + $this->mediaFiles = $array; + $txt = $this->loadTemplate('tree_media'); + $this->mediaFiles = $temp; + + return $txt; + } + + /** + * Method for listing the folder tree in modals. + * + * @param array $array The value of the present node for recursion + * + * @return string + * + * @note Uses recursion + * @since 4.1.0 + */ + protected function mediaFolderTree($array) + { + $temp = $this->mediaFiles; + $this->mediaFiles = $array; + $txt = $this->loadTemplate('media_folders'); + $this->mediaFiles = $temp; + + return $txt; + } } diff --git a/administrator/components/com_templates/src/View/Templates/HtmlView.php b/administrator/components/com_templates/src/View/Templates/HtmlView.php index fbbed69c8023c..ab6b28838445f 100644 --- a/administrator/components/com_templates/src/View/Templates/HtmlView.php +++ b/administrator/components/com_templates/src/View/Templates/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->total = $this->get('Total'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->preview = ComponentHelper::getParams('com_templates')->get('template_positions_display'); - $this->file = base64_encode('home'); - $this->pluginState = PluginHelper::isEnabled('installer', 'override'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_templates'); - $clientId = (int) $this->get('State')->get('client_id'); - - // Add a shortcut to the styles list view. - ToolbarHelper::link('index.php?option=com_templates&view=styles&client_id=' . $clientId, 'COM_TEMPLATES_MANAGER_STYLES_BUTTON', 'brush thememanager'); - - // Set the title. - if ($clientId === 1) - { - ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_TEMPLATES_ADMIN'), 'icon-code thememanager'); - } - else - { - ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_TEMPLATES_SITE'), 'icon-code thememanager'); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_templates'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Templates:_Templates'); - } + /** + * The list of templates + * + * @var array + * @since 1.6 + */ + protected $items; + + /** + * The pagination object + * + * @var object + * @since 1.6 + */ + protected $pagination; + + /** + * The model state + * + * @var object + * @since 1.6 + */ + protected $state; + + /** + * @var string + * @since 3.2 + */ + protected $file; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is the parameter enabled to show template positions in the frontend? + * + * @var boolean + * @since 4.0.0 + */ + public $preview; + + /** + * The state of installer override plugin. + * + * @var array + * + * @since 4.0.0 + */ + protected $pluginState; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->total = $this->get('Total'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->preview = ComponentHelper::getParams('com_templates')->get('template_positions_display'); + $this->file = base64_encode('home'); + $this->pluginState = PluginHelper::isEnabled('installer', 'override'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_templates'); + $clientId = (int) $this->get('State')->get('client_id'); + + // Add a shortcut to the styles list view. + ToolbarHelper::link('index.php?option=com_templates&view=styles&client_id=' . $clientId, 'COM_TEMPLATES_MANAGER_STYLES_BUTTON', 'brush thememanager'); + + // Set the title. + if ($clientId === 1) { + ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_TEMPLATES_ADMIN'), 'icon-code thememanager'); + } else { + ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_TEMPLATES_SITE'), 'icon-code thememanager'); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_templates'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Templates:_Templates'); + } } diff --git a/administrator/components/com_templates/tmpl/style/edit.php b/administrator/components/com_templates/tmpl/style/edit.php index f0aa6a48d4c67..92103a45ea775 100644 --- a/administrator/components/com_templates/tmpl/style/edit.php +++ b/administrator/components/com_templates/tmpl/style/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $this->useCoreUI = true; @@ -27,90 +28,90 @@
    - - -
    - 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - - -
    -
    -

    - item->template); ?> -

    -
    - - item->client_id == 0 ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); ?> - -
    -
    -

    item->xml->description); ?>

    - fieldset = 'description'; - $description = LayoutHelper::render('joomla.edit.fieldset', $this); - ?> - -

    - - - -

    - -
    - fieldset = 'basic'; - $html = LayoutHelper::render('joomla.edit.fieldset', $this); - echo $html ? '
    ' . $html : ''; - ?> -
    -
    - fields = array( - 'home', - 'client_id', - 'template' - ); - ?> - - form->renderField('inheritable'); ?> - form->renderField('parent'); ?> -
    -
    - - - - -
    - -
    - -
    -
    - - - - fieldsets = array(); - $this->ignore_fieldsets = array('basic', 'description'); - echo LayoutHelper::render('joomla.edit.params', $this); - ?> - - authorise('core.edit', 'com_menus') && $this->item->client_id == 0 && $this->canDo->get('core.edit.state')) : ?> - -
    - -
    - loadTemplate('assignment'); ?> -
    -
    - - - - - - - -
    + + +
    + 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + + +
    +
    +

    + item->template); ?> +

    +
    + + item->client_id == 0 ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); ?> + +
    +
    +

    item->xml->description); ?>

    + fieldset = 'description'; + $description = LayoutHelper::render('joomla.edit.fieldset', $this); + ?> + +

    + + + +

    + +
    + fieldset = 'basic'; + $html = LayoutHelper::render('joomla.edit.fieldset', $this); + echo $html ? '
    ' . $html : ''; + ?> +
    +
    + fields = array( + 'home', + 'client_id', + 'template' + ); + ?> + + form->renderField('inheritable'); ?> + form->renderField('parent'); ?> +
    +
    + + + + +
    + +
    + +
    +
    + + + + fieldsets = array(); + $this->ignore_fieldsets = array('basic', 'description'); + echo LayoutHelper::render('joomla.edit.params', $this); + ?> + + authorise('core.edit', 'com_menus') && $this->item->client_id == 0 && $this->canDo->get('core.edit.state')) : ?> + +
    + +
    + loadTemplate('assignment'); ?> +
    +
    + + + + + + + +
    diff --git a/administrator/components/com_templates/tmpl/style/edit_assignment.php b/administrator/components/com_templates/tmpl/style/edit_assignment.php index b0a03841f953e..bd3cae0f46d2d 100644 --- a/administrator/components/com_templates/tmpl/style/edit_assignment.php +++ b/administrator/components/com_templates/tmpl/style/edit_assignment.php @@ -1,4 +1,5 @@
    - +
    diff --git a/administrator/components/com_templates/tmpl/styles/default.php b/administrator/components/com_templates/tmpl/styles/default.php index a610932134949..371221698e528 100644 --- a/administrator/components/com_templates/tmpl/styles/default.php +++ b/administrator/components/com_templates/tmpl/styles/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $clientId = (int) $this->state->get('client_id', 0); @@ -27,127 +28,127 @@ $listDirn = $this->escape($this->state->get('list.direction')); ?>
    -
    -
    -
    - $this, 'options' => array('selectorFieldName' => 'client_id'))); ?> - total > 0) : ?> - - - - - - - - - - - - - - - - - items as $i => $item) : - $canCreate = $user->authorise('core.create', 'com_templates'); - $canEdit = $user->authorise('core.edit', 'com_templates'); - $canChange = $user->authorise('core.edit.state', 'com_templates'); - ?> - - - - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - - - -
    - id, false, 'cid', 'cb', $item->title); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - - - preview) : ?> - client_id === 1 ? 'administrator' : 'site'; ?> - - - - - - - - - home == '0' || $item->home == '1') : ?> - home != '0', $i, 'styles.', $canChange && $item->home != '1'); ?> - - - image) : ?> - image . '.gif', $item->language_title, array('title' => Text::sprintf('COM_TEMPLATES_GRID_UNSET_LANGUAGE', $item->language_title)), true); ?> - - home; ?> - - - - image) : ?> - image . '.gif', $item->language_title, array('title' => $item->language_title), true); ?> - - home; ?> - - - - home == '1') : ?> - - home != '0' && $item->home != '1') : ?> - escape($item->language_title)); ?> - assigned > 0) : ?> - escape($item->assigned)); ?> - - - - - - escape($item->template)); ?> - - - id; ?> -
    +
    +
    +
    + $this, 'options' => array('selectorFieldName' => 'client_id'))); ?> + total > 0) : ?> + + + + + + + + + + + + + + + + + items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_templates'); + $canEdit = $user->authorise('core.edit', 'com_templates'); + $canChange = $user->authorise('core.edit.state', 'com_templates'); + ?> + + + + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + + + +
    + id, false, 'cid', 'cb', $item->title); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + + + preview) : ?> + client_id === 1 ? 'administrator' : 'site'; ?> + + + + + + + + + home == '0' || $item->home == '1') : ?> + home != '0', $i, 'styles.', $canChange && $item->home != '1'); ?> + + + image) : ?> + image . '.gif', $item->language_title, array('title' => Text::sprintf('COM_TEMPLATES_GRID_UNSET_LANGUAGE', $item->language_title)), true); ?> + + home; ?> + + + + image) : ?> + image . '.gif', $item->language_title, array('title' => $item->language_title), true); ?> + + home; ?> + + + + home == '1') : ?> + + home != '0' && $item->home != '1') : ?> + escape($item->language_title)); ?> + assigned > 0) : ?> + escape($item->assigned)); ?> + + + + + + escape($item->template)); ?> + + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
    -
    -
    + + + +
    +
    +
    diff --git a/administrator/components/com_templates/tmpl/template/default.php b/administrator/components/com_templates/tmpl/template/default.php index 4320ad44335b3..7985896073716 100644 --- a/administrator/components/com_templates/tmpl/template/default.php +++ b/administrator/components/com_templates/tmpl/template/default.php @@ -1,4 +1,5 @@ useScript('form.validate') - ->useScript('keepalive') - ->useScript('diff') - ->useScript('com_templates.admin-template-compare') - ->useScript('com_templates.admin-template-toggle-switch'); + ->useScript('keepalive') + ->useScript('diff') + ->useScript('com_templates.admin-template-compare') + ->useScript('com_templates.admin-template-toggle-switch'); // No access if not global SuperUser -if (!Factory::getUser()->authorise('core.admin')) -{ - Factory::getApplication()->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'danger'); +if (!Factory::getUser()->authorise('core.admin')) { + Factory::getApplication()->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'danger'); } -if ($this->type == 'image') -{ - $wa->usePreset('cropperjs'); +if ($this->type == 'image') { + $wa->usePreset('cropperjs'); } $wa->useStyle('com_templates.admin-templates') - ->useScript('com_templates.admin-templates'); + ->useScript('com_templates.admin-templates'); -if ($this->type == 'font') -{ - $wa->addInlineStyle(" +if ($this->type == 'font') { + $wa->addInlineStyle(" @font-face { font-family: previewFont; src: url('" . $this->font['address'] . "') @@ -59,411 +57,411 @@ ?>
    - 'editor', 'recall' => true, 'breakpoint' => 768]); ?> - -
    -
    - type == 'file') : ?> -

    get('isMedia', 0) ? '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . str_replace('//', '/', base64_decode($this->file)) : '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element . str_replace('//', '/', base64_decode($this->file))), $this->template->element); ?>

    - - - type == 'image') : ?> -

    image['path'], $this->template->element); ?>

    - - - type == 'font') : ?> -

    font['rel_path'], $this->template->element); ?>

    - - -
    - type == 'file' && !empty($this->source->coreFile)) : ?> -
    -
    - form->renderField('show_core'); ?> - form->renderField('show_diff'); ?> -
    -
    - -
    -
    - -
    -
    - type == 'home') : ?> - -
    - - -

    -

    - - - -

    -
    - type == 'file') : ?> -
    -
    - source->filename); ?> - source->coreFile)) : ?> -

    - -
    - -
    - form->getInput('source'); ?> -
    - - - form->getInput('extension_id'); ?> - form->getInput('filename'); ?> -
    -
    - source->coreFile)) : ?> - source->coreFile); ?> - source->filePath); ?> -
    -

    -
    - form->getInput('core'); ?> -
    -
    -
    -

    -
    -
    -
    -
    -
    -
    - -
    - type == 'archive') : ?> - -
    - - - -
    - type == 'image') : ?> - escape(basename($this->image['address'])); ?> - -
    -
    - - - - - - - - -
    -
    - type == 'font') : ?> -
    -
    -
    -

    H1. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

    -

    H2. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

    -

    H3. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

    -

    H4. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

    -
    H5. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML
    -
    H6. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML
    -

    Bold. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

    -

    Italics. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

    -

    Unordered List

    -
      -
    • Item
    • -
    • Item
    • -
    • Item
      -
        -
      • Item
      • -
      • Item
      • -
      • Item
        -
          -
        • Item
        • -
        • Item
        • -
        • Item
        • -
        -
      • -
      -
    • -
    -

    Ordered List

    -
      -
    1. Item
    2. -
    3. Item
    4. -
    5. Item
      -
        -
      • Item
      • -
      • Item
      • -
      • Item
        -
          -
        • Item
        • -
        • Item
        • -
        • Item
        • -
        -
      • -
      -
    6. -
    - - -
    -
    -
    - -
    -
    -
    - - -
    -
    -
    - -
      - - overridesList['modules'] as $module) : ?> -
    • - path - . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; - ?> - -  name; ?> - -
    • - -
    -
    -
    -
    -
    - -
      - - overridesList['components'] as $key => $value) : ?> -
    • - -   - -
        - -
      • - path - . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; - ?> - -  name; ?> - -
      • - -
      -
    • - -
    -
    -
    -
    -
    - -
      - - overridesList['plugins'] as $key => $group) : ?> -
    • - -   - -
        - -
      • - path - . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; - ?> - - name; ?> - -
      • - -
      -
    • - -
    -
    -
    -
    -
    - -
      - - overridesList['layouts'] as $key => $value) : ?> -
    • - -   - -
        - -
      • - path - . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&' . $token . '&isMedia=' . $input->get('isMedia', 0); - ?> - -  name; ?> - -
      • - -
      -
    • - -
    -
    -
    -
    - + 'editor', 'recall' => true, 'breakpoint' => 768]); ?> + +
    +
    + type == 'file') : ?> +

    get('isMedia', 0) ? '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . str_replace('//', '/', base64_decode($this->file)) : '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element . str_replace('//', '/', base64_decode($this->file))), $this->template->element); ?>

    + + + type == 'image') : ?> +

    image['path'], $this->template->element); ?>

    + + + type == 'font') : ?> +

    font['rel_path'], $this->template->element); ?>

    + + +
    + type == 'file' && !empty($this->source->coreFile)) : ?> +
    +
    + form->renderField('show_core'); ?> + form->renderField('show_diff'); ?> +
    +
    + +
    +
    + +
    +
    + type == 'home') : ?> + +
    + + +

    +

    + + + +

    +
    + type == 'file') : ?> +
    +
    + source->filename); ?> + source->coreFile)) : ?> +

    + +
    + +
    + form->getInput('source'); ?> +
    + + + form->getInput('extension_id'); ?> + form->getInput('filename'); ?> +
    +
    + source->coreFile)) : ?> + source->coreFile); ?> + source->filePath); ?> +
    +

    +
    + form->getInput('core'); ?> +
    +
    +
    +

    +
    +
    +
    +
    +
    +
    + +
    + type == 'archive') : ?> + +
    + + + +
    + type == 'image') : ?> + escape(basename($this->image['address'])); ?> + +
    +
    + + + + + + + + +
    +
    + type == 'font') : ?> +
    +
    +
    +

    H1. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

    +

    H2. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

    +

    H3. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

    +

    H4. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

    +
    H5. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML
    +
    H6. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML
    +

    Bold. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

    +

    Italics. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

    +

    Unordered List

    +
      +
    • Item
    • +
    • Item
    • +
    • Item
      +
        +
      • Item
      • +
      • Item
      • +
      • Item
        +
          +
        • Item
        • +
        • Item
        • +
        • Item
        • +
        +
      • +
      +
    • +
    +

    Ordered List

    +
      +
    1. Item
    2. +
    3. Item
    4. +
    5. Item
      +
        +
      • Item
      • +
      • Item
      • +
      • Item
        +
          +
        • Item
        • +
        • Item
        • +
        • Item
        • +
        +
      • +
      +
    6. +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + +
      + + overridesList['modules'] as $module) : ?> +
    • + path + . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; + ?> + +  name; ?> + +
    • + +
    +
    +
    +
    +
    + +
      + + overridesList['components'] as $key => $value) : ?> +
    • + +   + +
        + +
      • + path + . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; + ?> + +  name; ?> + +
      • + +
      +
    • + +
    +
    +
    +
    +
    + +
      + + overridesList['plugins'] as $key => $group) : ?> +
    • + +   + +
        + +
      • + path + . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; + ?> + + name; ?> + +
      • + +
      +
    • + +
    +
    +
    +
    +
    + +
      + + overridesList['layouts'] as $key => $value) : ?> +
    • + +   + +
        + +
      • + path + . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&' . $token . '&isMedia=' . $input->get('isMedia', 0); + ?> + +  name; ?> + +
      • + +
      +
    • + +
    +
    +
    +
    + - pluginState) : ?> - - loadTemplate('updated_files'); ?> - - + pluginState) : ?> + + loadTemplate('updated_files'); ?> + + - -
    -
    - loadTemplate('description'); ?> -
    -
    - + +
    +
    + loadTemplate('description'); ?> +
    +
    + - + - template->xmldata->inheritable) && (string) $this->template->xmldata->inheritable === '1' ? 'child' : 'copy'; - $copyModalData = array( - 'selector' => $taskName . 'Modal', - 'params' => array( - 'title' => Text::_('COM_TEMPLATES_TEMPLATE_' . strtoupper($taskName)), - 'footer' => $this->loadTemplate('modal_' . $taskName . '_footer') - ), - 'body' => $this->loadTemplate('modal_' . $taskName . '_body') - ); - ?> -
    - - -
    - type != 'home') : ?> - 'renameModal', - 'params' => array( - 'title' => Text::sprintf('COM_TEMPLATES_RENAME_FILE', str_replace('//', '/', $this->fileName)), - 'footer' => $this->loadTemplate('modal_rename_footer') - ), - 'body' => $this->loadTemplate('modal_rename_body') - ); - ?> -
    - - -
    - - type != 'home') : ?> - 'deleteModal', - 'params' => array( - 'title' => Text::_('COM_TEMPLATES_ARE_YOU_SURE'), - 'footer' => $this->loadTemplate('modal_delete_footer') - ), - 'body' => $this->loadTemplate('modal_delete_body') - ); - ?> - - - 'fileModal', - 'params' => array( - 'title' => Text::_('COM_TEMPLATES_NEW_FILE_HEADER'), - 'footer' => $this->loadTemplate('modal_file_footer'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - ), - 'body' => $this->loadTemplate('modal_file_body') - ); - ?> - - 'folderModal', - 'params' => array( - 'title' => Text::_('COM_TEMPLATES_MANAGE_FOLDERS'), - 'footer' => $this->loadTemplate('modal_folder_footer'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - ), - 'body' => $this->loadTemplate('modal_folder_body') - ); - ?> - - type == 'image') : ?> - 'resizeModal', - 'params' => array( - 'title' => Text::_('COM_TEMPLATES_RESIZE_IMAGE'), - 'footer' => $this->loadTemplate('modal_resize_footer') - ), - 'body' => $this->loadTemplate('modal_resize_body') - ); - ?> -
    - - -
    - + template->xmldata->inheritable) && (string) $this->template->xmldata->inheritable === '1' ? 'child' : 'copy'; + $copyModalData = array( + 'selector' => $taskName . 'Modal', + 'params' => array( + 'title' => Text::_('COM_TEMPLATES_TEMPLATE_' . strtoupper($taskName)), + 'footer' => $this->loadTemplate('modal_' . $taskName . '_footer') + ), + 'body' => $this->loadTemplate('modal_' . $taskName . '_body') + ); + ?> +
    + + +
    + type != 'home') : ?> + 'renameModal', + 'params' => array( + 'title' => Text::sprintf('COM_TEMPLATES_RENAME_FILE', str_replace('//', '/', $this->fileName)), + 'footer' => $this->loadTemplate('modal_rename_footer') + ), + 'body' => $this->loadTemplate('modal_rename_body') + ); + ?> +
    + + +
    + + type != 'home') : ?> + 'deleteModal', + 'params' => array( + 'title' => Text::_('COM_TEMPLATES_ARE_YOU_SURE'), + 'footer' => $this->loadTemplate('modal_delete_footer') + ), + 'body' => $this->loadTemplate('modal_delete_body') + ); + ?> + + + 'fileModal', + 'params' => array( + 'title' => Text::_('COM_TEMPLATES_NEW_FILE_HEADER'), + 'footer' => $this->loadTemplate('modal_file_footer'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + ), + 'body' => $this->loadTemplate('modal_file_body') + ); + ?> + + 'folderModal', + 'params' => array( + 'title' => Text::_('COM_TEMPLATES_MANAGE_FOLDERS'), + 'footer' => $this->loadTemplate('modal_folder_footer'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + ), + 'body' => $this->loadTemplate('modal_folder_body') + ); + ?> + + type == 'image') : ?> + 'resizeModal', + 'params' => array( + 'title' => Text::_('COM_TEMPLATES_RESIZE_IMAGE'), + 'footer' => $this->loadTemplate('modal_resize_footer') + ), + 'body' => $this->loadTemplate('modal_resize_body') + ); + ?> +
    + + +
    +
    diff --git a/administrator/components/com_templates/tmpl/template/default_description.php b/administrator/components/com_templates/tmpl/template/default_description.php index 24119159bcbb6..72afa2cbde22c 100644 --- a/administrator/components/com_templates/tmpl/template/default_description.php +++ b/administrator/components/com_templates/tmpl/template/default_description.php @@ -1,4 +1,5 @@
    -
    - template); ?> - template); ?> -
    -

    template->element); ?>

    - template->client_id); ?> -

    template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->path, $this->template->element); ?>

    -

    template->xmldata->get('description')); ?>

    +
    + template); ?> + template); ?> +
    +

    template->element); ?>

    + template->client_id); ?> +

    template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->path, $this->template->element); ?>

    +

    template->xmldata->get('description')); ?>

    diff --git a/administrator/components/com_templates/tmpl/template/default_folders.php b/administrator/components/com_templates/tmpl/template/default_folders.php index 400efb3cb1f6f..1675dab6e7b1f 100644 --- a/administrator/components/com_templates/tmpl/template/default_folders.php +++ b/administrator/components/com_templates/tmpl/template/default_folders.php @@ -1,4 +1,5 @@ diff --git a/administrator/components/com_templates/tmpl/template/default_media_folders.php b/administrator/components/com_templates/tmpl/template/default_media_folders.php index 38ea9c2d0fafa..f3491ab3a8fa8 100644 --- a/administrator/components/com_templates/tmpl/template/default_media_folders.php +++ b/administrator/components/com_templates/tmpl/template/default_media_folders.php @@ -1,4 +1,5 @@ mediaFiles)) -{ - return; +if (!count($this->mediaFiles)) { + return; } ksort($this->mediaFiles, SORT_STRING); ?> diff --git a/administrator/components/com_templates/tmpl/template/default_modal_child_body.php b/administrator/components/com_templates/tmpl/template/default_modal_child_body.php index 3b9b83e6414a4..507b0767372af 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_child_body.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_child_body.php @@ -1,4 +1,5 @@ styles) > 0) -{ - foreach ($this->styles as $style) - { - $options[] = HTMLHelper::_('select.option', $style->id, $style->title, 'value', 'text'); - } +if (count($this->styles) > 0) { + foreach ($this->styles as $style) { + $options[] = HTMLHelper::_('select.option', $style->id, $style->title, 'value', 'text'); + } } $fancySelectData = [ - 'autocomplete' => 'off', - 'autofocus' => false, - 'class' => '', - 'description' => '', - 'disabled' => false, - 'group' => false, - 'id' => 'style_ids', - 'hidden' => false, - 'hint' => '', - 'label' => '', - 'labelclass' => '', - 'onchange' => '', - 'onclick' => '', - 'multiple' => true, - 'pattern' => '', - 'readonly' => false, - 'repeat' => false, - 'required' => false, - 'size' => 4, - 'spellcheck' => false, - 'validate' => '', - 'value' => '0', - 'options' => $options, - 'dataAttributes' => [], - 'dataAttribute' => '', - 'name' => 'style_ids[]', + 'autocomplete' => 'off', + 'autofocus' => false, + 'class' => '', + 'description' => '', + 'disabled' => false, + 'group' => false, + 'id' => 'style_ids', + 'hidden' => false, + 'hint' => '', + 'label' => '', + 'labelclass' => '', + 'onchange' => '', + 'onclick' => '', + 'multiple' => true, + 'pattern' => '', + 'readonly' => false, + 'repeat' => false, + 'required' => false, + 'size' => 4, + 'spellcheck' => false, + 'validate' => '', + 'value' => '0', + 'options' => $options, + 'dataAttributes' => [], + 'dataAttribute' => '', + 'name' => 'style_ids[]', ]; ?>
    -
    -
    -
    -
    - -
    -
    - - - - -
    -
    -
    -
    - -
    -
    - - - - -
    -
    -
    -
    +
    +
    +
    +
    + +
    +
    + + + + +
    +
    +
    +
    + +
    +
    + + + + +
    +
    +
    +
    diff --git a/administrator/components/com_templates/tmpl/template/default_modal_child_footer.php b/administrator/components/com_templates/tmpl/template/default_modal_child_footer.php index 8f0188e2952ec..f1c4dfb6d72f3 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_child_footer.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_child_footer.php @@ -1,4 +1,5 @@
    -
    -
    -
    -
    - -
    -
    - - - - -
    -
    -
    -
    +
    +
    +
    +
    + +
    +
    + + + + +
    +
    +
    +
    diff --git a/administrator/components/com_templates/tmpl/template/default_modal_copy_footer.php b/administrator/components/com_templates/tmpl/template/default_modal_copy_footer.php index 9eeeff74b40a7..17d0f3f17a4de 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_copy_footer.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_copy_footer.php @@ -1,4 +1,5 @@
    -
    -
    -

    fileName)); ?>

    -
    -
    +
    +
    +

    fileName)); ?>

    +
    +
    diff --git a/administrator/components/com_templates/tmpl/template/default_modal_delete_footer.php b/administrator/components/com_templates/tmpl/template/default_modal_delete_footer.php index 89a1b12fcd307..9895aadd69b81 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_delete_footer.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_delete_footer.php @@ -1,4 +1,5 @@ input; ?>
    - - - - - - - + + + + + + +
    diff --git a/administrator/components/com_templates/tmpl/template/default_modal_file_body.php b/administrator/components/com_templates/tmpl/template/default_modal_file_body.php index d3dbea84b4f32..926cd1fec345d 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_file_body.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_file_body.php @@ -19,87 +19,87 @@ $input = Factory::getApplication()->input; ?>
    -
    -
    - -
    -
    -
    - - -
    -
    - - -
    - - - - -
    -
    -
    - - -
    - - - -
    - state->get('params')->get('upload_limit'); ?> - - -
    - type != 'home') : ?> -
    -
    -
    - - - - - -
    - -
    - -
    -
    -
    +
    +
    + +
    +
    +
    + + +
    +
    + + +
    + + + + +
    +
    +
    + + +
    + + + +
    + state->get('params')->get('upload_limit'); ?> + + +
    + type != 'home') : ?> +
    +
    +
    + + + + + +
    + +
    + +
    +
    +
    diff --git a/administrator/components/com_templates/tmpl/template/default_modal_file_footer.php b/administrator/components/com_templates/tmpl/template/default_modal_file_footer.php index 25a77c25295b8..2a58ea613436e 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_file_footer.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_file_footer.php @@ -1,4 +1,5 @@ input; ?>
    -
    -
    - -
    -
    -
    - - - - - -
    - -
    -
    -
    -
    +
    +
    + +
    +
    +
    + + + + + +
    + +
    +
    +
    +
    diff --git a/administrator/components/com_templates/tmpl/template/default_modal_folder_footer.php b/administrator/components/com_templates/tmpl/template/default_modal_folder_footer.php index 3f0c071ceb171..1fbf670f9b7b3 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_folder_footer.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_folder_footer.php @@ -1,4 +1,5 @@ input; ?>
    -
    - - - - - -
    +
    + + + + + +
    diff --git a/administrator/components/com_templates/tmpl/template/default_modal_rename_body.php b/administrator/components/com_templates/tmpl/template/default_modal_rename_body.php index d954ebb64d839..531e6d7f49c57 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_rename_body.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_rename_body.php @@ -1,4 +1,5 @@
    -
    -
    -
    -
    - -
    -
    -
    - - .fileName); ?> -
    -
    -
    -
    -
    +
    +
    +
    +
    + +
    +
    +
    + + .fileName); ?> +
    +
    +
    +
    +
    diff --git a/administrator/components/com_templates/tmpl/template/default_modal_rename_footer.php b/administrator/components/com_templates/tmpl/template/default_modal_rename_footer.php index fd6c82fb5222e..d0bd1b966997b 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_rename_footer.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_rename_footer.php @@ -1,4 +1,5 @@
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    diff --git a/administrator/components/com_templates/tmpl/template/default_modal_resize_footer.php b/administrator/components/com_templates/tmpl/template/default_modal_resize_footer.php index 204efeeaca772..9b4601b2ea1f9 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_resize_footer.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_resize_footer.php @@ -1,4 +1,5 @@ diff --git a/administrator/components/com_templates/tmpl/template/default_tree_media.php b/administrator/components/com_templates/tmpl/template/default_tree_media.php index c32114297f9e1..32c014bda09fd 100644 --- a/administrator/components/com_templates/tmpl/template/default_tree_media.php +++ b/administrator/components/com_templates/tmpl/template/default_tree_media.php @@ -1,4 +1,5 @@ mediaFiles)) -{ - return; +if (!count($this->mediaFiles)) { + return; } ksort($this->mediaFiles, SORT_STRING); ?>
      - mediaFiles as $key => $value) : ?> - - fileName); - $count = 0; + mediaFiles as $key => $value) : ?> + + fileName); + $count = 0; - $keyArrayCount = count($keyArray); + $keyArrayCount = count($keyArray); - if (count($fileArray) >= $keyArrayCount) - { - for ($i = 0; $i < $keyArrayCount; $i++) - { - if ($keyArray[$i] === $fileArray[$i]) - { - $count++; - } - } + if (count($fileArray) >= $keyArrayCount) { + for ($i = 0; $i < $keyArrayCount; $i++) { + if ($keyArray[$i] === $fileArray[$i]) { + $count++; + } + } - if ($count === $keyArrayCount) - { - $class = 'folder show'; - } - else - { - $class = 'folder'; - } - } - else - { - $class = 'folder'; - } + if ($count === $keyArrayCount) { + $class = 'folder show'; + } else { + $class = 'folder'; + } + } else { + $class = 'folder'; + } - ?> -
    • - -  escape(end($explodeArray)); ?> - - mediaTree($value); ?> -
    • - - -
    • - -  escape($value->name); ?> - -
    • - - + ?> +
    • + +  escape(end($explodeArray)); ?> + + mediaTree($value); ?> +
    • + + +
    • + +  escape($value->name); ?> + +
    • + +
    diff --git a/administrator/components/com_templates/tmpl/template/default_updated_files.php b/administrator/components/com_templates/tmpl/template/default_updated_files.php index 385122cbd610f..699340f344efa 100644 --- a/administrator/components/com_templates/tmpl/template/default_updated_files.php +++ b/administrator/components/com_templates/tmpl/template/default_updated_files.php @@ -1,4 +1,5 @@
    -
    -
    - updatedList) !== 0) : ?> - - - - - - - - - - - - - updatedList as $i => $value) : ?> - - - - - - - - - - -
    - - - - - - - - - - - -
    - hash_id, false, 'cid', 'cb', '', 'updateForm'); ?> - - state, $i, 'template.', 1, 'cb', null, null, 'updateForm'); ?> - - hash_id); ?> - - created_date; ?> - 0 ? HTMLHelper::_('date', $created_date, Text::_('DATE_FORMAT_FILTER_DATETIME')) : '-'; ?> - - modified_date)) : ?> - - - modified_date, Text::_('DATE_FORMAT_FILTER_DATETIME')); ?> - - - action; ?> -
    - - - - -
    - - -
    - -
    -
    +
    +
    + updatedList) !== 0) : ?> + + + + + + + + + + + + + updatedList as $i => $value) : ?> + + + + + + + + + + +
    + + + + + + + + + + + +
    + hash_id, false, 'cid', 'cb', '', 'updateForm'); ?> + + state, $i, 'template.', 1, 'cb', null, null, 'updateForm'); ?> + + hash_id); ?> + + created_date; ?> + 0 ? HTMLHelper::_('date', $created_date, Text::_('DATE_FORMAT_FILTER_DATETIME')) : '-'; ?> + + modified_date)) : ?> + + + modified_date, Text::_('DATE_FORMAT_FILTER_DATETIME')); ?> + + + action; ?> +
    + + + + +
    + + +
    + +
    +
    diff --git a/administrator/components/com_templates/tmpl/template/readonly.php b/administrator/components/com_templates/tmpl/template/readonly.php index 157cff97362c5..0d9f62bcce178 100644 --- a/administrator/components/com_templates/tmpl/template/readonly.php +++ b/administrator/components/com_templates/tmpl/template/readonly.php @@ -1,4 +1,5 @@ input; ?>
    - 'description', 'recall' => true, 'breakpoint' => 768]); ?> - -
    -
    - loadTemplate('description'); ?> -
    -
    - - - - + 'description', 'recall' => true, 'breakpoint' => 768]); ?> + +
    +
    + loadTemplate('description'); ?> +
    +
    + + + +
    diff --git a/administrator/components/com_templates/tmpl/templates/default.php b/administrator/components/com_templates/tmpl/templates/default.php index 42959af23cca7..231f899bccd57 100644 --- a/administrator/components/com_templates/tmpl/templates/default.php +++ b/administrator/components/com_templates/tmpl/templates/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -26,117 +27,117 @@ ?>
    -
    -
    -
    - $this, 'options' => array('selectorFieldName' => 'client_id'))); ?> - total > 0) : ?> - - - - - - - - - - pluginState) : ?> - - - - - - items as $i => $item) : ?> - - - - - - - pluginState) : ?> - - - - - -
    - , - , - -
    - - - - - - - - - - - -
    - - - - - name)); ?> -
    - preview) : ?> - client_id === 1 ? 'administrator' : 'site'; ?> - - - - - - - -
    - xmldata->inheritable) && $item->xmldata->inheritable) : ?> -
    - - -
    - - xmldata->parent) && (string) $item->xmldata->parent !== '') : ?> -
    - - xmldata->parent); ?> -
    - -
    - escape($item->xmldata->get('version')); ?> - - escape($item->xmldata->get('creationDate')); ?> - - xmldata->get('author')) : ?> -
    escape($author); ?>
    - - — - - xmldata->get('authorEmail')) : ?> -
    escape($email); ?>
    - - xmldata->get('authorUrl')) : ?> - - -
    - updated)) : ?> - updated); ?> - - - -
    +
    +
    +
    + $this, 'options' => array('selectorFieldName' => 'client_id'))); ?> + total > 0) : ?> + + + + + + + + + + pluginState) : ?> + + + + + + items as $i => $item) : ?> + + + + + + + pluginState) : ?> + + + + + +
    + , + , + +
    + + + + + + + + + + + +
    + + + + + name)); ?> +
    + preview) : ?> + client_id === 1 ? 'administrator' : 'site'; ?> + + + + + + + +
    + xmldata->inheritable) && $item->xmldata->inheritable) : ?> +
    + + +
    + + xmldata->parent) && (string) $item->xmldata->parent !== '') : ?> +
    + + xmldata->parent); ?> +
    + +
    + escape($item->xmldata->get('version')); ?> + + escape($item->xmldata->get('creationDate')); ?> + + xmldata->get('author')) : ?> +
    escape($author); ?>
    + + — + + xmldata->get('authorEmail')) : ?> +
    escape($email); ?>
    + + xmldata->get('authorUrl')) : ?> + + +
    + updated)) : ?> + updated); ?> + + + +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
    -
    -
    + + + +
    +
    +
    diff --git a/administrator/components/com_users/helpers/debug.php b/administrator/components/com_users/helpers/debug.php index d1dc18a415293..b364baa23d1b9 100644 --- a/administrator/components/com_users/helpers/debug.php +++ b/administrator/components/com_users/helpers/debug.php @@ -1,4 +1,5 @@ get('DatabaseDriver'); - $coreMfaPlugins = ['email', 'totp', 'webauthn', 'yubikey']; + /** @var DatabaseDriver $db */ + $db = Factory::getContainer()->get('DatabaseDriver'); + $coreMfaPlugins = ['email', 'totp', 'webauthn', 'yubikey']; - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('multifactorauth')) - ->whereIn($db->quoteName('element'), $coreMfaPlugins, ParameterType::STRING); - $db->setQuery($query); - $db->execute(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('multifactorauth')) + ->whereIn($db->quoteName('element'), $coreMfaPlugins, ParameterType::STRING); + $db->setQuery($query); + $db->execute(); - $url = 'index.php?option=com_plugins&filter[folder]=multifactorauth'; - Factory::getApplication()->redirect($url); + $url = 'index.php?option=com_plugins&filter[folder]=multifactorauth'; + Factory::getApplication()->redirect($url); } diff --git a/administrator/components/com_users/services/provider.php b/administrator/components/com_users/services/provider.php index b2efe40383c44..d444883bf62c0 100644 --- a/administrator/components/com_users/services/provider.php +++ b/administrator/components/com_users/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Users')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Users')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Users')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Users')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Users')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Users')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new UsersComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new UsersComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_users/src/Controller/CallbackController.php b/administrator/components/com_users/src/Controller/CallbackController.php index debe38bdd938a..63ee61ad2162a 100644 --- a/administrator/components/com_users/src/Controller/CallbackController.php +++ b/administrator/components/com_users/src/Controller/CallbackController.php @@ -1,4 +1,5 @@ registerDefaultTask('callback'); - } + $this->registerDefaultTask('callback'); + } - /** - * Implement a callback feature, typically used for OAuth2 authentication - * - * @param bool $cachable Can this view be cached - * @param array|bool $urlparams An array of safe url parameters and their variable types, for valid values see - * {@link JFilterInput::clean()}. - * - * @return void - * @since 4.2.0 - */ - public function callback($cachable = false, $urlparams = false): void - { - $app = $this->app; + /** + * Implement a callback feature, typically used for OAuth2 authentication + * + * @param bool $cachable Can this view be cached + * @param array|bool $urlparams An array of safe url parameters and their variable types, for valid values see + * {@link JFilterInput::clean()}. + * + * @return void + * @since 4.2.0 + */ + public function callback($cachable = false, $urlparams = false): void + { + $app = $this->app; - // Get the Method and make sure it's non-empty - $method = $this->input->getCmd('method', ''); + // Get the Method and make sure it's non-empty + $method = $this->input->getCmd('method', ''); - if (empty($method)) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + if (empty($method)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - PluginHelper::importPlugin('multifactorauth'); + PluginHelper::importPlugin('multifactorauth'); - $event = new Callback($method); - $this->app->getDispatcher()->dispatch($event->getName(), $event); + $event = new Callback($method); + $this->app->getDispatcher()->dispatch($event->getName(), $event); - /** - * The first plugin to handle the request should either redirect or close the application. If we are still here - * no plugin handled the request successfully. Show an error. - */ - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * The first plugin to handle the request should either redirect or close the application. If we are still here + * no plugin handled the request successfully. Show an error. + */ + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } } diff --git a/administrator/components/com_users/src/Controller/CaptiveController.php b/administrator/components/com_users/src/Controller/CaptiveController.php index 384840b98a0f0..a4d9e8ab798e9 100644 --- a/administrator/components/com_users/src/Controller/CaptiveController.php +++ b/administrator/components/com_users/src/Controller/CaptiveController.php @@ -1,4 +1,5 @@ registerTask('captive', 'display'); - } - - /** - * Displays the captive login page - * - * @param boolean $cachable Ignored. This page is never cached. - * @param boolean|array $urlparams Ignored. This page is never cached. - * - * @return void - * @throws Exception - * @since 4.2.0 - */ - public function display($cachable = false, $urlparams = false): void - { - $user = $this->app->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - // Only allow logged in Users - if ($user->guest) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - // Get the view object - $viewLayout = $this->input->get('layout', 'default', 'string'); - $view = $this->getView('Captive', 'html', '', - [ - 'base_path' => $this->basePath, - 'layout' => $viewLayout, - ] - ); - - $view->document = $this->app->getDocument(); - - // If we're already logged in go to the site's home page - if ((int) $this->app->getSession()->get('com_users.mfa_checked', 0) === 1) - { - $url = Route::_('index.php?option=com_users&task=methods.display', false); - - $this->setRedirect($url); - } - - // Pass the model to the view - /** @var CaptiveModel $model */ - $model = $this->getModel('Captive'); - $view->setModel($model, true); - - /** @var BackupcodesModel $codesModel */ - $codesModel = $this->getModel('Backupcodes'); - $view->setModel($codesModel, false); - - try - { - // Suppress all modules on the page except those explicitly allowed - $model->suppressAllModules(); - } - catch (Exception $e) - { - // If we can't kill the modules we can still survive. - } - - // Pass the MFA record ID to the model - $recordId = $this->input->getInt('record_id', null); - $model->setState('record_id', $recordId); - - // Do not go through $this->display() because it overrides the model. - $view->display(); - } - - /** - * Validate the MFA code entered by the user - * - * @param bool $cachable Ignored. This page is never cached. - * @param array $urlparameters Ignored. This page is never cached. - * - * @return void - * @throws Exception - * @since 4.2.0 - */ - public function validate($cachable = false, $urlparameters = []) - { - // CSRF Check - $this->checkToken($this->input->getMethod()); - - // Get the MFA parameters from the request - $recordId = $this->input->getInt('record_id', null); - $code = $this->input->get('code', null, 'raw'); - /** @var CaptiveModel $model */ - $model = $this->getModel('Captive'); - - // Validate the MFA record - $model->setState('record_id', $recordId); - $record = $model->getRecord(); - - if (empty($record)) - { - $event = new NotifyActionLog('onComUsersCaptiveValidateInvalidMethod'); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - - throw new RuntimeException(Text::_('COM_USERS_MFA_INVALID_METHOD'), 500); - } - - // Validate the code - $user = $this->app->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - $event = new Validate($record, $user, $code); - $results = $this->app - ->getDispatcher() - ->dispatch($event->getName(), $event) - ->getArgument('result', []); - - $isValidCode = false; - - if ($record->method === 'backupcodes') - { - /** @var BackupcodesModel $codesModel */ - $codesModel = $this->getModel('Backupcodes'); - $results = [$codesModel->isBackupCode($code, $user)]; - /** - * This is required! Do not remove! - * - * There is a store() call below. It saves the in-memory MFA record to the database. That includes the - * options key which contains the configuration of the Method. For backup codes, these are the actual codes - * you can use. When we check for a backup code validity we also "burn" it, i.e. we remove it from the - * options table and save that to the database. However, this DOES NOT update the $record here. Therefore - * the call to saveRecord() would overwrite the database contents with a record that _includes_ the backup - * code we had just burned. As a result the single use backup codes end up being multiple use. - * - * By doing a getRecord() here, right after we have "burned" any correct backup codes, we resolve this - * issue. The loaded record will reflect the database contents where the options DO NOT include the code we - * just used. Therefore the call to store() will result in the correct database state, i.e. the used backup - * code being removed. - */ - $record = $model->getRecord(); - } - - $isValidCode = array_reduce( - $results, - function (bool $carry, $result) - { - return $carry || boolval($result); - }, - false - ); - - if (!$isValidCode) - { - // The code is wrong. Display an error and go back. - $captiveURL = Route::_('index.php?option=com_users&view=captive&record_id=' . $recordId, false); - $message = Text::_('COM_USERS_MFA_INVALID_CODE'); - $this->setRedirect($captiveURL, $message, 'error'); - - $event = new NotifyActionLog('onComUsersCaptiveValidateFailed', [$record->title]); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - - return; - } - - // Update the Last Used, UA and IP columns - $jNow = Date::getInstance(); + /** + * Public constructor + * + * @param array $config Plugin configuration + * @param MVCFactoryInterface|null $factory MVC Factory for the com_users component + * @param CMSApplication|null $app CMS application object + * @param Input|null $input Joomla CMS input object + * + * @since 4.2.0 + */ + public function __construct(array $config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('captive', 'display'); + } + + /** + * Displays the captive login page + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + public function display($cachable = false, $urlparams = false): void + { + $user = $this->app->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + // Only allow logged in Users + if ($user->guest) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Get the view object + $viewLayout = $this->input->get('layout', 'default', 'string'); + $view = $this->getView( + 'Captive', + 'html', + '', + [ + 'base_path' => $this->basePath, + 'layout' => $viewLayout, + ] + ); + + $view->document = $this->app->getDocument(); + + // If we're already logged in go to the site's home page + if ((int) $this->app->getSession()->get('com_users.mfa_checked', 0) === 1) { + $url = Route::_('index.php?option=com_users&task=methods.display', false); + + $this->setRedirect($url); + } + + // Pass the model to the view + /** @var CaptiveModel $model */ + $model = $this->getModel('Captive'); + $view->setModel($model, true); + + /** @var BackupcodesModel $codesModel */ + $codesModel = $this->getModel('Backupcodes'); + $view->setModel($codesModel, false); + + try { + // Suppress all modules on the page except those explicitly allowed + $model->suppressAllModules(); + } catch (Exception $e) { + // If we can't kill the modules we can still survive. + } + + // Pass the MFA record ID to the model + $recordId = $this->input->getInt('record_id', null); + $model->setState('record_id', $recordId); + + // Do not go through $this->display() because it overrides the model. + $view->display(); + } + + /** + * Validate the MFA code entered by the user + * + * @param bool $cachable Ignored. This page is never cached. + * @param array $urlparameters Ignored. This page is never cached. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + public function validate($cachable = false, $urlparameters = []) + { + // CSRF Check + $this->checkToken($this->input->getMethod()); + + // Get the MFA parameters from the request + $recordId = $this->input->getInt('record_id', null); + $code = $this->input->get('code', null, 'raw'); + /** @var CaptiveModel $model */ + $model = $this->getModel('Captive'); + + // Validate the MFA record + $model->setState('record_id', $recordId); + $record = $model->getRecord(); + + if (empty($record)) { + $event = new NotifyActionLog('onComUsersCaptiveValidateInvalidMethod'); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + throw new RuntimeException(Text::_('COM_USERS_MFA_INVALID_METHOD'), 500); + } + + // Validate the code + $user = $this->app->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + $event = new Validate($record, $user, $code); + $results = $this->app + ->getDispatcher() + ->dispatch($event->getName(), $event) + ->getArgument('result', []); + + $isValidCode = false; + + if ($record->method === 'backupcodes') { + /** @var BackupcodesModel $codesModel */ + $codesModel = $this->getModel('Backupcodes'); + $results = [$codesModel->isBackupCode($code, $user)]; + /** + * This is required! Do not remove! + * + * There is a store() call below. It saves the in-memory MFA record to the database. That includes the + * options key which contains the configuration of the Method. For backup codes, these are the actual codes + * you can use. When we check for a backup code validity we also "burn" it, i.e. we remove it from the + * options table and save that to the database. However, this DOES NOT update the $record here. Therefore + * the call to saveRecord() would overwrite the database contents with a record that _includes_ the backup + * code we had just burned. As a result the single use backup codes end up being multiple use. + * + * By doing a getRecord() here, right after we have "burned" any correct backup codes, we resolve this + * issue. The loaded record will reflect the database contents where the options DO NOT include the code we + * just used. Therefore the call to store() will result in the correct database state, i.e. the used backup + * code being removed. + */ + $record = $model->getRecord(); + } + + $isValidCode = array_reduce( + $results, + function (bool $carry, $result) { + return $carry || boolval($result); + }, + false + ); + + if (!$isValidCode) { + // The code is wrong. Display an error and go back. + $captiveURL = Route::_('index.php?option=com_users&view=captive&record_id=' . $recordId, false); + $message = Text::_('COM_USERS_MFA_INVALID_CODE'); + $this->setRedirect($captiveURL, $message, 'error'); + + $event = new NotifyActionLog('onComUsersCaptiveValidateFailed', [$record->title]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + return; + } + + // Update the Last Used, UA and IP columns + $jNow = Date::getInstance(); // phpcs:ignore $record->last_used = $jNow->toSql(); - $record->store(); + $record->store(); - // Flag the user as fully logged in - $session = $this->app->getSession(); - $session->set('com_users.mfa_checked', 1); - $session->set('com_users.mandatory_mfa_setup', 0); + // Flag the user as fully logged in + $session = $this->app->getSession(); + $session->set('com_users.mfa_checked', 1); + $session->set('com_users.mandatory_mfa_setup', 0); - // Get the return URL stored by the plugin in the session - $returnUrl = $session->get('com_users.return_url', ''); + // Get the return URL stored by the plugin in the session + $returnUrl = $session->get('com_users.return_url', ''); - // If the return URL is not set or not internal to this site redirect to the site's front page - if (empty($returnUrl) || !Uri::isInternal($returnUrl)) - { - $returnUrl = Uri::base(); - } + // If the return URL is not set or not internal to this site redirect to the site's front page + if (empty($returnUrl) || !Uri::isInternal($returnUrl)) { + $returnUrl = Uri::base(); + } - $this->setRedirect($returnUrl); + $this->setRedirect($returnUrl); - $event = new NotifyActionLog('onComUsersCaptiveValidateSuccess', [$record->title]); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - } + $event = new NotifyActionLog('onComUsersCaptiveValidateSuccess', [$record->title]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + } } diff --git a/administrator/components/com_users/src/Controller/DisplayController.php b/administrator/components/com_users/src/Controller/DisplayController.php index 018bbde7b6782..34b2f3a6e519b 100644 --- a/administrator/components/com_users/src/Controller/DisplayController.php +++ b/administrator/components/com_users/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ get('core.admin'); - - // Default permissions. - default: - return true; - } - } - - /** - * Method to display a view. - * - * @param boolean $cachable If true, the view output will be cached - * @param array $urlparams An array of safe URL parameters and their variable types, - * for valid values see {@link \Joomla\CMS\Filter\InputFilter::clean()}. - * - * @return BaseController|boolean This object to support chaining or false on failure. - * - * @since 1.5 - */ - public function display($cachable = false, $urlparams = array()) - { - $view = $this->input->get('view', 'users'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); - - if (!$this->canView($view)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - // Check for edit form. - if ($view == 'user' && $layout == 'edit' && !$this->checkEditId('com_users.edit.user', $id)) - { - // 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', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_users&view=users', false)); - - return false; - } - elseif ($view == 'group' && $layout == 'edit' && !$this->checkEditId('com_users.edit.group', $id)) - { - // 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', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_users&view=groups', false)); - - return false; - } - elseif ($view == 'level' && $layout == 'edit' && !$this->checkEditId('com_users.edit.level', $id)) - { - // 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', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_users&view=levels', false)); - - return false; - } - elseif ($view == 'note' && $layout == 'edit' && !$this->checkEditId('com_users.edit.note', $id)) - { - // 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', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_users&view=notes', false)); - - return false; - } - elseif (in_array($view, ['captive', 'callback', 'methods', 'method'])) - { - $controller = $this->factory->createController($view, 'Administrator', [], $this->app, $this->input); - $task = $this->input->get('task', ''); - - return $controller->execute($task); - } - - return parent::display($cachable, $urlparams); - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'users'; + + /** + * Checks whether a user can see this view. + * + * @param string $view The view name. + * + * @return boolean + * + * @since 1.6 + */ + protected function canView($view) + { + $canDo = ContentHelper::getActions('com_users'); + + switch ($view) { + // Special permissions. + case 'groups': + case 'group': + case 'levels': + case 'level': + return $canDo->get('core.admin'); + + // Default permissions. + default: + return true; + } + } + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, + * for valid values see {@link \Joomla\CMS\Filter\InputFilter::clean()}. + * + * @return BaseController|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', 'users'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); + + if (!$this->canView($view)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Check for edit form. + if ($view == 'user' && $layout == 'edit' && !$this->checkEditId('com_users.edit.user', $id)) { + // 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', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_users&view=users', false)); + + return false; + } elseif ($view == 'group' && $layout == 'edit' && !$this->checkEditId('com_users.edit.group', $id)) { + // 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', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_users&view=groups', false)); + + return false; + } elseif ($view == 'level' && $layout == 'edit' && !$this->checkEditId('com_users.edit.level', $id)) { + // 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', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_users&view=levels', false)); + + return false; + } elseif ($view == 'note' && $layout == 'edit' && !$this->checkEditId('com_users.edit.note', $id)) { + // 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', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_users&view=notes', false)); + + return false; + } elseif (in_array($view, ['captive', 'callback', 'methods', 'method'])) { + $controller = $this->factory->createController($view, 'Administrator', [], $this->app, $this->input); + $task = $this->input->get('task', ''); + + return $controller->execute($task); + } + + return parent::display($cachable, $urlparams); + } } diff --git a/administrator/components/com_users/src/Controller/GroupController.php b/administrator/components/com_users/src/Controller/GroupController.php index d2d55aafcd778..a45e5dda4aabc 100644 --- a/administrator/components/com_users/src/Controller/GroupController.php +++ b/administrator/components/com_users/src/Controller/GroupController.php @@ -1,4 +1,5 @@ app->getIdentity()->authorise('core.admin', $this->option) && parent::allowSave($data, $key)); - } + /** + * Method to check if you can save a new or existing record. + * + * Overrides Joomla\CMS\MVC\Controller\FormController::allowSave to check the core.admin permission. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowSave($data, $key = 'id') + { + return ($this->app->getIdentity()->authorise('core.admin', $this->option) && parent::allowSave($data, $key)); + } - /** - * Overrides Joomla\CMS\MVC\Controller\FormController::allowEdit - * - * Checks that non-Super Admins are not editing Super Admins. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - // Check if this group is a Super Admin - if (Access::checkGroup($data[$key], 'core.admin')) - { - // If I'm not a Super Admin, then disallow the edit. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - return false; - } - } + /** + * Overrides Joomla\CMS\MVC\Controller\FormController::allowEdit + * + * Checks that non-Super Admins are not editing Super Admins. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + // Check if this group is a Super Admin + if (Access::checkGroup($data[$key], 'core.admin')) { + // If I'm not a Super Admin, then disallow the edit. + if (!$this->app->getIdentity()->authorise('core.admin')) { + return false; + } + } - return parent::allowEdit($data, $key); - } + return parent::allowEdit($data, $key); + } } diff --git a/administrator/components/com_users/src/Controller/GroupsController.php b/administrator/components/com_users/src/Controller/GroupsController.php index 9b71d8870add9..aba26d9d7ccbe 100644 --- a/administrator/components/com_users/src/Controller/GroupsController.php +++ b/administrator/components/com_users/src/Controller/GroupsController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Users\Administrator\Controller; use Joomla\CMS\Access\Exception\NotAllowed; @@ -21,120 +23,115 @@ */ class GroupsController extends AdminController { - /** - * @var string The prefix to use with controller messages. - * @since 1.6 - */ - protected $text_prefix = 'COM_USERS_GROUPS'; - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return object The model. - * - * @since 1.6 - */ - public function getModel($name = 'Group', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Removes an item. - * - * Overrides Joomla\CMS\MVC\Controller\AdminController::delete to check the core.admin permission. - * - * @return void - * - * @since 1.6 - */ - public function delete() - { - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - parent::delete(); - } - - /** - * Method to publish a list of records. - * - * Overrides Joomla\CMS\MVC\Controller\AdminController::publish to check the core.admin permission. - * - * @return void - * - * @since 1.6 - */ - public function publish() - { - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - parent::publish(); - } - - /** - * Changes the order of one or more records. - * - * Overrides Joomla\CMS\MVC\Controller\AdminController::reorder to check the core.admin permission. - * - * @return boolean True on success - * - * @since 1.6 - */ - public function reorder() - { - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - return parent::reorder(); - } - - /** - * Method to save the submitted ordering values for records. - * - * Overrides Joomla\CMS\MVC\Controller\AdminController::saveorder to check the core.admin permission. - * - * @return boolean True on success - * - * @since 1.6 - */ - public function saveorder() - { - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - return parent::saveorder(); - } - - /** - * Check in of one or more records. - * - * Overrides Joomla\CMS\MVC\Controller\AdminController::checkin to check the core.admin permission. - * - * @return boolean True on success - * - * @since 1.6 - */ - public function checkin() - { - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - return parent::checkin(); - } + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_USERS_GROUPS'; + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Group', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Removes an item. + * + * Overrides Joomla\CMS\MVC\Controller\AdminController::delete to check the core.admin permission. + * + * @return void + * + * @since 1.6 + */ + public function delete() + { + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + parent::delete(); + } + + /** + * Method to publish a list of records. + * + * Overrides Joomla\CMS\MVC\Controller\AdminController::publish to check the core.admin permission. + * + * @return void + * + * @since 1.6 + */ + public function publish() + { + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + parent::publish(); + } + + /** + * Changes the order of one or more records. + * + * Overrides Joomla\CMS\MVC\Controller\AdminController::reorder to check the core.admin permission. + * + * @return boolean True on success + * + * @since 1.6 + */ + public function reorder() + { + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + return parent::reorder(); + } + + /** + * Method to save the submitted ordering values for records. + * + * Overrides Joomla\CMS\MVC\Controller\AdminController::saveorder to check the core.admin permission. + * + * @return boolean True on success + * + * @since 1.6 + */ + public function saveorder() + { + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + return parent::saveorder(); + } + + /** + * Check in of one or more records. + * + * Overrides Joomla\CMS\MVC\Controller\AdminController::checkin to check the core.admin permission. + * + * @return boolean True on success + * + * @since 1.6 + */ + public function checkin() + { + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + return parent::checkin(); + } } diff --git a/administrator/components/com_users/src/Controller/LevelController.php b/administrator/components/com_users/src/Controller/LevelController.php index 4028dd10fb9ea..69e03f92b80cf 100644 --- a/administrator/components/com_users/src/Controller/LevelController.php +++ b/administrator/components/com_users/src/Controller/LevelController.php @@ -1,4 +1,5 @@ app->getIdentity()->authorise('core.admin', $this->option) && parent::allowSave($data, $key)); - } - - /** - * Overrides JControllerForm::allowEdit - * - * Checks that non-Super Admins are not editing Super Admins. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 3.8.8 - */ - protected function allowEdit($data = array(), $key = 'id') - { - // Check for if Super Admin can edit - $viewLevel = $this->getModel('Level', 'Administrator')->getItem((int) $data['id']); - - // If this group is super admin and this user is not super admin, canEdit is false - if (!$this->app->getIdentity()->authorise('core.admin') && $viewLevel->rules && Access::checkGroup($viewLevel->rules[0], 'core.admin')) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false - ) - ); - - return false; - } - - return parent::allowEdit($data, $key); - } - - /** - * Removes an item. - * - * Overrides Joomla\CMS\MVC\Controller\FormController::delete to check the core.admin permission. - * - * @return void - * - * @since 1.6 - */ - public function delete() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - elseif (empty($ids)) - { - $this->setMessage(Text::_('COM_USERS_NO_LEVELS_SELECTED'), 'warning'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Remove the items. - if ($model->delete($ids)) - { - $this->setMessage(Text::plural('COM_USERS_N_LEVELS_DELETED', count($ids))); - } - } - - $this->setRedirect('index.php?option=com_users&view=levels'); - } + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_USERS_LEVEL'; + + /** + * Method to check if you can save a new or existing record. + * + * Overrides Joomla\CMS\MVC\Controller\FormController::allowSave to check the core.admin permission. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowSave($data, $key = 'id') + { + return ($this->app->getIdentity()->authorise('core.admin', $this->option) && parent::allowSave($data, $key)); + } + + /** + * Overrides JControllerForm::allowEdit + * + * Checks that non-Super Admins are not editing Super Admins. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 3.8.8 + */ + protected function allowEdit($data = array(), $key = 'id') + { + // Check for if Super Admin can edit + $viewLevel = $this->getModel('Level', 'Administrator')->getItem((int) $data['id']); + + // If this group is super admin and this user is not super admin, canEdit is false + if (!$this->app->getIdentity()->authorise('core.admin') && $viewLevel->rules && Access::checkGroup($viewLevel->rules[0], 'core.admin')) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(), + false + ) + ); + + return false; + } + + return parent::allowEdit($data, $key); + } + + /** + * Removes an item. + * + * Overrides Joomla\CMS\MVC\Controller\FormController::delete to check the core.admin permission. + * + * @return void + * + * @since 1.6 + */ + public function delete() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } elseif (empty($ids)) { + $this->setMessage(Text::_('COM_USERS_NO_LEVELS_SELECTED'), 'warning'); + } else { + // Get the model. + $model = $this->getModel(); + + // Remove the items. + if ($model->delete($ids)) { + $this->setMessage(Text::plural('COM_USERS_N_LEVELS_DELETED', count($ids))); + } + } + + $this->setRedirect('index.php?option=com_users&view=levels'); + } } diff --git a/administrator/components/com_users/src/Controller/LevelsController.php b/administrator/components/com_users/src/Controller/LevelsController.php index d4ffb80051d27..a0712d0449fb6 100644 --- a/administrator/components/com_users/src/Controller/LevelsController.php +++ b/administrator/components/com_users/src/Controller/LevelsController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Users\Administrator\Controller; \defined('_JEXEC') or die; @@ -19,25 +21,25 @@ */ class LevelsController extends AdminController { - /** - * @var string The prefix to use with controller messages. - * @since 1.6 - */ - protected $text_prefix = 'COM_USERS_LEVELS'; + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_USERS_LEVELS'; - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 1.6 - */ - public function getModel($name = 'Level', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Level', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_users/src/Controller/MailController.php b/administrator/components/com_users/src/Controller/MailController.php index efd0d59520040..2f0bccc29ab41 100644 --- a/administrator/components/com_users/src/Controller/MailController.php +++ b/administrator/components/com_users/src/Controller/MailController.php @@ -1,4 +1,5 @@ app->get('massmailoff', 0) == 1) - { - $this->app->redirect(Route::_('index.php', false)); - } + /** + * Send the mail + * + * @return void + * + * @since 1.6 + */ + public function send() + { + // Redirect to admin index if mass mailer disabled in conf + if ($this->app->get('massmailoff', 0) == 1) { + $this->app->redirect(Route::_('index.php', false)); + } - // Check for request forgeries. - $this->checkToken('request'); + // Check for request forgeries. + $this->checkToken('request'); - $model = $this->getModel('Mail'); + $model = $this->getModel('Mail'); - if ($model->send()) - { - $type = 'message'; - } - else - { - $type = 'error'; - } + if ($model->send()) { + $type = 'message'; + } else { + $type = 'error'; + } - $msg = $model->getError(); - $this->setRedirect('index.php?option=com_users&view=mail', $msg, $type); - } + $msg = $model->getError(); + $this->setRedirect('index.php?option=com_users&view=mail', $msg, $type); + } - /** - * Cancel the mail - * - * @return void - * - * @since 1.6 - */ - public function cancel() - { - // Check for request forgeries. - $this->checkToken('request'); + /** + * Cancel the mail + * + * @return void + * + * @since 1.6 + */ + public function cancel() + { + // Check for request forgeries. + $this->checkToken('request'); - // Clear data from session. - $this->app->setUserState('com_users.display.mail.data', null); + // Clear data from session. + $this->app->setUserState('com_users.display.mail.data', null); - $this->setRedirect('index.php?option=com_users&view=users'); - } + $this->setRedirect('index.php?option=com_users&view=users'); + } } diff --git a/administrator/components/com_users/src/Controller/MethodController.php b/administrator/components/com_users/src/Controller/MethodController.php index 18918db7f0afa..6c25750e0e7a2 100644 --- a/administrator/components/com_users/src/Controller/MethodController.php +++ b/administrator/components/com_users/src/Controller/MethodController.php @@ -1,4 +1,5 @@ assertLoggedInUser(); - - // Make sure I am allowed to edit the specified user - $userId = $this->input->getInt('user_id', null); - $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - - $this->assertCanEdit($user); - - // Also make sure the Method really does exist - $method = $this->input->getCmd('method'); - $this->assertMethodExists($method); - - /** @var MethodModel $model */ - $model = $this->getModel('Method'); - $model->setState('method', $method); - - // Pass the return URL to the view - $returnURL = $this->input->getBase64('returnurl'); - $viewLayout = $this->input->get('layout', 'default', 'string'); - $view = $this->getView('Method', 'html'); - $view->setLayout($viewLayout); - $view->returnURL = $returnURL; - $view->user = $user; - $view->document = $this->app->getDocument(); - - $view->setModel($model, true); - - $event = new NotifyActionLog('onComUsersControllerMethodBeforeAdd', [$user, $method]); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - - $view->display(); - } - - /** - * Edit an existing MFA Method - * - * @param boolean $cachable Ignored. This page is never cached. - * @param boolean|array $urlparams Ignored. This page is never cached. - * - * @return void - * @throws Exception - * @since 4.2.0 - */ - public function edit($cachable = false, $urlparams = []): void - { - $this->assertLoggedInUser(); - - // Make sure I am allowed to edit the specified user - $userId = $this->input->getInt('user_id', null); - $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - - $this->assertCanEdit($user); - - // Also make sure the Method really does exist - $id = $this->input->getInt('id'); - $record = $this->assertValidRecordId($id, $user); - - if ($id <= 0) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - /** @var MethodModel $model */ - $model = $this->getModel('Method'); - $model->setState('id', $id); - - // Pass the return URL to the view - $returnURL = $this->input->getBase64('returnurl'); - $viewLayout = $this->input->get('layout', 'default', 'string'); - $view = $this->getView('Method', 'html'); - $view->setLayout($viewLayout); - $view->returnURL = $returnURL; - $view->user = $user; - $view->document = $this->app->getDocument(); - - $view->setModel($model, true); - - $event = new NotifyActionLog('onComUsersControllerMethodBeforeEdit', [$id, $user]); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - - $view->display(); - } - - /** - * Regenerate backup codes - * - * @param boolean $cachable Ignored. This page is never cached. - * @param boolean|array $urlparams Ignored. This page is never cached. - * - * @return void - * @throws Exception - * @since 4.2.0 - */ - public function regenerateBackupCodes($cachable = false, $urlparams = []): void - { - $this->assertLoggedInUser(); - - $this->checkToken($this->input->getMethod()); - - // Make sure I am allowed to edit the specified user - $userId = $this->input->getInt('user_id', null); - $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - $this->assertCanEdit($user); - - /** @var BackupcodesModel $model */ - $model = $this->getModel('Backupcodes'); - $model->regenerateBackupCodes($user); - - $backupCodesRecord = $model->getBackupCodesRecord($user); - - // Redirect - $redirectUrl = 'index.php?option=com_users&task=method.edit&user_id=' . $userId . '&id=' . $backupCodesRecord->id; - $returnURL = $this->input->getBase64('returnurl'); - - if (!empty($returnURL)) - { - $redirectUrl .= '&returnurl=' . $returnURL; - } - - $this->setRedirect(Route::_($redirectUrl, false)); - - $event = new NotifyActionLog('onComUsersControllerMethodAfterRegenerateBackupCodes'); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - } - - /** - * Delete an existing MFA Method - * - * @param boolean $cachable Ignored. This page is never cached. - * @param boolean|array $urlparams Ignored. This page is never cached. - * - * @return void - * @since 4.2.0 - */ - public function delete($cachable = false, $urlparams = []): void - { - $this->assertLoggedInUser(); - - $this->checkToken($this->input->getMethod()); - - // Make sure I am allowed to edit the specified user - $userId = $this->input->getInt('user_id', null); - $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - $this->assertCanDelete($user); - - // Also make sure the Method really does exist - $id = $this->input->getInt('id'); - $record = $this->assertValidRecordId($id, $user); - - if ($id <= 0) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $type = null; - $message = null; - - $event = new NotifyActionLog('onComUsersControllerMethodBeforeDelete', [$id, $user]); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - - try - { - $record->delete(); - } - catch (Exception $e) - { - $message = $e->getMessage(); - $type = 'error'; - } - - // Redirect - $url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false); - $returnURL = $this->input->getBase64('returnurl'); - - if (!empty($returnURL)) - { - $url = base64_decode($returnURL); - } - - $this->setRedirect($url, $message, $type); - } - - /** - * Save the MFA Method - * - * @param boolean $cachable Ignored. This page is never cached. - * @param boolean|array $urlparams Ignored. This page is never cached. - * - * @return void - * @since 4.2.0 - */ - public function save($cachable = false, $urlparams = []): void - { - $this->assertLoggedInUser(); - - $this->checkToken($this->input->getMethod()); - - // Make sure I am allowed to edit the specified user - $userId = $this->input->getInt('user_id', null); - $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - $this->assertCanEdit($user); - - // Redirect - $url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false); - $returnURL = $this->input->getBase64('returnurl'); - - if (!empty($returnURL)) - { - $url = base64_decode($returnURL); - } - - // The record must either be new (ID zero) or exist - $id = $this->input->getInt('id', 0); - $record = $this->assertValidRecordId($id, $user); - - // If it's a new record we need to read the Method from the request and update the (not yet created) record. - if ($record->id == 0) - { - $methodName = $this->input->getCmd('method'); - $this->assertMethodExists($methodName); - $record->method = $methodName; - } - - /** @var MethodModel $model */ - $model = $this->getModel('Method'); - - // Ask the plugin to validate the input by calling onUserMultifactorSaveSetup - $result = []; - $input = $this->app->input; - - $event = new NotifyActionLog('onComUsersControllerMethodBeforeSave', [$id, $user]); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - - try - { - $event = new SaveSetup($record, $input); - $pluginResults = $this->app - ->getDispatcher() - ->dispatch($event->getName(), $event) - ->getArgument('result', []); - - foreach ($pluginResults as $pluginResult) - { - $result = array_merge($result, $pluginResult); - } - } - catch (RuntimeException $e) - { - // Go back to the edit page - $nonSefUrl = 'index.php?option=com_users&task=method.'; - - if ($id) - { - $nonSefUrl .= 'edit&id=' . (int) $id; - } - else - { - $nonSefUrl .= 'add&method=' . $record->method; - } - - $nonSefUrl .= '&user_id=' . $userId; - - if (!empty($returnURL)) - { - $nonSefUrl .= '&returnurl=' . urlencode($returnURL); - } - - $url = Route::_($nonSefUrl, false); - $this->setRedirect($url, $e->getMessage(), 'error'); - - return; - } - - // Update the record's options with the plugin response - $title = $this->input->getString('title', null); - $title = trim($title); - - if (empty($title)) - { - $method = $model->getMethod($record->method); - $title = $method['display']; - } - - // Update the record's "default" flag - $default = $this->input->getBool('default', false); - $record->title = $title; - $record->options = $result; - $record->default = $default ? 1 : 0; - - // Ask the model to save the record - $saved = $record->store(); - - if (!$saved) - { - // Go back to the edit page - $nonSefUrl = 'index.php?option=com_users&task=method.'; - - if ($id) - { - $nonSefUrl .= 'edit&id=' . (int) $id; - } - else - { - $nonSefUrl .= 'add'; - } - - $nonSefUrl .= '&user_id=' . $userId; - - if (!empty($returnURL)) - { - $nonSefUrl .= '&returnurl=' . urlencode($returnURL); - } - - $url = Route::_($nonSefUrl, false); - $this->setRedirect($url, $record->getError(), 'error'); - - return; - } - - $this->setRedirect($url); - } - - /** - * Assert that the provided ID is a valid record identified for the given user - * - * @param int $id Record ID to check - * @param User|null $user User record. Null to use current user. - * - * @return MfaTable The loaded record - * @since 4.2.0 - */ - private function assertValidRecordId($id, ?User $user = null): MfaTable - { - if (is_null($user)) - { - $user = $this->app->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - /** @var MethodModel $model */ - $model = $this->getModel('Method'); - - $model->setState('id', $id); - - $record = $model->getRecord($user); + /** + * Public constructor + * + * @param array $config Plugin configuration + * @param MVCFactoryInterface|null $factory MVC Factory for the com_users component + * @param CMSApplication|null $app CMS application object + * @param Input|null $input Joomla CMS input object + * + * @since 4.2.0 + */ + public function __construct(array $config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null) + { + // We have to tell Joomla what is the name of the view, otherwise it defaults to the name of the *component*. + $config['default_view'] = 'method'; + $config['default_task'] = 'add'; + + parent::__construct($config, $factory, $app, $input); + } + + /** + * Execute a task by triggering a Method in the derived class. + * + * @param string $task The task to perform. If no matching task is found, the '__default' task is executed, if + * defined. + * + * @return mixed The value returned by the called Method. + * + * @throws Exception + * @since 4.2.0 + */ + public function execute($task) + { + if (empty($task) || $task === 'display') { + $task = 'add'; + } + + return parent::execute($task); + } + + /** + * Add a new MFA Method + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + public function add($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + + $this->assertCanEdit($user); + + // Also make sure the Method really does exist + $method = $this->input->getCmd('method'); + $this->assertMethodExists($method); + + /** @var MethodModel $model */ + $model = $this->getModel('Method'); + $model->setState('method', $method); + + // Pass the return URL to the view + $returnURL = $this->input->getBase64('returnurl'); + $viewLayout = $this->input->get('layout', 'default', 'string'); + $view = $this->getView('Method', 'html'); + $view->setLayout($viewLayout); + $view->returnURL = $returnURL; + $view->user = $user; + $view->document = $this->app->getDocument(); + + $view->setModel($model, true); + + $event = new NotifyActionLog('onComUsersControllerMethodBeforeAdd', [$user, $method]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + $view->display(); + } + + /** + * Edit an existing MFA Method + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + public function edit($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + + $this->assertCanEdit($user); + + // Also make sure the Method really does exist + $id = $this->input->getInt('id'); + $record = $this->assertValidRecordId($id, $user); + + if ($id <= 0) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + /** @var MethodModel $model */ + $model = $this->getModel('Method'); + $model->setState('id', $id); + + // Pass the return URL to the view + $returnURL = $this->input->getBase64('returnurl'); + $viewLayout = $this->input->get('layout', 'default', 'string'); + $view = $this->getView('Method', 'html'); + $view->setLayout($viewLayout); + $view->returnURL = $returnURL; + $view->user = $user; + $view->document = $this->app->getDocument(); + + $view->setModel($model, true); + + $event = new NotifyActionLog('onComUsersControllerMethodBeforeEdit', [$id, $user]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + $view->display(); + } + + /** + * Regenerate backup codes + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + public function regenerateBackupCodes($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + $this->checkToken($this->input->getMethod()); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $this->assertCanEdit($user); + + /** @var BackupcodesModel $model */ + $model = $this->getModel('Backupcodes'); + $model->regenerateBackupCodes($user); + + $backupCodesRecord = $model->getBackupCodesRecord($user); + + // Redirect + $redirectUrl = 'index.php?option=com_users&task=method.edit&user_id=' . $userId . '&id=' . $backupCodesRecord->id; + $returnURL = $this->input->getBase64('returnurl'); + + if (!empty($returnURL)) { + $redirectUrl .= '&returnurl=' . $returnURL; + } + + $this->setRedirect(Route::_($redirectUrl, false)); + + $event = new NotifyActionLog('onComUsersControllerMethodAfterRegenerateBackupCodes'); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + } + + /** + * Delete an existing MFA Method + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @since 4.2.0 + */ + public function delete($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + $this->checkToken($this->input->getMethod()); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $this->assertCanDelete($user); + + // Also make sure the Method really does exist + $id = $this->input->getInt('id'); + $record = $this->assertValidRecordId($id, $user); + + if ($id <= 0) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $type = null; + $message = null; + + $event = new NotifyActionLog('onComUsersControllerMethodBeforeDelete', [$id, $user]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + try { + $record->delete(); + } catch (Exception $e) { + $message = $e->getMessage(); + $type = 'error'; + } + + // Redirect + $url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false); + $returnURL = $this->input->getBase64('returnurl'); + + if (!empty($returnURL)) { + $url = base64_decode($returnURL); + } + + $this->setRedirect($url, $message, $type); + } + + /** + * Save the MFA Method + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @since 4.2.0 + */ + public function save($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + $this->checkToken($this->input->getMethod()); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $this->assertCanEdit($user); + + // Redirect + $url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false); + $returnURL = $this->input->getBase64('returnurl'); + + if (!empty($returnURL)) { + $url = base64_decode($returnURL); + } + + // The record must either be new (ID zero) or exist + $id = $this->input->getInt('id', 0); + $record = $this->assertValidRecordId($id, $user); + + // If it's a new record we need to read the Method from the request and update the (not yet created) record. + if ($record->id == 0) { + $methodName = $this->input->getCmd('method'); + $this->assertMethodExists($methodName); + $record->method = $methodName; + } + + /** @var MethodModel $model */ + $model = $this->getModel('Method'); + + // Ask the plugin to validate the input by calling onUserMultifactorSaveSetup + $result = []; + $input = $this->app->input; + + $event = new NotifyActionLog('onComUsersControllerMethodBeforeSave', [$id, $user]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + try { + $event = new SaveSetup($record, $input); + $pluginResults = $this->app + ->getDispatcher() + ->dispatch($event->getName(), $event) + ->getArgument('result', []); + + foreach ($pluginResults as $pluginResult) { + $result = array_merge($result, $pluginResult); + } + } catch (RuntimeException $e) { + // Go back to the edit page + $nonSefUrl = 'index.php?option=com_users&task=method.'; + + if ($id) { + $nonSefUrl .= 'edit&id=' . (int) $id; + } else { + $nonSefUrl .= 'add&method=' . $record->method; + } + + $nonSefUrl .= '&user_id=' . $userId; + + if (!empty($returnURL)) { + $nonSefUrl .= '&returnurl=' . urlencode($returnURL); + } + + $url = Route::_($nonSefUrl, false); + $this->setRedirect($url, $e->getMessage(), 'error'); + + return; + } + + // Update the record's options with the plugin response + $title = $this->input->getString('title', null); + $title = trim($title); + + if (empty($title)) { + $method = $model->getMethod($record->method); + $title = $method['display']; + } + + // Update the record's "default" flag + $default = $this->input->getBool('default', false); + $record->title = $title; + $record->options = $result; + $record->default = $default ? 1 : 0; + + // Ask the model to save the record + $saved = $record->store(); + + if (!$saved) { + // Go back to the edit page + $nonSefUrl = 'index.php?option=com_users&task=method.'; + + if ($id) { + $nonSefUrl .= 'edit&id=' . (int) $id; + } else { + $nonSefUrl .= 'add'; + } + + $nonSefUrl .= '&user_id=' . $userId; + + if (!empty($returnURL)) { + $nonSefUrl .= '&returnurl=' . urlencode($returnURL); + } + + $url = Route::_($nonSefUrl, false); + $this->setRedirect($url, $record->getError(), 'error'); + + return; + } + + $this->setRedirect($url); + } + + /** + * Assert that the provided ID is a valid record identified for the given user + * + * @param int $id Record ID to check + * @param User|null $user User record. Null to use current user. + * + * @return MfaTable The loaded record + * @since 4.2.0 + */ + private function assertValidRecordId($id, ?User $user = null): MfaTable + { + if (is_null($user)) { + $user = $this->app->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + /** @var MethodModel $model */ + $model = $this->getModel('Method'); + + $model->setState('id', $id); + + $record = $model->getRecord($user); // phpcs:ignore if (is_null($record) || ($record->id != $id) || ($record->user_id != $user->id)) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - return $record; - } - - /** - * Assert that the user can add / edit MFA methods. - * - * @param User|null $user User record. Null to use current user. - * - * @return void - * @throws RuntimeException|Exception - * @since 4.2.0 - */ - private function assertCanEdit(?User $user = null): void - { - if (!MfaHelper::canAddEditMethod($user)) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - } - - /** - * Assert that the user can delete MFA records / disable MFA. - * - * @param User|null $user User record. Null to use current user. - * - * @return void - * @throws RuntimeException|Exception - * @since 4.2.0 - */ - private function assertCanDelete(?User $user = null): void - { - if (!MfaHelper::canDeleteMethod($user)) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - } - - /** - * Assert that the specified MFA Method exists, is activated and enabled for the current user - * - * @param string|null $method The Method to check - * - * @return void - * @since 4.2.0 - */ - private function assertMethodExists(?string $method): void - { - /** @var MethodModel $model */ - $model = $this->getModel('Method'); - - if (empty($method) || !$model->methodExists($method)) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - } - - /** - * Assert that there is a logged in user. - * - * @return void - * @since 4.2.0 - */ - private function assertLoggedInUser(): void - { - $user = $this->app->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - if ($user->guest) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - } + { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + return $record; + } + + /** + * Assert that the user can add / edit MFA methods. + * + * @param User|null $user User record. Null to use current user. + * + * @return void + * @throws RuntimeException|Exception + * @since 4.2.0 + */ + private function assertCanEdit(?User $user = null): void + { + if (!MfaHelper::canAddEditMethod($user)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } + + /** + * Assert that the user can delete MFA records / disable MFA. + * + * @param User|null $user User record. Null to use current user. + * + * @return void + * @throws RuntimeException|Exception + * @since 4.2.0 + */ + private function assertCanDelete(?User $user = null): void + { + if (!MfaHelper::canDeleteMethod($user)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } + + /** + * Assert that the specified MFA Method exists, is activated and enabled for the current user + * + * @param string|null $method The Method to check + * + * @return void + * @since 4.2.0 + */ + private function assertMethodExists(?string $method): void + { + /** @var MethodModel $model */ + $model = $this->getModel('Method'); + + if (empty($method) || !$model->methodExists($method)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } + + /** + * Assert that there is a logged in user. + * + * @return void + * @since 4.2.0 + */ + private function assertLoggedInUser(): void + { + $user = $this->app->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + if ($user->guest) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_users/src/Controller/MethodsController.php b/administrator/components/com_users/src/Controller/MethodsController.php index 92a6fd2b20cc9..4eb1909eed1e0 100644 --- a/administrator/components/com_users/src/Controller/MethodsController.php +++ b/administrator/components/com_users/src/Controller/MethodsController.php @@ -1,4 +1,5 @@ assertLoggedInUser(); - - $this->checkToken($this->input->getMethod()); - - // Make sure I am allowed to edit the specified user - $userId = $this->input->getInt('user_id', null); - $user = ($userId === null) - ? $this->app->getIdentity() - : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - $user = $user ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - if (!MfaHelper::canDeleteMethod($user)) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - // Delete all MFA Methods for the user - /** @var MethodsModel $model */ - $model = $this->getModel('Methods'); - $type = null; - $message = null; - - $event = new NotifyActionLog('onComUsersControllerMethodsBeforeDisable', [$user]); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - - try - { - $model->deleteAll($user); - } - catch (Exception $e) - { - $message = $e->getMessage(); - $type = 'error'; - } - - // Redirect + /** + * Public constructor + * + * @param array $config Plugin configuration + * @param MVCFactoryInterface|null $factory MVC Factory for the com_users component + * @param CMSApplication|null $app CMS application object + * @param Input|null $input Joomla CMS input object + * + * @since 4.2.0 + */ + public function __construct($config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null) + { + // We have to tell Joomla what is the name of the view, otherwise it defaults to the name of the *component*. + $config['default_view'] = 'Methods'; + + parent::__construct($config, $factory, $app, $input); + } + + /** + * Disable Multi-factor Authentication for the current user + * + * @param bool $cachable Can this view be cached + * @param array $urlparams An array of safe url parameters and their variable types, for valid values see + * {@link JFilterInput::clean()}. + * + * @return void + * @since 4.2.0 + */ + public function disable($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + $this->checkToken($this->input->getMethod()); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = ($userId === null) + ? $this->app->getIdentity() + : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $user = $user ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + if (!MfaHelper::canDeleteMethod($user)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Delete all MFA Methods for the user + /** @var MethodsModel $model */ + $model = $this->getModel('Methods'); + $type = null; + $message = null; + + $event = new NotifyActionLog('onComUsersControllerMethodsBeforeDisable', [$user]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + try { + $model->deleteAll($user); + } catch (Exception $e) { + $message = $e->getMessage(); + $type = 'error'; + } + + // Redirect // phpcs:ignore $url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false); - $returnURL = $this->input->getBase64('returnurl'); - - if (!empty($returnURL)) - { - $url = base64_decode($returnURL); - } - - $this->setRedirect($url, $message, $type); - } - - /** - * List all available Multi-factor Authentication Methods available and guide the user to setting them up - * - * @param bool $cachable Can this view be cached - * @param array $urlparams An array of safe url parameters and their variable types, for valid values see - * {@link JFilterInput::clean()}. - * - * @return void - * @since 4.2.0 - */ - public function display($cachable = false, $urlparams = []): void - { - $this->assertLoggedInUser(); - - // Make sure I am allowed to edit the specified user - $userId = $this->input->getInt('user_id', null); - $user = ($userId === null) - ? $this->app->getIdentity() - : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - $user = $user ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - if (!MfaHelper::canShowConfigurationInterface($user)) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $returnURL = $this->input->getBase64('returnurl'); - $viewLayout = $this->input->get('layout', 'default', 'string'); - $view = $this->getView('Methods', 'html'); - $view->setLayout($viewLayout); - $view->returnURL = $returnURL; - $view->user = $user; - $view->document = $this->app->getDocument(); - - $methodsModel = $this->getModel('Methods'); - $view->setModel($methodsModel, true); - - $backupCodesModel = $this->getModel('Backupcodes'); - $view->setModel($backupCodesModel, false); - - $view->display(); - } - - /** - * Disable Multi-factor Authentication for the current user - * - * @param bool $cachable Can this view be cached - * @param array $urlparams An array of safe url parameters and their variable types, for valid values see - * {@link JFilterInput::clean()}. - * - * @return void - * @since 4.2.0 - */ - public function doNotShowThisAgain($cachable = false, $urlparams = []): void - { - $this->assertLoggedInUser(); - - $this->checkToken($this->input->getMethod()); - - // Make sure I am allowed to edit the specified user - $userId = $this->input->getInt('user_id', null); - $user = ($userId === null) - ? $this->app->getIdentity() - : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - $user = $user ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - if (!MfaHelper::canAddEditMethod($user)) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $event = new NotifyActionLog('onComUsersControllerMethodsBeforeDoNotShowThisAgain', [$user]); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - - /** @var MethodsModel $model */ - $model = $this->getModel('Methods'); - $model->setFlag($user, true); - - // Redirect - $url = Uri::base(); - $returnURL = $this->input->getBase64('returnurl'); - - if (!empty($returnURL)) - { - $url = base64_decode($returnURL); - } - - $this->setRedirect($url); - } - - /** - * Assert that there is a user currently logged in - * - * @return void - * @since 4.2.0 - */ - private function assertLoggedInUser(): void - { - $user = $this->app->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - if ($user->guest) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - } + $returnURL = $this->input->getBase64('returnurl'); + + if (!empty($returnURL)) { + $url = base64_decode($returnURL); + } + + $this->setRedirect($url, $message, $type); + } + + /** + * List all available Multi-factor Authentication Methods available and guide the user to setting them up + * + * @param bool $cachable Can this view be cached + * @param array $urlparams An array of safe url parameters and their variable types, for valid values see + * {@link JFilterInput::clean()}. + * + * @return void + * @since 4.2.0 + */ + public function display($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = ($userId === null) + ? $this->app->getIdentity() + : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $user = $user ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + if (!MfaHelper::canShowConfigurationInterface($user)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $returnURL = $this->input->getBase64('returnurl'); + $viewLayout = $this->input->get('layout', 'default', 'string'); + $view = $this->getView('Methods', 'html'); + $view->setLayout($viewLayout); + $view->returnURL = $returnURL; + $view->user = $user; + $view->document = $this->app->getDocument(); + + $methodsModel = $this->getModel('Methods'); + $view->setModel($methodsModel, true); + + $backupCodesModel = $this->getModel('Backupcodes'); + $view->setModel($backupCodesModel, false); + + $view->display(); + } + + /** + * Disable Multi-factor Authentication for the current user + * + * @param bool $cachable Can this view be cached + * @param array $urlparams An array of safe url parameters and their variable types, for valid values see + * {@link JFilterInput::clean()}. + * + * @return void + * @since 4.2.0 + */ + public function doNotShowThisAgain($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + $this->checkToken($this->input->getMethod()); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = ($userId === null) + ? $this->app->getIdentity() + : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $user = $user ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + if (!MfaHelper::canAddEditMethod($user)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $event = new NotifyActionLog('onComUsersControllerMethodsBeforeDoNotShowThisAgain', [$user]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + /** @var MethodsModel $model */ + $model = $this->getModel('Methods'); + $model->setFlag($user, true); + + // Redirect + $url = Uri::base(); + $returnURL = $this->input->getBase64('returnurl'); + + if (!empty($returnURL)) { + $url = base64_decode($returnURL); + } + + $this->setRedirect($url); + } + + /** + * Assert that there is a user currently logged in + * + * @return void + * @since 4.2.0 + */ + private function assertLoggedInUser(): void + { + $user = $this->app->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + if ($user->guest) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_users/src/Controller/NoteController.php b/administrator/components/com_users/src/Controller/NoteController.php index 34b5ccc374fe6..d0590084b2a87 100644 --- a/administrator/components/com_users/src/Controller/NoteController.php +++ b/administrator/components/com_users/src/Controller/NoteController.php @@ -1,4 +1,5 @@ input->get('u_id', 0, 'int'); - - if ($userId) - { - $append .= '&u_id=' . $userId; - } - - return $append; - } + use VersionableControllerTrait; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 2.5 + */ + protected $text_prefix = 'COM_USERS_NOTE'; + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $key The name of the primary key variable. + * + * @return string The arguments to append to the redirect URL. + * + * @since 2.5 + */ + protected function getRedirectToItemAppend($recordId = null, $key = 'id') + { + $append = parent::getRedirectToItemAppend($recordId, $key); + + $userId = $this->input->get('u_id', 0, 'int'); + + if ($userId) { + $append .= '&u_id=' . $userId; + } + + return $append; + } } diff --git a/administrator/components/com_users/src/Controller/NotesController.php b/administrator/components/com_users/src/Controller/NotesController.php index cb49b92318e7d..20ac0fd70981f 100644 --- a/administrator/components/com_users/src/Controller/NotesController.php +++ b/administrator/components/com_users/src/Controller/NotesController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 2.5 + */ + public function getModel($name = 'Note', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_users/src/Controller/UserController.php b/administrator/components/com_users/src/Controller/UserController.php index 5694b46d2f1e4..8d6e96d59fb08 100644 --- a/administrator/components/com_users/src/Controller/UserController.php +++ b/administrator/components/com_users/src/Controller/UserController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Users\Administrator\Controller; \defined('_JEXEC') or die; @@ -23,139 +25,132 @@ */ class UserController extends FormController { - /** - * @var string The prefix to use with controller messages. - * @since 1.6 - */ - protected $text_prefix = 'COM_USERS_USER'; - - /** - * Overrides Joomla\CMS\MVC\Controller\FormController::allowEdit - * - * Checks that non-Super Admins are not editing Super Admins. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean True if allowed, false otherwise. - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - // Check if this person is a Super Admin - if (Access::check($data[$key], 'core.admin')) - { - // If I'm not a Super Admin, then disallow the edit. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - return false; - } - } - - // Allow users to edit their own account - if (isset($data[$key]) && (int) $this->app->getIdentity()->id === (int) $data[$key]) - { - return true; - } - - return parent::allowEdit($data, $key); - } - - /** - * Override parent cancel to redirect when using status edit account. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 4.0.0 - */ - public function cancel($key = null) - { - $result = parent::cancel(); - - if ($return = $this->input->get('return', '', 'BASE64')) - { - $return = base64_decode($return); - - // Don't redirect to an external URL. - if (!Uri::isInternal($return)) - { - $return = Uri::base(); - } - - $this->app->redirect($return); - } - - return $result; - } - - /** - * Override parent save to redirect when using status edit account. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 4.0.0 - */ - public function save($key = null, $urlVar = null) - { - $result = parent::save($key, $urlVar); - - $task = $this->getTask(); - - if ($task === 'save' && $return = $this->input->get('return', '', 'BASE64')) - { - $return = base64_decode($return); - - // Don't redirect to an external URL. - if (!Uri::isInternal($return)) - { - $return = Uri::base(); - } - - $this->setRedirect($return); - } - - return $result; - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True on success, false on failure - * - * @since 2.5 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('User', 'Administrator', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_users&view=users' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 3.1 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - } + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_USERS_USER'; + + /** + * Overrides Joomla\CMS\MVC\Controller\FormController::allowEdit + * + * Checks that non-Super Admins are not editing Super Admins. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean True if allowed, false otherwise. + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + // Check if this person is a Super Admin + if (Access::check($data[$key], 'core.admin')) { + // If I'm not a Super Admin, then disallow the edit. + if (!$this->app->getIdentity()->authorise('core.admin')) { + return false; + } + } + + // Allow users to edit their own account + if (isset($data[$key]) && (int) $this->app->getIdentity()->id === (int) $data[$key]) { + return true; + } + + return parent::allowEdit($data, $key); + } + + /** + * Override parent cancel to redirect when using status edit account. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 4.0.0 + */ + public function cancel($key = null) + { + $result = parent::cancel(); + + if ($return = $this->input->get('return', '', 'BASE64')) { + $return = base64_decode($return); + + // Don't redirect to an external URL. + if (!Uri::isInternal($return)) { + $return = Uri::base(); + } + + $this->app->redirect($return); + } + + return $result; + } + + /** + * Override parent save to redirect when using status edit account. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 4.0.0 + */ + public function save($key = null, $urlVar = null) + { + $result = parent::save($key, $urlVar); + + $task = $this->getTask(); + + if ($task === 'save' && $return = $this->input->get('return', '', 'BASE64')) { + $return = base64_decode($return); + + // Don't redirect to an external URL. + if (!Uri::isInternal($return)) { + $return = Uri::base(); + } + + $this->setRedirect($return); + } + + return $result; + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True on success, false on failure + * + * @since 2.5 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('User', 'Administrator', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_users&view=users' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 3.1 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + } } diff --git a/administrator/components/com_users/src/Controller/UsersController.php b/administrator/components/com_users/src/Controller/UsersController.php index f5d9f0b07e7eb..727f60015640e 100644 --- a/administrator/components/com_users/src/Controller/UsersController.php +++ b/administrator/components/com_users/src/Controller/UsersController.php @@ -1,4 +1,5 @@ registerTask('block', 'changeBlock'); - $this->registerTask('unblock', 'changeBlock'); - } - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return object The model. - * - * @since 1.6 - */ - public function getModel($name = 'User', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to change the block status on a record. - * - * @return void - * - * @since 1.6 - */ - public function changeBlock() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('block' => 1, 'unblock' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'warning'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Change the state of the records. - if (!$model->block($ids, $value)) - { - $this->setMessage($model->getError(), 'error'); - } - else - { - if ($value == 1) - { - $this->setMessage(Text::plural('COM_USERS_N_USERS_BLOCKED', count($ids))); - } - elseif ($value == 0) - { - $this->setMessage(Text::plural('COM_USERS_N_USERS_UNBLOCKED', count($ids))); - } - } - } - - $this->setRedirect('index.php?option=com_users&view=users'); - } - - /** - * Method to activate a record. - * - * @return void - * - * @since 1.6 - */ - public function activate() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'error'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Change the state of the records. - if (!$model->activate($ids)) - { - $this->setMessage($model->getError(), 'error'); - } - else - { - $this->setMessage(Text::plural('COM_USERS_N_USERS_ACTIVATED', count($ids))); - } - } - - $this->setRedirect('index.php?option=com_users&view=users'); - } - - /** - * Method to get the number of active users - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('Users'); - - $model->setState('filter.state', 0); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_USERS_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_USERS_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_USERS_USERS'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The CMSApplication for the dispatcher + * @param Input $input Input + * + * @since 1.6 + * @see BaseController + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('block', 'changeBlock'); + $this->registerTask('unblock', 'changeBlock'); + } + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'User', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to change the block status on a record. + * + * @return void + * + * @since 1.6 + */ + public function changeBlock() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('block' => 1, 'unblock' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'warning'); + } else { + // Get the model. + $model = $this->getModel(); + + // Change the state of the records. + if (!$model->block($ids, $value)) { + $this->setMessage($model->getError(), 'error'); + } else { + if ($value == 1) { + $this->setMessage(Text::plural('COM_USERS_N_USERS_BLOCKED', count($ids))); + } elseif ($value == 0) { + $this->setMessage(Text::plural('COM_USERS_N_USERS_UNBLOCKED', count($ids))); + } + } + } + + $this->setRedirect('index.php?option=com_users&view=users'); + } + + /** + * Method to activate a record. + * + * @return void + * + * @since 1.6 + */ + public function activate() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'error'); + } else { + // Get the model. + $model = $this->getModel(); + + // Change the state of the records. + if (!$model->activate($ids)) { + $this->setMessage($model->getError(), 'error'); + } else { + $this->setMessage(Text::plural('COM_USERS_N_USERS_ACTIVATED', count($ids))); + } + } + + $this->setRedirect('index.php?option=com_users&view=users'); + } + + /** + * Method to get the number of active users + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Users'); + + $model->setState('filter.state', 0); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_USERS_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_USERS_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } } diff --git a/administrator/components/com_users/src/DataShape/CaptiveRenderOptions.php b/administrator/components/com_users/src/DataShape/CaptiveRenderOptions.php index 273bb73de9ed7..4542388548552 100644 --- a/administrator/components/com_users/src/DataShape/CaptiveRenderOptions.php +++ b/administrator/components/com_users/src/DataShape/CaptiveRenderOptions.php @@ -1,4 +1,5 @@ field_type = $value; - } - - /** - * Setter for the input_attributes property. - * - * @param array $value The value to set - * - * @return void - * @@since 4.2.0 - */ + } + + /** + * Setter for the input_attributes property. + * + * @param array $value The value to set + * + * @return void + * @@since 4.2.0 + */ // phpcs:ignore protected function setInput_attributes(array $value) - { - $forbiddenAttributes = ['id', 'type', 'name', 'value']; + { + $forbiddenAttributes = ['id', 'type', 'name', 'value']; - foreach ($forbiddenAttributes as $key) - { - if (isset($value[$key])) - { - unset($value[$key]); - } - } + foreach ($forbiddenAttributes as $key) { + if (isset($value[$key])) { + unset($value[$key]); + } + } // phpcs:ignore $this->input_attributes = $value; - } + } } diff --git a/administrator/components/com_users/src/DataShape/MethodDescriptor.php b/administrator/components/com_users/src/DataShape/MethodDescriptor.php index 4988c573b6d21..2b9ee51080615 100644 --- a/administrator/components/com_users/src/DataShape/MethodDescriptor.php +++ b/administrator/components/com_users/src/DataShape/MethodDescriptor.php @@ -1,4 +1,5 @@ active[$record->id] = $record; - } + /** + * Adds an active MFA method + * + * @param MfaTable $record The MFA method record to add + * + * @return void + * @since 4.2.0 + */ + public function addActiveMethod(MfaTable $record) + { + $this->active[$record->id] = $record; + } } diff --git a/administrator/components/com_users/src/DataShape/SetupRenderOptions.php b/administrator/components/com_users/src/DataShape/SetupRenderOptions.php index fcf612b05c805..3034fd67333bb 100644 --- a/administrator/components/com_users/src/DataShape/SetupRenderOptions.php +++ b/administrator/components/com_users/src/DataShape/SetupRenderOptions.php @@ -1,4 +1,5 @@ custom HTML). See above - * - * @var array - * @since 4.2.0 - */ + /** + * Any tabular data to display (label => custom HTML). See above + * + * @var array + * @since 4.2.0 + */ // phpcs:ignore protected $tabular_data = []; - /** - * Hidden fields to include in the form (name => value) - * - * @var array - * @since 4.2.0 - */ + /** + * Hidden fields to include in the form (name => value) + * + * @var array + * @since 4.2.0 + */ // phpcs:ignore protected $hidden_data = []; - /** - * How to render the MFA setup code field. "input" (HTML input element) or "custom" (custom HTML) - * - * @var string - * @since 4.2.0 - */ + /** + * How to render the MFA setup code field. "input" (HTML input element) or "custom" (custom HTML) + * + * @var string + * @since 4.2.0 + */ // phpcs:ignore protected $field_type = 'input'; - /** - * The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type. - * - * @var string - * @since 4.2.0 - */ + /** + * The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type. + * + * @var string + * @since 4.2.0 + */ // phpcs:ignore protected $input_type = 'text'; - /** - * Attributes other than type and id which will be added to the HTML input box. - * - * @var array - * @@since 4.2.0 - */ + /** + * Attributes other than type and id which will be added to the HTML input box. + * + * @var array + * @@since 4.2.0 + */ // phpcs:ignore protected $input_attributes = []; - /** - * Pre-filled value for the HTML input box. Typically used for fixed codes, the fixed YubiKey ID etc. - * - * @var string - * @since 4.2.0 - */ + /** + * Pre-filled value for the HTML input box. Typically used for fixed codes, the fixed YubiKey ID etc. + * + * @var string + * @since 4.2.0 + */ // phpcs:ignore protected $input_value = ''; - /** - * Placeholder text for the HTML input box. Leave empty if you don't need it. - * - * @var string - * @since 4.2.0 - */ - protected $placeholder = ''; + /** + * Placeholder text for the HTML input box. Leave empty if you don't need it. + * + * @var string + * @since 4.2.0 + */ + protected $placeholder = ''; - /** - * Label to show above the HTML input box. Leave empty if you don't need it. - * - * @var string - * @since 4.2.0 - */ - protected $label = ''; + /** + * Label to show above the HTML input box. Leave empty if you don't need it. + * + * @var string + * @since 4.2.0 + */ + protected $label = ''; - /** - * Custom HTML. Only used when field_type = custom. - * - * @var string - * @since 4.2.0 - */ - protected $html = ''; + /** + * Custom HTML. Only used when field_type = custom. + * + * @var string + * @since 4.2.0 + */ + protected $html = ''; - /** - * Should I show the submit button (apply the MFA setup)? - * - * @var boolean - * @since 4.2.0 - */ + /** + * Should I show the submit button (apply the MFA setup)? + * + * @var boolean + * @since 4.2.0 + */ // phpcs:ignore protected $show_submit = true; - /** - * Additional CSS classes for the submit button (apply the MFA setup) - * - * @var string - * @since 4.2.0 - */ + /** + * Additional CSS classes for the submit button (apply the MFA setup) + * + * @var string + * @since 4.2.0 + */ // phpcs:ignore protected $submit_class = ''; - /** - * Icon class to use for the submit button - * - * @var string - * @since 4.2.0 - */ + /** + * Icon class to use for the submit button + * + * @var string + * @since 4.2.0 + */ // phpcs:ignore protected $submit_icon = 'icon icon-ok'; - /** - * Language key to use for the text on the submit button - * - * @var string - * @since 4.2.0 - */ + /** + * Language key to use for the text on the submit button + * + * @var string + * @since 4.2.0 + */ // phpcs:ignore protected $submit_text = 'JSAVE'; - /** - * Custom HTML to display below the MFA setup form - * - * @var string - * @since 4.2.0 - */ + /** + * Custom HTML to display below the MFA setup form + * + * @var string + * @since 4.2.0 + */ // phpcs:ignore protected $post_message = ''; - /** - * A URL with help content for this Method to display to the user - * - * @var string - * @since 4.2.0 - */ + /** + * A URL with help content for this Method to display to the user + * + * @var string + * @since 4.2.0 + */ // phpcs:ignore protected $help_url = ''; - /** - * Setter for the field_type property - * - * @param string $value One of self::FIELD_INPUT, self::FIELD_CUSTOM - * - * @since 4.2.0 - * @throws InvalidArgumentException - */ + /** + * Setter for the field_type property + * + * @param string $value One of self::FIELD_INPUT, self::FIELD_CUSTOM + * + * @since 4.2.0 + * @throws InvalidArgumentException + */ // phpcs:ignore protected function setField_type($value) - { - if (!in_array($value, [self::FIELD_INPUT, self::FIELD_CUSTOM])) - { - throw new InvalidArgumentException('Invalid value for property field_type.'); - } + { + if (!in_array($value, [self::FIELD_INPUT, self::FIELD_CUSTOM])) { + throw new InvalidArgumentException('Invalid value for property field_type.'); + } // phpcs:ignore $this->field_type = $value; - } + } - /** - * Setter for the input_attributes property. - * - * @param array $value The value to set - * - * @return void - * @@since 4.2.0 - */ + /** + * Setter for the input_attributes property. + * + * @param array $value The value to set + * + * @return void + * @@since 4.2.0 + */ // phpcs:ignore protected function setInput_attributes(array $value) - { - $forbiddenAttributes = ['id', 'type', 'name', 'value']; + { + $forbiddenAttributes = ['id', 'type', 'name', 'value']; - foreach ($forbiddenAttributes as $key) - { - if (isset($value[$key])) - { - unset($value[$key]); - } - } + foreach ($forbiddenAttributes as $key) { + if (isset($value[$key])) { + unset($value[$key]); + } + } // phpcs:ignore $this->input_attributes = $value; - } + } } diff --git a/administrator/components/com_users/src/Dispatcher/Dispatcher.php b/administrator/components/com_users/src/Dispatcher/Dispatcher.php index 2a3b6e79e2773..adafeb2ea2578 100644 --- a/administrator/components/com_users/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_users/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->getCmd('task'); - $view = $this->input->getCmd('view'); - $layout = $this->input->getCmd('layout'); - $allowedTasks = ['user.edit', 'user.apply', 'user.save', 'user.cancel']; + /** + * Override checkAccess to allow users edit profile without having to have core.manager permission + * + * @return void + * + * @since 4.0.0 + */ + protected function checkAccess() + { + $task = $this->input->getCmd('task'); + $view = $this->input->getCmd('view'); + $layout = $this->input->getCmd('layout'); + $allowedTasks = ['user.edit', 'user.apply', 'user.save', 'user.cancel']; - // Allow users to edit their own account - if (in_array($task, $allowedTasks, true) || ($view === 'user' && $layout === 'edit')) - { - $user = $this->app->getIdentity(); - $id = $this->input->getInt('id'); + // Allow users to edit their own account + if (in_array($task, $allowedTasks, true) || ($view === 'user' && $layout === 'edit')) { + $user = $this->app->getIdentity(); + $id = $this->input->getInt('id'); - if ((int) $user->id === $id) - { - return; - } - } + if ((int) $user->id === $id) { + return; + } + } - /** - * Special case: Multi-factor Authentication - * - * We allow access to all MFA views and tasks. Access control for MFA tasks is performed in - * the Controllers since what is allowed depends on who is logged in and whose account you - * are trying to modify. Implementing these checks in the Dispatcher would violate the - * separation of concerns. - */ - $allowedViews = ['callback', 'captive', 'method', 'methods']; - $isAllowedTask = array_reduce( - $allowedViews, - function ($carry, $taskPrefix) use ($task) - { - return $carry || strpos($task ?? '', $taskPrefix . '.') === 0; - }, - false - ); + /** + * Special case: Multi-factor Authentication + * + * We allow access to all MFA views and tasks. Access control for MFA tasks is performed in + * the Controllers since what is allowed depends on who is logged in and whose account you + * are trying to modify. Implementing these checks in the Dispatcher would violate the + * separation of concerns. + */ + $allowedViews = ['callback', 'captive', 'method', 'methods']; + $isAllowedTask = array_reduce( + $allowedViews, + function ($carry, $taskPrefix) use ($task) { + return $carry || strpos($task ?? '', $taskPrefix . '.') === 0; + }, + false + ); - if (in_array(strtolower($view ?? ''), $allowedViews) || $isAllowedTask) - { - return; - } + if (in_array(strtolower($view ?? ''), $allowedViews) || $isAllowedTask) { + return; + } - parent::checkAccess(); - } + parent::checkAccess(); + } } diff --git a/administrator/components/com_users/src/Extension/UsersComponent.php b/administrator/components/com_users/src/Extension/UsersComponent.php index 0b9dcd89fcf01..24bd6a84233ff 100644 --- a/administrator/components/com_users/src/Extension/UsersComponent.php +++ b/administrator/components/com_users/src/Extension/UsersComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('users', new Users); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('users', new Users()); + } - /** - * Returns a valid section for the given section. If it is not valid then null is returned. - * - * @param string $section The section to get the mapping for - * @param object|null $item The content item or null - * - * @return string|null The new section or null - * - * @since 4.0.0 - */ - public function validateSection($section, $item = null) - { - if (Factory::getApplication()->isClient('site')) - { - switch ($section) - { - case 'registration': - case 'profile': - return 'user'; - } - } + /** + * Returns a valid section for the given section. If it is not valid then null is returned. + * + * @param string $section The section to get the mapping for + * @param object|null $item The content item or null + * + * @return string|null The new section or null + * + * @since 4.0.0 + */ + public function validateSection($section, $item = null) + { + if (Factory::getApplication()->isClient('site')) { + switch ($section) { + case 'registration': + case 'profile': + return 'user'; + } + } - if ($section === 'user') - { - return $section; - } + if ($section === 'user') { + return $section; + } - // We don't know other sections. - return null; - } + // We don't know other sections. + return null; + } - /** - * Returns valid contexts. - * - * @return array Associative array with contexts as keys and translated strings as values - * - * @since 4.0.0 - */ - public function getContexts(): array - { - $language = Factory::getApplication()->getLanguage(); - $language->load('com_users', JPATH_ADMINISTRATOR); + /** + * Returns valid contexts. + * + * @return array Associative array with contexts as keys and translated strings as values + * + * @since 4.0.0 + */ + public function getContexts(): array + { + $language = Factory::getApplication()->getLanguage(); + $language->load('com_users', JPATH_ADMINISTRATOR); - return [ - 'com_users.user' => $language->_('COM_USERS'), - ]; - } + return [ + 'com_users.user' => $language->_('COM_USERS'), + ]; + } } diff --git a/administrator/components/com_users/src/Field/GroupparentField.php b/administrator/components/com_users/src/Field/GroupparentField.php index 8bf8532860612..44d837c5a0ef4 100644 --- a/administrator/components/com_users/src/Field/GroupparentField.php +++ b/administrator/components/com_users/src/Field/GroupparentField.php @@ -1,4 +1,5 @@ $userGroupsOptionsData) - { - if ((int) $userGroupsOptionsData->parent_id === (int) $fatherId) - { - unset($userGroupsOptions[$userGroupsOptionsId]); + /** + * Method to clean the Usergroup Options from all children starting by a given father + * + * @param array $userGroupsOptions The usergroup options to clean + * @param integer $fatherId The father ID to start with + * + * @return array The cleaned field options + * + * @since 3.9.4 + */ + private function cleanOptionsChildrenByFather($userGroupsOptions, $fatherId) + { + foreach ($userGroupsOptions as $userGroupsOptionsId => $userGroupsOptionsData) { + if ((int) $userGroupsOptionsData->parent_id === (int) $fatherId) { + unset($userGroupsOptions[$userGroupsOptionsId]); - $userGroupsOptions = $this->cleanOptionsChildrenByFather($userGroupsOptions, $userGroupsOptionsId); - } - } + $userGroupsOptions = $this->cleanOptionsChildrenByFather($userGroupsOptions, $userGroupsOptionsId); + } + } - return $userGroupsOptions; - } + return $userGroupsOptions; + } - /** - * Method to get the field options. - * - * @return array The field option objects - * - * @since 1.6 - */ - protected function getOptions() - { - $options = UserGroupsHelper::getInstance()->getAll(); - $currentGroupId = (int) Factory::getApplication()->input->get('id', 0, 'int'); + /** + * Method to get the field options. + * + * @return array The field option objects + * + * @since 1.6 + */ + protected function getOptions() + { + $options = UserGroupsHelper::getInstance()->getAll(); + $currentGroupId = (int) Factory::getApplication()->input->get('id', 0, 'int'); - // Prevent to set yourself as parent - if ($currentGroupId) - { - unset($options[$currentGroupId]); - } + // Prevent to set yourself as parent + if ($currentGroupId) { + unset($options[$currentGroupId]); + } - // We should not remove any groups when we are creating a new group - if ($currentGroupId !== 0) - { - // Prevent parenting direct children and children of children of this item. - $options = $this->cleanOptionsChildrenByFather($options, $currentGroupId); - } + // We should not remove any groups when we are creating a new group + if ($currentGroupId !== 0) { + // Prevent parenting direct children and children of children of this item. + $options = $this->cleanOptionsChildrenByFather($options, $currentGroupId); + } - $options = array_values($options); - $isSuperAdmin = Factory::getUser()->authorise('core.admin'); + $options = array_values($options); + $isSuperAdmin = Factory::getUser()->authorise('core.admin'); - // Pad the option text with spaces using depth level as a multiplier. - for ($i = 0, $n = count($options); $i < $n; $i++) - { - // Show groups only if user is super admin or group is not super admin - if ($isSuperAdmin || !Access::checkGroup($options[$i]->id, 'core.admin')) - { - $options[$i]->value = $options[$i]->id; - $options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->title; - } - else - { - unset($options[$i]); - } - } + // Pad the option text with spaces using depth level as a multiplier. + for ($i = 0, $n = count($options); $i < $n; $i++) { + // Show groups only if user is super admin or group is not super admin + if ($isSuperAdmin || !Access::checkGroup($options[$i]->id, 'core.admin')) { + $options[$i]->value = $options[$i]->id; + $options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->title; + } else { + unset($options[$i]); + } + } - // Merge any additional options in the XML definition. - return array_merge(parent::getOptions(), $options); - } + // Merge any additional options in the XML definition. + return array_merge(parent::getOptions(), $options); + } } diff --git a/administrator/components/com_users/src/Field/LevelsField.php b/administrator/components/com_users/src/Field/LevelsField.php index ddf70480a85c5..4cd83f4e9c54b 100644 --- a/administrator/components/com_users/src/Field/LevelsField.php +++ b/administrator/components/com_users/src/Field/LevelsField.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('name AS text, element AS value') - ->from('#__extensions') - ->where('enabled >= 1') - ->where('type =' . $db->quote('component')); - - $items = $db->setQuery($query)->loadObjectList(); - - if (count($items)) - { - $lang = Factory::getLanguage(); - - foreach ($items as &$item) - { - // Load language - $extension = $item->value; - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) - || $lang->load("$extension.sys", $source); - - // Translate component name - $item->text = Text::_($item->text); - } - - // Sort by component name - $items = ArrayHelper::sortObjects($items, 'text', 1, true, true); - } - - return $items; - } - - /** - * Get a list of the actions for the component or code actions. - * - * @param string $component The name of the component. - * - * @return array - * - * @since 1.6 - */ - public static function getDebugActions($component = null) - { - $actions = array(); - - // Try to get actions for the component - if (!empty($component)) - { - $component_actions = Access::getActionsFromFile(JPATH_ADMINISTRATOR . '/components/' . $component . '/access.xml'); - - if (!empty($component_actions)) - { - foreach ($component_actions as &$action) - { - $descr = (string) $action->title; - - if (!empty($action->description)) - { - $descr = (string) $action->description; - } - - $actions[$action->title] = array($action->name, $descr); - } - } - } - - // Use default actions from configuration if no component selected or component doesn't have actions - if (empty($actions)) - { - $filename = JPATH_ADMINISTRATOR . '/components/com_config/forms/application.xml'; - - if (is_file($filename)) - { - $xml = simplexml_load_file($filename); - - foreach ($xml->children()->fieldset as $fieldset) - { - if ('permissions' == (string) $fieldset['name']) - { - foreach ($fieldset->children() as $field) - { - if ('rules' == (string) $field['name']) - { - foreach ($field->children() as $action) - { - $descr = (string) $action['title']; - - if (isset($action['description']) && !empty($action['description'])) - { - $descr = (string) $action['description']; - } - - $actions[(string) $action['title']] = array( - (string) $action['name'], - $descr - ); - } - - break; - } - } - } - } - - // Load language - $lang = Factory::getLanguage(); - $extension = 'com_config'; - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - - $lang->load($extension, JPATH_ADMINISTRATOR, null, false, false) - || $lang->load($extension, $source, null, false, false) - || $lang->load($extension, JPATH_ADMINISTRATOR, $lang->getDefault(), false, false) - || $lang->load($extension, $source, $lang->getDefault(), false, false); - } - } - - return $actions; - } - - /** - * Get a list of filter options for the levels. - * - * @return array An array of \JHtmlOption elements. - */ - public static function getLevelsOptions() - { - // Build the filter options. - $options = array(); - $options[] = HTMLHelper::_('select.option', '1', Text::sprintf('COM_USERS_OPTION_LEVEL_COMPONENT', 1)); - $options[] = HTMLHelper::_('select.option', '2', Text::sprintf('COM_USERS_OPTION_LEVEL_CATEGORY', 2)); - $options[] = HTMLHelper::_('select.option', '3', Text::sprintf('COM_USERS_OPTION_LEVEL_DEEPER', 3)); - $options[] = HTMLHelper::_('select.option', '4', '4'); - $options[] = HTMLHelper::_('select.option', '5', '5'); - $options[] = HTMLHelper::_('select.option', '6', '6'); - - return $options; - } + /** + * Get a list of the components. + * + * @return array + * + * @since 1.6 + */ + public static function getComponents() + { + // Initialise variable. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('name AS text, element AS value') + ->from('#__extensions') + ->where('enabled >= 1') + ->where('type =' . $db->quote('component')); + + $items = $db->setQuery($query)->loadObjectList(); + + if (count($items)) { + $lang = Factory::getLanguage(); + + foreach ($items as &$item) { + // Load language + $extension = $item->value; + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) + || $lang->load("$extension.sys", $source); + + // Translate component name + $item->text = Text::_($item->text); + } + + // Sort by component name + $items = ArrayHelper::sortObjects($items, 'text', 1, true, true); + } + + return $items; + } + + /** + * Get a list of the actions for the component or code actions. + * + * @param string $component The name of the component. + * + * @return array + * + * @since 1.6 + */ + public static function getDebugActions($component = null) + { + $actions = array(); + + // Try to get actions for the component + if (!empty($component)) { + $component_actions = Access::getActionsFromFile(JPATH_ADMINISTRATOR . '/components/' . $component . '/access.xml'); + + if (!empty($component_actions)) { + foreach ($component_actions as &$action) { + $descr = (string) $action->title; + + if (!empty($action->description)) { + $descr = (string) $action->description; + } + + $actions[$action->title] = array($action->name, $descr); + } + } + } + + // Use default actions from configuration if no component selected or component doesn't have actions + if (empty($actions)) { + $filename = JPATH_ADMINISTRATOR . '/components/com_config/forms/application.xml'; + + if (is_file($filename)) { + $xml = simplexml_load_file($filename); + + foreach ($xml->children()->fieldset as $fieldset) { + if ('permissions' == (string) $fieldset['name']) { + foreach ($fieldset->children() as $field) { + if ('rules' == (string) $field['name']) { + foreach ($field->children() as $action) { + $descr = (string) $action['title']; + + if (isset($action['description']) && !empty($action['description'])) { + $descr = (string) $action['description']; + } + + $actions[(string) $action['title']] = array( + (string) $action['name'], + $descr + ); + } + + break; + } + } + } + } + + // Load language + $lang = Factory::getLanguage(); + $extension = 'com_config'; + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + + $lang->load($extension, JPATH_ADMINISTRATOR, null, false, false) + || $lang->load($extension, $source, null, false, false) + || $lang->load($extension, JPATH_ADMINISTRATOR, $lang->getDefault(), false, false) + || $lang->load($extension, $source, $lang->getDefault(), false, false); + } + } + + return $actions; + } + + /** + * Get a list of filter options for the levels. + * + * @return array An array of \JHtmlOption elements. + */ + public static function getLevelsOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', Text::sprintf('COM_USERS_OPTION_LEVEL_COMPONENT', 1)); + $options[] = HTMLHelper::_('select.option', '2', Text::sprintf('COM_USERS_OPTION_LEVEL_CATEGORY', 2)); + $options[] = HTMLHelper::_('select.option', '3', Text::sprintf('COM_USERS_OPTION_LEVEL_DEEPER', 3)); + $options[] = HTMLHelper::_('select.option', '4', '4'); + $options[] = HTMLHelper::_('select.option', '5', '5'); + $options[] = HTMLHelper::_('select.option', '6', '6'); + + return $options; + } } diff --git a/administrator/components/com_users/src/Helper/Mfa.php b/administrator/components/com_users/src/Helper/Mfa.php index 34b7ab5c84ae5..1964296ff4785 100644 --- a/administrator/components/com_users/src/Helper/Mfa.php +++ b/administrator/components/com_users/src/Helper/Mfa.php @@ -1,4 +1,5 @@ input->getCmd('option', '') === 'com_users') - { - $app->getLanguage()->load('com_users'); - $app->getDocument() - ->getWebAssetManager() - ->getRegistry() - ->addExtensionRegistryFile('com_users'); - } - - // Get a model - /** @var MVCFactoryInterface $factory */ - $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory(); - - /** @var MethodsModel $methodsModel */ - $methodsModel = $factory->createModel('Methods', 'Administrator'); - /** @var BackupcodesModel $methodsModel */ - $backupCodesModel = $factory->createModel('Backupcodes', 'Administrator'); - - // Get a view object - $appRoot = $app->isClient('site') ? \JPATH_SITE : \JPATH_ADMINISTRATOR; - $prefix = $app->isClient('site') ? 'Site' : 'Administrator'; - /** @var HtmlView $view */ - $view = $factory->createView('Methods', $prefix, 'Html', - [ - 'base_path' => $appRoot . '/components/com_users', - ] - ); - $view->setModel($methodsModel, true); - /** @noinspection PhpParamsInspection */ - $view->setModel($backupCodesModel); - $view->document = $app->getDocument(); - $view->returnURL = base64_encode(Uri::getInstance()->toString()); - $view->user = $user; - $view->set('forHMVC', true); - - @ob_start(); - - try - { - $view->display(); - } - catch (\Throwable $e) - { - @ob_end_clean(); - - /** - * This is intentional! When you are developing a Multi-factor Authentication plugin you - * will inevitably mess something up and end up with an error. This would cause the - * entire MFA configuration page to disappear. No problem! Set Debug System to Yes in - * Global Configuration and you can see the error exception which will help you solve - * your problem. - */ - if (defined('JDEBUG') && JDEBUG) - { - throw $e; - } - - return null; - } - - return @ob_get_clean(); - } - - /** - * Get a list of all of the MFA Methods - * - * @return MethodDescriptor[] - * @since 4.2.0 - */ - public static function getMfaMethods(): array - { - PluginHelper::importPlugin('multifactorauth'); - - if (is_null(self::$allMFAs)) - { - // Get all the plugin results - $event = new GetMethod; - $temp = Factory::getApplication() - ->getDispatcher() - ->dispatch($event->getName(), $event) - ->getArgument('result', []); - - // Normalize the results - self::$allMFAs = []; - - foreach ($temp as $method) - { - if (!is_array($method) && !($method instanceof MethodDescriptor)) - { - continue; - } - - $method = $method instanceof MethodDescriptor - ? $method : new MethodDescriptor($method); - - if (empty($method['name'])) - { - continue; - } - - self::$allMFAs[$method['name']] = $method; - } - } - - return self::$allMFAs; - } - - /** - * Is the current user allowed to add/edit MFA methods for $user? - * - * This is only allowed if I am adding / editing methods for myself. - * - * If the target user is a member of any group disallowed to use MFA this will return false. - * - * @param User|null $user The user you want to know if we're allowed to edit - * - * @return boolean - * @throws Exception - * @since 4.2.0 - */ - public static function canAddEditMethod(?User $user = null): bool - { - // Cannot do MFA operations on no user or a guest user. - if (is_null($user) || $user->guest) - { - return false; - } - - // If the user is in a user group which disallows MFA we cannot allow adding / editing methods. - $neverMFAGroups = ComponentHelper::getParams('com_users')->get('neverMFAUserGroups', []); - $neverMFAGroups = is_array($neverMFAGroups) ? $neverMFAGroups : []; - - if (count(array_intersect($user->getAuthorisedGroups(), $neverMFAGroups))) - { - return false; - } - - // Check if this is the same as the logged-in user. - $myUser = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - return $myUser->id === $user->id; - } - - /** - * Is the current user allowed to delete MFA methods / disable MFA for $user? - * - * This is allowed if: - * - The user being queried is the same as the logged-in user - * - The logged-in user is a Super User AND the queried user is NOT a Super User. - * - * Note that Super Users can be edited by their own user only for security reasons. If a Super - * User gets locked out they must use the Backup Codes to regain access. If that's not possible, - * they will need to delete their records from the `#__user_mfa` table. - * - * @param User|null $user The user being queried. - * - * @return boolean - * @throws Exception - * @since 4.2.0 - */ - public static function canDeleteMethod(?User $user = null): bool - { - // Cannot do MFA operations on no user or a guest user. - if (is_null($user) || $user->guest) - { - return false; - } - - $myUser = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - return $myUser->id === $user->id - || ($myUser->authorise('core.admin') && !$user->authorise('core.admin')); - } - - /** - * Return all MFA records for a specific user - * - * @param int|null $userId User ID. NULL for currently logged in user. - * - * @return MfaTable[] - * @throws Exception - * - * @since 4.2.0 - */ - public static function getUserMfaRecords(?int $userId): array - { - if (empty($userId)) - { - $user = Factory::getApplication()->getIdentity() ?: Factory::getUser(); - $userId = $user->id ?: 0; - } - - /** @var DatabaseDriver $db */ - $db = Factory::getContainer()->get('DatabaseDriver'); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__user_mfa')) - ->where($db->quoteName('user_id') . ' = :user_id') - ->bind(':user_id', $userId, ParameterType::INTEGER); - - try - { - $ids = $db->setQuery($query)->loadColumn() ?: []; - } - catch (Exception $e) - { - $ids = []; - } - - if (empty($ids)) - { - return []; - } - - /** @var MVCFactoryInterface $factory */ - $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory(); - - // Map all results to MFA table objects - $records = array_map( - function ($id) use ($factory) - { - /** @var MfaTable $record */ - $record = $factory->createTable('Mfa', 'Administrator'); - $loaded = $record->load($id); - - return $loaded ? $record : null; - }, - $ids - ); - - // Let's remove Methods we couldn't decrypt when reading from the database. - $hasBackupCodes = false; - - $records = array_filter( - $records, - function ($record) use (&$hasBackupCodes) - { - $isValid = !is_null($record) && (!empty($record->options)); - - if ($isValid && ($record->method === 'backupcodes')) - { - $hasBackupCodes = true; - } - - return $isValid; - } - ); - - // If the only Method is backup codes it's as good as having no records - if ((count($records) === 1) && $hasBackupCodes) - { - return []; - } - - return $records; - } - - /** - * Are the conditions for showing the MFA configuration interface met? - * - * @param User|null $user The user to be configured - * - * @return boolean - * @throws Exception - * @since 4.2.0 - */ - public static function canShowConfigurationInterface(?User $user = null): bool - { - // If I have no user to check against that's all the checking I can do. - if (empty($user)) - { - return false; - } - - // I need at least one MFA method plugin for the setup interface to make any sense. - $plugins = PluginHelper::getPlugin('multifactorauth'); - - if (count($plugins) < 1) - { - return false; - } - - /** @var CMSApplication $app */ - $app = Factory::getApplication(); - - // We can only show a configuration page in the front- or backend application. - if (!$app->isClient('site') && !$app->isClient('administrator')) - { - return false; - } - - // Only show the configuration page if we have an HTML document - if (!($app->getDocument() instanceof HtmlDocument)) - { - return false; - } - - // I must be able to add, edit or delete the user's MFA settings - return self::canAddEditMethod($user) || self::canDeleteMethod($user); - } + /** + * Cache of all currently active MFAs + * + * @var array|null + * @since 4.2.0 + */ + protected static $allMFAs = null; + + /** + * Are we inside the administrator application + * + * @var boolean + * @since 4.2.0 + */ + protected static $isAdmin = null; + + /** + * Get the HTML for the Multi-factor Authentication configuration interface for a user. + * + * This helper method uses a sort of primitive HMVC to display the com_users' Methods page which + * renders the MFA configuration interface. + * + * @param User $user The user we are going to show the configuration UI for. + * + * @return string|null The HTML of the UI; null if we cannot / must not show it. + * @throws Exception + * @since 4.2.0 + */ + public static function getConfigurationInterface(User $user): ?string + { + // Check the conditions + if (!self::canShowConfigurationInterface($user)) { + return null; + } + + /** @var CMSApplication $app */ + $app = Factory::getApplication(); + + if (!$app->input->getCmd('option', '') === 'com_users') { + $app->getLanguage()->load('com_users'); + $app->getDocument() + ->getWebAssetManager() + ->getRegistry() + ->addExtensionRegistryFile('com_users'); + } + + // Get a model + /** @var MVCFactoryInterface $factory */ + $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory(); + + /** @var MethodsModel $methodsModel */ + $methodsModel = $factory->createModel('Methods', 'Administrator'); + /** @var BackupcodesModel $methodsModel */ + $backupCodesModel = $factory->createModel('Backupcodes', 'Administrator'); + + // Get a view object + $appRoot = $app->isClient('site') ? \JPATH_SITE : \JPATH_ADMINISTRATOR; + $prefix = $app->isClient('site') ? 'Site' : 'Administrator'; + /** @var HtmlView $view */ + $view = $factory->createView( + 'Methods', + $prefix, + 'Html', + [ + 'base_path' => $appRoot . '/components/com_users', + ] + ); + $view->setModel($methodsModel, true); + /** @noinspection PhpParamsInspection */ + $view->setModel($backupCodesModel); + $view->document = $app->getDocument(); + $view->returnURL = base64_encode(Uri::getInstance()->toString()); + $view->user = $user; + $view->set('forHMVC', true); + + @ob_start(); + + try { + $view->display(); + } catch (\Throwable $e) { + @ob_end_clean(); + + /** + * This is intentional! When you are developing a Multi-factor Authentication plugin you + * will inevitably mess something up and end up with an error. This would cause the + * entire MFA configuration page to disappear. No problem! Set Debug System to Yes in + * Global Configuration and you can see the error exception which will help you solve + * your problem. + */ + if (defined('JDEBUG') && JDEBUG) { + throw $e; + } + + return null; + } + + return @ob_get_clean(); + } + + /** + * Get a list of all of the MFA Methods + * + * @return MethodDescriptor[] + * @since 4.2.0 + */ + public static function getMfaMethods(): array + { + PluginHelper::importPlugin('multifactorauth'); + + if (is_null(self::$allMFAs)) { + // Get all the plugin results + $event = new GetMethod(); + $temp = Factory::getApplication() + ->getDispatcher() + ->dispatch($event->getName(), $event) + ->getArgument('result', []); + + // Normalize the results + self::$allMFAs = []; + + foreach ($temp as $method) { + if (!is_array($method) && !($method instanceof MethodDescriptor)) { + continue; + } + + $method = $method instanceof MethodDescriptor + ? $method : new MethodDescriptor($method); + + if (empty($method['name'])) { + continue; + } + + self::$allMFAs[$method['name']] = $method; + } + } + + return self::$allMFAs; + } + + /** + * Is the current user allowed to add/edit MFA methods for $user? + * + * This is only allowed if I am adding / editing methods for myself. + * + * If the target user is a member of any group disallowed to use MFA this will return false. + * + * @param User|null $user The user you want to know if we're allowed to edit + * + * @return boolean + * @throws Exception + * @since 4.2.0 + */ + public static function canAddEditMethod(?User $user = null): bool + { + // Cannot do MFA operations on no user or a guest user. + if (is_null($user) || $user->guest) { + return false; + } + + // If the user is in a user group which disallows MFA we cannot allow adding / editing methods. + $neverMFAGroups = ComponentHelper::getParams('com_users')->get('neverMFAUserGroups', []); + $neverMFAGroups = is_array($neverMFAGroups) ? $neverMFAGroups : []; + + if (count(array_intersect($user->getAuthorisedGroups(), $neverMFAGroups))) { + return false; + } + + // Check if this is the same as the logged-in user. + $myUser = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + return $myUser->id === $user->id; + } + + /** + * Is the current user allowed to delete MFA methods / disable MFA for $user? + * + * This is allowed if: + * - The user being queried is the same as the logged-in user + * - The logged-in user is a Super User AND the queried user is NOT a Super User. + * + * Note that Super Users can be edited by their own user only for security reasons. If a Super + * User gets locked out they must use the Backup Codes to regain access. If that's not possible, + * they will need to delete their records from the `#__user_mfa` table. + * + * @param User|null $user The user being queried. + * + * @return boolean + * @throws Exception + * @since 4.2.0 + */ + public static function canDeleteMethod(?User $user = null): bool + { + // Cannot do MFA operations on no user or a guest user. + if (is_null($user) || $user->guest) { + return false; + } + + $myUser = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + return $myUser->id === $user->id + || ($myUser->authorise('core.admin') && !$user->authorise('core.admin')); + } + + /** + * Return all MFA records for a specific user + * + * @param int|null $userId User ID. NULL for currently logged in user. + * + * @return MfaTable[] + * @throws Exception + * + * @since 4.2.0 + */ + public static function getUserMfaRecords(?int $userId): array + { + if (empty($userId)) { + $user = Factory::getApplication()->getIdentity() ?: Factory::getUser(); + $userId = $user->id ?: 0; + } + + /** @var DatabaseDriver $db */ + $db = Factory::getContainer()->get('DatabaseDriver'); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__user_mfa')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->bind(':user_id', $userId, ParameterType::INTEGER); + + try { + $ids = $db->setQuery($query)->loadColumn() ?: []; + } catch (Exception $e) { + $ids = []; + } + + if (empty($ids)) { + return []; + } + + /** @var MVCFactoryInterface $factory */ + $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory(); + + // Map all results to MFA table objects + $records = array_map( + function ($id) use ($factory) { + /** @var MfaTable $record */ + $record = $factory->createTable('Mfa', 'Administrator'); + $loaded = $record->load($id); + + return $loaded ? $record : null; + }, + $ids + ); + + // Let's remove Methods we couldn't decrypt when reading from the database. + $hasBackupCodes = false; + + $records = array_filter( + $records, + function ($record) use (&$hasBackupCodes) { + $isValid = !is_null($record) && (!empty($record->options)); + + if ($isValid && ($record->method === 'backupcodes')) { + $hasBackupCodes = true; + } + + return $isValid; + } + ); + + // If the only Method is backup codes it's as good as having no records + if ((count($records) === 1) && $hasBackupCodes) { + return []; + } + + return $records; + } + + /** + * Are the conditions for showing the MFA configuration interface met? + * + * @param User|null $user The user to be configured + * + * @return boolean + * @throws Exception + * @since 4.2.0 + */ + public static function canShowConfigurationInterface(?User $user = null): bool + { + // If I have no user to check against that's all the checking I can do. + if (empty($user)) { + return false; + } + + // I need at least one MFA method plugin for the setup interface to make any sense. + $plugins = PluginHelper::getPlugin('multifactorauth'); + + if (count($plugins) < 1) { + return false; + } + + /** @var CMSApplication $app */ + $app = Factory::getApplication(); + + // We can only show a configuration page in the front- or backend application. + if (!$app->isClient('site') && !$app->isClient('administrator')) { + return false; + } + + // Only show the configuration page if we have an HTML document + if (!($app->getDocument() instanceof HtmlDocument)) { + return false; + } + + // I must be able to add, edit or delete the user's MFA settings + return self::canAddEditMethod($user) || self::canDeleteMethod($user); + } } diff --git a/administrator/components/com_users/src/Helper/UsersHelper.php b/administrator/components/com_users/src/Helper/UsersHelper.php index e8ef7828aead1..1c3aac95ce9b7 100644 --- a/administrator/components/com_users/src/Helper/UsersHelper.php +++ b/administrator/components/com_users/src/Helper/UsersHelper.php @@ -1,4 +1,5 @@ getAll(); - - foreach ($options as &$option) - { - $option->value = $option->id; - $option->text = str_repeat('- ', $option->level) . $option->title; - } - - return $options; - } - - /** - * Creates a list of range options used in filter select list - * used in com_users on users view - * - * @return array - * - * @since 2.5 - */ - public static function getRangeOptions() - { - $options = array( - HTMLHelper::_('select.option', 'today', Text::_('COM_USERS_OPTION_RANGE_TODAY')), - HTMLHelper::_('select.option', 'past_week', Text::_('COM_USERS_OPTION_RANGE_PAST_WEEK')), - HTMLHelper::_('select.option', 'past_1month', Text::_('COM_USERS_OPTION_RANGE_PAST_1MONTH')), - HTMLHelper::_('select.option', 'past_3month', Text::_('COM_USERS_OPTION_RANGE_PAST_3MONTH')), - HTMLHelper::_('select.option', 'past_6month', Text::_('COM_USERS_OPTION_RANGE_PAST_6MONTH')), - HTMLHelper::_('select.option', 'past_year', Text::_('COM_USERS_OPTION_RANGE_PAST_YEAR')), - HTMLHelper::_('select.option', 'post_year', Text::_('COM_USERS_OPTION_RANGE_POST_YEAR')), - ); - - return $options; - } - - /** - * No longer used. - * - * @return array - * - * @since 3.2.0 - * @throws \Exception - * - * @deprecated 4.2.0 Will be removed in 5.0 - */ - public static function getTwoFactorMethods() - { - return []; - } - - /** - * Get a list of the User Groups for Viewing Access Levels - * - * @param string $rules User Groups in JSON format - * - * @return string $groups Comma separated list of User Groups - * - * @since 3.6 - */ - public static function getVisibleByGroups($rules) - { - $rules = json_decode($rules); - - if (!$rules) - { - return false; - } - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('title', 'text')) - ->from($db->quoteName('#__usergroups')) - ->whereIn($db->quoteName('id'), $rules); - $db->setQuery($query); - - $groups = $db->loadColumn(); - $groups = implode(', ', $groups); - - return $groups; - } - - /** - * Returns a valid section for users. If it is not valid then null - * is returned. - * - * @param string $section The section to get the mapping for - * - * @return string|null The new section - * - * @since 3.7.0 - * @throws \Exception - * @deprecated 5.0 Use \Joomla\Component\Users\Administrator\Extension\UsersComponent::validateSection() instead. - */ - public static function validateSection($section) - { - return Factory::getApplication()->bootComponent('com_users')->validateSection($section, null); - } - - /** - * Returns valid contexts - * - * @return array - * - * @since 3.7.0 - * @deprecated 5.0 Use \Joomla\Component\Users\Administrator\Extension\UsersComponent::getContexts() instead. - */ - public static function getContexts() - { - return Factory::getApplication()->bootComponent('com_users')->getContexts(); - } + /** + * @var CMSObject A cache for the available actions. + * @since 1.6 + */ + protected static $actions; + + /** + * Get a list of filter options for the blocked state of a user. + * + * @return array An array of \JHtmlOption elements. + * + * @since 1.6 + */ + public static function getStateOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JENABLED')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JDISABLED')); + + return $options; + } + + /** + * Get a list of filter options for the activated state of a user. + * + * @return array An array of \JHtmlOption elements. + * + * @since 1.6 + */ + public static function getActiveOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('COM_USERS_ACTIVATED')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('COM_USERS_UNACTIVATED')); + + return $options; + } + + /** + * Get a list of the user groups for filtering. + * + * @return array An array of \JHtmlOption elements. + * + * @since 1.6 + */ + public static function getGroups() + { + $options = UserGroupsHelper::getInstance()->getAll(); + + foreach ($options as &$option) { + $option->value = $option->id; + $option->text = str_repeat('- ', $option->level) . $option->title; + } + + return $options; + } + + /** + * Creates a list of range options used in filter select list + * used in com_users on users view + * + * @return array + * + * @since 2.5 + */ + public static function getRangeOptions() + { + $options = array( + HTMLHelper::_('select.option', 'today', Text::_('COM_USERS_OPTION_RANGE_TODAY')), + HTMLHelper::_('select.option', 'past_week', Text::_('COM_USERS_OPTION_RANGE_PAST_WEEK')), + HTMLHelper::_('select.option', 'past_1month', Text::_('COM_USERS_OPTION_RANGE_PAST_1MONTH')), + HTMLHelper::_('select.option', 'past_3month', Text::_('COM_USERS_OPTION_RANGE_PAST_3MONTH')), + HTMLHelper::_('select.option', 'past_6month', Text::_('COM_USERS_OPTION_RANGE_PAST_6MONTH')), + HTMLHelper::_('select.option', 'past_year', Text::_('COM_USERS_OPTION_RANGE_PAST_YEAR')), + HTMLHelper::_('select.option', 'post_year', Text::_('COM_USERS_OPTION_RANGE_POST_YEAR')), + ); + + return $options; + } + + /** + * No longer used. + * + * @return array + * + * @since 3.2.0 + * @throws \Exception + * + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public static function getTwoFactorMethods() + { + return []; + } + + /** + * Get a list of the User Groups for Viewing Access Levels + * + * @param string $rules User Groups in JSON format + * + * @return string $groups Comma separated list of User Groups + * + * @since 3.6 + */ + public static function getVisibleByGroups($rules) + { + $rules = json_decode($rules); + + if (!$rules) { + return false; + } + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('title', 'text')) + ->from($db->quoteName('#__usergroups')) + ->whereIn($db->quoteName('id'), $rules); + $db->setQuery($query); + + $groups = $db->loadColumn(); + $groups = implode(', ', $groups); + + return $groups; + } + + /** + * Returns a valid section for users. If it is not valid then null + * is returned. + * + * @param string $section The section to get the mapping for + * + * @return string|null The new section + * + * @since 3.7.0 + * @throws \Exception + * @deprecated 5.0 Use \Joomla\Component\Users\Administrator\Extension\UsersComponent::validateSection() instead. + */ + public static function validateSection($section) + { + return Factory::getApplication()->bootComponent('com_users')->validateSection($section, null); + } + + /** + * Returns valid contexts + * + * @return array + * + * @since 3.7.0 + * @deprecated 5.0 Use \Joomla\Component\Users\Administrator\Extension\UsersComponent::getContexts() instead. + */ + public static function getContexts() + { + return Factory::getApplication()->bootComponent('com_users')->getContexts(); + } } diff --git a/administrator/components/com_users/src/Model/BackupcodesModel.php b/administrator/components/com_users/src/Model/BackupcodesModel.php index 26d5d2abd5f00..442a4ed224098 100644 --- a/administrator/components/com_users/src/Model/BackupcodesModel.php +++ b/administrator/components/com_users/src/Model/BackupcodesModel.php @@ -1,4 +1,5 @@ getIdentity() ?: - Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - /** @var MfaTable $record */ - $record = $this->getTable('Mfa', 'Administrator'); - $loaded = $record->load( - [ - 'user_id' => $user->id, - 'method' => 'backupcodes', - ] - ); - - if (!$loaded) - { - $record = null; - } - - return $record; - } - - /** - * Generate a new set of backup codes for the specified user. The generated codes are immediately saved to the - * database and the internal cache is updated. - * - * @param User|null $user Which user to generate codes for? - * - * @return void - * @throws \Exception - * @since 4.2.0 - */ - public function regenerateBackupCodes(User $user = null): void - { - // Make sure I have a user - if (empty($user)) - { - $user = Factory::getApplication()->getIdentity() ?: - Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - // Generate backup codes - $backupCodes = []; - - for ($i = 0; $i < 10; $i++) - { - // Each backup code is 2 groups of 4 digits - $backupCodes[$i] = sprintf('%04u%04u', random_int(0, 9999), random_int(0, 9999)); - } - - // Save the backup codes to the database and update the cache - $this->saveBackupCodes($backupCodes, $user); - } - - /** - * Saves the backup codes to the database - * - * @param array $codes An array of exactly 10 elements - * @param User|null $user The user for which to save the backup codes - * - * @return boolean - * @throws \Exception - * @since 4.2.0 - */ - public function saveBackupCodes(array $codes, ?User $user = null): bool - { - // Make sure I have a user - if (empty($user)) - { - $user = Factory::getApplication()->getIdentity() ?: - Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - // Try to load existing backup codes - $existingCodes = $this->getBackupCodes($user); - $jNow = Date::getInstance(); - - /** @var MfaTable $record */ - $record = $this->getTable('Mfa', 'Administrator'); - - if (is_null($existingCodes)) - { - $record->reset(); - - $newData = [ - 'user_id' => $user->id, - 'title' => Text::_('COM_USERS_PROFILE_OTEPS'), - 'method' => 'backupcodes', - 'default' => 0, - 'created_on' => $jNow->toSql(), - 'options' => $codes, - ]; - } - else - { - $record->load( - [ - 'user_id' => $user->id, - 'method' => 'backupcodes', - ] - ); - - $newData = [ - 'options' => $codes, - ]; - } - - $saved = $record->save($newData); - - if (!$saved) - { - return false; - } - - // Finally, update the cache - $this->cache[$user->id] = $codes; - - return true; - } - - /** - * Returns the backup codes for the specified user. Cached values will be preferentially returned, therefore you - * MUST go through this model's Methods ONLY when dealing with backup codes. - * - * @param User|null $user The user for which you want the backup codes - * - * @return array|null The backup codes, or null if they do not exist - * @throws \Exception - * @since 4.2.0 - */ - public function getBackupCodes(User $user = null): ?array - { - // Make sure I have a user - if (empty($user)) - { - $user = Factory::getApplication()->getIdentity() ?: - Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - if (isset($this->cache[$user->id])) - { - return $this->cache[$user->id]; - } - - // If there is no cached record try to load it from the database - $this->cache[$user->id] = null; - - // Try to load the record - /** @var MfaTable $record */ - $record = $this->getTable('Mfa', 'Administrator'); - $loaded = $record->load( - [ - 'user_id' => $user->id, - 'method' => 'backupcodes', - ] - ); - - if ($loaded) - { - $this->cache[$user->id] = $record->options; - } - - return $this->cache[$user->id]; - } - - /** - * Check if the provided string is a backup code. If it is, it will be removed from the list (replaced with an empty - * string) and the codes will be saved to the database. All comparisons are performed in a timing safe manner. - * - * @param string $code The code to check - * @param User|null $user The user to check against - * - * @return boolean - * @throws \Exception - * @since 4.2.0 - */ - public function isBackupCode($code, ?User $user = null): bool - { - // Load the backup codes - $codes = $this->getBackupCodes($user) ?: array_fill(0, 10, ''); - - // Keep only the numbers in the provided $code - $code = filter_var($code, FILTER_SANITIZE_NUMBER_INT); - $code = trim($code); - - // Check if the code is in the array. We always check against ten codes to prevent timing attacks which - // determine the amount of codes. - $result = false; - - // The two arrays let us always add an element to an array, therefore having PHP expend the same amount of time - // for the correct code, the incorrect codes and the fake codes. - $newArray = []; - $dummyArray = []; - - $realLength = count($codes); - $restLength = 10 - $realLength; - - for ($i = 0; $i < $realLength; $i++) - { - if (hash_equals($codes[$i], $code)) - { - // This may seem redundant but makes sure both branches of the if-block are isochronous - $result = $result || true; - $newArray[] = ''; - $dummyArray[] = $codes[$i]; - } - else - { - // This may seem redundant but makes sure both branches of the if-block are isochronous - $result = $result || false; - $dummyArray[] = ''; - $newArray[] = $codes[$i]; - } - } - - /** - * This is an intentional waste of time, symmetrical to the code above, making sure - * evaluating each of the total of ten elements takes the same time. This code should never - * run UNLESS someone messed up with our backup codes array and it no longer contains 10 - * elements. - */ - $otherResult = false; - - $temp1 = ''; - - for ($i = 0; $i < 10; $i++) - { - $temp1[$i] = random_int(0, 99999999); - } - - for ($i = 0; $i < $restLength; $i++) - { - if (Crypt::timingSafeCompare($temp1[$i], $code)) - { - $otherResult = $otherResult || true; - $newArray[] = ''; - $dummyArray[] = $temp1[$i]; - } - else - { - $otherResult = $otherResult || false; - $newArray[] = ''; - $dummyArray[] = $temp1[$i]; - } - } - - // This last check makes sure than an empty code does not validate - $result = $result && !hash_equals('', $code); - - // Save the backup codes - $this->saveBackupCodes($newArray, $user); - - // Finally return the result - return $result; - } + /** + * Caches the backup codes per user ID + * + * @var array + * @since 4.2.0 + */ + protected $cache = []; + + /** + * Get the backup codes record for the specified user + * + * @param User|null $user The user in question. Use null for the currently logged in user. + * + * @return MfaTable|null Record object or null if none is found + * @throws \Exception + * @since 4.2.0 + */ + public function getBackupCodesRecord(User $user = null): ?MfaTable + { + // Make sure I have a user + if (empty($user)) { + $user = Factory::getApplication()->getIdentity() ?: + Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + $loaded = $record->load( + [ + 'user_id' => $user->id, + 'method' => 'backupcodes', + ] + ); + + if (!$loaded) { + $record = null; + } + + return $record; + } + + /** + * Generate a new set of backup codes for the specified user. The generated codes are immediately saved to the + * database and the internal cache is updated. + * + * @param User|null $user Which user to generate codes for? + * + * @return void + * @throws \Exception + * @since 4.2.0 + */ + public function regenerateBackupCodes(User $user = null): void + { + // Make sure I have a user + if (empty($user)) { + $user = Factory::getApplication()->getIdentity() ?: + Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + // Generate backup codes + $backupCodes = []; + + for ($i = 0; $i < 10; $i++) { + // Each backup code is 2 groups of 4 digits + $backupCodes[$i] = sprintf('%04u%04u', random_int(0, 9999), random_int(0, 9999)); + } + + // Save the backup codes to the database and update the cache + $this->saveBackupCodes($backupCodes, $user); + } + + /** + * Saves the backup codes to the database + * + * @param array $codes An array of exactly 10 elements + * @param User|null $user The user for which to save the backup codes + * + * @return boolean + * @throws \Exception + * @since 4.2.0 + */ + public function saveBackupCodes(array $codes, ?User $user = null): bool + { + // Make sure I have a user + if (empty($user)) { + $user = Factory::getApplication()->getIdentity() ?: + Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + // Try to load existing backup codes + $existingCodes = $this->getBackupCodes($user); + $jNow = Date::getInstance(); + + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + + if (is_null($existingCodes)) { + $record->reset(); + + $newData = [ + 'user_id' => $user->id, + 'title' => Text::_('COM_USERS_PROFILE_OTEPS'), + 'method' => 'backupcodes', + 'default' => 0, + 'created_on' => $jNow->toSql(), + 'options' => $codes, + ]; + } else { + $record->load( + [ + 'user_id' => $user->id, + 'method' => 'backupcodes', + ] + ); + + $newData = [ + 'options' => $codes, + ]; + } + + $saved = $record->save($newData); + + if (!$saved) { + return false; + } + + // Finally, update the cache + $this->cache[$user->id] = $codes; + + return true; + } + + /** + * Returns the backup codes for the specified user. Cached values will be preferentially returned, therefore you + * MUST go through this model's Methods ONLY when dealing with backup codes. + * + * @param User|null $user The user for which you want the backup codes + * + * @return array|null The backup codes, or null if they do not exist + * @throws \Exception + * @since 4.2.0 + */ + public function getBackupCodes(User $user = null): ?array + { + // Make sure I have a user + if (empty($user)) { + $user = Factory::getApplication()->getIdentity() ?: + Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + if (isset($this->cache[$user->id])) { + return $this->cache[$user->id]; + } + + // If there is no cached record try to load it from the database + $this->cache[$user->id] = null; + + // Try to load the record + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + $loaded = $record->load( + [ + 'user_id' => $user->id, + 'method' => 'backupcodes', + ] + ); + + if ($loaded) { + $this->cache[$user->id] = $record->options; + } + + return $this->cache[$user->id]; + } + + /** + * Check if the provided string is a backup code. If it is, it will be removed from the list (replaced with an empty + * string) and the codes will be saved to the database. All comparisons are performed in a timing safe manner. + * + * @param string $code The code to check + * @param User|null $user The user to check against + * + * @return boolean + * @throws \Exception + * @since 4.2.0 + */ + public function isBackupCode($code, ?User $user = null): bool + { + // Load the backup codes + $codes = $this->getBackupCodes($user) ?: array_fill(0, 10, ''); + + // Keep only the numbers in the provided $code + $code = filter_var($code, FILTER_SANITIZE_NUMBER_INT); + $code = trim($code); + + // Check if the code is in the array. We always check against ten codes to prevent timing attacks which + // determine the amount of codes. + $result = false; + + // The two arrays let us always add an element to an array, therefore having PHP expend the same amount of time + // for the correct code, the incorrect codes and the fake codes. + $newArray = []; + $dummyArray = []; + + $realLength = count($codes); + $restLength = 10 - $realLength; + + for ($i = 0; $i < $realLength; $i++) { + if (hash_equals($codes[$i], $code)) { + // This may seem redundant but makes sure both branches of the if-block are isochronous + $result = $result || true; + $newArray[] = ''; + $dummyArray[] = $codes[$i]; + } else { + // This may seem redundant but makes sure both branches of the if-block are isochronous + $result = $result || false; + $dummyArray[] = ''; + $newArray[] = $codes[$i]; + } + } + + /** + * This is an intentional waste of time, symmetrical to the code above, making sure + * evaluating each of the total of ten elements takes the same time. This code should never + * run UNLESS someone messed up with our backup codes array and it no longer contains 10 + * elements. + */ + $otherResult = false; + + $temp1 = ''; + + for ($i = 0; $i < 10; $i++) { + $temp1[$i] = random_int(0, 99999999); + } + + for ($i = 0; $i < $restLength; $i++) { + if (Crypt::timingSafeCompare($temp1[$i], $code)) { + $otherResult = $otherResult || true; + $newArray[] = ''; + $dummyArray[] = $temp1[$i]; + } else { + $otherResult = $otherResult || false; + $newArray[] = ''; + $dummyArray[] = $temp1[$i]; + } + } + + // This last check makes sure than an empty code does not validate + $result = $result && !hash_equals('', $code); + + // Save the backup codes + $this->saveBackupCodes($newArray, $user); + + // Finally return the result + return $result; + } } diff --git a/administrator/components/com_users/src/Model/CaptiveModel.php b/administrator/components/com_users/src/Model/CaptiveModel.php index f61f47f3aa51b..1b873691e31c5 100644 --- a/administrator/components/com_users/src/Model/CaptiveModel.php +++ b/administrator/components/com_users/src/Model/CaptiveModel.php @@ -1,4 +1,5 @@ registerEvent('onAfterModuleList', [$this, 'onAfterModuleList']); - } - - /** - * Get the MFA records for the user which correspond to active plugins - * - * @param User|null $user The user for which to fetch records. Skip to use the current user. - * @param bool $includeBackupCodes Should I include the backup codes record? - * - * @return array - * @throws Exception - * - * @since 4.2.0 - */ - public function getRecords(User $user = null, bool $includeBackupCodes = false): array - { - if (is_null($user)) - { - $user = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - // Get the user's MFA records - $records = MfaHelper::getUserMfaRecords($user->id); - - // No MFA Methods? Then we obviously don't need to display a Captive login page. - if (empty($records)) - { - return []; - } - - // Get the enabled MFA Methods' names - $methodNames = $this->getActiveMethodNames(); - - // Filter the records based on currently active MFA Methods - $ret = []; - - $methodNames[] = 'backupcodes'; - $methodNames = array_unique($methodNames); - - if (!$includeBackupCodes) - { - $methodNames = array_filter( - $methodNames, - function ($method) - { - return $method != 'backupcodes'; - } - ); - } - - foreach ($records as $record) - { - // Backup codes must not be included in the list. We add them in the View, at the end of the list. - if (in_array($record->method, $methodNames)) - { - $ret[$record->id] = $record; - } - } - - return $ret; - } - - /** - * Return all the active MFA Methods' names - * - * @return array - * @since 4.2.0 - */ - private function getActiveMethodNames(): ?array - { - if (!is_null($this->activeMFAMethodNames)) - { - return $this->activeMFAMethodNames; - } - - // Let's get a list of all currently active MFA Methods - $mfaMethods = MfaHelper::getMfaMethods(); - - // If no MFA Method is active we can't really display a Captive login page. - if (empty($mfaMethods)) - { - $this->activeMFAMethodNames = []; - - return $this->activeMFAMethodNames; - } - - // Get a list of just the Method names - $this->activeMFAMethodNames = []; - - foreach ($mfaMethods as $mfaMethod) - { - $this->activeMFAMethodNames[] = $mfaMethod['name']; - } - - return $this->activeMFAMethodNames; - } - - /** - * Get the currently selected MFA record for the current user. If the record ID is empty, it does not correspond to - * the currently logged in user or does not correspond to an active plugin null is returned instead. - * - * @param User|null $user The user for which to fetch records. Skip to use the current user. - * - * @return MfaTable|null - * @throws Exception - * - * @since 4.2.0 - */ - public function getRecord(?User $user = null): ?MfaTable - { - $id = (int) $this->getState('record_id', null); - - if ($id <= 0) - { - return null; - } - - if (is_null($user)) - { - $user = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - /** @var MfaTable $record */ - $record = $this->getTable('Mfa', 'Administrator'); - $loaded = $record->load( - [ - 'user_id' => $user->id, - 'id' => $id, - ] - ); - - if (!$loaded) - { - return null; - } - - $methodNames = $this->getActiveMethodNames(); - - if (!in_array($record->method, $methodNames) && ($record->method != 'backupcodes')) - { - return null; - } - - return $record; - } - - /** - * Load the Captive login page render options for a specific MFA record - * - * @param MfaTable $record The MFA record to process - * - * @return CaptiveRenderOptions The rendering options - * @since 4.2.0 - */ - public function loadCaptiveRenderOptions(?MfaTable $record): CaptiveRenderOptions - { - $renderOptions = new CaptiveRenderOptions; - - if (empty($record)) - { - return $renderOptions; - } - - $event = new Captive($record); - $results = Factory::getApplication() - ->getDispatcher() - ->dispatch($event->getName(), $event) - ->getArgument('result', []); - - if (empty($results)) - { - if ($record->method === 'backupcodes') - { - return $renderOptions->merge( - [ - 'pre_message' => Text::_('COM_USERS_USER_BACKUPCODES_CAPTIVE_PROMPT'), - 'input_type' => 'number', - 'label' => Text::_('COM_USERS_USER_BACKUPCODE'), - ] - ); - } - - return $renderOptions; - } - - foreach ($results as $result) - { - if (empty($result)) - { - continue; - } - - return $renderOptions->merge($result); - } - - return $renderOptions; - } - - /** - * Returns the title to display in the Captive login page, or an empty string if no title is to be displayed. - * - * @return string - * @since 4.2.0 - */ - public function getPageTitle(): string - { - // In the frontend we can choose if we will display a title - $showTitle = (bool) ComponentHelper::getParams('com_users') - ->get('frontend_show_title', 1); - - if (!$showTitle) - { - return ''; - } - - return Text::_('COM_USERS_USER_MULTIFACTOR_AUTH'); - } - - /** - * Translate a MFA Method's name into its human-readable, display name - * - * @param string $name The internal MFA Method name - * - * @return string - * @since 4.2.0 - */ - public function translateMethodName(string $name): string - { - static $map = null; - - if (!is_array($map)) - { - $map = []; - $mfaMethods = MfaHelper::getMfaMethods(); - - if (!empty($mfaMethods)) - { - foreach ($mfaMethods as $mfaMethod) - { - $map[$mfaMethod['name']] = $mfaMethod['display']; - } - } - } - - if ($name == 'backupcodes') - { - return Text::_('COM_USERS_USER_BACKUPCODES'); - } - - return $map[$name] ?? $name; - } - - /** - * Translate a MFA Method's name into the relative URL if its logo image - * - * @param string $name The internal MFA Method name - * - * @return string - * @since 4.2.0 - */ - public function getMethodImage(string $name): string - { - static $map = null; - - if (!is_array($map)) - { - $map = []; - $mfaMethods = MfaHelper::getMfaMethods(); - - if (!empty($mfaMethods)) - { - foreach ($mfaMethods as $mfaMethod) - { - $map[$mfaMethod['name']] = $mfaMethod['image']; - } - } - } - - if ($name == 'backupcodes') - { - return 'media/com_users/images/emergency.svg'; - } - - return $map[$name] ?? $name; - } - - /** - * Process the modules list on Joomla! 4. - * - * Joomla! 4.x is passing an Event object. The first argument of the event object is the array of modules. After - * filtering it we have to overwrite the event argument (NOT just return the new list of modules). If a future - * version of Joomla! uses immutable events we'll have to use Reflection to do that or Joomla! would have to fix - * the way this event is handled, taking its return into account. For now, we just abuse the mutable event - * properties - a feature of the event objects we discussed in the Joomla! 4 Working Group back in August 2015. - * - * @param Event $event The Joomla! event object - * - * @return void - * @throws Exception - * - * @since 4.2.0 - */ - public function onAfterModuleList(Event $event): void - { - $modules = $event->getArgument(0); - - if (empty($modules)) - { - return; - } - - $this->filterModules($modules); - - $event->setArgument(0, $modules); - } - - /** - * This is the Method which actually filters the sites modules based on the allowed module positions specified by - * the user. - * - * @param array $modules The list of the site's modules. Passed by reference. - * - * @return void The by-reference value is modified instead. - * @since 4.2.0 - * @throws Exception - */ - private function filterModules(array &$modules): void - { - $allowedPositions = $this->getAllowedModulePositions(); - - if (empty($allowedPositions)) - { - $modules = []; - - return; - } - - $filtered = []; - - foreach ($modules as $module) - { - if (in_array($module->position, $allowedPositions)) - { - $filtered[] = $module; - } - } - - $modules = $filtered; - } - - /** - * Get a list of module positions we are allowed to display - * - * @return array - * @throws Exception - * - * @since 4.2.0 - */ - private function getAllowedModulePositions(): array - { - $isAdmin = Factory::getApplication()->isClient('administrator'); - - // Load the list of allowed module positions from the component's settings. May be different for front- and back-end - $configKey = 'allowed_positions_' . ($isAdmin ? 'backend' : 'frontend'); - $res = ComponentHelper::getParams('com_users')->get($configKey, []); - - // In the backend we must always add the 'title' module position - if ($isAdmin) - { - $res[] = 'title'; - $res[] = 'toolbar'; - } - - return $res; - } - + /** + * Cache of the names of the currently active MFA Methods + * + * @var array|null + * @since 4.2.0 + */ + protected $activeMFAMethodNames = null; + + /** + * Prevents Joomla from displaying any modules. + * + * This is implemented with a trick. If you use jdoc tags to load modules the JDocumentRendererHtmlModules + * uses JModuleHelper::getModules() to load the list of modules to render. This goes through JModuleHelper::load() + * which triggers the onAfterModuleList event after cleaning up the module list from duplicates. By resetting + * the list to an empty array we force Joomla to not display any modules. + * + * Similar code paths are followed by any canonical code which tries to load modules. So even if your template does + * not use jdoc tags this code will still work as expected. + * + * @param CMSApplication|null $app The CMS application to manipulate + * + * @return void + * @throws Exception + * + * @since 4.2.0 + */ + public function suppressAllModules(CMSApplication $app = null): void + { + if (is_null($app)) { + $app = Factory::getApplication(); + } + + $app->registerEvent('onAfterModuleList', [$this, 'onAfterModuleList']); + } + + /** + * Get the MFA records for the user which correspond to active plugins + * + * @param User|null $user The user for which to fetch records. Skip to use the current user. + * @param bool $includeBackupCodes Should I include the backup codes record? + * + * @return array + * @throws Exception + * + * @since 4.2.0 + */ + public function getRecords(User $user = null, bool $includeBackupCodes = false): array + { + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + // Get the user's MFA records + $records = MfaHelper::getUserMfaRecords($user->id); + + // No MFA Methods? Then we obviously don't need to display a Captive login page. + if (empty($records)) { + return []; + } + + // Get the enabled MFA Methods' names + $methodNames = $this->getActiveMethodNames(); + + // Filter the records based on currently active MFA Methods + $ret = []; + + $methodNames[] = 'backupcodes'; + $methodNames = array_unique($methodNames); + + if (!$includeBackupCodes) { + $methodNames = array_filter( + $methodNames, + function ($method) { + return $method != 'backupcodes'; + } + ); + } + + foreach ($records as $record) { + // Backup codes must not be included in the list. We add them in the View, at the end of the list. + if (in_array($record->method, $methodNames)) { + $ret[$record->id] = $record; + } + } + + return $ret; + } + + /** + * Return all the active MFA Methods' names + * + * @return array + * @since 4.2.0 + */ + private function getActiveMethodNames(): ?array + { + if (!is_null($this->activeMFAMethodNames)) { + return $this->activeMFAMethodNames; + } + + // Let's get a list of all currently active MFA Methods + $mfaMethods = MfaHelper::getMfaMethods(); + + // If no MFA Method is active we can't really display a Captive login page. + if (empty($mfaMethods)) { + $this->activeMFAMethodNames = []; + + return $this->activeMFAMethodNames; + } + + // Get a list of just the Method names + $this->activeMFAMethodNames = []; + + foreach ($mfaMethods as $mfaMethod) { + $this->activeMFAMethodNames[] = $mfaMethod['name']; + } + + return $this->activeMFAMethodNames; + } + + /** + * Get the currently selected MFA record for the current user. If the record ID is empty, it does not correspond to + * the currently logged in user or does not correspond to an active plugin null is returned instead. + * + * @param User|null $user The user for which to fetch records. Skip to use the current user. + * + * @return MfaTable|null + * @throws Exception + * + * @since 4.2.0 + */ + public function getRecord(?User $user = null): ?MfaTable + { + $id = (int) $this->getState('record_id', null); + + if ($id <= 0) { + return null; + } + + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + $loaded = $record->load( + [ + 'user_id' => $user->id, + 'id' => $id, + ] + ); + + if (!$loaded) { + return null; + } + + $methodNames = $this->getActiveMethodNames(); + + if (!in_array($record->method, $methodNames) && ($record->method != 'backupcodes')) { + return null; + } + + return $record; + } + + /** + * Load the Captive login page render options for a specific MFA record + * + * @param MfaTable $record The MFA record to process + * + * @return CaptiveRenderOptions The rendering options + * @since 4.2.0 + */ + public function loadCaptiveRenderOptions(?MfaTable $record): CaptiveRenderOptions + { + $renderOptions = new CaptiveRenderOptions(); + + if (empty($record)) { + return $renderOptions; + } + + $event = new Captive($record); + $results = Factory::getApplication() + ->getDispatcher() + ->dispatch($event->getName(), $event) + ->getArgument('result', []); + + if (empty($results)) { + if ($record->method === 'backupcodes') { + return $renderOptions->merge( + [ + 'pre_message' => Text::_('COM_USERS_USER_BACKUPCODES_CAPTIVE_PROMPT'), + 'input_type' => 'number', + 'label' => Text::_('COM_USERS_USER_BACKUPCODE'), + ] + ); + } + + return $renderOptions; + } + + foreach ($results as $result) { + if (empty($result)) { + continue; + } + + return $renderOptions->merge($result); + } + + return $renderOptions; + } + + /** + * Returns the title to display in the Captive login page, or an empty string if no title is to be displayed. + * + * @return string + * @since 4.2.0 + */ + public function getPageTitle(): string + { + // In the frontend we can choose if we will display a title + $showTitle = (bool) ComponentHelper::getParams('com_users') + ->get('frontend_show_title', 1); + + if (!$showTitle) { + return ''; + } + + return Text::_('COM_USERS_USER_MULTIFACTOR_AUTH'); + } + + /** + * Translate a MFA Method's name into its human-readable, display name + * + * @param string $name The internal MFA Method name + * + * @return string + * @since 4.2.0 + */ + public function translateMethodName(string $name): string + { + static $map = null; + + if (!is_array($map)) { + $map = []; + $mfaMethods = MfaHelper::getMfaMethods(); + + if (!empty($mfaMethods)) { + foreach ($mfaMethods as $mfaMethod) { + $map[$mfaMethod['name']] = $mfaMethod['display']; + } + } + } + + if ($name == 'backupcodes') { + return Text::_('COM_USERS_USER_BACKUPCODES'); + } + + return $map[$name] ?? $name; + } + + /** + * Translate a MFA Method's name into the relative URL if its logo image + * + * @param string $name The internal MFA Method name + * + * @return string + * @since 4.2.0 + */ + public function getMethodImage(string $name): string + { + static $map = null; + + if (!is_array($map)) { + $map = []; + $mfaMethods = MfaHelper::getMfaMethods(); + + if (!empty($mfaMethods)) { + foreach ($mfaMethods as $mfaMethod) { + $map[$mfaMethod['name']] = $mfaMethod['image']; + } + } + } + + if ($name == 'backupcodes') { + return 'media/com_users/images/emergency.svg'; + } + + return $map[$name] ?? $name; + } + + /** + * Process the modules list on Joomla! 4. + * + * Joomla! 4.x is passing an Event object. The first argument of the event object is the array of modules. After + * filtering it we have to overwrite the event argument (NOT just return the new list of modules). If a future + * version of Joomla! uses immutable events we'll have to use Reflection to do that or Joomla! would have to fix + * the way this event is handled, taking its return into account. For now, we just abuse the mutable event + * properties - a feature of the event objects we discussed in the Joomla! 4 Working Group back in August 2015. + * + * @param Event $event The Joomla! event object + * + * @return void + * @throws Exception + * + * @since 4.2.0 + */ + public function onAfterModuleList(Event $event): void + { + $modules = $event->getArgument(0); + + if (empty($modules)) { + return; + } + + $this->filterModules($modules); + + $event->setArgument(0, $modules); + } + + /** + * This is the Method which actually filters the sites modules based on the allowed module positions specified by + * the user. + * + * @param array $modules The list of the site's modules. Passed by reference. + * + * @return void The by-reference value is modified instead. + * @since 4.2.0 + * @throws Exception + */ + private function filterModules(array &$modules): void + { + $allowedPositions = $this->getAllowedModulePositions(); + + if (empty($allowedPositions)) { + $modules = []; + + return; + } + + $filtered = []; + + foreach ($modules as $module) { + if (in_array($module->position, $allowedPositions)) { + $filtered[] = $module; + } + } + + $modules = $filtered; + } + + /** + * Get a list of module positions we are allowed to display + * + * @return array + * @throws Exception + * + * @since 4.2.0 + */ + private function getAllowedModulePositions(): array + { + $isAdmin = Factory::getApplication()->isClient('administrator'); + + // Load the list of allowed module positions from the component's settings. May be different for front- and back-end + $configKey = 'allowed_positions_' . ($isAdmin ? 'backend' : 'frontend'); + $res = ComponentHelper::getParams('com_users')->get($configKey, []); + + // In the backend we must always add the 'title' module position + if ($isAdmin) { + $res[] = 'title'; + $res[] = 'toolbar'; + } + + return $res; + } } diff --git a/administrator/components/com_users/src/Model/DebuggroupModel.php b/administrator/components/com_users/src/Model/DebuggroupModel.php index bebde5ee18ce7..737c1b986ec80 100644 --- a/administrator/components/com_users/src/Model/DebuggroupModel.php +++ b/administrator/components/com_users/src/Model/DebuggroupModel.php @@ -1,4 +1,5 @@ getState('filter.component'); - - return DebugHelper::getDebugActions($component); - } - - /** - * Override getItems method. - * - * @return array - * - * @since 1.6 - */ - public function getItems() - { - $groupId = $this->getState('group_id'); - - if (($assets = parent::getItems()) && $groupId) - { - $actions = $this->getDebugActions(); - - foreach ($assets as &$asset) - { - $asset->checks = array(); - - foreach ($actions as $action) - { - $name = $action[0]; - $asset->checks[$name] = Access::checkGroup($groupId, $name, $asset->name); - } - } - } - - return $assets; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.lft', $direction = 'asc') - { - $app = Factory::getApplication(); - - // Adjust the context to support modal layouts. - $layout = $app->input->get('layout', 'default'); - - if ($layout) - { - $this->context .= '.' . $layout; - } - - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('group_id', $this->getUserStateFromRequest($this->context . '.group_id', 'group_id', 0, 'int', false)); - - $levelStart = $this->getUserStateFromRequest($this->context . '.filter.level_start', 'filter_level_start', '', 'cmd'); - $this->setState('filter.level_start', $levelStart); - - $value = $this->getUserStateFromRequest($this->context . '.filter.level_end', 'filter_level_end', '', 'cmd'); - - if ($value > 0 && $value < $levelStart) - { - $value = $levelStart; - } - - $this->setState('filter.level_end', $value); - - $this->setState('filter.component', $this->getUserStateFromRequest($this->context . '.filter.component', 'filter_component', '', 'string')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_users'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('group_id'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.level_start'); - $id .= ':' . $this->getState('filter.level_end'); - $id .= ':' . $this->getState('filter.component'); - - return parent::getStoreId($id); - } - - /** - * Get the group being debugged. - * - * @return CMSObject - * - * @since 1.6 - */ - public function getGroup() - { - $groupId = (int) $this->getState('group_id'); - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['id', 'title'])) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $groupId, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $group = $db->loadObject(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $group; - } - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.id, a.name, a.title, a.level, a.lft, a.rgt' - ) - ); - $query->from($db->quoteName('#__assets', 'a')); - - // Filter the items over the search string if set. - if ($this->getState('filter.search')) - { - $search = '%' . trim($this->getState('filter.search')) . '%'; - - // Add the clauses to the query. - $query->where( - '(' . $db->quoteName('a.name') . ' LIKE :name' - . ' OR ' . $db->quoteName('a.title') . ' LIKE :title)' - ) - ->bind(':name', $search) - ->bind(':title', $search); - } - - // Filter on the start and end levels. - $levelStart = (int) $this->getState('filter.level_start'); - $levelEnd = (int) $this->getState('filter.level_end'); - - if ($levelEnd > 0 && $levelEnd < $levelStart) - { - $levelEnd = $levelStart; - } - - if ($levelStart > 0) - { - $query->where($db->quoteName('a.level') . ' >= :levelStart') - ->bind(':levelStart', $levelStart, ParameterType::INTEGER); - } - - if ($levelEnd > 0) - { - $query->where($db->quoteName('a.level') . ' <= :levelEnd') - ->bind(':levelEnd', $levelEnd, ParameterType::INTEGER); - } - - // Filter the items over the component if set. - if ($this->getState('filter.component')) - { - $component = $this->getState('filter.component'); - $lcomponent = $component . '.%'; - $query->where( - '(' . $db->quoteName('a.name') . ' = :component' - . ' OR ' . $db->quoteName('a.name') . ' LIKE :lcomponent)' - ) - ->bind(':component', $component) - ->bind(':lcomponent', $lcomponent); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'a.title', + 'component', 'a.name', + 'a.lft', + 'a.id', + 'level_start', 'level_end', 'a.level', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Get a list of the actions. + * + * @return array + * + * @since 1.6 + */ + public function getDebugActions() + { + $component = $this->getState('filter.component'); + + return DebugHelper::getDebugActions($component); + } + + /** + * Override getItems method. + * + * @return array + * + * @since 1.6 + */ + public function getItems() + { + $groupId = $this->getState('group_id'); + + if (($assets = parent::getItems()) && $groupId) { + $actions = $this->getDebugActions(); + + foreach ($assets as &$asset) { + $asset->checks = array(); + + foreach ($actions as $action) { + $name = $action[0]; + $asset->checks[$name] = Access::checkGroup($groupId, $name, $asset->name); + } + } + } + + return $assets; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + $app = Factory::getApplication(); + + // Adjust the context to support modal layouts. + $layout = $app->input->get('layout', 'default'); + + if ($layout) { + $this->context .= '.' . $layout; + } + + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('group_id', $this->getUserStateFromRequest($this->context . '.group_id', 'group_id', 0, 'int', false)); + + $levelStart = $this->getUserStateFromRequest($this->context . '.filter.level_start', 'filter_level_start', '', 'cmd'); + $this->setState('filter.level_start', $levelStart); + + $value = $this->getUserStateFromRequest($this->context . '.filter.level_end', 'filter_level_end', '', 'cmd'); + + if ($value > 0 && $value < $levelStart) { + $value = $levelStart; + } + + $this->setState('filter.level_end', $value); + + $this->setState('filter.component', $this->getUserStateFromRequest($this->context . '.filter.component', 'filter_component', '', 'string')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_users'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('group_id'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.level_start'); + $id .= ':' . $this->getState('filter.level_end'); + $id .= ':' . $this->getState('filter.component'); + + return parent::getStoreId($id); + } + + /** + * Get the group being debugged. + * + * @return CMSObject + * + * @since 1.6 + */ + public function getGroup() + { + $groupId = (int) $this->getState('group_id'); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['id', 'title'])) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $groupId, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $group = $db->loadObject(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $group; + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.name, a.title, a.level, a.lft, a.rgt' + ) + ); + $query->from($db->quoteName('#__assets', 'a')); + + // Filter the items over the search string if set. + if ($this->getState('filter.search')) { + $search = '%' . trim($this->getState('filter.search')) . '%'; + + // Add the clauses to the query. + $query->where( + '(' . $db->quoteName('a.name') . ' LIKE :name' + . ' OR ' . $db->quoteName('a.title') . ' LIKE :title)' + ) + ->bind(':name', $search) + ->bind(':title', $search); + } + + // Filter on the start and end levels. + $levelStart = (int) $this->getState('filter.level_start'); + $levelEnd = (int) $this->getState('filter.level_end'); + + if ($levelEnd > 0 && $levelEnd < $levelStart) { + $levelEnd = $levelStart; + } + + if ($levelStart > 0) { + $query->where($db->quoteName('a.level') . ' >= :levelStart') + ->bind(':levelStart', $levelStart, ParameterType::INTEGER); + } + + if ($levelEnd > 0) { + $query->where($db->quoteName('a.level') . ' <= :levelEnd') + ->bind(':levelEnd', $levelEnd, ParameterType::INTEGER); + } + + // Filter the items over the component if set. + if ($this->getState('filter.component')) { + $component = $this->getState('filter.component'); + $lcomponent = $component . '.%'; + $query->where( + '(' . $db->quoteName('a.name') . ' = :component' + . ' OR ' . $db->quoteName('a.name') . ' LIKE :lcomponent)' + ) + ->bind(':component', $component) + ->bind(':lcomponent', $lcomponent); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } } diff --git a/administrator/components/com_users/src/Model/DebuguserModel.php b/administrator/components/com_users/src/Model/DebuguserModel.php index 9c9a23c04d399..1c8d0a49575d6 100644 --- a/administrator/components/com_users/src/Model/DebuguserModel.php +++ b/administrator/components/com_users/src/Model/DebuguserModel.php @@ -1,4 +1,5 @@ getState('filter.component'); - - return DebugHelper::getDebugActions($component); - } - - /** - * Override getItems method. - * - * @return array - * - * @since 1.6 - */ - public function getItems() - { - $userId = $this->getState('user_id'); - $user = Factory::getUser($userId); - - if (($assets = parent::getItems()) && $userId) - { - $actions = $this->getDebugActions(); - - foreach ($assets as &$asset) - { - $asset->checks = array(); - - foreach ($actions as $action) - { - $name = $action[0]; - $asset->checks[$name] = $user->authorise($name, $asset->name); - } - } - } - - return $assets; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState($ordering = 'a.lft', $direction = 'asc') - { - $app = Factory::getApplication(); - - // Adjust the context to support modal layouts. - $layout = $app->input->get('layout', 'default'); - - if ($layout) - { - $this->context .= '.' . $layout; - } - - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('user_id', $this->getUserStateFromRequest($this->context . '.user_id', 'user_id', 0, 'int', false)); - - $levelStart = $this->getUserStateFromRequest($this->context . '.filter.level_start', 'filter_level_start', '', 'cmd'); - $this->setState('filter.level_start', $levelStart); - - $value = $this->getUserStateFromRequest($this->context . '.filter.level_end', 'filter_level_end', '', 'cmd'); - - if ($value > 0 && $value < $levelStart) - { - $value = $levelStart; - } - - $this->setState('filter.level_end', $value); - - $this->setState('filter.component', $this->getUserStateFromRequest($this->context . '.filter.component', 'filter_component', '', 'string')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_users'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('user_id'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.level_start'); - $id .= ':' . $this->getState('filter.level_end'); - $id .= ':' . $this->getState('filter.component'); - - return parent::getStoreId($id); - } - - /** - * Get the user being debugged. - * - * @return User - * - * @since 1.6 - */ - public function getUser() - { - $userId = $this->getState('user_id'); - - return Factory::getUser($userId); - } - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.id, a.name, a.title, a.level, a.lft, a.rgt' - ) - ); - $query->from($db->quoteName('#__assets', 'a')); - - // Filter the items over the search string if set. - if ($this->getState('filter.search')) - { - $search = '%' . trim($this->getState('filter.search')) . '%'; - - // Add the clauses to the query. - $query->where( - '(' . $db->quoteName('a.name') . ' LIKE :name' - . ' OR ' . $db->quoteName('a.title') . ' LIKE :title)' - ) - ->bind(':name', $search) - ->bind(':title', $search); - } - - // Filter on the start and end levels. - $levelStart = (int) $this->getState('filter.level_start'); - $levelEnd = (int) $this->getState('filter.level_end'); - - if ($levelEnd > 0 && $levelEnd < $levelStart) - { - $levelEnd = $levelStart; - } - - if ($levelStart > 0) - { - $query->where($db->quoteName('a.level') . ' >= :levelStart') - ->bind(':levelStart', $levelStart, ParameterType::INTEGER); - } - - if ($levelEnd > 0) - { - $query->where($db->quoteName('a.level') . ' <= :levelEnd') - ->bind(':levelEnd', $levelEnd, ParameterType::INTEGER); - } - - // Filter the items over the component if set. - if ($this->getState('filter.component')) - { - $component = $this->getState('filter.component'); - $lcomponent = $component . '.%'; - $query->where( - '(' . $db->quoteName('a.name') . ' = :component' - . ' OR ' . $db->quoteName('a.name') . ' LIKE :lcomponent)' - ) - ->bind(':component', $component) - ->bind(':lcomponent', $lcomponent); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'a.title', + 'component', 'a.name', + 'a.lft', + 'a.id', + 'level_start', 'level_end', 'a.level', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Get a list of the actions. + * + * @return array + * + * @since 1.6 + */ + public function getDebugActions() + { + $component = $this->getState('filter.component'); + + return DebugHelper::getDebugActions($component); + } + + /** + * Override getItems method. + * + * @return array + * + * @since 1.6 + */ + public function getItems() + { + $userId = $this->getState('user_id'); + $user = Factory::getUser($userId); + + if (($assets = parent::getItems()) && $userId) { + $actions = $this->getDebugActions(); + + foreach ($assets as &$asset) { + $asset->checks = array(); + + foreach ($actions as $action) { + $name = $action[0]; + $asset->checks[$name] = $user->authorise($name, $asset->name); + } + } + } + + return $assets; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + $app = Factory::getApplication(); + + // Adjust the context to support modal layouts. + $layout = $app->input->get('layout', 'default'); + + if ($layout) { + $this->context .= '.' . $layout; + } + + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('user_id', $this->getUserStateFromRequest($this->context . '.user_id', 'user_id', 0, 'int', false)); + + $levelStart = $this->getUserStateFromRequest($this->context . '.filter.level_start', 'filter_level_start', '', 'cmd'); + $this->setState('filter.level_start', $levelStart); + + $value = $this->getUserStateFromRequest($this->context . '.filter.level_end', 'filter_level_end', '', 'cmd'); + + if ($value > 0 && $value < $levelStart) { + $value = $levelStart; + } + + $this->setState('filter.level_end', $value); + + $this->setState('filter.component', $this->getUserStateFromRequest($this->context . '.filter.component', 'filter_component', '', 'string')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_users'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('user_id'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.level_start'); + $id .= ':' . $this->getState('filter.level_end'); + $id .= ':' . $this->getState('filter.component'); + + return parent::getStoreId($id); + } + + /** + * Get the user being debugged. + * + * @return User + * + * @since 1.6 + */ + public function getUser() + { + $userId = $this->getState('user_id'); + + return Factory::getUser($userId); + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.name, a.title, a.level, a.lft, a.rgt' + ) + ); + $query->from($db->quoteName('#__assets', 'a')); + + // Filter the items over the search string if set. + if ($this->getState('filter.search')) { + $search = '%' . trim($this->getState('filter.search')) . '%'; + + // Add the clauses to the query. + $query->where( + '(' . $db->quoteName('a.name') . ' LIKE :name' + . ' OR ' . $db->quoteName('a.title') . ' LIKE :title)' + ) + ->bind(':name', $search) + ->bind(':title', $search); + } + + // Filter on the start and end levels. + $levelStart = (int) $this->getState('filter.level_start'); + $levelEnd = (int) $this->getState('filter.level_end'); + + if ($levelEnd > 0 && $levelEnd < $levelStart) { + $levelEnd = $levelStart; + } + + if ($levelStart > 0) { + $query->where($db->quoteName('a.level') . ' >= :levelStart') + ->bind(':levelStart', $levelStart, ParameterType::INTEGER); + } + + if ($levelEnd > 0) { + $query->where($db->quoteName('a.level') . ' <= :levelEnd') + ->bind(':levelEnd', $levelEnd, ParameterType::INTEGER); + } + + // Filter the items over the component if set. + if ($this->getState('filter.component')) { + $component = $this->getState('filter.component'); + $lcomponent = $component . '.%'; + $query->where( + '(' . $db->quoteName('a.name') . ' = :component' + . ' OR ' . $db->quoteName('a.name') . ' LIKE :lcomponent)' + ) + ->bind(':component', $component) + ->bind(':lcomponent', $lcomponent); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } } diff --git a/administrator/components/com_users/src/Model/GroupModel.php b/administrator/components/com_users/src/Model/GroupModel.php index 5b770fd105a57..281121f819a69 100644 --- a/administrator/components/com_users/src/Model/GroupModel.php +++ b/administrator/components/com_users/src/Model/GroupModel.php @@ -1,4 +1,5 @@ 'onUserAfterDeleteGroup', - 'event_after_save' => 'onUserAfterSaveGroup', - 'event_before_delete' => 'onUserBeforeDeleteGroup', - 'event_before_save' => 'onUserBeforeSaveGroup', - 'events_map' => array('delete' => 'user', 'save' => 'user') - ), $config - ); - - parent::__construct($config, $factory); - } - - /** - * Returns a reference to the a Table object, always creating it. - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - * - * @since 1.6 - */ - public function getTable($type = 'Usergroup', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - $return = Table::getInstance($type, $prefix, $config); - - return $return; - } - - /** - * Method to get the record form. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.group', 'group', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_users.edit.group.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_users.group', $data); - - return $data; - } - - /** - * Override preprocessForm to load the user plugin group instead of content. - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error loading the form. - */ - protected function preprocessForm(Form $form, $data, $group = '') - { - $obj = is_array($data) ? ArrayHelper::toObject($data, CMSObject::class) : $data; - - if (isset($obj->parent_id) && $obj->parent_id == 0 && $obj->id > 0) - { - $form->setFieldAttribute('parent_id', 'type', 'hidden'); - $form->setFieldAttribute('parent_id', 'hidden', 'true'); - } - - parent::preprocessForm($form, $data, 'user'); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - // Include the user plugins for events. - PluginHelper::importPlugin($this->events_map['save']); - - /** - * Check the super admin permissions for group - * We get the parent group permissions and then check the group permissions manually - * We have to calculate the group permissions manually because we haven't saved the group yet - */ - $parentSuperAdmin = Access::checkGroup($data['parent_id'], 'core.admin'); - - // Get core.admin rules from the root asset - $rules = Access::getAssetRules('root.1')->getData('core.admin'); - - // Get the value for the current group (will be true (allowed), false (denied), or null (inherit) - $groupSuperAdmin = $rules['core.admin']->allow($data['id']); - - // We only need to change the $groupSuperAdmin if the parent is true or false. Otherwise, the value set in the rule takes effect. - if ($parentSuperAdmin === false) - { - // If parent is false (Denied), effective value will always be false - $groupSuperAdmin = false; - } - elseif ($parentSuperAdmin === true) - { - // If parent is true (allowed), group is true unless explicitly set to false - $groupSuperAdmin = ($groupSuperAdmin === false) ? false : true; - } - - // Check for non-super admin trying to save with super admin group - $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); - - if (!$iAmSuperAdmin && $groupSuperAdmin) - { - $this->setError(Text::_('JLIB_USER_ERROR_NOT_SUPERADMIN')); - - return false; - } - - /** - * Check for super-admin changing self to be non-super-admin - * First, are we a super admin - */ - if ($iAmSuperAdmin) - { - // Next, are we a member of the current group? - $myGroups = Access::getGroupsByUser(Factory::getUser()->get('id'), false); - - if (in_array($data['id'], $myGroups)) - { - // Now, would we have super admin permissions without the current group? - $otherGroups = array_diff($myGroups, array($data['id'])); - $otherSuperAdmin = false; - - foreach ($otherGroups as $otherGroup) - { - $otherSuperAdmin = $otherSuperAdmin ?: Access::checkGroup($otherGroup, 'core.admin'); - } - - /** - * If we would not otherwise have super admin permissions - * and the current group does not have super admin permissions, throw an exception - */ - if ((!$otherSuperAdmin) && (!$groupSuperAdmin)) - { - $this->setError(Text::_('JLIB_USER_ERROR_CANNOT_DEMOTE_SELF')); - - return false; - } - } - } - - if (Factory::getApplication()->input->get('task') == 'save2copy') - { - $data['title'] = $this->generateGroupTitle($data['parent_id'], $data['title']); - } - - // Proceed with the save - return parent::save($data); - } - - /** - * Method to delete rows. - * - * @param array &$pks An array of item ids. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function delete(&$pks) - { - // Typecast variable. - $pks = (array) $pks; - $user = Factory::getUser(); - $groups = Access::getGroupsByUser($user->get('id')); - - // Get a row instance. - $table = $this->getTable(); - - // Load plugins. - PluginHelper::importPlugin($this->events_map['delete']); - - // Check if I am a Super Admin - $iAmSuperAdmin = $user->authorise('core.admin'); - - foreach ($pks as $pk) - { - // Do not allow to delete groups to which the current user belongs - if (in_array($pk, $groups)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_DELETE_ERROR_INVALID_GROUP'), 'error'); - - return false; - } - elseif (!$table->load($pk)) - { - // Item is not in the table. - $this->setError($table->getError()); - - return false; - } - } - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - // Access checks. - $allow = $user->authorise('core.edit.state', 'com_users'); - - // Don't allow non-super-admin to delete a super admin - $allow = (!$iAmSuperAdmin && Access::checkGroup($pk, 'core.admin')) ? false : $allow; - - if ($allow) - { - // Fire the before delete event. - Factory::getApplication()->triggerEvent($this->event_before_delete, array($table->getProperties())); - - if (!$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - else - { - // Trigger the after delete event. - Factory::getApplication()->triggerEvent($this->event_after_delete, array($table->getProperties(), true, $this->getError())); - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); - } - } - } - - return true; - } - - /** - * Method to generate the title of group on Save as Copy action - * - * @param integer $parentId The id of the parent. - * @param string $title The title of group - * - * @return string Contains the modified title. - * - * @since 3.3.7 - */ - protected function generateGroupTitle($parentId, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('title' => $title, 'parent_id' => $parentId))) - { - if ($title == $table->title) - { - $title = StringHelper::increment($title); - } - } - - return $title; - } + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $config = array_merge( + array( + 'event_after_delete' => 'onUserAfterDeleteGroup', + 'event_after_save' => 'onUserAfterSaveGroup', + 'event_before_delete' => 'onUserBeforeDeleteGroup', + 'event_before_save' => 'onUserBeforeSaveGroup', + 'events_map' => array('delete' => 'user', 'save' => 'user') + ), + $config + ); + + parent::__construct($config, $factory); + } + + /** + * Returns a reference to the a Table object, always creating it. + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + * + * @since 1.6 + */ + public function getTable($type = 'Usergroup', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + $return = Table::getInstance($type, $prefix, $config); + + return $return; + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.group', 'group', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_users.edit.group.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_users.group', $data); + + return $data; + } + + /** + * Override preprocessForm to load the user plugin group instead of content. + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error loading the form. + */ + protected function preprocessForm(Form $form, $data, $group = '') + { + $obj = is_array($data) ? ArrayHelper::toObject($data, CMSObject::class) : $data; + + if (isset($obj->parent_id) && $obj->parent_id == 0 && $obj->id > 0) { + $form->setFieldAttribute('parent_id', 'type', 'hidden'); + $form->setFieldAttribute('parent_id', 'hidden', 'true'); + } + + parent::preprocessForm($form, $data, 'user'); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + // Include the user plugins for events. + PluginHelper::importPlugin($this->events_map['save']); + + /** + * Check the super admin permissions for group + * We get the parent group permissions and then check the group permissions manually + * We have to calculate the group permissions manually because we haven't saved the group yet + */ + $parentSuperAdmin = Access::checkGroup($data['parent_id'], 'core.admin'); + + // Get core.admin rules from the root asset + $rules = Access::getAssetRules('root.1')->getData('core.admin'); + + // Get the value for the current group (will be true (allowed), false (denied), or null (inherit) + $groupSuperAdmin = $rules['core.admin']->allow($data['id']); + + // We only need to change the $groupSuperAdmin if the parent is true or false. Otherwise, the value set in the rule takes effect. + if ($parentSuperAdmin === false) { + // If parent is false (Denied), effective value will always be false + $groupSuperAdmin = false; + } elseif ($parentSuperAdmin === true) { + // If parent is true (allowed), group is true unless explicitly set to false + $groupSuperAdmin = ($groupSuperAdmin === false) ? false : true; + } + + // Check for non-super admin trying to save with super admin group + $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); + + if (!$iAmSuperAdmin && $groupSuperAdmin) { + $this->setError(Text::_('JLIB_USER_ERROR_NOT_SUPERADMIN')); + + return false; + } + + /** + * Check for super-admin changing self to be non-super-admin + * First, are we a super admin + */ + if ($iAmSuperAdmin) { + // Next, are we a member of the current group? + $myGroups = Access::getGroupsByUser(Factory::getUser()->get('id'), false); + + if (in_array($data['id'], $myGroups)) { + // Now, would we have super admin permissions without the current group? + $otherGroups = array_diff($myGroups, array($data['id'])); + $otherSuperAdmin = false; + + foreach ($otherGroups as $otherGroup) { + $otherSuperAdmin = $otherSuperAdmin ?: Access::checkGroup($otherGroup, 'core.admin'); + } + + /** + * If we would not otherwise have super admin permissions + * and the current group does not have super admin permissions, throw an exception + */ + if ((!$otherSuperAdmin) && (!$groupSuperAdmin)) { + $this->setError(Text::_('JLIB_USER_ERROR_CANNOT_DEMOTE_SELF')); + + return false; + } + } + } + + if (Factory::getApplication()->input->get('task') == 'save2copy') { + $data['title'] = $this->generateGroupTitle($data['parent_id'], $data['title']); + } + + // Proceed with the save + return parent::save($data); + } + + /** + * Method to delete rows. + * + * @param array &$pks An array of item ids. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function delete(&$pks) + { + // Typecast variable. + $pks = (array) $pks; + $user = Factory::getUser(); + $groups = Access::getGroupsByUser($user->get('id')); + + // Get a row instance. + $table = $this->getTable(); + + // Load plugins. + PluginHelper::importPlugin($this->events_map['delete']); + + // Check if I am a Super Admin + $iAmSuperAdmin = $user->authorise('core.admin'); + + foreach ($pks as $pk) { + // Do not allow to delete groups to which the current user belongs + if (in_array($pk, $groups)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_DELETE_ERROR_INVALID_GROUP'), 'error'); + + return false; + } elseif (!$table->load($pk)) { + // Item is not in the table. + $this->setError($table->getError()); + + return false; + } + } + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + // Access checks. + $allow = $user->authorise('core.edit.state', 'com_users'); + + // Don't allow non-super-admin to delete a super admin + $allow = (!$iAmSuperAdmin && Access::checkGroup($pk, 'core.admin')) ? false : $allow; + + if ($allow) { + // Fire the before delete event. + Factory::getApplication()->triggerEvent($this->event_before_delete, array($table->getProperties())); + + if (!$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } else { + // Trigger the after delete event. + Factory::getApplication()->triggerEvent($this->event_after_delete, array($table->getProperties(), true, $this->getError())); + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); + } + } + } + + return true; + } + + /** + * Method to generate the title of group on Save as Copy action + * + * @param integer $parentId The id of the parent. + * @param string $title The title of group + * + * @return string Contains the modified title. + * + * @since 3.3.7 + */ + protected function generateGroupTitle($parentId, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('title' => $title, 'parent_id' => $parentId))) { + if ($title == $table->title) { + $title = StringHelper::increment($title); + } + } + + return $title; + } } diff --git a/administrator/components/com_users/src/Model/GroupsModel.php b/administrator/components/com_users/src/Model/GroupsModel.php index c8f9083a80487..56038a2ec587d 100644 --- a/administrator/components/com_users/src/Model/GroupsModel.php +++ b/administrator/components/com_users/src/Model/GroupsModel.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Users\Administrator\Model; \defined('_JEXEC') or die; @@ -24,237 +26,219 @@ */ class GroupsModel extends ListModel { - /** - * Override parent constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * - * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel - * @since 3.2 - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null) - { - if (empty($config['filter_fields'])) - { - $config['filter_fields'] = array( - 'id', 'a.id', - 'parent_id', 'a.parent_id', - 'title', 'a.title', - 'lft', 'a.lft', - 'rgt', 'a.rgt', - ); - } - - parent::__construct($config, $factory); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.lft', $direction = 'asc') - { - // Load the parameters. - $params = ComponentHelper::getParams('com_users'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Gets the list of groups and adds expensive joins to the result set. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 1.6 - */ - public function getItems() - { - // Get a storage key. - $store = $this->getStoreId(); - - // Try to load the data from internal storage. - if (empty($this->cache[$store])) - { - $items = parent::getItems(); - - // Bail out on an error or empty list. - if (empty($items)) - { - $this->cache[$store] = $items; - - return $items; - } - - try - { - $items = $this->populateExtraData($items); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Add the items to the internal cache. - $this->cache[$store] = $items; - } - - return $this->cache[$store]; - } - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.*' - ) - ); - $query->from($db->quoteName('#__usergroups') . ' AS a'); - - // Filter the comments over the search string if set. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . trim($search) . '%'; - $query->where($db->quoteName('a.title') . ' LIKE :title'); - $query->bind(':title', $search); - } - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Populate level & path for items. - * - * @param array $items Array of \stdClass objects - * - * @return array - * - * @since 3.6.3 - */ - private function populateExtraData(array $items) - { - // First pass: get list of the group ids and reset the counts. - $groupsByKey = array(); - - foreach ($items as $item) - { - $groupsByKey[(int) $item->id] = $item; - } - - $groupIds = array_keys($groupsByKey); - - $db = $this->getDatabase(); - - // Get total enabled users in group. - $query = $db->getQuery(true); - - // Count the objects in the user group. - $query->select('map.group_id, COUNT(DISTINCT map.user_id) AS user_count') - ->from($db->quoteName('#__user_usergroup_map', 'map')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id')) - ->whereIn($db->quoteName('map.group_id'), $groupIds) - ->where($db->quoteName('u.block') . ' = 0') - ->group($db->quoteName('map.group_id')); - $db->setQuery($query); - - try - { - $countEnabled = $db->loadAssocList('group_id', 'count_enabled'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get total disabled users in group. - $query->clear(); - $query->select('map.group_id, COUNT(DISTINCT map.user_id) AS user_count') - ->from($db->quoteName('#__user_usergroup_map', 'map')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id')) - ->whereIn($db->quoteName('map.group_id'), $groupIds) - ->where($db->quoteName('u.block') . ' = 1') - ->group($db->quoteName('map.group_id')); - $db->setQuery($query); - - try - { - $countDisabled = $db->loadAssocList('group_id', 'count_disabled'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Inject the values back into the array. - foreach ($groupsByKey as &$item) - { - $item->count_enabled = isset($countEnabled[$item->id]) ? (int) $countEnabled[$item->id]['user_count'] : 0; - $item->count_disabled = isset($countDisabled[$item->id]) ? (int) $countDisabled[$item->id]['user_count'] : 0; - $item->user_count = $item->count_enabled + $item->count_disabled; - } - - $groups = new UserGroupsHelper($groupsByKey); - - return array_values($groups->getAll()); - } + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'parent_id', 'a.parent_id', + 'title', 'a.title', + 'lft', 'a.lft', + 'rgt', 'a.rgt', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + // Load the parameters. + $params = ComponentHelper::getParams('com_users'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Gets the list of groups and adds expensive joins to the result set. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 1.6 + */ + public function getItems() + { + // Get a storage key. + $store = $this->getStoreId(); + + // Try to load the data from internal storage. + if (empty($this->cache[$store])) { + $items = parent::getItems(); + + // Bail out on an error or empty list. + if (empty($items)) { + $this->cache[$store] = $items; + + return $items; + } + + try { + $items = $this->populateExtraData($items); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Add the items to the internal cache. + $this->cache[$store] = $items; + } + + return $this->cache[$store]; + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.*' + ) + ); + $query->from($db->quoteName('#__usergroups') . ' AS a'); + + // Filter the comments over the search string if set. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . trim($search) . '%'; + $query->where($db->quoteName('a.title') . ' LIKE :title'); + $query->bind(':title', $search); + } + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Populate level & path for items. + * + * @param array $items Array of \stdClass objects + * + * @return array + * + * @since 3.6.3 + */ + private function populateExtraData(array $items) + { + // First pass: get list of the group ids and reset the counts. + $groupsByKey = array(); + + foreach ($items as $item) { + $groupsByKey[(int) $item->id] = $item; + } + + $groupIds = array_keys($groupsByKey); + + $db = $this->getDatabase(); + + // Get total enabled users in group. + $query = $db->getQuery(true); + + // Count the objects in the user group. + $query->select('map.group_id, COUNT(DISTINCT map.user_id) AS user_count') + ->from($db->quoteName('#__user_usergroup_map', 'map')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id')) + ->whereIn($db->quoteName('map.group_id'), $groupIds) + ->where($db->quoteName('u.block') . ' = 0') + ->group($db->quoteName('map.group_id')); + $db->setQuery($query); + + try { + $countEnabled = $db->loadAssocList('group_id', 'count_enabled'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get total disabled users in group. + $query->clear(); + $query->select('map.group_id, COUNT(DISTINCT map.user_id) AS user_count') + ->from($db->quoteName('#__user_usergroup_map', 'map')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id')) + ->whereIn($db->quoteName('map.group_id'), $groupIds) + ->where($db->quoteName('u.block') . ' = 1') + ->group($db->quoteName('map.group_id')); + $db->setQuery($query); + + try { + $countDisabled = $db->loadAssocList('group_id', 'count_disabled'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Inject the values back into the array. + foreach ($groupsByKey as &$item) { + $item->count_enabled = isset($countEnabled[$item->id]) ? (int) $countEnabled[$item->id]['user_count'] : 0; + $item->count_disabled = isset($countDisabled[$item->id]) ? (int) $countDisabled[$item->id]['user_count'] : 0; + $item->user_count = $item->count_enabled + $item->count_disabled; + } + + $groups = new UserGroupsHelper($groupsByKey); + + return array_values($groups->getAll()); + } } diff --git a/administrator/components/com_users/src/Model/LevelModel.php b/administrator/components/com_users/src/Model/LevelModel.php index 3cd56420196db..3a3f12c9a3ca2 100644 --- a/administrator/components/com_users/src/Model/LevelModel.php +++ b/administrator/components/com_users/src/Model/LevelModel.php @@ -1,4 +1,5 @@ rules); - - if ($groups === null) - { - throw new \RuntimeException('Invalid rules schema'); - } - - $isAdmin = Factory::getUser()->authorise('core.admin'); - - // Check permissions - foreach ($groups as $group) - { - if (!$isAdmin && Access::checkGroup($group, 'core.admin')) - { - $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); - - return false; - } - } - - // Check if the access level is being used by any content. - if ($this->levelsInUse === null) - { - // Populate the list once. - $this->levelsInUse = array(); - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('DISTINCT access'); - - // Get all the tables and the prefix - $tables = $db->getTableList(); - $prefix = $db->getPrefix(); - - foreach ($tables as $table) - { - // Get all of the columns in the table - $fields = $db->getTableColumns($table); - - /** - * We are looking for the access field. If custom tables are using something other - * than the 'access' field they are on their own unfortunately. - * Also make sure the table prefix matches the live db prefix (eg, it is not a "bak_" table) - */ - if (strpos($table, $prefix) === 0 && isset($fields['access'])) - { - // Lookup the distinct values of the field. - $query->clear('from') - ->from($db->quoteName($table)); - $db->setQuery($query); - - try - { - $values = $db->loadColumn(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $this->levelsInUse = array_merge($this->levelsInUse, $values); - - // @todo Could assemble an array of the tables used by each view level list those, - // giving the user a clue in the error where to look. - } - } - - // Get uniques. - $this->levelsInUse = array_unique($this->levelsInUse); - - // Ok, after all that we are ready to check the record :) - } - - if (in_array($record->id, $this->levelsInUse)) - { - $this->setError(Text::sprintf('COM_USERS_ERROR_VIEW_LEVEL_IN_USE', $record->id, $record->title)); - - return false; - } - - return parent::canDelete($record); - } - - /** - * Returns a reference to the a Table object, always creating it. - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - * - * @since 1.6 - */ - public function getTable($type = 'ViewLevel', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - $return = Table::getInstance($type, $prefix, $config); - - return $return; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - $result = parent::getItem($pk); - - // Convert the params field to an array. - $result->rules = json_decode($result->rules); - - return $result; - } - - /** - * Method to get the record form. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.level', 'level', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_users.edit.level.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_users.level', $data); - - return $data; - } - - /** - * Method to preprocess the form - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error loading the form. - */ - protected function preprocessForm(Form $form, $data, $group = '') - { - // TO DO warning! - parent::preprocessForm($form, $data, 'user'); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - if (!isset($data['rules'])) - { - $data['rules'] = array(); - } - - $data['title'] = InputFilter::getInstance()->clean($data['title'], 'TRIM'); - - return parent::save($data); - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see \Joomla\CMS\Form\FormRule - * @see \JFilterInput - * @since 3.8.8 - */ - public function validate($form, $data, $group = null) - { - $isSuperAdmin = Factory::getUser()->authorise('core.admin'); - - // Non Super user should not be able to change the access levels of super user groups - if (!$isSuperAdmin) - { - if (!isset($data['rules']) || !is_array($data['rules'])) - { - $data['rules'] = array(); - } - - $groups = array_values(UserGroupsHelper::getInstance()->getAll()); - - $rules = array(); - - if (!empty($data['id'])) - { - $table = $this->getTable(); - - $table->load($data['id']); - - $rules = json_decode($table->rules); - } - - $rules = ArrayHelper::toInteger($rules); - - for ($i = 0, $n = count($groups); $i < $n; ++$i) - { - if (Access::checkGroup((int) $groups[$i]->id, 'core.admin')) - { - if (in_array((int) $groups[$i]->id, $rules) && !in_array((int) $groups[$i]->id, $data['rules'])) - { - $data['rules'][] = (int) $groups[$i]->id; - } - elseif (!in_array((int) $groups[$i]->id, $rules) && in_array((int) $groups[$i]->id, $data['rules'])) - { - $this->setError(Text::_('JLIB_USER_ERROR_NOT_SUPERADMIN')); - - return false; - } - } - } - } - - return parent::validate($form, $data, $group); - } + /** + * @var array A list of the access levels in use. + * @since 1.6 + */ + protected $levelsInUse = null; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + $groups = json_decode($record->rules); + + if ($groups === null) { + throw new \RuntimeException('Invalid rules schema'); + } + + $isAdmin = Factory::getUser()->authorise('core.admin'); + + // Check permissions + foreach ($groups as $group) { + if (!$isAdmin && Access::checkGroup($group, 'core.admin')) { + $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); + + return false; + } + } + + // Check if the access level is being used by any content. + if ($this->levelsInUse === null) { + // Populate the list once. + $this->levelsInUse = array(); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('DISTINCT access'); + + // Get all the tables and the prefix + $tables = $db->getTableList(); + $prefix = $db->getPrefix(); + + foreach ($tables as $table) { + // Get all of the columns in the table + $fields = $db->getTableColumns($table); + + /** + * We are looking for the access field. If custom tables are using something other + * than the 'access' field they are on their own unfortunately. + * Also make sure the table prefix matches the live db prefix (eg, it is not a "bak_" table) + */ + if (strpos($table, $prefix) === 0 && isset($fields['access'])) { + // Lookup the distinct values of the field. + $query->clear('from') + ->from($db->quoteName($table)); + $db->setQuery($query); + + try { + $values = $db->loadColumn(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + $this->levelsInUse = array_merge($this->levelsInUse, $values); + + // @todo Could assemble an array of the tables used by each view level list those, + // giving the user a clue in the error where to look. + } + } + + // Get uniques. + $this->levelsInUse = array_unique($this->levelsInUse); + + // Ok, after all that we are ready to check the record :) + } + + if (in_array($record->id, $this->levelsInUse)) { + $this->setError(Text::sprintf('COM_USERS_ERROR_VIEW_LEVEL_IN_USE', $record->id, $record->title)); + + return false; + } + + return parent::canDelete($record); + } + + /** + * Returns a reference to the a Table object, always creating it. + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + * + * @since 1.6 + */ + public function getTable($type = 'ViewLevel', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + $return = Table::getInstance($type, $prefix, $config); + + return $return; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + $result = parent::getItem($pk); + + // Convert the params field to an array. + $result->rules = json_decode($result->rules); + + return $result; + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.level', 'level', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_users.edit.level.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_users.level', $data); + + return $data; + } + + /** + * Method to preprocess the form + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error loading the form. + */ + protected function preprocessForm(Form $form, $data, $group = '') + { + // TO DO warning! + parent::preprocessForm($form, $data, 'user'); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + if (!isset($data['rules'])) { + $data['rules'] = array(); + } + + $data['title'] = InputFilter::getInstance()->clean($data['title'], 'TRIM'); + + return parent::save($data); + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see \Joomla\CMS\Form\FormRule + * @see \JFilterInput + * @since 3.8.8 + */ + public function validate($form, $data, $group = null) + { + $isSuperAdmin = Factory::getUser()->authorise('core.admin'); + + // Non Super user should not be able to change the access levels of super user groups + if (!$isSuperAdmin) { + if (!isset($data['rules']) || !is_array($data['rules'])) { + $data['rules'] = array(); + } + + $groups = array_values(UserGroupsHelper::getInstance()->getAll()); + + $rules = array(); + + if (!empty($data['id'])) { + $table = $this->getTable(); + + $table->load($data['id']); + + $rules = json_decode($table->rules); + } + + $rules = ArrayHelper::toInteger($rules); + + for ($i = 0, $n = count($groups); $i < $n; ++$i) { + if (Access::checkGroup((int) $groups[$i]->id, 'core.admin')) { + if (in_array((int) $groups[$i]->id, $rules) && !in_array((int) $groups[$i]->id, $data['rules'])) { + $data['rules'][] = (int) $groups[$i]->id; + } elseif (!in_array((int) $groups[$i]->id, $rules) && in_array((int) $groups[$i]->id, $data['rules'])) { + $this->setError(Text::_('JLIB_USER_ERROR_NOT_SUPERADMIN')); + + return false; + } + } + } + } + + return parent::validate($form, $data, $group); + } } diff --git a/administrator/components/com_users/src/Model/LevelsModel.php b/administrator/components/com_users/src/Model/LevelsModel.php index 7bd91ce32c56e..c6b61a3b8c4ae 100644 --- a/administrator/components/com_users/src/Model/LevelsModel.php +++ b/administrator/components/com_users/src/Model/LevelsModel.php @@ -1,4 +1,5 @@ setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.*' - ) - ); - $query->from($db->quoteName('#__viewlevels') . ' AS a'); - - // Add the level in the tree. - $query->group('a.id, a.title, a.ordering, a.rules'); - - // Filter the items over the search string if set. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . trim($search) . '%'; - $query->where('a.title LIKE :title') - ->bind(':title', $search); - } - } - - $query->group('a.id'); - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to adjust the ordering of a row. - * - * @param integer $pk The ID of the primary key to move. - * @param integer $direction Increment, usually +1 or -1 - * - * @return boolean False on failure or error, true otherwise. - */ - public function reorder($pk, $direction = 0) - { - // Sanitize the id and adjustment. - $pk = (!empty($pk)) ? $pk : (int) $this->getState('level.id'); - $user = Factory::getUser(); - - // Get an instance of the record's table. - $table = Table::getInstance('ViewLevel', 'Joomla\\CMS\Table\\'); - - // Load the row. - if (!$table->load($pk)) - { - $this->setError($table->getError()); - - return false; - } - - // Access checks. - $allow = $user->authorise('core.edit.state', 'com_users'); - - if (!$allow) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); - - return false; - } - - // Move the row. - // @todo: Where clause to restrict category. - $table->move($pk); - - return true; - } - - /** - * Saves the manually set order of records. - * - * @param array $pks An array of primary key ids. - * @param integer $order Order position - * - * @return boolean Boolean true on success, boolean false - * - * @throws \Exception - */ - public function saveorder($pks, $order) - { - $table = Table::getInstance('viewlevel', 'Joomla\\CMS\Table\\'); - $user = Factory::getUser(); - $conditions = array(); - - if (empty($pks)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_ERROR_LEVELS_NOLEVELS_SELECTED'), 'error'); - - return false; - } - - // Update ordering values - foreach ($pks as $i => $pk) - { - $table->load((int) $pk); - - // Access checks. - $allow = $user->authorise('core.edit.state', 'com_users'); - - if (!$allow) - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - } - elseif ($table->ordering != $order[$i]) - { - $table->ordering = $order[$i]; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - } - - // Execute reorder for each category. - foreach ($conditions as $cond) - { - $table->load($cond[0]); - $table->reorder($cond[1]); - } - - return true; - } + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'ordering', 'a.ordering', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.ordering', $direction = 'asc') + { + // Load the parameters. + $params = ComponentHelper::getParams('com_users'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.*' + ) + ); + $query->from($db->quoteName('#__viewlevels') . ' AS a'); + + // Add the level in the tree. + $query->group('a.id, a.title, a.ordering, a.rules'); + + // Filter the items over the search string if set. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . trim($search) . '%'; + $query->where('a.title LIKE :title') + ->bind(':title', $search); + } + } + + $query->group('a.id'); + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to adjust the ordering of a row. + * + * @param integer $pk The ID of the primary key to move. + * @param integer $direction Increment, usually +1 or -1 + * + * @return boolean False on failure or error, true otherwise. + */ + public function reorder($pk, $direction = 0) + { + // Sanitize the id and adjustment. + $pk = (!empty($pk)) ? $pk : (int) $this->getState('level.id'); + $user = Factory::getUser(); + + // Get an instance of the record's table. + $table = Table::getInstance('ViewLevel', 'Joomla\\CMS\Table\\'); + + // Load the row. + if (!$table->load($pk)) { + $this->setError($table->getError()); + + return false; + } + + // Access checks. + $allow = $user->authorise('core.edit.state', 'com_users'); + + if (!$allow) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); + + return false; + } + + // Move the row. + // @todo: Where clause to restrict category. + $table->move($pk); + + return true; + } + + /** + * Saves the manually set order of records. + * + * @param array $pks An array of primary key ids. + * @param integer $order Order position + * + * @return boolean Boolean true on success, boolean false + * + * @throws \Exception + */ + public function saveorder($pks, $order) + { + $table = Table::getInstance('viewlevel', 'Joomla\\CMS\Table\\'); + $user = Factory::getUser(); + $conditions = array(); + + if (empty($pks)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_ERROR_LEVELS_NOLEVELS_SELECTED'), 'error'); + + return false; + } + + // Update ordering values + foreach ($pks as $i => $pk) { + $table->load((int) $pk); + + // Access checks. + $allow = $user->authorise('core.edit.state', 'com_users'); + + if (!$allow) { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + } elseif ($table->ordering != $order[$i]) { + $table->ordering = $order[$i]; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } + } + + // Execute reorder for each category. + foreach ($conditions as $cond) { + $table->load($cond[0]); + $table->reorder($cond[1]); + } + + return true; + } } diff --git a/administrator/components/com_users/src/Model/MailModel.php b/administrator/components/com_users/src/Model/MailModel.php index 35b3c5dfc696e..ce3bf1a667d9c 100644 --- a/administrator/components/com_users/src/Model/MailModel.php +++ b/administrator/components/com_users/src/Model/MailModel.php @@ -1,4 +1,5 @@ loadForm('com_users.mail', 'mail', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_users.display.mail.data', array()); - - $this->preprocessData('com_users.mail', $data); - - return $data; - } - - /** - * Method to preprocess the form - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error loading the form. - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - parent::preprocessForm($form, $data, $group); - } - - /** - * Send the email - * - * @return boolean - * - * @throws \Exception - */ - public function send() - { - $app = Factory::getApplication(); - $data = $app->input->post->get('jform', array(), 'array'); - $user = Factory::getUser(); - $access = new Access; - $db = $this->getDatabase(); - $language = Factory::getLanguage(); - - $mode = array_key_exists('mode', $data) ? (int) $data['mode'] : 0; - $subject = array_key_exists('subject', $data) ? $data['subject'] : ''; - $grp = array_key_exists('group', $data) ? (int) $data['group'] : 0; - $recurse = array_key_exists('recurse', $data) ? (int) $data['recurse'] : 0; - $bcc = array_key_exists('bcc', $data) ? (int) $data['bcc'] : 0; - $disabled = array_key_exists('disabled', $data) ? (int) $data['disabled'] : 0; - $message_body = array_key_exists('message', $data) ? $data['message'] : ''; - - // Automatically removes html formatting - if (!$mode) - { - $message_body = InputFilter::getInstance()->clean($message_body, 'string'); - } - - // Check for a message body and subject - if (!$message_body || !$subject) - { - $app->setUserState('com_users.display.mail.data', $data); - $this->setError(Text::_('COM_USERS_MAIL_PLEASE_FILL_IN_THE_FORM_CORRECTLY')); - - return false; - } - - // Get users in the group out of the ACL, if group is provided. - $to = $grp !== 0 ? $access->getUsersByGroup($grp, $recurse) : array(); - - // When group is provided but no users are found in the group. - if ($grp !== 0 && !$to) - { - $rows = array(); - } - else - { - // Get all users email and group except for senders - $uid = (int) $user->id; - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('email'), - $db->quoteName('name'), - ] - ) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('id') . ' != :id') - ->bind(':id', $uid, ParameterType::INTEGER); - - if ($grp !== 0) - { - $query->whereIn($db->quoteName('id'), $to); - } - - if ($disabled === 0) - { - $query->where($db->quoteName('block') . ' = 0'); - } - - $db->setQuery($query); - $rows = $db->loadObjectList(); - } - - // Check to see if there are any users in this group before we continue - if (!$rows) - { - $app->setUserState('com_users.display.mail.data', $data); - - if (in_array($user->id, $to)) - { - $this->setError(Text::_('COM_USERS_MAIL_ONLY_YOU_COULD_BE_FOUND_IN_THIS_GROUP')); - } - else - { - $this->setError(Text::_('COM_USERS_MAIL_NO_USERS_COULD_BE_FOUND_IN_THIS_GROUP')); - } - - return false; - } - - // Get the Mailer - $mailer = new MailTemplate('com_users.massmail.mail', $language->getTag()); - $params = ComponentHelper::getParams('com_users'); - - try - { - // Build email message format. - $data = [ - 'subject' => stripslashes($subject), - 'body' => $message_body, - 'subjectprefix' => $params->get('mailSubjectPrefix', ''), - 'bodysuffix' => $params->get('mailBodySuffix', '') - ]; - $mailer->addTemplateData($data); - - $recipientType = $bcc ? 'bcc' : 'to'; - - // Add recipients - foreach ($rows as $row) - { - $mailer->addRecipient($row->email, $row->name, $recipientType); - } - - if ($bcc) - { - $mailer->addRecipient($app->get('mailfrom'), $app->get('fromname')); - } - - // Send the Mail - $rs = $mailer->send(); - } - catch (MailDisabledException | phpMailerException $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $rs = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $rs = false; - } - } - - // Check for an error - if ($rs !== true) - { - $app->setUserState('com_users.display.mail.data', $data); - $this->setError($mailer->ErrorInfo); - - return false; - } - elseif (empty($rs)) - { - $app->setUserState('com_users.display.mail.data', $data); - $this->setError(Text::_('COM_USERS_MAIL_THE_MAIL_COULD_NOT_BE_SENT')); - - return false; - } - else - { - /** - * Fill the data (specially for the 'mode', 'group' and 'bcc': they could not exist in the array - * when the box is not checked and in this case, the default value would be used instead of the '0' - * one) - */ - $data['mode'] = $mode; - $data['subject'] = $subject; - $data['group'] = $grp; - $data['recurse'] = $recurse; - $data['bcc'] = $bcc; - $data['message'] = $message_body; - $app->setUserState('com_users.display.mail.data', array()); - $app->enqueueMessage(Text::plural('COM_USERS_MAIL_EMAIL_SENT_TO_N_USERS', count($rows)), 'message'); - - return true; - } - } + /** + * Method to get the row form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.mail', 'mail', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_users.display.mail.data', array()); + + $this->preprocessData('com_users.mail', $data); + + return $data; + } + + /** + * Method to preprocess the form + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error loading the form. + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + parent::preprocessForm($form, $data, $group); + } + + /** + * Send the email + * + * @return boolean + * + * @throws \Exception + */ + public function send() + { + $app = Factory::getApplication(); + $data = $app->input->post->get('jform', array(), 'array'); + $user = Factory::getUser(); + $access = new Access(); + $db = $this->getDatabase(); + $language = Factory::getLanguage(); + + $mode = array_key_exists('mode', $data) ? (int) $data['mode'] : 0; + $subject = array_key_exists('subject', $data) ? $data['subject'] : ''; + $grp = array_key_exists('group', $data) ? (int) $data['group'] : 0; + $recurse = array_key_exists('recurse', $data) ? (int) $data['recurse'] : 0; + $bcc = array_key_exists('bcc', $data) ? (int) $data['bcc'] : 0; + $disabled = array_key_exists('disabled', $data) ? (int) $data['disabled'] : 0; + $message_body = array_key_exists('message', $data) ? $data['message'] : ''; + + // Automatically removes html formatting + if (!$mode) { + $message_body = InputFilter::getInstance()->clean($message_body, 'string'); + } + + // Check for a message body and subject + if (!$message_body || !$subject) { + $app->setUserState('com_users.display.mail.data', $data); + $this->setError(Text::_('COM_USERS_MAIL_PLEASE_FILL_IN_THE_FORM_CORRECTLY')); + + return false; + } + + // Get users in the group out of the ACL, if group is provided. + $to = $grp !== 0 ? $access->getUsersByGroup($grp, $recurse) : array(); + + // When group is provided but no users are found in the group. + if ($grp !== 0 && !$to) { + $rows = array(); + } else { + // Get all users email and group except for senders + $uid = (int) $user->id; + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('email'), + $db->quoteName('name'), + ] + ) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('id') . ' != :id') + ->bind(':id', $uid, ParameterType::INTEGER); + + if ($grp !== 0) { + $query->whereIn($db->quoteName('id'), $to); + } + + if ($disabled === 0) { + $query->where($db->quoteName('block') . ' = 0'); + } + + $db->setQuery($query); + $rows = $db->loadObjectList(); + } + + // Check to see if there are any users in this group before we continue + if (!$rows) { + $app->setUserState('com_users.display.mail.data', $data); + + if (in_array($user->id, $to)) { + $this->setError(Text::_('COM_USERS_MAIL_ONLY_YOU_COULD_BE_FOUND_IN_THIS_GROUP')); + } else { + $this->setError(Text::_('COM_USERS_MAIL_NO_USERS_COULD_BE_FOUND_IN_THIS_GROUP')); + } + + return false; + } + + // Get the Mailer + $mailer = new MailTemplate('com_users.massmail.mail', $language->getTag()); + $params = ComponentHelper::getParams('com_users'); + + try { + // Build email message format. + $data = [ + 'subject' => stripslashes($subject), + 'body' => $message_body, + 'subjectprefix' => $params->get('mailSubjectPrefix', ''), + 'bodysuffix' => $params->get('mailBodySuffix', '') + ]; + $mailer->addTemplateData($data); + + $recipientType = $bcc ? 'bcc' : 'to'; + + // Add recipients + foreach ($rows as $row) { + $mailer->addRecipient($row->email, $row->name, $recipientType); + } + + if ($bcc) { + $mailer->addRecipient($app->get('mailfrom'), $app->get('fromname')); + } + + // Send the Mail + $rs = $mailer->send(); + } catch (MailDisabledException | phpMailerException $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $rs = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $rs = false; + } + } + + // Check for an error + if ($rs !== true) { + $app->setUserState('com_users.display.mail.data', $data); + $this->setError($mailer->ErrorInfo); + + return false; + } elseif (empty($rs)) { + $app->setUserState('com_users.display.mail.data', $data); + $this->setError(Text::_('COM_USERS_MAIL_THE_MAIL_COULD_NOT_BE_SENT')); + + return false; + } else { + /** + * Fill the data (specially for the 'mode', 'group' and 'bcc': they could not exist in the array + * when the box is not checked and in this case, the default value would be used instead of the '0' + * one) + */ + $data['mode'] = $mode; + $data['subject'] = $subject; + $data['group'] = $grp; + $data['recurse'] = $recurse; + $data['bcc'] = $bcc; + $data['message'] = $message_body; + $app->setUserState('com_users.display.mail.data', array()); + $app->enqueueMessage(Text::plural('COM_USERS_MAIL_EMAIL_SENT_TO_N_USERS', count($rows)), 'message'); + + return true; + } + } } diff --git a/administrator/components/com_users/src/Model/MethodModel.php b/administrator/components/com_users/src/Model/MethodModel.php index c909f6cf8b78f..32df3181299a8 100644 --- a/administrator/components/com_users/src/Model/MethodModel.php +++ b/administrator/components/com_users/src/Model/MethodModel.php @@ -1,4 +1,5 @@ methodExists($method)) - { - return [ - 'name' => $method, - 'display' => '', - 'shortinfo' => '', - 'image' => '', - 'canDisable' => true, - 'allowMultiple' => true, - ]; - } - - return $this->mfaMethods[$method]; - } - - /** - * Is the specified MFA Method available? - * - * @param string $method The Method to check. - * - * @return boolean - * @since 4.2.0 - */ - public function methodExists(string $method): bool - { - if (!is_array($this->mfaMethods)) - { - $this->populateMfaMethods(); - } - - return isset($this->mfaMethods[$method]); - } - - /** - * @param User|null $user The user record. Null to use the currently logged in user. - * - * @return array - * @throws Exception - * - * @since 4.2.0 - */ - public function getRenderOptions(?User $user = null): SetupRenderOptions - { - if (is_null($user)) - { - $user = Factory::getApplication()->getIdentity() ?: Factory::getUser(); - } - - $renderOptions = new SetupRenderOptions; - - $event = new GetSetup($this->getRecord($user)); - $results = Factory::getApplication() - ->getDispatcher() - ->dispatch($event->getName(), $event) - ->getArgument('result', []); - - if (empty($results)) - { - return $renderOptions; - } - - foreach ($results as $result) - { - if (empty($result)) - { - continue; - } - - return $renderOptions->merge($result); - } - - return $renderOptions; - } - - /** - * Get the specified MFA record. It will return a fake default record when no record ID is specified. - * - * @param User|null $user The user record. Null to use the currently logged in user. - * - * @return MfaTable - * @throws Exception - * - * @since 4.2.0 - */ - public function getRecord(User $user = null): MfaTable - { - if (is_null($user)) - { - $user = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - $defaultRecord = $this->getDefaultRecord($user); - $id = (int) $this->getState('id', 0); - - if ($id <= 0) - { - return $defaultRecord; - } - - /** @var MfaTable $record */ - $record = $this->getTable('Mfa', 'Administrator'); - $loaded = $record->load( - [ - 'user_id' => $user->id, - 'id' => $id, - ] - ); - - if (!$loaded) - { - return $defaultRecord; - } - - if (!$this->methodExists($record->method)) - { - return $defaultRecord; - } - - return $record; - } - - /** - * Return the title to use for the page - * - * @return string - * - * @since 4.2.0 - */ - public function getPageTitle(): string - { - $task = $this->getState('task', 'edit'); - - switch ($task) - { - case 'mfa': - $key = 'COM_USERS_USER_MULTIFACTOR_AUTH'; - break; - - default: - $key = sprintf('COM_USERS_MFA_%s_PAGE_HEAD', $task); - break; - } - - return Text::_($key); - } - - /** - * @param User|null $user The user record. Null to use the current user. - * - * @return MfaTable - * @throws Exception - * - * @since 4.2.0 - */ - protected function getDefaultRecord(?User $user = null): MfaTable - { - if (is_null($user)) - { - $user = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - $method = $this->getState('method'); - $title = ''; - - if (is_null($this->mfaMethods)) - { - $this->populateMfaMethods(); - } - - if ($method && isset($this->mfaMethods[$method])) - { - $title = $this->mfaMethods[$method]['display']; - } - - /** @var MfaTable $record */ - $record = $this->getTable('Mfa', 'Administrator'); - - $record->bind( - [ - 'id' => null, - 'user_id' => $user->id, - 'title' => $title, - 'method' => $method, - 'default' => 0, - 'options' => [], - ] - ); - - return $record; - } - - /** - * Populate the list of MFA Methods - * - * @return void - * @since 4.2.0 - */ - private function populateMfaMethods(): void - { - $this->mfaMethods = []; - $mfaMethods = MfaHelper::getMfaMethods(); - - if (empty($mfaMethods)) - { - return; - } - - foreach ($mfaMethods as $method) - { - $this->mfaMethods[$method['name']] = $method; - } - - // We also need to add the backup codes Method - $this->mfaMethods['backupcodes'] = [ - 'name' => 'backupcodes', - 'display' => Text::_('COM_USERS_USER_BACKUPCODES'), - 'shortinfo' => Text::_('COM_USERS_USER_BACKUPCODES_DESC'), - 'image' => 'media/com_users/images/emergency.svg', - 'canDisable' => false, - 'allowMultiple' => false, - ]; - } + /** + * List of MFA Methods + * + * @var array + * @since 4.2.0 + */ + protected $mfaMethods = null; + + /** + * Get the specified MFA Method's record + * + * @param string $method The Method to retrieve. + * + * @return array + * @since 4.2.0 + */ + public function getMethod(string $method): array + { + if (!$this->methodExists($method)) { + return [ + 'name' => $method, + 'display' => '', + 'shortinfo' => '', + 'image' => '', + 'canDisable' => true, + 'allowMultiple' => true, + ]; + } + + return $this->mfaMethods[$method]; + } + + /** + * Is the specified MFA Method available? + * + * @param string $method The Method to check. + * + * @return boolean + * @since 4.2.0 + */ + public function methodExists(string $method): bool + { + if (!is_array($this->mfaMethods)) { + $this->populateMfaMethods(); + } + + return isset($this->mfaMethods[$method]); + } + + /** + * @param User|null $user The user record. Null to use the currently logged in user. + * + * @return array + * @throws Exception + * + * @since 4.2.0 + */ + public function getRenderOptions(?User $user = null): SetupRenderOptions + { + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() ?: Factory::getUser(); + } + + $renderOptions = new SetupRenderOptions(); + + $event = new GetSetup($this->getRecord($user)); + $results = Factory::getApplication() + ->getDispatcher() + ->dispatch($event->getName(), $event) + ->getArgument('result', []); + + if (empty($results)) { + return $renderOptions; + } + + foreach ($results as $result) { + if (empty($result)) { + continue; + } + + return $renderOptions->merge($result); + } + + return $renderOptions; + } + + /** + * Get the specified MFA record. It will return a fake default record when no record ID is specified. + * + * @param User|null $user The user record. Null to use the currently logged in user. + * + * @return MfaTable + * @throws Exception + * + * @since 4.2.0 + */ + public function getRecord(User $user = null): MfaTable + { + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + $defaultRecord = $this->getDefaultRecord($user); + $id = (int) $this->getState('id', 0); + + if ($id <= 0) { + return $defaultRecord; + } + + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + $loaded = $record->load( + [ + 'user_id' => $user->id, + 'id' => $id, + ] + ); + + if (!$loaded) { + return $defaultRecord; + } + + if (!$this->methodExists($record->method)) { + return $defaultRecord; + } + + return $record; + } + + /** + * Return the title to use for the page + * + * @return string + * + * @since 4.2.0 + */ + public function getPageTitle(): string + { + $task = $this->getState('task', 'edit'); + + switch ($task) { + case 'mfa': + $key = 'COM_USERS_USER_MULTIFACTOR_AUTH'; + break; + + default: + $key = sprintf('COM_USERS_MFA_%s_PAGE_HEAD', $task); + break; + } + + return Text::_($key); + } + + /** + * @param User|null $user The user record. Null to use the current user. + * + * @return MfaTable + * @throws Exception + * + * @since 4.2.0 + */ + protected function getDefaultRecord(?User $user = null): MfaTable + { + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + $method = $this->getState('method'); + $title = ''; + + if (is_null($this->mfaMethods)) { + $this->populateMfaMethods(); + } + + if ($method && isset($this->mfaMethods[$method])) { + $title = $this->mfaMethods[$method]['display']; + } + + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + + $record->bind( + [ + 'id' => null, + 'user_id' => $user->id, + 'title' => $title, + 'method' => $method, + 'default' => 0, + 'options' => [], + ] + ); + + return $record; + } + + /** + * Populate the list of MFA Methods + * + * @return void + * @since 4.2.0 + */ + private function populateMfaMethods(): void + { + $this->mfaMethods = []; + $mfaMethods = MfaHelper::getMfaMethods(); + + if (empty($mfaMethods)) { + return; + } + + foreach ($mfaMethods as $method) { + $this->mfaMethods[$method['name']] = $method; + } + + // We also need to add the backup codes Method + $this->mfaMethods['backupcodes'] = [ + 'name' => 'backupcodes', + 'display' => Text::_('COM_USERS_USER_BACKUPCODES'), + 'shortinfo' => Text::_('COM_USERS_USER_BACKUPCODES_DESC'), + 'image' => 'media/com_users/images/emergency.svg', + 'canDisable' => false, + 'allowMultiple' => false, + ]; + } } diff --git a/administrator/components/com_users/src/Model/MethodsModel.php b/administrator/components/com_users/src/Model/MethodsModel.php index 3b34a3470f56b..e010a703edef9 100644 --- a/administrator/components/com_users/src/Model/MethodsModel.php +++ b/administrator/components/com_users/src/Model/MethodsModel.php @@ -1,4 +1,5 @@ getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - if ($user->guest) - { - return []; - } - - // Get an associative array of MFA Methods - $rawMethods = MfaHelper::getMfaMethods(); - $methods = []; - - foreach ($rawMethods as $method) - { - $method['active'] = []; - $methods[$method['name']] = $method; - } - - // Put the user MFA records into the Methods array - $userMfaRecords = MfaHelper::getUserMfaRecords($user->id); - - if (!empty($userMfaRecords)) - { - foreach ($userMfaRecords as $record) - { - if (!isset($methods[$record->method])) - { - continue; - } - - $methods[$record->method]->addActiveMethod($record); - } - } - - return $methods; - } - - /** - * Delete all Multi-factor Authentication Methods for the given user. - * - * @param User|null $user The user object to reset MFA for. Null to use the current user. - * - * @return void - * @throws Exception - * - * @since 4.2.0 - */ - public function deleteAll(?User $user = null): void - { - // Make sure we have a user object - if (is_null($user)) - { - $user = Factory::getApplication()->getIdentity() ?: Factory::getUser(); - } - - // If the user object is a guest (who can't have MFA) we abort with an error - if ($user->guest) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__user_mfa')) - ->where($db->quoteName('user_id') . ' = :user_id') - ->bind(':user_id', $user->id, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - } - - /** - * Format a relative timestamp. It deals with timestamps today and yesterday in a special manner. Example returns: - * Yesterday, 13:12 - * Today, 08:33 - * January 1, 2015 - * - * @param string $dateTimeText The database time string to use, e.g. "2017-01-13 13:25:36" - * - * @return string The formatted, human-readable date - * @throws Exception - * - * @since 4.2.0 - */ - public function formatRelative(?string $dateTimeText): string - { - if (empty($dateTimeText)) - { - return Text::_('JNEVER'); - } - - // The timestamp is given in UTC. Make sure Joomla! parses it as such. - $utcTimeZone = new DateTimeZone('UTC'); - $jDate = new Date($dateTimeText, $utcTimeZone); - $unixStamp = $jDate->toUnix(); - - // I'm pretty sure we didn't have MFA in Joomla back in 1970 ;) - if ($unixStamp < 0) - { - return Text::_('JNEVER'); - } - - // I need to display the date in the user's local timezone. That's how you do it. - $user = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - $userTZ = $user->getParam('timezone', 'UTC'); - $tz = new DateTimeZone($userTZ); - $jDate->setTimezone($tz); - - // Default format string: way in the past, the time of the day is not important - $formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_PAST'); - $containerString = Text::_('COM_USERS_MFA_LBL_PAST'); - - // If the timestamp is within the last 72 hours we may need a special format - if ($unixStamp > (time() - (72 * 3600))) - { - // Is this timestamp today? - $jNow = new Date; - $jNow->setTimezone($tz); - $checkNow = $jNow->format('Ymd', true); - $checkDate = $jDate->format('Ymd', true); - - if ($checkDate == $checkNow) - { - $formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_TODAY'); - $containerString = Text::_('COM_USERS_MFA_LBL_TODAY'); - } - else - { - // Is this timestamp yesterday? - $jYesterday = clone $jNow; - $jYesterday->setTime(0, 0, 0); - $oneSecond = new DateInterval('PT1S'); - $jYesterday->sub($oneSecond); - $checkYesterday = $jYesterday->format('Ymd', true); - - if ($checkDate == $checkYesterday) - { - $formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_YESTERDAY'); - $containerString = Text::_('COM_USERS_MFA_LBL_YESTERDAY'); - } - } - } - - return sprintf($containerString, $jDate->format($formatString, true)); - } - - /** - * Set the user's "don't show this again" flag. - * - * @param User $user The user to check - * @param bool $flag True to set the flag, false to unset it (it will be set to 0, actually) - * - * @return void - * - * @since 4.2.0 - */ - public function setFlag(User $user, bool $flag = true): void - { - $db = $this->getDatabase(); - $profileKey = 'mfa.dontshow'; - $query = $db->getQuery(true) - ->select($db->quoteName('profile_value')) - ->from($db->quoteName('#__user_profiles')) - ->where($db->quoteName('user_id') . ' = :user_id') - ->where($db->quoteName('profile_key') . ' = :profileKey') - ->bind(':user_id', $user->id, ParameterType::INTEGER) - ->bind(':profileKey', $profileKey, ParameterType::STRING); - - try - { - $result = $db->setQuery($query)->loadResult(); - } - catch (Exception $e) - { - return; - } - - $exists = !is_null($result); - - $object = (object) [ - 'user_id' => $user->id, - 'profile_key' => 'mfa.dontshow', - 'profile_value' => ($flag ? 1 : 0), - 'ordering' => 1, - ]; - - if (!$exists) - { - $db->insertObject('#__user_profiles', $object); - } - else - { - $db->updateObject('#__user_profiles', $object, ['user_id', 'profile_key']); - } - } + /** + * Returns a list of all available MFA methods and their currently active records for a given user. + * + * @param User|null $user The user object. Skip to use the current user. + * + * @return array + * @throws Exception + * + * @since 4.2.0 + */ + public function getMethods(?User $user = null): array + { + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + if ($user->guest) { + return []; + } + + // Get an associative array of MFA Methods + $rawMethods = MfaHelper::getMfaMethods(); + $methods = []; + + foreach ($rawMethods as $method) { + $method['active'] = []; + $methods[$method['name']] = $method; + } + + // Put the user MFA records into the Methods array + $userMfaRecords = MfaHelper::getUserMfaRecords($user->id); + + if (!empty($userMfaRecords)) { + foreach ($userMfaRecords as $record) { + if (!isset($methods[$record->method])) { + continue; + } + + $methods[$record->method]->addActiveMethod($record); + } + } + + return $methods; + } + + /** + * Delete all Multi-factor Authentication Methods for the given user. + * + * @param User|null $user The user object to reset MFA for. Null to use the current user. + * + * @return void + * @throws Exception + * + * @since 4.2.0 + */ + public function deleteAll(?User $user = null): void + { + // Make sure we have a user object + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() ?: Factory::getUser(); + } + + // If the user object is a guest (who can't have MFA) we abort with an error + if ($user->guest) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__user_mfa')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->bind(':user_id', $user->id, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + } + + /** + * Format a relative timestamp. It deals with timestamps today and yesterday in a special manner. Example returns: + * Yesterday, 13:12 + * Today, 08:33 + * January 1, 2015 + * + * @param string $dateTimeText The database time string to use, e.g. "2017-01-13 13:25:36" + * + * @return string The formatted, human-readable date + * @throws Exception + * + * @since 4.2.0 + */ + public function formatRelative(?string $dateTimeText): string + { + if (empty($dateTimeText)) { + return Text::_('JNEVER'); + } + + // The timestamp is given in UTC. Make sure Joomla! parses it as such. + $utcTimeZone = new DateTimeZone('UTC'); + $jDate = new Date($dateTimeText, $utcTimeZone); + $unixStamp = $jDate->toUnix(); + + // I'm pretty sure we didn't have MFA in Joomla back in 1970 ;) + if ($unixStamp < 0) { + return Text::_('JNEVER'); + } + + // I need to display the date in the user's local timezone. That's how you do it. + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + $userTZ = $user->getParam('timezone', 'UTC'); + $tz = new DateTimeZone($userTZ); + $jDate->setTimezone($tz); + + // Default format string: way in the past, the time of the day is not important + $formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_PAST'); + $containerString = Text::_('COM_USERS_MFA_LBL_PAST'); + + // If the timestamp is within the last 72 hours we may need a special format + if ($unixStamp > (time() - (72 * 3600))) { + // Is this timestamp today? + $jNow = new Date(); + $jNow->setTimezone($tz); + $checkNow = $jNow->format('Ymd', true); + $checkDate = $jDate->format('Ymd', true); + + if ($checkDate == $checkNow) { + $formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_TODAY'); + $containerString = Text::_('COM_USERS_MFA_LBL_TODAY'); + } else { + // Is this timestamp yesterday? + $jYesterday = clone $jNow; + $jYesterday->setTime(0, 0, 0); + $oneSecond = new DateInterval('PT1S'); + $jYesterday->sub($oneSecond); + $checkYesterday = $jYesterday->format('Ymd', true); + + if ($checkDate == $checkYesterday) { + $formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_YESTERDAY'); + $containerString = Text::_('COM_USERS_MFA_LBL_YESTERDAY'); + } + } + } + + return sprintf($containerString, $jDate->format($formatString, true)); + } + + /** + * Set the user's "don't show this again" flag. + * + * @param User $user The user to check + * @param bool $flag True to set the flag, false to unset it (it will be set to 0, actually) + * + * @return void + * + * @since 4.2.0 + */ + public function setFlag(User $user, bool $flag = true): void + { + $db = $this->getDatabase(); + $profileKey = 'mfa.dontshow'; + $query = $db->getQuery(true) + ->select($db->quoteName('profile_value')) + ->from($db->quoteName('#__user_profiles')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->where($db->quoteName('profile_key') . ' = :profileKey') + ->bind(':user_id', $user->id, ParameterType::INTEGER) + ->bind(':profileKey', $profileKey, ParameterType::STRING); + + try { + $result = $db->setQuery($query)->loadResult(); + } catch (Exception $e) { + return; + } + + $exists = !is_null($result); + + $object = (object) [ + 'user_id' => $user->id, + 'profile_key' => 'mfa.dontshow', + 'profile_value' => ($flag ? 1 : 0), + 'ordering' => 1, + ]; + + if (!$exists) { + $db->insertObject('#__user_profiles', $object); + } else { + $db->updateObject('#__user_profiles', $object, ['user_id', 'profile_key']); + } + } } diff --git a/administrator/components/com_users/src/Model/NoteModel.php b/administrator/components/com_users/src/Model/NoteModel.php index 974f7a62d16f5..61e7eb17c9f3a 100644 --- a/administrator/components/com_users/src/Model/NoteModel.php +++ b/administrator/components/com_users/src/Model/NoteModel.php @@ -1,4 +1,5 @@ loadForm('com_users.note', 'note', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 2.5 - * @throws \Exception - */ - public function getItem($pk = null) - { - $result = parent::getItem($pk); - - // Get the dispatcher and load the content plugins. - PluginHelper::importPlugin('content'); - - // Load the user plugins for backward compatibility (v3.3.3 and earlier). - PluginHelper::importPlugin('user'); - - // Trigger the data preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.note', $result)); - - return $result; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Get the application - $app = Factory::getApplication(); - - // Check the session for previously entered form data. - $data = $app->getUserState('com_users.edit.note.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Prime some default values. - if ($this->getState('note.id') == 0) - { - $data->set('catid', $app->input->get('catid', $app->getUserState('com_users.notes.filter.category_id'), 'int')); - } - - $userId = $app->input->get('u_id', 0, 'int'); - - if ($userId != 0) - { - $data->user_id = $userId; - } - } - - $this->preprocessData('com_users.note', $data); - - return $data; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 2.5 - * @throws \Exception - */ - protected function populateState() - { - parent::populateState(); - - $userId = Factory::getApplication()->input->get('u_id', 0, 'int'); - $this->setState('note.user_id', $userId); - } + use VersionableModelTrait; + + /** + * The type alias for this content type. + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_users.note'; + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|bool A Form object on success, false on failure + * + * @since 2.5 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.note', 'note', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 2.5 + * @throws \Exception + */ + public function getItem($pk = null) + { + $result = parent::getItem($pk); + + // Get the dispatcher and load the content plugins. + PluginHelper::importPlugin('content'); + + // Load the user plugins for backward compatibility (v3.3.3 and earlier). + PluginHelper::importPlugin('user'); + + // Trigger the data preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.note', $result)); + + return $result; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Get the application + $app = Factory::getApplication(); + + // Check the session for previously entered form data. + $data = $app->getUserState('com_users.edit.note.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Prime some default values. + if ($this->getState('note.id') == 0) { + $data->set('catid', $app->input->get('catid', $app->getUserState('com_users.notes.filter.category_id'), 'int')); + } + + $userId = $app->input->get('u_id', 0, 'int'); + + if ($userId != 0) { + $data->user_id = $userId; + } + } + + $this->preprocessData('com_users.note', $data); + + return $data; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 2.5 + * @throws \Exception + */ + protected function populateState() + { + parent::populateState(); + + $userId = Factory::getApplication()->input->get('u_id', 0, 'int'); + $this->setState('note.user_id', $userId); + } } diff --git a/administrator/components/com_users/src/Model/NotesModel.php b/administrator/components/com_users/src/Model/NotesModel.php index c6778588cfd21..4921bb4ba94b3 100644 --- a/administrator/components/com_users/src/Model/NotesModel.php +++ b/administrator/components/com_users/src/Model/NotesModel.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState('list.select', - 'a.id, a.subject, a.checked_out, a.checked_out_time,' . - 'a.catid, a.created_time, a.review_time,' . - 'a.state, a.publish_up, a.publish_down' - ) - ); - $query->from('#__user_notes AS a'); - - // Join over the category - $query->select('c.title AS category_title, c.params AS category_params') - ->join('LEFT', '#__categories AS c ON c.id = a.catid'); - - // Join over the users for the note user. - $query->select('u.name AS user_name') - ->join('LEFT', '#__users AS u ON u.id = a.user_id'); - - // Join over the users for the checked out user. - $query->select('uc.name AS editor') - ->join('LEFT', '#__users AS uc ON uc.id = a.checked_out'); - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search3 = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $search3, ParameterType::INTEGER); - } - elseif (stripos($search, 'uid:') === 0) - { - $search4 = (int) substr($search, 4); - $query->where($db->quoteName('a.user_id') . ' = :id'); - $query->bind(':id', $search4, ParameterType::INTEGER); - } - else - { - $search = '%' . trim($search) . '%'; - $query->where( - '(' . $db->quoteName('a.subject') . ' LIKE :subject' - . ' OR ' . $db->quoteName('u.name') . ' LIKE :name' - . ' OR ' . $db->quoteName('u.username') . ' LIKE :username)' - ); - $query->bind(':subject', $search); - $query->bind(':name', $search); - $query->bind(':username', $search); - } - } - - // Filter by published state - $published = $this->getState('filter.published'); - - if (is_numeric($published)) - { - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $published, ParameterType::INTEGER); - } - elseif ($published !== '*') - { - $query->whereIn($db->quoteName('a.state'), [0, 1]); - } - - // Filter by a single category. - $categoryId = (int) $this->getState('filter.category_id'); - - if ($categoryId) - { - $query->where($db->quoteName('a.catid') . ' = :catid') - ->bind(':catid', $categoryId, ParameterType::INTEGER); - } - - // Filter by a single user. - $userId = (int) $this->getState('filter.user_id'); - - if ($userId) - { - // Add the body and where filter. - $query->select('a.body') - ->where($db->quoteName('a.user_id') . ' = :user_id') - ->bind(':user_id', $userId, ParameterType::INTEGER); - } - - // Filter on the level. - if ($level = $this->getState('filter.level')) - { - $level = (int) $level; - $query->where($db->quoteName('c.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.review_time')) . ' ' . $db->escape($this->getState('list.direction', 'DESC'))); - - return $query; - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.category_id'); - $id .= ':' . $this->getState('filter.user_id'); - $id .= ':' . $this->getState('filter.level'); - - return parent::getStoreId($id); - } - - /** - * Gets a user object if the user filter is set. - * - * @return User The User object - * - * @since 2.5 - */ - public function getUser() - { - $user = new User; - - // Filter by search in title - $search = (int) $this->getState('filter.user_id'); - - if ($search != 0) - { - $user->load((int) $search); - } - - return $user; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState($ordering = 'a.review_time', $direction = 'desc') - { - // Adjust the context to support modal layouts. - if ($layout = Factory::getApplication()->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - parent::populateState($ordering, $direction); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + // Set the list ordering fields. + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'user_id', 'a.user_id', + 'u.name', + 'subject', 'a.subject', + 'catid', 'a.catid', 'category_id', + 'state', 'a.state', 'published', + 'c.title', + 'review_time', 'a.review_time', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'level', 'c.level', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery A DatabaseQuery object to retrieve the data set. + * + * @since 2.5 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.subject, a.checked_out, a.checked_out_time,' . + 'a.catid, a.created_time, a.review_time,' . + 'a.state, a.publish_up, a.publish_down' + ) + ); + $query->from('#__user_notes AS a'); + + // Join over the category + $query->select('c.title AS category_title, c.params AS category_params') + ->join('LEFT', '#__categories AS c ON c.id = a.catid'); + + // Join over the users for the note user. + $query->select('u.name AS user_name') + ->join('LEFT', '#__users AS u ON u.id = a.user_id'); + + // Join over the users for the checked out user. + $query->select('uc.name AS editor') + ->join('LEFT', '#__users AS uc ON uc.id = a.checked_out'); + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search3 = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $search3, ParameterType::INTEGER); + } elseif (stripos($search, 'uid:') === 0) { + $search4 = (int) substr($search, 4); + $query->where($db->quoteName('a.user_id') . ' = :id'); + $query->bind(':id', $search4, ParameterType::INTEGER); + } else { + $search = '%' . trim($search) . '%'; + $query->where( + '(' . $db->quoteName('a.subject') . ' LIKE :subject' + . ' OR ' . $db->quoteName('u.name') . ' LIKE :name' + . ' OR ' . $db->quoteName('u.username') . ' LIKE :username)' + ); + $query->bind(':subject', $search); + $query->bind(':name', $search); + $query->bind(':username', $search); + } + } + + // Filter by published state + $published = $this->getState('filter.published'); + + if (is_numeric($published)) { + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $published, ParameterType::INTEGER); + } elseif ($published !== '*') { + $query->whereIn($db->quoteName('a.state'), [0, 1]); + } + + // Filter by a single category. + $categoryId = (int) $this->getState('filter.category_id'); + + if ($categoryId) { + $query->where($db->quoteName('a.catid') . ' = :catid') + ->bind(':catid', $categoryId, ParameterType::INTEGER); + } + + // Filter by a single user. + $userId = (int) $this->getState('filter.user_id'); + + if ($userId) { + // Add the body and where filter. + $query->select('a.body') + ->where($db->quoteName('a.user_id') . ' = :user_id') + ->bind(':user_id', $userId, ParameterType::INTEGER); + } + + // Filter on the level. + if ($level = $this->getState('filter.level')) { + $level = (int) $level; + $query->where($db->quoteName('c.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.review_time')) . ' ' . $db->escape($this->getState('list.direction', 'DESC'))); + + return $query; + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.category_id'); + $id .= ':' . $this->getState('filter.user_id'); + $id .= ':' . $this->getState('filter.level'); + + return parent::getStoreId($id); + } + + /** + * Gets a user object if the user filter is set. + * + * @return User The User object + * + * @since 2.5 + */ + public function getUser() + { + $user = new User(); + + // Filter by search in title + $search = (int) $this->getState('filter.user_id'); + + if ($search != 0) { + $user->load((int) $search); + } + + return $user; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState($ordering = 'a.review_time', $direction = 'desc') + { + // Adjust the context to support modal layouts. + if ($layout = Factory::getApplication()->input->get('layout')) { + $this->context .= '.' . $layout; + } + + parent::populateState($ordering, $direction); + } } diff --git a/administrator/components/com_users/src/Model/UserModel.php b/administrator/components/com_users/src/Model/UserModel.php index c2240b21609d0..e60ea6c50a649 100644 --- a/administrator/components/com_users/src/Model/UserModel.php +++ b/administrator/components/com_users/src/Model/UserModel.php @@ -1,4 +1,5 @@ 'onUserAfterDelete', - 'event_after_save' => 'onUserAfterSave', - 'event_before_delete' => 'onUserBeforeDelete', - 'event_before_save' => 'onUserBeforeSave', - 'events_map' => array('save' => 'user', 'delete' => 'user', 'validate' => 'user') - ), $config - ); - - parent::__construct($config, $factory); - } - - /** - * Returns a reference to the a Table object, always creating it. - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - * - * @since 1.6 - */ - public function getTable($type = 'User', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - $table = Table::getInstance($type, $prefix, $config); - - return $table; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('user.id'); - - if ($this->_item === null) - { - $this->_item = array(); - } - - if (!isset($this->_item[$pk])) - { - $this->_item[$pk] = parent::getItem($pk); - } - - return $this->_item[$pk]; - } - - /** - * Method to get the record form. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.user', 'user', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - $user = Factory::getUser(); - - // If the user needs to change their password, mark the password fields as required - if ($user->requireReset) - { - $form->setFieldAttribute('password', 'required', 'true'); - $form->setFieldAttribute('password2', 'required', 'true'); - } - - // When multilanguage is set, a user's default site language should also be a Content Language - if (Multilanguage::isEnabled()) - { - $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); - } - - $userId = (int) $form->getValue('id'); - - // The user should not be able to set the requireReset value on their own account - if ($userId === (int) $user->id) - { - $form->removeField('requireReset'); - } - - /** - * If users without core.manage permission editing their own account, remove some fields which they should - * not be allowed to change and prevent them to change user name if configured - */ - if (!$user->authorise('core.manage', 'com_users') && (int) $user->id === $userId) - { - if (!ComponentHelper::getParams('com_users')->get('change_login_name')) - { - $form->setFieldAttribute('username', 'required', 'false'); - $form->setFieldAttribute('username', 'readonly', 'true'); - $form->setFieldAttribute('username', 'description', 'COM_USERS_USER_FIELD_NOCHANGE_USERNAME_DESC'); - } - - $form->removeField('lastResetTime'); - $form->removeField('resetCount'); - $form->removeField('sendEmail'); - $form->removeField('block'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_users.edit.user.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_users.profile', $data, 'user'); - - return $data; - } - - /** - * Override Joomla\CMS\MVC\Model\AdminModel::preprocessForm to ensure the correct plugin group is loaded. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - * @throws \Exception - */ - public function save($data) - { - $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('user.id'); - $user = User::getInstance($pk); - - $my = Factory::getUser(); - $iAmSuperAdmin = $my->authorise('core.admin'); - - // User cannot modify own user groups - if ((int) $user->id == (int) $my->id && !$iAmSuperAdmin && isset($data['groups'])) - { - // Form was probably tampered with - Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_USERS_ERROR_CANNOT_EDIT_OWN_GROUP'), 'warning'); - - $data['groups'] = null; - } - - if ($data['block'] && $pk == $my->id && !$my->block) - { - $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF')); - - return false; - } - - // Make sure user groups is selected when add/edit an account - if (empty($data['groups']) && ((int) $user->id != (int) $my->id || $iAmSuperAdmin)) - { - $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_SAVE_ACCOUNT_WITHOUT_GROUPS')); - - return false; - } - - // Make sure that we are not removing ourself from Super Admin group - if ($iAmSuperAdmin && $my->get('id') == $pk) - { - // Check that at least one of our new groups is Super Admin - $stillSuperAdmin = false; - $myNewGroups = $data['groups']; - - foreach ($myNewGroups as $group) - { - $stillSuperAdmin = $stillSuperAdmin ?: Access::checkGroup($group, 'core.admin'); - } - - if (!$stillSuperAdmin) - { - $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_DEMOTE_SELF')); - - return false; - } - } - - // Bind the data. - if (!$user->bind($data)) - { - $this->setError($user->getError()); - - return false; - } - - // Store the data. - if (!$user->save()) - { - $this->setError($user->getError()); - - return false; - } - - // Destroy all active sessions for the user after changing the password or blocking him - if ($data['password2'] || $data['block']) - { - UserHelper::destroyUserSessions($user->id, true); - } - - $this->setState('user.id', $user->id); - - return true; - } - - /** - * Method to delete rows. - * - * @param array &$pks An array of item ids. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function delete(&$pks) - { - $user = Factory::getUser(); - $table = $this->getTable(); - $pks = (array) $pks; - - // Check if I am a Super Admin - $iAmSuperAdmin = $user->authorise('core.admin'); - - PluginHelper::importPlugin($this->events_map['delete']); - - if (in_array($user->id, $pks)) - { - $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_DELETE_SELF')); - - return false; - } - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - // Access checks. - $allow = $user->authorise('core.delete', 'com_users'); - - // Don't allow non-super-admin to delete a super admin - $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; - - if ($allow) - { - // Get users data for the users to delete. - $user_to_delete = Factory::getUser($pk); - - // Fire the before delete event. - Factory::getApplication()->triggerEvent($this->event_before_delete, array($table->getProperties())); - - if (!$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - else - { - // Trigger the after delete event. - Factory::getApplication()->triggerEvent($this->event_after_delete, array($user_to_delete->getProperties(), true, $this->getError())); - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); - } - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - return true; - } - - /** - * Method to block user records. - * - * @param array &$pks The ids of the items to publish. - * @param integer $value The value of the published state - * - * @return boolean True on success. - * - * @since 1.6 - * @throws \Exception - */ - public function block(&$pks, $value = 1) - { - $app = Factory::getApplication(); - $user = Factory::getUser(); - - // Check if I am a Super Admin - $iAmSuperAdmin = $user->authorise('core.admin'); - $table = $this->getTable(); - $pks = (array) $pks; - - PluginHelper::importPlugin($this->events_map['save']); - - // Prepare the logout options. - $options = array( - 'clientid' => $app->get('shared_session', '0') ? null : 0, - ); - - // Access checks. - foreach ($pks as $i => $pk) - { - if ($value == 1 && $pk == $user->get('id')) - { - // Cannot block yourself. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF'), 'error'); - } - elseif ($table->load($pk)) - { - $old = $table->getProperties(); - $allow = $user->authorise('core.edit.state', 'com_users'); - - // Don't allow non-super-admin to delete a super admin - $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; - - if ($allow) - { - // Skip changing of same state - if ($table->block == $value) - { - unset($pks[$i]); - continue; - } - - $table->block = (int) $value; - - // If unblocking, also change password reset count to zero to unblock reset - if ($table->block === 0) - { - $table->resetCount = 0; - } - - // Allow an exception to be thrown. - try - { - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($old, false, $table->getProperties())); - - if (in_array(false, $result, true)) - { - // Plugin will have to raise its own error or throw an exception. - return false; - } - - // Store the table. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - if ($table->block) - { - UserHelper::destroyUserSessions($table->id); - } - - // Trigger the after save event - Factory::getApplication()->triggerEvent($this->event_after_save, [$table->getProperties(), false, true, null]); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Log the user out. - if ($value) - { - $app->logout($table->id, $options); - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - } - } - } - - return true; - } - - /** - * Method to activate user records. - * - * @param array &$pks The ids of the items to activate. - * - * @return boolean True on success. - * - * @since 1.6 - * @throws \Exception - */ - public function activate(&$pks) - { - $user = Factory::getUser(); - - // Check if I am a Super Admin - $iAmSuperAdmin = $user->authorise('core.admin'); - $table = $this->getTable(); - $pks = (array) $pks; - - PluginHelper::importPlugin($this->events_map['save']); - - // Access checks. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - $old = $table->getProperties(); - $allow = $user->authorise('core.edit.state', 'com_users'); - - // Don't allow non-super-admin to delete a super admin - $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; - - if (empty($table->activation)) - { - // Ignore activated accounts. - unset($pks[$i]); - } - elseif ($allow) - { - $table->block = 0; - $table->activation = ''; - - // Allow an exception to be thrown. - try - { - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($old, false, $table->getProperties())); - - if (in_array(false, $result, true)) - { - // Plugin will have to raise it's own error or throw an exception. - return false; - } - - // Store the table. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Fire the after save event - Factory::getApplication()->triggerEvent($this->event_after_save, [$table->getProperties(), false, true, null]); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - } - } - } - - return true; - } - - /** - * Method to perform batch operations on an item or a set of items. - * - * @param array $commands An array of commands to perform. - * @param array $pks An array of item ids. - * @param array $contexts An array of item contexts. - * - * @return boolean Returns true on success, false on failure. - * - * @since 2.5 - */ - public function batch($commands, $pks, $contexts) - { - // Sanitize user ids. - $pks = array_unique($pks); - $pks = ArrayHelper::toInteger($pks); - - // Remove any values of zero. - if (array_search(0, $pks, true)) - { - unset($pks[array_search(0, $pks, true)]); - } - - if (empty($pks)) - { - $this->setError(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED')); - - return false; - } - - $done = false; - - if (!empty($commands['group_id'])) - { - $cmd = ArrayHelper::getValue($commands, 'group_action', 'add'); - - if (!$this->batchUser((int) $commands['group_id'], $pks, $cmd)) - { - return false; - } - - $done = true; - } - - if (!empty($commands['reset_id'])) - { - if (!$this->batchReset($pks, $commands['reset_id'])) - { - return false; - } - - $done = true; - } - - if (!$done) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_INSUFFICIENT_BATCH_INFORMATION')); - - return false; - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Batch flag users as being required to reset their passwords - * - * @param array $userIds An array of user IDs on which to operate - * @param string $action The action to perform - * - * @return boolean True on success, false on failure - * - * @since 3.2 - */ - public function batchReset($userIds, $action) - { - $userIds = ArrayHelper::toInteger($userIds); - - // Check if I am a Super Admin - $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); - - // Non-super super user cannot work with super-admin user. - if (!$iAmSuperAdmin && UserHelper::checkSuperUserInUsers($userIds)) - { - $this->setError(Text::_('COM_USERS_ERROR_CANNOT_BATCH_SUPERUSER')); - - return false; - } - - // Set the action to perform - if ($action === 'yes') - { - $value = 1; - } - else - { - $value = 0; - } - - // Prune out the current user if they are in the supplied user ID array - $userIds = array_diff($userIds, array(Factory::getUser()->id)); - - if (empty($userIds)) - { - $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_REQUIRERESET_SELF')); - - return false; - } - - // Get the DB object - $db = $this->getDatabase(); - - $userIds = ArrayHelper::toInteger($userIds); - - $query = $db->getQuery(true); - - // Update the reset flag - $query->update($db->quoteName('#__users')) - ->set($db->quoteName('requireReset') . ' = :requireReset') - ->whereIn($db->quoteName('id'), $userIds) - ->bind(':requireReset', $value, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return true; - } - - /** - * Perform batch operations - * - * @param integer $groupId The group ID which assignments are being edited - * @param array $userIds An array of user IDs on which to operate - * @param string $action The action to perform - * - * @return boolean True on success, false on failure - * - * @since 1.6 - */ - public function batchUser($groupId, $userIds, $action) - { - $userIds = ArrayHelper::toInteger($userIds); - - // Check if I am a Super Admin - $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); - - // Non-super super user cannot work with super-admin user. - if (!$iAmSuperAdmin && UserHelper::checkSuperUserInUsers($userIds)) - { - $this->setError(Text::_('COM_USERS_ERROR_CANNOT_BATCH_SUPERUSER')); - - return false; - } - - // Non-super admin cannot work with super-admin group. - if ((!$iAmSuperAdmin && Access::checkGroup($groupId, 'core.admin')) || $groupId < 1) - { - $this->setError(Text::_('COM_USERS_ERROR_INVALID_GROUP')); - - return false; - } - - // Get the DB object - $db = $this->getDatabase(); - - switch ($action) - { - // Sets users to a selected group - case 'set': - $doDelete = 'all'; - $doAssign = true; - break; - - // Remove users from a selected group - case 'del': - $doDelete = 'group'; - break; - - // Add users to a selected group - case 'add': - default: - $doAssign = true; - break; - } - - // Remove the users from the group if requested. - if (isset($doDelete)) - { - $query = $db->getQuery(true); - - // Remove users from the group - $query->delete($db->quoteName('#__user_usergroup_map')) - ->whereIn($db->quoteName('user_id'), $userIds); - - // Only remove users from selected group - if ($doDelete == 'group') - { - $query->where($db->quoteName('group_id') . ' = :group_id') - ->bind(':group_id', $groupId, ParameterType::INTEGER); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - // Assign the users to the group if requested. - if (isset($doAssign)) - { - $query = $db->getQuery(true); - - // First, we need to check if the user is already assigned to a group - $query->select($db->quoteName('user_id')) - ->from($db->quoteName('#__user_usergroup_map')) - ->where($db->quoteName('group_id') . ' = :group_id') - ->bind(':group_id', $groupId, ParameterType::INTEGER); - $db->setQuery($query); - $users = $db->loadColumn(); - - // Build the values clause for the assignment query. - $query->clear(); - $groups = false; - - foreach ($userIds as $id) - { - if (!in_array($id, $users)) - { - $query->values($id . ',' . $groupId); - $groups = true; - } - } - - // If we have no users to process, throw an error to notify the user - if (!$groups) - { - $this->setError(Text::_('COM_USERS_ERROR_NO_ADDITIONS')); - - return false; - } - - $query->insert($db->quoteName('#__user_usergroup_map')) - ->columns(array($db->quoteName('user_id'), $db->quoteName('group_id'))); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - return true; - } - - /** - * Gets the available groups. - * - * @return array An array of groups - * - * @since 1.6 - */ - public function getGroups() - { - $user = Factory::getUser(); - - if ($user->authorise('core.edit', 'com_users') && $user->authorise('core.manage', 'com_users')) - { - $model = $this->bootComponent('com_users') - ->getMVCFactory()->createModel('Groups', 'Administrator', ['ignore_request' => true]); - - return $model->getItems(); - } - else - { - return null; - } - } - - /** - * Gets the groups this object is assigned to - * - * @param integer $userId The user ID to retrieve the groups for - * - * @return array An array of assigned groups - * - * @since 1.6 - */ - public function getAssignedGroups($userId = null) - { - $userId = (!empty($userId)) ? $userId : (int) $this->getState('user.id'); - - if (empty($userId)) - { - $result = array(); - $form = $this->getForm(); - - if ($form) - { - $groupsIDs = $form->getValue('groups'); - } - - if (!empty($groupsIDs)) - { - $result = $groupsIDs; - } - else - { - $params = ComponentHelper::getParams('com_users'); - - if ($groupId = $params->get('new_usertype', $params->get('guest_usergroup', 1))) - { - $result[] = $groupId; - } - } - } - else - { - $result = UserHelper::getUserGroups($userId); - } - - return $result; - } - - /** - * No longer used - * - * @param integer $userId Ignored - * - * @return \stdClass - * - * @since 3.2 - * @deprecated 4.2.0 Will be removed in 5.0 - */ - public function getOtpConfig($userId = null) - { - @trigger_error( - sprintf( - '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getUserMfaRecords() instead.', - __METHOD__ - ), - E_USER_DEPRECATED - ); - - // Return the configuration object - return (object) array( - 'method' => 'none', - 'config' => array(), - 'otep' => array() - ); - } - - /** - * No longer used - * - * @param integer $userId Ignored - * @param \stdClass $otpConfig Ignored - * - * @return boolean True on success - * - * @since 3.2 - * @deprecated 4.2.0 Will be removed in 5.0 - */ - public function setOtpConfig($userId, $otpConfig) - { - @trigger_error( - sprintf( - '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.', - __METHOD__ - ), - E_USER_DEPRECATED - ); - - return true; - } - - /** - * No longer used - * - * @return string - * - * @since 3.2 - * @deprecated 4.2.0 Will be removed in 5.0 - */ - public function getOtpConfigEncryptionKey() - { - @trigger_error( - sprintf( - '%s() is deprecated. Use \Joomla\CMS\Factory::getApplication()->get(\'secret\') instead', - __METHOD__ - ), - E_USER_DEPRECATED - ); - - return Factory::getApplication()->get('secret'); - } - - /** - * No longer used - * - * @param integer $userId Ignored - * - * @return array Empty array - * - * @since 3.2 - * @throws \Exception - * - * @deprecated 4.2.0 Will be removed in 5.0. - */ - public function getTwofactorform($userId = null) - { - @trigger_error( - sprintf( - '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getConfigurationInterface()', - __METHOD__ - ), - E_USER_DEPRECATED - ); - - return []; - } - - /** - * No longer used - * - * @param integer $userId Ignored - * @param integer $count Ignored - * - * @return array Empty array - * - * @since 3.2 - * @deprecated 4.2.0 Wil be removed in 5.0. - */ - public function generateOteps($userId, $count = 10) - { - @trigger_error( - sprintf( - '%s() is deprecated. See \Joomla\Component\Users\Administrator\Model\BackupcodesModel::saveBackupCodes()', - __METHOD__ - ), - E_USER_DEPRECATED - ); - - return []; - } - - /** - * No longer used. Always returns true. - * - * @param integer $userId Ignored - * @param string $secretKey Ignored - * @param array $options Ignored - * - * @return boolean Always true - * - * @since 3.2 - * @throws \Exception - * - * @deprecated 4.2.0 Will be removed in 5.0. MFA validation is done in the captive login. - */ - public function isValidSecretKey($userId, $secretKey, $options = array()) - { - @trigger_error( - sprintf( - '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.', - __METHOD__ - ), - E_USER_DEPRECATED - ); - - return true; - } - - /** - * No longer used - * - * @param integer $userId Ignored - * @param string $otep Ignored - * @param object $otpConfig Ignored - * - * @return boolean Always true - * - * @since 3.2 - * @deprecated 4.2.0 Will be removed in 5.0 - */ - public function isValidOtep($userId, $otep, $otpConfig = null) - { - @trigger_error( - sprintf( - '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.', - __METHOD__ - ), - E_USER_DEPRECATED - ); - - return true; - } + /** + * An item. + * + * @var array + */ + protected $_item = null; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $config = array_merge( + array( + 'event_after_delete' => 'onUserAfterDelete', + 'event_after_save' => 'onUserAfterSave', + 'event_before_delete' => 'onUserBeforeDelete', + 'event_before_save' => 'onUserBeforeSave', + 'events_map' => array('save' => 'user', 'delete' => 'user', 'validate' => 'user') + ), + $config + ); + + parent::__construct($config, $factory); + } + + /** + * Returns a reference to the a Table object, always creating it. + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + * + * @since 1.6 + */ + public function getTable($type = 'User', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + $table = Table::getInstance($type, $prefix, $config); + + return $table; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('user.id'); + + if ($this->_item === null) { + $this->_item = array(); + } + + if (!isset($this->_item[$pk])) { + $this->_item[$pk] = parent::getItem($pk); + } + + return $this->_item[$pk]; + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.user', 'user', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + $user = Factory::getUser(); + + // If the user needs to change their password, mark the password fields as required + if ($user->requireReset) { + $form->setFieldAttribute('password', 'required', 'true'); + $form->setFieldAttribute('password2', 'required', 'true'); + } + + // When multilanguage is set, a user's default site language should also be a Content Language + if (Multilanguage::isEnabled()) { + $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); + } + + $userId = (int) $form->getValue('id'); + + // The user should not be able to set the requireReset value on their own account + if ($userId === (int) $user->id) { + $form->removeField('requireReset'); + } + + /** + * If users without core.manage permission editing their own account, remove some fields which they should + * not be allowed to change and prevent them to change user name if configured + */ + if (!$user->authorise('core.manage', 'com_users') && (int) $user->id === $userId) { + if (!ComponentHelper::getParams('com_users')->get('change_login_name')) { + $form->setFieldAttribute('username', 'required', 'false'); + $form->setFieldAttribute('username', 'readonly', 'true'); + $form->setFieldAttribute('username', 'description', 'COM_USERS_USER_FIELD_NOCHANGE_USERNAME_DESC'); + } + + $form->removeField('lastResetTime'); + $form->removeField('resetCount'); + $form->removeField('sendEmail'); + $form->removeField('block'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_users.edit.user.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_users.profile', $data, 'user'); + + return $data; + } + + /** + * Override Joomla\CMS\MVC\Model\AdminModel::preprocessForm to ensure the correct plugin group is loaded. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + * @throws \Exception + */ + public function save($data) + { + $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('user.id'); + $user = User::getInstance($pk); + + $my = Factory::getUser(); + $iAmSuperAdmin = $my->authorise('core.admin'); + + // User cannot modify own user groups + if ((int) $user->id == (int) $my->id && !$iAmSuperAdmin && isset($data['groups'])) { + // Form was probably tampered with + Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_USERS_ERROR_CANNOT_EDIT_OWN_GROUP'), 'warning'); + + $data['groups'] = null; + } + + if ($data['block'] && $pk == $my->id && !$my->block) { + $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF')); + + return false; + } + + // Make sure user groups is selected when add/edit an account + if (empty($data['groups']) && ((int) $user->id != (int) $my->id || $iAmSuperAdmin)) { + $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_SAVE_ACCOUNT_WITHOUT_GROUPS')); + + return false; + } + + // Make sure that we are not removing ourself from Super Admin group + if ($iAmSuperAdmin && $my->get('id') == $pk) { + // Check that at least one of our new groups is Super Admin + $stillSuperAdmin = false; + $myNewGroups = $data['groups']; + + foreach ($myNewGroups as $group) { + $stillSuperAdmin = $stillSuperAdmin ?: Access::checkGroup($group, 'core.admin'); + } + + if (!$stillSuperAdmin) { + $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_DEMOTE_SELF')); + + return false; + } + } + + // Bind the data. + if (!$user->bind($data)) { + $this->setError($user->getError()); + + return false; + } + + // Store the data. + if (!$user->save()) { + $this->setError($user->getError()); + + return false; + } + + // Destroy all active sessions for the user after changing the password or blocking him + if ($data['password2'] || $data['block']) { + UserHelper::destroyUserSessions($user->id, true); + } + + $this->setState('user.id', $user->id); + + return true; + } + + /** + * Method to delete rows. + * + * @param array &$pks An array of item ids. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function delete(&$pks) + { + $user = Factory::getUser(); + $table = $this->getTable(); + $pks = (array) $pks; + + // Check if I am a Super Admin + $iAmSuperAdmin = $user->authorise('core.admin'); + + PluginHelper::importPlugin($this->events_map['delete']); + + if (in_array($user->id, $pks)) { + $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_DELETE_SELF')); + + return false; + } + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + // Access checks. + $allow = $user->authorise('core.delete', 'com_users'); + + // Don't allow non-super-admin to delete a super admin + $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; + + if ($allow) { + // Get users data for the users to delete. + $user_to_delete = Factory::getUser($pk); + + // Fire the before delete event. + Factory::getApplication()->triggerEvent($this->event_before_delete, array($table->getProperties())); + + if (!$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } else { + // Trigger the after delete event. + Factory::getApplication()->triggerEvent($this->event_after_delete, array($user_to_delete->getProperties(), true, $this->getError())); + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); + } + } else { + $this->setError($table->getError()); + + return false; + } + } + + return true; + } + + /** + * Method to block user records. + * + * @param array &$pks The ids of the items to publish. + * @param integer $value The value of the published state + * + * @return boolean True on success. + * + * @since 1.6 + * @throws \Exception + */ + public function block(&$pks, $value = 1) + { + $app = Factory::getApplication(); + $user = Factory::getUser(); + + // Check if I am a Super Admin + $iAmSuperAdmin = $user->authorise('core.admin'); + $table = $this->getTable(); + $pks = (array) $pks; + + PluginHelper::importPlugin($this->events_map['save']); + + // Prepare the logout options. + $options = array( + 'clientid' => $app->get('shared_session', '0') ? null : 0, + ); + + // Access checks. + foreach ($pks as $i => $pk) { + if ($value == 1 && $pk == $user->get('id')) { + // Cannot block yourself. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF'), 'error'); + } elseif ($table->load($pk)) { + $old = $table->getProperties(); + $allow = $user->authorise('core.edit.state', 'com_users'); + + // Don't allow non-super-admin to delete a super admin + $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; + + if ($allow) { + // Skip changing of same state + if ($table->block == $value) { + unset($pks[$i]); + continue; + } + + $table->block = (int) $value; + + // If unblocking, also change password reset count to zero to unblock reset + if ($table->block === 0) { + $table->resetCount = 0; + } + + // Allow an exception to be thrown. + try { + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($old, false, $table->getProperties())); + + if (in_array(false, $result, true)) { + // Plugin will have to raise its own error or throw an exception. + return false; + } + + // Store the table. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + if ($table->block) { + UserHelper::destroyUserSessions($table->id); + } + + // Trigger the after save event + Factory::getApplication()->triggerEvent($this->event_after_save, [$table->getProperties(), false, true, null]); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Log the user out. + if ($value) { + $app->logout($table->id, $options); + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + } + } + } + + return true; + } + + /** + * Method to activate user records. + * + * @param array &$pks The ids of the items to activate. + * + * @return boolean True on success. + * + * @since 1.6 + * @throws \Exception + */ + public function activate(&$pks) + { + $user = Factory::getUser(); + + // Check if I am a Super Admin + $iAmSuperAdmin = $user->authorise('core.admin'); + $table = $this->getTable(); + $pks = (array) $pks; + + PluginHelper::importPlugin($this->events_map['save']); + + // Access checks. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + $old = $table->getProperties(); + $allow = $user->authorise('core.edit.state', 'com_users'); + + // Don't allow non-super-admin to delete a super admin + $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; + + if (empty($table->activation)) { + // Ignore activated accounts. + unset($pks[$i]); + } elseif ($allow) { + $table->block = 0; + $table->activation = ''; + + // Allow an exception to be thrown. + try { + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($old, false, $table->getProperties())); + + if (in_array(false, $result, true)) { + // Plugin will have to raise it's own error or throw an exception. + return false; + } + + // Store the table. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Fire the after save event + Factory::getApplication()->triggerEvent($this->event_after_save, [$table->getProperties(), false, true, null]); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + } + } + } + + return true; + } + + /** + * Method to perform batch operations on an item or a set of items. + * + * @param array $commands An array of commands to perform. + * @param array $pks An array of item ids. + * @param array $contexts An array of item contexts. + * + * @return boolean Returns true on success, false on failure. + * + * @since 2.5 + */ + public function batch($commands, $pks, $contexts) + { + // Sanitize user ids. + $pks = array_unique($pks); + $pks = ArrayHelper::toInteger($pks); + + // Remove any values of zero. + if (array_search(0, $pks, true)) { + unset($pks[array_search(0, $pks, true)]); + } + + if (empty($pks)) { + $this->setError(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED')); + + return false; + } + + $done = false; + + if (!empty($commands['group_id'])) { + $cmd = ArrayHelper::getValue($commands, 'group_action', 'add'); + + if (!$this->batchUser((int) $commands['group_id'], $pks, $cmd)) { + return false; + } + + $done = true; + } + + if (!empty($commands['reset_id'])) { + if (!$this->batchReset($pks, $commands['reset_id'])) { + return false; + } + + $done = true; + } + + if (!$done) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_INSUFFICIENT_BATCH_INFORMATION')); + + return false; + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Batch flag users as being required to reset their passwords + * + * @param array $userIds An array of user IDs on which to operate + * @param string $action The action to perform + * + * @return boolean True on success, false on failure + * + * @since 3.2 + */ + public function batchReset($userIds, $action) + { + $userIds = ArrayHelper::toInteger($userIds); + + // Check if I am a Super Admin + $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); + + // Non-super super user cannot work with super-admin user. + if (!$iAmSuperAdmin && UserHelper::checkSuperUserInUsers($userIds)) { + $this->setError(Text::_('COM_USERS_ERROR_CANNOT_BATCH_SUPERUSER')); + + return false; + } + + // Set the action to perform + if ($action === 'yes') { + $value = 1; + } else { + $value = 0; + } + + // Prune out the current user if they are in the supplied user ID array + $userIds = array_diff($userIds, array(Factory::getUser()->id)); + + if (empty($userIds)) { + $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_REQUIRERESET_SELF')); + + return false; + } + + // Get the DB object + $db = $this->getDatabase(); + + $userIds = ArrayHelper::toInteger($userIds); + + $query = $db->getQuery(true); + + // Update the reset flag + $query->update($db->quoteName('#__users')) + ->set($db->quoteName('requireReset') . ' = :requireReset') + ->whereIn($db->quoteName('id'), $userIds) + ->bind(':requireReset', $value, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return true; + } + + /** + * Perform batch operations + * + * @param integer $groupId The group ID which assignments are being edited + * @param array $userIds An array of user IDs on which to operate + * @param string $action The action to perform + * + * @return boolean True on success, false on failure + * + * @since 1.6 + */ + public function batchUser($groupId, $userIds, $action) + { + $userIds = ArrayHelper::toInteger($userIds); + + // Check if I am a Super Admin + $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); + + // Non-super super user cannot work with super-admin user. + if (!$iAmSuperAdmin && UserHelper::checkSuperUserInUsers($userIds)) { + $this->setError(Text::_('COM_USERS_ERROR_CANNOT_BATCH_SUPERUSER')); + + return false; + } + + // Non-super admin cannot work with super-admin group. + if ((!$iAmSuperAdmin && Access::checkGroup($groupId, 'core.admin')) || $groupId < 1) { + $this->setError(Text::_('COM_USERS_ERROR_INVALID_GROUP')); + + return false; + } + + // Get the DB object + $db = $this->getDatabase(); + + switch ($action) { + // Sets users to a selected group + case 'set': + $doDelete = 'all'; + $doAssign = true; + break; + + // Remove users from a selected group + case 'del': + $doDelete = 'group'; + break; + + // Add users to a selected group + case 'add': + default: + $doAssign = true; + break; + } + + // Remove the users from the group if requested. + if (isset($doDelete)) { + $query = $db->getQuery(true); + + // Remove users from the group + $query->delete($db->quoteName('#__user_usergroup_map')) + ->whereIn($db->quoteName('user_id'), $userIds); + + // Only remove users from selected group + if ($doDelete == 'group') { + $query->where($db->quoteName('group_id') . ' = :group_id') + ->bind(':group_id', $groupId, ParameterType::INTEGER); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + // Assign the users to the group if requested. + if (isset($doAssign)) { + $query = $db->getQuery(true); + + // First, we need to check if the user is already assigned to a group + $query->select($db->quoteName('user_id')) + ->from($db->quoteName('#__user_usergroup_map')) + ->where($db->quoteName('group_id') . ' = :group_id') + ->bind(':group_id', $groupId, ParameterType::INTEGER); + $db->setQuery($query); + $users = $db->loadColumn(); + + // Build the values clause for the assignment query. + $query->clear(); + $groups = false; + + foreach ($userIds as $id) { + if (!in_array($id, $users)) { + $query->values($id . ',' . $groupId); + $groups = true; + } + } + + // If we have no users to process, throw an error to notify the user + if (!$groups) { + $this->setError(Text::_('COM_USERS_ERROR_NO_ADDITIONS')); + + return false; + } + + $query->insert($db->quoteName('#__user_usergroup_map')) + ->columns(array($db->quoteName('user_id'), $db->quoteName('group_id'))); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + return true; + } + + /** + * Gets the available groups. + * + * @return array An array of groups + * + * @since 1.6 + */ + public function getGroups() + { + $user = Factory::getUser(); + + if ($user->authorise('core.edit', 'com_users') && $user->authorise('core.manage', 'com_users')) { + $model = $this->bootComponent('com_users') + ->getMVCFactory()->createModel('Groups', 'Administrator', ['ignore_request' => true]); + + return $model->getItems(); + } else { + return null; + } + } + + /** + * Gets the groups this object is assigned to + * + * @param integer $userId The user ID to retrieve the groups for + * + * @return array An array of assigned groups + * + * @since 1.6 + */ + public function getAssignedGroups($userId = null) + { + $userId = (!empty($userId)) ? $userId : (int) $this->getState('user.id'); + + if (empty($userId)) { + $result = array(); + $form = $this->getForm(); + + if ($form) { + $groupsIDs = $form->getValue('groups'); + } + + if (!empty($groupsIDs)) { + $result = $groupsIDs; + } else { + $params = ComponentHelper::getParams('com_users'); + + if ($groupId = $params->get('new_usertype', $params->get('guest_usergroup', 1))) { + $result[] = $groupId; + } + } + } else { + $result = UserHelper::getUserGroups($userId); + } + + return $result; + } + + /** + * No longer used + * + * @param integer $userId Ignored + * + * @return \stdClass + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public function getOtpConfig($userId = null) + { + @trigger_error( + sprintf( + '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getUserMfaRecords() instead.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + // Return the configuration object + return (object) array( + 'method' => 'none', + 'config' => array(), + 'otep' => array() + ); + } + + /** + * No longer used + * + * @param integer $userId Ignored + * @param \stdClass $otpConfig Ignored + * + * @return boolean True on success + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public function setOtpConfig($userId, $otpConfig) + { + @trigger_error( + sprintf( + '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return true; + } + + /** + * No longer used + * + * @return string + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public function getOtpConfigEncryptionKey() + { + @trigger_error( + sprintf( + '%s() is deprecated. Use \Joomla\CMS\Factory::getApplication()->get(\'secret\') instead', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return Factory::getApplication()->get('secret'); + } + + /** + * No longer used + * + * @param integer $userId Ignored + * + * @return array Empty array + * + * @since 3.2 + * @throws \Exception + * + * @deprecated 4.2.0 Will be removed in 5.0. + */ + public function getTwofactorform($userId = null) + { + @trigger_error( + sprintf( + '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getConfigurationInterface()', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return []; + } + + /** + * No longer used + * + * @param integer $userId Ignored + * @param integer $count Ignored + * + * @return array Empty array + * + * @since 3.2 + * @deprecated 4.2.0 Wil be removed in 5.0. + */ + public function generateOteps($userId, $count = 10) + { + @trigger_error( + sprintf( + '%s() is deprecated. See \Joomla\Component\Users\Administrator\Model\BackupcodesModel::saveBackupCodes()', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return []; + } + + /** + * No longer used. Always returns true. + * + * @param integer $userId Ignored + * @param string $secretKey Ignored + * @param array $options Ignored + * + * @return boolean Always true + * + * @since 3.2 + * @throws \Exception + * + * @deprecated 4.2.0 Will be removed in 5.0. MFA validation is done in the captive login. + */ + public function isValidSecretKey($userId, $secretKey, $options = array()) + { + @trigger_error( + sprintf( + '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return true; + } + + /** + * No longer used + * + * @param integer $userId Ignored + * @param string $otep Ignored + * @param object $otpConfig Ignored + * + * @return boolean Always true + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public function isValidOtep($userId, $otep, $otpConfig = null) + { + @trigger_error( + sprintf( + '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return true; + } } diff --git a/administrator/components/com_users/src/Model/UsersModel.php b/administrator/components/com_users/src/Model/UsersModel.php index 2cd6c20c56b6e..b452861ffdf4c 100644 --- a/administrator/components/com_users/src/Model/UsersModel.php +++ b/administrator/components/com_users/src/Model/UsersModel.php @@ -1,4 +1,5 @@ input->get('layout', 'default', 'cmd')) - { - $this->context .= '.' . $layout; - } - - $groups = json_decode(base64_decode($app->input->get('groups', '', 'BASE64'))); - - if (isset($groups)) - { - $groups = ArrayHelper::toInteger($groups); - } - - $this->setState('filter.groups', $groups); - - $excluded = json_decode(base64_decode($app->input->get('excluded', '', 'BASE64'))); - - if (isset($excluded)) - { - $excluded = ArrayHelper::toInteger($excluded); - } - - $this->setState('filter.excluded', $excluded); - - // Load the parameters. - $params = ComponentHelper::getParams('com_users'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.active'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.group_id'); - $id .= ':' . $this->getState('filter.range'); - - if (PluginHelper::isEnabled('multifactorauth')) - { - $id .= ':' . $this->getState('filter.mfa'); - } - - return parent::getStoreId($id); - } - - /** - * Gets the list of users and adds expensive joins to the result set. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 1.6 - */ - public function getItems() - { - // Get a storage key. - $store = $this->getStoreId(); - - // Try to load the data from internal storage. - if (empty($this->cache[$store])) - { - $groups = $this->getState('filter.groups'); - $groupId = $this->getState('filter.group_id'); - - if (isset($groups) && (empty($groups) || $groupId && !in_array($groupId, $groups))) - { - $items = array(); - } - else - { - $items = parent::getItems(); - } - - // Bail out on an error or empty list. - if (empty($items)) - { - $this->cache[$store] = $items; - - return $items; - } - - // Joining the groups with the main query is a performance hog. - // Find the information only on the result set. - - // First pass: get list of the user ids and reset the counts. - $userIds = array(); - - foreach ($items as $item) - { - $userIds[] = (int) $item->id; + /** + * A list of filter variables to not merge into the model's state + * + * @var array + * @since 4.0.0 + */ + protected $filterForbiddenList = array('groups', 'excluded'); + + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'username', 'a.username', + 'email', 'a.email', + 'block', 'a.block', + 'sendEmail', 'a.sendEmail', + 'registerDate', 'a.registerDate', + 'lastvisitDate', 'a.lastvisitDate', + 'activation', 'a.activation', + 'active', + 'group_id', + 'range', + 'lastvisitrange', + 'state', + 'mfa' + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState($ordering = 'a.name', $direction = 'asc') + { + $app = Factory::getApplication(); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout', 'default', 'cmd')) { + $this->context .= '.' . $layout; + } + + $groups = json_decode(base64_decode($app->input->get('groups', '', 'BASE64'))); + + if (isset($groups)) { + $groups = ArrayHelper::toInteger($groups); + } + + $this->setState('filter.groups', $groups); + + $excluded = json_decode(base64_decode($app->input->get('excluded', '', 'BASE64'))); + + if (isset($excluded)) { + $excluded = ArrayHelper::toInteger($excluded); + } + + $this->setState('filter.excluded', $excluded); + + // Load the parameters. + $params = ComponentHelper::getParams('com_users'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.active'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.group_id'); + $id .= ':' . $this->getState('filter.range'); + + if (PluginHelper::isEnabled('multifactorauth')) { + $id .= ':' . $this->getState('filter.mfa'); + } + + return parent::getStoreId($id); + } + + /** + * Gets the list of users and adds expensive joins to the result set. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 1.6 + */ + public function getItems() + { + // Get a storage key. + $store = $this->getStoreId(); + + // Try to load the data from internal storage. + if (empty($this->cache[$store])) { + $groups = $this->getState('filter.groups'); + $groupId = $this->getState('filter.group_id'); + + if (isset($groups) && (empty($groups) || $groupId && !in_array($groupId, $groups))) { + $items = array(); + } else { + $items = parent::getItems(); + } + + // Bail out on an error or empty list. + if (empty($items)) { + $this->cache[$store] = $items; + + return $items; + } + + // Joining the groups with the main query is a performance hog. + // Find the information only on the result set. + + // First pass: get list of the user ids and reset the counts. + $userIds = array(); + + foreach ($items as $item) { + $userIds[] = (int) $item->id; // phpcs:ignore $item->group_count = 0; // phpcs:ignore $item->group_names = ''; // phpcs:ignore $item->note_count = 0; - } - - // Get the counts from the database only for the users in the list. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Join over the group mapping table. - $query->select('map.user_id, COUNT(map.group_id) AS group_count') - ->from('#__user_usergroup_map AS map') - ->whereIn($db->quoteName('map.user_id'), $userIds) - ->group('map.user_id') - // Join over the user groups table. - ->join('LEFT', '#__usergroups AS g2 ON g2.id = map.group_id'); - - $db->setQuery($query); - - // Load the counts into an array indexed on the user id field. - try - { - $userGroups = $db->loadObjectList('user_id'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $query->clear() - ->select('n.user_id, COUNT(n.id) As note_count') - ->from('#__user_notes AS n') - ->whereIn($db->quoteName('n.user_id'), $userIds) - ->where('n.state >= 0') - ->group('n.user_id'); - - $db->setQuery($query); - - // Load the counts into an array indexed on the aro.value field (the user id). - try - { - $userNotes = $db->loadObjectList('user_id'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Second pass: collect the group counts into the master items array. - foreach ($items as &$item) - { - if (isset($userGroups[$item->id])) - { + } + + // Get the counts from the database only for the users in the list. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Join over the group mapping table. + $query->select('map.user_id, COUNT(map.group_id) AS group_count') + ->from('#__user_usergroup_map AS map') + ->whereIn($db->quoteName('map.user_id'), $userIds) + ->group('map.user_id') + // Join over the user groups table. + ->join('LEFT', '#__usergroups AS g2 ON g2.id = map.group_id'); + + $db->setQuery($query); + + // Load the counts into an array indexed on the user id field. + try { + $userGroups = $db->loadObjectList('user_id'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + $query->clear() + ->select('n.user_id, COUNT(n.id) As note_count') + ->from('#__user_notes AS n') + ->whereIn($db->quoteName('n.user_id'), $userIds) + ->where('n.state >= 0') + ->group('n.user_id'); + + $db->setQuery($query); + + // Load the counts into an array indexed on the aro.value field (the user id). + try { + $userNotes = $db->loadObjectList('user_id'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Second pass: collect the group counts into the master items array. + foreach ($items as &$item) { + if (isset($userGroups[$item->id])) { // phpcs:ignore $item->group_count = $userGroups[$item->id]->group_count; - // Group_concat in other databases is not supported + // Group_concat in other databases is not supported // phpcs:ignore $item->group_names = $this->getUserDisplayedGroups($item->id); - } + } - if (isset($userNotes[$item->id])) - { + if (isset($userNotes[$item->id])) { // phpcs:ignore $item->note_count = $userNotes[$item->id]->note_count; - } - } - - // Add the items to the internal cache. - $this->cache[$store] = $items; - } - - return $this->cache[$store]; - } - - /** - * Get the filter form - * - * @param array $data data - * @param boolean $loadData load current data - * - * @return Form|null The \JForm object or null if the form can't be found - * - * @since 4.2.0 - */ - public function getFilterForm($data = [], $loadData = true) - { - $form = parent::getFilterForm($data, $loadData); - - if ($form && !PluginHelper::isEnabled('multifactorauth')) - { - $form->removeField('mfa', 'filter'); - } - - return $form; - } - - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.*' - ) - ); - - $query->from($db->quoteName('#__users') . ' AS a'); - - // Include MFA information - if (PluginHelper::isEnabled('multifactorauth')) - { - $subQuery = $db->getQuery(true) - ->select( - [ - 'MIN(' . $db->quoteName('user_id') . ') AS ' . $db->quoteName('uid'), - 'COUNT(*) AS ' . $db->quoteName('mfaRecords') - ] - ) - ->from($db->quoteName('#__user_mfa')) - ->group($db->quoteName('user_id')); - $query->select($db->quoteName('mfa.mfaRecords')) - ->join( - 'left', - '(' . $subQuery . ') AS ' . $db->quoteName('mfa'), - $db->quoteName('mfa.uid') . ' = ' . $db->quoteName('a.id') - ); - - $mfaState = $this->getState('filter.mfa'); - - if (is_numeric($mfaState)) - { - $mfaState = (int) $mfaState; - - if ($mfaState === 1) - { - $query->where( - '((' . $db->quoteName('mfa.mfaRecords') . ' > 0) OR (' . - $db->quoteName('a.otpKey') . ' IS NOT NULL AND ' . - $db->quoteName('a.otpKey') . ' != ' . $db->quote('') . '))' - ); - } - else - { - $query->where( - '((' . $db->quoteName('mfa.mfaRecords') . ' = 0 OR ' . - $db->quoteName('mfa.mfaRecords') . ' IS NULL) AND (' . - $db->quoteName('a.otpKey') . ' IS NULL OR ' . - $db->quoteName('a.otpKey') . ' = ' . $db->quote('') . '))' - ); - } - } - } - - // If the model is set to check item state, add to the query. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $query->where($db->quoteName('a.block') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - - // If the model is set to check the activated state, add to the query. - $active = $this->getState('filter.active'); - - if (is_numeric($active)) - { - if ($active == '0') - { - $query->whereIn($db->quoteName('a.activation'), ['', '0']); - } - elseif ($active == '1') - { - $query->where($query->length($db->quoteName('a.activation')) . ' > 1'); - } - } - - // Filter the items over the group id if set. - $groupId = $this->getState('filter.group_id'); - $groups = $this->getState('filter.groups'); - - if ($groupId || isset($groups)) - { - $query->join('LEFT', '#__user_usergroup_map AS map2 ON map2.user_id = a.id') - ->group( - $db->quoteName( - array( - 'a.id', - 'a.name', - 'a.username', - 'a.password', - 'a.block', - 'a.sendEmail', - 'a.registerDate', - 'a.lastvisitDate', - 'a.activation', - 'a.params', - 'a.email', - 'a.lastResetTime', - 'a.resetCount', - 'a.otpKey', - 'a.otep', - 'a.requireReset' - ) - ) - ); - - if ($groupId) - { - $groupId = (int) $groupId; - $query->where($db->quoteName('map2.group_id') . ' = :group_id') - ->bind(':group_id', $groupId, ParameterType::INTEGER); - } - - if (isset($groups)) - { - $query->whereIn($db->quoteName('map2.group_id'), $groups); - } - } - - // Filter the items over the search string if set. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - elseif (stripos($search, 'username:') === 0) - { - $search = '%' . substr($search, 9) . '%'; - $query->where($db->quoteName('a.username') . ' LIKE :username'); - $query->bind(':username', $search); - } - else - { - $search = '%' . trim($search) . '%'; - - // Add the clauses to the query. - $query->where( - '(' . $db->quoteName('a.name') . ' LIKE :name' - . ' OR ' . $db->quoteName('a.username') . ' LIKE :username' - . ' OR ' . $db->quoteName('a.email') . ' LIKE :email)' - ) - ->bind(':name', $search) - ->bind(':username', $search) - ->bind(':email', $search); - } - } - - // Add filter for registration time ranges select list. UI Visitors get a range of predefined - // values. API users can do a full range based on ISO8601 - $range = $this->getState('filter.range'); - $registrationStart = $this->getState('filter.registrationDateStart'); - $registrationEnd = $this->getState('filter.registrationDateEnd'); - - // Apply the range filter. - if ($range || ($registrationStart && $registrationEnd)) - { - if ($range) - { - $dates = $this->buildDateRange($range); - } - else - { - $dates = [ - 'dNow' => $registrationEnd, - 'dStart' => $registrationStart, - ]; - } - - if ($dates['dStart'] !== false) - { - $dStart = $dates['dStart']->format('Y-m-d H:i:s'); - - if ($dates['dNow'] === false) - { - $query->where($db->quoteName('a.registerDate') . ' < :registerDate'); - $query->bind(':registerDate', $dStart); - } - else - { - $dNow = $dates['dNow']->format('Y-m-d H:i:s'); - - $query->where($db->quoteName('a.registerDate') . ' BETWEEN :registerDate1 AND :registerDate2'); - $query->bind(':registerDate1', $dStart); - $query->bind(':registerDate2', $dNow); - } - } - } - - // Add filter for last visit time ranges select list. UI Visitors get a range of predefined - // values. API users can do a full range based on ISO8601 - $lastvisitrange = $this->getState('filter.lastvisitrange'); - $lastVisitStart = $this->getState('filter.lastVisitStart'); - $lastVisitEnd = $this->getState('filter.lastVisitEnd'); - - // Apply the range filter. - if ($lastvisitrange || ($lastVisitStart && $lastVisitEnd)) - { - if ($lastvisitrange) - { - $dates = $this->buildDateRange($lastvisitrange); - } - else - { - $dates = [ - 'dNow' => $lastVisitEnd, - 'dStart' => $lastVisitStart, - ]; - } - - if ($dates['dStart'] === false) - { - $query->where($db->quoteName('a.lastvisitDate') . ' IS NULL'); - } - else - { - $query->where($db->quoteName('a.lastvisitDate') . ' IS NOT NULL'); - - $dStart = $dates['dStart']->format('Y-m-d H:i:s'); - - if ($dates['dNow'] === false) - { - $query->where($db->quoteName('a.lastvisitDate') . ' < :lastvisitDate'); - $query->bind(':lastvisitDate', $dStart); - } - else - { - $dNow = $dates['dNow']->format('Y-m-d H:i:s'); - - $query->where($db->quoteName('a.lastvisitDate') . ' BETWEEN :lastvisitDate1 AND :lastvisitDate2'); - $query->bind(':lastvisitDate1', $dStart); - $query->bind(':lastvisitDate2', $dNow); - } - } - } - - // Filter by excluded users - $excluded = $this->getState('filter.excluded'); - - if (!empty($excluded)) - { - $query->whereNotIn($db->quoteName('id'), $excluded); - } - - // Add the list ordering clause. - $query->order( - $db->quoteName($db->escape($this->getState('list.ordering', 'a.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) - ); - - return $query; - } - - /** - * Construct the date range to filter on. - * - * @param string $range The textual range to construct the filter for. - * - * @return array The date range to filter on. - * - * @since 3.6.0 - * @throws \Exception - */ - private function buildDateRange($range) - { - // Get UTC for now. - $dNow = new Date; - $dStart = clone $dNow; - - switch ($range) - { - case 'past_week': - $dStart->modify('-7 day'); - break; - - case 'past_1month': - $dStart->modify('-1 month'); - break; - - case 'past_3month': - $dStart->modify('-3 month'); - break; - - case 'past_6month': - $dStart->modify('-6 month'); - $arr = []; - break; - - case 'post_year': - $dNow = false; - - // No break - - case 'past_year': - $dStart->modify('-1 year'); - break; - - case 'today': - // Ranges that need to align with local 'days' need special treatment. - $app = Factory::getApplication(); - $offset = $app->get('offset'); - - // Reset the start time to be the beginning of today, local time. - $dStart = new Date('now', $offset); - $dStart->setTime(0, 0, 0); - - // Now change the timezone back to UTC. - $tz = new \DateTimeZone('GMT'); - $dStart->setTimezone($tz); - break; - case 'never': - $dNow = false; - $dStart = false; - break; - } - - return array('dNow' => $dNow, 'dStart' => $dStart); - } - - /** - * SQL server change - * - * @param integer $userId User identifier - * - * @return string Groups titles imploded :$ - */ - protected function getUserDisplayedGroups($userId) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__usergroups', 'ug')) - ->join('LEFT', $db->quoteName('#__user_usergroup_map', 'map') . ' ON (ug.id = map.group_id)') - ->where($db->quoteName('map.user_id') . ' = :user_id') - ->bind(':user_id', $userId, ParameterType::INTEGER); - - try - { - $result = $db->setQuery($query)->loadColumn(); - } - catch (\RuntimeException $e) - { - $result = array(); - } - - return implode("\n", $result); - } + } + } + + // Add the items to the internal cache. + $this->cache[$store] = $items; + } + + return $this->cache[$store]; + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return Form|null The \JForm object or null if the form can't be found + * + * @since 4.2.0 + */ + public function getFilterForm($data = [], $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + if ($form && !PluginHelper::isEnabled('multifactorauth')) { + $form->removeField('mfa', 'filter'); + } + + return $form; + } + + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.*' + ) + ); + + $query->from($db->quoteName('#__users') . ' AS a'); + + // Include MFA information + if (PluginHelper::isEnabled('multifactorauth')) { + $subQuery = $db->getQuery(true) + ->select( + [ + 'MIN(' . $db->quoteName('user_id') . ') AS ' . $db->quoteName('uid'), + 'COUNT(*) AS ' . $db->quoteName('mfaRecords') + ] + ) + ->from($db->quoteName('#__user_mfa')) + ->group($db->quoteName('user_id')); + $query->select($db->quoteName('mfa.mfaRecords')) + ->join( + 'left', + '(' . $subQuery . ') AS ' . $db->quoteName('mfa'), + $db->quoteName('mfa.uid') . ' = ' . $db->quoteName('a.id') + ); + + $mfaState = $this->getState('filter.mfa'); + + if (is_numeric($mfaState)) { + $mfaState = (int) $mfaState; + + if ($mfaState === 1) { + $query->where( + '((' . $db->quoteName('mfa.mfaRecords') . ' > 0) OR (' . + $db->quoteName('a.otpKey') . ' IS NOT NULL AND ' . + $db->quoteName('a.otpKey') . ' != ' . $db->quote('') . '))' + ); + } else { + $query->where( + '((' . $db->quoteName('mfa.mfaRecords') . ' = 0 OR ' . + $db->quoteName('mfa.mfaRecords') . ' IS NULL) AND (' . + $db->quoteName('a.otpKey') . ' IS NULL OR ' . + $db->quoteName('a.otpKey') . ' = ' . $db->quote('') . '))' + ); + } + } + } + + // If the model is set to check item state, add to the query. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $query->where($db->quoteName('a.block') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } + + // If the model is set to check the activated state, add to the query. + $active = $this->getState('filter.active'); + + if (is_numeric($active)) { + if ($active == '0') { + $query->whereIn($db->quoteName('a.activation'), ['', '0']); + } elseif ($active == '1') { + $query->where($query->length($db->quoteName('a.activation')) . ' > 1'); + } + } + + // Filter the items over the group id if set. + $groupId = $this->getState('filter.group_id'); + $groups = $this->getState('filter.groups'); + + if ($groupId || isset($groups)) { + $query->join('LEFT', '#__user_usergroup_map AS map2 ON map2.user_id = a.id') + ->group( + $db->quoteName( + array( + 'a.id', + 'a.name', + 'a.username', + 'a.password', + 'a.block', + 'a.sendEmail', + 'a.registerDate', + 'a.lastvisitDate', + 'a.activation', + 'a.params', + 'a.email', + 'a.lastResetTime', + 'a.resetCount', + 'a.otpKey', + 'a.otep', + 'a.requireReset' + ) + ) + ); + + if ($groupId) { + $groupId = (int) $groupId; + $query->where($db->quoteName('map2.group_id') . ' = :group_id') + ->bind(':group_id', $groupId, ParameterType::INTEGER); + } + + if (isset($groups)) { + $query->whereIn($db->quoteName('map2.group_id'), $groups); + } + } + + // Filter the items over the search string if set. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } elseif (stripos($search, 'username:') === 0) { + $search = '%' . substr($search, 9) . '%'; + $query->where($db->quoteName('a.username') . ' LIKE :username'); + $query->bind(':username', $search); + } else { + $search = '%' . trim($search) . '%'; + + // Add the clauses to the query. + $query->where( + '(' . $db->quoteName('a.name') . ' LIKE :name' + . ' OR ' . $db->quoteName('a.username') . ' LIKE :username' + . ' OR ' . $db->quoteName('a.email') . ' LIKE :email)' + ) + ->bind(':name', $search) + ->bind(':username', $search) + ->bind(':email', $search); + } + } + + // Add filter for registration time ranges select list. UI Visitors get a range of predefined + // values. API users can do a full range based on ISO8601 + $range = $this->getState('filter.range'); + $registrationStart = $this->getState('filter.registrationDateStart'); + $registrationEnd = $this->getState('filter.registrationDateEnd'); + + // Apply the range filter. + if ($range || ($registrationStart && $registrationEnd)) { + if ($range) { + $dates = $this->buildDateRange($range); + } else { + $dates = [ + 'dNow' => $registrationEnd, + 'dStart' => $registrationStart, + ]; + } + + if ($dates['dStart'] !== false) { + $dStart = $dates['dStart']->format('Y-m-d H:i:s'); + + if ($dates['dNow'] === false) { + $query->where($db->quoteName('a.registerDate') . ' < :registerDate'); + $query->bind(':registerDate', $dStart); + } else { + $dNow = $dates['dNow']->format('Y-m-d H:i:s'); + + $query->where($db->quoteName('a.registerDate') . ' BETWEEN :registerDate1 AND :registerDate2'); + $query->bind(':registerDate1', $dStart); + $query->bind(':registerDate2', $dNow); + } + } + } + + // Add filter for last visit time ranges select list. UI Visitors get a range of predefined + // values. API users can do a full range based on ISO8601 + $lastvisitrange = $this->getState('filter.lastvisitrange'); + $lastVisitStart = $this->getState('filter.lastVisitStart'); + $lastVisitEnd = $this->getState('filter.lastVisitEnd'); + + // Apply the range filter. + if ($lastvisitrange || ($lastVisitStart && $lastVisitEnd)) { + if ($lastvisitrange) { + $dates = $this->buildDateRange($lastvisitrange); + } else { + $dates = [ + 'dNow' => $lastVisitEnd, + 'dStart' => $lastVisitStart, + ]; + } + + if ($dates['dStart'] === false) { + $query->where($db->quoteName('a.lastvisitDate') . ' IS NULL'); + } else { + $query->where($db->quoteName('a.lastvisitDate') . ' IS NOT NULL'); + + $dStart = $dates['dStart']->format('Y-m-d H:i:s'); + + if ($dates['dNow'] === false) { + $query->where($db->quoteName('a.lastvisitDate') . ' < :lastvisitDate'); + $query->bind(':lastvisitDate', $dStart); + } else { + $dNow = $dates['dNow']->format('Y-m-d H:i:s'); + + $query->where($db->quoteName('a.lastvisitDate') . ' BETWEEN :lastvisitDate1 AND :lastvisitDate2'); + $query->bind(':lastvisitDate1', $dStart); + $query->bind(':lastvisitDate2', $dNow); + } + } + } + + // Filter by excluded users + $excluded = $this->getState('filter.excluded'); + + if (!empty($excluded)) { + $query->whereNotIn($db->quoteName('id'), $excluded); + } + + // Add the list ordering clause. + $query->order( + $db->quoteName($db->escape($this->getState('list.ordering', 'a.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) + ); + + return $query; + } + + /** + * Construct the date range to filter on. + * + * @param string $range The textual range to construct the filter for. + * + * @return array The date range to filter on. + * + * @since 3.6.0 + * @throws \Exception + */ + private function buildDateRange($range) + { + // Get UTC for now. + $dNow = new Date(); + $dStart = clone $dNow; + + switch ($range) { + case 'past_week': + $dStart->modify('-7 day'); + break; + + case 'past_1month': + $dStart->modify('-1 month'); + break; + + case 'past_3month': + $dStart->modify('-3 month'); + break; + + case 'past_6month': + $dStart->modify('-6 month'); + $arr = []; + break; + + case 'post_year': + $dNow = false; + + // No break + + case 'past_year': + $dStart->modify('-1 year'); + break; + + case 'today': + // Ranges that need to align with local 'days' need special treatment. + $app = Factory::getApplication(); + $offset = $app->get('offset'); + + // Reset the start time to be the beginning of today, local time. + $dStart = new Date('now', $offset); + $dStart->setTime(0, 0, 0); + + // Now change the timezone back to UTC. + $tz = new \DateTimeZone('GMT'); + $dStart->setTimezone($tz); + break; + case 'never': + $dNow = false; + $dStart = false; + break; + } + + return array('dNow' => $dNow, 'dStart' => $dStart); + } + + /** + * SQL server change + * + * @param integer $userId User identifier + * + * @return string Groups titles imploded :$ + */ + protected function getUserDisplayedGroups($userId) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__usergroups', 'ug')) + ->join('LEFT', $db->quoteName('#__user_usergroup_map', 'map') . ' ON (ug.id = map.group_id)') + ->where($db->quoteName('map.user_id') . ' = :user_id') + ->bind(':user_id', $userId, ParameterType::INTEGER); + + try { + $result = $db->setQuery($query)->loadColumn(); + } catch (\RuntimeException $e) { + $result = array(); + } + + return implode("\n", $result); + } } diff --git a/administrator/components/com_users/src/Service/Encrypt.php b/administrator/components/com_users/src/Service/Encrypt.php index 935be2eff6144..eb1ca97d6b0e3 100644 --- a/administrator/components/com_users/src/Service/Encrypt.php +++ b/administrator/components/com_users/src/Service/Encrypt.php @@ -1,4 +1,5 @@ initialize(); - } - - /** - * Encrypt the plaintext $data and return the ciphertext prefixed by ###AES128### - * - * @param string $data The plaintext data - * - * @return string The ciphertext, prefixed by ###AES128### - * - * @since 4.2.0 - */ - public function encrypt(string $data): string - { - if (!is_object($this->aes)) - { - return $data; - } - - $this->aes->setPassword($this->getPassword(), false); - $encrypted = $this->aes->encryptString($data, true); - - return '###AES128###' . $encrypted; - } - - /** - * Decrypt the ciphertext, prefixed by ###AES128###, and return the plaintext. - * - * @param string $data The ciphertext, prefixed by ###AES128### - * @param bool $legacy Use legacy key expansion? Use it to decrypt data encrypted with FOF 3. - * - * @return string The plaintext data - * - * @since 4.2.0 - */ - public function decrypt(string $data, bool $legacy = false): string - { - if (substr($data, 0, 12) != '###AES128###') - { - return $data; - } - - $data = substr($data, 12); - - if (!is_object($this->aes)) - { - return $data; - } - - $this->aes->setPassword($this->getPassword(), $legacy); - $decrypted = $this->aes->decryptString($data, true, $legacy); - - // Decrypted data is null byte padded. We have to remove the padding before proceeding. - return rtrim($decrypted, "\0"); - } - - /** - * Initialize the AES cryptography object - * - * @return void - * @since 4.2.0 - */ - private function initialize(): void - { - if (is_object($this->aes)) - { - return; - } - - $password = $this->getPassword(); - - if (empty($password)) - { - return; - } - - $this->aes = new Aes('cbc'); - $this->aes->setPassword($password); - } - - /** - * Returns the password used to encrypt information in the component - * - * @return string - * - * @since 4.2.0 - */ - private function getPassword(): string - { - try - { - return Factory::getApplication()->get('secret', ''); - } - catch (\Exception $e) - { - return ''; - } - } + /** + * The encryption engine used by this service + * + * @var Aes + * @since 4.2.0 + */ + private $aes; + + /** + * EncryptService constructor. + * + * @since 4.2.0 + */ + public function __construct() + { + $this->initialize(); + } + + /** + * Encrypt the plaintext $data and return the ciphertext prefixed by ###AES128### + * + * @param string $data The plaintext data + * + * @return string The ciphertext, prefixed by ###AES128### + * + * @since 4.2.0 + */ + public function encrypt(string $data): string + { + if (!is_object($this->aes)) { + return $data; + } + + $this->aes->setPassword($this->getPassword(), false); + $encrypted = $this->aes->encryptString($data, true); + + return '###AES128###' . $encrypted; + } + + /** + * Decrypt the ciphertext, prefixed by ###AES128###, and return the plaintext. + * + * @param string $data The ciphertext, prefixed by ###AES128### + * @param bool $legacy Use legacy key expansion? Use it to decrypt data encrypted with FOF 3. + * + * @return string The plaintext data + * + * @since 4.2.0 + */ + public function decrypt(string $data, bool $legacy = false): string + { + if (substr($data, 0, 12) != '###AES128###') { + return $data; + } + + $data = substr($data, 12); + + if (!is_object($this->aes)) { + return $data; + } + + $this->aes->setPassword($this->getPassword(), $legacy); + $decrypted = $this->aes->decryptString($data, true, $legacy); + + // Decrypted data is null byte padded. We have to remove the padding before proceeding. + return rtrim($decrypted, "\0"); + } + + /** + * Initialize the AES cryptography object + * + * @return void + * @since 4.2.0 + */ + private function initialize(): void + { + if (is_object($this->aes)) { + return; + } + + $password = $this->getPassword(); + + if (empty($password)) { + return; + } + + $this->aes = new Aes('cbc'); + $this->aes->setPassword($password); + } + + /** + * Returns the password used to encrypt information in the component + * + * @return string + * + * @since 4.2.0 + */ + private function getPassword(): string + { + try { + return Factory::getApplication()->get('secret', ''); + } catch (\Exception $e) { + return ''; + } + } } diff --git a/administrator/components/com_users/src/Service/HTML/Users.php b/administrator/components/com_users/src/Service/HTML/Users.php index 1f8e9edfe6194..de6dd2e6e1ddb 100644 --- a/administrator/components/com_users/src/Service/HTML/Users.php +++ b/administrator/components/com_users/src/Service/HTML/Users.php @@ -1,4 +1,5 @@ element if the specified file exists, otherwise, a null string - * - * @since 2.5 - * @throws \Exception - */ - public function image($src) - { - $src = preg_replace('#[^A-Z0-9\-_\./]#i', '', $src); - $file = JPATH_SITE . '/' . $src; - - Path::check($file); - - if (!file_exists($file)) - { - return ''; - } - - return ''; - } - - /** - * Displays an icon to add a note for this user. - * - * @param integer $userId The user ID - * - * @return string A link to add a note - * - * @since 2.5 - */ - public function addNote($userId) - { - $title = Text::_('COM_USERS_ADD_NOTE'); - - return '' . $title . ''; - } - - /** - * Displays an icon to filter the notes list on this user. - * - * @param integer $count The number of notes for the user - * @param integer $userId The user ID - * - * @return string A link to apply a filter - * - * @since 2.5 - */ - public function filterNotes($count, $userId) - { - if (empty($count)) - { - return ''; - } - - $title = Text::_('COM_USERS_FILTER_NOTES'); - - return '' . $title . ''; - } - - /** - * Displays a note icon. - * - * @param integer $count The number of notes for the user - * @param integer $userId The user ID - * - * @return string A link to a modal window with the user notes - * - * @since 2.5 - */ - public function notes($count, $userId) - { - if (empty($count)) - { - return ''; - } - - $title = Text::plural('COM_USERS_N_USER_NOTES', $count); - - return ''; - } - - /** - * Renders the modal html. - * - * @param integer $count The number of notes for the user - * @param integer $userId The user ID - * - * @return string The html for the rendered modal - * - * @since 3.4.1 - */ - public function notesModal($count, $userId) - { - if (empty($count)) - { - return ''; - } - - $title = Text::plural('COM_USERS_N_USER_NOTES', $count); - $footer = ''; - - return HTMLHelper::_( - 'bootstrap.renderModal', - 'userModal_' . (int) $userId, - array( - 'title' => $title, - 'backdrop' => 'static', - 'keyboard' => true, - 'closeButton' => true, - 'footer' => $footer, - 'url' => Route::_('index.php?option=com_users&view=notes&tmpl=component&layout=modal&filter[user_id]=' . (int) $userId), - 'height' => '300px', - 'width' => '800px', - ) - ); - - } - - /** - * Build an array of block/unblock user states to be used by jgrid.state, - * State options will be different for any user - * and for currently logged in user - * - * @param boolean $self True if state array is for currently logged in user - * - * @return array a list of possible states to display - * - * @since 3.0 - */ - public function blockStates( $self = false) - { - if ($self) - { - $states = array( - 1 => array( - 'task' => 'unblock', - 'text' => '', - 'active_title' => 'COM_USERS_TOOLBAR_BLOCK', - 'inactive_title' => '', - 'tip' => true, - 'active_class' => 'unpublish', - 'inactive_class' => 'unpublish', - ), - 0 => array( - 'task' => 'block', - 'text' => '', - 'active_title' => '', - 'inactive_title' => 'COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF', - 'tip' => true, - 'active_class' => 'publish', - 'inactive_class' => 'publish', - ) - ); - } - else - { - $states = array( - 1 => array( - 'task' => 'unblock', - 'text' => '', - 'active_title' => 'COM_USERS_TOOLBAR_UNBLOCK', - 'inactive_title' => '', - 'tip' => true, - 'active_class' => 'unpublish', - 'inactive_class' => 'unpublish', - ), - 0 => array( - 'task' => 'block', - 'text' => '', - 'active_title' => 'COM_USERS_TOOLBAR_BLOCK', - 'inactive_title' => '', - 'tip' => true, - 'active_class' => 'publish', - 'inactive_class' => 'publish', - ) - ); - } - - return $states; - } - - /** - * Build an array of activate states to be used by jgrid.state, - * - * @return array a list of possible states to display - * - * @since 3.0 - */ - public function activateStates() - { - $states = array( - 1 => array( - 'task' => 'activate', - 'text' => '', - 'active_title' => 'COM_USERS_TOOLBAR_ACTIVATE', - 'inactive_title' => '', - 'tip' => true, - 'active_class' => 'unpublish', - 'inactive_class' => 'unpublish', - ), - 0 => array( - 'task' => '', - 'text' => '', - 'active_title' => '', - 'inactive_title' => 'COM_USERS_ACTIVATED', - 'tip' => true, - 'active_class' => 'publish', - 'inactive_class' => 'publish', - ) - ); - - return $states; - } - - /** - * Get the sanitized value - * - * @param mixed $value Value of the field - * - * @return mixed String/void - * - * @since 1.6 - */ - public function value($value) - { - if (is_string($value)) - { - $value = trim($value); - } - - if (empty($value)) - { - return Text::_('COM_USERS_PROFILE_VALUE_NOT_FOUND'); - } - - elseif (!is_array($value)) - { - return htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); - } - } - - /** - * Get the space symbol - * - * @param mixed $value Value of the field - * - * @return string - * - * @since 1.6 - */ - public function spacer($value) - { - return ''; - } - - /** - * Get the sanitized template style - * - * @param mixed $value Value of the field - * - * @return mixed String/void - * - * @since 1.6 - */ - public function templatestyle($value) - { - if (empty($value)) - { - return static::value($value); - } - else - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__template_styles')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $value, ParameterType::INTEGER); - $db->setQuery($query); - $title = $db->loadResult(); - - if ($title) - { - return htmlspecialchars($title, ENT_COMPAT, 'UTF-8'); - } - else - { - return static::value(''); - } - } - } - - /** - * Get the sanitized language - * - * @param mixed $value Value of the field - * - * @return mixed String/void - * - * @since 1.6 - */ - public function admin_language($value) - { - if (!$value) - { - return static::value($value); - } - - $path = LanguageHelper::getLanguagePath(JPATH_ADMINISTRATOR, $value); - $file = $path . '/langmetadata.xml'; - - if (!is_file($file)) - { - // For language packs from before 4.0. - $file = $path . '/' . $value . '.xml'; - - if (!is_file($file)) - { - return static::value($value); - } - } - - $result = LanguageHelper::parseXMLLanguageFile($file); - - if ($result) - { - return htmlspecialchars($result['name'], ENT_COMPAT, 'UTF-8'); - } - - return static::value($value); - } - - /** - * Get the sanitized language - * - * @param mixed $value Value of the field - * - * @return mixed String/void - * - * @since 1.6 - */ - public function language($value) - { - if (!$value) - { - return static::value($value); - } - - $path = LanguageHelper::getLanguagePath(JPATH_SITE, $value); - $file = $path . '/langmetadata.xml'; - - if (!is_file($file)) - { - // For language packs from before 4.0. - $file = $path . '/' . $value . '.xml'; - - if (!is_file($file)) - { - return static::value($value); - } - } - - $result = LanguageHelper::parseXMLLanguageFile($file); - - if ($result) - { - return htmlspecialchars($result['name'], ENT_COMPAT, 'UTF-8'); - } - - return static::value($value); - } - - /** - * Get the sanitized editor name - * - * @param mixed $value Value of the field - * - * @return mixed String/void - * - * @since 1.6 - */ - public function editor($value) - { - if (empty($value)) - { - return static::value($value); - } - else - { - $db = Factory::getDbo(); - $lang = Factory::getLanguage(); - $query = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('element') . ' = :element') - ->where($db->quoteName('folder') . ' = ' . $db->quote('editors')) - ->bind(':element', $value); - $db->setQuery($query); - $title = $db->loadResult(); - - if ($title) - { - $lang->load("plg_editors_$value.sys", JPATH_ADMINISTRATOR) - || $lang->load("plg_editors_$value.sys", JPATH_PLUGINS . '/editors/' . $value); - $lang->load($title . '.sys'); - - return Text::_($title); - } - else - { - return static::value(''); - } - } - } + /** + * Display an image. + * + * @param string $src The source of the image + * + * @return string A element if the specified file exists, otherwise, a null string + * + * @since 2.5 + * @throws \Exception + */ + public function image($src) + { + $src = preg_replace('#[^A-Z0-9\-_\./]#i', '', $src); + $file = JPATH_SITE . '/' . $src; + + Path::check($file); + + if (!file_exists($file)) { + return ''; + } + + return ''; + } + + /** + * Displays an icon to add a note for this user. + * + * @param integer $userId The user ID + * + * @return string A link to add a note + * + * @since 2.5 + */ + public function addNote($userId) + { + $title = Text::_('COM_USERS_ADD_NOTE'); + + return '' . $title . ''; + } + + /** + * Displays an icon to filter the notes list on this user. + * + * @param integer $count The number of notes for the user + * @param integer $userId The user ID + * + * @return string A link to apply a filter + * + * @since 2.5 + */ + public function filterNotes($count, $userId) + { + if (empty($count)) { + return ''; + } + + $title = Text::_('COM_USERS_FILTER_NOTES'); + + return '' . $title . ''; + } + + /** + * Displays a note icon. + * + * @param integer $count The number of notes for the user + * @param integer $userId The user ID + * + * @return string A link to a modal window with the user notes + * + * @since 2.5 + */ + public function notes($count, $userId) + { + if (empty($count)) { + return ''; + } + + $title = Text::plural('COM_USERS_N_USER_NOTES', $count); + + return ''; + } + + /** + * Renders the modal html. + * + * @param integer $count The number of notes for the user + * @param integer $userId The user ID + * + * @return string The html for the rendered modal + * + * @since 3.4.1 + */ + public function notesModal($count, $userId) + { + if (empty($count)) { + return ''; + } + + $title = Text::plural('COM_USERS_N_USER_NOTES', $count); + $footer = ''; + + return HTMLHelper::_( + 'bootstrap.renderModal', + 'userModal_' . (int) $userId, + array( + 'title' => $title, + 'backdrop' => 'static', + 'keyboard' => true, + 'closeButton' => true, + 'footer' => $footer, + 'url' => Route::_('index.php?option=com_users&view=notes&tmpl=component&layout=modal&filter[user_id]=' . (int) $userId), + 'height' => '300px', + 'width' => '800px', + ) + ); + } + + /** + * Build an array of block/unblock user states to be used by jgrid.state, + * State options will be different for any user + * and for currently logged in user + * + * @param boolean $self True if state array is for currently logged in user + * + * @return array a list of possible states to display + * + * @since 3.0 + */ + public function blockStates($self = false) + { + if ($self) { + $states = array( + 1 => array( + 'task' => 'unblock', + 'text' => '', + 'active_title' => 'COM_USERS_TOOLBAR_BLOCK', + 'inactive_title' => '', + 'tip' => true, + 'active_class' => 'unpublish', + 'inactive_class' => 'unpublish', + ), + 0 => array( + 'task' => 'block', + 'text' => '', + 'active_title' => '', + 'inactive_title' => 'COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF', + 'tip' => true, + 'active_class' => 'publish', + 'inactive_class' => 'publish', + ) + ); + } else { + $states = array( + 1 => array( + 'task' => 'unblock', + 'text' => '', + 'active_title' => 'COM_USERS_TOOLBAR_UNBLOCK', + 'inactive_title' => '', + 'tip' => true, + 'active_class' => 'unpublish', + 'inactive_class' => 'unpublish', + ), + 0 => array( + 'task' => 'block', + 'text' => '', + 'active_title' => 'COM_USERS_TOOLBAR_BLOCK', + 'inactive_title' => '', + 'tip' => true, + 'active_class' => 'publish', + 'inactive_class' => 'publish', + ) + ); + } + + return $states; + } + + /** + * Build an array of activate states to be used by jgrid.state, + * + * @return array a list of possible states to display + * + * @since 3.0 + */ + public function activateStates() + { + $states = array( + 1 => array( + 'task' => 'activate', + 'text' => '', + 'active_title' => 'COM_USERS_TOOLBAR_ACTIVATE', + 'inactive_title' => '', + 'tip' => true, + 'active_class' => 'unpublish', + 'inactive_class' => 'unpublish', + ), + 0 => array( + 'task' => '', + 'text' => '', + 'active_title' => '', + 'inactive_title' => 'COM_USERS_ACTIVATED', + 'tip' => true, + 'active_class' => 'publish', + 'inactive_class' => 'publish', + ) + ); + + return $states; + } + + /** + * Get the sanitized value + * + * @param mixed $value Value of the field + * + * @return mixed String/void + * + * @since 1.6 + */ + public function value($value) + { + if (is_string($value)) { + $value = trim($value); + } + + if (empty($value)) { + return Text::_('COM_USERS_PROFILE_VALUE_NOT_FOUND'); + } elseif (!is_array($value)) { + return htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); + } + } + + /** + * Get the space symbol + * + * @param mixed $value Value of the field + * + * @return string + * + * @since 1.6 + */ + public function spacer($value) + { + return ''; + } + + /** + * Get the sanitized template style + * + * @param mixed $value Value of the field + * + * @return mixed String/void + * + * @since 1.6 + */ + public function templatestyle($value) + { + if (empty($value)) { + return static::value($value); + } else { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__template_styles')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $value, ParameterType::INTEGER); + $db->setQuery($query); + $title = $db->loadResult(); + + if ($title) { + return htmlspecialchars($title, ENT_COMPAT, 'UTF-8'); + } else { + return static::value(''); + } + } + } + + /** + * Get the sanitized language + * + * @param mixed $value Value of the field + * + * @return mixed String/void + * + * @since 1.6 + */ + public function admin_language($value) + { + if (!$value) { + return static::value($value); + } + + $path = LanguageHelper::getLanguagePath(JPATH_ADMINISTRATOR, $value); + $file = $path . '/langmetadata.xml'; + + if (!is_file($file)) { + // For language packs from before 4.0. + $file = $path . '/' . $value . '.xml'; + + if (!is_file($file)) { + return static::value($value); + } + } + + $result = LanguageHelper::parseXMLLanguageFile($file); + + if ($result) { + return htmlspecialchars($result['name'], ENT_COMPAT, 'UTF-8'); + } + + return static::value($value); + } + + /** + * Get the sanitized language + * + * @param mixed $value Value of the field + * + * @return mixed String/void + * + * @since 1.6 + */ + public function language($value) + { + if (!$value) { + return static::value($value); + } + + $path = LanguageHelper::getLanguagePath(JPATH_SITE, $value); + $file = $path . '/langmetadata.xml'; + + if (!is_file($file)) { + // For language packs from before 4.0. + $file = $path . '/' . $value . '.xml'; + + if (!is_file($file)) { + return static::value($value); + } + } + + $result = LanguageHelper::parseXMLLanguageFile($file); + + if ($result) { + return htmlspecialchars($result['name'], ENT_COMPAT, 'UTF-8'); + } + + return static::value($value); + } + + /** + * Get the sanitized editor name + * + * @param mixed $value Value of the field + * + * @return mixed String/void + * + * @since 1.6 + */ + public function editor($value) + { + if (empty($value)) { + return static::value($value); + } else { + $db = Factory::getDbo(); + $lang = Factory::getLanguage(); + $query = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = :element') + ->where($db->quoteName('folder') . ' = ' . $db->quote('editors')) + ->bind(':element', $value); + $db->setQuery($query); + $title = $db->loadResult(); + + if ($title) { + $lang->load("plg_editors_$value.sys", JPATH_ADMINISTRATOR) + || $lang->load("plg_editors_$value.sys", JPATH_PLUGINS . '/editors/' . $value); + $lang->load($title . '.sys'); + + return Text::_($title); + } else { + return static::value(''); + } + } + } } diff --git a/administrator/components/com_users/src/Table/MfaTable.php b/administrator/components/com_users/src/Table/MfaTable.php index 4e201e279c051..324e175f4e093 100644 --- a/administrator/components/com_users/src/Table/MfaTable.php +++ b/administrator/components/com_users/src/Table/MfaTable.php @@ -1,4 +1,5 @@ encryptService = new Encrypt; - } - - /** - * Method to store a row in the database from the Table instance properties. - * - * If a primary key value is set the row with that primary key value will be updated with the instance property values. - * If no primary key value is set a new row will be inserted into the database with the properties from the Table instance. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success. - * - * @since 4.2.0 - */ - public function store($updateNulls = true) - { - // Encrypt the options before saving them - $this->options = $this->encryptService->encrypt(json_encode($this->options ?: [])); - - // Set last_used date to null if empty or zero date + /** + * Table constructor + * + * @param DatabaseDriver $db Database driver object + * @param DispatcherInterface|null $dispatcher Events dispatcher object + * + * @since 4.2.0 + */ + public function __construct(DatabaseDriver $db, DispatcherInterface $dispatcher = null) + { + parent::__construct('#__user_mfa', 'id', $db, $dispatcher); + + $this->encryptService = new Encrypt(); + } + + /** + * Method to store a row in the database from the Table instance properties. + * + * If a primary key value is set the row with that primary key value will be updated with the instance property values. + * If no primary key value is set a new row will be inserted into the database with the properties from the Table instance. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success. + * + * @since 4.2.0 + */ + public function store($updateNulls = true) + { + // Encrypt the options before saving them + $this->options = $this->encryptService->encrypt(json_encode($this->options ?: [])); + + // Set last_used date to null if empty or zero date // phpcs:ignore if (!((int) $this->last_used)) - { + { // phpcs:ignore $this->last_used = null; - } + } // phpcs:ignore $records = MfaHelper::getUserMfaRecords($this->user_id); - if ($this->id) - { - // Existing record. Remove it from the list of records. - $records = array_filter( - $records, - function ($rec) { - return $rec->id != $this->id; - } - ); - } - - // Update the dates on a new record - if (empty($this->id)) - { + if ($this->id) { + // Existing record. Remove it from the list of records. + $records = array_filter( + $records, + function ($rec) { + return $rec->id != $this->id; + } + ); + } + + // Update the dates on a new record + if (empty($this->id)) { // phpcs:ignore $this->created_on = Date::getInstance()->toSql(); // phpcs:ignore $this->last_used = null; - } - - // Do I need to mark this record as the default? - if ($this->default == 0) - { - $hasDefaultRecord = array_reduce( - $records, - function ($carry, $record) - { - return $carry || ($record->default == 1); - }, - false - ); - - $this->default = $hasDefaultRecord ? 0 : 1; - } - - // Let's find out if we are saving a new MFA method record without having backup codes yet. - $mustCreateBackupCodes = false; - - if (empty($this->id) && $this->method !== 'backupcodes') - { - // Do I have any backup records? - $hasBackupCodes = array_reduce( - $records, - function (bool $carry, $record) - { - return $carry || $record->method === 'backupcodes'; - }, - false - ); - - $mustCreateBackupCodes = !$hasBackupCodes; - - // If the only other entry is the backup records one I need to make this the default method - if ($hasBackupCodes && count($records) === 1) - { - $this->default = 1; - } - } - - // Store the record - try - { - $result = parent::store($updateNulls); - } - catch (Throwable $e) - { - $this->setError($e->getMessage()); - - $result = false; - } - - // Decrypt the options (they must be decrypted in memory) - $this->decryptOptions(); - - if ($result) - { - // If this record is the default unset the default flag from all other records - $this->switchDefaultRecord(); - - // Do I need to generate backup codes? - if ($mustCreateBackupCodes) - { - $this->generateBackupCodes(); - } - } - - return $result; - } - - /** - * Method to load a row from the database by primary key and bind the fields to the Table instance properties. - * - * @param mixed $keys An optional primary key value to load the row by, or an array of fields to match. - * If not set the instance property value is used. - * @param boolean $reset True to reset the default values before loading the new row. - * - * @return boolean True if successful. False if row not found. - * - * @since 4.2.0 - * @throws \InvalidArgumentException - * @throws RuntimeException - * @throws \UnexpectedValueException - */ - public function load($keys = null, $reset = true) - { - $result = parent::load($keys, $reset); - - if ($result) - { - $this->decryptOptions(); - } - - return $result; - } - - /** - * Method to delete a row from the database table by primary key value. - * - * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used. - * - * @return boolean True on success. - * - * @since 4.2.0 - * @throws \UnexpectedValueException - */ - public function delete($pk = null) - { - $record = $this; - - if ($pk != $this->id) - { - $record = clone $this; - $record->reset(); - $result = $record->load($pk); - - if (!$result) - { - // If the record does not exist I will stomp my feet and deny your request - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - } - - $user = Factory::getApplication()->getIdentity() - ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - // The user must be a registered user, not a guest - if ($user->guest) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - // Save flags used onAfterDelete - $this->deleteFlags[$record->id] = [ - 'default' => $record->default, + } + + // Do I need to mark this record as the default? + if ($this->default == 0) { + $hasDefaultRecord = array_reduce( + $records, + function ($carry, $record) { + return $carry || ($record->default == 1); + }, + false + ); + + $this->default = $hasDefaultRecord ? 0 : 1; + } + + // Let's find out if we are saving a new MFA method record without having backup codes yet. + $mustCreateBackupCodes = false; + + if (empty($this->id) && $this->method !== 'backupcodes') { + // Do I have any backup records? + $hasBackupCodes = array_reduce( + $records, + function (bool $carry, $record) { + return $carry || $record->method === 'backupcodes'; + }, + false + ); + + $mustCreateBackupCodes = !$hasBackupCodes; + + // If the only other entry is the backup records one I need to make this the default method + if ($hasBackupCodes && count($records) === 1) { + $this->default = 1; + } + } + + // Store the record + try { + $result = parent::store($updateNulls); + } catch (Throwable $e) { + $this->setError($e->getMessage()); + + $result = false; + } + + // Decrypt the options (they must be decrypted in memory) + $this->decryptOptions(); + + if ($result) { + // If this record is the default unset the default flag from all other records + $this->switchDefaultRecord(); + + // Do I need to generate backup codes? + if ($mustCreateBackupCodes) { + $this->generateBackupCodes(); + } + } + + return $result; + } + + /** + * Method to load a row from the database by primary key and bind the fields to the Table instance properties. + * + * @param mixed $keys An optional primary key value to load the row by, or an array of fields to match. + * If not set the instance property value is used. + * @param boolean $reset True to reset the default values before loading the new row. + * + * @return boolean True if successful. False if row not found. + * + * @since 4.2.0 + * @throws \InvalidArgumentException + * @throws RuntimeException + * @throws \UnexpectedValueException + */ + public function load($keys = null, $reset = true) + { + $result = parent::load($keys, $reset); + + if ($result) { + $this->decryptOptions(); + } + + return $result; + } + + /** + * Method to delete a row from the database table by primary key value. + * + * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used. + * + * @return boolean True on success. + * + * @since 4.2.0 + * @throws \UnexpectedValueException + */ + public function delete($pk = null) + { + $record = $this; + + if ($pk != $this->id) { + $record = clone $this; + $record->reset(); + $result = $record->load($pk); + + if (!$result) { + // If the record does not exist I will stomp my feet and deny your request + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } + + $user = Factory::getApplication()->getIdentity() + ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + // The user must be a registered user, not a guest + if ($user->guest) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Save flags used onAfterDelete + $this->deleteFlags[$record->id] = [ + 'default' => $record->default, // phpcs:ignore 'numRecords' => $this->getNumRecords($record->user_id), // phpcs:ignore 'user_id' => $record->user_id, - 'method' => $record->method, - ]; + 'method' => $record->method, + ]; - if (\is_null($pk)) - { + if (\is_null($pk)) { // phpcs:ignore $pk = [$this->_tbl_key => $this->id]; - } - elseif (!\is_array($pk)) - { + } elseif (!\is_array($pk)) { // phpcs:ignore $pk = [$this->_tbl_key => $pk]; - } - - $isDeleted = parent::delete($pk); - - if ($isDeleted) - { - $this->afterDelete($pk); - } - - return $isDeleted; - } - - /** - * Decrypt the possibly encrypted options - * - * @return void - * @since 4.2.0 - */ - private function decryptOptions(): void - { - // Try with modern decryption - $decrypted = @json_decode($this->encryptService->decrypt($this->options ?? ''), true); - - if (is_string($decrypted)) - { - $decrypted = @json_decode($decrypted, true); - } - - // Fall back to legacy decryption - if (!is_array($decrypted)) - { - $decrypted = @json_decode($this->encryptService->decrypt($this->options ?? '', true), true); - - if (is_string($decrypted)) - { - $decrypted = @json_decode($decrypted, true); - } - } - - $this->options = $decrypted ?: []; - } - - /** - * If this record is set to be the default, unset the default flag from the other records for the same user. - * - * @return void - * @since 4.2.0 - */ - private function switchDefaultRecord(): void - { - if (!$this->default) - { - return; - } - - /** - * This record is marked as default, therefore we need to unset the default flag from all other records for this - * user. - */ - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->update($db->quoteName('#__user_mfa')) - ->set($db->quoteName('default') . ' = 0') - ->where($db->quoteName('user_id') . ' = :user_id') - ->where($db->quoteName('id') . ' != :id') + } + + $isDeleted = parent::delete($pk); + + if ($isDeleted) { + $this->afterDelete($pk); + } + + return $isDeleted; + } + + /** + * Decrypt the possibly encrypted options + * + * @return void + * @since 4.2.0 + */ + private function decryptOptions(): void + { + // Try with modern decryption + $decrypted = @json_decode($this->encryptService->decrypt($this->options ?? ''), true); + + if (is_string($decrypted)) { + $decrypted = @json_decode($decrypted, true); + } + + // Fall back to legacy decryption + if (!is_array($decrypted)) { + $decrypted = @json_decode($this->encryptService->decrypt($this->options ?? '', true), true); + + if (is_string($decrypted)) { + $decrypted = @json_decode($decrypted, true); + } + } + + $this->options = $decrypted ?: []; + } + + /** + * If this record is set to be the default, unset the default flag from the other records for the same user. + * + * @return void + * @since 4.2.0 + */ + private function switchDefaultRecord(): void + { + if (!$this->default) { + return; + } + + /** + * This record is marked as default, therefore we need to unset the default flag from all other records for this + * user. + */ + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__user_mfa')) + ->set($db->quoteName('default') . ' = 0') + ->where($db->quoteName('user_id') . ' = :user_id') + ->where($db->quoteName('id') . ' != :id') // phpcs:ignore ->bind(':user_id', $this->user_id, ParameterType::INTEGER) - ->bind(':id', $this->id, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - } - - /** - * Regenerate backup code is the flag is set. - * - * @return void - * @throws Exception - * @since 4.2.0 - */ - private function generateBackupCodes(): void - { - /** @var MVCFactoryInterface $factory */ - $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory(); - - /** @var BackupcodesModel $backupCodes */ - $backupCodes = $factory->createModel('Backupcodes', 'Administrator'); + ->bind(':id', $this->id, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + } + + /** + * Regenerate backup code is the flag is set. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + private function generateBackupCodes(): void + { + /** @var MVCFactoryInterface $factory */ + $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory(); + + /** @var BackupcodesModel $backupCodes */ + $backupCodes = $factory->createModel('Backupcodes', 'Administrator'); // phpcs:ignore $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($this->user_id); - $backupCodes->regenerateBackupCodes($user); - } - - /** - * Runs after successfully deleting a record - * - * @param int|array $pk The promary key of the deleted record - * - * @return void - * @since 4.2.0 - */ - private function afterDelete($pk): void - { - if (is_array($pk)) - { + $backupCodes->regenerateBackupCodes($user); + } + + /** + * Runs after successfully deleting a record + * + * @param int|array $pk The promary key of the deleted record + * + * @return void + * @since 4.2.0 + */ + private function afterDelete($pk): void + { + if (is_array($pk)) { // phpcs:ignore $pk = $pk[$this->_tbl_key] ?? array_shift($pk); - } - - if (!isset($this->deleteFlags[$pk])) - { - return; - } - - if (($this->deleteFlags[$pk]['numRecords'] <= 2) && ($this->deleteFlags[$pk]['method'] != 'backupcodes')) - { - /** - * This was the second to last MFA record in the database (the last one is the `backupcodes`). Therefore, we - * need to delete the remaining entry and go away. We don't trigger this if the Method we are deleting was - * the `backupcodes` because we might just be regenerating the backup codes. - */ - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__user_mfa')) - ->where($db->quoteName('user_id') . ' = :user_id') - ->bind(':user_id', $this->deleteFlags[$pk]['user_id'], ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - unset($this->deleteFlags[$pk]); - - return; - } - - // This was the default record. Promote the next available record to default. - if ($this->deleteFlags[$pk]['default']) - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__user_mfa')) - ->where($db->quoteName('user_id') . ' = :user_id') - ->where($db->quoteName('method') . ' != ' . $db->quote('backupcodes')) - ->bind(':user_id', $this->deleteFlags[$pk]['user_id'], ParameterType::INTEGER); - $ids = $db->setQuery($query)->loadColumn(); - - if (empty($ids)) - { - return; - } - - $id = array_shift($ids); - $query = $db->getQuery(true) - ->update($db->quoteName('#__user_mfa')) - ->set($db->quoteName('default') . ' = 1') - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - } - } - - /** - * Get the number of MFA records for a give user ID - * - * @param int $userId The user ID to check - * - * @return integer - * - * @since 4.2.0 - */ - private function getNumRecords(int $userId): int - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__user_mfa')) - ->where($db->quoteName('user_id') . ' = :user_id') - ->bind(':user_id', $userId, ParameterType::INTEGER); - $numOldRecords = $db->setQuery($query)->loadResult(); - - return (int) $numOldRecords; - } + } + + if (!isset($this->deleteFlags[$pk])) { + return; + } + + if (($this->deleteFlags[$pk]['numRecords'] <= 2) && ($this->deleteFlags[$pk]['method'] != 'backupcodes')) { + /** + * This was the second to last MFA record in the database (the last one is the `backupcodes`). Therefore, we + * need to delete the remaining entry and go away. We don't trigger this if the Method we are deleting was + * the `backupcodes` because we might just be regenerating the backup codes. + */ + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__user_mfa')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->bind(':user_id', $this->deleteFlags[$pk]['user_id'], ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + unset($this->deleteFlags[$pk]); + + return; + } + + // This was the default record. Promote the next available record to default. + if ($this->deleteFlags[$pk]['default']) { + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__user_mfa')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->where($db->quoteName('method') . ' != ' . $db->quote('backupcodes')) + ->bind(':user_id', $this->deleteFlags[$pk]['user_id'], ParameterType::INTEGER); + $ids = $db->setQuery($query)->loadColumn(); + + if (empty($ids)) { + return; + } + + $id = array_shift($ids); + $query = $db->getQuery(true) + ->update($db->quoteName('#__user_mfa')) + ->set($db->quoteName('default') . ' = 1') + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + } + } + + /** + * Get the number of MFA records for a give user ID + * + * @param int $userId The user ID to check + * + * @return integer + * + * @since 4.2.0 + */ + private function getNumRecords(int $userId): int + { + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__user_mfa')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->bind(':user_id', $userId, ParameterType::INTEGER); + $numOldRecords = $db->setQuery($query)->loadResult(); + + return (int) $numOldRecords; + } } diff --git a/administrator/components/com_users/src/Table/NoteTable.php b/administrator/components/com_users/src/Table/NoteTable.php index b7b22b26a5a87..7496acc27d617 100644 --- a/administrator/components/com_users/src/Table/NoteTable.php +++ b/administrator/components/com_users/src/Table/NoteTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_users.note'; - parent::__construct('#__user_notes', 'id', $db); - - $this->setColumnAlias('published', 'state'); - } - - /** - * Overloaded store method for the notes table. - * - * @param boolean $updateNulls Toggle whether null values should be updated. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate()->toSql(); - $userId = Factory::getUser()->get('id'); - - if (!((int) $this->review_time)) - { - $this->review_time = null; - } - - if ($this->id) - { - // Existing item - $this->modified_time = $date; - $this->modified_user_id = $userId; - } - else - { - // New record. - $this->created_time = $date; - $this->created_user_id = $userId; - $this->modified_time = $date; - $this->modified_user_id = $userId; - } - - // Attempt to store the data. - return parent::store($updateNulls); - } - - /** - * Method to perform sanity checks on the Table instance properties to ensure they are safe to store in the database. - * - * @return boolean True if the instance is sane and able to be stored in the database. - * - * @since 4.0.0 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (empty($this->modified_time)) - { - $this->modified_time = $this->created_time; - } - - if (empty($this->modified_user_id)) - { - $this->modified_user_id = $this->created_user_id; - } - - return true; - } - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Constructor + * + * @param DatabaseDriver $db Database object + * + * @since 2.5 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_users.note'; + parent::__construct('#__user_notes', 'id', $db); + + $this->setColumnAlias('published', 'state'); + } + + /** + * Overloaded store method for the notes table. + * + * @param boolean $updateNulls Toggle whether null values should be updated. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate()->toSql(); + $userId = Factory::getUser()->get('id'); + + if (!((int) $this->review_time)) { + $this->review_time = null; + } + + if ($this->id) { + // Existing item + $this->modified_time = $date; + $this->modified_user_id = $userId; + } else { + // New record. + $this->created_time = $date; + $this->created_user_id = $userId; + $this->modified_time = $date; + $this->modified_user_id = $userId; + } + + // Attempt to store the data. + return parent::store($updateNulls); + } + + /** + * Method to perform sanity checks on the Table instance properties to ensure they are safe to store in the database. + * + * @return boolean True if the instance is sane and able to be stored in the database. + * + * @since 4.0.0 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (empty($this->modified_time)) { + $this->modified_time = $this->created_time; + } + + if (empty($this->modified_user_id)) { + $this->modified_user_id = $this->created_user_id; + } + + return true; + } + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } } diff --git a/administrator/components/com_users/src/View/Captive/HtmlView.php b/administrator/components/com_users/src/View/Captive/HtmlView.php index 739de5d1e5b7d..a569c5b6ff346 100644 --- a/administrator/components/com_users/src/View/Captive/HtmlView.php +++ b/administrator/components/com_users/src/View/Captive/HtmlView.php @@ -1,4 +1,5 @@ setSiteTemplateStyle(); - - $app = Factory::getApplication(); - $user = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - PluginHelper::importPlugin('multifactorauth'); - $event = new BeforeDisplayMethods($user); - $app->getDispatcher()->dispatch($event->getName(), $event); - - /** @var CaptiveModel $model */ - $model = $this->getModel(); - - // Load data from the model - $this->isAdmin = $app->isClient('administrator'); - $this->records = $this->get('records'); - $this->record = $this->get('record'); - $this->mfaMethods = MfaHelper::getMfaMethods(); - - if (!empty($this->records)) - { - /** @var BackupcodesModel $codesModel */ - $codesModel = $this->getModel('Backupcodes'); - $backupCodesRecord = $codesModel->getBackupCodesRecord(); - - if (!is_null($backupCodesRecord)) - { - $backupCodesRecord->title = Text::_('COM_USERS_USER_BACKUPCODES'); - $this->records[] = $backupCodesRecord; - } - } - - // If we only have one record there's no point asking the user to select a MFA Method - if (empty($this->record) && !empty($this->records)) - { - // Default to the first record - $this->record = reset($this->records); - - // If we have multiple records try to make this record the default - if (count($this->records) > 1) - { - foreach ($this->records as $record) - { - if ($record->default) - { - $this->record = $record; - - break; - } - } - } - } - - // Set the correct layout based on the availability of a MFA record - $this->setLayout('default'); - - // If we have no record selected or explicitly asked to run the 'select' task use the correct layout - if (is_null($this->record) || ($model->getState('task') == 'select')) - { - $this->setLayout('select'); - } - - switch ($this->getLayout()) - { - case 'select': - $this->allowEntryBatching = 1; - - $event = new NotifyActionLog('onComUsersCaptiveShowSelect', []); - Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); - break; - - case 'default': - default: - $this->renderOptions = $model->loadCaptiveRenderOptions($this->record); - $this->allowEntryBatching = $this->renderOptions['allowEntryBatching'] ?? 0; - - $event = new NotifyActionLog( - 'onComUsersCaptiveShowCaptive', - [ - $this->escape($this->record->title), - ] - ); - Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); - break; - } - - // Which title should I use for the page? - $this->title = $this->get('PageTitle'); - - // Back-end: always show a title in the 'title' module position, not in the page body - if ($this->isAdmin) - { - ToolbarHelper::title(Text::_('COM_USERS_HEADING_MFA'), 'users user-lock'); - $this->title = ''; - } - - if ($this->isAdmin && $this->getLayout() === 'default') - { - $bar = Toolbar::getInstance(); - $button = (new BasicButton('user-mfa-submit')) - ->text($this->renderOptions['submit_text']) - ->icon($this->renderOptions['submit_icon']); - $bar->appendButton($button); - - $button = (new BasicButton('user-mfa-logout')) - ->text('COM_USERS_MFA_LOGOUT') - ->buttonClass('btn btn-danger') - ->icon('icon icon-lock'); - $bar->appendButton($button); - - if (count($this->records) > 1) - { - $arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; - $button = (new BasicButton('user-mfa-choose-another')) - ->text('COM_USERS_MFA_USE_DIFFERENT_METHOD') - ->icon('icon-' . $arrow); - $bar->appendButton($button); - } - } - - // Display the view - parent::display($tpl); - } + use SiteTemplateTrait; + + /** + * The MFA Method records for the current user which correspond to enabled plugins + * + * @var array + * @since 4.2.0 + */ + public $records = []; + + /** + * The currently selected MFA Method record against which we'll be authenticating + * + * @var null|stdClass + * @since 4.2.0 + */ + public $record = null; + + /** + * The Captive MFA page's rendering options + * + * @var array|null + * @since 4.2.0 + */ + public $renderOptions = null; + + /** + * The title to display at the top of the page + * + * @var string + * @since 4.2.0 + */ + public $title = ''; + + /** + * Is this an administrator page? + * + * @var boolean + * @since 4.2.0 + */ + public $isAdmin = false; + + /** + * Does the currently selected Method allow authenticating against all of its records? + * + * @var boolean + * @since 4.2.0 + */ + public $allowEntryBatching = false; + + /** + * All enabled MFA Methods (plugins) + * + * @var array + * @since 4.2.0 + */ + public $mfaMethods; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void A string if successful, otherwise an Error object. + * + * @throws Exception + * @since 4.2.0 + */ + public function display($tpl = null) + { + $this->setSiteTemplateStyle(); + + $app = Factory::getApplication(); + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + PluginHelper::importPlugin('multifactorauth'); + $event = new BeforeDisplayMethods($user); + $app->getDispatcher()->dispatch($event->getName(), $event); + + /** @var CaptiveModel $model */ + $model = $this->getModel(); + + // Load data from the model + $this->isAdmin = $app->isClient('administrator'); + $this->records = $this->get('records'); + $this->record = $this->get('record'); + $this->mfaMethods = MfaHelper::getMfaMethods(); + + if (!empty($this->records)) { + /** @var BackupcodesModel $codesModel */ + $codesModel = $this->getModel('Backupcodes'); + $backupCodesRecord = $codesModel->getBackupCodesRecord(); + + if (!is_null($backupCodesRecord)) { + $backupCodesRecord->title = Text::_('COM_USERS_USER_BACKUPCODES'); + $this->records[] = $backupCodesRecord; + } + } + + // If we only have one record there's no point asking the user to select a MFA Method + if (empty($this->record) && !empty($this->records)) { + // Default to the first record + $this->record = reset($this->records); + + // If we have multiple records try to make this record the default + if (count($this->records) > 1) { + foreach ($this->records as $record) { + if ($record->default) { + $this->record = $record; + + break; + } + } + } + } + + // Set the correct layout based on the availability of a MFA record + $this->setLayout('default'); + + // If we have no record selected or explicitly asked to run the 'select' task use the correct layout + if (is_null($this->record) || ($model->getState('task') == 'select')) { + $this->setLayout('select'); + } + + switch ($this->getLayout()) { + case 'select': + $this->allowEntryBatching = 1; + + $event = new NotifyActionLog('onComUsersCaptiveShowSelect', []); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + break; + + case 'default': + default: + $this->renderOptions = $model->loadCaptiveRenderOptions($this->record); + $this->allowEntryBatching = $this->renderOptions['allowEntryBatching'] ?? 0; + + $event = new NotifyActionLog( + 'onComUsersCaptiveShowCaptive', + [ + $this->escape($this->record->title), + ] + ); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + break; + } + + // Which title should I use for the page? + $this->title = $this->get('PageTitle'); + + // Back-end: always show a title in the 'title' module position, not in the page body + if ($this->isAdmin) { + ToolbarHelper::title(Text::_('COM_USERS_HEADING_MFA'), 'users user-lock'); + $this->title = ''; + } + + if ($this->isAdmin && $this->getLayout() === 'default') { + $bar = Toolbar::getInstance(); + $button = (new BasicButton('user-mfa-submit')) + ->text($this->renderOptions['submit_text']) + ->icon($this->renderOptions['submit_icon']); + $bar->appendButton($button); + + $button = (new BasicButton('user-mfa-logout')) + ->text('COM_USERS_MFA_LOGOUT') + ->buttonClass('btn btn-danger') + ->icon('icon icon-lock'); + $bar->appendButton($button); + + if (count($this->records) > 1) { + $arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + $button = (new BasicButton('user-mfa-choose-another')) + ->text('COM_USERS_MFA_USE_DIFFERENT_METHOD') + ->icon('icon-' . $arrow); + $bar->appendButton($button); + } + } + + // Display the view + parent::display($tpl); + } } diff --git a/administrator/components/com_users/src/View/Debuggroup/HtmlView.php b/administrator/components/com_users/src/View/Debuggroup/HtmlView.php index 77dc0693c26fc..f40d59b3ee272 100644 --- a/administrator/components/com_users/src/View/Debuggroup/HtmlView.php +++ b/administrator/components/com_users/src/View/Debuggroup/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser()->authorise('core.manage', 'com_users')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $this->actions = $this->get('DebugActions'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->group = $this->get('Group'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::sprintf('COM_USERS_VIEW_DEBUG_GROUP_TITLE', $this->group->id, $this->escape($this->group->title)), 'users groups'); - ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_users'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Permissions_for_Group'); - } + /** + * List of component actions + * + * @var array + */ + protected $actions; + + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * The id and title for the user group. + * + * @var \stdClass + * @since 4.0.0 + */ + protected $group; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + // Access check. + if (!$this->getCurrentUser()->authorise('core.manage', 'com_users')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $this->actions = $this->get('DebugActions'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->group = $this->get('Group'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::sprintf('COM_USERS_VIEW_DEBUG_GROUP_TITLE', $this->group->id, $this->escape($this->group->title)), 'users groups'); + ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_users'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Permissions_for_Group'); + } } diff --git a/administrator/components/com_users/src/View/Debuguser/HtmlView.php b/administrator/components/com_users/src/View/Debuguser/HtmlView.php index 7d2c463949056..4b9206df6684d 100644 --- a/administrator/components/com_users/src/View/Debuguser/HtmlView.php +++ b/administrator/components/com_users/src/View/Debuguser/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser()->authorise('core.manage', 'com_users')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $this->actions = $this->get('DebugActions'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->user = $this->get('User'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::sprintf('COM_USERS_VIEW_DEBUG_USER_TITLE', $this->user->id, $this->escape($this->user->name)), 'users user'); - ToolbarHelper::cancel('user.cancel', 'JTOOLBAR_CLOSE'); - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_users'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Permissions_for_User'); - } + /** + * List of component actions + * + * @var array + */ + protected $actions; + + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * The user object of the user being debugged. + * + * @var User + */ + protected $user; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + // Access check. + if (!$this->getCurrentUser()->authorise('core.manage', 'com_users')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $this->actions = $this->get('DebugActions'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->user = $this->get('User'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::sprintf('COM_USERS_VIEW_DEBUG_USER_TITLE', $this->user->id, $this->escape($this->user->name)), 'users user'); + ToolbarHelper::cancel('user.cancel', 'JTOOLBAR_CLOSE'); + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_users'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Permissions_for_User'); + } } diff --git a/administrator/components/com_users/src/View/Group/HtmlView.php b/administrator/components/com_users/src/View/Group/HtmlView.php index ab9e05db06366..67d57aadc89a2 100644 --- a/administrator/components/com_users/src/View/Group/HtmlView.php +++ b/administrator/components/com_users/src/View/Group/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $isNew = ($this->item->id == 0); - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::_($isNew ? 'COM_USERS_VIEW_NEW_GROUP_TITLE' : 'COM_USERS_VIEW_EDIT_GROUP_TITLE'), 'users-cog groups-add'); - - $toolbarButtons = []; - - if ($canDo->get('core.edit') || $canDo->get('core.create')) - { - ToolbarHelper::apply('group.apply'); - $toolbarButtons[] = ['save', 'group.save']; - } - - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'group.save2new']; - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'group.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('group.cancel'); - } - else - { - ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Users:_New_or_Edit_Group'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $item; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $isNew = ($this->item->id == 0); + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::_($isNew ? 'COM_USERS_VIEW_NEW_GROUP_TITLE' : 'COM_USERS_VIEW_EDIT_GROUP_TITLE'), 'users-cog groups-add'); + + $toolbarButtons = []; + + if ($canDo->get('core.edit') || $canDo->get('core.create')) { + ToolbarHelper::apply('group.apply'); + $toolbarButtons[] = ['save', 'group.save']; + } + + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'group.save2new']; + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'group.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('group.cancel'); + } else { + ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Users:_New_or_Edit_Group'); + } } diff --git a/administrator/components/com_users/src/View/Groups/HtmlView.php b/administrator/components/com_users/src/View/Groups/HtmlView.php index fe17b424c0bc9..b956092a04c90 100644 --- a/administrator/components/com_users/src/View/Groups/HtmlView.php +++ b/administrator/components/com_users/src/View/Groups/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::_('COM_USERS_VIEW_GROUPS_TITLE'), 'users-cog groups'); - - if ($canDo->get('core.create')) - { - ToolbarHelper::addNew('group.add'); - } - - if ($canDo->get('core.delete')) - { - ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'groups.delete', 'JTOOLBAR_DELETE'); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_users'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Users:_Groups'); - } + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::_('COM_USERS_VIEW_GROUPS_TITLE'), 'users-cog groups'); + + if ($canDo->get('core.create')) { + ToolbarHelper::addNew('group.add'); + } + + if ($canDo->get('core.delete')) { + ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'groups.delete', 'JTOOLBAR_DELETE'); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_users'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Users:_Groups'); + } } diff --git a/administrator/components/com_users/src/View/Level/HtmlView.php b/administrator/components/com_users/src/View/Level/HtmlView.php index 8d53c2c7d2ae3..eb9bbfaac65be 100644 --- a/administrator/components/com_users/src/View/Level/HtmlView.php +++ b/administrator/components/com_users/src/View/Level/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $isNew = ($this->item->id == 0); - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::_($isNew ? 'COM_USERS_VIEW_NEW_LEVEL_TITLE' : 'COM_USERS_VIEW_EDIT_LEVEL_TITLE'), 'user-lock levels-add'); - - $toolbarButtons = []; - - if ($canDo->get('core.edit') || $canDo->get('core.create')) - { - ToolbarHelper::apply('level.apply'); - $toolbarButtons[] = ['save', 'level.save']; - } - - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'level.save2new']; - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'level.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('level.cancel'); - } - else - { - ToolbarHelper::cancel('level.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Users:_Edit_Viewing_Access_Level'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $item; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $isNew = ($this->item->id == 0); + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::_($isNew ? 'COM_USERS_VIEW_NEW_LEVEL_TITLE' : 'COM_USERS_VIEW_EDIT_LEVEL_TITLE'), 'user-lock levels-add'); + + $toolbarButtons = []; + + if ($canDo->get('core.edit') || $canDo->get('core.create')) { + ToolbarHelper::apply('level.apply'); + $toolbarButtons[] = ['save', 'level.save']; + } + + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'level.save2new']; + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'level.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('level.cancel'); + } else { + ToolbarHelper::cancel('level.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Users:_Edit_Viewing_Access_Level'); + } } diff --git a/administrator/components/com_users/src/View/Levels/HtmlView.php b/administrator/components/com_users/src/View/Levels/HtmlView.php index 6c4429c6c64a7..e94d23ab9b26c 100644 --- a/administrator/components/com_users/src/View/Levels/HtmlView.php +++ b/administrator/components/com_users/src/View/Levels/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::_('COM_USERS_VIEW_LEVELS_TITLE'), 'user-lock levels'); - - if ($canDo->get('core.create')) - { - ToolbarHelper::addNew('level.add'); - } - - if ($canDo->get('core.delete')) - { - ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'level.delete', 'JTOOLBAR_DELETE'); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_users'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Users:_Viewing_Access_Levels'); - } + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::_('COM_USERS_VIEW_LEVELS_TITLE'), 'user-lock levels'); + + if ($canDo->get('core.create')) { + ToolbarHelper::addNew('level.add'); + } + + if ($canDo->get('core.delete')) { + ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'level.delete', 'JTOOLBAR_DELETE'); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_users'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Users:_Viewing_Access_Levels'); + } } diff --git a/administrator/components/com_users/src/View/Mail/HtmlView.php b/administrator/components/com_users/src/View/Mail/HtmlView.php index beb275ee904b6..4ad42cc833064 100644 --- a/administrator/components/com_users/src/View/Mail/HtmlView.php +++ b/administrator/components/com_users/src/View/Mail/HtmlView.php @@ -1,4 +1,5 @@ get('massmailoff', 0) == 1) - { - Factory::getApplication()->redirect(Route::_('index.php', false)); - } + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @throws \Exception + */ + public function display($tpl = null) + { + // Redirect to admin index if mass mailer disabled in conf + if (Factory::getApplication()->get('massmailoff', 0) == 1) { + Factory::getApplication()->redirect(Route::_('index.php', false)); + } - // Get data from the model - $this->form = $this->get('Form'); + // Get data from the model + $this->form = $this->get('Form'); - $this->addToolbar(); - parent::display($tpl); - } + $this->addToolbar(); + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); - ToolbarHelper::title(Text::_('COM_USERS_MASS_MAIL'), 'users massmail'); - ToolbarHelper::custom('mail.send', 'envelope', '', 'COM_USERS_TOOLBAR_MAIL_SEND_MAIL', false); - ToolbarHelper::cancel('mail.cancel'); - ToolbarHelper::divider(); - ToolbarHelper::preferences('com_users'); - ToolbarHelper::divider(); - ToolbarHelper::help('Mass_Mail_Users'); - } + ToolbarHelper::title(Text::_('COM_USERS_MASS_MAIL'), 'users massmail'); + ToolbarHelper::custom('mail.send', 'envelope', '', 'COM_USERS_TOOLBAR_MAIL_SEND_MAIL', false); + ToolbarHelper::cancel('mail.cancel'); + ToolbarHelper::divider(); + ToolbarHelper::preferences('com_users'); + ToolbarHelper::divider(); + ToolbarHelper::help('Mass_Mail_Users'); + } } diff --git a/administrator/components/com_users/src/View/Method/HtmlView.php b/administrator/components/com_users/src/View/Method/HtmlView.php index 450c7f19f1c79..bbdaf82728016 100644 --- a/administrator/components/com_users/src/View/Method/HtmlView.php +++ b/administrator/components/com_users/src/View/Method/HtmlView.php @@ -1,4 +1,5 @@ user)) - { - $this->user = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - /** @var MethodModel $model */ - $model = $this->getModel(); - $this->setLayout('edit'); - $this->renderOptions = $model->getRenderOptions($this->user); - $this->record = $model->getRecord($this->user); - $this->title = $model->getPageTitle(); - $this->isAdmin = $app->isClient('administrator'); - - // Backup codes are a special case, rendered with a special layout - if ($this->record->method == 'backupcodes') - { - $this->setLayout('backupcodes'); - - $backupCodes = $this->record->options; - - if (!is_array($backupCodes)) - { - $backupCodes = []; - } - - $backupCodes = array_filter( - $backupCodes, - function ($x) { - return !empty($x); - } - ); - - if (count($backupCodes) % 2 != 0) - { - $backupCodes[] = ''; - } - - /** - * The call to array_merge resets the array indices. This is necessary since array_filter kept the indices, - * meaning our elements are completely out of order. - */ - $this->backupCodes = array_merge($backupCodes); - } - - // Set up the isEditExisting property. - $this->isEditExisting = !empty($this->record->id); - - // Back-end: always show a title in the 'title' module position, not in the page body - if ($this->isAdmin) - { - ToolbarHelper::title($this->title, 'users user-lock'); - - $helpUrl = $this->renderOptions['help_url']; - - if (!empty($helpUrl)) - { - ToolbarHelper::help('', false, $helpUrl); - } - - $this->title = ''; - } - - $returnUrl = empty($this->returnURL) ? '' : base64_decode($this->returnURL); - $returnUrl = $returnUrl ?: Route::_('index.php?option=com_users&task=methods.display&user_id=' . $this->user->id); - - if ($this->isAdmin && $this->getLayout() === 'edit') - { - $bar = Toolbar::getInstance(); - $button = (new BasicButton('user-mfa-edit-save')) - ->text($this->renderOptions['submit_text']) - ->icon($this->renderOptions['submit_icon']) - ->onclick('document.getElementById(\'user-mfa-edit-save\').click()'); - - if ($this->renderOptions['show_submit'] || $this->isEditExisting) - { - $bar->appendButton($button); - } - - $button = (new LinkButton('user-mfa-edit-cancel')) - ->text('JCANCEL') - ->buttonClass('btn btn-danger') - ->icon('icon-cancel-2') - ->url($returnUrl); - $bar->appendButton($button); - } - elseif ($this->isAdmin && $this->getLayout() === 'backupcodes') - { - $bar = Toolbar::getInstance(); - - $arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; - $button = (new LinkButton('user-mfa-edit-cancel')) - ->text('JTOOLBAR_BACK') - ->icon('icon-' . $arrow) - ->url($returnUrl); - $bar->appendButton($button); - - $button = (new LinkButton('user-mfa-edit-cancel')) - ->text('COM_USERS_MFA_BACKUPCODES_RESET') - ->buttonClass('btn btn-danger') - ->icon('icon-refresh') - ->url( - Route::_( - sprintf( - "index.php?option=com_users&task=method.regenerateBackupCodes&user_id=%s&%s=1&returnurl=%s", - $this->user->id, - Factory::getApplication()->getFormToken(), - base64_encode($returnUrl) - ) - ) - ); - $bar->appendButton($button); - } - - // Display the view - parent::display($tpl); - } + /** + * Is this an administrator page? + * + * @var boolean + * @since 4.2.0 + */ + public $isAdmin = false; + + /** + * The editor page render options + * + * @var array + * @since 4.2.0 + */ + public $renderOptions = []; + + /** + * The MFA Method record being edited + * + * @var object + * @since 4.2.0 + */ + public $record = null; + + /** + * The title text for this page + * + * @var string + * @since 4.2.0 + */ + public $title = ''; + + /** + * The return URL to use for all links and forms + * + * @var string + * @since 4.2.0 + */ + public $returnURL = null; + + /** + * The user object used to display this page + * + * @var User + * @since 4.2.0 + */ + public $user = null; + + /** + * The backup codes for the current user. Only applies when the backup codes record is being "edited" + * + * @var array + * @since 4.2.0 + */ + public $backupCodes = []; + + /** + * Am I editing an existing Method? If it's false then I'm adding a new Method. + * + * @var boolean + * @since 4.2.0 + */ + public $isEditExisting = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @throws \Exception + * @see \JViewLegacy::loadTemplate() + * @since 4.2.0 + */ + public function display($tpl = null): void + { + $app = Factory::getApplication(); + + if (empty($this->user)) { + $this->user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + /** @var MethodModel $model */ + $model = $this->getModel(); + $this->setLayout('edit'); + $this->renderOptions = $model->getRenderOptions($this->user); + $this->record = $model->getRecord($this->user); + $this->title = $model->getPageTitle(); + $this->isAdmin = $app->isClient('administrator'); + + // Backup codes are a special case, rendered with a special layout + if ($this->record->method == 'backupcodes') { + $this->setLayout('backupcodes'); + + $backupCodes = $this->record->options; + + if (!is_array($backupCodes)) { + $backupCodes = []; + } + + $backupCodes = array_filter( + $backupCodes, + function ($x) { + return !empty($x); + } + ); + + if (count($backupCodes) % 2 != 0) { + $backupCodes[] = ''; + } + + /** + * The call to array_merge resets the array indices. This is necessary since array_filter kept the indices, + * meaning our elements are completely out of order. + */ + $this->backupCodes = array_merge($backupCodes); + } + + // Set up the isEditExisting property. + $this->isEditExisting = !empty($this->record->id); + + // Back-end: always show a title in the 'title' module position, not in the page body + if ($this->isAdmin) { + ToolbarHelper::title($this->title, 'users user-lock'); + + $helpUrl = $this->renderOptions['help_url']; + + if (!empty($helpUrl)) { + ToolbarHelper::help('', false, $helpUrl); + } + + $this->title = ''; + } + + $returnUrl = empty($this->returnURL) ? '' : base64_decode($this->returnURL); + $returnUrl = $returnUrl ?: Route::_('index.php?option=com_users&task=methods.display&user_id=' . $this->user->id); + + if ($this->isAdmin && $this->getLayout() === 'edit') { + $bar = Toolbar::getInstance(); + $button = (new BasicButton('user-mfa-edit-save')) + ->text($this->renderOptions['submit_text']) + ->icon($this->renderOptions['submit_icon']) + ->onclick('document.getElementById(\'user-mfa-edit-save\').click()'); + + if ($this->renderOptions['show_submit'] || $this->isEditExisting) { + $bar->appendButton($button); + } + + $button = (new LinkButton('user-mfa-edit-cancel')) + ->text('JCANCEL') + ->buttonClass('btn btn-danger') + ->icon('icon-cancel-2') + ->url($returnUrl); + $bar->appendButton($button); + } elseif ($this->isAdmin && $this->getLayout() === 'backupcodes') { + $bar = Toolbar::getInstance(); + + $arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + $button = (new LinkButton('user-mfa-edit-cancel')) + ->text('JTOOLBAR_BACK') + ->icon('icon-' . $arrow) + ->url($returnUrl); + $bar->appendButton($button); + + $button = (new LinkButton('user-mfa-edit-cancel')) + ->text('COM_USERS_MFA_BACKUPCODES_RESET') + ->buttonClass('btn btn-danger') + ->icon('icon-refresh') + ->url( + Route::_( + sprintf( + "index.php?option=com_users&task=method.regenerateBackupCodes&user_id=%s&%s=1&returnurl=%s", + $this->user->id, + Factory::getApplication()->getFormToken(), + base64_encode($returnUrl) + ) + ) + ); + $bar->appendButton($button); + } + + // Display the view + parent::display($tpl); + } } diff --git a/administrator/components/com_users/src/View/Methods/HtmlView.php b/administrator/components/com_users/src/View/Methods/HtmlView.php index 6a2b19de241f0..e8156398acddd 100644 --- a/administrator/components/com_users/src/View/Methods/HtmlView.php +++ b/administrator/components/com_users/src/View/Methods/HtmlView.php @@ -1,4 +1,5 @@ setSiteTemplateStyle(); - - $app = Factory::getApplication(); - - if (empty($this->user)) - { - $this->user = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - /** @var MethodsModel $model */ - $model = $this->getModel(); - - if ($this->getLayout() !== 'firsttime') - { - $this->setLayout('default'); - } - - $this->methods = $model->getMethods($this->user); - $this->isAdmin = $app->isClient('administrator'); - $activeRecords = 0; - - foreach ($this->methods as $methodName => $method) - { - $methodActiveRecords = count($method['active']); - - if (!$methodActiveRecords) - { - continue; - } - - $activeRecords += $methodActiveRecords; - $this->mfaActive = true; - - foreach ($method['active'] as $record) - { - if ($record->default) - { - $this->defaultMethod = $methodName; - - break; - } - } - } - - // If there are no backup codes yet we should create new ones - /** @var BackupcodesModel $model */ - $model = $this->getModel('backupcodes'); - $backupCodes = $model->getBackupCodes($this->user); - - if ($activeRecords && empty($backupCodes)) - { - $model->regenerateBackupCodes($this->user); - } - - $backupCodesRecord = $model->getBackupCodesRecord($this->user); - - if (!is_null($backupCodesRecord)) - { - $this->methods = array_merge( - [ - 'backupcodes' => new MethodDescriptor( - [ - 'name' => 'backupcodes', - 'display' => Text::_('COM_USERS_USER_BACKUPCODES'), - 'shortinfo' => Text::_('COM_USERS_USER_BACKUPCODES_DESC'), - 'image' => 'media/com_users/images/emergency.svg', - 'canDisable' => false, - 'active' => [$backupCodesRecord], - ] - ) - ], - $this->methods - ); - } - - $this->isMandatoryMFASetup = $activeRecords === 0 && $app->getSession()->get('com_users.mandatory_mfa_setup', 0) === 1; - - // Back-end: always show a title in the 'title' module position, not in the page body - if ($this->isAdmin) - { - ToolbarHelper::title(Text::_('COM_USERS_MFA_LIST_PAGE_HEAD'), 'users user-lock'); - - if (Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_users')) - { - ToolbarHelper::back('JTOOLBAR_BACK', Route::_('index.php?option=com_users')); - } - } - - // Display the view - parent::display($tpl); - - $event = new NotifyActionLog('onComUsersViewMethodsAfterDisplay', [$this]); - Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); - - Text::script('JGLOBAL_CONFIRM_DELETE'); - } + use SiteTemplateTrait; + + /** + * Is this an administrator page? + * + * @var boolean + * @since 4.2.0 + */ + public $isAdmin = false; + + /** + * The MFA Methods available for this user + * + * @var array + * @since 4.2.0 + */ + public $methods = []; + + /** + * The return URL to use for all links and forms + * + * @var string + * @since 4.2.0 + */ + public $returnURL = null; + + /** + * Are there any active MFA Methods at all? + * + * @var boolean + * @since 4.2.0 + */ + public $mfaActive = false; + + /** + * Which Method has the default record? + * + * @var string + * @since 4.2.0 + */ + public $defaultMethod = ''; + + /** + * The user object used to display this page + * + * @var User + * @since 4.2.0 + */ + public $user = null; + + /** + * Is this page part of the mandatory Multi-factor Authentication setup? + * + * @var boolean + * @since 4.2.0 + */ + public $isMandatoryMFASetup = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @throws \Exception + * @see \JViewLegacy::loadTemplate() + * @since 4.2.0 + */ + public function display($tpl = null): void + { + $this->setSiteTemplateStyle(); + + $app = Factory::getApplication(); + + if (empty($this->user)) { + $this->user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + /** @var MethodsModel $model */ + $model = $this->getModel(); + + if ($this->getLayout() !== 'firsttime') { + $this->setLayout('default'); + } + + $this->methods = $model->getMethods($this->user); + $this->isAdmin = $app->isClient('administrator'); + $activeRecords = 0; + + foreach ($this->methods as $methodName => $method) { + $methodActiveRecords = count($method['active']); + + if (!$methodActiveRecords) { + continue; + } + + $activeRecords += $methodActiveRecords; + $this->mfaActive = true; + + foreach ($method['active'] as $record) { + if ($record->default) { + $this->defaultMethod = $methodName; + + break; + } + } + } + + // If there are no backup codes yet we should create new ones + /** @var BackupcodesModel $model */ + $model = $this->getModel('backupcodes'); + $backupCodes = $model->getBackupCodes($this->user); + + if ($activeRecords && empty($backupCodes)) { + $model->regenerateBackupCodes($this->user); + } + + $backupCodesRecord = $model->getBackupCodesRecord($this->user); + + if (!is_null($backupCodesRecord)) { + $this->methods = array_merge( + [ + 'backupcodes' => new MethodDescriptor( + [ + 'name' => 'backupcodes', + 'display' => Text::_('COM_USERS_USER_BACKUPCODES'), + 'shortinfo' => Text::_('COM_USERS_USER_BACKUPCODES_DESC'), + 'image' => 'media/com_users/images/emergency.svg', + 'canDisable' => false, + 'active' => [$backupCodesRecord], + ] + ) + ], + $this->methods + ); + } + + $this->isMandatoryMFASetup = $activeRecords === 0 && $app->getSession()->get('com_users.mandatory_mfa_setup', 0) === 1; + + // Back-end: always show a title in the 'title' module position, not in the page body + if ($this->isAdmin) { + ToolbarHelper::title(Text::_('COM_USERS_MFA_LIST_PAGE_HEAD'), 'users user-lock'); + + if (Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_users')) { + ToolbarHelper::back('JTOOLBAR_BACK', Route::_('index.php?option=com_users')); + } + } + + // Display the view + parent::display($tpl); + + $event = new NotifyActionLog('onComUsersViewMethodsAfterDisplay', [$this]); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + + Text::script('JGLOBAL_CONFIRM_DELETE'); + } } diff --git a/administrator/components/com_users/src/View/Note/HtmlView.php b/administrator/components/com_users/src/View/Note/HtmlView.php index 48ac938246924..e9d305e956f19 100644 --- a/administrator/components/com_users/src/View/Note/HtmlView.php +++ b/administrator/components/com_users/src/View/Note/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - parent::display($tpl); - $this->addToolbar(); - } - - /** - * Display the toolbar. - * - * @return void - * - * @since 2.5 - * @throws \Exception - */ - protected function addToolbar() - { - $input = Factory::getApplication()->input; - $input->set('hidemainmenu', 1); - - $user = $this->getCurrentUser(); - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); - - // Since we don't track these assets at the item level, use the category id. - $canDo = ContentHelper::getActions('com_users', 'category', $this->item->catid); - - ToolbarHelper::title(Text::_('COM_USERS_NOTES'), 'users user'); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if (!$checkedOut && ($canDo->get('core.edit') || count($user->getAuthorisedCategories('com_users', 'core.create')))) - { - ToolbarHelper::apply('note.apply'); - $toolbarButtons[] = ['save', 'note.save']; - } - - if (!$checkedOut && count($user->getAuthorisedCategories('com_users', 'core.create'))) - { - $toolbarButtons[] = ['save2new', 'note.save2new']; - } - - // If an existing item, can save to a copy. - if (!$isNew && (count($user->getAuthorisedCategories('com_users', 'core.create')) > 0)) - { - $toolbarButtons[] = ['save2copy', 'note.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('note.cancel'); - } - else - { - ToolbarHelper::cancel('note.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) - { - ToolbarHelper::versions('com_users.note', $this->item->id); - } - } - - ToolbarHelper::divider(); - ToolbarHelper::help('User_Notes:_New_or_Edit'); - } + /** + * The edit form. + * + * @var \Joomla\CMS\Form\Form + * + * @since 2.5 + */ + protected $form; + + /** + * The item data. + * + * @var object + * @since 2.5 + */ + protected $item; + + /** + * The model state. + * + * @var CMSObject + * @since 2.5 + */ + protected $state; + + /** + * Override the display method for the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 2.5 + * @throws \Exception + */ + public function display($tpl = null) + { + // Initialise view variables. + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + parent::display($tpl); + $this->addToolbar(); + } + + /** + * Display the toolbar. + * + * @return void + * + * @since 2.5 + * @throws \Exception + */ + protected function addToolbar() + { + $input = Factory::getApplication()->input; + $input->set('hidemainmenu', 1); + + $user = $this->getCurrentUser(); + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); + + // Since we don't track these assets at the item level, use the category id. + $canDo = ContentHelper::getActions('com_users', 'category', $this->item->catid); + + ToolbarHelper::title(Text::_('COM_USERS_NOTES'), 'users user'); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if (!$checkedOut && ($canDo->get('core.edit') || count($user->getAuthorisedCategories('com_users', 'core.create')))) { + ToolbarHelper::apply('note.apply'); + $toolbarButtons[] = ['save', 'note.save']; + } + + if (!$checkedOut && count($user->getAuthorisedCategories('com_users', 'core.create'))) { + $toolbarButtons[] = ['save2new', 'note.save2new']; + } + + // If an existing item, can save to a copy. + if (!$isNew && (count($user->getAuthorisedCategories('com_users', 'core.create')) > 0)) { + $toolbarButtons[] = ['save2copy', 'note.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('note.cancel'); + } else { + ToolbarHelper::cancel('note.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) { + ToolbarHelper::versions('com_users.note', $this->item->id); + } + } + + ToolbarHelper::divider(); + ToolbarHelper::help('User_Notes:_New_or_Edit'); + } } diff --git a/administrator/components/com_users/src/View/Notes/HtmlView.php b/administrator/components/com_users/src/View/Notes/HtmlView.php index a3c7c490c721d..48b44b857c4b7 100644 --- a/administrator/components/com_users/src/View/Notes/HtmlView.php +++ b/administrator/components/com_users/src/View/Notes/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->user = $this->get('User'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Turn parameters into registry objects - foreach ($this->items as $item) - { - $item->cparams = new Registry($item->category_params); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Display the toolbar. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_users', 'category', $this->state->get('filter.category_id')); - - ToolbarHelper::title(Text::_('COM_USERS_VIEW_NOTES_TITLE'), 'users user'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('note.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('notes.publish')->listCheck(true); - $childBar->unpublish('notes.unpublish')->listCheck(true); - $childBar->archive('notes.archive')->listCheck(true); - $childBar->checkin('notes.checkin')->listCheck(true); - } - - if ($this->state->get('filter.published') != -2 && $canDo->get('core.edit.state')) - { - $childBar->trash('notes.trash'); - } - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('notes.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_users'); - } - - $toolbar->help('User_Notes'); - } + /** + * A list of user note objects. + * + * @var array + * @since 2.5 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 2.5 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 2.5 + */ + protected $state; + + /** + * The model state. + * + * @var User + * @since 2.5 + */ + protected $user; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Override the display method for the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Initialise view variables. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->user = $this->get('User'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Turn parameters into registry objects + foreach ($this->items as $item) { + $item->cparams = new Registry($item->category_params); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Display the toolbar. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_users', 'category', $this->state->get('filter.category_id')); + + ToolbarHelper::title(Text::_('COM_USERS_VIEW_NOTES_TITLE'), 'users user'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('note.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('notes.publish')->listCheck(true); + $childBar->unpublish('notes.unpublish')->listCheck(true); + $childBar->archive('notes.archive')->listCheck(true); + $childBar->checkin('notes.checkin')->listCheck(true); + } + + if ($this->state->get('filter.published') != -2 && $canDo->get('core.edit.state')) { + $childBar->trash('notes.trash'); + } + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('notes.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_users'); + } + + $toolbar->help('User_Notes'); + } } diff --git a/administrator/components/com_users/src/View/SiteTemplateTrait.php b/administrator/components/com_users/src/View/SiteTemplateTrait.php index 0683a6ec9fede..cd915b2424438 100644 --- a/administrator/components/com_users/src/View/SiteTemplateTrait.php +++ b/administrator/components/com_users/src/View/SiteTemplateTrait.php @@ -1,4 +1,5 @@ get('captive_template', ''); - - if (empty($templateStyle) || !$app->isClient('site')) - { - return; - } + /** + * Set a specific site template style in the frontend application + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + private function setSiteTemplateStyle(): void + { + $app = Factory::getApplication(); + $templateStyle = (int) ComponentHelper::getParams('com_users')->get('captive_template', ''); - $itemId = $app->input->get('Itemid'); + if (empty($templateStyle) || !$app->isClient('site')) { + return; + } - if (!empty($itemId)) - { - return; - } + $itemId = $app->input->get('Itemid'); - $app->input->set('templateStyle', $templateStyle); + if (!empty($itemId)) { + return; + } - try - { - $refApp = new ReflectionObject($app); - $refTemplate = $refApp->getProperty('template'); - $refTemplate->setAccessible(true); - $refTemplate->setValue($app, null); - } - catch (ReflectionException $e) - { - return; - } + $app->input->set('templateStyle', $templateStyle); - $template = $app->getTemplate(true); + try { + $refApp = new ReflectionObject($app); + $refTemplate = $refApp->getProperty('template'); + $refTemplate->setAccessible(true); + $refTemplate->setValue($app, null); + } catch (ReflectionException $e) { + return; + } - $app->set('theme', $template->template); - $app->set('themeParams', $template->params); - } + $template = $app->getTemplate(true); + $app->set('theme', $template->template); + $app->set('themeParams', $template->params); + } } diff --git a/administrator/components/com_users/src/View/User/HtmlView.php b/administrator/components/com_users/src/View/User/HtmlView.php index 38bc006953d2c..08a3564264d74 100644 --- a/administrator/components/com_users/src/View/User/HtmlView.php +++ b/administrator/components/com_users/src/View/User/HtmlView.php @@ -1,4 +1,5 @@ item = $this->get('Item')) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_NOT_EXIST'), 'error'); - $app->redirect('index.php?option=com_users&view=users'); - } - - $this->form = $this->get('Form'); - $this->state = $this->get('State'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Prevent user from modifying own group(s) - $user = Factory::getApplication()->getIdentity(); - - if ((int) $user->id != (int) $this->item->id || $user->authorise('core.admin')) - { - $this->grouplist = $this->get('Groups'); - $this->groups = $this->get('AssignedGroups'); - } - - $this->form->setValue('password', null); - $this->form->setValue('password2', null); - - /** @var User $userBeingEdited */ - $userBeingEdited = Factory::getContainer() - ->get(UserFactoryInterface::class) - ->loadUserById($this->item->id); - - if ($this->item->id > 0 && (int) $userBeingEdited->id == (int) $this->item->id) - { - try - { - $this->mfaConfigurationUI = Mfa::canShowConfigurationInterface($userBeingEdited) - ? Mfa::getConfigurationInterface($userBeingEdited) - : ''; - } - catch (\Exception $e) - { - // In case something goes really wrong with the plugins; prevents hard breaks. - $this->mfaConfigurationUI = null; - } - } - - parent::display($tpl); - - $this->addToolbar(); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = Factory::getApplication()->getIdentity(); - $canDo = ContentHelper::getActions('com_users'); - $isNew = ($this->item->id == 0); - $isProfile = $this->item->id == $user->id; - - ToolbarHelper::title( - Text::_( - $isNew ? 'COM_USERS_VIEW_NEW_USER_TITLE' : ($isProfile ? 'COM_USERS_VIEW_EDIT_PROFILE_TITLE' : 'COM_USERS_VIEW_EDIT_USER_TITLE') - ), - 'user ' . ($isNew ? 'user-add' : ($isProfile ? 'user-profile' : 'user-edit')) - ); - - $toolbarButtons = []; - - if ($canDo->get('core.edit') || $canDo->get('core.create') || $isProfile) - { - ToolbarHelper::apply('user.apply'); - $toolbarButtons[] = ['save', 'user.save']; - } - - if ($canDo->get('core.create') && $canDo->get('core.manage')) - { - $toolbarButtons[] = ['save2new', 'user.save2new']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('user.cancel'); - } - else - { - ToolbarHelper::cancel('user.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Users:_Edit_Profile'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * Gets the available groups + * + * @var array + */ + protected $grouplist; + + /** + * The groups this user is assigned to + * + * @var array + * @since 1.6 + */ + protected $groups; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The Multi-factor Authentication configuration interface for the user. + * + * @var string|null + * @since 4.2.0 + */ + protected $mfaConfigurationUI; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.5 + */ + public function display($tpl = null) + { + // If no item found, dont show the edit screen, redirect with message + if (false === $this->item = $this->get('Item')) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_NOT_EXIST'), 'error'); + $app->redirect('index.php?option=com_users&view=users'); + } + + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Prevent user from modifying own group(s) + $user = Factory::getApplication()->getIdentity(); + + if ((int) $user->id != (int) $this->item->id || $user->authorise('core.admin')) { + $this->grouplist = $this->get('Groups'); + $this->groups = $this->get('AssignedGroups'); + } + + $this->form->setValue('password', null); + $this->form->setValue('password2', null); + + /** @var User $userBeingEdited */ + $userBeingEdited = Factory::getContainer() + ->get(UserFactoryInterface::class) + ->loadUserById($this->item->id); + + if ($this->item->id > 0 && (int) $userBeingEdited->id == (int) $this->item->id) { + try { + $this->mfaConfigurationUI = Mfa::canShowConfigurationInterface($userBeingEdited) + ? Mfa::getConfigurationInterface($userBeingEdited) + : ''; + } catch (\Exception $e) { + // In case something goes really wrong with the plugins; prevents hard breaks. + $this->mfaConfigurationUI = null; + } + } + + parent::display($tpl); + + $this->addToolbar(); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = Factory::getApplication()->getIdentity(); + $canDo = ContentHelper::getActions('com_users'); + $isNew = ($this->item->id == 0); + $isProfile = $this->item->id == $user->id; + + ToolbarHelper::title( + Text::_( + $isNew ? 'COM_USERS_VIEW_NEW_USER_TITLE' : ($isProfile ? 'COM_USERS_VIEW_EDIT_PROFILE_TITLE' : 'COM_USERS_VIEW_EDIT_USER_TITLE') + ), + 'user ' . ($isNew ? 'user-add' : ($isProfile ? 'user-profile' : 'user-edit')) + ); + + $toolbarButtons = []; + + if ($canDo->get('core.edit') || $canDo->get('core.create') || $isProfile) { + ToolbarHelper::apply('user.apply'); + $toolbarButtons[] = ['save', 'user.save']; + } + + if ($canDo->get('core.create') && $canDo->get('core.manage')) { + $toolbarButtons[] = ['save2new', 'user.save2new']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('user.cancel'); + } else { + ToolbarHelper::cancel('user.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Users:_Edit_Profile'); + } } diff --git a/administrator/components/com_users/src/View/Users/HtmlView.php b/administrator/components/com_users/src/View/Users/HtmlView.php index ee56bdc8a47c4..7f8742f3a172f 100644 --- a/administrator/components/com_users/src/View/Users/HtmlView.php +++ b/administrator/components/com_users/src/View/Users/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->canDo = ContentHelper::getActions('com_users'); - $this->db = Factory::getDbo(); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = $this->canDo; - $user = $this->getCurrentUser(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_USERS_VIEW_USERS_TITLE'), 'users user'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('user.add'); - } - - if ($canDo->get('core.edit.state') || $canDo->get('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('users.activate', 'COM_USERS_TOOLBAR_ACTIVATE', true); - $childBar->unpublish('users.block', 'COM_USERS_TOOLBAR_BLOCK', true); - $childBar->standardButton('unblock') - ->text('COM_USERS_TOOLBAR_UNBLOCK') - ->task('users.unblock') - ->listCheck(true); - - // Add a batch button - if ($user->authorise('core.create', 'com_users') - && $user->authorise('core.edit', 'com_users') - && $user->authorise('core.edit.state', 'com_users')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - - if ($canDo->get('core.delete')) - { - $childBar->delete('users.delete') - ->text('JTOOLBAR_DELETE') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_users'); - } - - $toolbar->help('Users'); - } + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * A Form instance with filter fields. + * + * @var \Joomla\CMS\Form\Form + * + * @since 3.6.3 + */ + public $filterForm; + + /** + * An array with active filters. + * + * @var array + * @since 3.6.3 + */ + public $activeFilters; + + /** + * An ACL object to verify user rights. + * + * @var CMSObject + * @since 3.6.3 + */ + protected $canDo; + + /** + * An instance of DatabaseDriver. + * + * @var DatabaseDriver + * @since 3.6.3 + * + * @deprecated 5.0 Will be removed without replacement + */ + protected $db; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->canDo = ContentHelper::getActions('com_users'); + $this->db = Factory::getDbo(); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = $this->canDo; + $user = $this->getCurrentUser(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_USERS_VIEW_USERS_TITLE'), 'users user'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('user.add'); + } + + if ($canDo->get('core.edit.state') || $canDo->get('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('users.activate', 'COM_USERS_TOOLBAR_ACTIVATE', true); + $childBar->unpublish('users.block', 'COM_USERS_TOOLBAR_BLOCK', true); + $childBar->standardButton('unblock') + ->text('COM_USERS_TOOLBAR_UNBLOCK') + ->task('users.unblock') + ->listCheck(true); + + // Add a batch button + if ( + $user->authorise('core.create', 'com_users') + && $user->authorise('core.edit', 'com_users') + && $user->authorise('core.edit.state', 'com_users') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + + if ($canDo->get('core.delete')) { + $childBar->delete('users.delete') + ->text('JTOOLBAR_DELETE') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_users'); + } + + $toolbar->help('Users'); + } } diff --git a/administrator/components/com_users/tmpl/debuggroup/default.php b/administrator/components/com_users/tmpl/debuggroup/default.php index ac658cdd6c8e4..b76b09280a550 100644 --- a/administrator/components/com_users/tmpl/debuggroup/default.php +++ b/administrator/components/com_users/tmpl/debuggroup/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
    -
    - $this)); ?> -
    - - - - - - - actions as $key => $action) : ?> - - - - - - - - items as $i => $item) : ?> - - - - actions as $action) : ?> - checks[$name]; - if ($check === true) : - $class = 'text-success icon-check'; - $button = 'btn-success'; - $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); - elseif ($check === false) : - $class = 'text-danger icon-times'; - $button = 'btn-danger'; - $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); - elseif ($check === null) : - $class = 'text-danger icon-minus-circle'; - $button = 'btn-warning'; - $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); - else : - $class = ''; - $button = ''; - $text = ''; - endif; - ?> - - - - - - - -
    - , - , - -
    - - - - - - - - - -
    - escape(Text::_($item->title)); ?> - - $item->level + 1)) . $this->escape($item->name); ?> - - - - - lft; ?> - - rgt; ?> - - id; ?> -
    -
    -    -    -   -
    +
    + $this)); ?> +
    + + + + + + + actions as $key => $action) : ?> + + + + + + + + items as $i => $item) : ?> + + + + actions as $action) : ?> + checks[$name]; + if ($check === true) : + $class = 'text-success icon-check'; + $button = 'btn-success'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); + elseif ($check === false) : + $class = 'text-danger icon-times'; + $button = 'btn-danger'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); + elseif ($check === null) : + $class = 'text-danger icon-minus-circle'; + $button = 'btn-warning'; + $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); + else : + $class = ''; + $button = ''; + $text = ''; + endif; + ?> + + + + + + + +
    + , + , + +
    + + + + + + + + + +
    + escape(Text::_($item->title)); ?> + + $item->level + 1)) . $this->escape($item->name); ?> + + + + + lft; ?> + - rgt; ?> + + id; ?> +
    +
    +    +    +   +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> -
    - - - -
    +
    + + + +
    diff --git a/administrator/components/com_users/tmpl/debuguser/default.php b/administrator/components/com_users/tmpl/debuguser/default.php index 75b108ef8b93b..159af56b5cbd4 100644 --- a/administrator/components/com_users/tmpl/debuguser/default.php +++ b/administrator/components/com_users/tmpl/debuguser/default.php @@ -1,4 +1,5 @@ actions as $action) : - $name = $action[0]; - if (in_array($name, ['core.login.site', 'core.login.admin', 'core.login.offline', 'core.login.api', 'core.admin'])) : - $loginActions[] = $action; - else : - $actions[] = $action; - endif; + $name = $action[0]; + if (in_array($name, ['core.login.site', 'core.login.admin', 'core.login.offline', 'core.login.api', 'core.admin'])) : + $loginActions[] = $action; + else : + $actions[] = $action; + endif; endforeach; ?>
    -
    - $this)); ?> -
    - items[0]->checks[$name]; - if ($check === true) : - $class = 'text-success icon-check'; - $button = 'btn-success'; - $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); - elseif ($check === false) : - $class = 'text-danger icon-times'; - $button = 'btn-danger'; - $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); - elseif ($check === null) : - $class = 'text-danger icon-minus-circle'; - $button = 'btn-warning'; - $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); - else : - $class = ''; - $button = ''; - $text = ''; - endif; - ?> -
    - - - -
    - -
    +
    + $this)); ?> +
    + items[0]->checks[$name]; + if ($check === true) : + $class = 'text-success icon-check'; + $button = 'btn-success'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); + elseif ($check === false) : + $class = 'text-danger icon-times'; + $button = 'btn-danger'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); + elseif ($check === null) : + $class = 'text-danger icon-minus-circle'; + $button = 'btn-warning'; + $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); + else : + $class = ''; + $button = ''; + $text = ''; + endif; + ?> +
    + + + +
    + +
    - - - - - - - $action) : ?> - - - - - - - - items as $i => $item) :?> - - - - - checks[$name]; - if ($check === true) : - $class = 'text-success icon-check'; - $button = 'btn-success'; - $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); - elseif ($check === false) : - $class = 'text-danger icon-times'; - $button = 'btn-danger'; - $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); - elseif ($check === null) : - $class = 'text-danger icon-minus-circle'; - $button = 'btn-warning'; - $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); - else : - $class = ''; - $button = ''; - $text = ''; - endif; - ?> - - - - - - - -
    - , - , - -
    - - - - - - - - - -
    - escape(Text::_($item->title)); ?> - - $item->level + 1)) . $this->escape($item->name); ?> - - - - - lft; ?> - - rgt; ?> - - id; ?> -
    + + + + + + + $action) : ?> + + + + + + + + items as $i => $item) :?> + + + + + checks[$name]; + if ($check === true) : + $class = 'text-success icon-check'; + $button = 'btn-success'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); + elseif ($check === false) : + $class = 'text-danger icon-times'; + $button = 'btn-danger'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); + elseif ($check === null) : + $class = 'text-danger icon-minus-circle'; + $button = 'btn-warning'; + $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); + else : + $class = ''; + $button = ''; + $text = ''; + endif; + ?> + + + + + + + +
    + , + , + +
    + + + + + + + + + +
    + escape(Text::_($item->title)); ?> + + $item->level + 1)) . $this->escape($item->name); ?> + + + + + lft; ?> + - rgt; ?> + + id; ?> +
    -
    -    -    - -
    +
    +    +    + +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - -
    + + + +
    diff --git a/administrator/components/com_users/tmpl/group/edit.php b/administrator/components/com_users/tmpl/group/edit.php index 2bcbb242f6092..d4c95a5542b23 100644 --- a/administrator/components/com_users/tmpl/group/edit.php +++ b/administrator/components/com_users/tmpl/group/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $this->useCoreUI = true; ?>
    - 'details', 'recall' => true, 'breakpoint' => 768]); ?> - -
    - form->renderField('title'); ?> - form->renderField('parent_id'); ?> -
    - - ignore_fieldsets = array('group_details'); ?> - - + 'details', 'recall' => true, 'breakpoint' => 768]); ?> + +
    + form->renderField('title'); ?> + form->renderField('parent_id'); ?> +
    + + ignore_fieldsets = array('group_details'); ?> + + - - + +
    diff --git a/administrator/components/com_users/tmpl/groups/default.php b/administrator/components/com_users/tmpl/groups/default.php index def943cd56c4b..36d4d22e6e9cc 100644 --- a/administrator/components/com_users/tmpl/groups/default.php +++ b/administrator/components/com_users/tmpl/groups/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_users.admin-users-groups') - ->useScript('multiselect') - ->useScript('table.columns'); + ->useScript('multiselect') + ->useScript('table.columns'); ?>
    -
    -
    -
    - $this, 'options' => array('filterButton' => false))); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - items as $i => $item) : - $canCreate = $user->authorise('core.create', 'com_users'); - $canEdit = $user->authorise('core.edit', 'com_users'); +
    +
    +
    + $this, 'options' => array('filterButton' => false))); ?> + items)) : ?> +
    + + +
    + +
    - , - , - -
    - - - - - - - - - - - - - -
    + + + + + + + + + + + + + items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_users'); + $canEdit = $user->authorise('core.edit', 'com_users'); - // If this group is super admin and this user is not super admin, $canEdit is false - if (!$user->authorise('core.admin') && Access::checkGroup($item->id, 'core.admin')) - { - $canEdit = false; - } - $canChange = $user->authorise('core.edit.state', 'com_users'); - ?> - - - - - - - - - - -
    + , + , + +
    + + + + + + + + + + + + + +
    - - id, false, 'cid', 'cb', $item->title); ?> - - - $item->level + 1)); ?> - - - escape($item->title); ?> - - escape($item->title); ?> - - - - - - - - - count_enabled; ?> - - - - - count_disabled; ?> - - - - id; ?> -
    + // If this group is super admin and this user is not super admin, $canEdit is false + if (!$user->authorise('core.admin') && Access::checkGroup($item->id, 'core.admin')) { + $canEdit = false; + } + $canChange = $user->authorise('core.edit.state', 'com_users'); + ?> + + + + id, false, 'cid', 'cb', $item->title); ?> + + + + $item->level + 1)); ?> + + + escape($item->title); ?> + + escape($item->title); ?> + + + + + + + + + + + count_enabled; ?> + + + + + + count_disabled; ?> + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
    -
    -
    + + + + + +
    diff --git a/administrator/components/com_users/tmpl/level/edit.php b/administrator/components/com_users/tmpl/level/edit.php index 389362f311ef2..e4985cc6a9043 100644 --- a/administrator/components/com_users/tmpl/level/edit.php +++ b/administrator/components/com_users/tmpl/level/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
    - 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - -
    - -
    -
    - form->getLabel('title'); ?> -
    -
    - form->getInput('title'); ?> -
    -
    -
    - - - -
    - -
    - item->rules, true); ?> -
    -
    - - - - - - + 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + +
    + +
    +
    + form->getLabel('title'); ?> +
    +
    + form->getInput('title'); ?> +
    +
    +
    + + + +
    + +
    + item->rules, true); ?> +
    +
    + + + + + +
    diff --git a/administrator/components/com_users/tmpl/levels/default.php b/administrator/components/com_users/tmpl/levels/default.php index cefbc0692e3b2..83e260fb91351 100644 --- a/administrator/components/com_users/tmpl/levels/default.php +++ b/administrator/components/com_users/tmpl/levels/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'a.ordering'; -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_users&task=levels.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_users&task=levels.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
    -
    -
    -
    - $this, 'options' => array('filterButton' => false))); ?> +
    +
    +
    + $this, 'options' => array('filterButton' => false))); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction=""> - items); ?> - items as $i => $item) : - $ordering = ($listOrder == 'a.ordering'); - $canCreate = $user->authorise('core.create', 'com_users'); - $canEdit = $user->authorise('core.edit', 'com_users'); - $canChange = $user->authorise('core.edit.state', 'com_users'); + items)) : ?> +
    + + +
    + +
    - , - , - -
    - - - - - - - - - -
    + + + + + + + + + + + class="js-draggable" data-url="" data-direction=""> + items); ?> + items as $i => $item) : + $ordering = ($listOrder == 'a.ordering'); + $canCreate = $user->authorise('core.create', 'com_users'); + $canEdit = $user->authorise('core.edit', 'com_users'); + $canChange = $user->authorise('core.edit.state', 'com_users'); - // Decode level groups - $groups = json_decode($item->rules); + // Decode level groups + $groups = json_decode($item->rules); - // If this group is super admin and this user is not super admin, $canEdit is false - if (!Factory::getUser()->authorise('core.admin') && $groups && Access::checkGroup($groups[0], 'core.admin')) - { - $canEdit = false; - $canChange = false; - } - ?> - - - - - - - - - -
    + , + , + +
    + + + + + + + + + +
    - - id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - - - - escape($item->title); ?> - - escape($item->title); ?> - - - rules); ?> - - id; ?> -
    + // If this group is super admin and this user is not super admin, $canEdit is false + if (!Factory::getUser()->authorise('core.admin') && $groups && Access::checkGroup($groups[0], 'core.admin')) { + $canEdit = false; + $canChange = false; + } + ?> + + + + id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + + + + + escape($item->title); ?> + + escape($item->title); ?> + + + + rules); ?> + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
    -
    -
    + + + + +
    +
    +
    diff --git a/administrator/components/com_users/tmpl/mail/default.php b/administrator/components/com_users/tmpl/mail/default.php index 6a5eae8916997..209d5700f76db 100644 --- a/administrator/components/com_users/tmpl/mail/default.php +++ b/administrator/components/com_users/tmpl/mail/default.php @@ -1,4 +1,5 @@
    -
    -
    -
    -
    - form->getLabel('subject'); ?> - - get('mailSubjectPrefix'))) : ?> - get('mailSubjectPrefix'); ?> - - form->getInput('subject'); ?> - -
    -
    - form->getLabel('message'); ?> - form->getInput('message'); ?> - get('mailBodySuffix'))) : ?> -
    -
    - get('mailBodySuffix'); ?> -
    -
    - -
    -
    - - -
    -
    -
    - form->getInput('recurse'); ?> - form->getLabel('recurse'); ?> -
    -
    - form->getInput('mode'); ?> - form->getLabel('mode'); ?> -
    -
    - form->getInput('disabled'); ?> - form->getLabel('disabled'); ?> -
    -
    - form->getInput('bcc'); ?> - form->getLabel('bcc'); ?> -
    -
    - form->getLabel('group'); ?> - form->getInput('group'); ?> -
    -
    -
    +
    +
    +
    +
    + form->getLabel('subject'); ?> + + get('mailSubjectPrefix'))) : ?> + get('mailSubjectPrefix'); ?> + + form->getInput('subject'); ?> + +
    +
    + form->getLabel('message'); ?> + form->getInput('message'); ?> + get('mailBodySuffix'))) : ?> +
    +
    + get('mailBodySuffix'); ?> +
    +
    + +
    +
    + + +
    +
    +
    + form->getInput('recurse'); ?> + form->getLabel('recurse'); ?> +
    +
    + form->getInput('mode'); ?> + form->getLabel('mode'); ?> +
    +
    + form->getInput('disabled'); ?> + form->getLabel('disabled'); ?> +
    +
    + form->getInput('bcc'); ?> + form->getLabel('bcc'); ?> +
    +
    + form->getLabel('group'); ?> + form->getInput('group'); ?> +
    +
    +
    diff --git a/administrator/components/com_users/tmpl/note/edit.php b/administrator/components/com_users/tmpl/note/edit.php index ba80a12a1f4d8..8fd2b6d215227 100644 --- a/administrator/components/com_users/tmpl/note/edit.php +++ b/administrator/components/com_users/tmpl/note/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
    -
    -
    -
    -
    -
    - form->renderField('subject'); ?> - form->renderField('user_id'); ?> - form->renderField('catid'); ?> - form->renderField('state'); ?> - form->renderField('review_time'); ?> - form->renderField('version_note'); ?> +
    +
    +
    +
    +
    + form->renderField('subject'); ?> + form->renderField('user_id'); ?> + form->renderField('catid'); ?> + form->renderField('state'); ?> + form->renderField('review_time'); ?> + form->renderField('version_note'); ?> - - -
    -
    - form->renderField('body'); ?> -
    -
    -
    -
    -
    + + +
    +
    + form->renderField('body'); ?> +
    +
    +
    +
    +
    diff --git a/administrator/components/com_users/tmpl/notes/default.php b/administrator/components/com_users/tmpl/notes/default.php index 07586c547a7d5..b158e7cf1374f 100644 --- a/administrator/components/com_users/tmpl/notes/default.php +++ b/administrator/components/com_users/tmpl/notes/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -26,103 +27,103 @@ ?>
    -
    -
    -
    - $this)); ?> +
    +
    +
    + $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - items as $i => $item) : - $canEdit = $user->authorise('core.edit', 'com_users.category.' . $item->catid); - $canCheckin = $user->authorise('core.admin', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_users.category.' . $item->catid) && $canCheckin; - $subject = $item->subject ?: Text::_('COM_USERS_EMPTY_SUBJECT'); - ?> - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - -
    - id, false, 'cid', 'cb', $subject); ?> - - state, $i, 'notes.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'notes.', $canCheckin); ?> - - subject ?: Text::_('COM_USERS_EMPTY_SUBJECT'); ?> - - - escape($subject); ?> - - escape($subject); ?> - -
    - escape($item->category_title); ?> -
    -
    - escape($item->user_name); ?> - - review_time !== null) : ?> - review_time, Text::_('DATE_FORMAT_LC4')); ?> - - - - - id; ?> -
    + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + items as $i => $item) : + $canEdit = $user->authorise('core.edit', 'com_users.category.' . $item->catid); + $canCheckin = $user->authorise('core.admin', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_users.category.' . $item->catid) && $canCheckin; + $subject = $item->subject ?: Text::_('COM_USERS_EMPTY_SUBJECT'); + ?> + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + +
    + id, false, 'cid', 'cb', $subject); ?> + + state, $i, 'notes.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'notes.', $canCheckin); ?> + + subject ?: Text::_('COM_USERS_EMPTY_SUBJECT'); ?> + + + escape($subject); ?> + + escape($subject); ?> + +
    + escape($item->category_title); ?> +
    +
    + escape($item->user_name); ?> + + review_time !== null) : ?> + review_time, Text::_('DATE_FORMAT_LC4')); ?> + + + + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + -
    - - - -
    -
    -
    -
    +
    + + + +
    +
    +
    +
    diff --git a/administrator/components/com_users/tmpl/notes/emptystate.php b/administrator/components/com_users/tmpl/notes/emptystate.php index 2366086b4218b..0b963fa82243e 100644 --- a/administrator/components/com_users/tmpl/notes/emptystate.php +++ b/administrator/components/com_users/tmpl/notes/emptystate.php @@ -1,4 +1,5 @@ 'COM_USERS_NOTES', - 'formURL' => 'index.php?option=com_users&view=notes', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:User_Notes', - 'icon' => 'icon-users user', + 'textPrefix' => 'COM_USERS_NOTES', + 'formURL' => 'index.php?option=com_users&view=notes', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:User_Notes', + 'icon' => 'icon-users user', ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_users')) -{ - $displayData['createURL'] = 'index.php?option=com_users&task=note.add'; +if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_users')) { + $displayData['createURL'] = 'index.php?option=com_users&task=note.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_users/tmpl/notes/modal.php b/administrator/components/com_users/tmpl/notes/modal.php index deea2d14628d2..19b781d81d4da 100644 --- a/administrator/components/com_users/tmpl/notes/modal.php +++ b/administrator/components/com_users/tmpl/notes/modal.php @@ -1,4 +1,5 @@
    -

    user->name, $this->user->id); ?>

    +

    user->name, $this->user->id); ?>

    items)) : ?> - + -
      - items as $item) : ?> -
    • -
      - subject) : ?> -

      id, $this->escape($item->subject)); ?>

      - -

      id, Text::_('COM_USERS_EMPTY_SUBJECT')); ?>

      - -
      - -
      - created_time, Text::_('DATE_FORMAT_LC2')); ?> -
      - - cparams->get('image'); ?> - - catid && isset($category_image)) : ?> -
      - -
      - -
      - escape($item->category_title); ?> -
      - - -
      -
      - body) ? HTMLHelper::_('content.prepare', $item->body) : ''); ?> -
      -
    • - -
    +
      + items as $item) : ?> +
    • +
      + subject) : ?> +

      id, $this->escape($item->subject)); ?>

      + +

      id, Text::_('COM_USERS_EMPTY_SUBJECT')); ?>

      + +
      + +
      + created_time, Text::_('DATE_FORMAT_LC2')); ?> +
      + + cparams->get('image'); ?> + + catid && isset($category_image)) : ?> +
      + +
      + +
      + escape($item->category_title); ?> +
      + + +
      +
      + body) ? HTMLHelper::_('content.prepare', $item->body) : ''); ?> +
      +
    • + +
    diff --git a/administrator/components/com_users/tmpl/user/edit.php b/administrator/components/com_users/tmpl/user/edit.php index 1df46ed313115..d408bbb6906bf 100644 --- a/administrator/components/com_users/tmpl/user/edit.php +++ b/administrator/components/com_users/tmpl/user/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $input = Factory::getApplication()->input; @@ -33,50 +34,50 @@ ?>
    -

    form->getValue('name', null, Text::_('COM_USERS_USER_NEW_USER_TITLE')); ?>

    - -
    - 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - -
    - -
    - form->renderFieldset('user_details'); ?> -
    -
    - - - - grouplist) : ?> - -
    - -
    - loadTemplate('groups'); ?> -
    -
    - - - - ignore_fieldsets = array('user_details'); - echo LayoutHelper::render('joomla.edit.params', $this); - ?> - - mfaConfigurationUI)) : ?> - -
    - - mfaConfigurationUI ?> -
    - - - - -
    - - - - +

    form->getValue('name', null, Text::_('COM_USERS_USER_NEW_USER_TITLE')); ?>

    + +
    + 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + +
    + +
    + form->renderFieldset('user_details'); ?> +
    +
    + + + + grouplist) : ?> + +
    + +
    + loadTemplate('groups'); ?> +
    +
    + + + + ignore_fieldsets = array('user_details'); + echo LayoutHelper::render('joomla.edit.params', $this); + ?> + + mfaConfigurationUI)) : ?> + +
    + + mfaConfigurationUI ?> +
    + + + + +
    + + + +
    diff --git a/administrator/components/com_users/tmpl/user/edit_groups.php b/administrator/components/com_users/tmpl/user/edit_groups.php index d8d97b88cd771..fa5d093f04713 100644 --- a/administrator/components/com_users/tmpl/user/edit_groups.php +++ b/administrator/components/com_users/tmpl/user/edit_groups.php @@ -1,4 +1,5 @@ -groups, true); ?> +groups, true); diff --git a/administrator/components/com_users/tmpl/users/default_batch_body.php b/administrator/components/com_users/tmpl/users/default_batch_body.php index b74fd0e867b5b..1c4b4b3ccc19e 100644 --- a/administrator/components/com_users/tmpl/users/default_batch_body.php +++ b/administrator/components/com_users/tmpl/users/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\HTML\HTMLHelper; @@ -13,16 +15,16 @@ // Create the copy/move options. $options = array( - HTMLHelper::_('select.option', 'add', Text::_('COM_USERS_BATCH_ADD')), - HTMLHelper::_('select.option', 'del', Text::_('COM_USERS_BATCH_DELETE')), - HTMLHelper::_('select.option', 'set', Text::_('COM_USERS_BATCH_SET')) + HTMLHelper::_('select.option', 'add', Text::_('COM_USERS_BATCH_ADD')), + HTMLHelper::_('select.option', 'del', Text::_('COM_USERS_BATCH_DELETE')), + HTMLHelper::_('select.option', 'set', Text::_('COM_USERS_BATCH_SET')) ); // Create the reset password options. $resetOptions = array( - HTMLHelper::_('select.option', '', Text::_('COM_USERS_NO_ACTION')), - HTMLHelper::_('select.option', 'yes', Text::_('JYES')), - HTMLHelper::_('select.option', 'no', Text::_('JNO')) + HTMLHelper::_('select.option', '', Text::_('COM_USERS_NO_ACTION')), + HTMLHelper::_('select.option', 'yes', Text::_('JYES')), + HTMLHelper::_('select.option', 'no', Text::_('JNO')) ); /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ @@ -32,33 +34,33 @@ ?>
    -
    -
    - -
    - -
    -
    -
    -
    - - - - -
    -
    -
    -
    - - - - -
    -
    -
    +
    +
    + +
    + +
    +
    +
    +
    + + + + +
    +
    +
    +
    + + + + +
    +
    +
    diff --git a/administrator/components/com_users/tmpl/users/default_batch_footer.php b/administrator/components/com_users/tmpl/users/default_batch_footer.php index 7f35695fde22f..e8db055384e61 100644 --- a/administrator/components/com_users/tmpl/users/default_batch_footer.php +++ b/administrator/components/com_users/tmpl/users/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/administrator/components/com_users/tmpl/users/modal.php b/administrator/components/com_users/tmpl/users/modal.php index c1dae97df77ef..787156ab8c865 100644 --- a/administrator/components/com_users/tmpl/users/modal.php +++ b/administrator/components/com_users/tmpl/users/modal.php @@ -1,4 +1,5 @@
    -
    - -
    -   -
    - - $this)); ?> - items)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - items as $item) : ?> - - - - - - - - - - -
    - , - , - -
    - - - - - - - - - - - -
    - - escape($item->name); ?> - - - escape($item->username); ?> - - - - - - - - - - group_names, false); ?> - - id; ?> -
    + + +
    +   +
    + + $this)); ?> + items)) : ?> +
    + + +
    + + + + + + + + + + + + + + + + items as $item) : ?> + + + + + + + + + + +
    + , + , + +
    + + + + + + + + + + + +
    + + escape($item->name); ?> + + + escape($item->username); ?> + + + + + + + + + + group_names, false); ?> + + id; ?> +
    - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - - - -
    + + + + + + +
    diff --git a/administrator/components/com_workflow/services/provider.php b/administrator/components/com_workflow/services/provider.php index dbaf8e74cc1b8..81471b5803650 100644 --- a/administrator/components/com_workflow/services/provider.php +++ b/administrator/components/com_workflow/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Workflow')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Workflow')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Workflow')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Workflow')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_workflow/src/Controller/DisplayController.php b/administrator/components/com_workflow/src/Controller/DisplayController.php index 7be435b39e32e..275d9af7584c7 100644 --- a/administrator/components/com_workflow/src/Controller/DisplayController.php +++ b/administrator/components/com_workflow/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - } - - /** - * Method to display a view. - * - * @param boolean $cachable If true, the view output will be cached - * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. - * - * @return BaseController|boolean This object to support chaining. - * - * @since 1.5 - */ - public function display($cachable = false, $urlparams = array()) - { - $view = $this->input->get('view'); - $layout = $this->input->get('layout'); - $id = $this->input->getInt('id'); - - // Check for edit form. - if (in_array($view, ['workflow', 'stage', 'transition']) && $layout == 'edit' && !$this->checkEditId('com_workflow.edit.' . $view, $id)) - { - // 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', $id), 'error'); - } - - $url = 'index.php?option=com_workflow&view=' . Inflector::pluralize($view) . '&extension=' . $this->input->getCmd('extension'); - - $this->setRedirect(Route::_($url, false)); - - return false; - } - - return parent::display(); - } + /** + * The default view. + * + * @var string + * @since 4.0.0 + */ + protected $default_view = 'workflows'; + + /** + * The extension for which the workflow apply. + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + } + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return BaseController|boolean This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view'); + $layout = $this->input->get('layout'); + $id = $this->input->getInt('id'); + + // Check for edit form. + if (in_array($view, ['workflow', 'stage', 'transition']) && $layout == 'edit' && !$this->checkEditId('com_workflow.edit.' . $view, $id)) { + // 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', $id), 'error'); + } + + $url = 'index.php?option=com_workflow&view=' . Inflector::pluralize($view) . '&extension=' . $this->input->getCmd('extension'); + + $this->setRedirect(Route::_($url, false)); + + return false; + } + + return parent::display(); + } } diff --git a/administrator/components/com_workflow/src/Controller/StageController.php b/administrator/components/com_workflow/src/Controller/StageController.php index 63be67755ad4d..f8dbe28d4f7ce 100644 --- a/administrator/components/com_workflow/src/Controller/StageController.php +++ b/administrator/components/com_workflow/src/Controller/StageController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Workflow\Administrator\Controller; \defined('_JEXEC') or die; @@ -23,159 +25,151 @@ */ class StageController extends FormController { - /** - * The workflow in where the stage belongs to - * - * @var integer - * @since 4.0.0 - */ - protected $workflowId; - - /** - * The extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension or workflow id is set - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If workflow id is not set try to get it from input or throw an exception - if (empty($this->workflowId)) - { - $this->workflowId = $this->input->getInt('workflow_id'); - - if (empty($this->workflowId)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); - } - } - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - } - - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', $this->extension . '.workflow.' . (int) $this->workflowId); - } - - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = isset($data[$key]) ? (int) $data[$key] : 0; - $user = $this->app->getIdentity(); - - $record = $this->getModel()->getItem($recordId); - - if (empty($record->id)) - { - return false; - } - - // Check "edit" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->extension . '.stage.' . $recordId)) - { - return true; - } - - // Check "edit own" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->extension . '.stage.' . $recordId)) - { - return !empty($record) && $record->created_by == $user->id; - } - - return false; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - - $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); - - return $append; - } + /** + * The workflow in where the stage belongs to + * + * @var integer + * @since 4.0.0 + */ + protected $workflowId; + + /** + * The extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension or workflow id is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If workflow id is not set try to get it from input or throw an exception + if (empty($this->workflowId)) { + $this->workflowId = $this->input->getInt('workflow_id'); + + if (empty($this->workflowId)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); + } + } + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', $this->extension . '.workflow.' . (int) $this->workflowId); + } + + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = isset($data[$key]) ? (int) $data[$key] : 0; + $user = $this->app->getIdentity(); + + $record = $this->getModel()->getItem($recordId); + + if (empty($record->id)) { + return false; + } + + // Check "edit" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->extension . '.stage.' . $recordId)) { + return true; + } + + // Check "edit own" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->extension . '.stage.' . $recordId)) { + return !empty($record) && $record->created_by == $user->id; + } + + return false; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + + $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); + + return $append; + } } diff --git a/administrator/components/com_workflow/src/Controller/StagesController.php b/administrator/components/com_workflow/src/Controller/StagesController.php index 61be18cba531c..2abdbb7e03f6d 100644 --- a/administrator/components/com_workflow/src/Controller/StagesController.php +++ b/administrator/components/com_workflow/src/Controller/StagesController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Workflow\Administrator\Controller; \defined('_JEXEC') or die; @@ -25,182 +27,170 @@ */ class StagesController extends AdminController { - /** - * The workflow in where the stage belongs to - * - * @var integer - * @since 4.0.0 - */ - protected $workflowId; - - /** - * The extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * The prefix to use with controller messages. - * - * @var string - * @since 4.0.0 - */ - protected $text_prefix = 'COM_WORKFLOW_STAGES'; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension or workflow id is set - */ - public function __construct(array $config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If workflow id is not set try to get it from input or throw an exception - if (empty($this->workflowId)) - { - $this->workflowId = $this->input->getInt('workflow_id'); - - if (empty($this->workflowId)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); - } - } - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - - $this->registerTask('unsetDefault', 'setDefault'); - } - - /** - * Proxy for getModel - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config The array of possible config values. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 4.0.0 - */ - public function getModel($name = 'Stage', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to set the home property for a list of items - * - * @return void - * - * @since 4.0.0 - */ - public function setDefault() - { - // Check for request forgeries - $this->checkToken(); - - // Get items to publish from the request. - $cid = (array) $this->input->get('cid', array(), 'int'); - $data = array('setDefault' => 1, 'unsetDefault' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($data, $task, 0, 'int'); - - if (!$value) - { - $this->setMessage(Text::_('COM_WORKFLOW_DISABLE_DEFAULT'), 'warning'); - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . '&extension=' . $this->extension, false - ) - ); - - return; - } - - // Remove zero values resulting from input filter - $cid = array_filter($cid); - - if (empty($cid)) - { - $this->setMessage(Text::_('COM_WORKFLOW_NO_ITEM_SELECTED'), 'warning'); - } - elseif (count($cid) > 1) - { - $this->setMessage(Text::_('COM_WORKFLOW_TOO_MANY_STAGES'), 'error'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Make sure the item ids are integers - $id = reset($cid); - - // Publish the items. - if (!$model->setDefault($id, $value)) - { - $this->setMessage($model->getError(), 'warning'); - } - else - { - $this->setMessage(Text::_('COM_WORKFLOW_STAGE_SET_DEFAULT')); - } - } - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . '&extension=' . $this->extension - . '&workflow_id=' . $this->workflowId, false - ) - ); - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - return '&extension=' . $this->extension . ($this->section ? '.' . $this->section : '') . '&workflow_id=' . $this->workflowId; - } + /** + * The workflow in where the stage belongs to + * + * @var integer + * @since 4.0.0 + */ + protected $workflowId; + + /** + * The extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 4.0.0 + */ + protected $text_prefix = 'COM_WORKFLOW_STAGES'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension or workflow id is set + */ + public function __construct(array $config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If workflow id is not set try to get it from input or throw an exception + if (empty($this->workflowId)) { + $this->workflowId = $this->input->getInt('workflow_id'); + + if (empty($this->workflowId)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); + } + } + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + + $this->registerTask('unsetDefault', 'setDefault'); + } + + /** + * Proxy for getModel + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 4.0.0 + */ + public function getModel($name = 'Stage', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to set the home property for a list of items + * + * @return void + * + * @since 4.0.0 + */ + public function setDefault() + { + // Check for request forgeries + $this->checkToken(); + + // Get items to publish from the request. + $cid = (array) $this->input->get('cid', array(), 'int'); + $data = array('setDefault' => 1, 'unsetDefault' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($data, $task, 0, 'int'); + + if (!$value) { + $this->setMessage(Text::_('COM_WORKFLOW_DISABLE_DEFAULT'), 'warning'); + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->extension, + false + ) + ); + + return; + } + + // Remove zero values resulting from input filter + $cid = array_filter($cid); + + if (empty($cid)) { + $this->setMessage(Text::_('COM_WORKFLOW_NO_ITEM_SELECTED'), 'warning'); + } elseif (count($cid) > 1) { + $this->setMessage(Text::_('COM_WORKFLOW_TOO_MANY_STAGES'), 'error'); + } else { + // Get the model. + $model = $this->getModel(); + + // Make sure the item ids are integers + $id = reset($cid); + + // Publish the items. + if (!$model->setDefault($id, $value)) { + $this->setMessage($model->getError(), 'warning'); + } else { + $this->setMessage(Text::_('COM_WORKFLOW_STAGE_SET_DEFAULT')); + } + } + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->extension + . '&workflow_id=' . $this->workflowId, + false + ) + ); + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + return '&extension=' . $this->extension . ($this->section ? '.' . $this->section : '') . '&workflow_id=' . $this->workflowId; + } } diff --git a/administrator/components/com_workflow/src/Controller/TransitionController.php b/administrator/components/com_workflow/src/Controller/TransitionController.php index fd7b19e89c586..1dc3288044e72 100644 --- a/administrator/components/com_workflow/src/Controller/TransitionController.php +++ b/administrator/components/com_workflow/src/Controller/TransitionController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Workflow\Administrator\Controller; \defined('_JEXEC') or die; @@ -23,160 +25,152 @@ */ class TransitionController extends FormController { - /** - * The workflow where the transition takes place - * - * @var integer - * @since 4.0.0 - */ - protected $workflowId; - - /** - * The extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension or workflow id is set - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If workflow id is not set try to get it from input or throw an exception - if (empty($this->workflowId)) - { - $this->workflowId = $this->input->getInt('workflow_id'); - - if (empty($this->workflowId)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); - } - } - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - } - - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', $this->extension . '.workflow.' . (int) $this->workflowId); - } - - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = isset($data[$key]) ? (int) $data[$key] : 0; - $user = $this->app->getIdentity(); - - $model = $this->getModel(); - - $item = $model->getItem($recordId); - - if (empty($item->id)) - { - return false; - } - - // Check "edit" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->extension . '.transition.' . $recordId)) - { - return true; - } - - // Check "edit own" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->extension . '.transition.' . $recordId)) - { - return !empty($item) && $item->created_by == $user->id; - } - - return false; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension; - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension; - - return $append; - } + /** + * The workflow where the transition takes place + * + * @var integer + * @since 4.0.0 + */ + protected $workflowId; + + /** + * The extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension or workflow id is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If workflow id is not set try to get it from input or throw an exception + if (empty($this->workflowId)) { + $this->workflowId = $this->input->getInt('workflow_id'); + + if (empty($this->workflowId)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); + } + } + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', $this->extension . '.workflow.' . (int) $this->workflowId); + } + + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = isset($data[$key]) ? (int) $data[$key] : 0; + $user = $this->app->getIdentity(); + + $model = $this->getModel(); + + $item = $model->getItem($recordId); + + if (empty($item->id)) { + return false; + } + + // Check "edit" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->extension . '.transition.' . $recordId)) { + return true; + } + + // Check "edit own" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->extension . '.transition.' . $recordId)) { + return !empty($item) && $item->created_by == $user->id; + } + + return false; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension; + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension; + + return $append; + } } diff --git a/administrator/components/com_workflow/src/Controller/TransitionsController.php b/administrator/components/com_workflow/src/Controller/TransitionsController.php index 31670f514b4e3..48b818cb0c74f 100644 --- a/administrator/components/com_workflow/src/Controller/TransitionsController.php +++ b/administrator/components/com_workflow/src/Controller/TransitionsController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Workflow\Administrator\Controller; \defined('_JEXEC') or die; @@ -23,115 +25,110 @@ */ class TransitionsController extends AdminController { - /** - * The workflow where the transition takes place - * - * @var integer - * @since 4.0.0 - */ - protected $workflowId; - - /** - * The extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * The prefix to use with controller messages. - * - * @var string - * @since 4.0.0 - */ - protected $text_prefix = 'COM_WORKFLOW_TRANSITIONS'; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension or workflow id is set - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If workflow id is not set try to get it from input or throw an exception - if (empty($this->workflowId)) - { - $this->workflowId = $this->input->getInt('workflow_id'); - - if (empty($this->workflowId)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); - } - } - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - } - - /** - * Proxy for getModel - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config The array of possible config values. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 4.0.0 - */ - public function getModel($name = 'Transition', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - - $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : '') - . '&workflow_id=' . $this->workflowId; - - return $append; - } + /** + * The workflow where the transition takes place + * + * @var integer + * @since 4.0.0 + */ + protected $workflowId; + + /** + * The extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 4.0.0 + */ + protected $text_prefix = 'COM_WORKFLOW_TRANSITIONS'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension or workflow id is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If workflow id is not set try to get it from input or throw an exception + if (empty($this->workflowId)) { + $this->workflowId = $this->input->getInt('workflow_id'); + + if (empty($this->workflowId)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); + } + } + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + } + + /** + * Proxy for getModel + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 4.0.0 + */ + public function getModel($name = 'Transition', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + + $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : '') + . '&workflow_id=' . $this->workflowId; + + return $append; + } } diff --git a/administrator/components/com_workflow/src/Controller/WorkflowController.php b/administrator/components/com_workflow/src/Controller/WorkflowController.php index 9d4208b24310b..67d847b5e0677 100644 --- a/administrator/components/com_workflow/src/Controller/WorkflowController.php +++ b/administrator/components/com_workflow/src/Controller/WorkflowController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Workflow\Administrator\Controller; \defined('_JEXEC') or die; @@ -25,223 +27,214 @@ */ class WorkflowController extends FormController { - /** - * The extension for which the workflows apply. - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension is set - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - } - - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', $this->extension); - } - - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = isset($data[$key]) ? (int) $data[$key] : 0; - $user = $this->app->getIdentity(); - - $record = $this->getModel()->getItem($recordId); - - if (empty($record->id)) - { - return false; - } - - // Check "edit" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->extension . '.workflow.' . $recordId)) - { - return true; - } - - // Check "edit own" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->extension . '.workflow.' . $recordId)) - { - return !empty($record) && $record->created_by == $user->id; - } - - return false; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); - - return $append; - } - - /** - * Function that allows child controller access to model data - * after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 4.0.0 - */ - public function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - $task = $this->getTask(); - - // The save2copy task needs to be handled slightly differently. - if ($task === 'save2copy') - { - $table = $model->getTable(); - - $key = $table->getKeyName(); - - $recordId = (int) $this->input->getInt($key); - - // @todo Moves queries out of the controller. - $db = $model->getDbo(); - $query = $db->getQuery(true); - - $query->select('*') - ->from($db->quoteName('#__workflow_stages')) - ->where($db->quoteName('workflow_id') . ' = :id') - ->bind(':id', $recordId, ParameterType::INTEGER); - - $statuses = $db->setQuery($query)->loadAssocList(); - - $smodel = $this->getModel('Stage'); - - $workflowID = (int) $model->getState($model->getName() . '.id'); - - $mapping = []; - - foreach ($statuses as $status) - { - $table = $smodel->getTable(); - - $oldID = $status['id']; - - $status['workflow_id'] = $workflowID; - $status['id'] = 0; - - unset($status['asset_id']); - - $table->save($status); - - $mapping[$oldID] = (int) $table->id; - } - - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__workflow_transitions')) - ->where($db->quoteName('workflow_id') . ' = :id') - ->bind(':id', $recordId, ParameterType::INTEGER); - - $transitions = $db->setQuery($query)->loadAssocList(); - - $tmodel = $this->getModel('Transition'); - - foreach ($transitions as $transition) - { - $table = $tmodel->getTable(); - - $transition['from_stage_id'] = $transition['from_stage_id'] != -1 ? $mapping[$transition['from_stage_id']] : -1; - $transition['to_stage_id'] = $mapping[$transition['to_stage_id']]; + /** + * The extension for which the workflows apply. + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', $this->extension); + } + + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = isset($data[$key]) ? (int) $data[$key] : 0; + $user = $this->app->getIdentity(); + + $record = $this->getModel()->getItem($recordId); + + if (empty($record->id)) { + return false; + } + + // Check "edit" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->extension . '.workflow.' . $recordId)) { + return true; + } + + // Check "edit own" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->extension . '.workflow.' . $recordId)) { + return !empty($record) && $record->created_by == $user->id; + } + + return false; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); + + return $append; + } + + /** + * Function that allows child controller access to model data + * after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 4.0.0 + */ + public function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + $task = $this->getTask(); + + // The save2copy task needs to be handled slightly differently. + if ($task === 'save2copy') { + $table = $model->getTable(); + + $key = $table->getKeyName(); + + $recordId = (int) $this->input->getInt($key); + + // @todo Moves queries out of the controller. + $db = $model->getDbo(); + $query = $db->getQuery(true); + + $query->select('*') + ->from($db->quoteName('#__workflow_stages')) + ->where($db->quoteName('workflow_id') . ' = :id') + ->bind(':id', $recordId, ParameterType::INTEGER); + + $statuses = $db->setQuery($query)->loadAssocList(); + + $smodel = $this->getModel('Stage'); + + $workflowID = (int) $model->getState($model->getName() . '.id'); + + $mapping = []; + + foreach ($statuses as $status) { + $table = $smodel->getTable(); + + $oldID = $status['id']; + + $status['workflow_id'] = $workflowID; + $status['id'] = 0; + + unset($status['asset_id']); + + $table->save($status); + + $mapping[$oldID] = (int) $table->id; + } + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__workflow_transitions')) + ->where($db->quoteName('workflow_id') . ' = :id') + ->bind(':id', $recordId, ParameterType::INTEGER); + + $transitions = $db->setQuery($query)->loadAssocList(); + + $tmodel = $this->getModel('Transition'); + + foreach ($transitions as $transition) { + $table = $tmodel->getTable(); + + $transition['from_stage_id'] = $transition['from_stage_id'] != -1 ? $mapping[$transition['from_stage_id']] : -1; + $transition['to_stage_id'] = $mapping[$transition['to_stage_id']]; - $transition['workflow_id'] = $workflowID; - $transition['id'] = 0; + $transition['workflow_id'] = $workflowID; + $transition['id'] = 0; - unset($transition['asset_id']); + unset($transition['asset_id']); - $table->save($transition); - } - } - } + $table->save($transition); + } + } + } } diff --git a/administrator/components/com_workflow/src/Controller/WorkflowsController.php b/administrator/components/com_workflow/src/Controller/WorkflowsController.php index 82ea1104490f6..6f02117a7c4bf 100644 --- a/administrator/components/com_workflow/src/Controller/WorkflowsController.php +++ b/administrator/components/com_workflow/src/Controller/WorkflowsController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Workflow\Administrator\Controller; \defined('_JEXEC') or die; @@ -25,163 +27,150 @@ */ class WorkflowsController extends AdminController { - /** - * The extension for which the workflows apply. - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension is set - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - - $this->registerTask('unsetDefault', 'setDefault'); - } - - /** - * Proxy for getModel - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config The array of possible config values. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 4.0.0 - */ - public function getModel($name = 'Workflow', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to set the home property for a list of items - * - * @return void - * - * @since 4.0.0 - */ - public function setDefault() - { - // Check for request forgeries - $this->checkToken(); - - // Get items to publish from the request. - $cid = (array) $this->input->get('cid', array(), 'int'); - $data = array('setDefault' => 1, 'unsetDefault' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($data, $task, 0, 'int'); - - if (!$value) - { - $this->setMessage(Text::_('COM_WORKFLOW_DISABLE_DEFAULT'), 'warning'); - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''), false - ) - ); - - return; - } - - // Remove zero values resulting from input filter - $cid = array_filter($cid); - - if (empty($cid)) - { - $this->setMessage(Text::_('COM_WORKFLOW_NO_ITEM_SELECTED'), 'warning'); - } - elseif (count($cid) > 1) - { - $this->setMessage(Text::_('COM_WORKFLOW_TOO_MANY_WORKFLOWS'), 'error'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Make sure the item ids are integers - $id = reset($cid); - - // Publish the items. - if (!$model->setDefault($id, $value)) - { - $this->setMessage($model->getError(), 'warning'); - } - else - { - if ($value === 1) - { - $ntext = 'COM_WORKFLOW_SET_DEFAULT'; - } - else - { - $ntext = 'COM_WORKFLOW_ITEM_UNSET_DEFAULT'; - } - - $this->setMessage(Text::_($ntext, count($cid))); - } - } - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''), false - ) - ); - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - return '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); - } + /** + * The extension for which the workflows apply. + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + + $this->registerTask('unsetDefault', 'setDefault'); + } + + /** + * Proxy for getModel + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 4.0.0 + */ + public function getModel($name = 'Workflow', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to set the home property for a list of items + * + * @return void + * + * @since 4.0.0 + */ + public function setDefault() + { + // Check for request forgeries + $this->checkToken(); + + // Get items to publish from the request. + $cid = (array) $this->input->get('cid', array(), 'int'); + $data = array('setDefault' => 1, 'unsetDefault' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($data, $task, 0, 'int'); + + if (!$value) { + $this->setMessage(Text::_('COM_WORKFLOW_DISABLE_DEFAULT'), 'warning'); + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''), + false + ) + ); + + return; + } + + // Remove zero values resulting from input filter + $cid = array_filter($cid); + + if (empty($cid)) { + $this->setMessage(Text::_('COM_WORKFLOW_NO_ITEM_SELECTED'), 'warning'); + } elseif (count($cid) > 1) { + $this->setMessage(Text::_('COM_WORKFLOW_TOO_MANY_WORKFLOWS'), 'error'); + } else { + // Get the model. + $model = $this->getModel(); + + // Make sure the item ids are integers + $id = reset($cid); + + // Publish the items. + if (!$model->setDefault($id, $value)) { + $this->setMessage($model->getError(), 'warning'); + } else { + if ($value === 1) { + $ntext = 'COM_WORKFLOW_SET_DEFAULT'; + } else { + $ntext = 'COM_WORKFLOW_ITEM_UNSET_DEFAULT'; + } + + $this->setMessage(Text::_($ntext, count($cid))); + } + } + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''), + false + ) + ); + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + return '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); + } } diff --git a/administrator/components/com_workflow/src/Dispatcher/Dispatcher.php b/administrator/components/com_workflow/src/Dispatcher/Dispatcher.php index e34a303e7291e..0050cd063ff14 100644 --- a/administrator/components/com_workflow/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_workflow/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ getApplication()->input->getCmd('extension'); + /** + * Workflows have to check for extension permission + * + * @return void + */ + protected function checkAccess() + { + $extension = $this->getApplication()->input->getCmd('extension'); - $parts = explode('.', $extension); + $parts = explode('.', $extension); - // Check the user has permission to access this component if in the backend - if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.manage.workflow', $parts[0])) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + // Check the user has permission to access this component if in the backend + if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.manage.workflow', $parts[0])) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_workflow/src/Field/ComponentsWorkflowField.php b/administrator/components/com_workflow/src/Field/ComponentsWorkflowField.php index 681abc8f7b090..33338736106a7 100644 --- a/administrator/components/com_workflow/src/Field/ComponentsWorkflowField.php +++ b/administrator/components/com_workflow/src/Field/ComponentsWorkflowField.php @@ -1,4 +1,5 @@ getDatabase(); - - $query = $db->getQuery(true) - ->select('DISTINCT a.name AS text, a.element AS value') - ->from('#__extensions as a') - ->where('a.enabled >= 1') - ->where('a.type =' . $db->quote('component')); - - $items = $db->setQuery($query)->loadObjectList(); - - $options = []; - - if (count($items)) - { - $lang = Factory::getLanguage(); - - $components = []; - - // Search for components supporting Fieldgroups - suppose that these components support fields as well - foreach ($items as &$item) - { - $availableActions = Access::getActionsFromFile( - JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', - "/access/section[@name='workflow']/" - ); - - if (!empty($availableActions)) - { - // Load language - $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; - $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) - || $lang->load($item->value . 'sys', $source); - - // Translate component name - $item->text = Text::_($item->text); - - $components[] = $item; - } - } - - if (empty($components)) - { - return []; - } - - foreach ($components as $component) - { - // Search for different contexts - $c = Factory::getApplication()->bootComponent($component->value); - - if ($c instanceof WorkflowServiceInterface) - { - $contexts = $c->getContexts(); - - foreach ($contexts as $context) - { - $newOption = new \stdClass; - $newOption->value = strtolower($component->value . '.' . $context); - $newOption->text = $component->text . ' - ' . Text::_($context); - $options[] = $newOption; - } - } - else - { - $options[] = $component; - } - } - - // Sort by name - $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); - } - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $items); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'ComponentsWorkflow'; + + /** + * Method to get a list of options for a list input. + * + * @return array An array of JHtml options. + * + * @since 3.7.0 + */ + protected function getOptions() + { + // Initialise variable. + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('DISTINCT a.name AS text, a.element AS value') + ->from('#__extensions as a') + ->where('a.enabled >= 1') + ->where('a.type =' . $db->quote('component')); + + $items = $db->setQuery($query)->loadObjectList(); + + $options = []; + + if (count($items)) { + $lang = Factory::getLanguage(); + + $components = []; + + // Search for components supporting Fieldgroups - suppose that these components support fields as well + foreach ($items as &$item) { + $availableActions = Access::getActionsFromFile( + JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', + "/access/section[@name='workflow']/" + ); + + if (!empty($availableActions)) { + // Load language + $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; + $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) + || $lang->load($item->value . 'sys', $source); + + // Translate component name + $item->text = Text::_($item->text); + + $components[] = $item; + } + } + + if (empty($components)) { + return []; + } + + foreach ($components as $component) { + // Search for different contexts + $c = Factory::getApplication()->bootComponent($component->value); + + if ($c instanceof WorkflowServiceInterface) { + $contexts = $c->getContexts(); + + foreach ($contexts as $context) { + $newOption = new \stdClass(); + $newOption->value = strtolower($component->value . '.' . $context); + $newOption->text = $component->text . ' - ' . Text::_($context); + $options[] = $newOption; + } + } else { + $options[] = $component; + } + } + + // Sort by name + $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $items); + + return $options; + } } diff --git a/administrator/components/com_workflow/src/Field/WorkflowcontextsField.php b/administrator/components/com_workflow/src/Field/WorkflowcontextsField.php index 08752825cdfd0..e2b21eba0e02f 100644 --- a/administrator/components/com_workflow/src/Field/WorkflowcontextsField.php +++ b/administrator/components/com_workflow/src/Field/WorkflowcontextsField.php @@ -1,4 +1,5 @@ getOptions()) < 2) - { - $this->layout = 'joomla.form.field.hidden'; - } + /** + * Method to get the field input markup for a generic list. + * Use the multiple attribute to enable multiselect. + * + * @return string The field input markup. + * + * @since 4.0.0 + */ + protected function getInput() + { + if (count($this->getOptions()) < 2) { + $this->layout = 'joomla.form.field.hidden'; + } - return parent::getInput(); - } + return parent::getInput(); + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 4.0.0 - */ - protected function getOptions() - { - $parts = explode('.', $this->value); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 4.0.0 + */ + protected function getOptions() + { + $parts = explode('.', $this->value); - $component = Factory::getApplication()->bootComponent($parts[0]); + $component = Factory::getApplication()->bootComponent($parts[0]); - if ($component instanceof WorkflowServiceInterface) - { - return $component->getWorkflowContexts(); - } + if ($component instanceof WorkflowServiceInterface) { + return $component->getWorkflowContexts(); + } - return []; - } + return []; + } } diff --git a/administrator/components/com_workflow/src/Helper/StageHelper.php b/administrator/components/com_workflow/src/Helper/StageHelper.php index 3348c32aa4ff9..b79048c4edb93 100644 --- a/administrator/components/com_workflow/src/Helper/StageHelper.php +++ b/administrator/components/com_workflow/src/Helper/StageHelper.php @@ -1,4 +1,5 @@ option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - - $this->setState('filter.extension', $extension); - } - - /** - * Method to change the title - * - * @param integer $categoryId The id of the category. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 4.0.0 - */ - protected function generateNewTitle($categoryId, $alias, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('title' => $title))) - { - $title = StringHelper::increment($title); - } - - return array($title, $alias); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function save($data) - { - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - $app = Factory::getApplication(); - $user = $app->getIdentity(); - $input = $app->input; - $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); - - if (empty($data['workflow_id'])) - { - $data['workflow_id'] = $workflowID; - } - - $workflow = $this->getTable('Workflow'); - - $workflow->load($data['workflow_id']); - - $parts = explode('.', $workflow->extension); - - if (isset($data['rules']) && !$user->authorise('core.admin', $parts[0])) - { - unset($data['rules']); - } - - // Make sure we use the correct extension when editing an existing workflow - $key = $table->getKeyName(); - $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); - - if ($pk > 0) - { - $table->load($pk); - - if ((int) $table->workflow_id) - { - $data['workflow_id'] = (int) $table->workflow_id; - } - } - - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - - // Alter the title for save as copy - if ($origTable->load(['title' => $data['title']])) - { - list($title) = $this->generateNewTitle(0, '', $data['title']); - $data['title'] = $title; - } - - $data['published'] = 0; - $data['default'] = 0; - } - - return parent::save($data); - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission for the component. - * - * @since 4.0.0 - */ - protected function canDelete($record) - { - $table = $this->getTable('Workflow', 'Administrator'); - - $table->load($record->workflow_id); - - if (empty($record->id) || $record->published != -2) - { - return false; - } - - $app = Factory::getApplication(); - $extension = $app->getUserStateFromRequest('com_workflow.stage.filter.extension', 'extension', null, 'cmd'); - - $parts = explode('.', $extension); - - $component = reset($parts); - - if (!Factory::getUser()->authorise('core.delete', $component . '.state.' . (int) $record->id) || $record->default) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); - - return false; - } - - return true; - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 4.0.0 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - $app = Factory::getApplication(); - $context = $this->option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - - if (!\property_exists($record, 'workflow_id')) - { - $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); - $record->workflow_id = $workflowID; - } - - // Check for existing workflow. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $extension . '.state.' . (int) $record->id); - } - - // Default to component settings if workflow isn't known. - return $user->authorise('core.edit.state', $extension); - } - - /** - * Abstract method for getting the form from the model. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 4.0.0 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm( - 'com_workflow.state', - 'stage', - array( - 'control' => 'jform', - 'load_data' => $loadData - ) - ); - - if (empty($form)) - { - return false; - } - - $id = $data['id'] ?? $form->getValue('id'); - - $item = $this->getItem($id); - - $canEditState = $this->canEditState((object) $item); - - // Modify the form based on access controls. - if (!$canEditState || !empty($item->default)) - { - if (!$canEditState) - { - $form->setFieldAttribute('published', 'disabled', 'true'); - $form->setFieldAttribute('published', 'required', 'false'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - $form->setFieldAttribute('default', 'disabled', 'true'); - $form->setFieldAttribute('default', 'required', 'false'); - $form->setFieldAttribute('default', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState( - 'com_workflow.edit.state.data', - array() - ); - - if (empty($data)) - { - $data = $this->getItem(); - } - - return $data; - } - - /** - * Method to change the home state of one or more items. - * - * @param array $pk A list of the primary keys to change. - * @param integer $value The value of the home state. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function setDefault($pk, $value = 1) - { - $table = $this->getTable(); - - if ($table->load($pk)) - { - if (!$table->published) - { - $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); - - return false; - } - } - - if (empty($table->id) || !$this->canEditState($table)) - { - Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - - return false; - } - - if ($value) - { - // Verify that the home page for this language is unique per client id - if ($table->load(array('default' => '1', 'workflow_id' => $table->workflow_id))) - { - $table->default = 0; - $table->store(); - } - } - - if ($table->load($pk)) - { - $table->default = $value; - $table->store(); - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the published state of one or more records. - * - * @param array &$pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function publish(&$pks, $value = 1) - { - $table = $this->getTable(); - $pks = (array) $pks; - $app = Factory::getApplication(); - $extension = $app->getUserStateFromRequest('com_workflow.state.filter.extension', 'extension', null, 'cmd'); - - // Default item existence checks. - if ($value != 1) - { - foreach ($pks as $i => $pk) - { - if ($table->load($pk) && $table->default) - { - // Prune items that you can't change. - $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DISABLE_DEFAULT'), 'error'); - - unset($pks[$i]); - } - } - } - - return parent::publish($pks, $value); - } - - /** - * Method to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 4.0.0 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $extension = Factory::getApplication()->input->get('extension'); - - $parts = explode('.', $extension); - - $extension = array_shift($parts); - - // Set the access control rules field component value. - $form->setFieldAttribute('rules', 'component', $extension); - - parent::preprocessForm($form, $data, $group); - } + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 4.0.0 + */ + public function populateState() + { + parent::populateState(); + + $app = Factory::getApplication(); + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + + $this->setState('filter.extension', $extension); + } + + /** + * Method to change the title + * + * @param integer $categoryId The id of the category. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 4.0.0 + */ + protected function generateNewTitle($categoryId, $alias, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('title' => $title))) { + $title = StringHelper::increment($title); + } + + return array($title, $alias); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function save($data) + { + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + $app = Factory::getApplication(); + $user = $app->getIdentity(); + $input = $app->input; + $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + + if (empty($data['workflow_id'])) { + $data['workflow_id'] = $workflowID; + } + + $workflow = $this->getTable('Workflow'); + + $workflow->load($data['workflow_id']); + + $parts = explode('.', $workflow->extension); + + if (isset($data['rules']) && !$user->authorise('core.admin', $parts[0])) { + unset($data['rules']); + } + + // Make sure we use the correct extension when editing an existing workflow + $key = $table->getKeyName(); + $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); + + if ($pk > 0) { + $table->load($pk); + + if ((int) $table->workflow_id) { + $data['workflow_id'] = (int) $table->workflow_id; + } + } + + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + + // Alter the title for save as copy + if ($origTable->load(['title' => $data['title']])) { + list($title) = $this->generateNewTitle(0, '', $data['title']); + $data['title'] = $title; + } + + $data['published'] = 0; + $data['default'] = 0; + } + + return parent::save($data); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 4.0.0 + */ + protected function canDelete($record) + { + $table = $this->getTable('Workflow', 'Administrator'); + + $table->load($record->workflow_id); + + if (empty($record->id) || $record->published != -2) { + return false; + } + + $app = Factory::getApplication(); + $extension = $app->getUserStateFromRequest('com_workflow.stage.filter.extension', 'extension', null, 'cmd'); + + $parts = explode('.', $extension); + + $component = reset($parts); + + if (!Factory::getUser()->authorise('core.delete', $component . '.state.' . (int) $record->id) || $record->default) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); + + return false; + } + + return true; + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 4.0.0 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + $app = Factory::getApplication(); + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + + if (!\property_exists($record, 'workflow_id')) { + $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + $record->workflow_id = $workflowID; + } + + // Check for existing workflow. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $extension . '.state.' . (int) $record->id); + } + + // Default to component settings if workflow isn't known. + return $user->authorise('core.edit.state', $extension); + } + + /** + * Abstract method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 4.0.0 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm( + 'com_workflow.state', + 'stage', + array( + 'control' => 'jform', + 'load_data' => $loadData + ) + ); + + if (empty($form)) { + return false; + } + + $id = $data['id'] ?? $form->getValue('id'); + + $item = $this->getItem($id); + + $canEditState = $this->canEditState((object) $item); + + // Modify the form based on access controls. + if (!$canEditState || !empty($item->default)) { + if (!$canEditState) { + $form->setFieldAttribute('published', 'disabled', 'true'); + $form->setFieldAttribute('published', 'required', 'false'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + $form->setFieldAttribute('default', 'disabled', 'true'); + $form->setFieldAttribute('default', 'required', 'false'); + $form->setFieldAttribute('default', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState( + 'com_workflow.edit.state.data', + array() + ); + + if (empty($data)) { + $data = $this->getItem(); + } + + return $data; + } + + /** + * Method to change the home state of one or more items. + * + * @param array $pk A list of the primary keys to change. + * @param integer $value The value of the home state. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function setDefault($pk, $value = 1) + { + $table = $this->getTable(); + + if ($table->load($pk)) { + if (!$table->published) { + $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); + + return false; + } + } + + if (empty($table->id) || !$this->canEditState($table)) { + Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + + return false; + } + + if ($value) { + // Verify that the home page for this language is unique per client id + if ($table->load(array('default' => '1', 'workflow_id' => $table->workflow_id))) { + $table->default = 0; + $table->store(); + } + } + + if ($table->load($pk)) { + $table->default = $value; + $table->store(); + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the published state of one or more records. + * + * @param array &$pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function publish(&$pks, $value = 1) + { + $table = $this->getTable(); + $pks = (array) $pks; + $app = Factory::getApplication(); + $extension = $app->getUserStateFromRequest('com_workflow.state.filter.extension', 'extension', null, 'cmd'); + + // Default item existence checks. + if ($value != 1) { + foreach ($pks as $i => $pk) { + if ($table->load($pk) && $table->default) { + // Prune items that you can't change. + $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DISABLE_DEFAULT'), 'error'); + + unset($pks[$i]); + } + } + } + + return parent::publish($pks, $value); + } + + /** + * Method to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $extension = Factory::getApplication()->input->get('extension'); + + $parts = explode('.', $extension); + + $extension = array_shift($parts); + + // Set the access control rules field component value. + $form->setFieldAttribute('rules', 'component', $extension); + + parent::preprocessForm($form, $data, $group); + } } diff --git a/administrator/components/com_workflow/src/Model/StagesModel.php b/administrator/components/com_workflow/src/Model/StagesModel.php index 53fb31fd4789b..0dc3b075e341b 100644 --- a/administrator/components/com_workflow/src/Model/StagesModel.php +++ b/administrator/components/com_workflow/src/Model/StagesModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.filter.workflow_id', 'workflow_id', 1, 'int'); - $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); - - if ($workflowID) - { - $table = $this->getTable('Workflow', 'Administrator'); - - if ($table->load($workflowID)) - { - $this->setState('active_workflow', $table->title); - } - } - - $this->setState('filter.workflow_id', $workflowID); - $this->setState('filter.extension', $extension); - - parent::populateState($ordering, $direction); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 4.0.0 - */ - protected function getReorderConditions($table) - { - return [ - $this->getDatabase()->quoteName('workflow_id') . ' = ' . (int) $table->workflow_id, - ]; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\Table\Table A Table object - * - * @since 4.0.0 - */ - public function getTable($type = 'Stage', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * Method to get the data that should be injected in the form. - * - * @return string The query to database. - * - * @since 4.0.0 - */ - public function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query - ->select( - [ - $db->quoteName('s.id'), - $db->quoteName('s.title'), - $db->quoteName('s.ordering'), - $db->quoteName('s.default'), - $db->quoteName('s.published'), - $db->quoteName('s.checked_out'), - $db->quoteName('s.checked_out_time'), - $db->quoteName('s.description'), - $db->quoteName('uc.name', 'editor'), - ] - ) - ->from($db->quoteName('#__workflow_stages', 's')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('s.checked_out')); - - // Filter by extension - if ($workflowID = (int) $this->getState('filter.workflow_id')) - { - $query->where($db->quoteName('s.workflow_id') . ' = :id') - ->bind(':id', $workflowID, ParameterType::INTEGER); - } - - $status = (string) $this->getState('filter.published'); - - // Filter by publish state - if (is_numeric($status)) - { - $status = (int) $status; - $query->where($db->quoteName('s.published') . ' = :status') - ->bind(':status', $status, ParameterType::INTEGER); - } - elseif ($status === '') - { - $query->where($db->quoteName('s.published') . ' IN (0, 1)'); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName('s.title') . ' LIKE :search1 OR ' . $db->quoteName('s.description') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 's.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Returns a workflow object - * - * @return object The workflow - * - * @since 4.0.0 - */ - public function getWorkflow() - { - $table = $this->getTable('Workflow', 'Administrator'); - - $workflowId = (int) $this->getState('filter.workflow_id'); - - if ($workflowId > 0) - { - $table->load($workflowId); - } - - return (object) $table->getProperties(); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see JController + * @since 4.0.0 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 's.id', + 'title', 's.title', + 'ordering','s.ordering', + 'published', 's.published' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState($ordering = 's.ordering', $direction = 'ASC') + { + $app = Factory::getApplication(); + + $workflowID = $app->getUserStateFromRequest($this->context . '.filter.workflow_id', 'workflow_id', 1, 'int'); + $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); + + if ($workflowID) { + $table = $this->getTable('Workflow', 'Administrator'); + + if ($table->load($workflowID)) { + $this->setState('active_workflow', $table->title); + } + } + + $this->setState('filter.workflow_id', $workflowID); + $this->setState('filter.extension', $extension); + + parent::populateState($ordering, $direction); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 4.0.0 + */ + protected function getReorderConditions($table) + { + return [ + $this->getDatabase()->quoteName('workflow_id') . ' = ' . (int) $table->workflow_id, + ]; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\Table\Table A Table object + * + * @since 4.0.0 + */ + public function getTable($type = 'Stage', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * Method to get the data that should be injected in the form. + * + * @return string The query to database. + * + * @since 4.0.0 + */ + public function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query + ->select( + [ + $db->quoteName('s.id'), + $db->quoteName('s.title'), + $db->quoteName('s.ordering'), + $db->quoteName('s.default'), + $db->quoteName('s.published'), + $db->quoteName('s.checked_out'), + $db->quoteName('s.checked_out_time'), + $db->quoteName('s.description'), + $db->quoteName('uc.name', 'editor'), + ] + ) + ->from($db->quoteName('#__workflow_stages', 's')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('s.checked_out')); + + // Filter by extension + if ($workflowID = (int) $this->getState('filter.workflow_id')) { + $query->where($db->quoteName('s.workflow_id') . ' = :id') + ->bind(':id', $workflowID, ParameterType::INTEGER); + } + + $status = (string) $this->getState('filter.published'); + + // Filter by publish state + if (is_numeric($status)) { + $status = (int) $status; + $query->where($db->quoteName('s.published') . ' = :status') + ->bind(':status', $status, ParameterType::INTEGER); + } elseif ($status === '') { + $query->where($db->quoteName('s.published') . ' IN (0, 1)'); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName('s.title') . ' LIKE :search1 OR ' . $db->quoteName('s.description') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 's.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Returns a workflow object + * + * @return object The workflow + * + * @since 4.0.0 + */ + public function getWorkflow() + { + $table = $this->getTable('Workflow', 'Administrator'); + + $workflowId = (int) $this->getState('filter.workflow_id'); + + if ($workflowId > 0) { + $table->load($workflowId); + } + + return (object) $table->getProperties(); + } } diff --git a/administrator/components/com_workflow/src/Model/TransitionModel.php b/administrator/components/com_workflow/src/Model/TransitionModel.php index 1e9708ccd5cc2..56b53617a5c3a 100644 --- a/administrator/components/com_workflow/src/Model/TransitionModel.php +++ b/administrator/components/com_workflow/src/Model/TransitionModel.php @@ -1,4 +1,5 @@ option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - - $this->setState('filter.extension', $extension); - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission for the component. - * - * @since 4.0.0 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - $app = Factory::getApplication(); - $extension = $app->getUserStateFromRequest('com_workflow.transition.filter.extension', 'extension', null, 'cmd'); - - return Factory::getUser()->authorise('core.delete', $extension . '.transition.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 4.0.0 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - $app = Factory::getApplication(); - $context = $this->option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - - if (!\property_exists($record, 'workflow_id')) - { - $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); - $record->workflow_id = $workflowID; - } - - // Check for existing workflow. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $extension . '.transition.' . (int) $record->id); - } - - // Default to component settings if workflow isn't known. - return $user->authorise('core.edit.state', $extension); - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return \Joomla\CMS\Object\CMSObject|boolean Object on success, false on failure. - * - * @since 4.0.0 - */ - public function getItem($pk = null) - { - $item = parent::getItem($pk); - - if (property_exists($item, 'options')) - { - $registry = new Registry($item->options); - $item->options = $registry->toArray(); - } - - return $item; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function save($data) - { - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - $app = Factory::getApplication(); - $user = $app->getIdentity(); - $input = $app->input; - - $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); - - if (empty($data['workflow_id'])) - { - $data['workflow_id'] = $workflowID; - } - - $workflow = $this->getTable('Workflow'); - - $workflow->load($data['workflow_id']); - - $parts = explode('.', $workflow->extension); - - if (isset($data['rules']) && !$user->authorise('core.admin', $parts[0])) - { - unset($data['rules']); - } - - // Make sure we use the correct workflow_id when editing an existing transition - $key = $table->getKeyName(); - $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); - - if ($pk > 0) - { - $table->load($pk); - - if ((int) $table->workflow_id) - { - $data['workflow_id'] = (int) $table->workflow_id; - } - } - - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - - // Alter the title for save as copy - if ($origTable->load(['title' => $data['title']])) - { - list($title) = $this->generateNewTitle(0, '', $data['title']); - $data['title'] = $title; - } - - $data['published'] = 0; - } - - return parent::save($data); - } - - /** - * Method to change the title - * - * @param integer $categoryId The id of the category. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 4.0.0 - */ - protected function generateNewTitle($categoryId, $alias, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('title' => $title))) - { - $title = StringHelper::increment($title); - } - - return array($title, $alias); - } - - /** - * Abstract method for getting the form from the model. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure - * - * @since 4.0.0 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm( - 'com_workflow.transition', - 'transition', - array( - 'control' => 'jform', - 'load_data' => $loadData - ) - ); - - if (empty($form)) - { - return false; - } - - $id = $data['id'] ?? $form->getValue('id'); - - $item = $this->getItem($id); - - $canEditState = $this->canEditState((object) $item); - - // Modify the form based on access controls. - if (!$canEditState) - { - $form->setFieldAttribute('published', 'disabled', 'true'); - $form->setFieldAttribute('published', 'required', 'false'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - if (!empty($item->workflow_id)) - { - $data['workflow_id'] = (int) $item->workflow_id; - } - - if (empty($data['workflow_id'])) - { - $context = $this->option . '.' . $this->name; - - $data['workflow_id'] = (int) Factory::getApplication()->getUserStateFromRequest( - $context . '.filter.workflow_id', 'workflow_id', - 0, - 'int' - ); - } - - $where = $this->getDatabase()->quoteName('workflow_id') . ' = ' . (int) $data['workflow_id']; - $where .= ' AND ' . $this->getDatabase()->quoteName('published') . ' = 1'; - - $form->setFieldAttribute('from_stage_id', 'sql_where', $where); - $form->setFieldAttribute('to_stage_id', 'sql_where', $where); - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState( - 'com_workflow.edit.transition.data', - array() - ); - - if (empty($data)) - { - $data = $this->getItem(); - } - - return $data; - } - - public function getWorkflow() - { - $app = Factory::getApplication(); - - $context = $this->option . '.' . $this->name; - - $workflow_id = (int) $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); - - $workflow = $this->getTable('Workflow'); - - $workflow->load($workflow_id); - - return (object) $workflow->getProperties(); - } - - /** - * Trigger the form preparation for the workflow group - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @see FormField - * @since 4.0.0 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $extension = Factory::getApplication()->input->get('extension'); - - $parts = explode('.', $extension); - - $extension = array_shift($parts); - - // Set the access control rules field component value. - $form->setFieldAttribute('rules', 'component', $extension); - - // Import the appropriate plugin group. - PluginHelper::importPlugin('workflow'); - - parent::preprocessForm($form, $data, $group); - } + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 4.0.0 + */ + public function populateState() + { + parent::populateState(); + + $app = Factory::getApplication(); + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + + $this->setState('filter.extension', $extension); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 4.0.0 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + $app = Factory::getApplication(); + $extension = $app->getUserStateFromRequest('com_workflow.transition.filter.extension', 'extension', null, 'cmd'); + + return Factory::getUser()->authorise('core.delete', $extension . '.transition.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 4.0.0 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + $app = Factory::getApplication(); + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + + if (!\property_exists($record, 'workflow_id')) { + $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + $record->workflow_id = $workflowID; + } + + // Check for existing workflow. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $extension . '.transition.' . (int) $record->id); + } + + // Default to component settings if workflow isn't known. + return $user->authorise('core.edit.state', $extension); + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return \Joomla\CMS\Object\CMSObject|boolean Object on success, false on failure. + * + * @since 4.0.0 + */ + public function getItem($pk = null) + { + $item = parent::getItem($pk); + + if (property_exists($item, 'options')) { + $registry = new Registry($item->options); + $item->options = $registry->toArray(); + } + + return $item; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function save($data) + { + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + $app = Factory::getApplication(); + $user = $app->getIdentity(); + $input = $app->input; + + $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + + if (empty($data['workflow_id'])) { + $data['workflow_id'] = $workflowID; + } + + $workflow = $this->getTable('Workflow'); + + $workflow->load($data['workflow_id']); + + $parts = explode('.', $workflow->extension); + + if (isset($data['rules']) && !$user->authorise('core.admin', $parts[0])) { + unset($data['rules']); + } + + // Make sure we use the correct workflow_id when editing an existing transition + $key = $table->getKeyName(); + $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); + + if ($pk > 0) { + $table->load($pk); + + if ((int) $table->workflow_id) { + $data['workflow_id'] = (int) $table->workflow_id; + } + } + + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + + // Alter the title for save as copy + if ($origTable->load(['title' => $data['title']])) { + list($title) = $this->generateNewTitle(0, '', $data['title']); + $data['title'] = $title; + } + + $data['published'] = 0; + } + + return parent::save($data); + } + + /** + * Method to change the title + * + * @param integer $categoryId The id of the category. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 4.0.0 + */ + protected function generateNewTitle($categoryId, $alias, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('title' => $title))) { + $title = StringHelper::increment($title); + } + + return array($title, $alias); + } + + /** + * Abstract method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure + * + * @since 4.0.0 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm( + 'com_workflow.transition', + 'transition', + array( + 'control' => 'jform', + 'load_data' => $loadData + ) + ); + + if (empty($form)) { + return false; + } + + $id = $data['id'] ?? $form->getValue('id'); + + $item = $this->getItem($id); + + $canEditState = $this->canEditState((object) $item); + + // Modify the form based on access controls. + if (!$canEditState) { + $form->setFieldAttribute('published', 'disabled', 'true'); + $form->setFieldAttribute('published', 'required', 'false'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + if (!empty($item->workflow_id)) { + $data['workflow_id'] = (int) $item->workflow_id; + } + + if (empty($data['workflow_id'])) { + $context = $this->option . '.' . $this->name; + + $data['workflow_id'] = (int) Factory::getApplication()->getUserStateFromRequest( + $context . '.filter.workflow_id', + 'workflow_id', + 0, + 'int' + ); + } + + $where = $this->getDatabase()->quoteName('workflow_id') . ' = ' . (int) $data['workflow_id']; + $where .= ' AND ' . $this->getDatabase()->quoteName('published') . ' = 1'; + + $form->setFieldAttribute('from_stage_id', 'sql_where', $where); + $form->setFieldAttribute('to_stage_id', 'sql_where', $where); + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState( + 'com_workflow.edit.transition.data', + array() + ); + + if (empty($data)) { + $data = $this->getItem(); + } + + return $data; + } + + public function getWorkflow() + { + $app = Factory::getApplication(); + + $context = $this->option . '.' . $this->name; + + $workflow_id = (int) $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + + $workflow = $this->getTable('Workflow'); + + $workflow->load($workflow_id); + + return (object) $workflow->getProperties(); + } + + /** + * Trigger the form preparation for the workflow group + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @see FormField + * @since 4.0.0 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $extension = Factory::getApplication()->input->get('extension'); + + $parts = explode('.', $extension); + + $extension = array_shift($parts); + + // Set the access control rules field component value. + $form->setFieldAttribute('rules', 'component', $extension); + + // Import the appropriate plugin group. + PluginHelper::importPlugin('workflow'); + + parent::preprocessForm($form, $data, $group); + } } diff --git a/administrator/components/com_workflow/src/Model/TransitionsModel.php b/administrator/components/com_workflow/src/Model/TransitionsModel.php index 8ecf6afd2ea3e..137691d6f17d4 100644 --- a/administrator/components/com_workflow/src/Model/TransitionsModel.php +++ b/administrator/components/com_workflow/src/Model/TransitionsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.filter.workflow_id', 'workflow_id', 1, 'int'); - $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); - - if ($workflowID) - { - $table = $this->getTable('Workflow', 'Administrator'); - - if ($table->load($workflowID)) - { - $this->setState('active_workflow', $table->title); - } - } - - $this->setState('filter.workflow_id', $workflowID); - $this->setState('filter.extension', $extension); - - parent::populateState($ordering, $direction); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\Table\Table A Table object - * - * @since 4.0.0 - */ - public function getTable($type = 'Transition', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 4.0.0 - */ - protected function getReorderConditions($table) - { - return [ - $this->getDatabase()->quoteName('workflow_id') . ' = ' . (int) $table->workflow_id, - ]; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return string The query to database. - * - * @since 4.0.0 - */ - public function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query - ->select( - [ - $db->quoteName('t.id'), - $db->quoteName('t.title'), - $db->quoteName('t.from_stage_id'), - $db->quoteName('t.to_stage_id'), - $db->quoteName('t.published'), - $db->quoteName('t.checked_out'), - $db->quoteName('t.checked_out_time'), - $db->quoteName('t.ordering'), - $db->quoteName('t.description'), - $db->quoteName('f_stage.title', 'from_stage'), - $db->quoteName('t_stage.title', 'to_stage'), - $db->quoteName('uc.name', 'editor'), - ] - ) - ->from($db->quoteName('#__workflow_transitions', 't')) - ->join('LEFT', $db->quoteName('#__workflow_stages', 'f_stage'), $db->quoteName('f_stage.id') . ' = ' . $db->quoteName('t.from_stage_id')) - ->join('LEFT', $db->quoteName('#__workflow_stages', 't_stage'), $db->quoteName('t_stage.id') . ' = ' . $db->quoteName('t.to_stage_id')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('t.checked_out')); - - // Filter by extension - if ($workflowID = (int) $this->getState('filter.workflow_id')) - { - $query->where($db->quoteName('t.workflow_id') . ' = :id') - ->bind(':id', $workflowID, ParameterType::INTEGER); - } - - $status = (string) $this->getState('filter.published'); - - // Filter by status - if (is_numeric($status)) - { - $status = (int) $status; - $query->where($db->quoteName('t.published') . ' = :status') - ->bind(':status', $status, ParameterType::INTEGER); - } - elseif ($status === '') - { - $query->where($db->quoteName('t.published') . ' IN (0, 1)'); - } - - // Filter by column from_stage_id - if ($fromStage = (int) $this->getState('filter.from_stage')) - { - $query->where($db->quoteName('from_stage_id') . ' = :fromStage') - ->bind(':fromStage', $fromStage, ParameterType::INTEGER); - } - - // Filter by column to_stage_id - if ($toStage = (int) $this->getState('filter.to_stage')) - { - $query->where($db->quoteName('to_stage_id') . ' = :toStage') - ->bind(':toStage', $toStage, ParameterType::INTEGER); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName('t.title') . ' LIKE :search1 OR ' . $db->quoteName('t.description') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 't.id'); - $orderDirn = strtoupper($this->state->get('list.direction', 'ASC')); - - $query->order($db->escape($orderCol) . ' ' . ($orderDirn === 'DESC' ? 'DESC' : 'ASC')); - - return $query; - } - - /** - * Get the filter form - * - * @param array $data data - * @param boolean $loadData load current data - * - * @return \Joomla\CMS\Form\Form|boolean The Form object or false on error - * - * @since 4.0.0 - */ - public function getFilterForm($data = array(), $loadData = true) - { - $form = parent::getFilterForm($data, $loadData); - - $id = (int) $this->getState('filter.workflow_id'); - - if ($form) - { - $where = $this->getDatabase()->quoteName('workflow_id') . ' = ' . $id . ' AND ' . $this->getDatabase()->quoteName('published') . ' = 1'; - - $form->setFieldAttribute('from_stage', 'sql_where', $where, 'filter'); - $form->setFieldAttribute('to_stage', 'sql_where', $where, 'filter'); - } - - return $form; - } - - /** - * Returns a workflow object - * - * @return object The workflow - * - * @since 4.0.0 - */ - public function getWorkflow() - { - $table = $this->getTable('Workflow', 'Administrator'); - - $workflowId = (int) $this->getState('filter.workflow_id'); - - if ($workflowId > 0) - { - $table->load($workflowId); - } - - return (object) $table->getProperties(); - } - + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see JController + * @since 4.0.0 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 't.id', + 'published', 't.published', + 'ordering', 't.ordering', + 'title', 't.title', + 'from_stage', 't.from_stage_id', + 'to_stage', 't.to_stage_id' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState($ordering = 't.ordering', $direction = 'ASC') + { + $app = Factory::getApplication(); + $workflowID = $app->getUserStateFromRequest($this->context . '.filter.workflow_id', 'workflow_id', 1, 'int'); + $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); + + if ($workflowID) { + $table = $this->getTable('Workflow', 'Administrator'); + + if ($table->load($workflowID)) { + $this->setState('active_workflow', $table->title); + } + } + + $this->setState('filter.workflow_id', $workflowID); + $this->setState('filter.extension', $extension); + + parent::populateState($ordering, $direction); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\Table\Table A Table object + * + * @since 4.0.0 + */ + public function getTable($type = 'Transition', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 4.0.0 + */ + protected function getReorderConditions($table) + { + return [ + $this->getDatabase()->quoteName('workflow_id') . ' = ' . (int) $table->workflow_id, + ]; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return string The query to database. + * + * @since 4.0.0 + */ + public function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query + ->select( + [ + $db->quoteName('t.id'), + $db->quoteName('t.title'), + $db->quoteName('t.from_stage_id'), + $db->quoteName('t.to_stage_id'), + $db->quoteName('t.published'), + $db->quoteName('t.checked_out'), + $db->quoteName('t.checked_out_time'), + $db->quoteName('t.ordering'), + $db->quoteName('t.description'), + $db->quoteName('f_stage.title', 'from_stage'), + $db->quoteName('t_stage.title', 'to_stage'), + $db->quoteName('uc.name', 'editor'), + ] + ) + ->from($db->quoteName('#__workflow_transitions', 't')) + ->join('LEFT', $db->quoteName('#__workflow_stages', 'f_stage'), $db->quoteName('f_stage.id') . ' = ' . $db->quoteName('t.from_stage_id')) + ->join('LEFT', $db->quoteName('#__workflow_stages', 't_stage'), $db->quoteName('t_stage.id') . ' = ' . $db->quoteName('t.to_stage_id')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('t.checked_out')); + + // Filter by extension + if ($workflowID = (int) $this->getState('filter.workflow_id')) { + $query->where($db->quoteName('t.workflow_id') . ' = :id') + ->bind(':id', $workflowID, ParameterType::INTEGER); + } + + $status = (string) $this->getState('filter.published'); + + // Filter by status + if (is_numeric($status)) { + $status = (int) $status; + $query->where($db->quoteName('t.published') . ' = :status') + ->bind(':status', $status, ParameterType::INTEGER); + } elseif ($status === '') { + $query->where($db->quoteName('t.published') . ' IN (0, 1)'); + } + + // Filter by column from_stage_id + if ($fromStage = (int) $this->getState('filter.from_stage')) { + $query->where($db->quoteName('from_stage_id') . ' = :fromStage') + ->bind(':fromStage', $fromStage, ParameterType::INTEGER); + } + + // Filter by column to_stage_id + if ($toStage = (int) $this->getState('filter.to_stage')) { + $query->where($db->quoteName('to_stage_id') . ' = :toStage') + ->bind(':toStage', $toStage, ParameterType::INTEGER); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName('t.title') . ' LIKE :search1 OR ' . $db->quoteName('t.description') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 't.id'); + $orderDirn = strtoupper($this->state->get('list.direction', 'ASC')); + + $query->order($db->escape($orderCol) . ' ' . ($orderDirn === 'DESC' ? 'DESC' : 'ASC')); + + return $query; + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return \Joomla\CMS\Form\Form|boolean The Form object or false on error + * + * @since 4.0.0 + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + $id = (int) $this->getState('filter.workflow_id'); + + if ($form) { + $where = $this->getDatabase()->quoteName('workflow_id') . ' = ' . $id . ' AND ' . $this->getDatabase()->quoteName('published') . ' = 1'; + + $form->setFieldAttribute('from_stage', 'sql_where', $where, 'filter'); + $form->setFieldAttribute('to_stage', 'sql_where', $where, 'filter'); + } + + return $form; + } + + /** + * Returns a workflow object + * + * @return object The workflow + * + * @since 4.0.0 + */ + public function getWorkflow() + { + $table = $this->getTable('Workflow', 'Administrator'); + + $workflowId = (int) $this->getState('filter.workflow_id'); + + if ($workflowId > 0) { + $table->load($workflowId); + } + + return (object) $table->getProperties(); + } } diff --git a/administrator/components/com_workflow/src/Model/WorkflowModel.php b/administrator/components/com_workflow/src/Model/WorkflowModel.php index 6e232db185091..8367d3ad8c3f1 100644 --- a/administrator/components/com_workflow/src/Model/WorkflowModel.php +++ b/administrator/components/com_workflow/src/Model/WorkflowModel.php @@ -1,4 +1,5 @@ option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - - $this->setState('filter.extension', $extension); - } - - /** - * Method to change the title - * - * @param integer $categoryId The id of the category. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 4.0.0 - */ - protected function generateNewTitle($categoryId, $alias, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('title' => $title))) - { - $title = StringHelper::increment($title); - } - - return array($title, $alias); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function save($data) - { - $table = $this->getTable(); - $app = Factory::getApplication(); - $user = $app->getIdentity(); - $input = $app->input; - $context = $this->option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - $data['extension'] = !empty($data['extension']) ? $data['extension'] : $extension; - - // Make sure we use the correct extension when editing an existing workflow - $key = $table->getKeyName(); - $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); - - if ($pk > 0) - { - $table->load($pk); - - $data['extension'] = $table->extension; - } - - if (isset($data['rules']) && !$user->authorise('core.admin', $data['extension'])) - { - unset($data['rules']); - } - - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - - // Alter the title for save as copy - if ($origTable->load(['title' => $data['title']])) - { - list($title) = $this->generateNewTitle(0, '', $data['title']); - $data['title'] = $title; - } - - // Unpublish new copy - $data['published'] = 0; - $data['default'] = 0; - } - - $result = parent::save($data); - - // Create default stage for new workflow - if ($result && $input->getCmd('task') !== 'save2copy' && $this->getState($this->getName() . '.new')) - { - $workflow_id = (int) $this->getState($this->getName() . '.id'); - - $table = $this->getTable('Stage'); - - $table->id = 0; - $table->title = 'COM_WORKFLOW_BASIC_STAGE'; - $table->description = ''; - $table->workflow_id = $workflow_id; - $table->published = 1; - $table->default = 1; - - $table->store(); - } - - return $result; - } - - /** - * Abstract method for getting the form from the model. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure - * - * @since 4.0.0 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm( - 'com_workflow.workflow', - 'workflow', - array( - 'control' => 'jform', - 'load_data' => $loadData - ) - ); - - if (empty($form)) - { - return false; - } - - $id = $data['id'] ?? $form->getValue('id'); - - $item = $this->getItem($id); - - $canEditState = $this->canEditState((object) $item); - - // Modify the form based on access controls. - if (!$canEditState || !empty($item->default)) - { - if (!$canEditState) - { - $form->setFieldAttribute('published', 'disabled', 'true'); - $form->setFieldAttribute('published', 'required', 'false'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - $form->setFieldAttribute('default', 'disabled', 'true'); - $form->setFieldAttribute('default', 'required', 'false'); - $form->setFieldAttribute('default', 'filter', 'unset'); - } - - $form->setFieldAttribute('created', 'default', Factory::getDate()->format('Y-m-d H:i:s')); - $form->setFieldAttribute('modified', 'default', Factory::getDate()->format('Y-m-d H:i:s')); - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState( - 'com_workflow.edit.workflow.data', - array() - ); - - if (empty($data)) - { - $data = $this->getItem(); - } - - return $data; - } - - /** - * Method to preprocess the form. - * - * @param Form $form Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 4.0.0 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $extension = Factory::getApplication()->input->get('extension'); - - $parts = explode('.', $extension); - - $extension = array_shift($parts); - - // Set the access control rules field component value. - $form->setFieldAttribute('rules', 'component', $extension); - - parent::preprocessForm($form, $data, $group); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 4.0.0 - */ - protected function getReorderConditions($table) - { - $db = $this->getDatabase(); - - return [ - $db->quoteName('extension') . ' = ' . $db->quote($table->extension), - ]; - } - - /** - * Method to change the default state of one item. - * - * @param array $pk A list of the primary keys to change. - * @param integer $value The value of the home state. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function setDefault($pk, $value = 1) - { - $table = $this->getTable(); - - if ($table->load($pk)) - { - if ($table->published !== 1) - { - $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); - - return false; - } - } - - if (empty($table->id) || !$this->canEditState($table)) - { - Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - - return false; - } - - $date = Factory::getDate()->toSql(); - - if ($value) - { - // Unset other default item - if ($table->load( - [ - 'default' => '1', - 'extension' => $table->get('extension') - ] - )) - { - $table->default = 0; - $table->modified = $date; - $table->store(); - } - } - - if ($table->load($pk)) - { - $table->modified = $date; - $table->default = $value; - $table->store(); - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission for the component. - * - * @since 4.0.0 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - return Factory::getUser()->authorise('core.delete', $record->extension . '.workflow.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 4.0.0 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - - // Check for existing workflow. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $record->extension . '.workflow.' . (int) $record->id); - } - - // Default to component settings if workflow isn't known. - return $user->authorise('core.edit.state', $record->extension); - } - - /** - * Method to change the published state of one or more records. - * - * @param array &$pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function publish(&$pks, $value = 1) - { - $table = $this->getTable(); - $pks = (array) $pks; - - $date = Factory::getDate()->toSql(); - - // Default workflow item check. - foreach ($pks as $i => $pk) - { - if ($table->load($pk) && $value != 1 && $table->default) - { - // Prune items that you can't change. - Factory::getApplication()->enqueueMessage(Text::_('COM_WORKFLOW_UNPUBLISH_DEFAULT_ERROR'), 'error'); - unset($pks[$i]); - break; - } - } - - // Clean the cache. - $this->cleanCache(); - - // Ensure that previous checks don't empty the array. - if (empty($pks)) - { - return true; - } - - $table->load($pk); - $table->modified = $date; - $table->store(); - - return parent::publish($pks, $value); - } + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 4.0.0 + */ + public function populateState() + { + parent::populateState(); + + $app = Factory::getApplication(); + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + + $this->setState('filter.extension', $extension); + } + + /** + * Method to change the title + * + * @param integer $categoryId The id of the category. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 4.0.0 + */ + protected function generateNewTitle($categoryId, $alias, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('title' => $title))) { + $title = StringHelper::increment($title); + } + + return array($title, $alias); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function save($data) + { + $table = $this->getTable(); + $app = Factory::getApplication(); + $user = $app->getIdentity(); + $input = $app->input; + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + $data['extension'] = !empty($data['extension']) ? $data['extension'] : $extension; + + // Make sure we use the correct extension when editing an existing workflow + $key = $table->getKeyName(); + $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); + + if ($pk > 0) { + $table->load($pk); + + $data['extension'] = $table->extension; + } + + if (isset($data['rules']) && !$user->authorise('core.admin', $data['extension'])) { + unset($data['rules']); + } + + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + + // Alter the title for save as copy + if ($origTable->load(['title' => $data['title']])) { + list($title) = $this->generateNewTitle(0, '', $data['title']); + $data['title'] = $title; + } + + // Unpublish new copy + $data['published'] = 0; + $data['default'] = 0; + } + + $result = parent::save($data); + + // Create default stage for new workflow + if ($result && $input->getCmd('task') !== 'save2copy' && $this->getState($this->getName() . '.new')) { + $workflow_id = (int) $this->getState($this->getName() . '.id'); + + $table = $this->getTable('Stage'); + + $table->id = 0; + $table->title = 'COM_WORKFLOW_BASIC_STAGE'; + $table->description = ''; + $table->workflow_id = $workflow_id; + $table->published = 1; + $table->default = 1; + + $table->store(); + } + + return $result; + } + + /** + * Abstract method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure + * + * @since 4.0.0 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm( + 'com_workflow.workflow', + 'workflow', + array( + 'control' => 'jform', + 'load_data' => $loadData + ) + ); + + if (empty($form)) { + return false; + } + + $id = $data['id'] ?? $form->getValue('id'); + + $item = $this->getItem($id); + + $canEditState = $this->canEditState((object) $item); + + // Modify the form based on access controls. + if (!$canEditState || !empty($item->default)) { + if (!$canEditState) { + $form->setFieldAttribute('published', 'disabled', 'true'); + $form->setFieldAttribute('published', 'required', 'false'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + $form->setFieldAttribute('default', 'disabled', 'true'); + $form->setFieldAttribute('default', 'required', 'false'); + $form->setFieldAttribute('default', 'filter', 'unset'); + } + + $form->setFieldAttribute('created', 'default', Factory::getDate()->format('Y-m-d H:i:s')); + $form->setFieldAttribute('modified', 'default', Factory::getDate()->format('Y-m-d H:i:s')); + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState( + 'com_workflow.edit.workflow.data', + array() + ); + + if (empty($data)) { + $data = $this->getItem(); + } + + return $data; + } + + /** + * Method to preprocess the form. + * + * @param Form $form Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $extension = Factory::getApplication()->input->get('extension'); + + $parts = explode('.', $extension); + + $extension = array_shift($parts); + + // Set the access control rules field component value. + $form->setFieldAttribute('rules', 'component', $extension); + + parent::preprocessForm($form, $data, $group); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 4.0.0 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('extension') . ' = ' . $db->quote($table->extension), + ]; + } + + /** + * Method to change the default state of one item. + * + * @param array $pk A list of the primary keys to change. + * @param integer $value The value of the home state. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function setDefault($pk, $value = 1) + { + $table = $this->getTable(); + + if ($table->load($pk)) { + if ($table->published !== 1) { + $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); + + return false; + } + } + + if (empty($table->id) || !$this->canEditState($table)) { + Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + + return false; + } + + $date = Factory::getDate()->toSql(); + + if ($value) { + // Unset other default item + if ( + $table->load( + [ + 'default' => '1', + 'extension' => $table->get('extension') + ] + ) + ) { + $table->default = 0; + $table->modified = $date; + $table->store(); + } + } + + if ($table->load($pk)) { + $table->modified = $date; + $table->default = $value; + $table->store(); + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 4.0.0 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + return Factory::getUser()->authorise('core.delete', $record->extension . '.workflow.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 4.0.0 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + + // Check for existing workflow. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $record->extension . '.workflow.' . (int) $record->id); + } + + // Default to component settings if workflow isn't known. + return $user->authorise('core.edit.state', $record->extension); + } + + /** + * Method to change the published state of one or more records. + * + * @param array &$pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function publish(&$pks, $value = 1) + { + $table = $this->getTable(); + $pks = (array) $pks; + + $date = Factory::getDate()->toSql(); + + // Default workflow item check. + foreach ($pks as $i => $pk) { + if ($table->load($pk) && $value != 1 && $table->default) { + // Prune items that you can't change. + Factory::getApplication()->enqueueMessage(Text::_('COM_WORKFLOW_UNPUBLISH_DEFAULT_ERROR'), 'error'); + unset($pks[$i]); + break; + } + } + + // Clean the cache. + $this->cleanCache(); + + // Ensure that previous checks don't empty the array. + if (empty($pks)) { + return true; + } + + $table->load($pk); + $table->modified = $date; + $table->store(); + + return parent::publish($pks, $value); + } } diff --git a/administrator/components/com_workflow/src/Model/WorkflowsModel.php b/administrator/components/com_workflow/src/Model/WorkflowsModel.php index 9fcd14fbc68af..7b39184f4c4b7 100644 --- a/administrator/components/com_workflow/src/Model/WorkflowsModel.php +++ b/administrator/components/com_workflow/src/Model/WorkflowsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); - - $this->setState('filter.extension', $extension); - $parts = explode('.', $extension); - - // Extract the component name - $this->setState('filter.component', $parts[0]); - - // Extract the optional section name - $this->setState('filter.section', (count($parts) > 1) ? $parts[1] : null); - - parent::populateState($ordering, $direction); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\Table\Table A Table object - * - * @since 4.0.0 - */ - public function getTable($type = 'Workflow', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 4.0.0 - */ - public function getItems() - { - $items = parent::getItems(); - - if ($items) - { - $this->countItems($items); - } - - return $items; - } - - /** - * Get the filter form - * - * @param array $data data - * @param boolean $loadData load current data - * - * @return \Joomla\CMS\Form\Form|bool the Form object or false - * - * @since 4.0.0 - */ - public function getFilterForm($data = array(), $loadData = true) - { - $form = parent::getFilterForm($data, $loadData); - - if ($form) - { - $form->setValue('extension', null, $this->getState('filter.extension')); - } - - return $form; - } - - /** - * Add the number of transitions and states to all workflow items - * - * @param array $items The workflow items - * - * @return mixed An array of data items on success, false on failure. - * - * @since 4.0.0 - */ - protected function countItems($items) - { - $db = $this->getDatabase(); - - $ids = [0]; - - foreach ($items as $item) - { - $ids[] = (int) $item->id; - - $item->count_states = 0; - $item->count_transitions = 0; - } - - $query = $db->getQuery(true); - - $query->select( - [ - $db->quoteName('workflow_id'), - 'COUNT(*) AS ' . $db->quoteName('count'), - ] - ) - ->from($db->quoteName('#__workflow_stages')) - ->whereIn($db->quoteName('workflow_id'), $ids) - ->where($db->quoteName('published') . ' >= 0') - ->group($db->quoteName('workflow_id')); - - $status = $db->setQuery($query)->loadObjectList('workflow_id'); - - $query = $db->getQuery(true); - - $query->select( - [ - $db->quoteName('workflow_id'), - 'COUNT(*) AS ' . $db->quoteName('count'), - ] - ) - ->from($db->quoteName('#__workflow_transitions')) - ->whereIn($db->quoteName('workflow_id'), $ids) - ->where($db->quoteName('published') . ' >= 0') - ->group($db->quoteName('workflow_id')); - - $transitions = $db->setQuery($query)->loadObjectList('workflow_id'); - - foreach ($items as $item) - { - if (isset($status[$item->id])) - { - $item->count_states = (int) $status[$item->id]->count; - } - - if (isset($transitions[$item->id])) - { - $item->count_transitions = (int) $transitions[$item->id]->count; - } - } - } - - /** - * Method to get the data that should be injected in the form. - * - * @return string The query to database. - * - * @since 4.0.0 - */ - public function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select( - [ - $db->quoteName('w.id'), - $db->quoteName('w.title'), - $db->quoteName('w.created'), - $db->quoteName('w.modified'), - $db->quoteName('w.published'), - $db->quoteName('w.checked_out'), - $db->quoteName('w.checked_out_time'), - $db->quoteName('w.ordering'), - $db->quoteName('w.default'), - $db->quoteName('w.created_by'), - $db->quoteName('w.description'), - $db->quoteName('u.name'), - $db->quoteName('uc.name', 'editor'), - ] - ) - ->from($db->quoteName('#__workflows', 'w')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('w.created_by')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('w.checked_out')); - - // Filter by extension - if ($extension = $this->getState('filter.extension')) - { - $query->where($db->quoteName('extension') . ' = :extension') - ->bind(':extension', $extension); - } - - $status = (string) $this->getState('filter.published'); - - // Filter by status - if (is_numeric($status)) - { - $status = (int) $status; - $query->where($db->quoteName('w.published') . ' = :published') - ->bind(':published', $status, ParameterType::INTEGER); - } - elseif ($status === '') - { - $query->where($db->quoteName('w.published') . ' IN (0, 1)'); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName('w.title') . ' LIKE :search1 OR ' . $db->quoteName('w.description') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 'w.ordering'); - $orderDirn = strtoupper($this->state->get('list.direction', 'ASC')); - - $query->order($db->escape($orderCol) . ' ' . ($orderDirn === 'DESC' ? 'DESC' : 'ASC')); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see JController + * @since 4.0.0 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'w.id', + 'title', 'w.title', + 'published', 'w.published', + 'created_by', 'w.created_by', + 'created', 'w.created', + 'ordering', 'w.ordering', + 'modified', 'w.modified', + 'description', 'w.description' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState($ordering = 'w.ordering', $direction = 'asc') + { + $app = Factory::getApplication(); + $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); + + $this->setState('filter.extension', $extension); + $parts = explode('.', $extension); + + // Extract the component name + $this->setState('filter.component', $parts[0]); + + // Extract the optional section name + $this->setState('filter.section', (count($parts) > 1) ? $parts[1] : null); + + parent::populateState($ordering, $direction); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\Table\Table A Table object + * + * @since 4.0.0 + */ + public function getTable($type = 'Workflow', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 4.0.0 + */ + public function getItems() + { + $items = parent::getItems(); + + if ($items) { + $this->countItems($items); + } + + return $items; + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return \Joomla\CMS\Form\Form|bool the Form object or false + * + * @since 4.0.0 + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + if ($form) { + $form->setValue('extension', null, $this->getState('filter.extension')); + } + + return $form; + } + + /** + * Add the number of transitions and states to all workflow items + * + * @param array $items The workflow items + * + * @return mixed An array of data items on success, false on failure. + * + * @since 4.0.0 + */ + protected function countItems($items) + { + $db = $this->getDatabase(); + + $ids = [0]; + + foreach ($items as $item) { + $ids[] = (int) $item->id; + + $item->count_states = 0; + $item->count_transitions = 0; + } + + $query = $db->getQuery(true); + + $query->select( + [ + $db->quoteName('workflow_id'), + 'COUNT(*) AS ' . $db->quoteName('count'), + ] + ) + ->from($db->quoteName('#__workflow_stages')) + ->whereIn($db->quoteName('workflow_id'), $ids) + ->where($db->quoteName('published') . ' >= 0') + ->group($db->quoteName('workflow_id')); + + $status = $db->setQuery($query)->loadObjectList('workflow_id'); + + $query = $db->getQuery(true); + + $query->select( + [ + $db->quoteName('workflow_id'), + 'COUNT(*) AS ' . $db->quoteName('count'), + ] + ) + ->from($db->quoteName('#__workflow_transitions')) + ->whereIn($db->quoteName('workflow_id'), $ids) + ->where($db->quoteName('published') . ' >= 0') + ->group($db->quoteName('workflow_id')); + + $transitions = $db->setQuery($query)->loadObjectList('workflow_id'); + + foreach ($items as $item) { + if (isset($status[$item->id])) { + $item->count_states = (int) $status[$item->id]->count; + } + + if (isset($transitions[$item->id])) { + $item->count_transitions = (int) $transitions[$item->id]->count; + } + } + } + + /** + * Method to get the data that should be injected in the form. + * + * @return string The query to database. + * + * @since 4.0.0 + */ + public function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + [ + $db->quoteName('w.id'), + $db->quoteName('w.title'), + $db->quoteName('w.created'), + $db->quoteName('w.modified'), + $db->quoteName('w.published'), + $db->quoteName('w.checked_out'), + $db->quoteName('w.checked_out_time'), + $db->quoteName('w.ordering'), + $db->quoteName('w.default'), + $db->quoteName('w.created_by'), + $db->quoteName('w.description'), + $db->quoteName('u.name'), + $db->quoteName('uc.name', 'editor'), + ] + ) + ->from($db->quoteName('#__workflows', 'w')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('w.created_by')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('w.checked_out')); + + // Filter by extension + if ($extension = $this->getState('filter.extension')) { + $query->where($db->quoteName('extension') . ' = :extension') + ->bind(':extension', $extension); + } + + $status = (string) $this->getState('filter.published'); + + // Filter by status + if (is_numeric($status)) { + $status = (int) $status; + $query->where($db->quoteName('w.published') . ' = :published') + ->bind(':published', $status, ParameterType::INTEGER); + } elseif ($status === '') { + $query->where($db->quoteName('w.published') . ' IN (0, 1)'); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName('w.title') . ' LIKE :search1 OR ' . $db->quoteName('w.description') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 'w.ordering'); + $orderDirn = strtoupper($this->state->get('list.direction', 'ASC')); + + $query->order($db->escape($orderCol) . ' ' . ($orderDirn === 'DESC' ? 'DESC' : 'ASC')); + + return $query; + } } diff --git a/administrator/components/com_workflow/src/Table/StageTable.php b/administrator/components/com_workflow/src/Table/StageTable.php index 382e0422a0803..9091d11b65ffb 100644 --- a/administrator/components/com_workflow/src/Table/StageTable.php +++ b/administrator/components/com_workflow/src/Table/StageTable.php @@ -1,4 +1,5 @@ getDbo(); - $app = Factory::getApplication(); - $pk = (int) $pk; - - $query = $db->getQuery(true) - ->select($db->quoteName('default')) - ->from($db->quoteName('#__workflow_stages')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - $isDefault = $db->setQuery($query)->loadResult(); - - if ($isDefault) - { - $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DELETE_IS_DEFAULT'), 'error'); - - return false; - } - - try - { - $query = $db->getQuery(true) - ->delete($db->quoteName('#__workflow_transitions')) - ->where( - [ - $db->quoteName('to_stage_id') . ' = :idTo', - $db->quoteName('from_stage_id') . ' = :idFrom', - ], - 'OR' - ) - ->bind([':idTo', ':idFrom'], $pk, ParameterType::INTEGER); - - $db->setQuery($query)->execute(); - - return parent::delete($pk); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage(Text::sprintf('COM_WORKFLOW_MSG_WORKFLOWS_DELETE_ERROR', $e->getMessage()), 'error'); - } - - return false; - } - - /** - * Overloaded check function - * - * @return boolean True on success - * - * @see Table::check() - * @since 4.0.0 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (trim($this->title) === '') - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_STATE')); - - return false; - } - - if (!empty($this->default)) - { - if ((int) $this->published !== 1) - { - $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); - - return false; - } - } - else - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query - ->select($db->quoteName('id')) - ->from($db->quoteName('#__workflow_stages')) - ->where( - [ - $db->quoteName('workflow_id') . ' = :id', - $db->quoteName('default') . ' = 1', - ] - ) - ->bind(':id', $this->workflow_id, ParameterType::INTEGER); - - $id = $db->setQuery($query)->loadResult(); - - // If there is no default stage => set the current to default to recover - if (empty($id)) - { - $this->default = '1'; - } - // This stage is the default, but someone has tried to disable it => not allowed - elseif ($id === $this->id) - { - $this->setError(Text::_('COM_WORKFLOW_DISABLE_DEFAULT')); - - return false; - } - } - - return true; - } - - /** - * Overloaded store function - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return mixed False on failure, positive integer on success. - * - * @see Table::store() - * @since 4.0.0 - */ - public function store($updateNulls = true) - { - $table = new StageTable($this->getDbo()); - - if ($this->default == '1') - { - // Verify that the default is unique for this workflow - if ($table->load(array('default' => '1', 'workflow_id' => (int) $this->workflow_id))) - { - $table->default = 0; - $table->store(); - } - } - - return parent::store($updateNulls); - } - - /** - * Method to bind an associative array or object to the Table instance. - * This method only binds properties that are publicly accessible and optionally - * takes an array of properties to ignore when binding. - * - * @param array|object $src An associative array or object to bind to the Table instance. - * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean True on success. - * - * @since 4.0.0 - * @throws \InvalidArgumentException - */ - public function bind($src, $ignore = array()) - { - // Bind the rules. - if (isset($src['rules']) && \is_array($src['rules'])) - { - $rules = new Rules($src['rules']); - $this->setRules($rules); - } - - return parent::bind($src, $ignore); - } - - /** - * Method to compute the default name of the asset. - * The default name is in the form table_name.id - * where id is the value of the primary key of the table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetName() - { - $k = $this->_tbl_key; - $workflow = new WorkflowTable($this->getDbo()); - $workflow->load($this->workflow_id); - - $parts = explode('.', $workflow->extension); - - $extension = array_shift($parts); - - return $extension . '.stage.' . (int) $this->$k; - } - - /** - * Method to return the title to use for the asset table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetTitle() - { - return $this->title; - } - - /** - * Get the parent asset id for the record - * - * @param Table|null $table A Table object for the asset parent. - * @param integer|null $id The id for the asset - * - * @return integer The id of the asset's parent - * - * @since 4.0.0 - */ - protected function _getAssetParentId(Table $table = null, $id = null) - { - $asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo())); - - $workflow = new WorkflowTable($this->getDbo()); - $workflow->load($this->workflow_id); - - $parts = explode('.', $workflow->extension); - - $extension = array_shift($parts); - - $name = $extension . '.workflow.' . (int) $workflow->id; - - $asset->loadByName($name); - $assetId = $asset->id; - - return !empty($assetId) ? $assetId : parent::_getAssetParentId($table, $id); - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * @param DatabaseDriver $db Database connector object + * + * @since 4.0.0 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__workflow_stages', 'id', $db); + } + + /** + * Deletes workflow with transition and stages. + * + * @param int $pk Extension ids to delete. + * + * @return boolean True on success. + * + * @since 4.0.0 + * + * @throws \UnexpectedValueException + */ + public function delete($pk = null) + { + $db = $this->getDbo(); + $app = Factory::getApplication(); + $pk = (int) $pk; + + $query = $db->getQuery(true) + ->select($db->quoteName('default')) + ->from($db->quoteName('#__workflow_stages')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + $isDefault = $db->setQuery($query)->loadResult(); + + if ($isDefault) { + $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DELETE_IS_DEFAULT'), 'error'); + + return false; + } + + try { + $query = $db->getQuery(true) + ->delete($db->quoteName('#__workflow_transitions')) + ->where( + [ + $db->quoteName('to_stage_id') . ' = :idTo', + $db->quoteName('from_stage_id') . ' = :idFrom', + ], + 'OR' + ) + ->bind([':idTo', ':idFrom'], $pk, ParameterType::INTEGER); + + $db->setQuery($query)->execute(); + + return parent::delete($pk); + } catch (\RuntimeException $e) { + $app->enqueueMessage(Text::sprintf('COM_WORKFLOW_MSG_WORKFLOWS_DELETE_ERROR', $e->getMessage()), 'error'); + } + + return false; + } + + /** + * Overloaded check function + * + * @return boolean True on success + * + * @see Table::check() + * @since 4.0.0 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (trim($this->title) === '') { + $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_STATE')); + + return false; + } + + if (!empty($this->default)) { + if ((int) $this->published !== 1) { + $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); + + return false; + } + } else { + $db = $this->getDbo(); + $query = $db->getQuery(true); + + $query + ->select($db->quoteName('id')) + ->from($db->quoteName('#__workflow_stages')) + ->where( + [ + $db->quoteName('workflow_id') . ' = :id', + $db->quoteName('default') . ' = 1', + ] + ) + ->bind(':id', $this->workflow_id, ParameterType::INTEGER); + + $id = $db->setQuery($query)->loadResult(); + + // If there is no default stage => set the current to default to recover + if (empty($id)) { + $this->default = '1'; + } + // This stage is the default, but someone has tried to disable it => not allowed + elseif ($id === $this->id) { + $this->setError(Text::_('COM_WORKFLOW_DISABLE_DEFAULT')); + + return false; + } + } + + return true; + } + + /** + * Overloaded store function + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return mixed False on failure, positive integer on success. + * + * @see Table::store() + * @since 4.0.0 + */ + public function store($updateNulls = true) + { + $table = new StageTable($this->getDbo()); + + if ($this->default == '1') { + // Verify that the default is unique for this workflow + if ($table->load(array('default' => '1', 'workflow_id' => (int) $this->workflow_id))) { + $table->default = 0; + $table->store(); + } + } + + return parent::store($updateNulls); + } + + /** + * Method to bind an associative array or object to the Table instance. + * This method only binds properties that are publicly accessible and optionally + * takes an array of properties to ignore when binding. + * + * @param array|object $src An associative array or object to bind to the Table instance. + * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success. + * + * @since 4.0.0 + * @throws \InvalidArgumentException + */ + public function bind($src, $ignore = array()) + { + // Bind the rules. + if (isset($src['rules']) && \is_array($src['rules'])) { + $rules = new Rules($src['rules']); + $this->setRules($rules); + } + + return parent::bind($src, $ignore); + } + + /** + * Method to compute the default name of the asset. + * The default name is in the form table_name.id + * where id is the value of the primary key of the table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetName() + { + $k = $this->_tbl_key; + $workflow = new WorkflowTable($this->getDbo()); + $workflow->load($this->workflow_id); + + $parts = explode('.', $workflow->extension); + + $extension = array_shift($parts); + + return $extension . '.stage.' . (int) $this->$k; + } + + /** + * Method to return the title to use for the asset table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Get the parent asset id for the record + * + * @param Table|null $table A Table object for the asset parent. + * @param integer|null $id The id for the asset + * + * @return integer The id of the asset's parent + * + * @since 4.0.0 + */ + protected function _getAssetParentId(Table $table = null, $id = null) + { + $asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo())); + + $workflow = new WorkflowTable($this->getDbo()); + $workflow->load($this->workflow_id); + + $parts = explode('.', $workflow->extension); + + $extension = array_shift($parts); + + $name = $extension . '.workflow.' . (int) $workflow->id; + + $asset->loadByName($name); + $assetId = $asset->id; + + return !empty($assetId) ? $assetId : parent::_getAssetParentId($table, $id); + } } diff --git a/administrator/components/com_workflow/src/Table/TransitionTable.php b/administrator/components/com_workflow/src/Table/TransitionTable.php index 95b5b2de1ae8e..43775c0b63d6d 100644 --- a/administrator/components/com_workflow/src/Table/TransitionTable.php +++ b/administrator/components/com_workflow/src/Table/TransitionTable.php @@ -1,4 +1,5 @@ setRules($rules); - } - - return parent::bind($src, $ignore); - } - - /** - * Method to compute the default name of the asset. - * The default name is in the form table_name.id - * where id is the value of the primary key of the table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetName() - { - $k = $this->_tbl_key; - $workflow = new WorkflowTable($this->getDbo()); - $workflow->load($this->workflow_id); - - $parts = explode('.', $workflow->extension); - - $extension = array_shift($parts); - - return $extension . '.transition.' . (int) $this->$k; - } - - /** - * Method to return the title to use for the asset table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetTitle() - { - return $this->title; - } - - /** - * Get the parent asset id for the record - * - * @param Table $table A Table object for the asset parent. - * @param integer $id The id for the asset - * - * @return integer The id of the asset's parent - * - * @since 4.0.0 - */ - protected function _getAssetParentId(Table $table = null, $id = null) - { - $asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo())); - - $workflow = new WorkflowTable($this->getDbo()); - $workflow->load($this->workflow_id); - - $parts = explode('.', $workflow->extension); - - $extension = array_shift($parts); - - $name = $extension . '.workflow.' . (int) $workflow->id; - - $asset->loadByName($name); - $assetId = $asset->id; - - return !empty($assetId) ? $assetId : parent::_getAssetParentId($table, $id); - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * An array of key names to be json encoded in the bind function + * + * @var array + * + * @since 4.0.0 + */ + protected $_jsonEncode = [ + 'options' + ]; + + /** + * @param DatabaseDriver $db Database connector object + * + * @since 4.0.0 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__workflow_transitions', 'id', $db); + } + + /** + * Method to bind an associative array or object to the Table instance. + * This method only binds properties that are publicly accessible and optionally + * takes an array of properties to ignore when binding. + * + * @param array|object $src An associative array or object to bind to the Table instance. + * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success. + * + * @since 4.0.0 + * @throws \InvalidArgumentException + */ + public function bind($src, $ignore = array()) + { + // Bind the rules. + if (isset($src['rules']) && \is_array($src['rules'])) { + $rules = new Rules($src['rules']); + $this->setRules($rules); + } + + return parent::bind($src, $ignore); + } + + /** + * Method to compute the default name of the asset. + * The default name is in the form table_name.id + * where id is the value of the primary key of the table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetName() + { + $k = $this->_tbl_key; + $workflow = new WorkflowTable($this->getDbo()); + $workflow->load($this->workflow_id); + + $parts = explode('.', $workflow->extension); + + $extension = array_shift($parts); + + return $extension . '.transition.' . (int) $this->$k; + } + + /** + * Method to return the title to use for the asset table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Get the parent asset id for the record + * + * @param Table $table A Table object for the asset parent. + * @param integer $id The id for the asset + * + * @return integer The id of the asset's parent + * + * @since 4.0.0 + */ + protected function _getAssetParentId(Table $table = null, $id = null) + { + $asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo())); + + $workflow = new WorkflowTable($this->getDbo()); + $workflow->load($this->workflow_id); + + $parts = explode('.', $workflow->extension); + + $extension = array_shift($parts); + + $name = $extension . '.workflow.' . (int) $workflow->id; + + $asset->loadByName($name); + $assetId = $asset->id; + + return !empty($assetId) ? $assetId : parent::_getAssetParentId($table, $id); + } } diff --git a/administrator/components/com_workflow/src/Table/WorkflowTable.php b/administrator/components/com_workflow/src/Table/WorkflowTable.php index a9f7267bb39b7..1094ee48fe40f 100644 --- a/administrator/components/com_workflow/src/Table/WorkflowTable.php +++ b/administrator/components/com_workflow/src/Table/WorkflowTable.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Workflow\Administrator\Table; \defined('_JEXEC') or die; @@ -24,317 +26,291 @@ */ class WorkflowTable extends Table { - /** - * Indicates that columns fully support the NULL value in the database - * - * @var boolean - * - * @since 4.0.0 - */ - protected $_supportNullValue = true; - - /** - * @param DatabaseDriver $db Database connector object - * - * @since 4.0.0 - */ - public function __construct(DatabaseDriver $db) - { - $this->typeAlias = '{extension}.workflow'; - - parent::__construct('#__workflows', 'id', $db); - } - - /** - * Deletes workflow with transition and states. - * - * @param int $pk Extension ids to delete. - * - * @return boolean - * - * @since 4.0.0 - * - * @throws \Exception on ACL error - */ - public function delete($pk = null) - { - $db = $this->getDbo(); - $app = Factory::getApplication(); - $pk = (int) $pk; - - // Gets the workflow information that is going to be deleted. - $query = $db->getQuery(true) - ->select($db->quoteName('default')) - ->from($db->quoteName('#__workflows')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - $isDefault = $db->setQuery($query)->loadResult(); - - if ($isDefault) - { - $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DELETE_DEFAULT'), 'error'); - - return false; - } - - // Delete the workflow states, then transitions from all tables. - try - { - $query = $db->getQuery(true) - ->delete($db->quoteName('#__workflow_stages')) - ->where($db->quoteName('workflow_id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - $db->setQuery($query)->execute(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__workflow_transitions')) - ->where($db->quoteName('workflow_id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - $db->setQuery($query)->execute(); - - return parent::delete($pk); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage(Text::sprintf('COM_WORKFLOW_MSG_WORKFLOWS_DELETE_ERROR', $e->getMessage()), 'error'); - - return false; - } - } - - /** - * Overloaded check function - * - * @return boolean True on success - * - * @see Table::check() - * @since 4.0.0 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (trim($this->title) === '') - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_WORKFLOW')); - - return false; - } - - if (!empty($this->default)) - { - if ((int) $this->published !== 1) - { - $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); - - return false; - } - } - else - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query - ->select($db->quoteName('id')) - ->from($db->quoteName('#__workflows')) - ->where($db->quoteName('default') . ' = 1'); - - $id = $db->setQuery($query)->loadResult(); - - // If there is no default workflow => set the current to default to recover - if (empty($id)) - { - $this->default = '1'; - } - // This workflow is the default, but someone has tried to disable it => not allowed - elseif ($id === $this->id) - { - $this->setError(Text::_('COM_WORKFLOW_DISABLE_DEFAULT')); - - return false; - } - } - - return true; - } - - /** - * Overloaded store function - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return mixed False on failure, positive integer on success. - * - * @see Table::store() - * @since 4.0.0 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate(); - $user = Factory::getUser(); - - $table = new WorkflowTable($this->getDbo()); - - if ($this->id) - { - // Existing item - $this->modified_by = $user->id; - $this->modified = $date->toSql(); - } - else - { - $this->modified_by = 0; - } - - if (!(int) $this->created) - { - $this->created = $date->toSql(); - } - - if (empty($this->created_by)) - { - $this->created_by = $user->id; - } - - if (!(int) $this->modified) - { - $this->modified = $this->created; - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - - if ((int) $this->default === 1) - { - // Verify that the default is unique for this workflow - if ($table->load( - [ - 'default' => '1', - 'extension' => $this->extension - ] - )) - { - $table->default = 0; - $table->store(); - } - } - - return parent::store($updateNulls); - } - - /** - * Method to bind an associative array or object to the Table instance. - * This method only binds properties that are publicly accessible and optionally - * takes an array of properties to ignore when binding. - * - * @param array|object $src An associative array or object to bind to the Table instance. - * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean True on success. - * - * @since 4.0.0 - * @throws \InvalidArgumentException - */ - public function bind($src, $ignore = array()) - { - // Bind the rules. - if (isset($src['rules']) && \is_array($src['rules'])) - { - $rules = new Rules($src['rules']); - $this->setRules($rules); - } - - return parent::bind($src, $ignore); - } - - /** - * Method to compute the default name of the asset. - * The default name is in the form table_name.id - * where id is the value of the primary key of the table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetName() - { - $k = $this->_tbl_key; - - $parts = explode('.', $this->extension); - - $extension = array_shift($parts); - - return $extension . '.workflow.' . (int) $this->$k; - } - - /** - * Method to return the title to use for the asset table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetTitle() - { - return $this->title; - } - - /** - * Get the parent asset id for the record - * - * @param Table $table A Table object for the asset parent. - * @param integer $id The id for the asset - * - * @return integer The id of the asset's parent - * - * @since 4.0.0 - */ - protected function _getAssetParentId(Table $table = null, $id = null) - { - $assetId = null; - - $parts = explode('.', $this->extension); - - $extension = array_shift($parts); - - // Build the query to get the asset id for the parent category. - $query = $this->getDbo()->getQuery(true) - ->select($this->getDbo()->quoteName('id')) - ->from($this->getDbo()->quoteName('#__assets')) - ->where($this->getDbo()->quoteName('name') . ' = :extension') - ->bind(':extension', $extension); - - // Get the asset id from the database. - $this->getDbo()->setQuery($query); - - if ($result = $this->getDbo()->loadResult()) - { - $assetId = (int) $result; - } - - // Return the asset id. - if ($assetId) - { - return $assetId; - } - else - { - return parent::_getAssetParentId($table, $id); - } - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * @param DatabaseDriver $db Database connector object + * + * @since 4.0.0 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = '{extension}.workflow'; + + parent::__construct('#__workflows', 'id', $db); + } + + /** + * Deletes workflow with transition and states. + * + * @param int $pk Extension ids to delete. + * + * @return boolean + * + * @since 4.0.0 + * + * @throws \Exception on ACL error + */ + public function delete($pk = null) + { + $db = $this->getDbo(); + $app = Factory::getApplication(); + $pk = (int) $pk; + + // Gets the workflow information that is going to be deleted. + $query = $db->getQuery(true) + ->select($db->quoteName('default')) + ->from($db->quoteName('#__workflows')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + $isDefault = $db->setQuery($query)->loadResult(); + + if ($isDefault) { + $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DELETE_DEFAULT'), 'error'); + + return false; + } + + // Delete the workflow states, then transitions from all tables. + try { + $query = $db->getQuery(true) + ->delete($db->quoteName('#__workflow_stages')) + ->where($db->quoteName('workflow_id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + $db->setQuery($query)->execute(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__workflow_transitions')) + ->where($db->quoteName('workflow_id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + $db->setQuery($query)->execute(); + + return parent::delete($pk); + } catch (\RuntimeException $e) { + $app->enqueueMessage(Text::sprintf('COM_WORKFLOW_MSG_WORKFLOWS_DELETE_ERROR', $e->getMessage()), 'error'); + + return false; + } + } + + /** + * Overloaded check function + * + * @return boolean True on success + * + * @see Table::check() + * @since 4.0.0 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (trim($this->title) === '') { + $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_WORKFLOW')); + + return false; + } + + if (!empty($this->default)) { + if ((int) $this->published !== 1) { + $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); + + return false; + } + } else { + $db = $this->getDbo(); + $query = $db->getQuery(true); + + $query + ->select($db->quoteName('id')) + ->from($db->quoteName('#__workflows')) + ->where($db->quoteName('default') . ' = 1'); + + $id = $db->setQuery($query)->loadResult(); + + // If there is no default workflow => set the current to default to recover + if (empty($id)) { + $this->default = '1'; + } + // This workflow is the default, but someone has tried to disable it => not allowed + elseif ($id === $this->id) { + $this->setError(Text::_('COM_WORKFLOW_DISABLE_DEFAULT')); + + return false; + } + } + + return true; + } + + /** + * Overloaded store function + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return mixed False on failure, positive integer on success. + * + * @see Table::store() + * @since 4.0.0 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate(); + $user = Factory::getUser(); + + $table = new WorkflowTable($this->getDbo()); + + if ($this->id) { + // Existing item + $this->modified_by = $user->id; + $this->modified = $date->toSql(); + } else { + $this->modified_by = 0; + } + + if (!(int) $this->created) { + $this->created = $date->toSql(); + } + + if (empty($this->created_by)) { + $this->created_by = $user->id; + } + + if (!(int) $this->modified) { + $this->modified = $this->created; + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + + if ((int) $this->default === 1) { + // Verify that the default is unique for this workflow + if ( + $table->load( + [ + 'default' => '1', + 'extension' => $this->extension + ] + ) + ) { + $table->default = 0; + $table->store(); + } + } + + return parent::store($updateNulls); + } + + /** + * Method to bind an associative array or object to the Table instance. + * This method only binds properties that are publicly accessible and optionally + * takes an array of properties to ignore when binding. + * + * @param array|object $src An associative array or object to bind to the Table instance. + * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success. + * + * @since 4.0.0 + * @throws \InvalidArgumentException + */ + public function bind($src, $ignore = array()) + { + // Bind the rules. + if (isset($src['rules']) && \is_array($src['rules'])) { + $rules = new Rules($src['rules']); + $this->setRules($rules); + } + + return parent::bind($src, $ignore); + } + + /** + * Method to compute the default name of the asset. + * The default name is in the form table_name.id + * where id is the value of the primary key of the table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetName() + { + $k = $this->_tbl_key; + + $parts = explode('.', $this->extension); + + $extension = array_shift($parts); + + return $extension . '.workflow.' . (int) $this->$k; + } + + /** + * Method to return the title to use for the asset table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Get the parent asset id for the record + * + * @param Table $table A Table object for the asset parent. + * @param integer $id The id for the asset + * + * @return integer The id of the asset's parent + * + * @since 4.0.0 + */ + protected function _getAssetParentId(Table $table = null, $id = null) + { + $assetId = null; + + $parts = explode('.', $this->extension); + + $extension = array_shift($parts); + + // Build the query to get the asset id for the parent category. + $query = $this->getDbo()->getQuery(true) + ->select($this->getDbo()->quoteName('id')) + ->from($this->getDbo()->quoteName('#__assets')) + ->where($this->getDbo()->quoteName('name') . ' = :extension') + ->bind(':extension', $extension); + + // Get the asset id from the database. + $this->getDbo()->setQuery($query); + + if ($result = $this->getDbo()->loadResult()) { + $assetId = (int) $result; + } + + // Return the asset id. + if ($assetId) { + return $assetId; + } else { + return parent::_getAssetParentId($table, $id); + } + } } diff --git a/administrator/components/com_workflow/src/View/Stage/HtmlView.php b/administrator/components/com_workflow/src/View/Stage/HtmlView.php index 1c24c2cd7a3f1..dbb574edcb368 100644 --- a/administrator/components/com_workflow/src/View/Stage/HtmlView.php +++ b/administrator/components/com_workflow/src/View/Stage/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Workflow\Administrator\View\Stage; \defined('_JEXEC') or die; @@ -24,155 +26,147 @@ */ class HtmlView extends BaseHtmlView { - /** - * The model state - * - * @var object - * @since 4.0.0 - */ - protected $state; - - /** - * From object to generate fields - * - * @var \Joomla\CMS\Form\Form - * - * @since 4.0.0 - */ - protected $form; - - /** - * Items array - * - * @var object - * @since 4.0.0 - */ - protected $item; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display item view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - // Get the Data - $this->state = $this->get('State'); - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $extension = $this->state->get('filter.extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - // Set the toolbar - $this->addToolbar(); - - // Display the template - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $userId = $user->id; - $isNew = empty($this->item->id); - - $canDo = StageHelper::getActions($this->extension, 'stage', $this->item->id); - - ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_STAGE_ADD') : Text::_('COM_WORKFLOW_STAGE_EDIT'), 'address'); - - $toolbarButtons = []; - - if ($isNew) - { - // For new records, check the create permission. - if ($canDo->get('core.create')) - { - ToolbarHelper::apply('stage.apply'); - $toolbarButtons = [['save', 'stage.save'], ['save2new', 'stage.save2new']]; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel( - 'stage.cancel' - ); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - if ($itemEditable) - { - ToolbarHelper::apply('stage.apply'); - $toolbarButtons = [['save', 'stage.save']]; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'stage.save2new']; - $toolbarButtons[] = ['save2copy', 'stage.save2copy']; - } - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel( - 'stage.cancel', - 'JTOOLBAR_CLOSE' - ); - } - - ToolbarHelper::divider(); - } + /** + * The model state + * + * @var object + * @since 4.0.0 + */ + protected $state; + + /** + * From object to generate fields + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + protected $form; + + /** + * Items array + * + * @var object + * @since 4.0.0 + */ + protected $item; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display item view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + // Get the Data + $this->state = $this->get('State'); + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $extension = $this->state->get('filter.extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + // Set the toolbar + $this->addToolbar(); + + // Display the template + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = empty($this->item->id); + + $canDo = StageHelper::getActions($this->extension, 'stage', $this->item->id); + + ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_STAGE_ADD') : Text::_('COM_WORKFLOW_STAGE_EDIT'), 'address'); + + $toolbarButtons = []; + + if ($isNew) { + // For new records, check the create permission. + if ($canDo->get('core.create')) { + ToolbarHelper::apply('stage.apply'); + $toolbarButtons = [['save', 'stage.save'], ['save2new', 'stage.save2new']]; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel( + 'stage.cancel' + ); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + if ($itemEditable) { + ToolbarHelper::apply('stage.apply'); + $toolbarButtons = [['save', 'stage.save']]; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'stage.save2new']; + $toolbarButtons[] = ['save2copy', 'stage.save2copy']; + } + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel( + 'stage.cancel', + 'JTOOLBAR_CLOSE' + ); + } + + ToolbarHelper::divider(); + } } diff --git a/administrator/components/com_workflow/src/View/Stages/HtmlView.php b/administrator/components/com_workflow/src/View/Stages/HtmlView.php index 294f44037113b..d84af2f7ee400 100644 --- a/administrator/components/com_workflow/src/View/Stages/HtmlView.php +++ b/administrator/components/com_workflow/src/View/Stages/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Workflow\Administrator\View\Stages; \defined('_JEXEC') or die; @@ -27,198 +29,190 @@ */ class HtmlView extends BaseHtmlView { - /** - * An array of stages - * - * @var array - * @since 4.0.0 - */ - protected $stages; - - /** - * The model stage - * - * @var object - * @since 4.0.0 - */ - protected $stage; - - /** - * The HTML for displaying sidebar - * - * @var string - * @since 4.0.0 - */ - protected $sidebar; - - /** - * The pagination object - * - * @var \Joomla\CMS\Pagination\Pagination - * - * @since 4.0.0 - */ - protected $pagination; - - /** - * Form object for search filters - * - * @var \Joomla\CMS\Form\Form - * - * @since 4.0.0 - */ - public $filterForm; - - /** - * The active search filters - * - * @var array - * @since 4.0.0 - */ - public $activeFilters; - - /** - * The current workflow - * - * @var object - * @since 4.0.0 - */ - protected $workflow; - - /** - * The ID of current workflow - * - * @var integer - * @since 4.0.0 - */ - protected $workflowID; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display the view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - $this->state = $this->get('State'); - $this->stages = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->workflow = $this->get('Workflow'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->workflowID = $this->workflow->id; - - $parts = explode('.', $this->workflow->extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (!empty($this->stages)) - { - $extension = Factory::getApplication()->input->getCmd('extension'); - $workflow = new Workflow($extension); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions($this->extension, 'workflow', $this->workflowID); - - $user = $this->getCurrentUser(); - - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::sprintf('COM_WORKFLOW_STAGES_LIST', Text::_($this->state->get('active_workflow', ''))), 'address contact'); - - $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; - - ToolbarHelper::link( - Route::_('index.php?option=com_workflow&view=workflows&extension=' . $this->escape($this->workflow->extension)), - 'JTOOLBAR_BACK', - $arrow - ); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('stage.add'); - } - - if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('stages.publish', 'JTOOLBAR_ENABLE')->listCheck(true); - $childBar->unpublish('stages.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); - $childBar->makeDefault('stages.setDefault', 'COM_WORKFLOW_TOOLBAR_DEFAULT'); - - if ($canDo->get('core.admin')) - { - $childBar->checkin('stages.checkin')->listCheck(true); - } - - if ($this->state->get('filter.published') !== '-2') - { - $childBar->trash('stages.trash'); - } - } - - if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) - { - $toolbar->delete('stages.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - $toolbar->help('Stages_List:_Basic_Workflow'); - } + /** + * An array of stages + * + * @var array + * @since 4.0.0 + */ + protected $stages; + + /** + * The model stage + * + * @var object + * @since 4.0.0 + */ + protected $stage; + + /** + * The HTML for displaying sidebar + * + * @var string + * @since 4.0.0 + */ + protected $sidebar; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 4.0.0 + */ + protected $pagination; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * The current workflow + * + * @var object + * @since 4.0.0 + */ + protected $workflow; + + /** + * The ID of current workflow + * + * @var integer + * @since 4.0.0 + */ + protected $workflowID; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->stages = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->workflow = $this->get('Workflow'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->workflowID = $this->workflow->id; + + $parts = explode('.', $this->workflow->extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (!empty($this->stages)) { + $extension = Factory::getApplication()->input->getCmd('extension'); + $workflow = new Workflow($extension); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions($this->extension, 'workflow', $this->workflowID); + + $user = $this->getCurrentUser(); + + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::sprintf('COM_WORKFLOW_STAGES_LIST', Text::_($this->state->get('active_workflow', ''))), 'address contact'); + + $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + + ToolbarHelper::link( + Route::_('index.php?option=com_workflow&view=workflows&extension=' . $this->escape($this->workflow->extension)), + 'JTOOLBAR_BACK', + $arrow + ); + + if ($canDo->get('core.create')) { + $toolbar->addNew('stage.add'); + } + + if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('stages.publish', 'JTOOLBAR_ENABLE')->listCheck(true); + $childBar->unpublish('stages.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); + $childBar->makeDefault('stages.setDefault', 'COM_WORKFLOW_TOOLBAR_DEFAULT'); + + if ($canDo->get('core.admin')) { + $childBar->checkin('stages.checkin')->listCheck(true); + } + + if ($this->state->get('filter.published') !== '-2') { + $childBar->trash('stages.trash'); + } + } + + if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) { + $toolbar->delete('stages.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + $toolbar->help('Stages_List:_Basic_Workflow'); + } } diff --git a/administrator/components/com_workflow/src/View/Transition/HtmlView.php b/administrator/components/com_workflow/src/View/Transition/HtmlView.php index 52d9112090361..308d68fce0e67 100644 --- a/administrator/components/com_workflow/src/View/Transition/HtmlView.php +++ b/administrator/components/com_workflow/src/View/Transition/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Workflow\Administrator\View\Transition; \defined('_JEXEC') or die; @@ -24,194 +26,183 @@ */ class HtmlView extends BaseHtmlView { - /** - * The model state - * - * @var object - * @since 4.0.0 - */ - protected $state; - - /** - * Form object to generate fields - * - * @var \Joomla\CMS\Form\Form - * - * @since 4.0.0 - */ - protected $form; - - /** - * Items array - * - * @var object - * @since 4.0.0 - */ - protected $item; - - /** - * That is object of Application - * - * @var \Joomla\CMS\Application\CMSApplication - * @since 4.0.0 - */ - protected $app; - - /** - * The application input object. - * - * @var \Joomla\CMS\Input\Input - * @since 4.0.0 - */ - protected $input; - - /** - * The ID of current workflow - * - * @var integer - * @since 4.0.0 - */ - protected $workflowID; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display item view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - $this->app = Factory::getApplication(); - $this->input = $this->app->input; - - // Get the Data - $this->state = $this->get('State'); - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $extension = $this->state->get('filter.extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - // Get the ID of workflow - $this->workflowID = $this->input->getCmd("workflow_id"); - - // Set the toolbar - $this->addToolbar(); - - // Display the template - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $userId = $user->id; - $isNew = empty($this->item->id); - - $canDo = StageHelper::getActions($this->extension, 'transition', $this->item->id); - - ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_TRANSITION_ADD') : Text::_('COM_WORKFLOW_TRANSITION_EDIT'), 'address'); - - $toolbarButtons = []; - - $canCreate = $canDo->get('core.create'); - - if ($isNew) - { - // For new records, check the create permission. - if ($canCreate) - { - ToolbarHelper::apply('transition.apply'); - $toolbarButtons = [['save', 'transition.save'], ['save2new', 'transition.save2new']]; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel( - 'transition.cancel' - ); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - if ($itemEditable) - { - ToolbarHelper::apply('transition.apply'); - $toolbarButtons[] = ['save', 'transition.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canCreate) - { - $toolbarButtons[] = ['save2new', 'transition.save2new']; - $toolbarButtons[] = ['save2copy', 'transition.save2copy']; - } - } - - if (count($toolbarButtons) > 1) - { - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - } - else - { - ToolbarHelper::save('transition.save'); - } - - ToolbarHelper::cancel( - 'transition.cancel', - 'JTOOLBAR_CLOSE' - ); - } - - ToolbarHelper::divider(); - } + /** + * The model state + * + * @var object + * @since 4.0.0 + */ + protected $state; + + /** + * Form object to generate fields + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + protected $form; + + /** + * Items array + * + * @var object + * @since 4.0.0 + */ + protected $item; + + /** + * That is object of Application + * + * @var \Joomla\CMS\Application\CMSApplication + * @since 4.0.0 + */ + protected $app; + + /** + * The application input object. + * + * @var \Joomla\CMS\Input\Input + * @since 4.0.0 + */ + protected $input; + + /** + * The ID of current workflow + * + * @var integer + * @since 4.0.0 + */ + protected $workflowID; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display item view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->app = Factory::getApplication(); + $this->input = $this->app->input; + + // Get the Data + $this->state = $this->get('State'); + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $extension = $this->state->get('filter.extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + // Get the ID of workflow + $this->workflowID = $this->input->getCmd("workflow_id"); + + // Set the toolbar + $this->addToolbar(); + + // Display the template + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = empty($this->item->id); + + $canDo = StageHelper::getActions($this->extension, 'transition', $this->item->id); + + ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_TRANSITION_ADD') : Text::_('COM_WORKFLOW_TRANSITION_EDIT'), 'address'); + + $toolbarButtons = []; + + $canCreate = $canDo->get('core.create'); + + if ($isNew) { + // For new records, check the create permission. + if ($canCreate) { + ToolbarHelper::apply('transition.apply'); + $toolbarButtons = [['save', 'transition.save'], ['save2new', 'transition.save2new']]; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel( + 'transition.cancel' + ); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + if ($itemEditable) { + ToolbarHelper::apply('transition.apply'); + $toolbarButtons[] = ['save', 'transition.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canCreate) { + $toolbarButtons[] = ['save2new', 'transition.save2new']; + $toolbarButtons[] = ['save2copy', 'transition.save2copy']; + } + } + + if (count($toolbarButtons) > 1) { + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + } else { + ToolbarHelper::save('transition.save'); + } + + ToolbarHelper::cancel( + 'transition.cancel', + 'JTOOLBAR_CLOSE' + ); + } + + ToolbarHelper::divider(); + } } diff --git a/administrator/components/com_workflow/src/View/Transitions/HtmlView.php b/administrator/components/com_workflow/src/View/Transitions/HtmlView.php index 50653c395630e..b1bbef1d63769 100644 --- a/administrator/components/com_workflow/src/View/Transitions/HtmlView.php +++ b/administrator/components/com_workflow/src/View/Transitions/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Workflow\Administrator\View\Transitions; \defined('_JEXEC') or die; @@ -26,191 +28,184 @@ */ class HtmlView extends BaseHtmlView { - /** - * An array of transitions - * - * @var array - * @since 4.0.0 - */ - protected $transitions; - - /** - * The model state - * - * @var object - * @since 4.0.0 - */ - protected $state; - - /** - * The HTML for displaying sidebar - * - * @var string - * @since 4.0.0 - */ - protected $sidebar; - - /** - * The pagination object - * - * @var \Joomla\CMS\Pagination\Pagination - * - * @since 4.0.0 - */ - protected $pagination; - - /** - * Form object for search filters - * - * @var \Joomla\CMS\Form\Form - * - * @since 4.0.0 - */ - public $filterForm; - - /** - * The active search filters - * - * @var array - * @since 4.0.0 - */ - public $activeFilters; - - /** - * The current workflow - * - * @var object - * @since 4.0.0 - */ - protected $workflow; - - /** - * The ID of current workflow - * - * @var integer - * @since 4.0.0 - */ - protected $workflowID; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display the view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - $this->state = $this->get('State'); - $this->transitions = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->workflow = $this->get('Workflow'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->workflowID = $this->workflow->id; - - $parts = explode('.', $this->workflow->extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions($this->extension, 'workflow', $this->workflowID); - - $user = $this->getCurrentUser(); - - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::sprintf('COM_WORKFLOW_TRANSITIONS_LIST', Text::_($this->state->get('active_workflow'))), 'address contact'); - - $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; - - ToolbarHelper::link( - Route::_('index.php?option=com_workflow&view=workflows&extension=' . $this->escape($this->workflow->extension)), - 'JTOOLBAR_BACK', - $arrow - ); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('transition.add'); - } - - if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('transitions.publish', 'JTOOLBAR_ENABLE'); - $childBar->unpublish('transitions.unpublish', 'JTOOLBAR_DISABLE'); - - if ($canDo->get('core.admin')) - { - $childBar->checkin('transitions.checkin')->listCheck(true); - } - - if ($this->state->get('filter.published') !== '-2') - { - $childBar->trash('transitions.trash'); - } - } - - if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) - { - $toolbar->delete('transitions.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - $toolbar->help('Transitions_List:_Basic_Workflow'); - } + /** + * An array of transitions + * + * @var array + * @since 4.0.0 + */ + protected $transitions; + + /** + * The model state + * + * @var object + * @since 4.0.0 + */ + protected $state; + + /** + * The HTML for displaying sidebar + * + * @var string + * @since 4.0.0 + */ + protected $sidebar; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 4.0.0 + */ + protected $pagination; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * The current workflow + * + * @var object + * @since 4.0.0 + */ + protected $workflow; + + /** + * The ID of current workflow + * + * @var integer + * @since 4.0.0 + */ + protected $workflowID; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->transitions = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->workflow = $this->get('Workflow'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->workflowID = $this->workflow->id; + + $parts = explode('.', $this->workflow->extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions($this->extension, 'workflow', $this->workflowID); + + $user = $this->getCurrentUser(); + + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::sprintf('COM_WORKFLOW_TRANSITIONS_LIST', Text::_($this->state->get('active_workflow'))), 'address contact'); + + $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + + ToolbarHelper::link( + Route::_('index.php?option=com_workflow&view=workflows&extension=' . $this->escape($this->workflow->extension)), + 'JTOOLBAR_BACK', + $arrow + ); + + if ($canDo->get('core.create')) { + $toolbar->addNew('transition.add'); + } + + if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('transitions.publish', 'JTOOLBAR_ENABLE'); + $childBar->unpublish('transitions.unpublish', 'JTOOLBAR_DISABLE'); + + if ($canDo->get('core.admin')) { + $childBar->checkin('transitions.checkin')->listCheck(true); + } + + if ($this->state->get('filter.published') !== '-2') { + $childBar->trash('transitions.trash'); + } + } + + if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) { + $toolbar->delete('transitions.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + $toolbar->help('Transitions_List:_Basic_Workflow'); + } } diff --git a/administrator/components/com_workflow/src/View/Workflow/HtmlView.php b/administrator/components/com_workflow/src/View/Workflow/HtmlView.php index 0bc91d5cfe0cb..50d3300760ead 100644 --- a/administrator/components/com_workflow/src/View/Workflow/HtmlView.php +++ b/administrator/components/com_workflow/src/View/Workflow/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Workflow\Administrator\View\Workflow; \defined('_JEXEC') or die; @@ -24,158 +26,150 @@ */ class HtmlView extends BaseHtmlView { - /** - * The model state - * - * @var object - * @since 4.0.0 - */ - protected $state; - - /** - * The Form object - * - * @var \Joomla\CMS\Form\Form - */ - protected $form; - - /** - * The active item - * - * @var object - */ - protected $item; - - /** - * The ID of current workflow - * - * @var integer - * @since 4.0.0 - */ - protected $workflowID; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display item view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - // Get the Data - $this->state = $this->get('State'); - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $extension = $this->state->get('filter.extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - // Set the toolbar - $this->addToolbar(); - - // Display the template - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $userId = $user->id; - $isNew = empty($this->item->id); - - $canDo = WorkflowHelper::getActions($this->extension, 'workflow', $this->item->id); - - ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_WORKFLOWS_ADD') : Text::_('COM_WORKFLOW_WORKFLOWS_EDIT'), 'address'); - - $toolbarButtons = []; - - if ($isNew) - { - // For new records, check the create permission. - if ($canDo->get('core.create')) - { - ToolbarHelper::apply('workflow.apply'); - $toolbarButtons = [['save', 'workflow.save'], ['save2new', 'workflow.save2new']]; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel( - 'workflow.cancel' - ); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - if ($itemEditable) - { - ToolbarHelper::apply('workflow.apply'); - $toolbarButtons = [['save', 'workflow.save']]; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'workflow.save2new']; - $toolbarButtons[] = ['save2copy', 'workflow.save2copy']; - } - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel( - 'workflow.cancel', - 'JTOOLBAR_CLOSE' - ); - } - } + /** + * The model state + * + * @var object + * @since 4.0.0 + */ + protected $state; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The ID of current workflow + * + * @var integer + * @since 4.0.0 + */ + protected $workflowID; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display item view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + // Get the Data + $this->state = $this->get('State'); + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $extension = $this->state->get('filter.extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + // Set the toolbar + $this->addToolbar(); + + // Display the template + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = empty($this->item->id); + + $canDo = WorkflowHelper::getActions($this->extension, 'workflow', $this->item->id); + + ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_WORKFLOWS_ADD') : Text::_('COM_WORKFLOW_WORKFLOWS_EDIT'), 'address'); + + $toolbarButtons = []; + + if ($isNew) { + // For new records, check the create permission. + if ($canDo->get('core.create')) { + ToolbarHelper::apply('workflow.apply'); + $toolbarButtons = [['save', 'workflow.save'], ['save2new', 'workflow.save2new']]; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel( + 'workflow.cancel' + ); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + if ($itemEditable) { + ToolbarHelper::apply('workflow.apply'); + $toolbarButtons = [['save', 'workflow.save']]; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'workflow.save2new']; + $toolbarButtons[] = ['save2copy', 'workflow.save2copy']; + } + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel( + 'workflow.cancel', + 'JTOOLBAR_CLOSE' + ); + } + } } diff --git a/administrator/components/com_workflow/src/View/Workflows/HtmlView.php b/administrator/components/com_workflow/src/View/Workflows/HtmlView.php index c7732cce4f66b..a6e0cb67dc968 100644 --- a/administrator/components/com_workflow/src/View/Workflows/HtmlView.php +++ b/administrator/components/com_workflow/src/View/Workflows/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Workflow\Administrator\View\Workflows; \defined('_JEXEC') or die; @@ -25,171 +27,163 @@ */ class HtmlView extends BaseHtmlView { - /** - * An array of workflows - * - * @var array - * @since 4.0.0 - */ - protected $workflows; - - /** - * The model state - * - * @var object - * @since 4.0.0 - */ - protected $state; - - /** - * The pagination object - * - * @var \Joomla\CMS\Pagination\Pagination - * @since 4.0.0 - */ - protected $pagination; - - /** - * The HTML for displaying sidebar - * - * @var string - * @since 4.0.0 - */ - protected $sidebar; - - /** - * Form object for search filters - * - * @var \Joomla\CMS\Form\Form - * @since 4.0.0 - */ - public $filterForm; - - /** - * The active search filters - * - * @var array - * @since 4.0.0 - */ - public $activeFilters; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display the view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - $this->state = $this->get('State'); - $this->workflows = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $extension = $this->state->get('filter.extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions($this->extension, $this->section); - - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_WORKFLOW_WORKFLOWS_LIST'), 'file-alt contact'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('workflow.add'); - } - - if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('workflows.publish', 'JTOOLBAR_ENABLE'); - $childBar->unpublish('workflows.unpublish', 'JTOOLBAR_DISABLE'); - $childBar->makeDefault('workflows.setDefault', 'COM_WORKFLOW_TOOLBAR_DEFAULT'); - - if ($canDo->get('core.admin')) - { - $childBar->checkin('workflows.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) - { - $childBar->trash('workflows.trash'); - } - } - - if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) - { - $toolbar->delete('workflows.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences($this->extension); - } - - $toolbar->help('Workflows_List'); - } + /** + * An array of workflows + * + * @var array + * @since 4.0.0 + */ + protected $workflows; + + /** + * The model state + * + * @var object + * @since 4.0.0 + */ + protected $state; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 4.0.0 + */ + protected $pagination; + + /** + * The HTML for displaying sidebar + * + * @var string + * @since 4.0.0 + */ + protected $sidebar; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->workflows = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $extension = $this->state->get('filter.extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions($this->extension, $this->section); + + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_WORKFLOW_WORKFLOWS_LIST'), 'file-alt contact'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('workflow.add'); + } + + if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('workflows.publish', 'JTOOLBAR_ENABLE'); + $childBar->unpublish('workflows.unpublish', 'JTOOLBAR_DISABLE'); + $childBar->makeDefault('workflows.setDefault', 'COM_WORKFLOW_TOOLBAR_DEFAULT'); + + if ($canDo->get('core.admin')) { + $childBar->checkin('workflows.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) { + $childBar->trash('workflows.trash'); + } + } + + if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) { + $toolbar->delete('workflows.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences($this->extension); + } + + $toolbar->help('Workflows_List'); + } } diff --git a/administrator/components/com_workflow/tmpl/stage/edit.php b/administrator/components/com_workflow/tmpl/stage/edit.php index b6f53c27fbb9d..24a89fecbb458 100644 --- a/administrator/components/com_workflow/tmpl/stage/edit.php +++ b/administrator/components/com_workflow/tmpl/stage/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $user = $app->getIdentity(); @@ -35,54 +36,54 @@
    - + - - item->id != 0) : ?> -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - + + item->id != 0) : ?> +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + -
    - 'details', 'recall' => true, 'breakpoint' => 768]); ?> +
    + 'details', 'recall' => true, 'breakpoint' => 768]); ?> - -
    -
    - form->renderField('description'); ?> -
    -
    -
    - form->renderField('published'); ?> - form->renderField('default'); ?> -
    -
    -
    - + +
    +
    + form->renderField('description'); ?> +
    +
    +
    + form->renderField('published'); ?> + form->renderField('default'); ?> +
    +
    +
    + - authorise('core.admin', $this->extension)) : ?> - -
    - - form->getInput('rules'); ?> -
    - - + authorise('core.admin', $this->extension)) : ?> + +
    + + form->getInput('rules'); ?> +
    + + - + - form->getInput('workflow_id'); ?> - - -
    + form->getInput('workflow_id'); ?> + + +
    diff --git a/administrator/components/com_workflow/tmpl/stages/default.php b/administrator/components/com_workflow/tmpl/stages/default.php index 48c4a7969a085..d7de9de7c323b 100644 --- a/administrator/components/com_workflow/tmpl/stages/default.php +++ b/administrator/components/com_workflow/tmpl/stages/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $userId = $user->id; @@ -30,128 +32,128 @@ $saveOrder = ($listOrder == 's.ordering'); -if ($saveOrder) -{ - $saveOrderingUrl = 'index.php?option=com_workflow&task=stages.saveOrderAjax&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->extension) . '&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder) { + $saveOrderingUrl = 'index.php?option=com_workflow&task=stages.saveOrderAjax&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->extension) . '&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
    -
    - sidebar)) : ?> -
    - sidebar; ?> -
    - -
    -
    - $this)); - ?> - stages)) : ?> -
    - - -
    - - - - - - - - - - - - - - - stages as $i => $item): - $edit = Route::_('index.php?option=com_workflow&task=stage.edit&id=' . $item->id . '&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->extension); +
    + sidebar)) : ?> +
    + sidebar; ?> +
    + +
    +
    + $this)); + ?> + stages)) : ?> +
    + + +
    + +
    - , - , - -
    - - - - - - - - - - - -
    + + + + + + + + + + + + + stages as $i => $item) : + $edit = Route::_('index.php?option=com_workflow&task=stage.edit&id=' . $item->id . '&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->extension); - $canEdit = $user->authorise('core.edit', $this->extension . '.stage.' . $item->id); - $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $userId || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', $this->extension . '.stage.' . $item->id) && $canCheckin; + $canEdit = $user->authorise('core.edit', $this->extension . '.stage.' . $item->id); + $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $userId || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', $this->extension . '.stage.' . $item->id) && $canCheckin; - ?> - - - - - - - - - - -
    + , + , + +
    + + + + + + + + + + + +
    - id, false, 'cid', 'cb', Text::_($item->title)); ?> - - - - - - - - - - published, $i, 'stages.', $canChange); ?> - - default, $i, 'stages.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'stages.', $canCheckin); ?> - - - - escape(Text::_($item->title)); ?> - -
    escape(Text::_($item->description)); ?>
    - - escape(Text::_($item->title)); ?> -
    escape(Text::_($item->description)); ?>
    - -
    - id; ?> -
    - - pagination->getListFooter(); ?> + ?> + + + id, false, 'cid', 'cb', Text::_($item->title)); ?> + + + + + + + + + + + + published, $i, 'stages.', $canChange); ?> + + + default, $i, 'stages.', $canChange); ?> + + + checked_out) : ?> + editor, $item->checked_out_time, 'stages.', $canCheckin); ?> + + + + escape(Text::_($item->title)); ?> + +
    escape(Text::_($item->description)); ?>
    + + escape(Text::_($item->title)); ?> +
    escape(Text::_($item->description)); ?>
    + + + + id; ?> + + + + + + + pagination->getListFooter(); ?> - - - - - - -
    -
    -
    + + + + + + + + +
    diff --git a/administrator/components/com_workflow/tmpl/transition/edit.php b/administrator/components/com_workflow/tmpl/transition/edit.php index aaefb2ff14182..ee1f49c0be8fe 100644 --- a/administrator/components/com_workflow/tmpl/transition/edit.php +++ b/administrator/components/com_workflow/tmpl/transition/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $user = $app->getIdentity(); @@ -34,38 +35,37 @@ ?>
    - -
    - 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - -
    -
    - form->renderField('from_stage_id'); ?> - form->renderField('to_stage_id'); ?> - form->renderField('description'); ?> -
    -
    - -
    -
    - + +
    + 'details', 'recall' => true, 'breakpoint' => 768]); ?> - + +
    +
    + form->renderField('from_stage_id'); ?> + form->renderField('to_stage_id'); ?> + form->renderField('description'); ?> +
    +
    + +
    +
    + - authorise('core.admin', $this->extension)) : ?> + - -
    - - form->getInput('rules'); ?> -
    - - + authorise('core.admin', $this->extension)) : ?> + +
    + + form->getInput('rules'); ?> +
    + + - -
    - form->getInput('workflow_id'); ?> - - + +
    + form->getInput('workflow_id'); ?> + +
    diff --git a/administrator/components/com_workflow/tmpl/transitions/default.php b/administrator/components/com_workflow/tmpl/transitions/default.php index 6cc6eee815d58..97dee3f5c4a84 100644 --- a/administrator/components/com_workflow/tmpl/transitions/default.php +++ b/administrator/components/com_workflow/tmpl/transitions/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); -$user = Factory::getUser(); +$user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); @@ -29,136 +31,136 @@ $saveOrder = ($listOrder == 't.ordering'); -if ($saveOrder) -{ - $saveOrderingUrl = 'index.php?option=com_workflow&task=transitions.saveOrderAjax&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->workflow->extension) . '&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder) { + $saveOrderingUrl = 'index.php?option=com_workflow&task=transitions.saveOrderAjax&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->workflow->extension) . '&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
    -
    - sidebar)) : ?> -
    - sidebar; ?> -
    - -
    -
    - $this)); - ?> - transitions)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - transitions as $i => $item): - $edit = Route::_('index.php?option=com_workflow&task=transition.edit&id=' . $item->id . '&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->workflow->extension)); +
    + sidebar)) : ?> +
    + sidebar; ?> +
    + +
    +
    + $this)); + ?> + transitions)) : ?> +
    + + +
    + +
    - , - , - -
    - - - - - - - - - - - - - -
    + + + + + + + + + + + + + + transitions as $i => $item) : + $edit = Route::_('index.php?option=com_workflow&task=transition.edit&id=' . $item->id . '&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->workflow->extension)); - $canEdit = $user->authorise('core.edit', $this->extension . '.transition.' . $item->id); - $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $user->id || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', $this->extension . '.transition.' . $item->id) && $canCheckin; - ?> - - - - - - - - - - - -
    + , + , + +
    + + + + + + + + + + + + + +
    - id, false, 'cid', 'cb', Text::_($item->title)); ?> - - - - - - - - - - published, $i, 'transitions.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'transitions.', $canCheckin); ?> - - - - escape(Text::_($item->title)); ?> - -
    escape(Text::_($item->description)); ?>
    - - escape(Text::_($item->title)); ?> -
    escape(Text::_($item->description)); ?>
    - -
    - from_stage_id < 0): ?> - - - escape(Text::_($item->from_stage)); ?> - - - escape(Text::_($item->to_stage)); ?> - - id; ?> -
    - - pagination->getListFooter(); ?> - - - - - - -
    -
    -
    + $canEdit = $user->authorise('core.edit', $this->extension . '.transition.' . $item->id); + $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $user->id || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', $this->extension . '.transition.' . $item->id) && $canCheckin; + ?> + + + id, false, 'cid', 'cb', Text::_($item->title)); ?> + + + + + + + + + + + + published, $i, 'transitions.', $canChange); ?> + + + checked_out) : ?> + editor, $item->checked_out_time, 'transitions.', $canCheckin); ?> + + + + escape(Text::_($item->title)); ?> + +
    escape(Text::_($item->description)); ?>
    + + escape(Text::_($item->title)); ?> +
    escape(Text::_($item->description)); ?>
    + + + + from_stage_id < 0) : ?> + + + escape(Text::_($item->from_stage)); ?> + + + + escape(Text::_($item->to_stage)); ?> + + + id; ?> + + + + + + + pagination->getListFooter(); ?> + + + + + + + + +
    diff --git a/administrator/components/com_workflow/tmpl/workflow/edit.php b/administrator/components/com_workflow/tmpl/workflow/edit.php index 32725ab18acd8..055056043752a 100644 --- a/administrator/components/com_workflow/tmpl/workflow/edit.php +++ b/administrator/components/com_workflow/tmpl/workflow/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $user = $app->getIdentity(); @@ -34,57 +35,56 @@
    - - - - item->id != 0) : ?> -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - + -
    - 'general', 'recall' => true, 'breakpoint' => 768]); ?> + + item->id != 0) : ?> +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + - -
    -
    -
    - form->renderField('description'); ?> -
    -
    -
    -
    - form->renderField('published'); ?> - form->renderField('default'); ?> -
    -
    -
    - +
    + 'general', 'recall' => true, 'breakpoint' => 768]); ?> - authorise('core.admin', $this->extension)) : ?> + +
    +
    +
    + form->renderField('description'); ?> +
    +
    +
    +
    + form->renderField('published'); ?> + form->renderField('default'); ?> +
    +
    +
    + - -
    - - form->getInput('rules'); ?> -
    - + authorise('core.admin', $this->extension)) : ?> + +
    + + form->getInput('rules'); ?> +
    + - + - -
    - form->getInput('extension'); ?> - - + +
    + form->getInput('extension'); ?> + +
    diff --git a/administrator/components/com_workflow/tmpl/workflows/default.php b/administrator/components/com_workflow/tmpl/workflows/default.php index 449445fae312d..5da642bbe09a3 100644 --- a/administrator/components/com_workflow/tmpl/workflows/default.php +++ b/administrator/components/com_workflow/tmpl/workflows/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); @@ -29,15 +31,13 @@ $orderingColumn = 'created'; $saveOrderingUrl = ''; -if (strpos($listOrder, 'modified') !== false) -{ - $orderingColumn = 'modified'; +if (strpos($listOrder, 'modified') !== false) { + $orderingColumn = 'modified'; } -if ($saveOrder) -{ - $saveOrderingUrl = 'index.php?option=com_workflow&task=workflows.saveOrderAjax&tmpl=component&extension=' . $this->escape($this->extension) . '&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder) { + $saveOrderingUrl = 'index.php?option=com_workflow&task=workflows.saveOrderAjax&tmpl=component&extension=' . $this->escape($this->extension) . '&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $extension = $this->escape($this->state->get('filter.extension')); @@ -46,144 +46,147 @@ $userId = $user->id; ?>
    -
    - sidebar)) : ?> -
    - sidebar; ?> -
    - -
    -
    - $this, 'options' => array('selectorFieldName' => 'extension'))); - ?> - workflows)) : ?> -
    - - -
    - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="false"> - workflows as $i => $item): - $states = Route::_('index.php?option=com_workflow&view=stages&workflow_id=' . $item->id . '&extension=' . $extension); - $transitions = Route::_('index.php?option=com_workflow&view=transitions&workflow_id=' . $item->id . '&extension=' . $extension); - $edit = Route::_('index.php?option=com_workflow&task=workflow.edit&id=' . $item->id . '&extension=' . $extension); +
    + sidebar)) : ?> +
    + sidebar; ?> +
    + +
    +
    + $this, 'options' => array('selectorFieldName' => 'extension'))); + ?> + workflows)) : ?> +
    + + +
    + +
    - , - , - -
    - - - - - - - - - - - - - - - -
    + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="false"> + workflows as $i => $item) : + $states = Route::_('index.php?option=com_workflow&view=stages&workflow_id=' . $item->id . '&extension=' . $extension); + $transitions = Route::_('index.php?option=com_workflow&view=transitions&workflow_id=' . $item->id . '&extension=' . $extension); + $edit = Route::_('index.php?option=com_workflow&task=workflow.edit&id=' . $item->id . '&extension=' . $extension); - $canEdit = $user->authorise('core.edit', $extension . '.workflow.' . $item->id); - $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $userId || is_null($item->checked_out); - $canEditOwn = $user->authorise('core.edit.own', $extension . '.workflow.' . $item->id) && $item->created_by == $userId; - $canChange = $user->authorise('core.edit.state', $extension . '.workflow.' . $item->id) && $canCheckin; - ?> - - - - - - - - - - - -
    + , + , + +
    + + + + + + + + + + + + + + + +
    - id, false, 'cid', 'cb', Text::_($item->title)); ?> - - - - - - - - - - published, $i, 'workflows.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'workflows.', $canCheckin); ?> - - - - escape(Text::_($item->title)); ?> - -
    description; ?>
    - - escape(Text::_($item->title)); ?> -
    description; ?>
    - -
    - default, $i, 'workflows.', $canChange); ?> - - - count_states; ?> - - - - - count_transitions; ?> - - - - id; ?> -
    - - pagination->getListFooter(); ?> + $canEdit = $user->authorise('core.edit', $extension . '.workflow.' . $item->id); + $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $userId || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', $extension . '.workflow.' . $item->id) && $item->created_by == $userId; + $canChange = $user->authorise('core.edit.state', $extension . '.workflow.' . $item->id) && $canCheckin; + ?> + + + id, false, 'cid', 'cb', Text::_($item->title)); ?> + + + + + + + + + + + + published, $i, 'workflows.', $canChange); ?> + + + checked_out) : ?> + editor, $item->checked_out_time, 'workflows.', $canCheckin); ?> + + + + escape(Text::_($item->title)); ?> + +
    description; ?>
    + + escape(Text::_($item->title)); ?> +
    description; ?>
    + + + + default, $i, 'workflows.', $canChange); ?> + + + + count_states; ?> + + + + + + count_transitions; ?> + + + + + id; ?> + + + + + + pagination->getListFooter(); ?> - - - - -
    -
    -
    + + + + + + +
    diff --git a/administrator/components/com_wrapper/services/provider.php b/administrator/components/com_wrapper/services/provider.php index 0b52fecb1e1dc..cf2b51358aa72 100644 --- a/administrator/components/com_wrapper/services/provider.php +++ b/administrator/components/com_wrapper/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Wrapper')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Wrapper')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Wrapper')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new WrapperComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Wrapper')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Wrapper')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Wrapper')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new WrapperComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_wrapper/src/Extension/WrapperComponent.php b/administrator/components/com_wrapper/src/Extension/WrapperComponent.php index 5479cbbf9daf2..4f7ca65911813 100644 --- a/administrator/components/com_wrapper/src/Extension/WrapperComponent.php +++ b/administrator/components/com_wrapper/src/Extension/WrapperComponent.php @@ -1,4 +1,5 @@ alias('session.web', 'session.web.administrator') - ->alias('session', 'session.web.administrator') - ->alias('JSession', 'session.web.administrator') - ->alias(\Joomla\CMS\Session\Session::class, 'session.web.administrator') - ->alias(\Joomla\Session\Session::class, 'session.web.administrator') - ->alias(\Joomla\Session\SessionInterface::class, 'session.web.administrator'); + ->alias('session', 'session.web.administrator') + ->alias('JSession', 'session.web.administrator') + ->alias(\Joomla\CMS\Session\Session::class, 'session.web.administrator') + ->alias(\Joomla\Session\Session::class, 'session.web.administrator') + ->alias(\Joomla\Session\SessionInterface::class, 'session.web.administrator'); // Instantiate the application. $app = $container->get(\Joomla\CMS\Application\AdministratorApplication::class); diff --git a/administrator/includes/defines.php b/administrator/includes/defines.php index e43728b056177..3e8a3fe5fb526 100644 --- a/administrator/includes/defines.php +++ b/administrator/includes/defines.php @@ -1,4 +1,5 @@ isInDevelopmentState()))) -{ - if (file_exists(JPATH_INSTALLATION . '/index.php')) - { - header('Location: ../installation/index.php'); - - exit(); - } - else - { - echo 'No configuration file found and no installation code available. Exiting...'; - - exit; - } +if ( + !file_exists(JPATH_CONFIGURATION . '/configuration.php') + || (filesize(JPATH_CONFIGURATION . '/configuration.php') < 10) + || (file_exists(JPATH_INSTALLATION . '/index.php') && (false === (new Version())->isInDevelopmentState())) +) { + if (file_exists(JPATH_INSTALLATION . '/index.php')) { + header('Location: ../installation/index.php'); + + exit(); + } else { + echo 'No configuration file found and no installation code available. Exiting...'; + + exit; + } } // Pre-Load configuration. Don't remove the Output Buffering due to BOM issues, see JCode 26026 @@ -39,65 +38,59 @@ ob_end_clean(); // System configuration. -$config = new JConfig; +$config = new JConfig(); // Set the error_reporting, and adjust a global Error Handler -switch ($config->error_reporting) -{ - case 'default': - case '-1': - - break; +switch ($config->error_reporting) { + case 'default': + case '-1': + break; - case 'none': - case '0': - error_reporting(0); + case 'none': + case '0': + error_reporting(0); - break; + break; - case 'simple': - error_reporting(E_ERROR | E_WARNING | E_PARSE); - ini_set('display_errors', 1); + case 'simple': + error_reporting(E_ERROR | E_WARNING | E_PARSE); + ini_set('display_errors', 1); - break; + break; - case 'maximum': - case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 - error_reporting(E_ALL); - ini_set('display_errors', 1); + case 'maximum': + case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 + error_reporting(E_ALL); + ini_set('display_errors', 1); - break; + break; - default: - error_reporting($config->error_reporting); - ini_set('display_errors', 1); + default: + error_reporting($config->error_reporting); + ini_set('display_errors', 1); - break; + break; } define('JDEBUG', $config->debug); // Check deprecation logging -if (empty($config->log_deprecated)) -{ - // Reset handler for E_USER_DEPRECATED - set_error_handler(null, E_USER_DEPRECATED); -} -else -{ - // Make sure handler for E_USER_DEPRECATED is registered - set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); +if (empty($config->log_deprecated)) { + // Reset handler for E_USER_DEPRECATED + set_error_handler(null, E_USER_DEPRECATED); +} else { + // Make sure handler for E_USER_DEPRECATED is registered + set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); } -if (JDEBUG || $config->error_reporting === 'maximum') -{ - // Set new Exception handler with debug enabled - $errorHandler->setExceptionHandler( - [ - new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), - 'renderException' - ] - ); +if (JDEBUG || $config->error_reporting === 'maximum') { + // Set new Exception handler with debug enabled + $errorHandler->setExceptionHandler( + [ + new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), + 'renderException' + ] + ); } /** @@ -106,15 +99,12 @@ * We need to do this as high up the stack as we can, as the default in \Joomla\Utilities\IpHelper is to * $allowIpOverride = true which is the wrong default for a generic site NOT behind a trusted proxy/load balancer. */ -if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) -{ - // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR - IpHelper::setAllowIpOverrides(true); -} -else -{ - // We disable the allowing of IP overriding using headers by default. - IpHelper::setAllowIpOverrides(false); +if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) { + // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR + IpHelper::setAllowIpOverrides(true); +} else { + // We disable the allowing of IP overriding using headers by default. + IpHelper::setAllowIpOverrides(false); } unset($config); diff --git a/administrator/index.php b/administrator/index.php index cfec264f131a8..aba7c599d4637 100644 --- a/administrator/index.php +++ b/administrator/index.php @@ -1,4 +1,5 @@ def('prepare_content', 1)) -{ - PluginHelper::importPlugin('content'); - $module->content = HTMLHelper::_('content.prepare', $module->content, '', 'mod_custom.content'); +if ($params->def('prepare_content', 1)) { + PluginHelper::importPlugin('content'); + $module->content = HTMLHelper::_('content.prepare', $module->content, '', 'mod_custom.content'); } // Replace 'images/' to '../images/' when using an image from /images in backend. diff --git a/administrator/modules/mod_custom/tmpl/default.php b/administrator/modules/mod_custom/tmpl/default.php index dea6f8526b9df..c9262884b21d2 100644 --- a/administrator/modules/mod_custom/tmpl/default.php +++ b/administrator/modules/mod_custom/tmpl/default.php @@ -1,4 +1,5 @@
    - content; ?> + content; ?>
    diff --git a/administrator/modules/mod_feed/mod_feed.php b/administrator/modules/mod_feed/mod_feed.php index 7c5175edb75d3..3a3c9d760d88b 100644 --- a/administrator/modules/mod_feed/mod_feed.php +++ b/administrator/modules/mod_feed/mod_feed.php @@ -1,4 +1,5 @@ get('rssurl', ''); - - // Get RSS parsed object - try - { - $feed = new FeedFactory; - $rssDoc = $feed->getFeed($rssurl); - } - catch (\Exception $e) - { - return Text::_('MOD_FEED_ERR_FEED_NOT_RETRIEVED'); - } - - if (empty($rssDoc)) - { - return Text::_('MOD_FEED_ERR_FEED_NOT_RETRIEVED'); - } - - return $rssDoc; - } + /** + * Method to load a feed. + * + * @param \Joomla\Registry\Registry $params The parameters object. + * + * @return \Joomla\CMS\Feed\Feed|string Return a JFeedReader object or a string message if error. + * + * @since 1.5 + */ + public static function getFeed($params) + { + // Module params + $rssurl = $params->get('rssurl', ''); + + // Get RSS parsed object + try { + $feed = new FeedFactory(); + $rssDoc = $feed->getFeed($rssurl); + } catch (\Exception $e) { + return Text::_('MOD_FEED_ERR_FEED_NOT_RETRIEVED'); + } + + if (empty($rssDoc)) { + return Text::_('MOD_FEED_ERR_FEED_NOT_RETRIEVED'); + } + + return $rssDoc; + } } diff --git a/administrator/modules/mod_feed/tmpl/default.php b/administrator/modules/mod_feed/tmpl/default.php index 6adc4e1c62098..7ec1a395848a1 100644 --- a/administrator/modules/mod_feed/tmpl/default.php +++ b/administrator/modules/mod_feed/tmpl/default.php @@ -1,4 +1,5 @@ ' . Text::_('MOD_FEED_ERR_NO_URL') . ''; - - return; -} +if (empty($rssurl)) { + echo '
    ' . Text::_('MOD_FEED_ERR_NO_URL') . '
    '; -if (!empty($feed) && is_string($feed)) -{ - echo $feed; + return; } -else -{ - $lang = $app->getLanguage(); - $myrtl = $params->get('rssrtl', 0); - $direction = ' '; - if ($lang->isRtl() && $myrtl == 0) - { - $direction = ' redirect-rtl'; - } - // Feed description - elseif ($lang->isRtl() && $myrtl == 1) - { - $direction = ' redirect-ltr'; - } - elseif ($lang->isRtl() && $myrtl == 2) - { - $direction = ' redirect-rtl'; - } - elseif ($myrtl == 0) - { - $direction = ' redirect-ltr'; - } - elseif ($myrtl == 1) - { - $direction = ' redirect-ltr'; - } - elseif ($myrtl == 2) - { - $direction = ' redirect-rtl'; - } +if (!empty($feed) && is_string($feed)) { + echo $feed; +} else { + $lang = $app->getLanguage(); + $myrtl = $params->get('rssrtl', 0); + $direction = ' '; - if ($feed != false) : - ?> -
    - isRtl() && $myrtl == 0) { + $direction = ' redirect-rtl'; + } + // Feed description + elseif ($lang->isRtl() && $myrtl == 1) { + $direction = ' redirect-ltr'; + } elseif ($lang->isRtl() && $myrtl == 2) { + $direction = ' redirect-rtl'; + } elseif ($myrtl == 0) { + $direction = ' redirect-ltr'; + } elseif ($myrtl == 1) { + $direction = ' redirect-ltr'; + } elseif ($myrtl == 2) { + $direction = ' redirect-rtl'; + } - // Feed title - if (!is_null($feed->title) && $params->get('rsstitle', 1)) : ?> -

    - - title; ?> -

    - get('rssdate', 1)) : ?> -

    - publishedDate, Text::_('DATE_FORMAT_LC3')); ?> -

    - + if ($feed != false) : + ?> +
    + - get('rssdesc', 1)) : ?> - description; ?> - + // Feed title + if (!is_null($feed->title) && $params->get('rsstitle', 1)) : ?> +

    + + title; ?> +

    + get('rssdate', 1)) : ?> +

    + publishedDate, Text::_('DATE_FORMAT_LC3')); ?> +

    + - - get('rssimage', 1) && $feed->image) : ?> - <?php echo $feed->image->title; ?> - + + get('rssdesc', 1)) : ?> + description; ?> + + + get('rssimage', 1) && $feed->image) : ?> + <?php echo $feed->image->title; ?> + - - -
      - get('rssitems', 3); $i++) : - if (!$feed->offsetExists($i)) : - break; - endif; - $uri = $feed[$i]->uri || !$feed[$i]->isPermaLink ? trim($feed[$i]->uri) : trim($feed[$i]->guid); - $uri = !$uri || stripos($uri, 'http') !== 0 ? $rssurl : $uri; - $text = $feed[$i]->content !== '' ? trim($feed[$i]->content) : ''; - ?> -
    • - - - - - + + +
        + get('rssitems', 3); $i++) : + if (!$feed->offsetExists($i)) : + break; + endif; + $uri = $feed[$i]->uri || !$feed[$i]->isPermaLink ? trim($feed[$i]->uri) : trim($feed[$i]->guid); + $uri = !$uri || stripos($uri, 'http') !== 0 ? $rssurl : $uri; + $text = $feed[$i]->content !== '' ? trim($feed[$i]->content) : ''; + ?> +
      • + + + + + - get('rssitemdate', 0)) : ?> -
        - publishedDate, Text::_('DATE_FORMAT_LC3')); ?> -
        - + get('rssitemdate', 0)) : ?> +
        + publishedDate, Text::_('DATE_FORMAT_LC3')); ?> +
        + - get('rssitemdesc', 1) && $text !== '') : ?> -
        - get('word_count', 0), true, false); - echo str_replace(''', "'", $text); - ?> -
        - -
      • - -
      - -
    - get('rssitemdesc', 1) && $text !== '') : ?> +
    + get('word_count', 0), true, false); + echo str_replace(''', "'", $text); + ?> +
    + + + + + +
    + -
    - -
    -
    - -
    + title="" + target="_blank"> +
    + +
    +
    + +
    diff --git a/administrator/modules/mod_latest/mod_latest.php b/administrator/modules/mod_latest/mod_latest.php index 28ed0c2486aa2..287f2e7bee024 100644 --- a/administrator/modules/mod_latest/mod_latest.php +++ b/administrator/modules/mod_latest/mod_latest.php @@ -1,4 +1,5 @@ get('workflow_enabled'); -if ($workflow_enabled) -{ - $app->getLanguage()->load('com_workflow'); +if ($workflow_enabled) { + $app->getLanguage()->load('com_workflow'); } -if ($params->get('automatic_title', 0)) -{ - $module->title = LatestHelper::getTitle($params); +if ($params->get('automatic_title', 0)) { + $module->title = LatestHelper::getTitle($params); } -if (count($list)) -{ - require ModuleHelper::getLayoutPath('mod_latest', $params->get('layout', 'default')); -} -else -{ - $app->getLanguage()->load('com_content'); - - echo LayoutHelper::render('joomla.content.emptystate_module', [ - 'textPrefix' => 'COM_CONTENT', - 'icon' => 'icon-copy', - ] - ); +if (count($list)) { + require ModuleHelper::getLayoutPath('mod_latest', $params->get('layout', 'default')); +} else { + $app->getLanguage()->load('com_content'); + + echo LayoutHelper::render('joomla.content.emptystate_module', [ + 'textPrefix' => 'COM_CONTENT', + 'icon' => 'icon-copy', + ]); } diff --git a/administrator/modules/mod_latest/src/Helper/LatestHelper.php b/administrator/modules/mod_latest/src/Helper/LatestHelper.php index 5353913a538db..9fcc36ce85382 100644 --- a/administrator/modules/mod_latest/src/Helper/LatestHelper.php +++ b/administrator/modules/mod_latest/src/Helper/LatestHelper.php @@ -1,4 +1,5 @@ setState('list.select', 'a.id, a.title, a.checked_out, a.checked_out_time, ' . - ' a.access, a.created, a.created_by, a.created_by_alias, a.featured, a.state, a.publish_up, a.publish_down' - ); - - // Set Ordering filter - switch ($params->get('ordering', 'c_dsc')) - { - case 'm_dsc': - $model->setState('list.ordering', 'a.modified DESC, a.created'); - $model->setState('list.direction', 'DESC'); - break; - - case 'c_dsc': - default: - $model->setState('list.ordering', 'a.created'); - $model->setState('list.direction', 'DESC'); - break; - } - - // Set Category Filter - $categoryId = $params->get('catid', null); - - if (is_numeric($categoryId)) - { - $model->setState('filter.category_id', $categoryId); - } - - // Set User Filter. - $userId = $user->get('id'); - - switch ($params->get('user_id', '0')) - { - case 'by_me': - $model->setState('filter.author_id', $userId); - break; - - case 'not_me': - $model->setState('filter.author_id', $userId); - $model->setState('filter.author_id.include', false); - break; - } - - // Set the Start and Limit - $model->setState('list.start', 0); - $model->setState('list.limit', $params->get('count', 5)); - - $items = $model->getItems(); - - if ($error = $model->getError()) - { - throw new \Exception($error, 500); - } - - // Set the links - foreach ($items as &$item) - { - $item->link = ''; - - if ($user->authorise('core.edit', 'com_content.article.' . $item->id) - || ($user->authorise('core.edit.own', 'com_content.article.' . $item->id) && ($userId === $item->created_by))) - { - $item->link = Route::_('index.php?option=com_content&task=article.edit&id=' . $item->id); - } - } - - return $items; - } - - /** - * Get the alternate title for the module. - * - * @param \Joomla\Registry\Registry $params The module parameters. - * - * @return string The alternate title for the module. - */ - public static function getTitle($params) - { - $who = $params->get('user_id', 0); - $catid = (int) $params->get('catid', null); - $type = $params->get('ordering') === 'c_dsc' ? '_CREATED' : '_MODIFIED'; - $title = ''; - - if ($catid) - { - $category = Categories::getInstance('Content')->get($catid); - $title = Text::_('MOD_POPULAR_UNEXISTING'); - - if ($category) - { - $title = $category->title; - } - } - - return Text::plural( - 'MOD_LATEST_TITLE' . $type . ($catid ? '_CATEGORY' : '') . ($who != '0' ? "_$who" : ''), - (int) $params->get('count', 5), - $title - ); - } + /** + * Get a list of articles. + * + * @param Registry &$params The module parameters. + * @param ArticlesModel $model The model. + * + * @return mixed An array of articles, or false on error. + */ + public static function getList(Registry &$params, ArticlesModel $model) + { + $user = Factory::getUser(); + + // Set List SELECT + $model->setState('list.select', 'a.id, a.title, a.checked_out, a.checked_out_time, ' . + ' a.access, a.created, a.created_by, a.created_by_alias, a.featured, a.state, a.publish_up, a.publish_down'); + + // Set Ordering filter + switch ($params->get('ordering', 'c_dsc')) { + case 'm_dsc': + $model->setState('list.ordering', 'a.modified DESC, a.created'); + $model->setState('list.direction', 'DESC'); + break; + + case 'c_dsc': + default: + $model->setState('list.ordering', 'a.created'); + $model->setState('list.direction', 'DESC'); + break; + } + + // Set Category Filter + $categoryId = $params->get('catid', null); + + if (is_numeric($categoryId)) { + $model->setState('filter.category_id', $categoryId); + } + + // Set User Filter. + $userId = $user->get('id'); + + switch ($params->get('user_id', '0')) { + case 'by_me': + $model->setState('filter.author_id', $userId); + break; + + case 'not_me': + $model->setState('filter.author_id', $userId); + $model->setState('filter.author_id.include', false); + break; + } + + // Set the Start and Limit + $model->setState('list.start', 0); + $model->setState('list.limit', $params->get('count', 5)); + + $items = $model->getItems(); + + if ($error = $model->getError()) { + throw new \Exception($error, 500); + } + + // Set the links + foreach ($items as &$item) { + $item->link = ''; + + if ( + $user->authorise('core.edit', 'com_content.article.' . $item->id) + || ($user->authorise('core.edit.own', 'com_content.article.' . $item->id) && ($userId === $item->created_by)) + ) { + $item->link = Route::_('index.php?option=com_content&task=article.edit&id=' . $item->id); + } + } + + return $items; + } + + /** + * Get the alternate title for the module. + * + * @param \Joomla\Registry\Registry $params The module parameters. + * + * @return string The alternate title for the module. + */ + public static function getTitle($params) + { + $who = $params->get('user_id', 0); + $catid = (int) $params->get('catid', null); + $type = $params->get('ordering') === 'c_dsc' ? '_CREATED' : '_MODIFIED'; + $title = ''; + + if ($catid) { + $category = Categories::getInstance('Content')->get($catid); + $title = Text::_('MOD_POPULAR_UNEXISTING'); + + if ($category) { + $title = $category->title; + } + } + + return Text::plural( + 'MOD_LATEST_TITLE' . $type . ($catid ? '_CATEGORY' : '') . ($who != '0' ? "_$who" : ''), + (int) $params->get('count', 5), + $title + ); + } } diff --git a/administrator/modules/mod_latest/tmpl/default.php b/administrator/modules/mod_latest/tmpl/default.php index 0a317f748fb48..f77973ade2cd1 100644 --- a/administrator/modules/mod_latest/tmpl/default.php +++ b/administrator/modules/mod_latest/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - - - - - $item) : ?> - - - - - - - - - - - - - - - + + + + + + + + + + + + + + $item) : ?> + + + + + + + + + + + + + + +
    title; ?>
    - checked_out) : ?> - editor, $item->checked_out_time, $module->id); ?> - - link) : ?> - - title, ENT_QUOTES, 'UTF-8'); ?> - - - title, ENT_QUOTES, 'UTF-8'); ?> - - - stage_title); ?> - - author_name; ?> - - created, Text::_('DATE_FORMAT_LC4')); ?> -
    - -
    title; ?>
    + checked_out) : ?> + editor, $item->checked_out_time, $module->id); ?> + + link) : ?> + + title, ENT_QUOTES, 'UTF-8'); ?> + + + title, ENT_QUOTES, 'UTF-8'); ?> + + + stage_title); ?> + + author_name; ?> + + created, Text::_('DATE_FORMAT_LC4')); ?> +
    + +
    diff --git a/administrator/modules/mod_latestactions/mod_latestactions.php b/administrator/modules/mod_latestactions/mod_latestactions.php index 3b60fff23fb17..91e0a7f37427a 100644 --- a/administrator/modules/mod_latestactions/mod_latestactions.php +++ b/administrator/modules/mod_latestactions/mod_latestactions.php @@ -1,4 +1,5 @@ getIdentity()->authorise('core.admin')) -{ - return; +if (!$app->getIdentity()->authorise('core.admin')) { + return; } $list = LatestActionsHelper::getList($params); -if ($params->get('automatic_title', 0)) -{ - $module->title = LatestActionsHelper::getTitle($params); +if ($params->get('automatic_title', 0)) { + $module->title = LatestActionsHelper::getTitle($params); } require ModuleHelper::getLayoutPath('mod_latestactions', $params->get('layout', 'default')); diff --git a/administrator/modules/mod_latestactions/src/Helper/LatestActionsHelper.php b/administrator/modules/mod_latestactions/src/Helper/LatestActionsHelper.php index 61aae97beef10..cb354c6ed8561 100644 --- a/administrator/modules/mod_latestactions/src/Helper/LatestActionsHelper.php +++ b/administrator/modules/mod_latestactions/src/Helper/LatestActionsHelper.php @@ -1,4 +1,5 @@ bootComponent('com_actionlogs')->getMVCFactory() - ->createModel('Actionlogs', 'Administrator', ['ignore_request' => true]); + /** + * Get a list of articles. + * + * @param Registry &$params The module parameters. + * + * @return mixed An array of action logs, or false on error. + * + * @since 3.9.1 + * + * @throws \Exception + */ + public static function getList(&$params) + { + /** @var \Joomla\Component\Actionlogs\Administrator\Model\ActionlogsModel $model */ + $model = Factory::getApplication()->bootComponent('com_actionlogs')->getMVCFactory() + ->createModel('Actionlogs', 'Administrator', ['ignore_request' => true]); - // Set the Start and Limit - $model->setState('list.start', 0); - $model->setState('list.limit', $params->get('count', 5)); - $model->setState('list.ordering', 'a.id'); - $model->setState('list.direction', 'DESC'); + // Set the Start and Limit + $model->setState('list.start', 0); + $model->setState('list.limit', $params->get('count', 5)); + $model->setState('list.ordering', 'a.id'); + $model->setState('list.direction', 'DESC'); - $rows = $model->getItems(); + $rows = $model->getItems(); - // Load all actionlog plugins language files - ActionlogsHelper::loadActionLogPluginsLanguage(); + // Load all actionlog plugins language files + ActionlogsHelper::loadActionLogPluginsLanguage(); - foreach ($rows as $row) - { - $row->message = ActionlogsHelper::getHumanReadableLogMessage($row); - } + foreach ($rows as $row) { + $row->message = ActionlogsHelper::getHumanReadableLogMessage($row); + } - return $rows; - } + return $rows; + } - /** - * Get the alternate title for the module - * - * @param Registry $params The module parameters. - * - * @return string The alternate title for the module. - * - * @since 3.9.1 - */ - public static function getTitle($params) - { - return Text::plural('MOD_LATESTACTIONS_TITLE', $params->get('count', 5)); - } + /** + * Get the alternate title for the module + * + * @param Registry $params The module parameters. + * + * @return string The alternate title for the module. + * + * @since 3.9.1 + */ + public static function getTitle($params) + { + return Text::plural('MOD_LATESTACTIONS_TITLE', $params->get('count', 5)); + } } diff --git a/administrator/modules/mod_latestactions/tmpl/default.php b/administrator/modules/mod_latestactions/tmpl/default.php index 0d27c28500318..a36b6fc510125 100644 --- a/administrator/modules/mod_latestactions/tmpl/default.php +++ b/administrator/modules/mod_latestactions/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - $item) : ?> - - - - - - - - - - - + + + + + + + + + + $item) : ?> + + + + + + + + + + +
    title; ?>
    - message; ?> - - log_date); ?> -
    - -
    title; ?>
    + message; ?> + + log_date); ?> +
    + +
    diff --git a/administrator/modules/mod_logged/mod_logged.php b/administrator/modules/mod_logged/mod_logged.php index e646784599a89..27a210d8570f3 100644 --- a/administrator/modules/mod_logged/mod_logged.php +++ b/administrator/modules/mod_logged/mod_logged.php @@ -1,4 +1,5 @@ get('automatic_title', 0)) -{ - $module->title = LoggedHelper::getTitle($params); +if ($params->get('automatic_title', 0)) { + $module->title = LoggedHelper::getTitle($params); } // Check if session metadata tracking is enabled -if ($app->get('session_metadata', true)) -{ - $users = LoggedHelper::getList($params, $app, Factory::getContainer()->get(DatabaseInterface::class)); +if ($app->get('session_metadata', true)) { + $users = LoggedHelper::getList($params, $app, Factory::getContainer()->get(DatabaseInterface::class)); - require ModuleHelper::getLayoutPath('mod_logged', $params->get('layout', 'default')); -} -else -{ - require ModuleHelper::getLayoutPath('mod_logged', 'disabled'); + require ModuleHelper::getLayoutPath('mod_logged', $params->get('layout', 'default')); +} else { + require ModuleHelper::getLayoutPath('mod_logged', 'disabled'); } diff --git a/administrator/modules/mod_logged/src/Helper/LoggedHelper.php b/administrator/modules/mod_logged/src/Helper/LoggedHelper.php index 490f2b5ea2bde..c653c63f8138b 100644 --- a/administrator/modules/mod_logged/src/Helper/LoggedHelper.php +++ b/administrator/modules/mod_logged/src/Helper/LoggedHelper.php @@ -1,4 +1,5 @@ getIdentity(); - $query = $db->getQuery(true) - ->select('s.time, s.client_id, u.id, u.name, u.username') - ->from('#__session AS s') - ->join('LEFT', '#__users AS u ON s.userid = u.id') - ->where('s.guest = 0') - ->setLimit($params->get('count', 5), 0); + /** + * Get a list of logged users. + * + * @param Registry $params The module parameters + * @param CMSApplication $app The application + * @param DatabaseInterface $db The database + * + * @return mixed An array of users, or false on error. + * + * @throws \RuntimeException + */ + public static function getList(Registry $params, CMSApplication $app, DatabaseInterface $db) + { + $user = $app->getIdentity(); + $query = $db->getQuery(true) + ->select('s.time, s.client_id, u.id, u.name, u.username') + ->from('#__session AS s') + ->join('LEFT', '#__users AS u ON s.userid = u.id') + ->where('s.guest = 0') + ->setLimit($params->get('count', 5), 0); - $db->setQuery($query); + $db->setQuery($query); - try - { - $results = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - throw $e; - } + try { + $results = $db->loadObjectList(); + } catch (\RuntimeException $e) { + throw $e; + } - foreach ($results as $k => $result) - { - $results[$k]->logoutLink = ''; + foreach ($results as $k => $result) { + $results[$k]->logoutLink = ''; - if ($user->authorise('core.manage', 'com_users')) - { - $results[$k]->editLink = Route::_('index.php?option=com_users&task=user.edit&id=' . $result->id); - $results[$k]->logoutLink = Route::_( - 'index.php?option=com_login&task=logout&uid=' . $result->id . '&' . Session::getFormToken() . '=1' - ); - } + if ($user->authorise('core.manage', 'com_users')) { + $results[$k]->editLink = Route::_('index.php?option=com_users&task=user.edit&id=' . $result->id); + $results[$k]->logoutLink = Route::_( + 'index.php?option=com_login&task=logout&uid=' . $result->id . '&' . Session::getFormToken() . '=1' + ); + } - if ($params->get('name', 1) == 0) - { - $results[$k]->name = $results[$k]->username; - } - } + if ($params->get('name', 1) == 0) { + $results[$k]->name = $results[$k]->username; + } + } - return $results; - } + return $results; + } - /** - * Get the alternate title for the module - * - * @param \Joomla\Registry\Registry $params The module parameters. - * - * @return string The alternate title for the module. - */ - public static function getTitle($params) - { - return Text::plural('MOD_LOGGED_TITLE', $params->get('count', 5)); - } + /** + * Get the alternate title for the module + * + * @param \Joomla\Registry\Registry $params The module parameters. + * + * @return string The alternate title for the module. + */ + public static function getTitle($params) + { + return Text::plural('MOD_LOGGED_TITLE', $params->get('count', 5)); + } } diff --git a/administrator/modules/mod_logged/tmpl/default.php b/administrator/modules/mod_logged/tmpl/default.php index 27726f16d8149..9e878efa45481 100644 --- a/administrator/modules/mod_logged/tmpl/default.php +++ b/administrator/modules/mod_logged/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + +
    title; ?>
    - get('name', 1) == 0) : ?> - - - - -
    - editLink)) : ?> - - name, ENT_QUOTES, 'UTF-8'); ?> - - - name, ENT_QUOTES, 'UTF-8'); ?> - - - client_id === null) : ?> - - - client_id) : ?> - - -
    - - -
    - -
    - time, Text::_('DATE_FORMAT_LC5')); ?> -
    title; ?>
    + get('name', 1) == 0) : ?> + + + + +
    + editLink)) : ?> + + name, ENT_QUOTES, 'UTF-8'); ?> + + + name, ENT_QUOTES, 'UTF-8'); ?> + + + client_id === null) : ?> + + + client_id) : ?> + + +
    + + +
    + +
    + time, Text::_('DATE_FORMAT_LC5')); ?> +
    diff --git a/administrator/modules/mod_logged/tmpl/disabled.php b/administrator/modules/mod_logged/tmpl/disabled.php index fade4218a5158..676696d68d817 100644 --- a/administrator/modules/mod_logged/tmpl/disabled.php +++ b/administrator/modules/mod_logged/tmpl/disabled.php @@ -1,4 +1,5 @@
    -

    - - -

    +

    + + +

    diff --git a/administrator/modules/mod_login/mod_login.php b/administrator/modules/mod_login/mod_login.php index 39759064dd82c..25e009c372ce2 100644 --- a/administrator/modules/mod_login/mod_login.php +++ b/administrator/modules/mod_login/mod_login.php @@ -1,4 +1,5 @@ getLanguage()->isRtl()) - { - foreach ($languages as &$language) - { - $language['text'] = $language['text'] . '‎'; - } - } + // Fix wrongly set parentheses in RTL languages + if (Factory::getApplication()->getLanguage()->isRtl()) { + foreach ($languages as &$language) { + $language['text'] = $language['text'] . '‎'; + } + } - array_unshift($languages, HTMLHelper::_('select.option', '', Text::_('JDEFAULTLANGUAGE'))); + array_unshift($languages, HTMLHelper::_('select.option', '', Text::_('JDEFAULTLANGUAGE'))); - return HTMLHelper::_('select.genericlist', $languages, 'lang', 'class="form-select"', 'value', 'text', null); - } + return HTMLHelper::_('select.genericlist', $languages, 'lang', 'class="form-select"', 'value', 'text', null); + } - /** - * Get the redirect URI after login. - * - * @return string - */ - public static function getReturnUri() - { - $uri = Uri::getInstance(); - $return = 'index.php' . $uri->toString(array('query')); + /** + * Get the redirect URI after login. + * + * @return string + */ + public static function getReturnUri() + { + $uri = Uri::getInstance(); + $return = 'index.php' . $uri->toString(array('query')); - if ($return != 'index.php?option=com_login') - { - return base64_encode($return); - } - else - { - return base64_encode('index.php'); - } - } + if ($return != 'index.php?option=com_login') { + return base64_encode($return); + } else { + return base64_encode('index.php'); + } + } } diff --git a/administrator/modules/mod_login/tmpl/default.php b/administrator/modules/mod_login/tmpl/default.php index 9249ecbaf6439..7a9234d9736fc 100644 --- a/administrator/modules/mod_login/tmpl/default.php +++ b/administrator/modules/mod_login/tmpl/default.php @@ -1,4 +1,5 @@ getDocument()->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('field.passwordview') - ->registerAndUseScript('mod_login.admin', 'mod_login/admin-login.min.js', [], ['defer' => true], ['core', 'form.validate']); + ->useScript('field.passwordview') + ->registerAndUseScript('mod_login.admin', 'mod_login/admin-login.min.js', [], ['defer' => true], ['core', 'form.validate']); Text::script('JSHOWPASSWORD'); Text::script('JHIDEPASSWORD'); ?>
    -
    - -
    - -
    +
    + +
    + +
    - -
    -
    -
    - -
    + +
    +
    +
    + +
    - - + + -
    -
    +
    +
    -
    - -
    - - -
    - - -
    - -
    - -
    - -
    - - - - -
    -
    +
    + +
    + + +
    + + +
    + +
    + +
    + +
    + + + + +
    +
    -
    - '_blank', - 'rel' => 'noopener nofollow', - 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGIN_CREDENTIALS')) - ] - ); ?> -
    +
    + '_blank', + 'rel' => 'noopener nofollow', + 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGIN_CREDENTIALS')) + ] + ); ?> +
    diff --git a/administrator/modules/mod_loginsupport/mod_loginsupport.php b/administrator/modules/mod_loginsupport/mod_loginsupport.php index 8bd2a0b2f5b73..77be815970ec0 100644 --- a/administrator/modules/mod_loginsupport/mod_loginsupport.php +++ b/administrator/modules/mod_loginsupport/mod_loginsupport.php @@ -1,4 +1,5 @@ get('automatic_title')) -{ - $module->title = Text::_('MOD_LOGINSUPPORT_TITLE'); +if ($params->get('automatic_title')) { + $module->title = Text::_('MOD_LOGINSUPPORT_TITLE'); } require ModuleHelper::getLayoutPath('mod_loginsupport', $params->get('layout', 'default')); diff --git a/administrator/modules/mod_loginsupport/tmpl/default.php b/administrator/modules/mod_loginsupport/tmpl/default.php index a01598dba85b9..78443fedee070 100644 --- a/administrator/modules/mod_loginsupport/tmpl/default.php +++ b/administrator/modules/mod_loginsupport/tmpl/default.php @@ -1,4 +1,5 @@
    -

    -
      -
    • - get('forum_url'), - Text::_('MOD_LOGINSUPPORT_FORUM'), - [ - 'target' => '_blank', - 'rel' => 'nofollow noopener', - 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_FORUM')) - ] - ); ?> -
    • -
    • - get('documentation_url'), - Text::_('MOD_LOGINSUPPORT_DOCUMENTATION'), - [ - 'target' => '_blank', - 'rel' => 'nofollow noopener', - 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_DOCUMENTATION')) - ] - ); ?> -
    • -
    • - get('news_url'), - Text::_('MOD_LOGINSUPPORT_NEWS'), - [ - 'target' => '_blank', - 'rel' => 'nofollow noopener', - 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_NEWS')) - ] - ); ?> -
    • -
    +

    +
      +
    • + get('forum_url'), + Text::_('MOD_LOGINSUPPORT_FORUM'), + [ + 'target' => '_blank', + 'rel' => 'nofollow noopener', + 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_FORUM')) + ] + ); ?> +
    • +
    • + get('documentation_url'), + Text::_('MOD_LOGINSUPPORT_DOCUMENTATION'), + [ + 'target' => '_blank', + 'rel' => 'nofollow noopener', + 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_DOCUMENTATION')) + ] + ); ?> +
    • +
    • + get('news_url'), + Text::_('MOD_LOGINSUPPORT_NEWS'), + [ + 'target' => '_blank', + 'rel' => 'nofollow noopener', + 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_NEWS')) + ] + ); ?> +
    • +
    diff --git a/administrator/modules/mod_menu/mod_menu.php b/administrator/modules/mod_menu/mod_menu.php index 5c5dbbf18bb32..484dd7e24687e 100644 --- a/administrator/modules/mod_menu/mod_menu.php +++ b/administrator/modules/mod_menu/mod_menu.php @@ -1,4 +1,5 @@ application = $application; - $this->root = new AdministratorMenuItem; - } - - /** - * Populate the menu items in the menu tree object - * - * @param Registry $params Menu configuration parameters - * @param bool $enabled Whether the menu should be enabled or disabled - * - * @return AdministratorMenuItem Root node of the menu tree - * - * @since 3.7.0 - */ - public function load($params, $enabled) - { - $this->params = $params; - $this->enabled = $enabled; - $menutype = $this->params->get('menutype', '*'); - - if ($menutype === '*') - { - $name = $this->params->get('preset', 'default'); - $this->root = MenusHelper::loadPreset($name); - } - else - { - $this->root = MenusHelper::getMenuItems($menutype, true); - - // Can we access everything important with this menu? Create a recovery menu! - if ($this->enabled - && $this->params->get('check', 1) - && $this->check($this->root, $this->params)) - { - $this->params->set('recovery', true); - - // In recovery mode, load the preset inside a special root node. - $this->root = new AdministratorMenuItem(['level' => 0]); - $heading = new AdministratorMenuItem(['title' => 'MOD_MENU_RECOVERY_MENU_ROOT', 'type' => 'heading']); - $this->root->addChild($heading); - - MenusHelper::loadPreset('default', true, $heading); - - $this->preprocess($this->root); - - $this->root->addChild(new AdministratorMenuItem(['type' => 'separator'])); - - // Add link to exit recovery mode - $uri = clone Uri::getInstance(); - $uri->setVar('recover_menu', 0); - - $this->root->addChild(new AdministratorMenuItem(['title' => 'MOD_MENU_RECOVERY_EXIT', 'type' => 'url', 'link' => $uri->toString()])); - - return $this->root; - } - } - - $this->preprocess($this->root); - - return $this->root; - } - - /** - * Method to render a given level of a menu using provided layout file - * - * @param string $layoutFile The layout file to be used to render - * @param AdministratorMenuItem $node Node to render the children of - * - * @return void - * - * @since 3.8.0 - */ - public function renderSubmenu($layoutFile, $node) - { - if (is_file($layoutFile)) - { - $children = $node->getChildren(); - - foreach ($children as $current) - { - $current->level = $node->level + 1; - - // This sets the scope to this object for the layout file and also isolates other `include`s - require $layoutFile; - } - } - } - - /** - * Check the flat list of menu items for important links - * - * @param AdministratorMenuItem $node The menu items array - * @param Registry $params Module options - * - * @return boolean Whether to show recovery menu - * - * @since 3.8.0 - */ - protected function check($node, Registry $params) - { - $me = $this->application->getIdentity(); - $authMenus = $me->authorise('core.manage', 'com_menus'); - $authModules = $me->authorise('core.manage', 'com_modules'); - - if (!$authMenus && !$authModules) - { - return false; - } - - $items = $node->getChildren(true); - $types = array_column($items, 'type'); - $elements = array_column($items, 'element'); - $rMenu = $authMenus && !\in_array('com_menus', $elements); - $rModule = $authModules && !\in_array('com_modules', $elements); - $rContainer = !\in_array('container', $types); - - if ($rMenu || $rModule || $rContainer) - { - $recovery = $this->application->getUserStateFromRequest('mod_menu.recovery', 'recover_menu', 0, 'int'); - - if ($recovery) - { - return true; - } - - $missing = array(); - - if ($rMenu) - { - $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_MENU_MANAGER'); - } - - if ($rModule) - { - $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_MODULE_MANAGER'); - } - - if ($rContainer) - { - $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_COMPONENTS_CONTAINER'); - } - - $uri = clone Uri::getInstance(); - $uri->setVar('recover_menu', 1); - - $table = Table::getInstance('MenuType'); - $menutype = $params->get('menutype'); - - $table->load(array('menutype' => $menutype)); - - $menutype = $table->get('title', $menutype); - $message = Text::sprintf('MOD_MENU_IMPORTANT_ITEMS_INACCESSIBLE_LIST_WARNING', $menutype, implode(', ', $missing), $uri); - - $this->application->enqueueMessage($message, 'warning'); - } - - return false; - } - - /** - * Filter and perform other preparatory tasks for loaded menu items based on access rights and module configurations for display - * - * @param AdministratorMenuItem $parent A menu item to process - * - * @return array - * - * @since 3.8.0 - */ - protected function preprocess($parent) - { - $user = $this->application->getIdentity(); - $language = $this->application->getLanguage(); - - $noSeparator = true; - $children = $parent->getChildren(); - - /** - * Trigger onPreprocessMenuItems for the current level of backend menu items. - * $children is an array of AdministratorMenuItem objects. A plugin can traverse the whole tree, - * but new nodes will only be run through this method if their parents have not been processed yet. - */ - $this->application->triggerEvent('onPreprocessMenuItems', array('com_menus.administrator.module', $children, $this->params, $this->enabled)); - - foreach ($children as $item) - { - $itemParams = $item->getParams(); - - // Exclude item with menu item option set to exclude from menu modules - if ($itemParams->get('menu_show', 1) == 0) - { - $parent->removeChild($item); - continue; - } - - $item->scope = $item->scope ?? 'default'; - $item->icon = $item->icon ?? ''; - - // Whether this scope can be displayed. Applies only to preset items. Db driven items should use un/published state. - if (($item->scope === 'help' && $this->params->get('showhelp', 1) == 0) || ($item->scope === 'edit' && !$this->params->get('shownew', 1))) - { - $parent->removeChild($item); - continue; - } - - if (substr($item->link, 0, 8) === 'special:') - { - $special = substr($item->link, 8); - - if ($special === 'language-forum') - { - $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; - } - elseif ($special === 'custom-forum') - { - $item->link = $this->params->get('forum_url'); - } - } - - $uri = new Uri($item->link); - $query = $uri->getQuery(true); - - /** - * If component is passed in the link via option variable, we set $item->element to this value for further - * processing. It is needed for links from menu items of third party extensions link to Joomla! core - * components like com_categories, com_fields... - */ - if ($option = $uri->getVar('option')) - { - $item->element = $option; - } - - // Exclude item if is not enabled - if ($item->element && !ComponentHelper::isEnabled($item->element)) - { - $parent->removeChild($item); - continue; - } - - /* - * Multilingual Associations if the site is not set as multilingual and/or Associations is not enabled in - * the Language Filter plugin - */ - - if ($item->element === 'com_associations' && !Associations::isEnabled()) - { - $parent->removeChild($item); - continue; - } - - // Exclude Mass Mail if disabled in global configuration - if ($item->scope === 'massmail' && ($this->application->get('mailonline', 1) == 0 || $this->application->get('massmailoff', 0) == 1)) - { - $parent->removeChild($item); - continue; - } - - // Exclude item if the component is not authorised - $assetName = $item->element; - - if ($item->element === 'com_categories') - { - $assetName = $query['extension'] ?? 'com_content'; - } - elseif ($item->element === 'com_fields') - { - // Only display Fields menus when enabled in the component - $createFields = null; - - if (isset($query['context'])) - { - $createFields = ComponentHelper::getParams(strstr($query['context'], '.', true))->get('custom_fields_enable', 1); - } - - if (!$createFields) - { - $parent->removeChild($item); - continue; - } - - list($assetName) = isset($query['context']) ? explode('.', $query['context'], 2) : array('com_fields'); - } - elseif ($item->element === 'com_cpanel' && $item->link === 'index.php') - { - continue; - } - elseif ($item->link === 'index.php?option=com_cpanel&view=help' - || $item->link === 'index.php?option=com_cpanel&view=cpanel&dashboard=help') - { - if ($this->params->get('showhelp', 1)) - { - continue; - } - - // Exclude help menu item if set such in mod_menu - $parent->removeChild($item); - continue; - } - elseif ($item->element === 'com_workflow') - { - // Only display Workflow menus when enabled in the component - $workflow = null; - - if (isset($query['extension'])) - { - $parts = explode('.', $query['extension']); - - $workflow = ComponentHelper::getParams($parts[0])->get('workflow_enabled') && $user->authorise('core.manage.workflow', $parts[0]); - } - - if (!$workflow) - { - $parent->removeChild($item); - continue; - } - - list($assetName) = isset($query['extension']) ? explode('.', $query['extension'], 2) : array('com_workflow'); - } - // Special case for components which only allow super user access - elseif (\in_array($item->element, array('com_config', 'com_privacy', 'com_actionlogs'), true) && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - elseif ($item->element === 'com_joomlaupdate' && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - elseif (($item->link === 'index.php?option=com_installer&view=install' || $item->link === 'index.php?option=com_installer&view=languages') - && !$user->authorise('core.admin')) - { - continue; - } - elseif ($item->element === 'com_admin') - { - if (isset($query['view']) && $query['view'] === 'sysinfo' && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - } - elseif ($item->link === 'index.php?option=com_messages&view=messages' && !$user->authorise('core.manage', 'com_users')) - { - $parent->removeChild($item); - continue; - } - - if ($assetName && !$user->authorise(($item->scope === 'edit') ? 'core.create' : 'core.manage', $assetName)) - { - $parent->removeChild($item); - continue; - } - - // Exclude if link is invalid - if (is_null($item->link) || !\in_array($item->type, array('separator', 'heading', 'container')) && trim($item->link) === '') - { - $parent->removeChild($item); - continue; - } - - // Process any children if exists - if ($item->hasChildren()) - { - $this->preprocess($item); - } - - // Populate automatic children for container items - if ($item->type === 'container') - { - $exclude = (array) $itemParams->get('hideitems') ?: array(); - $components = MenusHelper::getMenuItems('main', false, $exclude); - - // We are adding the nodes first to preprocess them, then sort them and add them again. - foreach ($components->getChildren() as $c) - { - $item->addChild($c); - } - - $this->preprocess($item); - $children = ArrayHelper::sortObjects($item->getChildren(), 'text', 1, false, true); - - foreach ($children as $c) - { - $item->addChild($c); - } - } - - // Exclude if there are no child items under heading or container - if (\in_array($item->type, array('heading', 'container')) && !$item->hasChildren() && empty($item->components)) - { - $parent->removeChild($item); - continue; - } - - // Remove repeated and edge positioned separators, It is important to put this check at the end of any logical filtering. - if ($item->type === 'separator') - { - if ($noSeparator) - { - $parent->removeChild($item); - continue; - } - - $noSeparator = true; - } - else - { - $noSeparator = false; - } - - // Ok we passed everything, load language at last only - if ($item->element) - { - $language->load($item->element . '.sys', JPATH_ADMINISTRATOR) || - $language->load($item->element . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->element); - } - - if ($item->type === 'separator' && $itemParams->get('text_separator') == 0) - { - $item->title = ''; - } - - $item->text = Text::_($item->title); - } - - // If last one was a separator remove it too. - $last = end($parent->getChildren()); - - if ($last && $last->type === 'separator' && $last->getSibling(false) && $last->getSibling(false)->type === 'separator') - { - $parent->removeChild($last); - } - } - - /** - * Method to get the CSS class name for an icon identifier or create one if - * a custom image path is passed as the identifier - * - * @param AdministratorMenuItem $node Node to get icon data from - * - * @return string CSS class name - * - * @since 3.8.0 - */ - public function getIconClass($node) - { - $identifier = $node->class; - - // Top level is special - if (trim($identifier) == '') - { - return null; - } - - // We were passed a class name - if (substr($identifier, 0, 6) == 'class:') - { - $class = substr($identifier, 6); - } - // We were passed background icon url. Build the CSS class for the icon - else - { - if ($identifier == null) - { - return null; - } - - $class = preg_replace('#\.[^.]*$#', '', basename($identifier)); - $class = preg_replace('#\.\.[^A-Za-z0-9\.\_\- ]#', '', $class); - } - - $html = 'icon-' . $class . ' icon-fw'; - - return $html; - } - - /** - * Create unique identifier - * - * @return string - * - * @since 4.0.0 - */ - public function getCounter() - { - $this->counter++; - - return $this->counter; - } + /** + * The root of the menu + * + * @var AdministratorMenuItem + * + * @since 4.0.0 + */ + protected $root; + + /** + * An array of AdministratorMenuItem nodes + * + * @var AdministratorMenuItem[] + * + * @since 4.0.0 + */ + protected $nodes = []; + + /** + * The module options + * + * @var Registry + * + * @since 3.8.0 + */ + protected $params; + + /** + * The menu bar state + * + * @var boolean + * + * @since 3.8.0 + */ + protected $enabled; + + /** + * The application + * + * @var boolean + * + * @since 4.0.0 + */ + protected $application; + + /** + * A counter for unique IDs + * + * @var integer + * + * @since 4.0.0 + */ + protected $counter = 0; + + /** + * CssMenu constructor. + * + * @param CMSApplication $application The application + * + * @since 4.0.0 + */ + public function __construct(CMSApplication $application) + { + $this->application = $application; + $this->root = new AdministratorMenuItem(); + } + + /** + * Populate the menu items in the menu tree object + * + * @param Registry $params Menu configuration parameters + * @param bool $enabled Whether the menu should be enabled or disabled + * + * @return AdministratorMenuItem Root node of the menu tree + * + * @since 3.7.0 + */ + public function load($params, $enabled) + { + $this->params = $params; + $this->enabled = $enabled; + $menutype = $this->params->get('menutype', '*'); + + if ($menutype === '*') { + $name = $this->params->get('preset', 'default'); + $this->root = MenusHelper::loadPreset($name); + } else { + $this->root = MenusHelper::getMenuItems($menutype, true); + + // Can we access everything important with this menu? Create a recovery menu! + if ( + $this->enabled + && $this->params->get('check', 1) + && $this->check($this->root, $this->params) + ) { + $this->params->set('recovery', true); + + // In recovery mode, load the preset inside a special root node. + $this->root = new AdministratorMenuItem(['level' => 0]); + $heading = new AdministratorMenuItem(['title' => 'MOD_MENU_RECOVERY_MENU_ROOT', 'type' => 'heading']); + $this->root->addChild($heading); + + MenusHelper::loadPreset('default', true, $heading); + + $this->preprocess($this->root); + + $this->root->addChild(new AdministratorMenuItem(['type' => 'separator'])); + + // Add link to exit recovery mode + $uri = clone Uri::getInstance(); + $uri->setVar('recover_menu', 0); + + $this->root->addChild(new AdministratorMenuItem(['title' => 'MOD_MENU_RECOVERY_EXIT', 'type' => 'url', 'link' => $uri->toString()])); + + return $this->root; + } + } + + $this->preprocess($this->root); + + return $this->root; + } + + /** + * Method to render a given level of a menu using provided layout file + * + * @param string $layoutFile The layout file to be used to render + * @param AdministratorMenuItem $node Node to render the children of + * + * @return void + * + * @since 3.8.0 + */ + public function renderSubmenu($layoutFile, $node) + { + if (is_file($layoutFile)) { + $children = $node->getChildren(); + + foreach ($children as $current) { + $current->level = $node->level + 1; + + // This sets the scope to this object for the layout file and also isolates other `include`s + require $layoutFile; + } + } + } + + /** + * Check the flat list of menu items for important links + * + * @param AdministratorMenuItem $node The menu items array + * @param Registry $params Module options + * + * @return boolean Whether to show recovery menu + * + * @since 3.8.0 + */ + protected function check($node, Registry $params) + { + $me = $this->application->getIdentity(); + $authMenus = $me->authorise('core.manage', 'com_menus'); + $authModules = $me->authorise('core.manage', 'com_modules'); + + if (!$authMenus && !$authModules) { + return false; + } + + $items = $node->getChildren(true); + $types = array_column($items, 'type'); + $elements = array_column($items, 'element'); + $rMenu = $authMenus && !\in_array('com_menus', $elements); + $rModule = $authModules && !\in_array('com_modules', $elements); + $rContainer = !\in_array('container', $types); + + if ($rMenu || $rModule || $rContainer) { + $recovery = $this->application->getUserStateFromRequest('mod_menu.recovery', 'recover_menu', 0, 'int'); + + if ($recovery) { + return true; + } + + $missing = array(); + + if ($rMenu) { + $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_MENU_MANAGER'); + } + + if ($rModule) { + $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_MODULE_MANAGER'); + } + + if ($rContainer) { + $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_COMPONENTS_CONTAINER'); + } + + $uri = clone Uri::getInstance(); + $uri->setVar('recover_menu', 1); + + $table = Table::getInstance('MenuType'); + $menutype = $params->get('menutype'); + + $table->load(array('menutype' => $menutype)); + + $menutype = $table->get('title', $menutype); + $message = Text::sprintf('MOD_MENU_IMPORTANT_ITEMS_INACCESSIBLE_LIST_WARNING', $menutype, implode(', ', $missing), $uri); + + $this->application->enqueueMessage($message, 'warning'); + } + + return false; + } + + /** + * Filter and perform other preparatory tasks for loaded menu items based on access rights and module configurations for display + * + * @param AdministratorMenuItem $parent A menu item to process + * + * @return array + * + * @since 3.8.0 + */ + protected function preprocess($parent) + { + $user = $this->application->getIdentity(); + $language = $this->application->getLanguage(); + + $noSeparator = true; + $children = $parent->getChildren(); + + /** + * Trigger onPreprocessMenuItems for the current level of backend menu items. + * $children is an array of AdministratorMenuItem objects. A plugin can traverse the whole tree, + * but new nodes will only be run through this method if their parents have not been processed yet. + */ + $this->application->triggerEvent('onPreprocessMenuItems', array('com_menus.administrator.module', $children, $this->params, $this->enabled)); + + foreach ($children as $item) { + $itemParams = $item->getParams(); + + // Exclude item with menu item option set to exclude from menu modules + if ($itemParams->get('menu_show', 1) == 0) { + $parent->removeChild($item); + continue; + } + + $item->scope = $item->scope ?? 'default'; + $item->icon = $item->icon ?? ''; + + // Whether this scope can be displayed. Applies only to preset items. Db driven items should use un/published state. + if (($item->scope === 'help' && $this->params->get('showhelp', 1) == 0) || ($item->scope === 'edit' && !$this->params->get('shownew', 1))) { + $parent->removeChild($item); + continue; + } + + if (substr($item->link, 0, 8) === 'special:') { + $special = substr($item->link, 8); + + if ($special === 'language-forum') { + $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; + } elseif ($special === 'custom-forum') { + $item->link = $this->params->get('forum_url'); + } + } + + $uri = new Uri($item->link); + $query = $uri->getQuery(true); + + /** + * If component is passed in the link via option variable, we set $item->element to this value for further + * processing. It is needed for links from menu items of third party extensions link to Joomla! core + * components like com_categories, com_fields... + */ + if ($option = $uri->getVar('option')) { + $item->element = $option; + } + + // Exclude item if is not enabled + if ($item->element && !ComponentHelper::isEnabled($item->element)) { + $parent->removeChild($item); + continue; + } + + /* + * Multilingual Associations if the site is not set as multilingual and/or Associations is not enabled in + * the Language Filter plugin + */ + + if ($item->element === 'com_associations' && !Associations::isEnabled()) { + $parent->removeChild($item); + continue; + } + + // Exclude Mass Mail if disabled in global configuration + if ($item->scope === 'massmail' && ($this->application->get('mailonline', 1) == 0 || $this->application->get('massmailoff', 0) == 1)) { + $parent->removeChild($item); + continue; + } + + // Exclude item if the component is not authorised + $assetName = $item->element; + + if ($item->element === 'com_categories') { + $assetName = $query['extension'] ?? 'com_content'; + } elseif ($item->element === 'com_fields') { + // Only display Fields menus when enabled in the component + $createFields = null; + + if (isset($query['context'])) { + $createFields = ComponentHelper::getParams(strstr($query['context'], '.', true))->get('custom_fields_enable', 1); + } + + if (!$createFields) { + $parent->removeChild($item); + continue; + } + + list($assetName) = isset($query['context']) ? explode('.', $query['context'], 2) : array('com_fields'); + } elseif ($item->element === 'com_cpanel' && $item->link === 'index.php') { + continue; + } elseif ( + $item->link === 'index.php?option=com_cpanel&view=help' + || $item->link === 'index.php?option=com_cpanel&view=cpanel&dashboard=help' + ) { + if ($this->params->get('showhelp', 1)) { + continue; + } + + // Exclude help menu item if set such in mod_menu + $parent->removeChild($item); + continue; + } elseif ($item->element === 'com_workflow') { + // Only display Workflow menus when enabled in the component + $workflow = null; + + if (isset($query['extension'])) { + $parts = explode('.', $query['extension']); + + $workflow = ComponentHelper::getParams($parts[0])->get('workflow_enabled') && $user->authorise('core.manage.workflow', $parts[0]); + } + + if (!$workflow) { + $parent->removeChild($item); + continue; + } + + list($assetName) = isset($query['extension']) ? explode('.', $query['extension'], 2) : array('com_workflow'); + } + // Special case for components which only allow super user access + elseif (\in_array($item->element, array('com_config', 'com_privacy', 'com_actionlogs'), true) && !$user->authorise('core.admin')) { + $parent->removeChild($item); + continue; + } elseif ($item->element === 'com_joomlaupdate' && !$user->authorise('core.admin')) { + $parent->removeChild($item); + continue; + } elseif ( + ($item->link === 'index.php?option=com_installer&view=install' || $item->link === 'index.php?option=com_installer&view=languages') + && !$user->authorise('core.admin') + ) { + continue; + } elseif ($item->element === 'com_admin') { + if (isset($query['view']) && $query['view'] === 'sysinfo' && !$user->authorise('core.admin')) { + $parent->removeChild($item); + continue; + } + } elseif ($item->link === 'index.php?option=com_messages&view=messages' && !$user->authorise('core.manage', 'com_users')) { + $parent->removeChild($item); + continue; + } + + if ($assetName && !$user->authorise(($item->scope === 'edit') ? 'core.create' : 'core.manage', $assetName)) { + $parent->removeChild($item); + continue; + } + + // Exclude if link is invalid + if (is_null($item->link) || !\in_array($item->type, array('separator', 'heading', 'container')) && trim($item->link) === '') { + $parent->removeChild($item); + continue; + } + + // Process any children if exists + if ($item->hasChildren()) { + $this->preprocess($item); + } + + // Populate automatic children for container items + if ($item->type === 'container') { + $exclude = (array) $itemParams->get('hideitems') ?: array(); + $components = MenusHelper::getMenuItems('main', false, $exclude); + + // We are adding the nodes first to preprocess them, then sort them and add them again. + foreach ($components->getChildren() as $c) { + $item->addChild($c); + } + + $this->preprocess($item); + $children = ArrayHelper::sortObjects($item->getChildren(), 'text', 1, false, true); + + foreach ($children as $c) { + $item->addChild($c); + } + } + + // Exclude if there are no child items under heading or container + if (\in_array($item->type, array('heading', 'container')) && !$item->hasChildren() && empty($item->components)) { + $parent->removeChild($item); + continue; + } + + // Remove repeated and edge positioned separators, It is important to put this check at the end of any logical filtering. + if ($item->type === 'separator') { + if ($noSeparator) { + $parent->removeChild($item); + continue; + } + + $noSeparator = true; + } else { + $noSeparator = false; + } + + // Ok we passed everything, load language at last only + if ($item->element) { + $language->load($item->element . '.sys', JPATH_ADMINISTRATOR) || + $language->load($item->element . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->element); + } + + if ($item->type === 'separator' && $itemParams->get('text_separator') == 0) { + $item->title = ''; + } + + $item->text = Text::_($item->title); + } + + // If last one was a separator remove it too. + $last = end($parent->getChildren()); + + if ($last && $last->type === 'separator' && $last->getSibling(false) && $last->getSibling(false)->type === 'separator') { + $parent->removeChild($last); + } + } + + /** + * Method to get the CSS class name for an icon identifier or create one if + * a custom image path is passed as the identifier + * + * @param AdministratorMenuItem $node Node to get icon data from + * + * @return string CSS class name + * + * @since 3.8.0 + */ + public function getIconClass($node) + { + $identifier = $node->class; + + // Top level is special + if (trim($identifier) == '') { + return null; + } + + // We were passed a class name + if (substr($identifier, 0, 6) == 'class:') { + $class = substr($identifier, 6); + } + // We were passed background icon url. Build the CSS class for the icon + else { + if ($identifier == null) { + return null; + } + + $class = preg_replace('#\.[^.]*$#', '', basename($identifier)); + $class = preg_replace('#\.\.[^A-Za-z0-9\.\_\- ]#', '', $class); + } + + $html = 'icon-' . $class . ' icon-fw'; + + return $html; + } + + /** + * Create unique identifier + * + * @return string + * + * @since 4.0.0 + */ + public function getCounter() + { + $this->counter++; + + return $this->counter; + } } diff --git a/administrator/modules/mod_menu/tmpl/default.php b/administrator/modules/mod_menu/tmpl/default.php index 01f57febbe85b..581f4e9fb9e67 100644 --- a/administrator/modules/mod_menu/tmpl/default.php +++ b/administrator/modules/mod_menu/tmpl/default.php @@ -1,4 +1,5 @@ getWebAssetManager(); $wa->getRegistry()->addExtensionRegistryFile('com_cpanel'); $wa->useScript('metismenujs') - ->registerAndUseScript('mod_menu.admin-menu', 'mod_menu/admin-menu.min.js', [], ['defer' => true], ['metismenujs']) - ->useScript('com_cpanel.admin-system-loader'); + ->registerAndUseScript('mod_menu.admin-menu', 'mod_menu/admin-menu.min.js', [], ['defer' => true], ['metismenujs']) + ->useScript('com_cpanel.admin-system-loader'); // Recurse through children of root node if they exist -if ($root->hasChildren()) -{ - echo '\n"; } diff --git a/administrator/modules/mod_menu/tmpl/default_submenu.php b/administrator/modules/mod_menu/tmpl/default_submenu.php index 566800a96aa78..13230279c5846 100644 --- a/administrator/modules/mod_menu/tmpl/default_submenu.php +++ b/administrator/modules/mod_menu/tmpl/default_submenu.php @@ -1,4 +1,5 @@ getParams(); // Build the CSS class suffix -if (!$this->enabled) -{ - $class .= ' disabled'; -} -elseif ($current->type == 'separator') -{ - $class = $current->title ? 'menuitem-group' : 'divider'; -} -elseif ($current->hasChildren()) -{ - $class .= ' parent'; +if (!$this->enabled) { + $class .= ' disabled'; +} elseif ($current->type == 'separator') { + $class = $current->title ? 'menuitem-group' : 'divider'; +} elseif ($current->hasChildren()) { + $class .= ' parent'; } -if ($current->level == 1) -{ - $class .= ' item-level-1'; -} -elseif ($current->level == 2) -{ - $class .= ' item-level-2'; -} -elseif ($current->level == 3) -{ - $class .= ' item-level-3'; +if ($current->level == 1) { + $class .= ' item-level-1'; +} elseif ($current->level == 2) { + $class .= ' item-level-2'; +} elseif ($current->level == 3) { + $class .= ' item-level-3'; } // Set the correct aria role and print the item -if ($current->type == 'separator') -{ - echo '
  • '; +if ($current->type == 'separator') { + echo '
  • '; } // Print a link if it exists @@ -67,18 +55,14 @@ $itemIconClass = ''; $itemImage = ''; -if ($current->hasChildren()) -{ - $linkClass[] = 'has-arrow'; +if ($current->hasChildren()) { + $linkClass[] = 'has-arrow'; - if ($current->level > 2) - { - $dataToggle = ' data-bs-toggle="dropdown"'; - } -} -else -{ - $linkClass[] = 'no-dropdown'; + if ($current->level > 2) { + $dataToggle = ' data-bs-toggle="dropdown"'; + } +} else { + $linkClass[] = 'no-dropdown'; } // Implode out $linkClass for rendering @@ -100,110 +84,86 @@ $iconImage = $current->icon; $homeImage = ''; -if ($iconClass === '' && $itemIconClass) -{ - $iconClass = ''; +if ($iconClass === '' && $itemIconClass) { + $iconClass = ''; } -if ($iconImage) -{ - if (substr($iconImage, 0, 6) == 'class:' && substr($iconImage, 6) == 'icon-home') - { - $iconImage = ''; - $iconImage .= '' . Text::_('JDEFAULT') . ''; - } - elseif (substr($iconImage, 0, 6) == 'image:') - { - $iconImage = ' ' . substr($iconImage, 6) . ''; - } - else - { - $iconImage = ''; - } +if ($iconImage) { + if (substr($iconImage, 0, 6) == 'class:' && substr($iconImage, 6) == 'icon-home') { + $iconImage = ''; + $iconImage .= '' . Text::_('JDEFAULT') . ''; + } elseif (substr($iconImage, 0, 6) == 'image:') { + $iconImage = ' ' . substr($iconImage, 6) . ''; + } else { + $iconImage = ''; + } } $itemImage = (empty($itemIconClass) && $itemImage) ? '  ' : ''; // If the item image is not set, the item title would not have margin. Here we add it. -if ($icon == '' && $iconClass == '' && $current->level == 1 && $current->target == '') -{ - $iconClass = ''; +if ($icon == '' && $iconClass == '' && $current->level == 1 && $current->target == '') { + $iconClass = ''; +} + +if ($link != '' && $current->target != '') { + echo '' + . $iconClass + . '' . $itemImage . Text::_($current->title) . '' . $ajax . ''; +} elseif ($link != '' && $current->type !== 'separator') { + echo '' + . $iconClass + . '' . $itemImage . Text::_($current->title) . '' . $iconImage . ''; +} elseif ($current->title != '' && $current->type !== 'separator') { + echo '' + . $iconClass + . '' . $itemImage . Text::_($current->title) . '' . $ajax . ''; +} elseif ($current->title != '' && $current->type === 'separator') { + echo '' . Text::_($current->title) . '' . $ajax; +} else { + echo '' . Text::_($current->title) . '' . $ajax; +} + +if ($currentParams->get('menu-quicktask') && (int) $this->params->get('shownew', 1) === 1) { + $params = $current->getParams(); + $user = $this->application->getIdentity(); + $link = $params->get('menu-quicktask'); + $icon = $params->get('menu-quicktask-icon', 'plus'); + $title = $params->get('menu-quicktask-title', 'MOD_MENU_QUICKTASK_NEW'); + $permission = $params->get('menu-quicktask-permission'); + $scope = $current->scope !== 'default' ? $current->scope : null; + + if (!$permission || $user->authorise($permission, $scope)) { + echo ''; + echo ''; + echo '' . Text::_($title) . ''; + echo ''; + } +} + +if (!empty($current->dashboard)) { + $titleDashboard = Text::sprintf('MOD_MENU_DASHBOARD_LINK', Text::_($current->title)); + echo '' + . '' + . '' . $titleDashboard . '' + . ''; } -if ($link != '' && $current->target != '') -{ - echo '' - . $iconClass - . '' . $itemImage . Text::_($current->title) . '' . $ajax . ''; -} -elseif ($link != '' && $current->type !== 'separator') -{ - echo '' - . $iconClass - . '' . $itemImage . Text::_($current->title) . '' . $iconImage . ''; -} -elseif ($current->title != '' && $current->type !== 'separator') -{ - echo '' - . $iconClass - . ''. $itemImage . Text::_($current->title) . '' . $ajax . ''; -} -elseif ($current->title != '' && $current->type === 'separator') -{ - echo '' . Text::_($current->title) . '' . $ajax; -} -else -{ - echo '' . Text::_($current->title) . '' . $ajax; -} +// Recurse through children if they exist +if ($this->enabled && $current->hasChildren()) { + if ($current->level > 1) { + $id = $current->id ? ' id="menu-' . strtolower($current->id) . '"' : ''; -if ($currentParams->get('menu-quicktask') && (int) $this->params->get('shownew', 1) === 1) -{ - $params = $current->getParams(); - $user = $this->application->getIdentity(); - $link = $params->get('menu-quicktask'); - $icon = $params->get('menu-quicktask-icon', 'plus'); - $title = $params->get('menu-quicktask-title', 'MOD_MENU_QUICKTASK_NEW'); - $permission = $params->get('menu-quicktask-permission'); - $scope = $current->scope !== 'default' ? $current->scope : null; - - if (!$permission || $user->authorise($permission, $scope)) - { - echo ''; - echo ''; - echo '' . Text::_($title) . ''; - echo ''; - } -} + echo '' . "\n"; + } else { + echo '
      ' . "\n"; + } -if (!empty($current->dashboard)) -{ - $titleDashboard = Text::sprintf('MOD_MENU_DASHBOARD_LINK', Text::_($current->title)); - echo '' - . '' - . '' . $titleDashboard . '' - . ''; -} + // WARNING: Do not use direct 'include' or 'require' as it is important to isolate the scope for each call + $this->renderSubmenu(__FILE__, $current); -// Recurse through children if they exist -if ($this->enabled && $current->hasChildren()) -{ - if ($current->level > 1) - { - $id = $current->id ? ' id="menu-' . strtolower($current->id) . '"' : ''; - - echo '' . "\n"; - } - else - { - echo '
        ' . "\n"; - } - - // WARNING: Do not use direct 'include' or 'require' as it is important to isolate the scope for each call - $this->renderSubmenu(__FILE__, $current); - - echo "
      \n"; + echo "
    \n"; } echo "
  • \n"; diff --git a/administrator/modules/mod_messages/mod_messages.php b/administrator/modules/mod_messages/mod_messages.php index 296f4e1109a59..36ce8130708c9 100644 --- a/administrator/modules/mod_messages/mod_messages.php +++ b/administrator/modules/mod_messages/mod_messages.php @@ -1,4 +1,5 @@ getIdentity()->authorise('core.login.admin') || !$app->getIdentity()->authorise('core.manage', 'com_messages')) -{ - return; +if (!$app->getIdentity()->authorise('core.login.admin') || !$app->getIdentity()->authorise('core.manage', 'com_messages')) { + return; } // Try to get the items from the messages model -try -{ - /** @var \Joomla\Component\Messages\Administrator\Model\MessagesModel $messagesModel */ - $messagesModel = $app->bootComponent('com_messages')->getMVCFactory() - ->createModel('Messages', 'Administrator', ['ignore_request' => true]); - $messagesModel->setState('filter.state', 0); - $messages = $messagesModel->getItems(); -} -catch (RuntimeException $e) -{ - $messages = []; +try { + /** @var \Joomla\Component\Messages\Administrator\Model\MessagesModel $messagesModel */ + $messagesModel = $app->bootComponent('com_messages')->getMVCFactory() + ->createModel('Messages', 'Administrator', ['ignore_request' => true]); + $messagesModel->setState('filter.state', 0); + $messages = $messagesModel->getItems(); +} catch (RuntimeException $e) { + $messages = []; - // Still render the error message from the Exception object - $app->enqueueMessage($e->getMessage(), 'error'); + // Still render the error message from the Exception object + $app->enqueueMessage($e->getMessage(), 'error'); } $countUnread = count($messages); diff --git a/administrator/modules/mod_messages/tmpl/default.php b/administrator/modules/mod_messages/tmpl/default.php index 43da51c9c8d8d..77e03cef5e79e 100644 --- a/administrator/modules/mod_messages/tmpl/default.php +++ b/administrator/modules/mod_messages/tmpl/default.php @@ -1,4 +1,5 @@ input->getBool('hidemainmenu'); -if ($hideLinks || $countUnread < 1) -{ - return; +if ($hideLinks || $countUnread < 1) { + return; } $route = 'index.php?option=com_messages&view=messages'; ?> -
    -
    - - -
    -
    -
    - -
    +
    +
    + + +
    +
    +
    + +
    diff --git a/administrator/modules/mod_multilangstatus/mod_multilangstatus.php b/administrator/modules/mod_multilangstatus/mod_multilangstatus.php index 463f03f1b4774..959e1fb4fefe9 100644 --- a/administrator/modules/mod_multilangstatus/mod_multilangstatus.php +++ b/administrator/modules/mod_multilangstatus/mod_multilangstatus.php @@ -1,4 +1,5 @@ input->getBool('hidemainmenu'); -if (!$multilanguageEnabled || $hideLinks) -{ - return; +if (!$multilanguageEnabled || $hideLinks) { + return; } $modalHTML = HTMLHelper::_( - 'bootstrap.renderModal', - 'multiLangModal', - array( - 'title' => Text::_('MOD_MULTILANGSTATUS'), - 'url' => Route::_('index.php?option=com_languages&view=multilangstatus&tmpl=component'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) + 'bootstrap.renderModal', + 'multiLangModal', + array( + 'title' => Text::_('MOD_MULTILANGSTATUS'), + 'url' => Route::_('index.php?option=com_languages&view=multilangstatus&tmpl=component'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) ); $app->getDocument()->getWebAssetManager() - ->registerAndUseScript('mod_multilangstatus.admin', 'mod_multilangstatus/admin-multilangstatus.min.js', [], ['type' => 'module', 'defer' => true]); + ->registerAndUseScript('mod_multilangstatus.admin', 'mod_multilangstatus/admin-multilangstatus.min.js', [], ['type' => 'module', 'defer' => true]); ?> -
    - -
    -
    - -
    +
    + +
    +
    + +
    diff --git a/administrator/modules/mod_popular/mod_popular.php b/administrator/modules/mod_popular/mod_popular.php index 9b6d859456671..6e283cfd5864c 100644 --- a/administrator/modules/mod_popular/mod_popular.php +++ b/administrator/modules/mod_popular/mod_popular.php @@ -1,4 +1,5 @@ get('automatic_title', 0)) -{ - $module->title = PopularHelper::getTitle($params); +if ($params->get('automatic_title', 0)) { + $module->title = PopularHelper::getTitle($params); } // If recording of hits is disabled. -if (!ComponentHelper::getParams('com_content')->get('record_hits', 1)) -{ - echo LayoutHelper::render('joomla.content.emptystate_module', [ - 'title' => 'JGLOBAL_RECORD_HITS_DISABLED', - 'icon' => 'icon-minus-circle', - ] - ); - - return; +if (!ComponentHelper::getParams('com_content')->get('record_hits', 1)) { + echo LayoutHelper::render('joomla.content.emptystate_module', [ + 'title' => 'JGLOBAL_RECORD_HITS_DISABLED', + 'icon' => 'icon-minus-circle', + ]); + + return; } // If there are some articles to display. -if (count($list)) -{ - require ModuleHelper::getLayoutPath('mod_popular', $params->get('layout', 'default')); +if (count($list)) { + require ModuleHelper::getLayoutPath('mod_popular', $params->get('layout', 'default')); - return; + return; } // If there are no articles to display, show empty state. $app->getLanguage()->load('com_content'); echo LayoutHelper::render('joomla.content.emptystate_module', [ - 'textPrefix' => 'COM_CONTENT', - 'icon' => 'icon-copy', - ] -); + 'textPrefix' => 'COM_CONTENT', + 'icon' => 'icon-copy', + ]); diff --git a/administrator/modules/mod_popular/src/Helper/PopularHelper.php b/administrator/modules/mod_popular/src/Helper/PopularHelper.php index 1572b9b821013..24c7e210a9864 100644 --- a/administrator/modules/mod_popular/src/Helper/PopularHelper.php +++ b/administrator/modules/mod_popular/src/Helper/PopularHelper.php @@ -1,4 +1,5 @@ setState('list.select', 'a.id, a.title, a.checked_out, a.checked_out_time, ' . - ' a.publish_up, a.hits' - ); - - // Set Ordering filter - $model->setState('list.ordering', 'a.hits'); - $model->setState('list.direction', 'DESC'); - - // Set Category Filter - $categoryId = $params->get('catid', null); - - if (is_numeric($categoryId)) - { - $model->setState('filter.category_id', $categoryId); - } - - // Set User Filter. - $userId = $user->get('id'); - - switch ($params->get('user_id', '0')) - { - case 'by_me': - $model->setState('filter.author_id', $userId); - break; - - case 'not_me': - $model->setState('filter.author_id', $userId); - $model->setState('filter.author_id.include', false); - break; - } - - // Set the Start and Limit - $model->setState('list.start', 0); - $model->setState('list.limit', $params->get('count', 5)); - - $items = $model->getItems(); - - if ($error = $model->getError()) - { - throw new \Exception($error, 500); - } - - // Set the links - foreach ($items as &$item) - { - $item->link = ''; - - if ($user->authorise('core.edit', 'com_content.article.' . $item->id) - || ($user->authorise('core.edit.own', 'com_content.article.' . $item->id) && ($userId === $item->created_by))) - { - $item->link = Route::_('index.php?option=com_content&task=article.edit&id=' . $item->id); - } - } - - return $items; - } - - /** - * Get the alternate title for the module - * - * @param Registry $params The module parameters. - * - * @return string The alternate title for the module. - */ - public static function getTitle($params) - { - $who = $params->get('user_id', 0); - $catid = (int) $params->get('catid', null); - $title = ''; - - if ($catid) - { - $category = Categories::getInstance('Content')->get($catid); - $title = Text::_('MOD_POPULAR_UNEXISTING'); - - if ($category) - { - $title = $category->title; - } - } - - return Text::plural( - 'MOD_POPULAR_TITLE' . ($catid ? '_CATEGORY' : '') . ($who != '0' ? "_$who" : ''), - (int) $params->get('count', 5), - $title - ); - } + /** + * Get a list of the most popular articles. + * + * @param Registry &$params The module parameters. + * @param ArticlesModel $model The model. + * + * @return mixed An array of articles, or false on error. + * + * @throws \Exception + */ + public static function getList(Registry &$params, ArticlesModel $model) + { + $user = Factory::getUser(); + + // Set List SELECT + $model->setState('list.select', 'a.id, a.title, a.checked_out, a.checked_out_time, ' . + ' a.publish_up, a.hits'); + + // Set Ordering filter + $model->setState('list.ordering', 'a.hits'); + $model->setState('list.direction', 'DESC'); + + // Set Category Filter + $categoryId = $params->get('catid', null); + + if (is_numeric($categoryId)) { + $model->setState('filter.category_id', $categoryId); + } + + // Set User Filter. + $userId = $user->get('id'); + + switch ($params->get('user_id', '0')) { + case 'by_me': + $model->setState('filter.author_id', $userId); + break; + + case 'not_me': + $model->setState('filter.author_id', $userId); + $model->setState('filter.author_id.include', false); + break; + } + + // Set the Start and Limit + $model->setState('list.start', 0); + $model->setState('list.limit', $params->get('count', 5)); + + $items = $model->getItems(); + + if ($error = $model->getError()) { + throw new \Exception($error, 500); + } + + // Set the links + foreach ($items as &$item) { + $item->link = ''; + + if ( + $user->authorise('core.edit', 'com_content.article.' . $item->id) + || ($user->authorise('core.edit.own', 'com_content.article.' . $item->id) && ($userId === $item->created_by)) + ) { + $item->link = Route::_('index.php?option=com_content&task=article.edit&id=' . $item->id); + } + } + + return $items; + } + + /** + * Get the alternate title for the module + * + * @param Registry $params The module parameters. + * + * @return string The alternate title for the module. + */ + public static function getTitle($params) + { + $who = $params->get('user_id', 0); + $catid = (int) $params->get('catid', null); + $title = ''; + + if ($catid) { + $category = Categories::getInstance('Content')->get($catid); + $title = Text::_('MOD_POPULAR_UNEXISTING'); + + if ($category) { + $title = $category->title; + } + } + + return Text::plural( + 'MOD_POPULAR_TITLE' . ($catid ? '_CATEGORY' : '') . ($who != '0' ? "_$who" : ''), + (int) $params->get('count', 5), + $title + ); + } } diff --git a/administrator/modules/mod_popular/tmpl/default.php b/administrator/modules/mod_popular/tmpl/default.php index 6992abc8befe8..4949093a8177e 100644 --- a/administrator/modules/mod_popular/tmpl/default.php +++ b/administrator/modules/mod_popular/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - - $item) : ?> - - hits; ?> - = 10000 ? 'danger' : ($hits >= 1000 ? 'warning' : ($hits >= 100 ? 'info' : 'secondary'))); ?> - - - - - - - - - - - - + + + + + + + + + + + $item) : ?> + + hits; ?> + = 10000 ? 'danger' : ($hits >= 1000 ? 'warning' : ($hits >= 100 ? 'info' : 'secondary'))); ?> + + + + + + + + + + + +
    title; ?>
    - checked_out) : ?> - editor, $item->checked_out_time); ?> - - link) : ?> - - title, ENT_QUOTES, 'UTF-8'); ?> - - - title, ENT_QUOTES, 'UTF-8'); ?> - - - hits; ?> - - publish_up, Text::_('DATE_FORMAT_LC4')); ?> -
    - -
    title; ?>
    + checked_out) : ?> + editor, $item->checked_out_time); ?> + + link) : ?> + + title, ENT_QUOTES, 'UTF-8'); ?> + + + title, ENT_QUOTES, 'UTF-8'); ?> + + + hits; ?> + + publish_up, Text::_('DATE_FORMAT_LC4')); ?> +
    + +
    diff --git a/administrator/modules/mod_post_installation_messages/mod_post_installation_messages.php b/administrator/modules/mod_post_installation_messages/mod_post_installation_messages.php index 1812161ddf736..9f9268c4ccf59 100644 --- a/administrator/modules/mod_post_installation_messages/mod_post_installation_messages.php +++ b/administrator/modules/mod_post_installation_messages/mod_post_installation_messages.php @@ -1,4 +1,5 @@ bootComponent('com_postinstall')->getMVCFactory() - ->createModel('Messages', 'Administrator', ['ignore_request' => true]); - $messagesCount = $messagesModel->getItemsCount(); -} -catch (RuntimeException $e) -{ - $messagesCount = 0; +try { + /** @var \Joomla\Component\Postinstall\Administrator\Model\MessagesModel $messagesModel */ + $messagesModel = $app->bootComponent('com_postinstall')->getMVCFactory() + ->createModel('Messages', 'Administrator', ['ignore_request' => true]); + $messagesCount = $messagesModel->getItemsCount(); +} catch (RuntimeException $e) { + $messagesCount = 0; - // Still render the error message from the Exception object - $app->enqueueMessage($e->getMessage(), 'error'); + // Still render the error message from the Exception object + $app->enqueueMessage($e->getMessage(), 'error'); } $joomlaFilesExtensionId = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; diff --git a/administrator/modules/mod_post_installation_messages/tmpl/default.php b/administrator/modules/mod_post_installation_messages/tmpl/default.php index 8efb6503ef488..f2b9cef548449 100644 --- a/administrator/modules/mod_post_installation_messages/tmpl/default.php +++ b/administrator/modules/mod_post_installation_messages/tmpl/default.php @@ -1,4 +1,5 @@ input->getBool('hidemainmenu'); -if ($hideLinks || $messagesCount < 1) -{ - return; +if ($hideLinks || $messagesCount < 1) { + return; } ?> getIdentity()->authorise('core.manage', 'com_postinstall')) : ?> - -
    -
    - - -
    -
    -
    - -
    -
    + +
    +
    + + +
    +
    +
    + +
    +
    diff --git a/administrator/modules/mod_privacy_dashboard/mod_privacy_dashboard.php b/administrator/modules/mod_privacy_dashboard/mod_privacy_dashboard.php index a58da39b7ee66..28c4bb5e75f88 100644 --- a/administrator/modules/mod_privacy_dashboard/mod_privacy_dashboard.php +++ b/administrator/modules/mod_privacy_dashboard/mod_privacy_dashboard.php @@ -1,4 +1,5 @@ getIdentity()->authorise('core.admin')) -{ - return; +if (!$app->getIdentity()->authorise('core.admin')) { + return; } // Boot component to ensure HTML helpers are loaded @@ -25,19 +25,15 @@ // Load the privacy component language file. $lang = $app->getLanguage(); $lang->load('com_privacy', JPATH_ADMINISTRATOR) - || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); + || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); $list = PrivacyDashboardHelper::getData(); -if (count($list)) -{ - require ModuleHelper::getLayoutPath('mod_privacy_dashboard', $params->get('layout', 'default')); -} -else -{ - echo LayoutHelper::render('joomla.content.emptystate_module', [ - 'textPrefix' => 'COM_PRIVACY_REQUESTS', - 'icon' => 'icon-lock', - ] - ); +if (count($list)) { + require ModuleHelper::getLayoutPath('mod_privacy_dashboard', $params->get('layout', 'default')); +} else { + echo LayoutHelper::render('joomla.content.emptystate_module', [ + 'textPrefix' => 'COM_PRIVACY_REQUESTS', + 'icon' => 'icon-lock', + ]); } diff --git a/administrator/modules/mod_privacy_dashboard/src/Helper/PrivacyDashboardHelper.php b/administrator/modules/mod_privacy_dashboard/src/Helper/PrivacyDashboardHelper.php index 7a7088d69cc8e..537acac30481d 100644 --- a/administrator/modules/mod_privacy_dashboard/src/Helper/PrivacyDashboardHelper.php +++ b/administrator/modules/mod_privacy_dashboard/src/Helper/PrivacyDashboardHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select( - [ - 'COUNT(*) AS count', - $db->quoteName('status'), - $db->quoteName('request_type'), - ] - ) - ->from($db->quoteName('#__privacy_requests')) - ->group($db->quoteName('status')) - ->group($db->quoteName('request_type')); + /** + * Method to retrieve information about the site privacy requests + * + * @return array Array containing site privacy requests + * + * @since 3.9.0 + */ + public static function getData() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + 'COUNT(*) AS count', + $db->quoteName('status'), + $db->quoteName('request_type'), + ] + ) + ->from($db->quoteName('#__privacy_requests')) + ->group($db->quoteName('status')) + ->group($db->quoteName('request_type')); - $db->setQuery($query); + $db->setQuery($query); - try - { - return $db->loadObjectList(); - } - catch (ExecutionFailureException $e) - { - return []; - } - } + try { + return $db->loadObjectList(); + } catch (ExecutionFailureException $e) { + return []; + } + } } diff --git a/administrator/modules/mod_privacy_dashboard/tmpl/default.php b/administrator/modules/mod_privacy_dashboard/tmpl/default.php index 0450c0f4746ba..c82f8205ede0a 100644 --- a/administrator/modules/mod_privacy_dashboard/tmpl/default.php +++ b/administrator/modules/mod_privacy_dashboard/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - - $item) : ?> - status, array(0, 1))) : ?> - count; ?> - - count; ?> - - - - - - - - - - - - + + + + + + + + + + + $item) : ?> + status, array(0, 1))) : ?> + count; ?> + + count; ?> + + + + + + + + + + + +
    title; ?>
    - - request_type); ?> - - - status); ?> - - count; ?> -
    - -
    title; ?>
    + + request_type); ?> + + + status); ?> + + count; ?> +
    + +
    -
    -
    -
    -
    +
    +
    +
    +
    diff --git a/administrator/modules/mod_privacy_status/mod_privacy_status.php b/administrator/modules/mod_privacy_status/mod_privacy_status.php index 964b15118d67d..afa43af91bbf1 100644 --- a/administrator/modules/mod_privacy_status/mod_privacy_status.php +++ b/administrator/modules/mod_privacy_status/mod_privacy_status.php @@ -1,4 +1,5 @@ getIdentity()->authorise('core.admin')) -{ - return; +if (!$app->getIdentity()->authorise('core.admin')) { + return; } // Boot component to ensure HTML helpers are loaded @@ -27,7 +27,7 @@ // Load the privacy component language file. $lang = $app->getLanguage(); $lang->load('com_privacy', JPATH_ADMINISTRATOR) - || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); + || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); $privacyPolicyInfo = PrivacyStatusHelper::getPrivacyPolicyInfo(); $requestFormPublished = PrivacyStatusHelper::getRequestFormPublished(); diff --git a/administrator/modules/mod_privacy_status/src/Helper/PrivacyStatusHelper.php b/administrator/modules/mod_privacy_status/src/Helper/PrivacyStatusHelper.php index 2bbeab6647f34..6d4c2ad94d2bc 100644 --- a/administrator/modules/mod_privacy_status/src/Helper/PrivacyStatusHelper.php +++ b/administrator/modules/mod_privacy_status/src/Helper/PrivacyStatusHelper.php @@ -1,4 +1,5 @@ false, - 'articlePublished' => false, - 'editLink' => '', - ]; - - /* - * Prior to 3.9.0 it was common for a plugin such as the User - Profile plugin to define a privacy policy or - * terms of service article, therefore we will also import the user plugin group to process this event. - */ - PluginHelper::importPlugin('privacy'); - PluginHelper::importPlugin('user'); - - Factory::getApplication()->triggerEvent('onPrivacyCheckPrivacyPolicyPublished', [&$policy]); - - return $policy; - } - - /** - * Check whether there is a menu item for the request form - * - * @return array Array containing a status of whether a menu is published for the request form and its current link - * - * @since 4.0.0 - */ - public static function getRequestFormPublished() - { - $status = [ - 'exists' => false, - 'published' => false, - 'link' => '', - ]; - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('id'), - $db->quoteName('published'), - $db->quoteName('language'), - ] - ) - ->from($db->quoteName('#__menu')) - ->where( - [ - $db->quoteName('client_id') . ' = 0', - $db->quoteName('link') . ' = ' . $db->quote('index.php?option=com_privacy&view=request'), - ] - ) - ->setLimit(1); - $db->setQuery($query); - - $menuItem = $db->loadObject(); - - // Check if the menu item exists in database - if ($menuItem) - { - $status['exists'] = true; - - // Check if the menu item is published - if ($menuItem->published == 1) - { - $status['published'] = true; - } - - // Add language to the url if the site is multilingual - if (Multilanguage::isEnabled() && $menuItem->language && $menuItem->language !== '*') - { - $lang = '&lang=' . $menuItem->language; - } - else - { - $lang = ''; - } - } - - $linkMode = Factory::getApplication()->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - if (!$menuItem) - { - if (Multilanguage::isEnabled()) - { - // Find the Itemid of the home menu item tagged to the site default language - $params = ComponentHelper::getParams('com_languages'); - $defaultSiteLanguage = $params->get('site'); - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->where( - [ - $db->quoteName('client_id') . ' = 0', - $db->quoteName('home') . ' = 1', - $db->quoteName('language') . ' = :language', - ] - ) - ->bind(':language', $defaultSiteLanguage) - ->setLimit(1); - $db->setQuery($query); - - $homeId = (int) $db->loadResult(); - $itemId = $homeId ? '&Itemid=' . $homeId : ''; - } - else - { - $itemId = ''; - } - - $status['link'] = Route::link('site', 'index.php?option=com_privacy&view=request' . $itemId, true, $linkMode); - } - else - { - $status['link'] = Route::link('site', 'index.php?Itemid=' . $menuItem->id . $lang, true, $linkMode); - } - - return $status; - } - - /** - * Method to return number privacy requests older than X days. - * - * @return integer - * - * @since 4.0.0 - */ - public static function getNumberUrgentRequests() - { - // Load the parameters. - $params = ComponentHelper::getComponent('com_privacy')->getParams(); - $notify = (int) $params->get('notify', 14); - $now = Factory::getDate()->toSql(); - $period = '-' . $notify; - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $query->select('COUNT(*)') - ->from($db->quoteName('#__privacy_requests')) - ->where( - [ - $db->quoteName('status') . ' = 1', - $query->dateAdd($db->quote($now), $period, 'DAY') . ' > ' . $db->quoteName('requested_at'), - ] - ); - $db->setQuery($query); - - return (int) $db->loadResult(); - } + /** + * Get the information about the published privacy policy + * + * @return array Array containing a status of whether a privacy policy is set and a link to the policy document for editing + * + * @since 4.0.0 + */ + public static function getPrivacyPolicyInfo() + { + $policy = [ + 'published' => false, + 'articlePublished' => false, + 'editLink' => '', + ]; + + /* + * Prior to 3.9.0 it was common for a plugin such as the User - Profile plugin to define a privacy policy or + * terms of service article, therefore we will also import the user plugin group to process this event. + */ + PluginHelper::importPlugin('privacy'); + PluginHelper::importPlugin('user'); + + Factory::getApplication()->triggerEvent('onPrivacyCheckPrivacyPolicyPublished', [&$policy]); + + return $policy; + } + + /** + * Check whether there is a menu item for the request form + * + * @return array Array containing a status of whether a menu is published for the request form and its current link + * + * @since 4.0.0 + */ + public static function getRequestFormPublished() + { + $status = [ + 'exists' => false, + 'published' => false, + 'link' => '', + ]; + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('id'), + $db->quoteName('published'), + $db->quoteName('language'), + ] + ) + ->from($db->quoteName('#__menu')) + ->where( + [ + $db->quoteName('client_id') . ' = 0', + $db->quoteName('link') . ' = ' . $db->quote('index.php?option=com_privacy&view=request'), + ] + ) + ->setLimit(1); + $db->setQuery($query); + + $menuItem = $db->loadObject(); + + // Check if the menu item exists in database + if ($menuItem) { + $status['exists'] = true; + + // Check if the menu item is published + if ($menuItem->published == 1) { + $status['published'] = true; + } + + // Add language to the url if the site is multilingual + if (Multilanguage::isEnabled() && $menuItem->language && $menuItem->language !== '*') { + $lang = '&lang=' . $menuItem->language; + } else { + $lang = ''; + } + } + + $linkMode = Factory::getApplication()->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + if (!$menuItem) { + if (Multilanguage::isEnabled()) { + // Find the Itemid of the home menu item tagged to the site default language + $params = ComponentHelper::getParams('com_languages'); + $defaultSiteLanguage = $params->get('site'); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->where( + [ + $db->quoteName('client_id') . ' = 0', + $db->quoteName('home') . ' = 1', + $db->quoteName('language') . ' = :language', + ] + ) + ->bind(':language', $defaultSiteLanguage) + ->setLimit(1); + $db->setQuery($query); + + $homeId = (int) $db->loadResult(); + $itemId = $homeId ? '&Itemid=' . $homeId : ''; + } else { + $itemId = ''; + } + + $status['link'] = Route::link('site', 'index.php?option=com_privacy&view=request' . $itemId, true, $linkMode); + } else { + $status['link'] = Route::link('site', 'index.php?Itemid=' . $menuItem->id . $lang, true, $linkMode); + } + + return $status; + } + + /** + * Method to return number privacy requests older than X days. + * + * @return integer + * + * @since 4.0.0 + */ + public static function getNumberUrgentRequests() + { + // Load the parameters. + $params = ComponentHelper::getComponent('com_privacy')->getParams(); + $notify = (int) $params->get('notify', 14); + $now = Factory::getDate()->toSql(); + $period = '-' . $notify; + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select('COUNT(*)') + ->from($db->quoteName('#__privacy_requests')) + ->where( + [ + $db->quoteName('status') . ' = 1', + $query->dateAdd($db->quote($now), $period, 'DAY') . ' > ' . $db->quoteName('requested_at'), + ] + ); + $db->setQuery($query); + + return (int) $db->loadResult(); + } } diff --git a/administrator/modules/mod_privacy_status/tmpl/default.php b/administrator/modules/mod_privacy_status/tmpl/default.php index 35bc270b8ccc3..5b1c3663b872e 100644 --- a/administrator/modules/mod_privacy_status/tmpl/default.php +++ b/administrator/modules/mod_privacy_status/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    - - - - - - - - - - - - - - - - - -
    - - - - - - -
    - - - - - - - - - - - - - - - - - -
    - - - -
    - - - - - - - - - - - - -
    - - 0) : ?> - - -
    - - - - - - - - - - - - - -
    - - -
    - -
    - - - - - - - - - - - - - - - - - -
    + + + + + + + + + + + + + + + + + +
    + + + + + + +
    + + + + + + + + + + + + + + + + + +
    + + + +
    + + + + + + + + + + + + +
    + + 0) : ?> + + +
    + + + + + + + + + + + + + +
    + + +
    + +
    + + + + + + + + + + + + + + + + + +
    diff --git a/administrator/modules/mod_quickicon/services/provider.php b/administrator/modules/mod_quickicon/services/provider.php index 86c396968362b..34f4e43c5ac33 100644 --- a/administrator/modules/mod_quickicon/services/provider.php +++ b/administrator/modules/mod_quickicon/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new ModuleDispatcherFactory('\\Joomla\\Module\\Quickicon')); - $container->registerServiceProvider(new HelperFactory('\\Joomla\\Module\\Quickicon\\Administrator\\Helper')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new ModuleDispatcherFactory('\\Joomla\\Module\\Quickicon')); + $container->registerServiceProvider(new HelperFactory('\\Joomla\\Module\\Quickicon\\Administrator\\Helper')); - $container->registerServiceProvider(new Module); - } + $container->registerServiceProvider(new Module()); + } }; diff --git a/administrator/modules/mod_quickicon/src/Dispatcher/Dispatcher.php b/administrator/modules/mod_quickicon/src/Dispatcher/Dispatcher.php index f047562713c66..0baa4156111c7 100644 --- a/administrator/modules/mod_quickicon/src/Dispatcher/Dispatcher.php +++ b/administrator/modules/mod_quickicon/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->bootModule('mod_quickicon', 'administrator')->getHelper('QuickIconHelper'); - $data['buttons'] = $helper->getButtons($data['params'], $this->getApplication()); + $helper = $this->app->bootModule('mod_quickicon', 'administrator')->getHelper('QuickIconHelper'); + $data['buttons'] = $helper->getButtons($data['params'], $this->getApplication()); - return $data; - } + return $data; + } } diff --git a/administrator/modules/mod_quickicon/src/Event/QuickIconsEvent.php b/administrator/modules/mod_quickicon/src/Event/QuickIconsEvent.php index 3e0da89908299..6af53bf9fcfbf 100644 --- a/administrator/modules/mod_quickicon/src/Event/QuickIconsEvent.php +++ b/administrator/modules/mod_quickicon/src/Event/QuickIconsEvent.php @@ -1,4 +1,5 @@ context; - } - - /** - * Set the event context - * - * @param string $context The event context - * - * @return string - * - * @since 4.0.0 - */ - public function setContext($context) - { - $this->context = $context; - - return $context; - } + /** + * The event context + * + * @var string + * @since 4.0.0 + */ + private $context; + + /** + * Get the event context + * + * @return string + * + * @since 4.0.0 + */ + public function getContext() + { + return $this->context; + } + + /** + * Set the event context + * + * @param string $context The event context + * + * @return string + * + * @since 4.0.0 + */ + public function setContext($context) + { + $this->context = $context; + + return $context; + } } diff --git a/administrator/modules/mod_quickicon/src/Helper/QuickIconHelper.php b/administrator/modules/mod_quickicon/src/Helper/QuickIconHelper.php index e6496e281e254..a939fa29bd5b2 100644 --- a/administrator/modules/mod_quickicon/src/Helper/QuickIconHelper.php +++ b/administrator/modules/mod_quickicon/src/Helper/QuickIconHelper.php @@ -1,4 +1,5 @@ get('context', 'mod_quickicon'); - - if (!isset($this->buttons[$key])) - { - // Load mod_quickicon language file in case this method is called before rendering the module - $application->getLanguage()->load('mod_quickicon'); - - $this->buttons[$key] = []; - - if ($params->get('show_users')) - { - $tmp = [ - 'image' => 'icon-users', - 'link' => Route::_('index.php?option=com_users&view=users'), - 'linkadd' => Route::_('index.php?option=com_users&task=user.add'), - 'name' => 'MOD_QUICKICON_USER_MANAGER', - 'access' => array('core.manage', 'com_users', 'core.create', 'com_users'), - 'group' => 'MOD_QUICKICON_SITE', - ]; - - if ($params->get('show_users') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_users&task=users.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_menuitems')) - { - $tmp = [ - 'image' => 'icon-list', - 'link' => Route::_('index.php?option=com_menus&view=items&menutype='), - 'linkadd' => Route::_('index.php?option=com_menus&task=item.add'), - 'name' => 'MOD_QUICKICON_MENUITEMS_MANAGER', - 'access' => array('core.manage', 'com_menus', 'core.create', 'com_menus'), - 'group' => 'MOD_QUICKICON_STRUCTURE', - ]; - - if ($params->get('show_menuitems') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_menus&task=items.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_articles')) - { - $tmp = [ - 'image' => 'icon-file-alt', - 'link' => Route::_('index.php?option=com_content&view=articles'), - 'linkadd' => Route::_('index.php?option=com_content&task=article.add'), - 'name' => 'MOD_QUICKICON_ARTICLE_MANAGER', - 'access' => array('core.manage', 'com_content', 'core.create', 'com_content'), - 'group' => 'MOD_QUICKICON_SITE', - ]; - - if ($params->get('show_articles') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_content&task=articles.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_tags')) - { - $tmp = [ - 'image' => 'icon-tag', - 'link' => Route::_('index.php?option=com_tags&view=tags'), - 'linkadd' => Route::_('index.php?option=com_tags&task=tag.edit'), - 'name' => 'MOD_QUICKICON_TAGS_MANAGER', - 'access' => array('core.manage', 'com_tags', 'core.create', 'com_tags'), - 'group' => 'MOD_QUICKICON_SITE', - ]; - - if ($params->get('show_tags') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_tags&task=tags.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_categories')) - { - $tmp = [ - 'image' => 'icon-folder-open', - 'link' => Route::_('index.php?option=com_categories&view=categories&extension=com_content'), - 'linkadd' => Route::_('index.php?option=com_categories&task=category.add'), - 'name' => 'MOD_QUICKICON_CATEGORY_MANAGER', - 'access' => array('core.manage', 'com_categories', 'core.create', 'com_categories'), - 'group' => 'MOD_QUICKICON_SITE', - ]; - - if ($params->get('show_categories') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_categories&task=categories.getQuickiconContent&extension=content&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_media')) - { - $this->buttons[$key][] = [ - 'image' => 'icon-images', - 'link' => Route::_('index.php?option=com_media'), - 'name' => 'MOD_QUICKICON_MEDIA_MANAGER', - 'access' => array('core.manage', 'com_media'), - 'group' => 'MOD_QUICKICON_SITE', - ]; - } - - if ($params->get('show_modules')) - { - $tmp = [ - 'image' => 'icon-cube', - 'link' => Route::_('index.php?option=com_modules&view=modules&client_id=0'), - 'linkadd' => Route::_('index.php?option=com_modules&view=select&client_id=0'), - 'name' => 'MOD_QUICKICON_MODULE_MANAGER', - 'access' => array('core.manage', 'com_modules'), - 'group' => 'MOD_QUICKICON_SITE' - ]; - - if ($params->get('show_modules') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_modules&task=modules.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_plugins')) - { - $tmp = [ - 'image' => 'icon-plug', - 'link' => Route::_('index.php?option=com_plugins'), - 'name' => 'MOD_QUICKICON_PLUGIN_MANAGER', - 'access' => array('core.manage', 'com_plugins'), - 'group' => 'MOD_QUICKICON_SITE' - ]; - - if ($params->get('show_plugins') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_plugins&task=plugins.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_template_styles')) - { - $this->buttons[$key][] = [ - 'image' => 'icon-paint-brush', - 'link' => Route::_('index.php?option=com_templates&view=styles&client_id=0'), - 'name' => 'MOD_QUICKICON_TEMPLATE_STYLES', - 'access' => array('core.admin', 'com_templates'), - 'group' => 'MOD_QUICKICON_SITE' - ]; - } - - if ($params->get('show_template_code')) - { - $this->buttons[$key][] = [ - 'image' => 'icon-code', - 'link' => Route::_('index.php?option=com_templates&view=templates&client_id=0'), - 'name' => 'MOD_QUICKICON_TEMPLATE_CODE', - 'access' => array('core.admin', 'com_templates'), - 'group' => 'MOD_QUICKICON_SITE' - ]; - } - - if ($params->get('show_checkin')) - { - $tmp = [ - 'image' => 'icon-unlock-alt', - 'link' => Route::_('index.php?option=com_checkin'), - 'name' => 'MOD_QUICKICON_CHECKINS', - 'access' => array('core.admin', 'com_checkin'), - 'group' => 'MOD_QUICKICON_SYSTEM' - ]; - - if ($params->get('show_checkin') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_checkin&task=getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_cache')) - { - $tmp = [ - 'image' => 'icon-cloud', - 'link' => Route::_('index.php?option=com_cache'), - 'name' => 'MOD_QUICKICON_CACHE', - 'access' => array('core.admin', 'com_cache'), - 'group' => 'MOD_QUICKICON_SYSTEM' - ]; - - if ($params->get('show_cache') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_cache&task=display.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_global')) - { - $this->buttons[$key][] = [ - 'image' => 'icon-cog', - 'link' => Route::_('index.php?option=com_config'), - 'name' => 'MOD_QUICKICON_GLOBAL_CONFIGURATION', - 'access' => array('core.manage', 'com_config', 'core.admin', 'com_config'), - 'group' => 'MOD_QUICKICON_SYSTEM', - ]; - } - - PluginHelper::importPlugin('quickicon'); - - $arrays = (array) $application->triggerEvent( - 'onGetIcons', - new QuickIconsEvent('onGetIcons', ['context' => $context]) - ); - - foreach ($arrays as $response) - { - if (!\is_array($response)) - { - continue; - } - - foreach ($response as $icon) - { - $default = array( - 'link' => null, - 'image' => null, - 'text' => null, - 'name' => null, - 'linkadd' => null, - 'access' => true, - 'class' => null, - 'group' => 'MOD_QUICKICON', - ); - - $icon = array_merge($default, $icon); - - if (!\is_null($icon['link']) && (!\is_null($icon['text']) || !\is_null($icon['name']))) - { - $this->buttons[$key][] = $icon; - } - } - } - } - - return $this->buttons[$key]; - } + /** + * Stack to hold buttons + * + * @var array[] + * @since 1.6 + */ + protected $buttons = array(); + + /** + * Helper method to return button list. + * + * This method returns the array by reference so it can be + * used to add custom buttons or remove default ones. + * + * @param Registry $params The module parameters + * @param CMSApplication $application The application + * + * @return array An array of buttons + * + * @since 1.6 + */ + public function getButtons(Registry $params, CMSApplication $application = null) + { + if ($application == null) { + $application = Factory::getApplication(); + } + + $key = (string) $params; + $context = (string) $params->get('context', 'mod_quickicon'); + + if (!isset($this->buttons[$key])) { + // Load mod_quickicon language file in case this method is called before rendering the module + $application->getLanguage()->load('mod_quickicon'); + + $this->buttons[$key] = []; + + if ($params->get('show_users')) { + $tmp = [ + 'image' => 'icon-users', + 'link' => Route::_('index.php?option=com_users&view=users'), + 'linkadd' => Route::_('index.php?option=com_users&task=user.add'), + 'name' => 'MOD_QUICKICON_USER_MANAGER', + 'access' => array('core.manage', 'com_users', 'core.create', 'com_users'), + 'group' => 'MOD_QUICKICON_SITE', + ]; + + if ($params->get('show_users') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_users&task=users.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_menuitems')) { + $tmp = [ + 'image' => 'icon-list', + 'link' => Route::_('index.php?option=com_menus&view=items&menutype='), + 'linkadd' => Route::_('index.php?option=com_menus&task=item.add'), + 'name' => 'MOD_QUICKICON_MENUITEMS_MANAGER', + 'access' => array('core.manage', 'com_menus', 'core.create', 'com_menus'), + 'group' => 'MOD_QUICKICON_STRUCTURE', + ]; + + if ($params->get('show_menuitems') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_menus&task=items.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_articles')) { + $tmp = [ + 'image' => 'icon-file-alt', + 'link' => Route::_('index.php?option=com_content&view=articles'), + 'linkadd' => Route::_('index.php?option=com_content&task=article.add'), + 'name' => 'MOD_QUICKICON_ARTICLE_MANAGER', + 'access' => array('core.manage', 'com_content', 'core.create', 'com_content'), + 'group' => 'MOD_QUICKICON_SITE', + ]; + + if ($params->get('show_articles') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_content&task=articles.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_tags')) { + $tmp = [ + 'image' => 'icon-tag', + 'link' => Route::_('index.php?option=com_tags&view=tags'), + 'linkadd' => Route::_('index.php?option=com_tags&task=tag.edit'), + 'name' => 'MOD_QUICKICON_TAGS_MANAGER', + 'access' => array('core.manage', 'com_tags', 'core.create', 'com_tags'), + 'group' => 'MOD_QUICKICON_SITE', + ]; + + if ($params->get('show_tags') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_tags&task=tags.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_categories')) { + $tmp = [ + 'image' => 'icon-folder-open', + 'link' => Route::_('index.php?option=com_categories&view=categories&extension=com_content'), + 'linkadd' => Route::_('index.php?option=com_categories&task=category.add'), + 'name' => 'MOD_QUICKICON_CATEGORY_MANAGER', + 'access' => array('core.manage', 'com_categories', 'core.create', 'com_categories'), + 'group' => 'MOD_QUICKICON_SITE', + ]; + + if ($params->get('show_categories') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_categories&task=categories.getQuickiconContent&extension=content&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_media')) { + $this->buttons[$key][] = [ + 'image' => 'icon-images', + 'link' => Route::_('index.php?option=com_media'), + 'name' => 'MOD_QUICKICON_MEDIA_MANAGER', + 'access' => array('core.manage', 'com_media'), + 'group' => 'MOD_QUICKICON_SITE', + ]; + } + + if ($params->get('show_modules')) { + $tmp = [ + 'image' => 'icon-cube', + 'link' => Route::_('index.php?option=com_modules&view=modules&client_id=0'), + 'linkadd' => Route::_('index.php?option=com_modules&view=select&client_id=0'), + 'name' => 'MOD_QUICKICON_MODULE_MANAGER', + 'access' => array('core.manage', 'com_modules'), + 'group' => 'MOD_QUICKICON_SITE' + ]; + + if ($params->get('show_modules') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_modules&task=modules.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_plugins')) { + $tmp = [ + 'image' => 'icon-plug', + 'link' => Route::_('index.php?option=com_plugins'), + 'name' => 'MOD_QUICKICON_PLUGIN_MANAGER', + 'access' => array('core.manage', 'com_plugins'), + 'group' => 'MOD_QUICKICON_SITE' + ]; + + if ($params->get('show_plugins') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_plugins&task=plugins.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_template_styles')) { + $this->buttons[$key][] = [ + 'image' => 'icon-paint-brush', + 'link' => Route::_('index.php?option=com_templates&view=styles&client_id=0'), + 'name' => 'MOD_QUICKICON_TEMPLATE_STYLES', + 'access' => array('core.admin', 'com_templates'), + 'group' => 'MOD_QUICKICON_SITE' + ]; + } + + if ($params->get('show_template_code')) { + $this->buttons[$key][] = [ + 'image' => 'icon-code', + 'link' => Route::_('index.php?option=com_templates&view=templates&client_id=0'), + 'name' => 'MOD_QUICKICON_TEMPLATE_CODE', + 'access' => array('core.admin', 'com_templates'), + 'group' => 'MOD_QUICKICON_SITE' + ]; + } + + if ($params->get('show_checkin')) { + $tmp = [ + 'image' => 'icon-unlock-alt', + 'link' => Route::_('index.php?option=com_checkin'), + 'name' => 'MOD_QUICKICON_CHECKINS', + 'access' => array('core.admin', 'com_checkin'), + 'group' => 'MOD_QUICKICON_SYSTEM' + ]; + + if ($params->get('show_checkin') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_checkin&task=getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_cache')) { + $tmp = [ + 'image' => 'icon-cloud', + 'link' => Route::_('index.php?option=com_cache'), + 'name' => 'MOD_QUICKICON_CACHE', + 'access' => array('core.admin', 'com_cache'), + 'group' => 'MOD_QUICKICON_SYSTEM' + ]; + + if ($params->get('show_cache') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_cache&task=display.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_global')) { + $this->buttons[$key][] = [ + 'image' => 'icon-cog', + 'link' => Route::_('index.php?option=com_config'), + 'name' => 'MOD_QUICKICON_GLOBAL_CONFIGURATION', + 'access' => array('core.manage', 'com_config', 'core.admin', 'com_config'), + 'group' => 'MOD_QUICKICON_SYSTEM', + ]; + } + + PluginHelper::importPlugin('quickicon'); + + $arrays = (array) $application->triggerEvent( + 'onGetIcons', + new QuickIconsEvent('onGetIcons', ['context' => $context]) + ); + + foreach ($arrays as $response) { + if (!\is_array($response)) { + continue; + } + + foreach ($response as $icon) { + $default = array( + 'link' => null, + 'image' => null, + 'text' => null, + 'name' => null, + 'linkadd' => null, + 'access' => true, + 'class' => null, + 'group' => 'MOD_QUICKICON', + ); + + $icon = array_merge($default, $icon); + + if (!\is_null($icon['link']) && (!\is_null($icon['text']) || !\is_null($icon['name']))) { + $this->buttons[$key][] = $icon; + } + } + } + } + + return $this->buttons[$key]; + } } diff --git a/administrator/modules/mod_quickicon/tmpl/default.php b/administrator/modules/mod_quickicon/tmpl/default.php index effa3b2e0d9a1..058db0db87e4d 100644 --- a/administrator/modules/mod_quickicon/tmpl/default.php +++ b/administrator/modules/mod_quickicon/tmpl/default.php @@ -1,4 +1,5 @@ - + diff --git a/administrator/modules/mod_sampledata/mod_sampledata.php b/administrator/modules/mod_sampledata/mod_sampledata.php index 7feac851a7841..79ffb01326e69 100644 --- a/administrator/modules/mod_sampledata/mod_sampledata.php +++ b/administrator/modules/mod_sampledata/mod_sampledata.php @@ -1,4 +1,5 @@ getDispatcher() - ->dispatch( - 'onSampledataGetOverview', - AbstractEvent::create( - 'onSampledataGetOverview', - [ - 'subject' => new \stdClass, - ] - ) - ) - ->getArgument('result') ?? []; - } + return Factory::getApplication() + ->getDispatcher() + ->dispatch( + 'onSampledataGetOverview', + AbstractEvent::create( + 'onSampledataGetOverview', + [ + 'subject' => new \stdClass(), + ] + ) + ) + ->getArgument('result') ?? []; + } } diff --git a/administrator/modules/mod_sampledata/tmpl/default.php b/administrator/modules/mod_sampledata/tmpl/default.php index 6effa8da33976..f11599975f1b0 100644 --- a/administrator/modules/mod_sampledata/tmpl/default.php +++ b/administrator/modules/mod_sampledata/tmpl/default.php @@ -1,4 +1,5 @@ getDocument()->getWebAssetManager() - ->registerAndUseScript('mod_sampledata', 'mod_sampledata/sampledata-process.js', [], ['type' => 'module'], ['core']); + ->registerAndUseScript('mod_sampledata', 'mod_sampledata/sampledata-process.js', [], ['type' => 'module'], ['core']); Text::script('MOD_SAMPLEDATA_COMPLETED'); Text::script('MOD_SAMPLEDATA_CONFIRM_START'); @@ -21,37 +22,37 @@ Text::script('MOD_SAMPLEDATA_INVALID_RESPONSE'); $app->getDocument()->addScriptOptions( - 'sample-data', - [ - 'icon' => Uri::root(true) . '/media/system/images/ajax-loader.gif', - ] + 'sample-data', + [ + 'icon' => Uri::root(true) . '/media/system/images/ajax-loader.gif', + ] ); ?> -
      - $item) : ?> -
    • -
      -
      - - title, ENT_QUOTES, 'UTF-8'); ?> -
      - -
      -

      description; ?>

      -
    • - -
    • -
      -
      -
      -
    • - -
    - - - +
      + $item) : ?> +
    • +
      +
      + + title, ENT_QUOTES, 'UTF-8'); ?> +
      + +
      +

      description; ?>

      +
    • + +
    • +
      +
      +
      +
    • + +
    + + + diff --git a/administrator/modules/mod_stats_admin/mod_stats_admin.php b/administrator/modules/mod_stats_admin/mod_stats_admin.php index 1e23913e04cb4..49e9cb3f8c2d1 100644 --- a/administrator/modules/mod_stats_admin/mod_stats_admin.php +++ b/administrator/modules/mod_stats_admin/mod_stats_admin.php @@ -1,4 +1,5 @@ getIdentity(); - - $rows = array(); - $query = $db->getQuery(true); - - $serverinfo = $params->get('serverinfo', 0); - $siteinfo = $params->get('siteinfo', 0); - - $i = 0; - - if ($serverinfo) - { - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_('MOD_STATS_PHP'); - $rows[$i]->icon = 'cogs'; - $rows[$i]->data = PHP_VERSION; - $i++; - - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_($db->name); - $rows[$i]->icon = 'database'; - $rows[$i]->data = $db->getVersion(); - $i++; - - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_('MOD_STATS_CACHING'); - $rows[$i]->icon = 'tachometer-alt'; - $rows[$i]->data = $app->get('caching') ? Text::_('JENABLED') : Text::_('JDISABLED'); - $i++; - - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_('MOD_STATS_GZIP'); - $rows[$i]->icon = 'bolt'; - $rows[$i]->data = $app->get('gzip') ? Text::_('JENABLED') : Text::_('JDISABLED'); - $i++; - } - - if ($siteinfo) - { - $query->select('COUNT(id) AS count_users') - ->from('#__users'); - $db->setQuery($query); - - try - { - $users = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $users = false; - } - - $query->clear() - ->select('COUNT(id) AS count_items') - ->from('#__content') - ->where('state = 1'); - $db->setQuery($query); - - try - { - $items = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $items = false; - } - - if ($users) - { - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_('MOD_STATS_USERS'); - $rows[$i]->icon = 'users'; - $rows[$i]->data = $users; - - if ($user->authorise('core.manage', 'com_users')) - { - $rows[$i]->link = Route::_('index.php?option=com_users'); - } - - $i++; - } - - if ($items) - { - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_('MOD_STATS_ARTICLES'); - $rows[$i]->icon = 'file'; - $rows[$i]->data = $items; - $rows[$i]->link = Route::_('index.php?option=com_content&view=articles&filter[published]=1'); - $i++; - } - } - - // Include additional data defined by published system plugins - PluginHelper::importPlugin('system'); - - $arrays = (array) $app->triggerEvent('onGetStats', array('mod_stats_admin')); - - foreach ($arrays as $response) - { - foreach ($response as $row) - { - // We only add a row if the title and data are given - if (isset($row['title']) && isset($row['data'])) - { - $rows[$i] = new \stdClass; - $rows[$i]->title = $row['title']; - $rows[$i]->icon = $row['icon'] ?? 'info'; - $rows[$i]->data = $row['data']; - $rows[$i]->link = isset($row['link']) ? $row['link'] : null; - $i++; - } - } - } - - return $rows; - } + /** + * Method to retrieve information about the site + * + * @param Registry $params The module parameters + * @param CMSApplication $app The application + * @param DatabaseInterface $db The database + * + * @return array Array containing site information + * + * @since 3.0 + */ + public static function getStats(Registry $params, CMSApplication $app, DatabaseInterface $db) + { + $user = $app->getIdentity(); + + $rows = array(); + $query = $db->getQuery(true); + + $serverinfo = $params->get('serverinfo', 0); + $siteinfo = $params->get('siteinfo', 0); + + $i = 0; + + if ($serverinfo) { + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_('MOD_STATS_PHP'); + $rows[$i]->icon = 'cogs'; + $rows[$i]->data = PHP_VERSION; + $i++; + + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_($db->name); + $rows[$i]->icon = 'database'; + $rows[$i]->data = $db->getVersion(); + $i++; + + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_('MOD_STATS_CACHING'); + $rows[$i]->icon = 'tachometer-alt'; + $rows[$i]->data = $app->get('caching') ? Text::_('JENABLED') : Text::_('JDISABLED'); + $i++; + + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_('MOD_STATS_GZIP'); + $rows[$i]->icon = 'bolt'; + $rows[$i]->data = $app->get('gzip') ? Text::_('JENABLED') : Text::_('JDISABLED'); + $i++; + } + + if ($siteinfo) { + $query->select('COUNT(id) AS count_users') + ->from('#__users'); + $db->setQuery($query); + + try { + $users = $db->loadResult(); + } catch (\RuntimeException $e) { + $users = false; + } + + $query->clear() + ->select('COUNT(id) AS count_items') + ->from('#__content') + ->where('state = 1'); + $db->setQuery($query); + + try { + $items = $db->loadResult(); + } catch (\RuntimeException $e) { + $items = false; + } + + if ($users) { + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_('MOD_STATS_USERS'); + $rows[$i]->icon = 'users'; + $rows[$i]->data = $users; + + if ($user->authorise('core.manage', 'com_users')) { + $rows[$i]->link = Route::_('index.php?option=com_users'); + } + + $i++; + } + + if ($items) { + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_('MOD_STATS_ARTICLES'); + $rows[$i]->icon = 'file'; + $rows[$i]->data = $items; + $rows[$i]->link = Route::_('index.php?option=com_content&view=articles&filter[published]=1'); + $i++; + } + } + + // Include additional data defined by published system plugins + PluginHelper::importPlugin('system'); + + $arrays = (array) $app->triggerEvent('onGetStats', array('mod_stats_admin')); + + foreach ($arrays as $response) { + foreach ($response as $row) { + // We only add a row if the title and data are given + if (isset($row['title']) && isset($row['data'])) { + $rows[$i] = new \stdClass(); + $rows[$i]->title = $row['title']; + $rows[$i]->icon = $row['icon'] ?? 'info'; + $rows[$i]->data = $row['data']; + $rows[$i]->link = isset($row['link']) ? $row['link'] : null; + $i++; + } + } + } + + return $rows; + } } diff --git a/administrator/modules/mod_stats_admin/tmpl/default.php b/administrator/modules/mod_stats_admin/tmpl/default.php index 732c2fcc4f76b..c79e483e02dc9 100644 --- a/administrator/modules/mod_stats_admin/tmpl/default.php +++ b/administrator/modules/mod_stats_admin/tmpl/default.php @@ -1,4 +1,5 @@
      - -
    • - title; ?> + +
    • + title; ?> - link)) : ?> - data; ?> - - data; ?> - -
    • - + link)) : ?> + data; ?> + + data; ?> + + +
    diff --git a/administrator/modules/mod_submenu/mod_submenu.php b/administrator/modules/mod_submenu/mod_submenu.php index 0fd9eb8720fd7..86cea5ee2aefa 100644 --- a/administrator/modules/mod_submenu/mod_submenu.php +++ b/administrator/modules/mod_submenu/mod_submenu.php @@ -1,4 +1,5 @@ get('menutype', '*'); $root = false; -if ($menutype === '*') -{ - $name = $params->get('preset', 'system'); - $root = MenusHelper::loadPreset($name); -} -else -{ - $root = MenusHelper::getMenuItems($menutype, true); +if ($menutype === '*') { + $name = $params->get('preset', 'system'); + $root = MenusHelper::loadPreset($name); +} else { + $root = MenusHelper::getMenuItems($menutype, true); } -if ($root && $root->hasChildren()) -{ - Factory::getLanguage()->load( - 'mod_menu', - JPATH_ADMINISTRATOR, - Factory::getLanguage()->getTag(), - true - ); +if ($root && $root->hasChildren()) { + Factory::getLanguage()->load( + 'mod_menu', + JPATH_ADMINISTRATOR, + Factory::getLanguage()->getTag(), + true + ); - Menu::preprocess($root); + Menu::preprocess($root); - // Render the module layout - require ModuleHelper::getLayoutPath('mod_submenu', $params->get('layout', 'default')); + // Render the module layout + require ModuleHelper::getLayoutPath('mod_submenu', $params->get('layout', 'default')); } diff --git a/administrator/modules/mod_submenu/src/Menu/Menu.php b/administrator/modules/mod_submenu/src/Menu/Menu.php index 61f7a50f6e09a..0ad4a42fc12ea 100644 --- a/administrator/modules/mod_submenu/src/Menu/Menu.php +++ b/administrator/modules/mod_submenu/src/Menu/Menu.php @@ -1,4 +1,5 @@ getIdentity(); - $children = $parent->getChildren(); - $language = Factory::getLanguage(); - - /** - * Trigger onPreprocessMenuItems for the current level of backend menu items. - * $children is an array of MenuItem objects. A plugin can traverse the whole tree, - * but new nodes will only be run through this method if their parents have not been processed yet. - */ - $app->triggerEvent('onPreprocessMenuItems', array('administrator.module.mod_submenu', $children)); - - foreach ($children as $item) - { - if (substr($item->link, 0, 8) === 'special:') - { - $special = substr($item->link, 8); - - if ($special === 'language-forum') - { - $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; - } - } - - $uri = new Uri($item->link); - $query = $uri->getQuery(true); - - /** - * This is needed to populate the element property when the component is no longer - * installed but its core menu items are left behind. - */ - if ($option = $uri->getVar('option')) - { - $item->element = $option; - } - - // Exclude item if is not enabled - if ($item->element && !ComponentHelper::isEnabled($item->element)) - { - $parent->removeChild($item); - continue; - } - - /* - * Multilingual Associations if the site is not set as multilingual and/or Associations is not enabled in - * the Language Filter plugin - */ - - if ($item->element === 'com_associations' && !Associations::isEnabled()) - { - $parent->removeChild($item); - continue; - } - - $itemParams = $item->getParams(); - - // Exclude item with menu item option set to exclude from menu modules - if ($itemParams->get('menu-permission')) - { - @list($action, $asset) = explode(';', $itemParams->get('menu-permission')); - - if (!$user->authorise($action, $asset)) - { - $parent->removeChild($item); - continue; - } - } - - // Populate automatic children for container items - if ($item->type === 'container') - { - $exclude = (array) $itemParams->get('hideitems') ?: array(); - $components = MenusHelper::getMenuItems('main', false, $exclude); - - // We are adding the nodes first to preprocess them, then sort them and add them again. - foreach ($components->getChildren() as $c) - { - if (!$c->hasChildren()) - { - $temp = clone $c; - $c->addChild($temp); - } - - $item->addChild($c); - } - - self::preprocess($item); - $children = ArrayHelper::sortObjects($item->getChildren(), 'text', 1, false, true); - - foreach ($children as $c) - { - $parent->addChild($c); - } - - $parent->removeChild($item); - continue; - } - - // Exclude Mass Mail if disabled in global configuration - if ($item->scope === 'massmail' && ($app->get('massmailoff', 0) == 1)) - { - $parent->removeChild($item); - continue; - } - - if ($item->element === 'com_fields') - { - parse_str($item->link, $query); - - // Only display Fields menus when enabled in the component - $createFields = null; - - if (isset($query['context'])) - { - $createFields = ComponentHelper::getParams(strstr($query['context'], '.', true))->get('custom_fields_enable', 1); - } - - if (!$createFields || !$user->authorise('core.manage', 'com_users')) - { - $parent->removeChild($item); - continue; - } - } - elseif ($item->element === 'com_workflow') - { - parse_str($item->link, $query); - - // Only display Workflow menus when enabled in the component - $workflow = null; - - if (isset($query['extension'])) - { - $parts = explode('.', $query['extension']); - - $workflow = ComponentHelper::getParams($parts[0])->get('workflow_enabled'); - } - - if (!$workflow) - { - $parent->removeChild($item); - continue; - } - - [$assetName] = isset($query['extension']) ? explode('.', $query['extension'], 2) : array('com_workflow'); - } - // Special case for components which only allow super user access - elseif (\in_array($item->element, array('com_config', 'com_privacy', 'com_actionlogs'), true) && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - elseif ($item->element === 'com_joomlaupdate' && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - elseif (($item->link === 'index.php?option=com_installer&view=install' || $item->link === 'index.php?option=com_installer&view=languages') - && !$user->authorise('core.admin')) - { - continue; - } - elseif ($item->element === 'com_admin') - { - parse_str($item->link, $query); - - if (isset($query['view']) && $query['view'] === 'sysinfo' && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - } - elseif ($item->element && !$user->authorise(($item->scope === 'edit') ? 'core.create' : 'core.manage', $item->element)) - { - $parent->removeChild($item); - continue; - } - elseif ($item->element === 'com_menus') - { - // Get badges for Menus containing a Home page. - $iconImage = $item->icon; - - if ($iconImage) - { - if (substr($iconImage, 0, 6) === 'class:' && substr($iconImage, 6) === 'icon-home') - { - $iconImage = ''; - $iconImage .= '' . Text::_('JDEFAULT') . ''; - } - elseif (substr($iconImage, 0, 6) === 'image:') - { - $iconImage = ' ' . substr($iconImage, 6) . ''; - } - - $item->iconImage = $iconImage; - } - } - - if ($item->hasChildren()) - { - self::preprocess($item); - } - - // Ok we passed everything, load language at last only - if ($item->element) - { - $language->load($item->element . '.sys', JPATH_ADMINISTRATOR) || - $language->load($item->element . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->element); - } - - if ($item->type === 'separator' && $item->getParams()->get('text_separator') == 0) - { - $item->title = ''; - } - - $item->text = Text::_($item->title); - } - } + /** + * Filter and perform other preparatory tasks for loaded menu items based on access rights and module configurations for display + * + * @param MenuItem $parent A menu item to process + * + * @return void + * + * @since 4.0.0 + */ + public static function preprocess($parent) + { + $app = Factory::getApplication(); + $user = $app->getIdentity(); + $children = $parent->getChildren(); + $language = Factory::getLanguage(); + + /** + * Trigger onPreprocessMenuItems for the current level of backend menu items. + * $children is an array of MenuItem objects. A plugin can traverse the whole tree, + * but new nodes will only be run through this method if their parents have not been processed yet. + */ + $app->triggerEvent('onPreprocessMenuItems', array('administrator.module.mod_submenu', $children)); + + foreach ($children as $item) { + if (substr($item->link, 0, 8) === 'special:') { + $special = substr($item->link, 8); + + if ($special === 'language-forum') { + $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; + } + } + + $uri = new Uri($item->link); + $query = $uri->getQuery(true); + + /** + * This is needed to populate the element property when the component is no longer + * installed but its core menu items are left behind. + */ + if ($option = $uri->getVar('option')) { + $item->element = $option; + } + + // Exclude item if is not enabled + if ($item->element && !ComponentHelper::isEnabled($item->element)) { + $parent->removeChild($item); + continue; + } + + /* + * Multilingual Associations if the site is not set as multilingual and/or Associations is not enabled in + * the Language Filter plugin + */ + + if ($item->element === 'com_associations' && !Associations::isEnabled()) { + $parent->removeChild($item); + continue; + } + + $itemParams = $item->getParams(); + + // Exclude item with menu item option set to exclude from menu modules + if ($itemParams->get('menu-permission')) { + @list($action, $asset) = explode(';', $itemParams->get('menu-permission')); + + if (!$user->authorise($action, $asset)) { + $parent->removeChild($item); + continue; + } + } + + // Populate automatic children for container items + if ($item->type === 'container') { + $exclude = (array) $itemParams->get('hideitems') ?: array(); + $components = MenusHelper::getMenuItems('main', false, $exclude); + + // We are adding the nodes first to preprocess them, then sort them and add them again. + foreach ($components->getChildren() as $c) { + if (!$c->hasChildren()) { + $temp = clone $c; + $c->addChild($temp); + } + + $item->addChild($c); + } + + self::preprocess($item); + $children = ArrayHelper::sortObjects($item->getChildren(), 'text', 1, false, true); + + foreach ($children as $c) { + $parent->addChild($c); + } + + $parent->removeChild($item); + continue; + } + + // Exclude Mass Mail if disabled in global configuration + if ($item->scope === 'massmail' && ($app->get('massmailoff', 0) == 1)) { + $parent->removeChild($item); + continue; + } + + if ($item->element === 'com_fields') { + parse_str($item->link, $query); + + // Only display Fields menus when enabled in the component + $createFields = null; + + if (isset($query['context'])) { + $createFields = ComponentHelper::getParams(strstr($query['context'], '.', true))->get('custom_fields_enable', 1); + } + + if (!$createFields || !$user->authorise('core.manage', 'com_users')) { + $parent->removeChild($item); + continue; + } + } elseif ($item->element === 'com_workflow') { + parse_str($item->link, $query); + + // Only display Workflow menus when enabled in the component + $workflow = null; + + if (isset($query['extension'])) { + $parts = explode('.', $query['extension']); + + $workflow = ComponentHelper::getParams($parts[0])->get('workflow_enabled'); + } + + if (!$workflow) { + $parent->removeChild($item); + continue; + } + + [$assetName] = isset($query['extension']) ? explode('.', $query['extension'], 2) : array('com_workflow'); + } + // Special case for components which only allow super user access + elseif (\in_array($item->element, array('com_config', 'com_privacy', 'com_actionlogs'), true) && !$user->authorise('core.admin')) { + $parent->removeChild($item); + continue; + } elseif ($item->element === 'com_joomlaupdate' && !$user->authorise('core.admin')) { + $parent->removeChild($item); + continue; + } elseif ( + ($item->link === 'index.php?option=com_installer&view=install' || $item->link === 'index.php?option=com_installer&view=languages') + && !$user->authorise('core.admin') + ) { + continue; + } elseif ($item->element === 'com_admin') { + parse_str($item->link, $query); + + if (isset($query['view']) && $query['view'] === 'sysinfo' && !$user->authorise('core.admin')) { + $parent->removeChild($item); + continue; + } + } elseif ($item->element && !$user->authorise(($item->scope === 'edit') ? 'core.create' : 'core.manage', $item->element)) { + $parent->removeChild($item); + continue; + } elseif ($item->element === 'com_menus') { + // Get badges for Menus containing a Home page. + $iconImage = $item->icon; + + if ($iconImage) { + if (substr($iconImage, 0, 6) === 'class:' && substr($iconImage, 6) === 'icon-home') { + $iconImage = ''; + $iconImage .= '' . Text::_('JDEFAULT') . ''; + } elseif (substr($iconImage, 0, 6) === 'image:') { + $iconImage = ' ' . substr($iconImage, 6) . ''; + } + + $item->iconImage = $iconImage; + } + } + + if ($item->hasChildren()) { + self::preprocess($item); + } + + // Ok we passed everything, load language at last only + if ($item->element) { + $language->load($item->element . '.sys', JPATH_ADMINISTRATOR) || + $language->load($item->element . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->element); + } + + if ($item->type === 'separator' && $item->getParams()->get('text_separator') == 0) { + $item->title = ''; + } + + $item->text = Text::_($item->title); + } + } } diff --git a/administrator/modules/mod_submenu/tmpl/default.php b/administrator/modules/mod_submenu/tmpl/default.php index 1bf99857c4f58..d79089e894e6f 100644 --- a/administrator/modules/mod_submenu/tmpl/default.php +++ b/administrator/modules/mod_submenu/tmpl/default.php @@ -1,4 +1,5 @@ getChildren() as $child) : ?> - hasChildren()) : ?> -
    -
    - img = $child->img ?? ''; + hasChildren()) : ?> +
    +
    + img = $child->img ?? ''; - if (substr($child->img, 0, 6) === 'class:') - { - $iconImage = ''; - } - elseif (substr($child->img, 0, 6) === 'image:') - { - $iconImage = ''; - } - elseif (!empty($child->img)) - { - $iconImage = ''; - } - elseif ($child->icon) - { - $iconImage = ''; - } - else - { - $iconImage = ''; - } - ?> -

    - - title); ?> -

    -
    -
    - + $sronly = Text::_($item->title) . ' - ' . $title; + ?> + + + + + + + + dashboard) : ?> + + + + + + + + + + +
    +
    + diff --git a/administrator/modules/mod_title/mod_title.php b/administrator/modules/mod_title/mod_title.php index edcbc9bfc2b77..3e1340ffba5f8 100644 --- a/administrator/modules/mod_title/mod_title.php +++ b/administrator/modules/mod_title/mod_title.php @@ -1,4 +1,5 @@ JComponentTitle)) -{ - $title = $app->JComponentTitle; +if (isset($app->JComponentTitle)) { + $title = $app->JComponentTitle; } require ModuleHelper::getLayoutPath('mod_title', $params->get('layout', 'default')); diff --git a/administrator/modules/mod_title/tmpl/default.php b/administrator/modules/mod_title/tmpl/default.php index 7db1059559420..0c24a082b2a65 100644 --- a/administrator/modules/mod_title/tmpl/default.php +++ b/administrator/modules/mod_title/tmpl/default.php @@ -1,4 +1,5 @@
    -
    - -
    +
    + +
    diff --git a/administrator/modules/mod_toolbar/mod_toolbar.php b/administrator/modules/mod_toolbar/mod_toolbar.php index 9b7fabde069a3..9466a9efd76b3 100644 --- a/administrator/modules/mod_toolbar/mod_toolbar.php +++ b/administrator/modules/mod_toolbar/mod_toolbar.php @@ -1,4 +1,5 @@ input->getBool('hidemainmenu'); -if ($hideLinks) -{ - return; +if ($hideLinks) { + return; } // Load the Bootstrap Dropdown HTMLHelper::_('bootstrap.dropdown', '.dropdown-toggle'); ?> diff --git a/administrator/modules/mod_version/mod_version.php b/administrator/modules/mod_version/mod_version.php index 5ce96037c52a8..c94b138b69e15 100644 --- a/administrator/modules/mod_version/mod_version.php +++ b/administrator/modules/mod_version/mod_version.php @@ -1,4 +1,5 @@ getShortVersion(); - } + return '‎' . $version->getShortVersion(); + } } diff --git a/administrator/modules/mod_version/tmpl/default.php b/administrator/modules/mod_version/tmpl/default.php index 5e77f801b56f0..fa0f143a44841 100644 --- a/administrator/modules/mod_version/tmpl/default.php +++ b/administrator/modules/mod_version/tmpl/default.php @@ -1,4 +1,5 @@
    - +
    diff --git a/administrator/templates/atum/component.php b/administrator/templates/atum/component.php index 2913982b05b50..0d52a6bed1064 100644 --- a/administrator/templates/atum/component.php +++ b/administrator/templates/atum/component.php @@ -1,4 +1,5 @@ usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) - ->useStyle('template.active.language') - ->useStyle('template.user') - ->addInlineStyle(':root { + ->useStyle('template.active.language') + ->useStyle('template.user') + ->addInlineStyle(':root { --hue: ' . $matches[1] . '; --template-bg-light: ' . $this->params->get('bg-light', '--template-bg-light') . '; --template-text-dark: ' . $this->params->get('text-dark', '--template-text-dark') . '; @@ -47,12 +48,12 @@ - - - + + + - - + + diff --git a/administrator/templates/atum/cpanel.php b/administrator/templates/atum/cpanel.php index e75d1c51179dd..8a2b1aafdd2c8 100644 --- a/administrator/templates/atum/cpanel.php +++ b/administrator/templates/atum/cpanel.php @@ -1,4 +1,5 @@ guest) -{ - require __DIR__ . '/error_login.php'; -} -else -{ - require __DIR__ . '/error_full.php'; +if ($user->guest) { + require __DIR__ . '/error_login.php'; +} else { + require __DIR__ . '/error_full.php'; } diff --git a/administrator/templates/atum/error_full.php b/administrator/templates/atum/error_full.php index 6886a175da8ff..0014d08860938 100644 --- a/administrator/templates/atum/error_full.php +++ b/administrator/templates/atum/error_full.php @@ -1,4 +1,5 @@ params->get('logoBrandLarge') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; $logoBrandSmall = $this->params->get('logoBrandSmall') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; $logoBrandLargeAlt = empty($this->params->get('logoBrandLargeAlt')) && empty($this->params->get('emptyLogoBrandLargeAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; $logoBrandSmallAlt = empty($this->params->get('logoBrandSmallAlt')) && empty($this->params->get('emptyLogoBrandSmallAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; - // Get the hue value - preg_match('#^hsla?\(([0-9]+)[\D]+([0-9]+)[\D]+([0-9]+)[\D]+([0-9](?:.\d+)?)?\)$#i', $this->params->get('hue', 'hsl(214, 63%, 20%)'), $matches); + // Get the hue value + preg_match('#^hsla?\(([0-9]+)[\D]+([0-9]+)[\D]+([0-9]+)[\D]+([0-9](?:.\d+)?)?\)$#i', $this->params->get('hue', 'hsl(214, 63%, 20%)'), $matches); - // Enable assets - $wa->usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) - ->useStyle('template.active.language') - ->useStyle('template.user') - ->addInlineStyle(':root { + // Enable assets + $wa->usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) + ->useStyle('template.active.language') + ->useStyle('template.user') + ->addInlineStyle(':root { --hue: ' . $matches[1] . '; --template-bg-light: ' . $this->params->get('bg-light', '#f0f4fb') . '; --template-text-dark: ' . $this->params->get('text-dark', '#495057') . '; @@ -66,122 +67,122 @@ }'); // Override 'template.active' asset to set correct ltr/rtl dependency -$wa->registerStyle('template.active', '', [], [], ['template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')]); + $wa->registerStyle('template.active', '', [], [], ['template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')]); // Set some meta data -$this->setMetaData('viewport', 'width=device-width, initial-scale=1'); + $this->setMetaData('viewport', 'width=device-width, initial-scale=1'); -$monochrome = (bool) $this->params->get('monochrome'); + $monochrome = (bool) $this->params->get('monochrome'); // @see administrator/templates/atum/html/layouts/status.php -$statusModules = LayoutHelper::render('status', ['modules' => 'status']); -?> + $statusModules = LayoutHelper::render('status', ['modules' => 'status']); + ?> - - - + + + - - - - -
    -
    - - - -
    -
    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -

    -
    - error->getCode(); ?> - error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> -
    - debug) : ?> -
    - renderBacktrace(); ?> - - error->getPrevious()) : ?> - - _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> - - setError($this->_error->getPrevious()); ?> - -

    -

    _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    - renderBacktrace(); ?> - setError($this->_error->getPrevious()); ?> - - - setError($this->error); ?> - -
    - -

    - - - -

    -
    - - countModules('bottom')) : ?> - - -
    -
    -
    - - - - - - -
    - + + + + +
    +
    + + + +
    +
    +
    +
    + +
    +
    +
    + +
    + + +
    +
    +

    +
    + error->getCode(); ?> + error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> +
    + debug) : ?> +
    + renderBacktrace(); ?> + + error->getPrevious()) : ?> + + _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> + + setError($this->_error->getPrevious()); ?> + +

    +

    _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    + renderBacktrace(); ?> + setError($this->_error->getPrevious()); ?> + + + setError($this->error); ?> + +
    + +

    + + + +

    +
    + + countModules('bottom')) : ?> + + +
    +
    +
    + + + + + + +
    + diff --git a/administrator/templates/atum/error_login.php b/administrator/templates/atum/error_login.php index 68144ad07aebc..fe6dc0f048681 100644 --- a/administrator/templates/atum/error_login.php +++ b/administrator/templates/atum/error_login.php @@ -1,4 +1,5 @@ params->get('logoBrandLarge') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; $loginLogo = $this->params->get('loginLogo') - ? Uri::root() . $this->params->get('loginLogo') - : Uri::root() . 'media/templates/administrator/atum/images/logos/login.svg'; + ? Uri::root() . $this->params->get('loginLogo') + : Uri::root() . 'media/templates/administrator/atum/images/logos/login.svg'; $logoBrandSmall = $this->params->get('logoBrandSmall') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; $logoBrandLargeAlt = empty($this->params->get('logoBrandLargeAlt')) && empty($this->params->get('emptyLogoBrandLargeAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; $logoBrandSmallAlt = empty($this->params->get('logoBrandSmallAlt')) && empty($this->params->get('emptyLogoBrandSmallAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; $loginLogoAlt = empty($this->params->get('loginLogoAlt')) && empty($this->params->get('emptyLoginLogoAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('loginLogoAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('loginLogoAlt'), ENT_COMPAT, 'UTF-8') . '"'; - // Get the hue value + // Get the hue value preg_match('#^hsla?\(([0-9]+)[\D]+([0-9]+)[\D]+([0-9]+)[\D]+([0-9](?:.\d+)?)?\)$#i', $this->params->get('hue', 'hsl(214, 63%, 20%)'), $matches); // Enable assets $wa->usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) - ->useStyle('template.active.language') - ->useStyle('template.user') - ->addInlineStyle(':root { + ->useStyle('template.active.language') + ->useStyle('template.user') + ->addInlineStyle(':root { --hue: ' . $matches[1] . '; --template-bg-light: ' . $this->params->get('bg-light', '#f0f4fb') . '; --template-text-dark: ' . $this->params->get('text-dark', '#495057') . '; @@ -83,83 +84,83 @@ - - - + + + - - - - -
    -
    -
    -
    -
    -
    -
    - > -
    -

    - -
    - error->getCode(); ?> - error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> -
    - debug) : ?> -
    - renderBacktrace(); ?> - - error->getPrevious()) : ?> - - _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> - - setError($this->_error->getPrevious()); ?> - -

    -

    _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    - renderBacktrace(); ?> - setError($this->_error->getPrevious()); ?> - - - setError($this->error); ?> - -
    - -
    -
    -
    -
    -
    - - -
    - + + + + +
    +
    +
    +
    +
    +
    +
    + > +
    +

    + +
    + error->getCode(); ?> + error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> +
    + debug) : ?> +
    + renderBacktrace(); ?> + + error->getPrevious()) : ?> + + _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> + + setError($this->_error->getPrevious()); ?> + +

    +

    _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    + renderBacktrace(); ?> + setError($this->_error->getPrevious()); ?> + + + setError($this->error); ?> + +
    + +
    +
    +
    +
    +
    + + +
    + diff --git a/administrator/templates/atum/html/layouts/chromes/body.php b/administrator/templates/atum/html/layouts/chromes/body.php index b73e693fe638b..ab4c8f317e2d9 100644 --- a/administrator/templates/atum/html/layouts/chromes/body.php +++ b/administrator/templates/atum/html/layouts/chromes/body.php @@ -1,4 +1,5 @@ content === '') -{ - return; +if ((string) $module->content === '') { + return; } $id = $module->id; @@ -41,31 +41,31 @@ ?>
    - < class="card pt-3"> - - isRtl() ? 'start' : 'end'; ?> - - - showtitle) : ?> - < class="card-header">title; ?>> - -
    - content; ?> -
    - > + < class="card pt-3"> + + isRtl() ? 'start' : 'end'; ?> + + + showtitle) : ?> + < class="card-header">title; ?>> + +
    + content; ?> +
    + >
    diff --git a/administrator/templates/atum/html/layouts/chromes/header-item.php b/administrator/templates/atum/html/layouts/chromes/header-item.php index 7e6d474557f08..6beaeadff20a3 100644 --- a/administrator/templates/atum/html/layouts/chromes/header-item.php +++ b/administrator/templates/atum/html/layouts/chromes/header-item.php @@ -1,4 +1,5 @@ content === '') -{ - return; +if ((string) $module->content === '') { + return; } ?>
    - content; ?> + content; ?>
    diff --git a/administrator/templates/atum/html/layouts/chromes/title.php b/administrator/templates/atum/html/layouts/chromes/title.php index c4979a36c2152..23278b578d33e 100644 --- a/administrator/templates/atum/html/layouts/chromes/title.php +++ b/administrator/templates/atum/html/layouts/chromes/title.php @@ -1,4 +1,5 @@ content === '') -{ - return; +if ((string) $module->content === '') { + return; } ?>
    -
    title; ?>
    +
    title; ?>
    content; ?> diff --git a/administrator/templates/atum/html/layouts/chromes/well.php b/administrator/templates/atum/html/layouts/chromes/well.php index 76d5bac523e9b..be61e19d8f5e0 100644 --- a/administrator/templates/atum/html/layouts/chromes/well.php +++ b/administrator/templates/atum/html/layouts/chromes/well.php @@ -1,4 +1,5 @@ content === '') -{ - return; +if ((string) $module->content === '') { + return; } $id = $module->id; @@ -44,39 +44,39 @@ ?>
    - < class="card mb-3 "> - showtitle) : ?> -
    - - isRtl() ? 'start' : 'end'; ?> - - + < class="card mb-3 "> + showtitle) : ?> +
    + + isRtl() ? 'start' : 'end'; ?> + + - showtitle) : ?> - <> - - title, ENT_QUOTES, 'UTF-8'); ?> - > - -
    - -
    - content; ?> -
    - > + showtitle) : ?> + <> + + title, ENT_QUOTES, 'UTF-8'); ?> + > + +
    + +
    + content; ?> +
    + >
    diff --git a/administrator/templates/atum/html/layouts/status.php b/administrator/templates/atum/html/layouts/status.php index 41a28e953104a..364eec92381a4 100644 --- a/administrator/templates/atum/html/layouts/status.php +++ b/administrator/templates/atum/html/layouts/status.php @@ -1,4 +1,5 @@ $mod) -{ - $out = $renderer->render($mod); +foreach ($modules as $key => $mod) { + $out = $renderer->render($mod); - if ($out !== '') - { - if (strpos($out, 'data-bs-toggle="modal"') !== false) - { - $dom = new \DOMDocument; - $dom->loadHTML($out); - $els = $dom->getElementsByTagName('a'); + if ($out !== '') { + if (strpos($out, 'data-bs-toggle="modal"') !== false) { + $dom = new \DOMDocument(); + $dom->loadHTML($out); + $els = $dom->getElementsByTagName('a'); - $moduleCollapsedHtml[] = $dom->saveHTML($els[0]); //$els[0]->nodeValue; - } - else - { - $moduleCollapsedHtml[] = $out; - } + $moduleCollapsedHtml[] = $dom->saveHTML($els[0]); //$els[0]->nodeValue; + } else { + $moduleCollapsedHtml[] = $out; + } - $moduleHtml[] = $out; - } + $moduleHtml[] = $out; + } } ?>
    - ' . $mod . '
    '; - } - ?> -
    - - -
    + ' . $mod . ''; + } + ?> +
    + + +
    diff --git a/administrator/templates/atum/index.php b/administrator/templates/atum/index.php index 734e0ca9e48f6..419590c1d88ab 100644 --- a/administrator/templates/atum/index.php +++ b/administrator/templates/atum/index.php @@ -1,4 +1,5 @@ params->get('logoBrandLarge') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; $logoBrandSmall = $this->params->get('logoBrandSmall') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; $logoBrandLargeAlt = empty($this->params->get('logoBrandLargeAlt')) && empty($this->params->get('emptyLogoBrandLargeAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; $logoBrandSmallAlt = empty($this->params->get('logoBrandSmallAlt')) && empty($this->params->get('emptyLogoBrandSmallAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; // Get the hue value preg_match('#^hsla?\(([0-9]+)[\D]+([0-9]+)[\D]+([0-9]+)[\D]+([0-9](?:.\d+)?)?\)$#i', $this->params->get('hue', 'hsl(214, 63%, 20%)'), $matches); // Enable assets $wa->usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) - ->useStyle('template.active.language') - ->useStyle('template.user') - ->addInlineStyle(':root { + ->useStyle('template.active.language') + ->useStyle('template.user') + ->addInlineStyle(':root { --hue: ' . $matches[1] . '; --template-bg-light: ' . $this->params->get('bg-light', '#f0f4fb') . '; --template-text-dark: ' . $this->params->get('text-dark', '#495057') . '; @@ -89,99 +90,99 @@ > - - - + + +
    - - - - - - - - - -
    - - - - -
    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - - -
    -
    - countModules('bottom')) : ?> - - -
    - -
    -
    + + + + + + + + + +
    + + + + +
    +
    +
    + +
    +
    +
    + +
    + + +
    +
    +
    + + +
    +
    + countModules('bottom')) : ?> + + +
    + +
    +
    diff --git a/administrator/templates/atum/login.php b/administrator/templates/atum/login.php index 98c3bb7691b5e..43130e5e7b39f 100644 --- a/administrator/templates/atum/login.php +++ b/administrator/templates/atum/login.php @@ -1,4 +1,5 @@ params->get('logoBrandLarge') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; $loginLogo = $this->params->get('loginLogo') - ? Uri::root() . $this->params->get('loginLogo') - : Uri::root() . 'media/templates/administrator/atum/images/logos/login.svg'; + ? Uri::root() . $this->params->get('loginLogo') + : Uri::root() . 'media/templates/administrator/atum/images/logos/login.svg'; $logoBrandSmall = $this->params->get('logoBrandSmall') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; $logoBrandLargeAlt = empty($this->params->get('logoBrandLargeAlt')) && empty($this->params->get('emptyLogoBrandLargeAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; $logoBrandSmallAlt = empty($this->params->get('logoBrandSmallAlt')) && empty($this->params->get('emptyLogoBrandSmallAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; $loginLogoAlt = empty($this->params->get('loginLogoAlt')) && empty($this->params->get('emptyLoginLogoAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('loginLogoAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('loginLogoAlt'), ENT_COMPAT, 'UTF-8') . '"'; // Get the hue value preg_match('#^hsla?\(([0-9]+)[\D]+([0-9]+)[\D]+([0-9]+)[\D]+([0-9](?:.\d+)?)?\)$#i', $this->params->get('hue', 'hsl(214, 63%, 20%)'), $matches); // Enable assets $wa->usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) - ->useStyle('template.active.language') - ->useStyle('template.user') - ->addInlineStyle(':root { + ->useStyle('template.active.language') + ->useStyle('template.user') + ->addInlineStyle(':root { --hue: ' . $matches[1] . '; --template-bg-light: ' . $this->params->get('bg-light', '#f0f4fb') . '; --template-text-dark: ' . $this->params->get('text-dark', '#495057') . '; @@ -89,62 +90,62 @@ - - - + + + - - - - - -
    -
    -
    - -
    - -
    -
    -
    - - -
    - + + + + + +
    +
    +
    + +
    + +
    +
    +
    + + +
    + diff --git a/administrator/templates/system/component.php b/administrator/templates/system/component.php index dd283dae3c972..672f4e6fb6648 100644 --- a/administrator/templates/system/component.php +++ b/administrator/templates/system/component.php @@ -1,4 +1,5 @@ - + - - + + diff --git a/administrator/templates/system/error.php b/administrator/templates/system/error.php index 53c7a91e622a1..ea06bd4e7dc0c 100644 --- a/administrator/templates/system/error.php +++ b/administrator/templates/system/error.php @@ -1,4 +1,5 @@ - - - + + + - - - - - - - -
    -

    error->getCode() ?> -

    -
    -

    - error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> - debug) : ?> -
    error->getFile(), ENT_QUOTES, 'UTF-8');?>:error->getLine(); ?> - -

    -

    - debug) : ?> -
    - renderBacktrace(); ?> - - error->getPrevious()) : ?> - - _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> - - setError($this->_error->getPrevious()); ?> - -

    -

    - _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> -
    _error->getFile(), ENT_QUOTES, 'UTF-8');?>:_error->getLine(); ?> -

    - renderBacktrace(); ?> - setError($this->_error->getPrevious()); ?> - - - setError($this->error); ?> - -
    - -
    + + + + + + + +
    +

    error->getCode() ?> -

    +
    +

    + error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> + debug) : ?> +
    error->getFile(), ENT_QUOTES, 'UTF-8');?>:error->getLine(); ?> + +

    +

    + debug) : ?> +
    + renderBacktrace(); ?> + + error->getPrevious()) : ?> + + _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> + + setError($this->_error->getPrevious()); ?> + +

    +

    + _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> +
    _error->getFile(), ENT_QUOTES, 'UTF-8');?>:_error->getLine(); ?> +

    + renderBacktrace(); ?> + setError($this->_error->getPrevious()); ?> + + + setError($this->error); ?> + +
    + +
    - + diff --git a/administrator/templates/system/index.php b/administrator/templates/system/index.php index 14ed2b2e575c2..c0d47db820eef 100644 --- a/administrator/templates/system/index.php +++ b/administrator/templates/system/index.php @@ -1,4 +1,5 @@ getExtensionFromInput(); - $data['extension'] = $extension; - - // TODO: This is a hack to drop the extension into the global input object - to satisfy how state is built - // we should be able to improve this in the future - $this->input->set('extension', $extension); - - return $data; - } - - /** - * Method to save a record. - * - * @param integer $recordKey The primary key of the item (if exists) - * - * @return integer The record ID on success, false on failure - * - * @since 4.0.6 - */ - protected function save($recordKey = null) - { - $recordId = parent::save($recordKey); - - if (!$recordId) - { - return $recordId; - } - - $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - - if (empty($data['location'])) - { - return $recordId; - } - - /** @var Category $category */ - $category = $this->getModel('Category')->getTable('Category'); - $category->load((int) $recordId); - - $reference = $category->parent_id; - - if (!empty($data['location_reference'])) - { - $reference = (int) $data['location_reference']; - } - - $category->setLocation($reference, $data['location']); - $category->store(); - - return $recordId; - } - - /** - * Basic display of an item view - * - * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayItem($id = null) - { - $this->modelState->set('filter.extension', $this->getExtensionFromInput()); - - return parent::displayItem($id); - } - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.extension', $this->getExtensionFromInput()); - - return parent::displayList(); - } - - /** - * Get extension from input - * - * @return string - * - * @since 4.0.0 - */ - private function getExtensionFromInput() - { - return $this->input->exists('extension') ? - $this->input->get('extension') : $this->input->post->get('extension'); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'categories'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'categories'; + + /** + * Method to allow extended classes to manipulate the data to be saved for an extension. + * + * @param array $data An array of input data. + * + * @return array + * + * @since 4.0.0 + */ + protected function preprocessSaveData(array $data): array + { + $extension = $this->getExtensionFromInput(); + $data['extension'] = $extension; + + // TODO: This is a hack to drop the extension into the global input object - to satisfy how state is built + // we should be able to improve this in the future + $this->input->set('extension', $extension); + + return $data; + } + + /** + * Method to save a record. + * + * @param integer $recordKey The primary key of the item (if exists) + * + * @return integer The record ID on success, false on failure + * + * @since 4.0.6 + */ + protected function save($recordKey = null) + { + $recordId = parent::save($recordKey); + + if (!$recordId) { + return $recordId; + } + + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + + if (empty($data['location'])) { + return $recordId; + } + + /** @var Category $category */ + $category = $this->getModel('Category')->getTable('Category'); + $category->load((int) $recordId); + + $reference = $category->parent_id; + + if (!empty($data['location_reference'])) { + $reference = (int) $data['location_reference']; + } + + $category->setLocation($reference, $data['location']); + $category->store(); + + return $recordId; + } + + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.extension', $this->getExtensionFromInput()); + + return parent::displayItem($id); + } + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.extension', $this->getExtensionFromInput()); + + return parent::displayList(); + } + + /** + * Get extension from input + * + * @return string + * + * @since 4.0.0 + */ + private function getExtensionFromInput() + { + return $this->input->exists('extension') ? + $this->input->get('extension') : $this->input->post->get('extension'); + } } diff --git a/api/components/com_categories/src/View/Categories/JsonapiView.php b/api/components/com_categories/src/View/Categories/JsonapiView.php index c2d244c67fb99..4caaac4e10de4 100644 --- a/api/components/com_categories/src/View/Categories/JsonapiView.php +++ b/api/components/com_categories/src/View/Categories/JsonapiView.php @@ -1,4 +1,5 @@ fieldsToRenderList[] = $field->name; - } + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + foreach (FieldsHelper::getFields('com_content.categories') as $field) { + $this->fieldsToRenderList[] = $field->name; + } - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Execute and display a template script. - * - * @param object $item Item - * - * @return string - * - * @since 4.0.0 - */ - public function displayItem($item = null) - { - foreach (FieldsHelper::getFields('com_content.categories') as $field) - { - $this->fieldsToRenderItem[] = $field->name; - } + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + foreach (FieldsHelper::getFields('com_content.categories') as $field) { + $this->fieldsToRenderItem[] = $field->name; + } - if ($item === null) - { - /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ - $model = $this->getModel(); - $item = $this->prepareItem($model->getItem()); - } + if ($item === null) { + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(); + $item = $this->prepareItem($model->getItem()); + } - if ($item->id === null) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->id === null) { + throw new RouteNotFoundException('Item does not exist'); + } - if ($item->extension != $this->getModel()->getState('filter.extension')) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->extension != $this->getModel()->getState('filter.extension')) { + throw new RouteNotFoundException('Item does not exist'); + } - return parent::displayItem($item); - } + return parent::displayItem($item); + } - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - foreach (FieldsHelper::getFields('com_content.categories', $item, true) as $field) - { - $item->{$field->name} = $field->apivalue ?? $field->rawvalue; - } + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + foreach (FieldsHelper::getFields('com_content.categories', $item, true) as $field) { + $item->{$field->name} = $field->apivalue ?? $field->rawvalue; + } - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/api/components/com_config/src/Controller/ApplicationController.php b/api/components/com_config/src/Controller/ApplicationController.php index c273f891f0323..0809701a5c57a 100644 --- a/api/components/com_config/src/Controller/ApplicationController.php +++ b/api/components/com_config/src/Controller/ApplicationController.php @@ -1,4 +1,5 @@ app->getDocument()->getType(); - $viewLayout = $this->input->get('layout', 'default', 'string'); - - try - { - /** @var JsonapiView $view */ - $view = $this->getView( - $this->default_view, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } - - /** @var ApplicationModel $model */ - $model = $this->getModel($this->contentType); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); - } - - // Push the model into the view (as default) - $view->setModel($model, true); - - $view->document = $this->app->getDocument(); - $view->displayList(); - - return $this; - } - - /** - * Method to edit an existing record. - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function edit() - { - /** @var ApplicationModel $model */ - $model = $this->getModel($this->contentType); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); - } - - // Access check. - if (!$this->allowEdit()) - { - throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); - } - - $data = json_decode($this->input->json->getRaw(), true); - - // Complete data array if needed - $oldData = $model->getData(); - $data = array_replace($oldData, $data); - - // @todo: Not the cleanest thing ever but it works... - Form::addFormPath(JPATH_COMPONENT_ADMINISTRATOR . '/forms'); - - // Must load after serving service-requests - $form = $model->getForm(); - - // Validate the posted data. - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - $errors = $model->getErrors(); - $messages = []; - - for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $messages[] = "{$errors[$i]->getMessage()}"; - } - else - { - $messages[] = "{$errors[$i]}"; - } - } - - throw new InvalidParameterException(implode("\n", $messages)); - } - - if (!$model->save($validData)) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500); - } - - return $this; - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'application'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'application'; + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $viewType = $this->app->getDocument()->getType(); + $viewLayout = $this->input->get('layout', 'default', 'string'); + + try { + /** @var JsonapiView $view */ + $view = $this->getView( + $this->default_view, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } + + /** @var ApplicationModel $model */ + $model = $this->getModel($this->contentType); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); + } + + // Push the model into the view (as default) + $view->setModel($model, true); + + $view->document = $this->app->getDocument(); + $view->displayList(); + + return $this; + } + + /** + * Method to edit an existing record. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function edit() + { + /** @var ApplicationModel $model */ + $model = $this->getModel($this->contentType); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); + } + + // Access check. + if (!$this->allowEdit()) { + throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); + } + + $data = json_decode($this->input->json->getRaw(), true); + + // Complete data array if needed + $oldData = $model->getData(); + $data = array_replace($oldData, $data); + + // @todo: Not the cleanest thing ever but it works... + Form::addFormPath(JPATH_COMPONENT_ADMINISTRATOR . '/forms'); + + // Must load after serving service-requests + $form = $model->getForm(); + + // Validate the posted data. + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + $errors = $model->getErrors(); + $messages = []; + + for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $messages[] = "{$errors[$i]->getMessage()}"; + } else { + $messages[] = "{$errors[$i]}"; + } + } + + throw new InvalidParameterException(implode("\n", $messages)); + } + + if (!$model->save($validData)) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500); + } + + return $this; + } } diff --git a/api/components/com_config/src/Controller/ComponentController.php b/api/components/com_config/src/Controller/ComponentController.php index ef60de1d7754d..f999bf9cdf970 100644 --- a/api/components/com_config/src/Controller/ComponentController.php +++ b/api/components/com_config/src/Controller/ComponentController.php @@ -1,4 +1,5 @@ app->getDocument()->getType(); - $viewLayout = $this->input->get('layout', 'default', 'string'); - - try - { - /** @var JsonapiView $view */ - $view = $this->getView( - $this->default_view, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } - - /** @var ComponentModel $model */ - $model = $this->getModel($this->contentType); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); - } - - // Push the model into the view (as default) - $view->setModel($model, true); - $view->set('component_name', $this->input->get('component_name')); - - $view->document = $this->app->getDocument(); - $view->displayList(); - - return $this; - } - - /** - * Method to edit an existing record. - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function edit() - { - /** @var ComponentModel $model */ - $model = $this->getModel($this->contentType); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); - } - - // Access check. - if (!$this->allowEdit()) - { - throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); - } - - $option = $this->input->get('component_name'); - - // @todo: Not the cleanest thing ever but it works... - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/' . $option); - - // Must load after serving service-requests - $form = $model->getForm(); - - $data = json_decode($this->input->json->getRaw(), true); - - $component = ComponentHelper::getComponent($option); - $oldData = $component->getParams()->toArray(); - $data = array_replace($oldData, $data); - - // Validate the posted data. - $validData = $model->validate($form, $data); - - if ($validData === false) - { - $errors = $model->getErrors(); - $messages = []; - - for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $messages[] = "{$errors[$i]->getMessage()}"; - } - else - { - $messages[] = "{$errors[$i]}"; - } - } - - throw new InvalidParameterException(implode("\n", $messages)); - } - - // Attempt to save the configuration. - $data = [ - 'params' => $validData, - 'id' => ExtensionHelper::getExtensionRecord($option, 'component')->extension_id, - 'option' => $option, - ]; - - if (!$model->save($data)) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500); - } - - return $this; - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'component'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'component'; + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $viewType = $this->app->getDocument()->getType(); + $viewLayout = $this->input->get('layout', 'default', 'string'); + + try { + /** @var JsonapiView $view */ + $view = $this->getView( + $this->default_view, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } + + /** @var ComponentModel $model */ + $model = $this->getModel($this->contentType); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); + } + + // Push the model into the view (as default) + $view->setModel($model, true); + $view->set('component_name', $this->input->get('component_name')); + + $view->document = $this->app->getDocument(); + $view->displayList(); + + return $this; + } + + /** + * Method to edit an existing record. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function edit() + { + /** @var ComponentModel $model */ + $model = $this->getModel($this->contentType); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); + } + + // Access check. + if (!$this->allowEdit()) { + throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); + } + + $option = $this->input->get('component_name'); + + // @todo: Not the cleanest thing ever but it works... + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/' . $option); + + // Must load after serving service-requests + $form = $model->getForm(); + + $data = json_decode($this->input->json->getRaw(), true); + + $component = ComponentHelper::getComponent($option); + $oldData = $component->getParams()->toArray(); + $data = array_replace($oldData, $data); + + // Validate the posted data. + $validData = $model->validate($form, $data); + + if ($validData === false) { + $errors = $model->getErrors(); + $messages = []; + + for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $messages[] = "{$errors[$i]->getMessage()}"; + } else { + $messages[] = "{$errors[$i]}"; + } + } + + throw new InvalidParameterException(implode("\n", $messages)); + } + + // Attempt to save the configuration. + $data = [ + 'params' => $validData, + 'id' => ExtensionHelper::getExtensionRecord($option, 'component')->extension_id, + 'option' => $option, + ]; + + if (!$model->save($data)) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500); + } + + return $this; + } } diff --git a/api/components/com_config/src/View/Application/JsonapiView.php b/api/components/com_config/src/View/Application/JsonapiView.php index 090b2ddb1404d..81748512fac0f 100644 --- a/api/components/com_config/src/View/Application/JsonapiView.php +++ b/api/components/com_config/src/View/Application/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $items = []; - - foreach ($model->getData() as $key => $value) - { - $item = (object) [$key => $value]; - $items[] = $this->prepareItem($item); - } - - // Set up links for pagination - $currentUrl = Uri::getInstance(); - $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; - $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); - - $offset = $currentPageQuery['offset']; - $limit = $currentPageQuery['limit']; - $totalItemsCount = \count($items); - $totalPagesAvailable = ceil($totalItemsCount / $limit); - - $items = array_splice($items, $offset, $limit); - - $this->document->addMeta('total-pages', $totalPagesAvailable) - ->addLink('self', (string) $currentUrl); - - // Check for first and previous pages - if ($offset > 0) - { - $firstPage = clone $currentUrl; - $firstPageQuery = $currentPageQuery; - $firstPageQuery['offset'] = 0; - $firstPage->setVar('page', $firstPageQuery); - - $previousPage = clone $currentUrl; - $previousPageQuery = $currentPageQuery; - $previousOffset = $currentPageQuery['offset'] - $limit; - $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; - $previousPage->setVar('page', $previousPageQuery); - - $this->document->addLink('first', $this->queryEncode((string) $firstPage)) - ->addLink('previous', $this->queryEncode((string) $previousPage)); - } - - // Check for next and last pages - if ($offset + $limit < $totalItemsCount) - { - $nextPage = clone $currentUrl; - $nextPageQuery = $currentPageQuery; - $nextOffset = $currentPageQuery['offset'] + $limit; - $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; - $nextPage->setVar('page', $nextPageQuery); - - $lastPage = clone $currentUrl; - $lastPageQuery = $currentPageQuery; - $lastPageQuery['offset'] = ($totalPagesAvailable - 1) * $limit; - $lastPage->setVar('page', $lastPageQuery); - - $this->document->addLink('next', $this->queryEncode((string) $nextPage)) - ->addLink('last', $this->queryEncode((string) $lastPage)); - } - - $collection = (new Collection($items, new JoomlaSerializer($this->type))); - - // Set the data into the document and render it - $this->document->setData($collection); - - return $this->document->render(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - $item->id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - - return $item; - } + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + /** @var ApplicationModel $model */ + $model = $this->getModel(); + $items = []; + + foreach ($model->getData() as $key => $value) { + $item = (object) [$key => $value]; + $items[] = $this->prepareItem($item); + } + + // Set up links for pagination + $currentUrl = Uri::getInstance(); + $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; + $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); + + $offset = $currentPageQuery['offset']; + $limit = $currentPageQuery['limit']; + $totalItemsCount = \count($items); + $totalPagesAvailable = ceil($totalItemsCount / $limit); + + $items = array_splice($items, $offset, $limit); + + $this->document->addMeta('total-pages', $totalPagesAvailable) + ->addLink('self', (string) $currentUrl); + + // Check for first and previous pages + if ($offset > 0) { + $firstPage = clone $currentUrl; + $firstPageQuery = $currentPageQuery; + $firstPageQuery['offset'] = 0; + $firstPage->setVar('page', $firstPageQuery); + + $previousPage = clone $currentUrl; + $previousPageQuery = $currentPageQuery; + $previousOffset = $currentPageQuery['offset'] - $limit; + $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; + $previousPage->setVar('page', $previousPageQuery); + + $this->document->addLink('first', $this->queryEncode((string) $firstPage)) + ->addLink('previous', $this->queryEncode((string) $previousPage)); + } + + // Check for next and last pages + if ($offset + $limit < $totalItemsCount) { + $nextPage = clone $currentUrl; + $nextPageQuery = $currentPageQuery; + $nextOffset = $currentPageQuery['offset'] + $limit; + $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; + $nextPage->setVar('page', $nextPageQuery); + + $lastPage = clone $currentUrl; + $lastPageQuery = $currentPageQuery; + $lastPageQuery['offset'] = ($totalPagesAvailable - 1) * $limit; + $lastPage->setVar('page', $lastPageQuery); + + $this->document->addLink('next', $this->queryEncode((string) $nextPage)) + ->addLink('last', $this->queryEncode((string) $lastPage)); + } + + $collection = (new Collection($items, new JoomlaSerializer($this->type))); + + // Set the data into the document and render it + $this->document->setData($collection); + + return $this->document->render(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; + + return $item; + } } diff --git a/api/components/com_config/src/View/Component/JsonapiView.php b/api/components/com_config/src/View/Component/JsonapiView.php index 1381854646ac1..6c600c8bfa205 100644 --- a/api/components/com_config/src/View/Component/JsonapiView.php +++ b/api/components/com_config/src/View/Component/JsonapiView.php @@ -1,4 +1,5 @@ get('component_name')); - - if ($component === null || !$component->enabled) - { - // @todo: exception component unavailable - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_INVALID_COMPONENT_NAME'), 400); - } - - $data = $component->getParams()->toObject(); - } - catch (\Exception $e) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500, $e); - } - - $items = []; - - foreach ($data as $key => $value) - { - $item = (object) [$key => $value]; - $items[] = $this->prepareItem($item); - } - - // Set up links for pagination - $currentUrl = Uri::getInstance(); - $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; - $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); - - $offset = $currentPageQuery['offset']; - $limit = $currentPageQuery['limit']; - $totalItemsCount = \count($items); - $totalPagesAvailable = ceil($totalItemsCount / $limit); - - $items = array_splice($items, $offset, $limit); - - $this->document->addMeta('total-pages', $totalPagesAvailable) - ->addLink('self', (string) $currentUrl); - - // Check for first and previous pages - if ($offset > 0) - { - $firstPage = clone $currentUrl; - $firstPageQuery = $currentPageQuery; - $firstPageQuery['offset'] = 0; - $firstPage->setVar('page', $firstPageQuery); - - $previousPage = clone $currentUrl; - $previousPageQuery = $currentPageQuery; - $previousOffset = $currentPageQuery['offset'] - $limit; - $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; - $previousPage->setVar('page', $previousPageQuery); - - $this->document->addLink('first', $this->queryEncode((string) $firstPage)) - ->addLink('previous', $this->queryEncode((string) $previousPage)); - } - - // Check for next and last pages - if ($offset + $limit < $totalItemsCount) - { - $nextPage = clone $currentUrl; - $nextPageQuery = $currentPageQuery; - $nextOffset = $currentPageQuery['offset'] + $limit; - $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; - $nextPage->setVar('page', $nextPageQuery); - - $lastPage = clone $currentUrl; - $lastPageQuery = $currentPageQuery; - $lastPageQuery['offset'] = ($totalPagesAvailable - 1) * $limit; - $lastPage->setVar('page', $lastPageQuery); - - $this->document->addLink('next', $this->queryEncode((string) $nextPage)) - ->addLink('last', $this->queryEncode((string) $lastPage)); - } - - $collection = (new Collection($items, new JoomlaSerializer($this->type))); - - // Set the data into the document and render it - $this->document->setData($collection); - - return $this->document->render(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - $item->id = ExtensionHelper::getExtensionRecord($this->get('component_name'), 'component')->extension_id; - - return $item; - } + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + try { + $component = ComponentHelper::getComponent($this->get('component_name')); + + if ($component === null || !$component->enabled) { + // @todo: exception component unavailable + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_INVALID_COMPONENT_NAME'), 400); + } + + $data = $component->getParams()->toObject(); + } catch (\Exception $e) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500, $e); + } + + $items = []; + + foreach ($data as $key => $value) { + $item = (object) [$key => $value]; + $items[] = $this->prepareItem($item); + } + + // Set up links for pagination + $currentUrl = Uri::getInstance(); + $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; + $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); + + $offset = $currentPageQuery['offset']; + $limit = $currentPageQuery['limit']; + $totalItemsCount = \count($items); + $totalPagesAvailable = ceil($totalItemsCount / $limit); + + $items = array_splice($items, $offset, $limit); + + $this->document->addMeta('total-pages', $totalPagesAvailable) + ->addLink('self', (string) $currentUrl); + + // Check for first and previous pages + if ($offset > 0) { + $firstPage = clone $currentUrl; + $firstPageQuery = $currentPageQuery; + $firstPageQuery['offset'] = 0; + $firstPage->setVar('page', $firstPageQuery); + + $previousPage = clone $currentUrl; + $previousPageQuery = $currentPageQuery; + $previousOffset = $currentPageQuery['offset'] - $limit; + $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; + $previousPage->setVar('page', $previousPageQuery); + + $this->document->addLink('first', $this->queryEncode((string) $firstPage)) + ->addLink('previous', $this->queryEncode((string) $previousPage)); + } + + // Check for next and last pages + if ($offset + $limit < $totalItemsCount) { + $nextPage = clone $currentUrl; + $nextPageQuery = $currentPageQuery; + $nextOffset = $currentPageQuery['offset'] + $limit; + $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; + $nextPage->setVar('page', $nextPageQuery); + + $lastPage = clone $currentUrl; + $lastPageQuery = $currentPageQuery; + $lastPageQuery['offset'] = ($totalPagesAvailable - 1) * $limit; + $lastPage->setVar('page', $lastPageQuery); + + $this->document->addLink('next', $this->queryEncode((string) $nextPage)) + ->addLink('last', $this->queryEncode((string) $lastPage)); + } + + $collection = (new Collection($items, new JoomlaSerializer($this->type))); + + // Set the data into the document and render it + $this->document->setData($collection); + + return $this->document->render(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = ExtensionHelper::getExtensionRecord($this->get('component_name'), 'component')->extension_id; + + return $item; + } } diff --git a/api/components/com_contact/src/Controller/ContactController.php b/api/components/com_contact/src/Controller/ContactController.php index b9805d33ab5e9..93663805e3c3f 100644 --- a/api/components/com_contact/src/Controller/ContactController.php +++ b/api/components/com_contact/src/Controller/ContactController.php @@ -1,4 +1,5 @@ name])) - { - !isset($data['com_fields']) && $data['com_fields'] = []; - - $data['com_fields'][$field->name] = $data[$field->name]; - unset($data[$field->name]); - } - } - - return $data; - } - - /** - * Submit contact form - * - * @param integer $id Leave empty if you want to retrieve data from the request - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function submitForm($id = null) - { - if ($id === null) - { - $id = $this->input->post->get('id', 0, 'int'); - } - - $modelName = Inflector::singularize($this->contentType); - - /** @var \Joomla\Component\Contact\Site\Model\ContactModel $model */ - $model = $this->getModel($modelName, 'Site'); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $model->setState('filter.published', 1); - - $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - $contact = $model->getItem($id); - - if ($contact->id === null) - { - throw new RouteNotFoundException('Item does not exist'); - } - - $contactParams = new Registry($contact->params); - - if (!$contactParams->get('show_email_form')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_DISPLAY_EMAIL_FORM')); - } - - // Contact plugins - PluginHelper::importPlugin('contact'); - - Form::addFormPath(JPATH_COMPONENT_SITE . '/forms'); - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - throw new \RuntimeException($model->getError(), 500); - } - - if (!$model->validate($form, $data)) - { - $errors = $model->getErrors(); - $messages = []; - - for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $messages[] = "{$errors[$i]->getMessage()}"; - } - else - { - $messages[] = "{$errors[$i]}"; - } - } - - throw new InvalidParameterException(implode("\n", $messages)); - } - - // Validation succeeded, continue with custom handlers - $results = $this->app->triggerEvent('onValidateContact', [&$contact, &$data]); - - foreach ($results as $result) - { - if ($result instanceof \Exception) - { - throw new InvalidParameterException($result->getMessage()); - } - } - - // Passed Validation: Process the contact plugins to integrate with other applications - $this->app->triggerEvent('onSubmitContact', [&$contact, &$data]); - - // Send the email - $sent = false; - - $params = ComponentHelper::getParams('com_contact'); - - if (!$params->get('custom_reply')) - { - $sent = $this->_sendEmail($data, $contact, $params->get('show_email_copy', 0)); - } - - if (!$sent) - { - throw new SendEmail('Error sending message'); - } - - return $this; - } - - /** - * Method to get a model object, loading it if required. - * - * @param array $data The data to send in the email. - * @param \stdClass $contact The user information to send the email to - * @param boolean $emailCopyToSender True to send a copy of the email to the user. - * - * @return boolean True on success sending the email, false on failure. - * - * @since 1.6.4 - */ - private function _sendEmail($data, $contact, $emailCopyToSender) - { - $app = $this->app; - - Factory::getLanguage()->load('com_contact', JPATH_SITE, $app->getLanguage()->getTag(), true); - - if ($contact->email_to == '' && $contact->user_id != 0) - { - $contact_user = User::getInstance($contact->user_id); - $contact->email_to = $contact_user->get('email'); - } - - $templateData = [ - 'sitename' => $app->get('sitename'), - 'name' => $data['contact_name'], - 'contactname' => $contact->name, - 'email' => PunycodeHelper::emailToPunycode($data['contact_email']), - 'subject' => $data['contact_subject'], - 'body' => stripslashes($data['contact_message']), - 'url' => Uri::base(), - 'customfields' => '', - ]; - - // Load the custom fields - if (!empty($data['com_fields']) && $fields = FieldsHelper::getFields('com_contact.mail', $contact, true, $data['com_fields'])) - { - $output = FieldsHelper::render( - 'com_contact.mail', - 'fields.render', - array( - 'context' => 'com_contact.mail', - 'item' => $contact, - 'fields' => $fields, - ) - ); - - if ($output) - { - $templateData['customfields'] = $output; - } - } - - try - { - $mailer = new MailTemplate('com_contact.mail', $app->getLanguage()->getTag()); - $mailer->addRecipient($contact->email_to); - $mailer->setReplyTo($templateData['email'], $templateData['name']); - $mailer->addTemplateData($templateData); - $sent = $mailer->send(); - - // If we are supposed to copy the sender, do so. - if ($emailCopyToSender == true && !empty($data['contact_email_copy'])) - { - $mailer = new MailTemplate('com_contact.mail.copy', $app->getLanguage()->getTag()); - $mailer->addRecipient($templateData['email']); - $mailer->setReplyTo($templateData['email'], $templateData['name']); - $mailer->addTemplateData($templateData); - $sent = $mailer->send(); - } - } - catch (MailDisabledException | phpMailerException $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $sent = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $sent = false; - } - } - - return $sent; - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'contacts'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'contacts'; + + /** + * Method to allow extended classes to manipulate the data to be saved for an extension. + * + * @param array $data An array of input data. + * + * @return array + * + * @since 4.0.0 + */ + protected function preprocessSaveData(array $data): array + { + foreach (FieldsHelper::getFields('com_contact.contact') as $field) { + if (isset($data[$field->name])) { + !isset($data['com_fields']) && $data['com_fields'] = []; + + $data['com_fields'][$field->name] = $data[$field->name]; + unset($data[$field->name]); + } + } + + return $data; + } + + /** + * Submit contact form + * + * @param integer $id Leave empty if you want to retrieve data from the request + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function submitForm($id = null) + { + if ($id === null) { + $id = $this->input->post->get('id', 0, 'int'); + } + + $modelName = Inflector::singularize($this->contentType); + + /** @var \Joomla\Component\Contact\Site\Model\ContactModel $model */ + $model = $this->getModel($modelName, 'Site'); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $model->setState('filter.published', 1); + + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + $contact = $model->getItem($id); + + if ($contact->id === null) { + throw new RouteNotFoundException('Item does not exist'); + } + + $contactParams = new Registry($contact->params); + + if (!$contactParams->get('show_email_form')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_DISPLAY_EMAIL_FORM')); + } + + // Contact plugins + PluginHelper::importPlugin('contact'); + + Form::addFormPath(JPATH_COMPONENT_SITE . '/forms'); + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + throw new \RuntimeException($model->getError(), 500); + } + + if (!$model->validate($form, $data)) { + $errors = $model->getErrors(); + $messages = []; + + for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $messages[] = "{$errors[$i]->getMessage()}"; + } else { + $messages[] = "{$errors[$i]}"; + } + } + + throw new InvalidParameterException(implode("\n", $messages)); + } + + // Validation succeeded, continue with custom handlers + $results = $this->app->triggerEvent('onValidateContact', [&$contact, &$data]); + + foreach ($results as $result) { + if ($result instanceof \Exception) { + throw new InvalidParameterException($result->getMessage()); + } + } + + // Passed Validation: Process the contact plugins to integrate with other applications + $this->app->triggerEvent('onSubmitContact', [&$contact, &$data]); + + // Send the email + $sent = false; + + $params = ComponentHelper::getParams('com_contact'); + + if (!$params->get('custom_reply')) { + $sent = $this->_sendEmail($data, $contact, $params->get('show_email_copy', 0)); + } + + if (!$sent) { + throw new SendEmail('Error sending message'); + } + + return $this; + } + + /** + * Method to get a model object, loading it if required. + * + * @param array $data The data to send in the email. + * @param \stdClass $contact The user information to send the email to + * @param boolean $emailCopyToSender True to send a copy of the email to the user. + * + * @return boolean True on success sending the email, false on failure. + * + * @since 1.6.4 + */ + private function _sendEmail($data, $contact, $emailCopyToSender) + { + $app = $this->app; + + Factory::getLanguage()->load('com_contact', JPATH_SITE, $app->getLanguage()->getTag(), true); + + if ($contact->email_to == '' && $contact->user_id != 0) { + $contact_user = User::getInstance($contact->user_id); + $contact->email_to = $contact_user->get('email'); + } + + $templateData = [ + 'sitename' => $app->get('sitename'), + 'name' => $data['contact_name'], + 'contactname' => $contact->name, + 'email' => PunycodeHelper::emailToPunycode($data['contact_email']), + 'subject' => $data['contact_subject'], + 'body' => stripslashes($data['contact_message']), + 'url' => Uri::base(), + 'customfields' => '', + ]; + + // Load the custom fields + if (!empty($data['com_fields']) && $fields = FieldsHelper::getFields('com_contact.mail', $contact, true, $data['com_fields'])) { + $output = FieldsHelper::render( + 'com_contact.mail', + 'fields.render', + array( + 'context' => 'com_contact.mail', + 'item' => $contact, + 'fields' => $fields, + ) + ); + + if ($output) { + $templateData['customfields'] = $output; + } + } + + try { + $mailer = new MailTemplate('com_contact.mail', $app->getLanguage()->getTag()); + $mailer->addRecipient($contact->email_to); + $mailer->setReplyTo($templateData['email'], $templateData['name']); + $mailer->addTemplateData($templateData); + $sent = $mailer->send(); + + // If we are supposed to copy the sender, do so. + if ($emailCopyToSender == true && !empty($data['contact_email_copy'])) { + $mailer = new MailTemplate('com_contact.mail.copy', $app->getLanguage()->getTag()); + $mailer->addRecipient($templateData['email']); + $mailer->setReplyTo($templateData['email'], $templateData['name']); + $mailer->addTemplateData($templateData); + $sent = $mailer->send(); + } + } catch (MailDisabledException | phpMailerException $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $sent = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $sent = false; + } + } + + return $sent; + } } diff --git a/api/components/com_contact/src/Serializer/ContactSerializer.php b/api/components/com_contact/src/Serializer/ContactSerializer.php index 23fe7333f3b40..e5faea3c5d0fa 100644 --- a/api/components/com_contact/src/Serializer/ContactSerializer.php +++ b/api/components/com_contact/src/Serializer/ContactSerializer.php @@ -1,4 +1,5 @@ type); - - foreach ($model->associations as $association) - { - $resources[] = (new Resource($association, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/contact/' . $association->id)); - } - - $collection = new Collection($resources, $serializer); - - return new Relationship($collection); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function category($model) - { - $serializer = new JoomlaSerializer('categories'); - - $resource = (new Resource($model->catid, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/categories/' . $model->catid)); - - return new Relationship($resource); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function createdBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->created_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); - - return new Relationship($resource); - } - - /** - * Build editor relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function modifiedBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->modified_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); - - return new Relationship($resource); - } - - /** - * Build contact user relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function userId($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->user_id, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->user_id)); - - return new Relationship($resource); - } + use TagApiSerializerTrait; + + /** + * Build content relationships by associations + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function languageAssociations($model) + { + $resources = []; + + // @todo: This can't be hardcoded in the future? + $serializer = new JoomlaSerializer($this->type); + + foreach ($model->associations as $association) { + $resources[] = (new Resource($association, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/contact/' . $association->id)); + } + + $collection = new Collection($resources, $serializer); + + return new Relationship($collection); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function category($model) + { + $serializer = new JoomlaSerializer('categories'); + + $resource = (new Resource($model->catid, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/categories/' . $model->catid)); + + return new Relationship($resource); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function createdBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->created_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); + + return new Relationship($resource); + } + + /** + * Build editor relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function modifiedBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->modified_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); + + return new Relationship($resource); + } + + /** + * Build contact user relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function userId($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->user_id, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->user_id)); + + return new Relationship($resource); + } } diff --git a/api/components/com_contact/src/View/Contacts/JsonapiView.php b/api/components/com_contact/src/View/Contacts/JsonapiView.php index de1091fd6a676..acbc36321acf4 100644 --- a/api/components/com_contact/src/View/Contacts/JsonapiView.php +++ b/api/components/com_contact/src/View/Contacts/JsonapiView.php @@ -1,4 +1,5 @@ serializer = new ContactSerializer($config['contentType']); - } - - parent::__construct($config); - } - - /** - * Execute and display a template script. - * - * @param array|null $items Array of items - * - * @return string - * - * @since 4.0.0 - */ - public function displayList(array $items = null) - { - foreach (FieldsHelper::getFields('com_contact.contact') as $field) - { - $this->fieldsToRenderList[] = $field->name; - } - - return parent::displayList(); - } - - /** - * Execute and display a template script. - * - * @param object $item Item - * - * @return string - * - * @since 4.0.0 - */ - public function displayItem($item = null) - { - foreach (FieldsHelper::getFields('com_contact.contact') as $field) - { - $this->fieldsToRenderItem[] = $field->name; - } - - if (Multilanguage::isEnabled()) - { - $this->fieldsToRenderItem[] = 'languageAssociations'; - $this->relationship[] = 'languageAssociations'; - } - - return parent::displayItem(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - foreach (FieldsHelper::getFields('com_contact.contact', $item, true) as $field) - { - $item->{$field->name} = $field->apivalue ?? $field->rawvalue; - } - - if (Multilanguage::isEnabled() && !empty($item->associations)) - { - $associations = []; - - foreach ($item->associations as $language => $association) - { - $itemId = explode(':', $association)[0]; - - $associations[] = (object) [ - 'id' => $itemId, - 'language' => $language, - ]; - } - - $item->associations = $associations; - } - - if (!empty($item->tags->tags)) - { - $tagsIds = explode(',', $item->tags->tags); - $tagsNames = $item->tagsHelper->getTagNames($tagsIds); - - $item->tags = array_combine($tagsIds, $tagsNames); - } - else - { - $item->tags = []; - } - - if (isset($item->image)) - { - $item->image = ContentHelper::resolve($item->image); - } - - return parent::prepareItem($item); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'alias', + 'name', + 'category', + 'created', + 'created_by', + 'created_by_alias', + 'modified', + 'modified_by', + 'image', + 'tags', + 'featured', + 'publish_up', + 'publish_down', + 'version', + 'hits', + 'metakey', + 'metadesc', + 'metadata', + 'con_position', + 'address', + 'suburb', + 'state', + 'country', + 'postcode', + 'telephone', + 'fax', + 'misc', + 'email_to', + 'default_con', + 'user_id', + 'access', + 'mobile', + 'webpage', + 'sortname1', + 'sortname2', + 'sortname3', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'alias', + 'name', + 'category', + 'created', + 'created_by', + 'created_by_alias', + 'modified', + 'modified_by', + 'image', + 'tags', + 'user_id', + ]; + + /** + * The relationships the item has + * + * @var array + * @since 4.0.0 + */ + protected $relationship = [ + 'category', + 'created_by', + 'modified_by', + 'user_id', + 'tags', + ]; + + /** + * Constructor. + * + * @param array $config A named configuration array for object construction. + * contentType: the name (optional) of the content type to use for the serialization + * + * @since 4.0.0 + */ + public function __construct($config = []) + { + if (\array_key_exists('contentType', $config)) { + $this->serializer = new ContactSerializer($config['contentType']); + } + + parent::__construct($config); + } + + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + foreach (FieldsHelper::getFields('com_contact.contact') as $field) { + $this->fieldsToRenderList[] = $field->name; + } + + return parent::displayList(); + } + + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + foreach (FieldsHelper::getFields('com_contact.contact') as $field) { + $this->fieldsToRenderItem[] = $field->name; + } + + if (Multilanguage::isEnabled()) { + $this->fieldsToRenderItem[] = 'languageAssociations'; + $this->relationship[] = 'languageAssociations'; + } + + return parent::displayItem(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + foreach (FieldsHelper::getFields('com_contact.contact', $item, true) as $field) { + $item->{$field->name} = $field->apivalue ?? $field->rawvalue; + } + + if (Multilanguage::isEnabled() && !empty($item->associations)) { + $associations = []; + + foreach ($item->associations as $language => $association) { + $itemId = explode(':', $association)[0]; + + $associations[] = (object) [ + 'id' => $itemId, + 'language' => $language, + ]; + } + + $item->associations = $associations; + } + + if (!empty($item->tags->tags)) { + $tagsIds = explode(',', $item->tags->tags); + $tagsNames = $item->tagsHelper->getTagNames($tagsIds); + + $item->tags = array_combine($tagsIds, $tagsNames); + } else { + $item->tags = []; + } + + if (isset($item->image)) { + $item->image = ContentHelper::resolve($item->image); + } + + return parent::prepareItem($item); + } } diff --git a/api/components/com_content/src/Controller/ArticlesController.php b/api/components/com_content/src/Controller/ArticlesController.php index bf616d451a866..ea4177b7f06e2 100644 --- a/api/components/com_content/src/Controller/ArticlesController.php +++ b/api/components/com_content/src/Controller/ArticlesController.php @@ -1,4 +1,5 @@ input->get('filter', [], 'array'); - $filter = InputFilter::getInstance(); - - if (\array_key_exists('author', $apiFilterInfo)) - { - $this->modelState->set('filter.author_id', $filter->clean($apiFilterInfo['author'], 'INT')); - } - - if (\array_key_exists('category', $apiFilterInfo)) - { - $this->modelState->set('filter.category_id', $filter->clean($apiFilterInfo['category'], 'INT')); - } - - if (\array_key_exists('search', $apiFilterInfo)) - { - $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); - } - - if (\array_key_exists('state', $apiFilterInfo)) - { - $this->modelState->set('filter.published', $filter->clean($apiFilterInfo['state'], 'INT')); - } - - if (\array_key_exists('language', $apiFilterInfo)) - { - $this->modelState->set('filter.language', $filter->clean($apiFilterInfo['language'], 'STRING')); - } - - $apiListInfo = $this->input->get('list', [], 'array'); - - if (array_key_exists('ordering', $apiListInfo)) - { - $this->modelState->set('list.ordering', $filter->clean($apiListInfo['ordering'], 'STRING')); - } - - if (array_key_exists('direction', $apiListInfo)) - { - $this->modelState->set('list.direction', $filter->clean($apiListInfo['direction'], 'STRING')); - } - - return parent::displayList(); - } - - /** - * Method to allow extended classes to manipulate the data to be saved for an extension. - * - * @param array $data An array of input data. - * - * @return array - * - * @since 4.0.0 - */ - protected function preprocessSaveData(array $data): array - { - foreach (FieldsHelper::getFields('com_content.article') as $field) - { - if (isset($data[$field->name])) - { - !isset($data['com_fields']) && $data['com_fields'] = []; - - $data['com_fields'][$field->name] = $data[$field->name]; - unset($data[$field->name]); - } - } - - return $data; - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'articles'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'articles'; + + /** + * Article list view amended to add filtering of data + * + * @return static A BaseController object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $apiFilterInfo = $this->input->get('filter', [], 'array'); + $filter = InputFilter::getInstance(); + + if (\array_key_exists('author', $apiFilterInfo)) { + $this->modelState->set('filter.author_id', $filter->clean($apiFilterInfo['author'], 'INT')); + } + + if (\array_key_exists('category', $apiFilterInfo)) { + $this->modelState->set('filter.category_id', $filter->clean($apiFilterInfo['category'], 'INT')); + } + + if (\array_key_exists('search', $apiFilterInfo)) { + $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); + } + + if (\array_key_exists('state', $apiFilterInfo)) { + $this->modelState->set('filter.published', $filter->clean($apiFilterInfo['state'], 'INT')); + } + + if (\array_key_exists('language', $apiFilterInfo)) { + $this->modelState->set('filter.language', $filter->clean($apiFilterInfo['language'], 'STRING')); + } + + $apiListInfo = $this->input->get('list', [], 'array'); + + if (array_key_exists('ordering', $apiListInfo)) { + $this->modelState->set('list.ordering', $filter->clean($apiListInfo['ordering'], 'STRING')); + } + + if (array_key_exists('direction', $apiListInfo)) { + $this->modelState->set('list.direction', $filter->clean($apiListInfo['direction'], 'STRING')); + } + + return parent::displayList(); + } + + /** + * Method to allow extended classes to manipulate the data to be saved for an extension. + * + * @param array $data An array of input data. + * + * @return array + * + * @since 4.0.0 + */ + protected function preprocessSaveData(array $data): array + { + foreach (FieldsHelper::getFields('com_content.article') as $field) { + if (isset($data[$field->name])) { + !isset($data['com_fields']) && $data['com_fields'] = []; + + $data['com_fields'][$field->name] = $data[$field->name]; + unset($data[$field->name]); + } + } + + return $data; + } } diff --git a/api/components/com_content/src/Helper/ContentHelper.php b/api/components/com_content/src/Helper/ContentHelper.php index d7e64de506108..5d521516304d7 100644 --- a/api/components/com_content/src/Helper/ContentHelper.php +++ b/api/components/com_content/src/Helper/ContentHelper.php @@ -1,4 +1,5 @@ type); - - foreach ($model->associations as $association) - { - $resources[] = (new Resource($association, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/articles/' . $association->id)); - } - - $collection = new Collection($resources, $serializer); - - return new Relationship($collection); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function category($model) - { - $serializer = new JoomlaSerializer('categories'); - - $resource = (new Resource($model->catid, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/categories/' . $model->catid)); - - return new Relationship($resource); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function createdBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->created_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); - - return new Relationship($resource); - } - - /** - * Build editor relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function modifiedBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->modified_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); - - return new Relationship($resource); - } + /** + * Build content relationships by associations + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function languageAssociations($model) + { + $resources = []; + + // @todo: This can't be hardcoded in the future? + $serializer = new JoomlaSerializer($this->type); + + foreach ($model->associations as $association) { + $resources[] = (new Resource($association, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/articles/' . $association->id)); + } + + $collection = new Collection($resources, $serializer); + + return new Relationship($collection); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function category($model) + { + $serializer = new JoomlaSerializer('categories'); + + $resource = (new Resource($model->catid, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/categories/' . $model->catid)); + + return new Relationship($resource); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function createdBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->created_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); + + return new Relationship($resource); + } + + /** + * Build editor relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function modifiedBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->modified_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); + + return new Relationship($resource); + } } diff --git a/api/components/com_content/src/View/Articles/JsonapiView.php b/api/components/com_content/src/View/Articles/JsonapiView.php index 6b4170a544495..52220f8e5a69f 100644 --- a/api/components/com_content/src/View/Articles/JsonapiView.php +++ b/api/components/com_content/src/View/Articles/JsonapiView.php @@ -1,4 +1,5 @@ serializer = new ContentSerializer($config['contentType']); - } - - parent::__construct($config); - } - - /** - * Execute and display a template script. - * - * @param array|null $items Array of items - * - * @return string - * - * @since 4.0.0 - */ - public function displayList(array $items = null) - { - foreach (FieldsHelper::getFields('com_content.article') as $field) - { - $this->fieldsToRenderList[] = $field->name; - } - - return parent::displayList(); - } - - /** - * Execute and display a template script. - * - * @param object $item Item - * - * @return string - * - * @since 4.0.0 - */ - public function displayItem($item = null) - { - $this->relationship[] = 'modified_by'; - - foreach (FieldsHelper::getFields('com_content.article') as $field) - { - $this->fieldsToRenderItem[] = $field->name; - } - - if (Multilanguage::isEnabled()) - { - $this->fieldsToRenderItem[] = 'languageAssociations'; - $this->relationship[] = 'languageAssociations'; - } - - return parent::displayItem(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - $item->text = $item->introtext . ' ' . $item->fulltext; - - // Process the content plugins. - PluginHelper::importPlugin('content'); - Factory::getApplication()->triggerEvent('onContentPrepare', ['com_content.article', &$item, &$item->params]); - - foreach (FieldsHelper::getFields('com_content.article', $item, true) as $field) - { - $item->{$field->name} = $field->apivalue ?? $field->rawvalue; - } - - if (Multilanguage::isEnabled() && !empty($item->associations)) - { - $associations = []; - - foreach ($item->associations as $language => $association) - { - $itemId = explode(':', $association)[0]; - - $associations[] = (object) [ - 'id' => $itemId, - 'language' => $language, - ]; - } - - $item->associations = $associations; - } - - if (!empty($item->tags->tags)) - { - $tagsIds = explode(',', $item->tags->tags); - $tagsNames = $item->tagsHelper->getTagNames($tagsIds); - - $item->tags = array_combine($tagsIds, $tagsNames); - } - else - { - $item->tags = []; - } - - if (isset($item->images)) - { - $registry = new Registry($item->images); - $item->images = $registry->toArray(); - - if (!empty($item->images['image_intro'])) - { - $item->images['image_intro'] = ContentHelper::resolve($item->images['image_intro']); - } - - if (!empty($item->images['image_fulltext'])) - { - $item->images['image_fulltext'] = ContentHelper::resolve($item->images['image_fulltext']); - } - } - - return parent::prepareItem($item); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'typeAlias', + 'asset_id', + 'title', + 'text', + 'tags', + 'language', + 'state', + 'category', + 'images', + 'metakey', + 'metadesc', + 'metadata', + 'access', + 'featured', + 'alias', + 'note', + 'publish_up', + 'publish_down', + 'urls', + 'created', + 'created_by', + 'created_by_alias', + 'modified', + 'modified_by', + 'hits', + 'version', + 'featured_up', + 'featured_down', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'typeAlias', + 'asset_id', + 'title', + 'text', + 'tags', + 'language', + 'state', + 'category', + 'images', + 'metakey', + 'metadesc', + 'metadata', + 'access', + 'featured', + 'alias', + 'note', + 'publish_up', + 'publish_down', + 'urls', + 'created', + 'created_by', + 'created_by_alias', + 'modified', + 'hits', + 'version', + 'featured_up', + 'featured_down', + ]; + + /** + * The relationships the item has + * + * @var array + * @since 4.0.0 + */ + protected $relationship = [ + 'category', + 'created_by', + 'tags', + ]; + + /** + * Constructor. + * + * @param array $config A named configuration array for object construction. + * contentType: the name (optional) of the content type to use for the serialization + * + * @since 4.0.0 + */ + public function __construct($config = []) + { + if (\array_key_exists('contentType', $config)) { + $this->serializer = new ContentSerializer($config['contentType']); + } + + parent::__construct($config); + } + + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + foreach (FieldsHelper::getFields('com_content.article') as $field) { + $this->fieldsToRenderList[] = $field->name; + } + + return parent::displayList(); + } + + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + $this->relationship[] = 'modified_by'; + + foreach (FieldsHelper::getFields('com_content.article') as $field) { + $this->fieldsToRenderItem[] = $field->name; + } + + if (Multilanguage::isEnabled()) { + $this->fieldsToRenderItem[] = 'languageAssociations'; + $this->relationship[] = 'languageAssociations'; + } + + return parent::displayItem(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->text = $item->introtext . ' ' . $item->fulltext; + + // Process the content plugins. + PluginHelper::importPlugin('content'); + Factory::getApplication()->triggerEvent('onContentPrepare', ['com_content.article', &$item, &$item->params]); + + foreach (FieldsHelper::getFields('com_content.article', $item, true) as $field) { + $item->{$field->name} = $field->apivalue ?? $field->rawvalue; + } + + if (Multilanguage::isEnabled() && !empty($item->associations)) { + $associations = []; + + foreach ($item->associations as $language => $association) { + $itemId = explode(':', $association)[0]; + + $associations[] = (object) [ + 'id' => $itemId, + 'language' => $language, + ]; + } + + $item->associations = $associations; + } + + if (!empty($item->tags->tags)) { + $tagsIds = explode(',', $item->tags->tags); + $tagsNames = $item->tagsHelper->getTagNames($tagsIds); + + $item->tags = array_combine($tagsIds, $tagsNames); + } else { + $item->tags = []; + } + + if (isset($item->images)) { + $registry = new Registry($item->images); + $item->images = $registry->toArray(); + + if (!empty($item->images['image_intro'])) { + $item->images['image_intro'] = ContentHelper::resolve($item->images['image_intro']); + } + + if (!empty($item->images['image_fulltext'])) { + $item->images['image_fulltext'] = ContentHelper::resolve($item->images['image_fulltext']); + } + } + + return parent::prepareItem($item); + } } diff --git a/api/components/com_contenthistory/src/Controller/HistoryController.php b/api/components/com_contenthistory/src/Controller/HistoryController.php index fba5a7f2dd75e..6a8e0dca2157b 100644 --- a/api/components/com_contenthistory/src/Controller/HistoryController.php +++ b/api/components/com_contenthistory/src/Controller/HistoryController.php @@ -1,4 +1,5 @@ modelState->set('type_alias', $this->getTypeAliasFromInput()); - $this->modelState->set('type_id', $this->getTypeIdFromInput()); - $this->modelState->set('item_id', $this->getTypeAliasFromInput() . '.' . $this->getItemIdFromInput()); - $this->modelState->set('list.ordering', 'h.save_date'); - $this->modelState->set('list.direction', 'DESC'); - - return parent::displayList(); - } - - /** - * Method to edit an existing record. - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function keep() - { - /** @var HistoryModel $model */ - $model = $this->getModel($this->contentType); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $recordId = $this->input->getInt('id'); - - if (!$recordId) - { - throw new Exception\ResourceNotFound(Text::_('JLIB_APPLICATION_ERROR_RECORD'), 404); - } - - $cid = [$recordId]; - - if (!$model->keep($cid)) - { - throw new Exception\Save(Text::plural('COM_CONTENTHISTORY_N_ITEMS_KEEP_TOGGLE', \count($cid))); - } - - return $this; - } - - /** - * Get item id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getItemIdFromInput() - { - return $this->input->exists('id') ? - $this->input->get('id') : $this->input->post->get('id'); - } - - /** - * Get type id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getTypeIdFromInput() - { - return $this->input->exists('type_id') ? - $this->input->get('type_id') : $this->input->post->get('type_id'); - } - - /** - * Get type alias from input - * - * @return string - * - * @since 4.0.0 - */ - private function getTypeAliasFromInput() - { - return $this->input->exists('type_alias') ? - $this->input->get('type_alias') : $this->input->post->get('type_alias'); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'history'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'history'; + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('type_alias', $this->getTypeAliasFromInput()); + $this->modelState->set('type_id', $this->getTypeIdFromInput()); + $this->modelState->set('item_id', $this->getTypeAliasFromInput() . '.' . $this->getItemIdFromInput()); + $this->modelState->set('list.ordering', 'h.save_date'); + $this->modelState->set('list.direction', 'DESC'); + + return parent::displayList(); + } + + /** + * Method to edit an existing record. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function keep() + { + /** @var HistoryModel $model */ + $model = $this->getModel($this->contentType); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $recordId = $this->input->getInt('id'); + + if (!$recordId) { + throw new Exception\ResourceNotFound(Text::_('JLIB_APPLICATION_ERROR_RECORD'), 404); + } + + $cid = [$recordId]; + + if (!$model->keep($cid)) { + throw new Exception\Save(Text::plural('COM_CONTENTHISTORY_N_ITEMS_KEEP_TOGGLE', \count($cid))); + } + + return $this; + } + + /** + * Get item id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getItemIdFromInput() + { + return $this->input->exists('id') ? + $this->input->get('id') : $this->input->post->get('id'); + } + + /** + * Get type id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getTypeIdFromInput() + { + return $this->input->exists('type_id') ? + $this->input->get('type_id') : $this->input->post->get('type_id'); + } + + /** + * Get type alias from input + * + * @return string + * + * @since 4.0.0 + */ + private function getTypeAliasFromInput() + { + return $this->input->exists('type_alias') ? + $this->input->get('type_alias') : $this->input->post->get('type_alias'); + } } diff --git a/api/components/com_contenthistory/src/View/History/JsonapiView.php b/api/components/com_contenthistory/src/View/History/JsonapiView.php index 686c049c8922b..2017bc6a272dd 100644 --- a/api/components/com_contenthistory/src/View/History/JsonapiView.php +++ b/api/components/com_contenthistory/src/View/History/JsonapiView.php @@ -1,4 +1,5 @@ id = $item->version_id; - unset($item->version_id); - - $item->version_data = (array) json_decode($item->version_data, true); - - return parent::prepareItem($item); - } + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'ucm_item_id', + 'ucm_type_id', + 'version_note', + 'save_date', + 'editor_user_id', + 'character_count', + 'sha1_hash', + 'version_data', + 'keep_forever', + 'editor', + ]; + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->version_id; + unset($item->version_id); + + $item->version_data = (array) json_decode($item->version_data, true); + + return parent::prepareItem($item); + } } diff --git a/api/components/com_fields/src/Controller/FieldsController.php b/api/components/com_fields/src/Controller/FieldsController.php index d3611102f6714..012265a0fd164 100644 --- a/api/components/com_fields/src/Controller/FieldsController.php +++ b/api/components/com_fields/src/Controller/FieldsController.php @@ -1,4 +1,5 @@ modelState->set('filter.context', $this->getContextFromInput()); + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.context', $this->getContextFromInput()); - return parent::displayItem($id); - } + return parent::displayItem($id); + } - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.context', $this->getContextFromInput()); + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.context', $this->getContextFromInput()); - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Get extension from input - * - * @return string - * - * @since 4.0.0 - */ - private function getContextFromInput() - { - return $this->input->exists('context') ? - $this->input->get('context') : $this->input->post->get('context'); - } + /** + * Get extension from input + * + * @return string + * + * @since 4.0.0 + */ + private function getContextFromInput() + { + return $this->input->exists('context') ? + $this->input->get('context') : $this->input->post->get('context'); + } } diff --git a/api/components/com_fields/src/Controller/GroupsController.php b/api/components/com_fields/src/Controller/GroupsController.php index 85e42217ec930..768e93936e9c4 100644 --- a/api/components/com_fields/src/Controller/GroupsController.php +++ b/api/components/com_fields/src/Controller/GroupsController.php @@ -1,4 +1,5 @@ modelState->set('filter.context', $this->getContextFromInput()); + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.context', $this->getContextFromInput()); - return parent::displayItem($id); - } + return parent::displayItem($id); + } - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.context', $this->getContextFromInput()); + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.context', $this->getContextFromInput()); - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Get extension from input - * - * @return string - * - * @since 4.0.0 - */ - private function getContextFromInput() - { - return $this->input->exists('context') ? - $this->input->get('context') : $this->input->post->get('context'); - } + /** + * Get extension from input + * + * @return string + * + * @since 4.0.0 + */ + private function getContextFromInput() + { + return $this->input->exists('context') ? + $this->input->get('context') : $this->input->post->get('context'); + } } diff --git a/api/components/com_fields/src/View/Fields/JsonapiView.php b/api/components/com_fields/src/View/Fields/JsonapiView.php index 06adcbcf9d51a..2101cbc2a420d 100644 --- a/api/components/com_fields/src/View/Fields/JsonapiView.php +++ b/api/components/com_fields/src/View/Fields/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $item = $this->prepareItem($model->getItem()); - } + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + if ($item === null) { + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(); + $item = $this->prepareItem($model->getItem()); + } - if ($item->id === null) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->id === null) { + throw new RouteNotFoundException('Item does not exist'); + } - if ($item->context != $this->getModel()->getState('filter.context')) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->context != $this->getModel()->getState('filter.context')) { + throw new RouteNotFoundException('Item does not exist'); + } - return parent::displayItem($item); - } + return parent::displayItem($item); + } } diff --git a/api/components/com_fields/src/View/Groups/JsonapiView.php b/api/components/com_fields/src/View/Groups/JsonapiView.php index 30d6f55e01e47..992f324aeff94 100644 --- a/api/components/com_fields/src/View/Groups/JsonapiView.php +++ b/api/components/com_fields/src/View/Groups/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $item = $this->prepareItem($model->getItem()); - } + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + if ($item === null) { + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(); + $item = $this->prepareItem($model->getItem()); + } - if ($item->id === null) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->id === null) { + throw new RouteNotFoundException('Item does not exist'); + } - if ($item->context != $this->getModel()->getState('filter.context')) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->context != $this->getModel()->getState('filter.context')) { + throw new RouteNotFoundException('Item does not exist'); + } - return parent::displayItem($item); - } + return parent::displayItem($item); + } } diff --git a/api/components/com_installer/src/Controller/ManageController.php b/api/components/com_installer/src/Controller/ManageController.php index c87ebc370f990..f11b91b51442d 100644 --- a/api/components/com_installer/src/Controller/ManageController.php +++ b/api/components/com_installer/src/Controller/ManageController.php @@ -1,4 +1,5 @@ input->get('core', $this->input->get->get('core')); + /** + * Extension list view amended to add filtering of data + * + * @return static A BaseController object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $requestBool = $this->input->get('core', $this->input->get->get('core')); - if (!\is_null($requestBool) && $requestBool !== 'true' && $requestBool !== 'false') - { - // Send the error response - $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'core'); + if (!\is_null($requestBool) && $requestBool !== 'true' && $requestBool !== 'false') { + // Send the error response + $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'core'); - throw new InvalidParameterException($error, 400, null, 'core'); - } + throw new InvalidParameterException($error, 400, null, 'core'); + } - if (!\is_null($requestBool)) - { - $this->modelState->set('filter.core', ($requestBool === 'true') ? '1' : '0'); - } + if (!\is_null($requestBool)) { + $this->modelState->set('filter.core', ($requestBool === 'true') ? '1' : '0'); + } - $this->modelState->set('filter.status', $this->input->get('status', $this->input->get->get('status', null, 'INT'), 'INT')); - $this->modelState->set('filter.type', $this->input->get('type', $this->input->get->get('type', null, 'STRING'), 'STRING')); + $this->modelState->set('filter.status', $this->input->get('status', $this->input->get->get('status', null, 'INT'), 'INT')); + $this->modelState->set('filter.type', $this->input->get('type', $this->input->get->get('type', null, 'STRING'), 'STRING')); - return parent::displayList(); - } + return parent::displayList(); + } } diff --git a/api/components/com_installer/src/View/Manage/JsonapiView.php b/api/components/com_installer/src/View/Manage/JsonapiView.php index 9cd75874731d7..b726c04b88335 100644 --- a/api/components/com_installer/src/View/Manage/JsonapiView.php +++ b/api/components/com_installer/src/View/Manage/JsonapiView.php @@ -1,4 +1,5 @@ id = $item->extension_id; - unset($item->extension_id); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->extension_id; + unset($item->extension_id); - return $item; - } + return $item; + } } diff --git a/api/components/com_languages/src/Controller/LanguagesController.php b/api/components/com_languages/src/Controller/LanguagesController.php index 5ef456a9d2416..0eb6e253c1a48 100644 --- a/api/components/com_languages/src/Controller/LanguagesController.php +++ b/api/components/com_languages/src/Controller/LanguagesController.php @@ -1,4 +1,5 @@ modelState->set('filter.language', $this->getLanguageFromInput()); - $this->modelState->set('filter.client', $this->getClientFromInput()); - - return parent::displayItem($id); - } - - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.language', $this->getLanguageFromInput()); - $this->modelState->set('filter.client', $this->getClientFromInput()); - - return parent::displayList(); - } - - /** - * Method to save a record. - * - * @param integer $recordKey The primary key of the item (if exists) - * - * @return integer The record ID on success, false on failure - * - * @since 4.0.0 - */ - protected function save($recordKey = null) - { - /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ - $model = $this->getModel(Inflector::singularize($this->contentType)); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $model->setState('filter.language', $this->input->post->get('lang_code')); - $model->setState('filter.client', $this->input->post->get('app')); - - $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - - // @todo: Not the cleanest thing ever but it works... - Form::addFormPath(JPATH_COMPONENT_ADMINISTRATOR . '/forms'); - - // Validate the posted data. - $form = $model->getForm($data, false); - - if (!$form) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_FORM_CREATE')); - } - - // Test whether the data is valid. - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - $errors = $model->getErrors(); - $messages = []; - - for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $messages[] = "{$errors[$i]->getMessage()}"; - } - else - { - $messages[] = "{$errors[$i]}"; - } - } - - throw new InvalidParameterException(implode("\n", $messages)); - } - - if (!isset($validData['tags'])) - { - $validData['tags'] = []; - } - - if (!$model->save($validData)) - { - throw new Exception\Save(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError())); - } - - return $validData['key']; - } - - /** - * Removes an item. - * - * @param integer $id The primary key to delete item. - * - * @return void - * - * @since 4.0.0 - */ - public function delete($id = null) - { - $id = $this->input->get('id', '', 'string'); - - $this->input->set('model', $this->contentType); - - $this->modelState->set('filter.language', $this->getLanguageFromInput()); - $this->modelState->set('filter.client', $this->getClientFromInput()); - - parent::delete($id); - } - - /** - * Get client code from input - * - * @return string - * - * @since 4.0.0 - */ - private function getClientFromInput() - { - return $this->input->exists('app') ? $this->input->get('app') : $this->input->post->get('app'); - } - - /** - * Get language code from input - * - * @return string - * - * @since 4.0.0 - */ - private function getLanguageFromInput() - { - return $this->input->exists('lang_code') ? - $this->input->get('lang_code') : $this->input->post->get('lang_code'); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'overrides'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'overrides'; + + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.language', $this->getLanguageFromInput()); + $this->modelState->set('filter.client', $this->getClientFromInput()); + + return parent::displayItem($id); + } + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.language', $this->getLanguageFromInput()); + $this->modelState->set('filter.client', $this->getClientFromInput()); + + return parent::displayList(); + } + + /** + * Method to save a record. + * + * @param integer $recordKey The primary key of the item (if exists) + * + * @return integer The record ID on success, false on failure + * + * @since 4.0.0 + */ + protected function save($recordKey = null) + { + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(Inflector::singularize($this->contentType)); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $model->setState('filter.language', $this->input->post->get('lang_code')); + $model->setState('filter.client', $this->input->post->get('app')); + + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + + // @todo: Not the cleanest thing ever but it works... + Form::addFormPath(JPATH_COMPONENT_ADMINISTRATOR . '/forms'); + + // Validate the posted data. + $form = $model->getForm($data, false); + + if (!$form) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_FORM_CREATE')); + } + + // Test whether the data is valid. + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + $errors = $model->getErrors(); + $messages = []; + + for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $messages[] = "{$errors[$i]->getMessage()}"; + } else { + $messages[] = "{$errors[$i]}"; + } + } + + throw new InvalidParameterException(implode("\n", $messages)); + } + + if (!isset($validData['tags'])) { + $validData['tags'] = []; + } + + if (!$model->save($validData)) { + throw new Exception\Save(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError())); + } + + return $validData['key']; + } + + /** + * Removes an item. + * + * @param integer $id The primary key to delete item. + * + * @return void + * + * @since 4.0.0 + */ + public function delete($id = null) + { + $id = $this->input->get('id', '', 'string'); + + $this->input->set('model', $this->contentType); + + $this->modelState->set('filter.language', $this->getLanguageFromInput()); + $this->modelState->set('filter.client', $this->getClientFromInput()); + + parent::delete($id); + } + + /** + * Get client code from input + * + * @return string + * + * @since 4.0.0 + */ + private function getClientFromInput() + { + return $this->input->exists('app') ? $this->input->get('app') : $this->input->post->get('app'); + } + + /** + * Get language code from input + * + * @return string + * + * @since 4.0.0 + */ + private function getLanguageFromInput() + { + return $this->input->exists('lang_code') ? + $this->input->get('lang_code') : $this->input->post->get('lang_code'); + } } diff --git a/api/components/com_languages/src/Controller/StringsController.php b/api/components/com_languages/src/Controller/StringsController.php index 4bdd554f7040e..e882f908270e7 100644 --- a/api/components/com_languages/src/Controller/StringsController.php +++ b/api/components/com_languages/src/Controller/StringsController.php @@ -1,4 +1,5 @@ input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - - if (!isset($data['searchstring']) || !\is_string($data['searchstring'])) - { - throw new InvalidParameterException("Invalid param 'searchstring'"); - } - - if (!isset($data['searchtype']) || !\in_array($data['searchtype'], ['constant', 'value'])) - { - throw new InvalidParameterException("Invalid param 'searchtype'"); - } - - $app = Factory::getApplication(); - $app->input->set('searchstring', $data['searchstring']); - $app->input->set('searchtype', $data['searchtype']); - $app->input->set('more', 0); - - $viewType = $this->app->getDocument()->getType(); - $viewName = $this->input->get('view', $this->default_view); - $viewLayout = $this->input->get('layout', 'default', 'string'); - - try - { - /** @var \Joomla\Component\Languages\Api\View\Strings\JsonapiView $view */ - $view = $this->getView( - $viewName, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } - - /** @var \Joomla\Component\Languages\Administrator\Model\StringsModel $model */ - $model = $this->getModel($this->contentType, '', ['ignore_request' => true]); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - // Push the model into the view (as default) - $view->setModel($model, true); - - $view->document = $this->app->getDocument(); - $view->displayList(); - - return $this; - } - - /** - * Refresh cache - * - * @return static A \JControllerLegacy object to support chaining. - * - * @throws \Exception - * @since 4.0.0 - */ - public function refresh() - { - /** @var \Joomla\Component\Languages\Administrator\Model\StringsModel $model */ - $model = $this->getModel($this->contentType, '', ['ignore_request' => true]); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $result = $model->refresh(); - - if ($result instanceof \Exception) - { - throw $result; - } - - return $this; - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'strings'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'strings'; + + /** + * Search by languages constants + * + * @return static A \JControllerLegacy object to support chaining. + * + * @throws InvalidParameterException + * @since 4.0.0 + */ + public function search() + { + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + + if (!isset($data['searchstring']) || !\is_string($data['searchstring'])) { + throw new InvalidParameterException("Invalid param 'searchstring'"); + } + + if (!isset($data['searchtype']) || !\in_array($data['searchtype'], ['constant', 'value'])) { + throw new InvalidParameterException("Invalid param 'searchtype'"); + } + + $app = Factory::getApplication(); + $app->input->set('searchstring', $data['searchstring']); + $app->input->set('searchtype', $data['searchtype']); + $app->input->set('more', 0); + + $viewType = $this->app->getDocument()->getType(); + $viewName = $this->input->get('view', $this->default_view); + $viewLayout = $this->input->get('layout', 'default', 'string'); + + try { + /** @var \Joomla\Component\Languages\Api\View\Strings\JsonapiView $view */ + $view = $this->getView( + $viewName, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } + + /** @var \Joomla\Component\Languages\Administrator\Model\StringsModel $model */ + $model = $this->getModel($this->contentType, '', ['ignore_request' => true]); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + // Push the model into the view (as default) + $view->setModel($model, true); + + $view->document = $this->app->getDocument(); + $view->displayList(); + + return $this; + } + + /** + * Refresh cache + * + * @return static A \JControllerLegacy object to support chaining. + * + * @throws \Exception + * @since 4.0.0 + */ + public function refresh() + { + /** @var \Joomla\Component\Languages\Administrator\Model\StringsModel $model */ + $model = $this->getModel($this->contentType, '', ['ignore_request' => true]); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $result = $model->refresh(); + + if ($result instanceof \Exception) { + throw $result; + } + + return $this; + } } diff --git a/api/components/com_languages/src/View/Languages/JsonapiView.php b/api/components/com_languages/src/View/Languages/JsonapiView.php index 803a9896d22c0..208ea1f59afa5 100644 --- a/api/components/com_languages/src/View/Languages/JsonapiView.php +++ b/api/components/com_languages/src/View/Languages/JsonapiView.php @@ -1,4 +1,5 @@ id = $item->lang_id; - unset($item->lang->id); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->lang_id; + unset($item->lang->id); - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/api/components/com_languages/src/View/Overrides/JsonapiView.php b/api/components/com_languages/src/View/Overrides/JsonapiView.php index eb23f85d65048..77c806f9bfb1d 100644 --- a/api/components/com_languages/src/View/Overrides/JsonapiView.php +++ b/api/components/com_languages/src/View/Overrides/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $id = $model->getState($model->getName() . '.id'); - $item = $this->prepareItem($model->getItem($id)); + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + /** @var \Joomla\Component\Languages\Administrator\Model\OverrideModel $model */ + $model = $this->getModel(); + $id = $model->getState($model->getName() . '.id'); + $item = $this->prepareItem($model->getItem($id)); - return parent::displayItem($item); - } - /** - * Execute and display a template script. - * - * @param array|null $items Array of items - * - * @return string - * - * @since 4.0.0 - */ - public function displayList(array $items = null) - { - /** @var \Joomla\Component\Languages\Administrator\Model\OverridesModel $model */ - $model = $this->getModel(); - $items = []; + return parent::displayItem($item); + } + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + /** @var \Joomla\Component\Languages\Administrator\Model\OverridesModel $model */ + $model = $this->getModel(); + $items = []; - foreach ($model->getOverrides() as $key => $override) - { - $item = (object) [ - 'key' => $key, - 'override' => $override, - ]; + foreach ($model->getOverrides() as $key => $override) { + $item = (object) [ + 'key' => $key, + 'override' => $override, + ]; - $items[] = $this->prepareItem($item); - } + $items[] = $this->prepareItem($item); + } - return parent::displayList($items); - } + return parent::displayList($items); + } - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - $item->id = $item->key; - $item->value = $item->override; - unset($item->key); - unset($item->override); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->key; + $item->value = $item->override; + unset($item->key); + unset($item->override); - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/api/components/com_languages/src/View/Strings/JsonapiView.php b/api/components/com_languages/src/View/Strings/JsonapiView.php index 73766eec29132..a386c6816f348 100644 --- a/api/components/com_languages/src/View/Strings/JsonapiView.php +++ b/api/components/com_languages/src/View/Strings/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $result = $model->search(); - - if ($result instanceof \Exception) - { - throw $result; - } - - $items = []; - - foreach ($result['results'] as $item) - { - $items[] = $this->prepareItem($item); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - if ($this->type === null) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CONTENT_TYPE_MISSING'), 400); - } - - $collection = (new Collection($items, new JoomlaSerializer($this->type))) - ->fields([$this->type => $this->fieldsToRenderList]); - - // Set the data into the document and render it - $this->document->setData($collection); - - return $this->document->render(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - $item->id = $item->constant; - unset($item->constant); - - return parent::prepareItem($item); - } + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'string', + 'file', + ]; + + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + /** @var \Joomla\Component\Languages\Administrator\Model\StringsModel $model */ + $model = $this->getModel(); + $result = $model->search(); + + if ($result instanceof \Exception) { + throw $result; + } + + $items = []; + + foreach ($result['results'] as $item) { + $items[] = $this->prepareItem($item); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + if ($this->type === null) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CONTENT_TYPE_MISSING'), 400); + } + + $collection = (new Collection($items, new JoomlaSerializer($this->type))) + ->fields([$this->type => $this->fieldsToRenderList]); + + // Set the data into the document and render it + $this->document->setData($collection); + + return $this->document->render(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->constant; + unset($item->constant); + + return parent::prepareItem($item); + } } diff --git a/api/components/com_menus/src/Controller/ItemsController.php b/api/components/com_menus/src/Controller/ItemsController.php index 44f5da57bab61..10a55e540ab56 100644 --- a/api/components/com_menus/src/Controller/ItemsController.php +++ b/api/components/com_menus/src/Controller/ItemsController.php @@ -1,4 +1,5 @@ modelState->set('filter.client_id', $this->getClientIdFromInput()); - - return parent::displayItem($id); - } - - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); - - return parent::displayList(); - } - - /** - * Method to add a new record. - * - * @return void - * - * @since 4.0.0 - * @throws NotAllowed - * @throws \RuntimeException - */ - public function add() - { - $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - - if (isset($data['menutype'])) - { - $this->input->set('menutype', $data['menutype']); - $this->input->set('com_menus.items.menutype', $data['menutype']); - } - - isset($data['type']) && $this->input->set('type', $data['type']); - isset($data['parent_id']) && $this->input->set('parent_id', $data['parent_id']); - isset($data['link']) && $this->input->set('link', $data['link']); - - $this->input->set('id', '0'); - - parent::add(); - } - - /** - * Method to edit an existing record. - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function edit() - { - $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - - if (isset($data['menutype'])) - { - $this->input->set('menutype', $data['menutype']); - $this->input->set('com_menus.items.menutype', $data['menutype']); - } - - isset($data['type']) && $this->input->set('type', $data['type']); - isset($data['parent_id']) && $this->input->set('parent_id', $data['parent_id']); - isset($data['link']) && $this->input->set('link', $data['link']); - - return parent::edit(); - } - - /** - * Return menu items types - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function getTypes() - { - $viewType = $this->app->getDocument()->getType(); - $viewName = $this->input->get('view', $this->default_view); - $viewLayout = $this->input->get('layout', 'default', 'string'); - - try - { - /** @var JsonapiView $view */ - $view = $this->getView( - $viewName, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } - - /** @var ListModel $model */ - $model = $this->getModel('menutypes', '', ['ignore_request' => true]); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $model->setState('client_id', $this->getClientIdFromInput()); - - $view->setModel($model, true); - - $view->document = $this->app->getDocument(); - - $view->displayListTypes(); - - return $this; - } - - /** - * Get client id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getClientIdFromInput() - { - return $this->input->exists('client_id') ? - $this->input->get('client_id') : $this->input->post->get('client_id'); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'items'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'items'; + + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + + return parent::displayItem($id); + } + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + + return parent::displayList(); + } + + /** + * Method to add a new record. + * + * @return void + * + * @since 4.0.0 + * @throws NotAllowed + * @throws \RuntimeException + */ + public function add() + { + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + + if (isset($data['menutype'])) { + $this->input->set('menutype', $data['menutype']); + $this->input->set('com_menus.items.menutype', $data['menutype']); + } + + isset($data['type']) && $this->input->set('type', $data['type']); + isset($data['parent_id']) && $this->input->set('parent_id', $data['parent_id']); + isset($data['link']) && $this->input->set('link', $data['link']); + + $this->input->set('id', '0'); + + parent::add(); + } + + /** + * Method to edit an existing record. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function edit() + { + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + + if (isset($data['menutype'])) { + $this->input->set('menutype', $data['menutype']); + $this->input->set('com_menus.items.menutype', $data['menutype']); + } + + isset($data['type']) && $this->input->set('type', $data['type']); + isset($data['parent_id']) && $this->input->set('parent_id', $data['parent_id']); + isset($data['link']) && $this->input->set('link', $data['link']); + + return parent::edit(); + } + + /** + * Return menu items types + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function getTypes() + { + $viewType = $this->app->getDocument()->getType(); + $viewName = $this->input->get('view', $this->default_view); + $viewLayout = $this->input->get('layout', 'default', 'string'); + + try { + /** @var JsonapiView $view */ + $view = $this->getView( + $viewName, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } + + /** @var ListModel $model */ + $model = $this->getModel('menutypes', '', ['ignore_request' => true]); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $model->setState('client_id', $this->getClientIdFromInput()); + + $view->setModel($model, true); + + $view->document = $this->app->getDocument(); + + $view->displayListTypes(); + + return $this; + } + + /** + * Get client id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getClientIdFromInput() + { + return $this->input->exists('client_id') ? + $this->input->get('client_id') : $this->input->post->get('client_id'); + } } diff --git a/api/components/com_menus/src/Controller/MenusController.php b/api/components/com_menus/src/Controller/MenusController.php index b7424ff70b22f..13b454043ef06 100644 --- a/api/components/com_menus/src/Controller/MenusController.php +++ b/api/components/com_menus/src/Controller/MenusController.php @@ -1,4 +1,5 @@ modelState->set('filter.client_id', $this->getClientIdFromInput()); + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); - return parent::displayItem($id); - } + return parent::displayItem($id); + } - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Get client id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getClientIdFromInput() - { - return $this->input->exists('client_id') ? - $this->input->get('client_id') : $this->input->post->get('client_id'); - } + /** + * Get client id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getClientIdFromInput() + { + return $this->input->exists('client_id') ? + $this->input->get('client_id') : $this->input->post->get('client_id'); + } } diff --git a/api/components/com_menus/src/View/Items/JsonapiView.php b/api/components/com_menus/src/View/Items/JsonapiView.php index 02b5b8d0920f8..7516379fb3319 100644 --- a/api/components/com_menus/src/View/Items/JsonapiView.php +++ b/api/components/com_menus/src/View/Items/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $items = []; - - foreach ($model->getTypeOptions() as $type => $data) - { - $groupItems = []; - - foreach ($data as $item) - { - $item->id = implode('/', $item->request); - $item->title = Text::_($item->title); - $item->description = Text::_($item->description); - $item->group = Text::_($type); - - $groupItems[] = $item; - } - - $items = array_merge($items, $groupItems); - } - - // Set up links for pagination - $currentUrl = Uri::getInstance(); - $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; - $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); - - $offset = $currentPageQuery['offset']; - $limit = $currentPageQuery['limit']; - $totalItemsCount = \count($items); - $totalPagesAvailable = ceil($totalItemsCount / $limit); - - $items = array_splice($items, $offset, $limit); - - $firstPage = clone $currentUrl; - $firstPageQuery = $currentPageQuery; - $firstPageQuery['offset'] = 0; - $firstPage->setVar('page', $firstPageQuery); - - $nextPage = clone $currentUrl; - $nextPageQuery = $currentPageQuery; - $nextOffset = $currentPageQuery['offset'] + $limit; - $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; - $nextPage->setVar('page', $nextPageQuery); - - $previousPage = clone $currentUrl; - $previousPageQuery = $currentPageQuery; - $previousOffset = $currentPageQuery['offset'] - $limit; - $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; - $previousPage->setVar('page', $previousPageQuery); - - $lastPage = clone $currentUrl; - $lastPageQuery = $currentPageQuery; - $lastPageQuery['offset'] = $totalPagesAvailable - $limit; - $lastPage->setVar('page', $lastPageQuery); - - $collection = (new Collection($items, new JoomlaSerializer('menutypes'))); - - // Set the data into the document and render it - $this->document->addMeta('total-pages', $totalPagesAvailable) - ->setData($collection) - ->addLink('self', (string) $currentUrl) - ->addLink('first', (string) $firstPage) - ->addLink('next', (string) $nextPage) - ->addLink('previous', (string) $previousPage) - ->addLink('last', (string) $lastPage); - - return $this->document->render(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - if (\is_string($item->params)) - { - $item->params = json_decode($item->params); - } - - return parent::prepareItem($item); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'parent_id', + 'level', + 'lft', + 'rgt', + 'alias', + 'typeAlias', + 'menutype', + 'title', + 'note', + 'path', + 'link', + 'type', + 'published', + 'component_id', + 'checked_out', + 'checked_out_time', + 'browserNav', + 'access', + 'img', + 'template_style_id', + 'params', + 'home', + 'language', + 'client_id', + 'publish_up', + 'publish_down', + 'request', + 'associations', + 'menuordering', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'menutype', + 'title', + 'alias', + 'note', + 'path', + 'link', + 'type', + 'parent_id', + 'level', + 'a.published', + 'component_id', + 'checked_out', + 'checked_out_time', + 'browserNav', + 'access', + 'img', + 'template_style_id', + 'params', + 'lft', + 'rgt', + 'home', + 'language', + 'client_id', + 'enabled', + 'publish_up', + 'publish_down', + 'published', + 'language_title', + 'language_image', + 'language_sef', + 'editor', + 'componentname', + 'access_level', + 'menutype_id', + 'menutype_title', + 'association', + 'name', + ]; + + /** + * Execute and display a list items types. + * + * @return string + * + * @since 4.0.0 + */ + public function displayListTypes() + { + /** @var \Joomla\Component\Menus\Administrator\Model\MenutypesModel $model */ + $model = $this->getModel(); + $items = []; + + foreach ($model->getTypeOptions() as $type => $data) { + $groupItems = []; + + foreach ($data as $item) { + $item->id = implode('/', $item->request); + $item->title = Text::_($item->title); + $item->description = Text::_($item->description); + $item->group = Text::_($type); + + $groupItems[] = $item; + } + + $items = array_merge($items, $groupItems); + } + + // Set up links for pagination + $currentUrl = Uri::getInstance(); + $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; + $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); + + $offset = $currentPageQuery['offset']; + $limit = $currentPageQuery['limit']; + $totalItemsCount = \count($items); + $totalPagesAvailable = ceil($totalItemsCount / $limit); + + $items = array_splice($items, $offset, $limit); + + $firstPage = clone $currentUrl; + $firstPageQuery = $currentPageQuery; + $firstPageQuery['offset'] = 0; + $firstPage->setVar('page', $firstPageQuery); + + $nextPage = clone $currentUrl; + $nextPageQuery = $currentPageQuery; + $nextOffset = $currentPageQuery['offset'] + $limit; + $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; + $nextPage->setVar('page', $nextPageQuery); + + $previousPage = clone $currentUrl; + $previousPageQuery = $currentPageQuery; + $previousOffset = $currentPageQuery['offset'] - $limit; + $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; + $previousPage->setVar('page', $previousPageQuery); + + $lastPage = clone $currentUrl; + $lastPageQuery = $currentPageQuery; + $lastPageQuery['offset'] = $totalPagesAvailable - $limit; + $lastPage->setVar('page', $lastPageQuery); + + $collection = (new Collection($items, new JoomlaSerializer('menutypes'))); + + // Set the data into the document and render it + $this->document->addMeta('total-pages', $totalPagesAvailable) + ->setData($collection) + ->addLink('self', (string) $currentUrl) + ->addLink('first', (string) $firstPage) + ->addLink('next', (string) $nextPage) + ->addLink('previous', (string) $previousPage) + ->addLink('last', (string) $lastPage); + + return $this->document->render(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + if (\is_string($item->params)) { + $item->params = json_decode($item->params); + } + + return parent::prepareItem($item); + } } diff --git a/api/components/com_menus/src/View/Menus/JsonapiView.php b/api/components/com_menus/src/View/Menus/JsonapiView.php index 0750e5a37febd..0fe01fc6ad6f2 100644 --- a/api/components/com_menus/src/View/Menus/JsonapiView.php +++ b/api/components/com_menus/src/View/Menus/JsonapiView.php @@ -1,4 +1,5 @@ id = $item->message_id; - unset($item->message_id); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->message_id; + unset($item->message_id); - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/api/components/com_modules/src/Controller/ModulesController.php b/api/components/com_modules/src/Controller/ModulesController.php index bb900e2bbb1dd..83c13b0fc1f15 100644 --- a/api/components/com_modules/src/Controller/ModulesController.php +++ b/api/components/com_modules/src/Controller/ModulesController.php @@ -1,4 +1,5 @@ modelState->set('filter.client_id', $this->getClientIdFromInput()); - - return parent::displayItem($id); - } - - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); - - return parent::displayList(); - } - - /** - * Return module items types - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function getTypes() - { - $viewType = $this->app->getDocument()->getType(); - $viewName = $this->input->get('view', $this->default_view); - $viewLayout = $this->input->get('layout', 'default', 'string'); - - try - { - /** @var JsonapiView $view */ - $view = $this->getView( - $viewName, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } - - /** @var SelectModel $model */ - $model = $this->getModel('select', '', ['ignore_request' => true]); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $model->setState('client_id', $this->getClientIdFromInput()); - - $view->setModel($model, true); - - $view->document = $this->app->getDocument(); - - $view->displayListTypes(); - - return $this; - } - - /** - * Get client id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getClientIdFromInput() - { - return $this->input->exists('client_id') ? - $this->input->get('client_id') : $this->input->post->get('client_id'); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'modules'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'modules'; + + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + + return parent::displayItem($id); + } + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + + return parent::displayList(); + } + + /** + * Return module items types + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function getTypes() + { + $viewType = $this->app->getDocument()->getType(); + $viewName = $this->input->get('view', $this->default_view); + $viewLayout = $this->input->get('layout', 'default', 'string'); + + try { + /** @var JsonapiView $view */ + $view = $this->getView( + $viewName, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } + + /** @var SelectModel $model */ + $model = $this->getModel('select', '', ['ignore_request' => true]); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $model->setState('client_id', $this->getClientIdFromInput()); + + $view->setModel($model, true); + + $view->document = $this->app->getDocument(); + + $view->displayListTypes(); + + return $this; + } + + /** + * Get client id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getClientIdFromInput() + { + return $this->input->exists('client_id') ? + $this->input->get('client_id') : $this->input->post->get('client_id'); + } } diff --git a/api/components/com_modules/src/View/Modules/JsonapiView.php b/api/components/com_modules/src/View/Modules/JsonapiView.php index 0cd3f3f65bc43..95a4235354c86 100644 --- a/api/components/com_modules/src/View/Modules/JsonapiView.php +++ b/api/components/com_modules/src/View/Modules/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - - if ($item === null) - { - $item = $this->prepareItem($model->getItem()); - } - - if ($item->id === null) - { - throw new RouteNotFoundException('Item does not exist'); - } - - if ((int) $model->getState('client_id') !== $item->client_id) - { - throw new RouteNotFoundException('Item does not exist'); - } - - return parent::displayItem($item); - } - - /** - * Execute and display a list modules types. - * - * @return string - * - * @since 4.0.0 - */ - public function displayListTypes() - { - /** @var SelectModel $model */ - $model = $this->getModel(); - $items = []; - - foreach ($model->getItems() as $item) - { - $item->id = $item->extension_id; - unset($item->extension_id); - - $items[] = $item; - } - - $this->fieldsToRenderList = ['id', 'name', 'module', 'xml', 'desc']; - - return parent::displayList($items); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'typeAlias', + 'asset_id', + 'title', + 'note', + 'content', + 'ordering', + 'position', + 'checked_out', + 'checked_out_time', + 'publish_up', + 'publish_down', + 'published', + 'module', + 'access', + 'showtitle', + 'params', + 'client_id', + 'language', + 'assigned', + 'assignment', + 'xml', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'title', + 'note', + 'position', + 'module', + 'language', + 'checked_out', + 'checked_out_time', + 'published', + 'enabled', + 'access', + 'ordering', + 'publish_up', + 'publish_down', + 'language_title', + 'language_image', + 'editor', + 'access_level', + 'pages', + 'name', + ]; + + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(); + + if ($item === null) { + $item = $this->prepareItem($model->getItem()); + } + + if ($item->id === null) { + throw new RouteNotFoundException('Item does not exist'); + } + + if ((int) $model->getState('client_id') !== $item->client_id) { + throw new RouteNotFoundException('Item does not exist'); + } + + return parent::displayItem($item); + } + + /** + * Execute and display a list modules types. + * + * @return string + * + * @since 4.0.0 + */ + public function displayListTypes() + { + /** @var SelectModel $model */ + $model = $this->getModel(); + $items = []; + + foreach ($model->getItems() as $item) { + $item->id = $item->extension_id; + unset($item->extension_id); + + $items[] = $item; + } + + $this->fieldsToRenderList = ['id', 'name', 'module', 'xml', 'desc']; + + return parent::displayList($items); + } } diff --git a/api/components/com_newsfeeds/src/Controller/FeedsController.php b/api/components/com_newsfeeds/src/Controller/FeedsController.php index 2612252576691..eaa3b9c24da83 100644 --- a/api/components/com_newsfeeds/src/Controller/FeedsController.php +++ b/api/components/com_newsfeeds/src/Controller/FeedsController.php @@ -1,4 +1,5 @@ type); - - foreach ($model->associations as $association) - { - $resources[] = (new Resource($association, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/newsfeeds/feeds/' . $association->id)); - } - - $collection = new Collection($resources, $serializer); - - return new Relationship($collection); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function category($model) - { - $serializer = new JoomlaSerializer('categories'); - - $resource = (new Resource($model->catid, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/newfeeds/categories/' . $model->catid)); - - return new Relationship($resource); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function createdBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->created_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); - - return new Relationship($resource); - } - - /** - * Build editor relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function modifiedBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->modified_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); - - return new Relationship($resource); - } + use TagApiSerializerTrait; + + /** + * Build content relationships by associations + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function languageAssociations($model) + { + $resources = []; + + // @todo: This can't be hardcoded in the future? + $serializer = new JoomlaSerializer($this->type); + + foreach ($model->associations as $association) { + $resources[] = (new Resource($association, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/newsfeeds/feeds/' . $association->id)); + } + + $collection = new Collection($resources, $serializer); + + return new Relationship($collection); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function category($model) + { + $serializer = new JoomlaSerializer('categories'); + + $resource = (new Resource($model->catid, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/newfeeds/categories/' . $model->catid)); + + return new Relationship($resource); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function createdBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->created_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); + + return new Relationship($resource); + } + + /** + * Build editor relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function modifiedBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->modified_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); + + return new Relationship($resource); + } } diff --git a/api/components/com_newsfeeds/src/View/Feeds/JsonapiView.php b/api/components/com_newsfeeds/src/View/Feeds/JsonapiView.php index 191b642735986..df323eb6a5596 100644 --- a/api/components/com_newsfeeds/src/View/Feeds/JsonapiView.php +++ b/api/components/com_newsfeeds/src/View/Feeds/JsonapiView.php @@ -1,4 +1,5 @@ serializer = new NewsfeedSerializer($config['contentType']); - } - - parent::__construct($config); - } - - /** - * Execute and display a template script. - * - * @param object $item Item - * - * @return string - * - * @since 4.0.0 - */ - public function displayItem($item = null) - { - if (Multilanguage::isEnabled()) - { - $this->fieldsToRenderItem[] = 'languageAssociations'; - $this->relationship[] = 'languageAssociations'; - } - - return parent::displayItem(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - if (Multilanguage::isEnabled() && !empty($item->associations)) - { - $associations = []; - - foreach ($item->associations as $language => $association) - { - $itemId = explode(':', $association)[0]; - - $associations[] = (object) [ - 'id' => $itemId, - 'language' => $language, - ]; - } - - $item->associations = $associations; - } - - if (!empty($item->tags->tags)) - { - $tagsIds = explode(',', $item->tags->tags); - $tagsNames = $item->tagsHelper->getTagNames($tagsIds); - - $item->tags = array_combine($tagsIds, $tagsNames); - } - else - { - $item->tags = []; - } - - return parent::prepareItem($item); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'category', + 'name', + 'alias', + 'link', + 'published', + 'numarticles', + 'cache_time', + 'checked_out', + 'checked_out_time', + 'ordering', + 'rtl', + 'access', + 'language', + 'params', + 'created', + 'created_by', + 'created_by_alias', + 'modified', + 'modified_by', + 'metakey', + 'metadesc', + 'metadata', + 'publish_up', + 'publish_down', + 'description', + 'version', + 'hits', + 'images', + 'tags', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'name', + 'alias', + 'checked_out', + 'checked_out_time', + 'category', + 'numarticles', + 'cache_time', + 'created_by', + 'published', + 'access', + 'ordering', + 'language', + 'publish_up', + 'publish_down', + 'language_title', + 'language_image', + 'editor', + 'access_level', + 'category_title', + ]; + + /** + * The relationships the item has + * + * @var array + * @since 4.0.0 + */ + protected $relationship = [ + 'category', + 'created_by', + 'modified_by', + 'tags', + ]; + + /** + * Constructor. + * + * @param array $config A named configuration array for object construction. + * contentType: the name (optional) of the content type to use for the serialization + * + * @since 4.0.0 + */ + public function __construct($config = []) + { + if (\array_key_exists('contentType', $config)) { + $this->serializer = new NewsfeedSerializer($config['contentType']); + } + + parent::__construct($config); + } + + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + if (Multilanguage::isEnabled()) { + $this->fieldsToRenderItem[] = 'languageAssociations'; + $this->relationship[] = 'languageAssociations'; + } + + return parent::displayItem(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + if (Multilanguage::isEnabled() && !empty($item->associations)) { + $associations = []; + + foreach ($item->associations as $language => $association) { + $itemId = explode(':', $association)[0]; + + $associations[] = (object) [ + 'id' => $itemId, + 'language' => $language, + ]; + } + + $item->associations = $associations; + } + + if (!empty($item->tags->tags)) { + $tagsIds = explode(',', $item->tags->tags); + $tagsNames = $item->tagsHelper->getTagNames($tagsIds); + + $item->tags = array_combine($tagsIds, $tagsNames); + } else { + $item->tags = []; + } + + return parent::prepareItem($item); + } } diff --git a/api/components/com_plugins/src/Controller/PluginsController.php b/api/components/com_plugins/src/Controller/PluginsController.php index 67bfb198175b0..d9cf70c263d5e 100644 --- a/api/components/com_plugins/src/Controller/PluginsController.php +++ b/api/components/com_plugins/src/Controller/PluginsController.php @@ -1,4 +1,5 @@ input->getInt('id'); - - if (!$recordId) - { - throw new Exception\ResourceNotFound(Text::_('JLIB_APPLICATION_ERROR_RECORD'), 404); - } - - $data = json_decode($this->input->json->getRaw(), true); - - foreach ($data as $key => $value) - { - if (!\in_array($key, ['enabled', 'access', 'ordering'])) - { - throw new InvalidParameterException("Invalid parameter {$key}.", 400); - } - } - - /** @var \Joomla\Component\Plugins\Administrator\Model\PluginModel $model */ - $model = $this->getModel(Inflector::singularize($this->contentType), '', ['ignore_request' => true]); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $item = $model->getItem($recordId); - - if (!isset($item->extension_id)) - { - throw new RouteNotFoundException('Item does not exist'); - } - - $data['folder'] = $item->folder; - $data['element'] = $item->element; - - $this->input->set('data', $data); - - return parent::edit(); - } - - /** - * Plugin list view with filtering of data - * - * @return static A BaseController object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $apiFilterInfo = $this->input->get('filter', [], 'array'); - $filter = InputFilter::getInstance(); - - if (\array_key_exists('element', $apiFilterInfo)) - { - $this->modelState->set('filter.element', $filter->clean($apiFilterInfo['element'], 'STRING')); - } - - if (\array_key_exists('status', $apiFilterInfo)) - { - $this->modelState->set('filter.enabled', $filter->clean($apiFilterInfo['status'], 'INT')); - } - - if (\array_key_exists('search', $apiFilterInfo)) - { - $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); - } - - if (\array_key_exists('type', $apiFilterInfo)) - { - $this->modelState->set('filter.folder', $filter->clean($apiFilterInfo['type'], 'STRING')); - } - - return parent::displayList(); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'plugins'; + + /** + * The default view for the display method. + * + * @var string + * + * @since 3.0 + */ + protected $default_view = 'plugins'; + + /** + * Method to edit an existing record. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function edit() + { + $recordId = $this->input->getInt('id'); + + if (!$recordId) { + throw new Exception\ResourceNotFound(Text::_('JLIB_APPLICATION_ERROR_RECORD'), 404); + } + + $data = json_decode($this->input->json->getRaw(), true); + + foreach ($data as $key => $value) { + if (!\in_array($key, ['enabled', 'access', 'ordering'])) { + throw new InvalidParameterException("Invalid parameter {$key}.", 400); + } + } + + /** @var \Joomla\Component\Plugins\Administrator\Model\PluginModel $model */ + $model = $this->getModel(Inflector::singularize($this->contentType), '', ['ignore_request' => true]); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $item = $model->getItem($recordId); + + if (!isset($item->extension_id)) { + throw new RouteNotFoundException('Item does not exist'); + } + + $data['folder'] = $item->folder; + $data['element'] = $item->element; + + $this->input->set('data', $data); + + return parent::edit(); + } + + /** + * Plugin list view with filtering of data + * + * @return static A BaseController object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $apiFilterInfo = $this->input->get('filter', [], 'array'); + $filter = InputFilter::getInstance(); + + if (\array_key_exists('element', $apiFilterInfo)) { + $this->modelState->set('filter.element', $filter->clean($apiFilterInfo['element'], 'STRING')); + } + + if (\array_key_exists('status', $apiFilterInfo)) { + $this->modelState->set('filter.enabled', $filter->clean($apiFilterInfo['status'], 'INT')); + } + + if (\array_key_exists('search', $apiFilterInfo)) { + $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); + } + + if (\array_key_exists('type', $apiFilterInfo)) { + $this->modelState->set('filter.folder', $filter->clean($apiFilterInfo['type'], 'STRING')); + } + + return parent::displayList(); + } } diff --git a/api/components/com_plugins/src/View/Plugins/JsonapiView.php b/api/components/com_plugins/src/View/Plugins/JsonapiView.php index c26d50b874951..0e947e0cd1426 100644 --- a/api/components/com_plugins/src/View/Plugins/JsonapiView.php +++ b/api/components/com_plugins/src/View/Plugins/JsonapiView.php @@ -1,4 +1,5 @@ id = $item->extension_id; - unset($item->extension_id); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->extension_id; + unset($item->extension_id); - return $item; - } + return $item; + } } diff --git a/api/components/com_privacy/src/Controller/ConsentsController.php b/api/components/com_privacy/src/Controller/ConsentsController.php index 66949d2da7a09..ae293fc8386a2 100644 --- a/api/components/com_privacy/src/Controller/ConsentsController.php +++ b/api/components/com_privacy/src/Controller/ConsentsController.php @@ -1,4 +1,5 @@ input->get('id', 0, 'int'); - } - - $this->input->set('model', $this->contentType); - - return parent::displayItem($id); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'consents'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'consents'; + + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + if ($id === null) { + $id = $this->input->get('id', 0, 'int'); + } + + $this->input->set('model', $this->contentType); + + return parent::displayItem($id); + } } diff --git a/api/components/com_privacy/src/Controller/RequestsController.php b/api/components/com_privacy/src/Controller/RequestsController.php index 772d1b0feeacd..ced1793808ecc 100644 --- a/api/components/com_privacy/src/Controller/RequestsController.php +++ b/api/components/com_privacy/src/Controller/RequestsController.php @@ -1,4 +1,5 @@ input->get('id', 0, 'int'); - } + /** + * Export request data + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function export($id = null) + { + if ($id === null) { + $id = $this->input->get('id', 0, 'int'); + } - $viewType = $this->app->getDocument()->getType(); - $viewName = $this->input->get('view', $this->default_view); - $viewLayout = $this->input->get('layout', 'default', 'string'); + $viewType = $this->app->getDocument()->getType(); + $viewName = $this->input->get('view', $this->default_view); + $viewLayout = $this->input->get('layout', 'default', 'string'); - try - { - /** @var JsonapiView $view */ - $view = $this->getView( - $viewName, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } + try { + /** @var JsonapiView $view */ + $view = $this->getView( + $viewName, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } - $model = $this->getModel('export'); + $model = $this->getModel('export'); - try - { - $modelName = $model->getName(); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } + try { + $modelName = $model->getName(); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } - $model->setState($modelName . '.request_id', $id); + $model->setState($modelName . '.request_id', $id); - $view->setModel($model, true); + $view->setModel($model, true); - $view->document = $this->app->getDocument(); - $view->export(); + $view->document = $this->app->getDocument(); + $view->export(); - return $this; - } + return $this; + } } diff --git a/api/components/com_privacy/src/View/Consents/JsonapiView.php b/api/components/com_privacy/src/View/Consents/JsonapiView.php index 60f8faddeb176..ca835622a1c45 100644 --- a/api/components/com_privacy/src/View/Consents/JsonapiView.php +++ b/api/components/com_privacy/src/View/Consents/JsonapiView.php @@ -1,4 +1,5 @@ get('state')->get($this->getName() . '.id'); - - if ($id === null) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ITEMID_MISSING')); - } - - /** @var \Joomla\CMS\MVC\Model\ListModel $model */ - $model = $this->getModel(); - $displayItem = null; - - foreach ($model->getItems() as $item) - { - $item = $this->prepareItem($item); - - if ($item->id === $id) - { - $displayItem = $item; - break; - } - } - - if ($displayItem === null) - { - throw new RouteNotFoundException('Item does not exist'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - if ($this->type === null) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CONTENT_TYPE_MISSING')); - } - - $serializer = new JoomlaSerializer($this->type); - $element = (new Resource($displayItem, $serializer)) - ->fields([$this->type => $this->fieldsToRenderItem]); - - $this->document->setData($element); - $this->document->addLink('self', Uri::current()); - - return $this->document->render(); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'user_id', + 'state', + 'created', + 'subject', + 'body', + 'remind', + 'token', + 'username', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'user_id', + 'state', + 'created', + 'subject', + 'body', + 'remind', + 'token', + 'username', + ]; + + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + $id = $this->get('state')->get($this->getName() . '.id'); + + if ($id === null) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ITEMID_MISSING')); + } + + /** @var \Joomla\CMS\MVC\Model\ListModel $model */ + $model = $this->getModel(); + $displayItem = null; + + foreach ($model->getItems() as $item) { + $item = $this->prepareItem($item); + + if ($item->id === $id) { + $displayItem = $item; + break; + } + } + + if ($displayItem === null) { + throw new RouteNotFoundException('Item does not exist'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + if ($this->type === null) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CONTENT_TYPE_MISSING')); + } + + $serializer = new JoomlaSerializer($this->type); + $element = (new Resource($displayItem, $serializer)) + ->fields([$this->type => $this->fieldsToRenderItem]); + + $this->document->setData($element); + $this->document->addLink('self', Uri::current()); + + return $this->document->render(); + } } diff --git a/api/components/com_privacy/src/View/Requests/JsonapiView.php b/api/components/com_privacy/src/View/Requests/JsonapiView.php index 65cdefc6bc8ea..506c5dcf460cb 100644 --- a/api/components/com_privacy/src/View/Requests/JsonapiView.php +++ b/api/components/com_privacy/src/View/Requests/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); + /** + * Execute and display a template script. + * + * @return string + * + * @since 4.0.0 + */ + public function export() + { + /** @var ExportModel $model */ + $model = $this->getModel(); - $exportData = $model->collectDataForExportRequest(); + $exportData = $model->collectDataForExportRequest(); - if ($exportData == false) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($exportData == false) { + throw new RouteNotFoundException('Item does not exist'); + } - $serializer = new JoomlaSerializer('export'); - $element = (new Resource($exportData, $serializer)); + $serializer = new JoomlaSerializer('export'); + $element = (new Resource($exportData, $serializer)); - $this->document->setData($element); - $this->document->addLink('self', Uri::current()); + $this->document->setData($element); + $this->document->addLink('self', Uri::current()); - return $this->document->render(); - } + return $this->document->render(); + } } diff --git a/api/components/com_redirect/src/Controller/RedirectController.php b/api/components/com_redirect/src/Controller/RedirectController.php index 58a1ba43b6ed1..7be23642f20ee 100644 --- a/api/components/com_redirect/src/Controller/RedirectController.php +++ b/api/components/com_redirect/src/Controller/RedirectController.php @@ -1,4 +1,5 @@ modelState->set('client_id', $this->getClientIdFromInput()); + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('client_id', $this->getClientIdFromInput()); - return parent::displayItem($id); - } + return parent::displayItem($id); + } - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('client_id', $this->getClientIdFromInput()); + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('client_id', $this->getClientIdFromInput()); - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Method to allow extended classes to manipulate the data to be saved for an extension. - * - * @param array $data An array of input data. - * - * @return array - * - * @since 4.0.0 - * @throws InvalidParameterException - */ - protected function preprocessSaveData(array $data): array - { - $data['client_id'] = $this->getClientIdFromInput(); + /** + * Method to allow extended classes to manipulate the data to be saved for an extension. + * + * @param array $data An array of input data. + * + * @return array + * + * @since 4.0.0 + * @throws InvalidParameterException + */ + protected function preprocessSaveData(array $data): array + { + $data['client_id'] = $this->getClientIdFromInput(); - // If we are updating an item the template is a readonly property based on the ID - if ($this->input->getMethod() === 'PATCH') - { - if (\array_key_exists('template', $data)) - { - throw new InvalidParameterException('The template property cannot be modified for an existing style'); - } + // If we are updating an item the template is a readonly property based on the ID + if ($this->input->getMethod() === 'PATCH') { + if (\array_key_exists('template', $data)) { + throw new InvalidParameterException('The template property cannot be modified for an existing style'); + } - $model = $this->getModel(Inflector::singularize($this->contentType), '', ['ignore_request' => true]); - $data['template'] = $model->getItem($this->input->getInt('id'))->template; - } + $model = $this->getModel(Inflector::singularize($this->contentType), '', ['ignore_request' => true]); + $data['template'] = $model->getItem($this->input->getInt('id'))->template; + } - return $data; - } + return $data; + } - /** - * Get client id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getClientIdFromInput() - { - return $this->input->exists('client_id') ? $this->input->get('client_id') : $this->input->post->get('client_id'); - } + /** + * Get client id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getClientIdFromInput() + { + return $this->input->exists('client_id') ? $this->input->get('client_id') : $this->input->post->get('client_id'); + } } diff --git a/api/components/com_templates/src/View/Styles/JsonapiView.php b/api/components/com_templates/src/View/Styles/JsonapiView.php index 880716be6f618..d4b56c7124e1e 100644 --- a/api/components/com_templates/src/View/Styles/JsonapiView.php +++ b/api/components/com_templates/src/View/Styles/JsonapiView.php @@ -1,4 +1,5 @@ client_id != $this->getModel()->getState('client_id')) - { - throw new RouteNotFoundException('Item does not exist'); - } + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + if ($item->client_id != $this->getModel()->getState('client_id')) { + throw new RouteNotFoundException('Item does not exist'); + } - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/api/components/com_users/src/Controller/GroupsController.php b/api/components/com_users/src/Controller/GroupsController.php index 8c49ef4b9cf73..b5740e3a4e529 100644 --- a/api/components/com_users/src/Controller/GroupsController.php +++ b/api/components/com_users/src/Controller/GroupsController.php @@ -1,4 +1,5 @@ name])) - { - !isset($data['com_fields']) && $data['com_fields'] = []; - - $data['com_fields'][$field->name] = $data[$field->name]; - unset($data[$field->name]); - } - } - - if (isset($data['password']) && $this->task !== 'add') - { - $data['password2'] = $data['password']; - } - - return $data; - } - - /** - * User list view with filtering of data - * - * @return static A BaseController object to support chaining. - * - * @since 4.0.0 - * @throws InvalidParameterException - */ - public function displayList() - { - $apiFilterInfo = $this->input->get('filter', [], 'array'); - $filter = InputFilter::getInstance(); - - if (\array_key_exists('state', $apiFilterInfo)) - { - $this->modelState->set('filter.state', $filter->clean($apiFilterInfo['state'], 'INT')); - } - - if (\array_key_exists('active', $apiFilterInfo)) - { - $this->modelState->set('filter.active', $filter->clean($apiFilterInfo['active'], 'INT')); - } - - if (\array_key_exists('groupid', $apiFilterInfo)) - { - $this->modelState->set('filter.group_id', $filter->clean($apiFilterInfo['groupid'], 'INT')); - } - - if (\array_key_exists('search', $apiFilterInfo)) - { - $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); - } - - if (\array_key_exists('registrationDateStart', $apiFilterInfo)) - { - $registrationStartInput = $filter->clean($apiFilterInfo['registrationDateStart'], 'STRING'); - $registrationStartDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $registrationStartInput); - - if (!$registrationStartDate) - { - // Send the error response - $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'registrationDateStart'); - - throw new InvalidParameterException($error, 400, null, 'registrationDateStart'); - } - - $this->modelState->set('filter.registrationDateStart', $registrationStartDate); - } - - if (\array_key_exists('registrationDateEnd', $apiFilterInfo)) - { - $registrationEndInput = $filter->clean($apiFilterInfo['registrationDateEnd'], 'STRING'); - $registrationEndDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $registrationEndInput); - - if (!$registrationEndDate) - { - // Send the error response - $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'registrationDateEnd'); - throw new InvalidParameterException($error, 400, null, 'registrationDateEnd'); - } - - $this->modelState->set('filter.registrationDateEnd', $registrationEndDate); - } - elseif (\array_key_exists('registrationDateStart', $apiFilterInfo) - && !\array_key_exists('registrationDateEnd', $apiFilterInfo)) - { - // If no end date specified the end date is now - $this->modelState->set('filter.registrationDateEnd', new Date); - } - - if (\array_key_exists('lastVisitDateStart', $apiFilterInfo)) - { - $lastVisitStartInput = $filter->clean($apiFilterInfo['lastVisitDateStart'], 'STRING'); - $lastVisitStartDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $lastVisitStartInput); - - if (!$lastVisitStartDate) - { - // Send the error response - $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'lastVisitDateStart'); - throw new InvalidParameterException($error, 400, null, 'lastVisitDateStart'); - } - - $this->modelState->set('filter.lastVisitStart', $lastVisitStartDate); - } - - if (\array_key_exists('lastVisitDateEnd', $apiFilterInfo)) - { - $lastVisitEndInput = $filter->clean($apiFilterInfo['lastVisitDateEnd'], 'STRING'); - $lastVisitEndDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $lastVisitEndInput); - - if (!$lastVisitEndDate) - { - // Send the error response - $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'lastVisitDateEnd'); - - throw new InvalidParameterException($error, 400, null, 'lastVisitDateEnd'); - } - - $this->modelState->set('filter.lastVisitEnd', $lastVisitEndDate); - } - elseif (\array_key_exists('lastVisitDateStart', $apiFilterInfo) - && !\array_key_exists('lastVisitDateEnd', $apiFilterInfo)) - { - // If no end date specified the end date is now - $this->modelState->set('filter.lastVisitEnd', new Date); - } - - return parent::displayList(); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'users'; + + /** + * The default view for the display method. + * + * @var string + * @since 4.0.0 + */ + protected $default_view = 'users'; + + /** + * Method to allow extended classes to manipulate the data to be saved for an extension. + * + * @param array $data An array of input data. + * + * @return array + * + * @since 4.0.0 + */ + protected function preprocessSaveData(array $data): array + { + foreach (FieldsHelper::getFields('com_users.user') as $field) { + if (isset($data[$field->name])) { + !isset($data['com_fields']) && $data['com_fields'] = []; + + $data['com_fields'][$field->name] = $data[$field->name]; + unset($data[$field->name]); + } + } + + if (isset($data['password']) && $this->task !== 'add') { + $data['password2'] = $data['password']; + } + + return $data; + } + + /** + * User list view with filtering of data + * + * @return static A BaseController object to support chaining. + * + * @since 4.0.0 + * @throws InvalidParameterException + */ + public function displayList() + { + $apiFilterInfo = $this->input->get('filter', [], 'array'); + $filter = InputFilter::getInstance(); + + if (\array_key_exists('state', $apiFilterInfo)) { + $this->modelState->set('filter.state', $filter->clean($apiFilterInfo['state'], 'INT')); + } + + if (\array_key_exists('active', $apiFilterInfo)) { + $this->modelState->set('filter.active', $filter->clean($apiFilterInfo['active'], 'INT')); + } + + if (\array_key_exists('groupid', $apiFilterInfo)) { + $this->modelState->set('filter.group_id', $filter->clean($apiFilterInfo['groupid'], 'INT')); + } + + if (\array_key_exists('search', $apiFilterInfo)) { + $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); + } + + if (\array_key_exists('registrationDateStart', $apiFilterInfo)) { + $registrationStartInput = $filter->clean($apiFilterInfo['registrationDateStart'], 'STRING'); + $registrationStartDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $registrationStartInput); + + if (!$registrationStartDate) { + // Send the error response + $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'registrationDateStart'); + + throw new InvalidParameterException($error, 400, null, 'registrationDateStart'); + } + + $this->modelState->set('filter.registrationDateStart', $registrationStartDate); + } + + if (\array_key_exists('registrationDateEnd', $apiFilterInfo)) { + $registrationEndInput = $filter->clean($apiFilterInfo['registrationDateEnd'], 'STRING'); + $registrationEndDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $registrationEndInput); + + if (!$registrationEndDate) { + // Send the error response + $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'registrationDateEnd'); + throw new InvalidParameterException($error, 400, null, 'registrationDateEnd'); + } + + $this->modelState->set('filter.registrationDateEnd', $registrationEndDate); + } elseif ( + \array_key_exists('registrationDateStart', $apiFilterInfo) + && !\array_key_exists('registrationDateEnd', $apiFilterInfo) + ) { + // If no end date specified the end date is now + $this->modelState->set('filter.registrationDateEnd', new Date()); + } + + if (\array_key_exists('lastVisitDateStart', $apiFilterInfo)) { + $lastVisitStartInput = $filter->clean($apiFilterInfo['lastVisitDateStart'], 'STRING'); + $lastVisitStartDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $lastVisitStartInput); + + if (!$lastVisitStartDate) { + // Send the error response + $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'lastVisitDateStart'); + throw new InvalidParameterException($error, 400, null, 'lastVisitDateStart'); + } + + $this->modelState->set('filter.lastVisitStart', $lastVisitStartDate); + } + + if (\array_key_exists('lastVisitDateEnd', $apiFilterInfo)) { + $lastVisitEndInput = $filter->clean($apiFilterInfo['lastVisitDateEnd'], 'STRING'); + $lastVisitEndDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $lastVisitEndInput); + + if (!$lastVisitEndDate) { + // Send the error response + $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'lastVisitDateEnd'); + + throw new InvalidParameterException($error, 400, null, 'lastVisitDateEnd'); + } + + $this->modelState->set('filter.lastVisitEnd', $lastVisitEndDate); + } elseif ( + \array_key_exists('lastVisitDateStart', $apiFilterInfo) + && !\array_key_exists('lastVisitDateEnd', $apiFilterInfo) + ) { + // If no end date specified the end date is now + $this->modelState->set('filter.lastVisitEnd', new Date()); + } + + return parent::displayList(); + } } diff --git a/api/components/com_users/src/View/Groups/JsonapiView.php b/api/components/com_users/src/View/Groups/JsonapiView.php index 34e70606616d0..7740066498a22 100644 --- a/api/components/com_users/src/View/Groups/JsonapiView.php +++ b/api/components/com_users/src/View/Groups/JsonapiView.php @@ -1,4 +1,5 @@ fieldsToRenderList[] = $field->name; - } + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + foreach (FieldsHelper::getFields('com_users.user') as $field) { + $this->fieldsToRenderList[] = $field->name; + } - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Execute and display a template script. - * - * @param object $item Item - * - * @return string - * - * @since 4.0.0 - */ - public function displayItem($item = null) - { - foreach (FieldsHelper::getFields('com_users.user') as $field) - { - $this->fieldsToRenderItem[] = $field->name; - } + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + foreach (FieldsHelper::getFields('com_users.user') as $field) { + $this->fieldsToRenderItem[] = $field->name; + } - return parent::displayItem(); - } + return parent::displayItem(); + } - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - if (empty($item->username)) - { - throw new RouteNotFoundException('Item does not exist'); - } + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + if (empty($item->username)) { + throw new RouteNotFoundException('Item does not exist'); + } - foreach (FieldsHelper::getFields('com_users.user', $item, true) as $field) - { - $item->{$field->name} = $field->apivalue ?? $field->rawvalue; - } + foreach (FieldsHelper::getFields('com_users.user', $item, true) as $field) { + $item->{$field->name} = $field->apivalue ?? $field->rawvalue; + } - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/api/includes/app.php b/api/includes/app.php index 50c934cd6c80c..9f10825f9f10b 100644 --- a/api/includes/app.php +++ b/api/includes/app.php @@ -1,4 +1,5 @@ alias('session', 'session.cli') - ->alias('JSession', 'session.cli') - ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); + ->alias('JSession', 'session.cli') + ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); // Instantiate the application. $app = $container->get(\Joomla\CMS\Application\ApiApplication::class); diff --git a/api/includes/defines.php b/api/includes/defines.php index 63922bd809819..6148b98c0969d 100644 --- a/api/includes/defines.php +++ b/api/includes/defines.php @@ -1,4 +1,5 @@ isInDevelopmentState()))) -{ - if (file_exists(JPATH_INSTALLATION . '/index.php')) - { - header('HTTP/1.1 500 Internal Server Error'); - echo json_encode( - array('error' => 'You must install Joomla to use the API') - ); - - exit(); - } - else - { - header('HTTP/1.1 500 Internal Server Error'); - echo json_encode( - array('error' => 'No configuration file found and no installation code available. Exiting...') - ); - - exit; - } +if ( + !file_exists(JPATH_CONFIGURATION . '/configuration.php') + || (filesize(JPATH_CONFIGURATION . '/configuration.php') < 10) + || (file_exists(JPATH_INSTALLATION . '/index.php') && (false === (new Version())->isInDevelopmentState())) +) { + if (file_exists(JPATH_INSTALLATION . '/index.php')) { + header('HTTP/1.1 500 Internal Server Error'); + echo json_encode( + array('error' => 'You must install Joomla to use the API') + ); + + exit(); + } else { + header('HTTP/1.1 500 Internal Server Error'); + echo json_encode( + array('error' => 'No configuration file found and no installation code available. Exiting...') + ); + + exit; + } } // Pre-Load configuration. Don't remove the Output Buffering due to BOM issues, see JCode 26026 @@ -45,64 +44,59 @@ ob_end_clean(); // System configuration. -$config = new JConfig; +$config = new JConfig(); // Set the error_reporting -switch ($config->error_reporting) -{ - case 'default': - case '-1': - break; +switch ($config->error_reporting) { + case 'default': + case '-1': + break; - case 'none': - case '0': - error_reporting(0); + case 'none': + case '0': + error_reporting(0); - break; + break; - case 'simple': - error_reporting(E_ERROR | E_WARNING | E_PARSE); - ini_set('display_errors', 1); + case 'simple': + error_reporting(E_ERROR | E_WARNING | E_PARSE); + ini_set('display_errors', 1); - break; + break; - case 'maximum': - case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 - error_reporting(E_ALL); - ini_set('display_errors', 1); + case 'maximum': + case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 + error_reporting(E_ALL); + ini_set('display_errors', 1); - break; + break; - default: - error_reporting($config->error_reporting); - ini_set('display_errors', 1); + default: + error_reporting($config->error_reporting); + ini_set('display_errors', 1); - break; + break; } define('JDEBUG', $config->debug); // Check deprecation logging -if (empty($config->log_deprecated)) -{ - // Reset handler for E_USER_DEPRECATED - set_error_handler(null, E_USER_DEPRECATED); -} -else -{ - // Make sure handler for E_USER_DEPRECATED is registered - set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); +if (empty($config->log_deprecated)) { + // Reset handler for E_USER_DEPRECATED + set_error_handler(null, E_USER_DEPRECATED); +} else { + // Make sure handler for E_USER_DEPRECATED is registered + set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); } -if (JDEBUG || $config->error_reporting === 'maximum') -{ - // Set new Exception handler with debug enabled - $errorHandler->setExceptionHandler( - [ - new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), - 'renderException', - ] - ); +if (JDEBUG || $config->error_reporting === 'maximum') { + // Set new Exception handler with debug enabled + $errorHandler->setExceptionHandler( + [ + new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), + 'renderException', + ] + ); } /** @@ -111,15 +105,12 @@ * We need to do this as high up the stack as we can, as the default in \Joomla\Utilities\IpHelper is to * $allowIpOverride = true which is the wrong default for a generic site NOT behind a trusted proxy/load balancer. */ -if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) -{ - // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR - IpHelper::setAllowIpOverrides(true); -} -else -{ - // We disable the allowing of IP overriding using headers by default. - IpHelper::setAllowIpOverrides(false); +if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) { + // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR + IpHelper::setAllowIpOverrides(true); +} else { + // We disable the allowing of IP overriding using headers by default. + IpHelper::setAllowIpOverrides(false); } unset($config); diff --git a/api/index.php b/api/index.php index 0f65a63313ebd..08dac50c4a97b 100644 --- a/api/index.php +++ b/api/index.php @@ -1,4 +1,5 @@ sprintf('Joomla requires PHP version %s to run', JOOMLA_MINIMUM_PHP)) - ); +if (version_compare(PHP_VERSION, JOOMLA_MINIMUM_PHP, '<')) { + header('HTTP/1.1 500 Internal Server Error'); + echo json_encode( + array('error' => sprintf('Joomla requires PHP version %s to run', JOOMLA_MINIMUM_PHP)) + ); - return; + return; } /** diff --git a/cli/joomla.php b/cli/joomla.php index fb078e1265d01..6b5bea7d05b57 100644 --- a/cli/joomla.php +++ b/cli/joomla.php @@ -1,4 +1,5 @@ alias('session', 'session.cli') - ->alias('JSession', 'session.cli') - ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); + ->alias('JSession', 'session.cli') + ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); $app = \Joomla\CMS\Factory::getContainer()->get(\Joomla\Console\Application::class); \Joomla\CMS\Factory::$application = $app; diff --git a/components/com_ajax/ajax.php b/components/com_ajax/ajax.php index 1b8fcc87b5718..1aa3212fd321f 100644 --- a/components/com_ajax/ajax.php +++ b/components/com_ajax/ajax.php @@ -1,4 +1,5 @@ get('module')) -{ - $module = $input->get('module'); - $table = Table::getInstance('extension'); - $moduleId = $table->find(array('type' => 'module', 'element' => 'mod_' . $module)); - - if ($moduleId && $table->load($moduleId) && $table->enabled) - { - $helperFile = JPATH_BASE . '/modules/mod_' . $module . '/helper.php'; - - if (strpos($module, '_')) - { - $parts = explode('_', $module); - } - elseif (strpos($module, '-')) - { - $parts = explode('-', $module); - } - - if ($parts) - { - $class = 'Mod'; - - foreach ($parts as $part) - { - $class .= ucfirst($part); - } - - $class .= 'Helper'; - } - else - { - $class = 'Mod' . ucfirst($module) . 'Helper'; - } - - $method = $input->get('method') ?: 'get'; - - $moduleInstance = $app->bootModule('mod_' . $module, $app->getName()); - - if ($moduleInstance instanceof \Joomla\CMS\Helper\HelperFactoryInterface && $helper = $moduleInstance->getHelper(substr($class, 3))) - { - $results = method_exists($helper, $method . 'Ajax') ? $helper->{$method . 'Ajax'}() : null; - } - - if ($results === null && is_file($helperFile)) - { - JLoader::register($class, $helperFile); - - if (method_exists($class, $method . 'Ajax')) - { - // Load language file for module - $basePath = JPATH_BASE; - $lang = Factory::getLanguage(); - $lang->load('mod_' . $module, $basePath) - || $lang->load('mod_' . $module, $basePath . '/modules/mod_' . $module); - - try - { - $results = call_user_func($class . '::' . $method . 'Ajax'); - } - catch (Exception $e) - { - $results = $e; - } - } - // Method does not exist - else - { - $results = new LogicException(Text::sprintf('COM_AJAX_METHOD_NOT_EXISTS', $method . 'Ajax'), 404); - } - } - // The helper file does not exist - elseif ($results === null) - { - $results = new RuntimeException(Text::sprintf('COM_AJAX_FILE_NOT_EXISTS', 'mod_' . $module . '/helper.php'), 404); - } - } - // Module is not published, you do not have access to it, or it is not assigned to the current menu item - else - { - $results = new LogicException(Text::sprintf('COM_AJAX_MODULE_NOT_ACCESSIBLE', 'mod_' . $module), 404); - } +elseif ($input->get('module')) { + $module = $input->get('module'); + $table = Table::getInstance('extension'); + $moduleId = $table->find(array('type' => 'module', 'element' => 'mod_' . $module)); + + if ($moduleId && $table->load($moduleId) && $table->enabled) { + $helperFile = JPATH_BASE . '/modules/mod_' . $module . '/helper.php'; + + if (strpos($module, '_')) { + $parts = explode('_', $module); + } elseif (strpos($module, '-')) { + $parts = explode('-', $module); + } + + if ($parts) { + $class = 'Mod'; + + foreach ($parts as $part) { + $class .= ucfirst($part); + } + + $class .= 'Helper'; + } else { + $class = 'Mod' . ucfirst($module) . 'Helper'; + } + + $method = $input->get('method') ?: 'get'; + + $moduleInstance = $app->bootModule('mod_' . $module, $app->getName()); + + if ($moduleInstance instanceof \Joomla\CMS\Helper\HelperFactoryInterface && $helper = $moduleInstance->getHelper(substr($class, 3))) { + $results = method_exists($helper, $method . 'Ajax') ? $helper->{$method . 'Ajax'}() : null; + } + + if ($results === null && is_file($helperFile)) { + JLoader::register($class, $helperFile); + + if (method_exists($class, $method . 'Ajax')) { + // Load language file for module + $basePath = JPATH_BASE; + $lang = Factory::getLanguage(); + $lang->load('mod_' . $module, $basePath) + || $lang->load('mod_' . $module, $basePath . '/modules/mod_' . $module); + + try { + $results = call_user_func($class . '::' . $method . 'Ajax'); + } catch (Exception $e) { + $results = $e; + } + } + // Method does not exist + else { + $results = new LogicException(Text::sprintf('COM_AJAX_METHOD_NOT_EXISTS', $method . 'Ajax'), 404); + } + } + // The helper file does not exist + elseif ($results === null) { + $results = new RuntimeException(Text::sprintf('COM_AJAX_FILE_NOT_EXISTS', 'mod_' . $module . '/helper.php'), 404); + } + } + // Module is not published, you do not have access to it, or it is not assigned to the current menu item + else { + $results = new LogicException(Text::sprintf('COM_AJAX_MODULE_NOT_ACCESSIBLE', 'mod_' . $module), 404); + } } /* * Plugin support by default is based on the "Ajax" plugin group. @@ -147,20 +129,16 @@ * (i.e. index.php?option=com_ajax&plugin=foo) * */ -elseif ($input->get('plugin')) -{ - $group = $input->get('group', 'ajax'); - PluginHelper::importPlugin($group); - $plugin = ucfirst($input->get('plugin')); - - try - { - $results = Factory::getApplication()->triggerEvent('onAjax' . $plugin); - } - catch (Exception $e) - { - $results = $e; - } +elseif ($input->get('plugin')) { + $group = $input->get('group', 'ajax'); + PluginHelper::importPlugin($group); + $plugin = ucfirst($input->get('plugin')); + + try { + $results = Factory::getApplication()->triggerEvent('onAjax' . $plugin); + } catch (Exception $e) { + $results = $e; + } } /* * Template support. @@ -170,118 +148,97 @@ * (i.e. index.php?option=com_ajax&template=foo). * */ -elseif ($input->get('template')) -{ - $template = $input->get('template'); - $table = Table::getInstance('extension'); - $templateId = $table->find(array('type' => 'template', 'element' => $template)); - - if ($templateId && $table->load($templateId) && $table->enabled) - { - $basePath = ($table->client_id) ? JPATH_ADMINISTRATOR : JPATH_SITE; - $helperFile = $basePath . '/templates/' . $template . '/helper.php'; - - if (strpos($template, '_')) - { - $parts = explode('_', $template); - } - elseif (strpos($template, '-')) - { - $parts = explode('-', $template); - } - - if ($parts) - { - $class = 'Tpl'; - - foreach ($parts as $part) - { - $class .= ucfirst($part); - } - - $class .= 'Helper'; - } - else - { - $class = 'Tpl' . ucfirst($template) . 'Helper'; - } - - $method = $input->get('method') ?: 'get'; - - if (is_file($helperFile)) - { - JLoader::register($class, $helperFile); - - if (method_exists($class, $method . 'Ajax')) - { - // Load language file for template - $lang = Factory::getLanguage(); - $lang->load('tpl_' . $template, $basePath) - || $lang->load('tpl_' . $template, $basePath . '/templates/' . $template); - - try - { - $results = call_user_func($class . '::' . $method . 'Ajax'); - } - catch (Exception $e) - { - $results = $e; - } - } - // Method does not exist - else - { - $results = new LogicException(Text::sprintf('COM_AJAX_METHOD_NOT_EXISTS', $method . 'Ajax'), 404); - } - } - // The helper file does not exist - else - { - $results = new RuntimeException(Text::sprintf('COM_AJAX_FILE_NOT_EXISTS', 'tpl_' . $template . '/helper.php'), 404); - } - } - // Template is not assigned to the current menu item - else - { - $results = new LogicException(Text::sprintf('COM_AJAX_TEMPLATE_NOT_ACCESSIBLE', 'tpl_' . $template), 404); - } +elseif ($input->get('template')) { + $template = $input->get('template'); + $table = Table::getInstance('extension'); + $templateId = $table->find(array('type' => 'template', 'element' => $template)); + + if ($templateId && $table->load($templateId) && $table->enabled) { + $basePath = ($table->client_id) ? JPATH_ADMINISTRATOR : JPATH_SITE; + $helperFile = $basePath . '/templates/' . $template . '/helper.php'; + + if (strpos($template, '_')) { + $parts = explode('_', $template); + } elseif (strpos($template, '-')) { + $parts = explode('-', $template); + } + + if ($parts) { + $class = 'Tpl'; + + foreach ($parts as $part) { + $class .= ucfirst($part); + } + + $class .= 'Helper'; + } else { + $class = 'Tpl' . ucfirst($template) . 'Helper'; + } + + $method = $input->get('method') ?: 'get'; + + if (is_file($helperFile)) { + JLoader::register($class, $helperFile); + + if (method_exists($class, $method . 'Ajax')) { + // Load language file for template + $lang = Factory::getLanguage(); + $lang->load('tpl_' . $template, $basePath) + || $lang->load('tpl_' . $template, $basePath . '/templates/' . $template); + + try { + $results = call_user_func($class . '::' . $method . 'Ajax'); + } catch (Exception $e) { + $results = $e; + } + } + // Method does not exist + else { + $results = new LogicException(Text::sprintf('COM_AJAX_METHOD_NOT_EXISTS', $method . 'Ajax'), 404); + } + } + // The helper file does not exist + else { + $results = new RuntimeException(Text::sprintf('COM_AJAX_FILE_NOT_EXISTS', 'tpl_' . $template . '/helper.php'), 404); + } + } + // Template is not assigned to the current menu item + else { + $results = new LogicException(Text::sprintf('COM_AJAX_TEMPLATE_NOT_ACCESSIBLE', 'tpl_' . $template), 404); + } } // Return the results in the desired format -switch ($format) -{ - // JSONinzed - case 'json': - echo new JsonResponse($results, null, false, $input->get('ignoreMessages', true, 'bool')); - - break; - - // Handle as raw format - default: - // Output exception - if ($results instanceof Exception) - { - // Log an error - Log::add($results->getMessage(), Log::ERROR); - - // Set status header code - $app->setHeader('status', $results->getCode(), true); - - // Echo exception type and message - $out = get_class($results) . ': ' . $results->getMessage(); - } - // Output string/ null - elseif (is_scalar($results)) - { - $out = (string) $results; - } - // Output array/ object - else - { - $out = implode((array) $results); - } - - echo $out; - - break; +switch ($format) { + // JSONinzed + case 'json': + echo new JsonResponse($results, null, false, $input->get('ignoreMessages', true, 'bool')); + + break; + + // Handle as raw format + default: + // Output exception + if ($results instanceof Exception) { + // Log an error + Log::add($results->getMessage(), Log::ERROR); + + // Set status header code + $app->setHeader('status', $results->getCode(), true); + + // Echo exception type and message + $out = get_class($results) . ': ' . $results->getMessage(); + } + // Output string/ null + elseif (is_scalar($results)) { + $out = (string) $results; + } + // Output array/ object + else { + $out = implode((array) $results); + } + + echo $out; + + break; } diff --git a/components/com_banners/src/Controller/DisplayController.php b/components/com_banners/src/Controller/DisplayController.php index 75b3f4f667b9e..1e8d4681038dd 100644 --- a/components/com_banners/src/Controller/DisplayController.php +++ b/components/com_banners/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->getInt('id', 0); + /** + * Method when a banner is clicked on. + * + * @return void + * + * @since 1.5 + */ + public function click() + { + $id = $this->input->getInt('id', 0); - if ($id) - { - /** @var \Joomla\Component\Banners\Site\Model\BannerModel $model */ - $model = $this->getModel('Banner', 'Site', array('ignore_request' => true)); - $model->setState('banner.id', $id); - $model->click(); - $this->setRedirect($model->getUrl()); - } - } + if ($id) { + /** @var \Joomla\Component\Banners\Site\Model\BannerModel $model */ + $model = $this->getModel('Banner', 'Site', array('ignore_request' => true)); + $model->setState('banner.id', $id); + $model->click(); + $this->setRedirect($model->getUrl()); + } + } } diff --git a/components/com_banners/src/Helper/BannerHelper.php b/components/com_banners/src/Helper/BannerHelper.php index 3476a5841c758..53b0a8f639eb2 100644 --- a/components/com_banners/src/Helper/BannerHelper.php +++ b/components/com_banners/src/Helper/BannerHelper.php @@ -1,4 +1,5 @@ getItem(); - - if (empty($item)) - { - throw new \Exception(Text::_('JERROR_PAGE_NOT_FOUND'), 404); - } - - $id = (int) $this->getState('banner.id'); - - // Update click count - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->update($db->quoteName('#__banners')) - ->set($db->quoteName('clicks') . ' = ' . $db->quoteName('clicks') . ' + 1') - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500); - } - - // Track clicks - $trackClicks = $item->track_clicks; - - if ($trackClicks < 0 && $item->cid) - { - $trackClicks = $item->client_track_clicks; - } - - if ($trackClicks < 0) - { - $config = ComponentHelper::getParams('com_banners'); - $trackClicks = $config->get('track_clicks'); - } - - if ($trackClicks > 0) - { - $trackDate = Factory::getDate()->format('Y-m-d H:00:00'); - $trackDate = Factory::getDate($trackDate)->toSql(); - - $query = $db->getQuery(true); - - $query->select($db->quoteName('count')) - ->from($db->quoteName('#__banner_tracks')) - ->where( - [ - $db->quoteName('track_type') . ' = 2', - $db->quoteName('banner_id') . ' = :id', - $db->quoteName('track_date') . ' = :trackDate', - ] - ) - ->bind(':id', $id, ParameterType::INTEGER) - ->bind(':trackDate', $trackDate); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500); - } - - $count = $db->loadResult(); - - $query = $db->getQuery(true); - - if ($count) - { - // Update count - $query->update($db->quoteName('#__banner_tracks')) - ->set($db->quoteName('count') . ' = ' . $db->quoteName('count') . ' + 1') - ->where( - [ - $db->quoteName('track_type') . ' = 2', - $db->quoteName('banner_id') . ' = :id', - $db->quoteName('track_date') . ' = :trackDate', - ] - ) - ->bind(':id', $id, ParameterType::INTEGER) - ->bind(':trackDate', $trackDate); - } - else - { - // Insert new count - $query->insert($db->quoteName('#__banner_tracks')) - ->columns( - [ - $db->quoteName('count'), - $db->quoteName('track_type'), - $db->quoteName('banner_id'), - $db->quoteName('track_date'), - ] - ) - ->values('1, 2 , :id, :trackDate') - ->bind(':id', $id, ParameterType::INTEGER) - ->bind(':trackDate', $trackDate); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500); - } - } - } - - /** - * Get the data for a banner. - * - * @return object - * - * @since 1.6 - */ - public function &getItem() - { - if (!isset($this->_item)) - { - /** @var \Joomla\CMS\Cache\Controller\CallbackController $cache */ - $cache = Factory::getCache('com_banners', 'callback'); - - $id = (int) $this->getState('banner.id'); - - // For PHP 5.3 compat we can't use $this in the lambda function below, so grab the database driver now to use it - $db = $this->getDatabase(); - - $loader = function ($id) use ($db) - { - $query = $db->getQuery(true); - - $query->select( - [ - $db->quoteName('a.clickurl'), - $db->quoteName('a.cid'), - $db->quoteName('a.track_clicks'), - $db->quoteName('cl.track_clicks', 'client_track_clicks'), - ] - ) - ->from($db->quoteName('#__banners', 'a')) - ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')) - ->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - return $db->loadObject(); - }; - - try - { - $this->_item = $cache->get($loader, array($id), md5(__METHOD__ . $id)); - } - catch (CacheExceptionInterface $e) - { - $this->_item = $loader($id); - } - } - - return $this->_item; - } - - /** - * Get the URL for a banner - * - * @return string - * - * @since 1.5 - */ - public function getUrl() - { - $item = $this->getItem(); - $url = $item->clickurl; - - // Check for links - if (!preg_match('#http[s]?://|index[2]?\.php#', $url)) - { - $url = "http://$url"; - } - - return $url; - } + /** + * Cached item object + * + * @var object + * @since 1.6 + */ + protected $_item; + + /** + * Clicks the URL, incrementing the counter + * + * @return void + * + * @since 1.5 + * @throws \Exception + */ + public function click() + { + $item = $this->getItem(); + + if (empty($item)) { + throw new \Exception(Text::_('JERROR_PAGE_NOT_FOUND'), 404); + } + + $id = (int) $this->getState('banner.id'); + + // Update click count + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->update($db->quoteName('#__banners')) + ->set($db->quoteName('clicks') . ' = ' . $db->quoteName('clicks') . ' + 1') + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500); + } + + // Track clicks + $trackClicks = $item->track_clicks; + + if ($trackClicks < 0 && $item->cid) { + $trackClicks = $item->client_track_clicks; + } + + if ($trackClicks < 0) { + $config = ComponentHelper::getParams('com_banners'); + $trackClicks = $config->get('track_clicks'); + } + + if ($trackClicks > 0) { + $trackDate = Factory::getDate()->format('Y-m-d H:00:00'); + $trackDate = Factory::getDate($trackDate)->toSql(); + + $query = $db->getQuery(true); + + $query->select($db->quoteName('count')) + ->from($db->quoteName('#__banner_tracks')) + ->where( + [ + $db->quoteName('track_type') . ' = 2', + $db->quoteName('banner_id') . ' = :id', + $db->quoteName('track_date') . ' = :trackDate', + ] + ) + ->bind(':id', $id, ParameterType::INTEGER) + ->bind(':trackDate', $trackDate); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500); + } + + $count = $db->loadResult(); + + $query = $db->getQuery(true); + + if ($count) { + // Update count + $query->update($db->quoteName('#__banner_tracks')) + ->set($db->quoteName('count') . ' = ' . $db->quoteName('count') . ' + 1') + ->where( + [ + $db->quoteName('track_type') . ' = 2', + $db->quoteName('banner_id') . ' = :id', + $db->quoteName('track_date') . ' = :trackDate', + ] + ) + ->bind(':id', $id, ParameterType::INTEGER) + ->bind(':trackDate', $trackDate); + } else { + // Insert new count + $query->insert($db->quoteName('#__banner_tracks')) + ->columns( + [ + $db->quoteName('count'), + $db->quoteName('track_type'), + $db->quoteName('banner_id'), + $db->quoteName('track_date'), + ] + ) + ->values('1, 2 , :id, :trackDate') + ->bind(':id', $id, ParameterType::INTEGER) + ->bind(':trackDate', $trackDate); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500); + } + } + } + + /** + * Get the data for a banner. + * + * @return object + * + * @since 1.6 + */ + public function &getItem() + { + if (!isset($this->_item)) { + /** @var \Joomla\CMS\Cache\Controller\CallbackController $cache */ + $cache = Factory::getCache('com_banners', 'callback'); + + $id = (int) $this->getState('banner.id'); + + // For PHP 5.3 compat we can't use $this in the lambda function below, so grab the database driver now to use it + $db = $this->getDatabase(); + + $loader = function ($id) use ($db) { + $query = $db->getQuery(true); + + $query->select( + [ + $db->quoteName('a.clickurl'), + $db->quoteName('a.cid'), + $db->quoteName('a.track_clicks'), + $db->quoteName('cl.track_clicks', 'client_track_clicks'), + ] + ) + ->from($db->quoteName('#__banners', 'a')) + ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')) + ->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + return $db->loadObject(); + }; + + try { + $this->_item = $cache->get($loader, array($id), md5(__METHOD__ . $id)); + } catch (CacheExceptionInterface $e) { + $this->_item = $loader($id); + } + } + + return $this->_item; + } + + /** + * Get the URL for a banner + * + * @return string + * + * @since 1.5 + */ + public function getUrl() + { + $item = $this->getItem(); + $url = $item->clickurl; + + // Check for links + if (!preg_match('#http[s]?://|index[2]?\.php#', $url)) { + $url = "http://$url"; + } + + return $url; + } } diff --git a/components/com_banners/src/Model/BannersModel.php b/components/com_banners/src/Model/BannersModel.php index 9b5316aac5bee..70eaa5e5adc0d 100644 --- a/components/com_banners/src/Model/BannersModel.php +++ b/components/com_banners/src/Model/BannersModel.php @@ -1,4 +1,5 @@ getState('filter.search'); - $id .= ':' . $this->getState('filter.tag_search'); - $id .= ':' . $this->getState('filter.client_id'); - $id .= ':' . serialize($this->getState('filter.category_id')); - $id .= ':' . serialize($this->getState('filter.keywords')); - - return parent::getStoreId($id); - } - - /** - * Method to get a DatabaseQuery object for retrieving the data set from a database. - * - * @return DatabaseQuery A DatabaseQuery object to retrieve the data set. - * - * @since 1.6 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $ordering = $this->getState('filter.ordering'); - $tagSearch = $this->getState('filter.tag_search'); - $cid = (int) $this->getState('filter.client_id'); - $categoryId = $this->getState('filter.category_id'); - $keywords = $this->getState('filter.keywords'); - $randomise = ($ordering === 'random'); - $nowDate = Factory::getDate()->toSql(); - - $query->select( - [ - $db->quoteName('a.id'), - $db->quoteName('a.type'), - $db->quoteName('a.name'), - $db->quoteName('a.clickurl'), - $db->quoteName('a.sticky'), - $db->quoteName('a.cid'), - $db->quoteName('a.description'), - $db->quoteName('a.params'), - $db->quoteName('a.custombannercode'), - $db->quoteName('a.track_impressions'), - $db->quoteName('cl.track_impressions', 'client_track_impressions'), - ] - ) - ->from($db->quoteName('#__banners', 'a')) - ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')) - ->where($db->quoteName('a.state') . ' = 1') - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_up') . ' IS NULL', - $db->quoteName('a.publish_up') . ' <= :nowDate1', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_down') . ' IS NULL', - $db->quoteName('a.publish_down') . ' >= :nowDate2', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.imptotal') . ' = 0', - $db->quoteName('a.impmade') . ' < ' . $db->quoteName('a.imptotal'), - ], - 'OR' - ) - ->bind([':nowDate1', ':nowDate2'], $nowDate); - - if ($cid) - { - $query->where( - [ - $db->quoteName('a.cid') . ' = :clientId', - $db->quoteName('cl.state') . ' = 1', - ] - ) - ->bind(':clientId', $cid, ParameterType::INTEGER); - } - - // Filter by a single or group of categories - if (is_numeric($categoryId)) - { - $categoryId = (int) $categoryId; - $type = $this->getState('filter.category_id.include', true) ? ' = ' : ' <> '; - - // Add subcategory check - if ($this->getState('filter.subcategories', false)) - { - $levels = (int) $this->getState('filter.max_category_levels', '1'); - - // Create a subquery for the subcategory list - $subQuery = $db->getQuery(true); - $subQuery->select($db->quoteName('sub.id')) - ->from($db->quoteName('#__categories', 'sub')) - ->join( - 'INNER', - $db->quoteName('#__categories', 'this'), - $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') - . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') - ) - ->where( - [ - $db->quoteName('this.id') . ' = :categoryId1', - $db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :levels', - ] - ); - - // Add the subquery to the main query - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.catid') . $type . ':categoryId2', - $db->quoteName('a.catid') . ' IN (' . $subQuery . ')', - ], - 'OR' - ) - ->bind([':categoryId1', ':categoryId2'], $categoryId, ParameterType::INTEGER) - ->bind(':levels', $levels, ParameterType::INTEGER); - } - else - { - $query->where($db->quoteName('a.catid') . $type . ':categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - } - elseif (is_array($categoryId) && (count($categoryId) > 0)) - { - $categoryId = ArrayHelper::toInteger($categoryId); - - if ($this->getState('filter.category_id.include', true)) - { - $query->whereIn($db->quoteName('a.catid'), $categoryId); - } - else - { - $query->whereNotIn($db->quoteName('a.catid'), $categoryId); - } - } - - if ($tagSearch) - { - if (!$keywords) - { - // No keywords, select nothing. - $query->where('0 != 0'); - } - else - { - $temp = array(); - $config = ComponentHelper::getParams('com_banners'); - $prefix = $config->get('metakey_prefix'); - - if ($categoryId) - { - $query->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('a.catid') . ' = ' . $db->quoteName('cat.id')); - } - - foreach ($keywords as $key => $keyword) - { - $regexp = '[[:<:]]' . $keyword . '[[:>:]]'; - $valuesToBind = [$keyword, $keyword, $regexp]; - - if ($cid) - { - $valuesToBind[] = $regexp; - } - - if ($categoryId) - { - $valuesToBind[] = $regexp; - } - - // Because values to $query->bind() are passed by reference, using $query->bindArray() here instead to prevent overwriting. - $bounded = $query->bindArray($valuesToBind, ParameterType::STRING); - - $condition1 = $db->quoteName('a.own_prefix') . ' = 1' - . ' AND ' . $db->quoteName('a.metakey_prefix') - . ' = SUBSTRING(' . $bounded[0] . ',1,LENGTH(' . $db->quoteName('a.metakey_prefix') . '))' - . ' OR ' . $db->quoteName('a.own_prefix') . ' = 0' - . ' AND ' . $db->quoteName('cl.own_prefix') . ' = 1' - . ' AND ' . $db->quoteName('cl.metakey_prefix') - . ' = SUBSTRING(' . $bounded[1] . ',1,LENGTH(' . $db->quoteName('cl.metakey_prefix') . '))' - . ' OR ' . $db->quoteName('a.own_prefix') . ' = 0' - . ' AND ' . $db->quoteName('cl.own_prefix') . ' = 0' - . ' AND ' . ($prefix == substr($keyword, 0, strlen($prefix)) ? '0 = 0' : '0 != 0'); - - $condition2 = $db->quoteName('a.metakey') . ' ' . $query->regexp($bounded[2]); - - if ($cid) - { - $condition2 .= ' OR ' . $db->quoteName('cl.metakey') . ' ' . $query->regexp($bounded[3]) . ' '; - } - - if ($categoryId) - { - $condition2 .= ' OR ' . $db->quoteName('cat.metakey') . ' ' . $query->regexp($bounded[4]) . ' '; - } - - $temp[] = "($condition1) AND ($condition2)"; - } - - $query->where('(' . implode(' OR ', $temp) . ')'); - } - } - - // Filter by language - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - $query->order($db->quoteName('a.sticky') . ' DESC, ' . ($randomise ? $query->rand() : $db->quoteName('a.ordering'))); - - return $query; - } - - /** - * Get a list of banners. - * - * @return array - * - * @since 1.6 - */ - public function getItems() - { - if ($this->getState('filter.tag_search')) - { - // Filter out empty keywords. - $keywords = array_values(array_filter(array_map('trim', $this->getState('filter.keywords')), 'strlen')); - - // Re-set state before running the query. - $this->setState('filter.keywords', $keywords); - - // If no keywords are provided, avoid running the query. - if (!$keywords) - { - $this->cache['items'] = array(); - - return $this->cache['items']; - } - } - - if (!isset($this->cache['items'])) - { - $this->cache['items'] = parent::getItems(); - - foreach ($this->cache['items'] as &$item) - { - $item->params = new Registry($item->params); - } - } - - return $this->cache['items']; - } - - /** - * Makes impressions on a list of banners - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - public function impress() - { - $trackDate = Factory::getDate()->format('Y-m-d H:00:00'); - $trackDate = Factory::getDate($trackDate)->toSql(); - $items = $this->getItems(); - $db = $this->getDatabase(); - $bid = []; - - if (!count($items)) - { - return; - } - - foreach ($items as $item) - { - $bid[] = (int) $item->id; - } - - // Increment impression made - $query = $db->getQuery(true); - $query->update($db->quoteName('#__banners')) - ->set($db->quoteName('impmade') . ' = ' . $db->quoteName('impmade') . ' + 1') - ->whereIn($db->quoteName('id'), $bid); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - throw new \Exception($e->getMessage(), 500); - } - - foreach ($items as $item) - { - // Track impressions - $trackImpressions = $item->track_impressions; - - if ($trackImpressions < 0 && $item->cid) - { - $trackImpressions = $item->client_track_impressions; - } - - if ($trackImpressions < 0) - { - $config = ComponentHelper::getParams('com_banners'); - $trackImpressions = $config->get('track_impressions'); - } - - if ($trackImpressions > 0) - { - // Is track already created? - // Update count - $query = $db->getQuery(true); - $query->update($db->quoteName('#__banner_tracks')) - ->set($db->quoteName('count') . ' = ' . $db->quoteName('count') . ' + 1') - ->where( - [ - $db->quoteName('track_type') . ' = 1', - $db->quoteName('banner_id') . ' = :id', - $db->quoteName('track_date') . ' = :trackDate', - ] - ) - ->bind(':id', $item->id, ParameterType::INTEGER) - ->bind(':trackDate', $trackDate); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - throw new \Exception($e->getMessage(), 500); - } - - if ($db->getAffectedRows() === 0) - { - // Insert new count - $query = $db->getQuery(true); - $query->insert($db->quoteName('#__banner_tracks')) - ->columns( - [ - $db->quoteName('count'), - $db->quoteName('track_type'), - $db->quoteName('banner_id'), - $db->quoteName('track_date'), - ] - ) - ->values('1, 1, :id, :trackDate') - ->bind(':id', $item->id, ParameterType::INTEGER) - ->bind(':trackDate', $trackDate); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - throw new \Exception($e->getMessage(), 500); - } - } - } - } - } + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.tag_search'); + $id .= ':' . $this->getState('filter.client_id'); + $id .= ':' . serialize($this->getState('filter.category_id')); + $id .= ':' . serialize($this->getState('filter.keywords')); + + return parent::getStoreId($id); + } + + /** + * Method to get a DatabaseQuery object for retrieving the data set from a database. + * + * @return DatabaseQuery A DatabaseQuery object to retrieve the data set. + * + * @since 1.6 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $ordering = $this->getState('filter.ordering'); + $tagSearch = $this->getState('filter.tag_search'); + $cid = (int) $this->getState('filter.client_id'); + $categoryId = $this->getState('filter.category_id'); + $keywords = $this->getState('filter.keywords'); + $randomise = ($ordering === 'random'); + $nowDate = Factory::getDate()->toSql(); + + $query->select( + [ + $db->quoteName('a.id'), + $db->quoteName('a.type'), + $db->quoteName('a.name'), + $db->quoteName('a.clickurl'), + $db->quoteName('a.sticky'), + $db->quoteName('a.cid'), + $db->quoteName('a.description'), + $db->quoteName('a.params'), + $db->quoteName('a.custombannercode'), + $db->quoteName('a.track_impressions'), + $db->quoteName('cl.track_impressions', 'client_track_impressions'), + ] + ) + ->from($db->quoteName('#__banners', 'a')) + ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')) + ->where($db->quoteName('a.state') . ' = 1') + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_up') . ' IS NULL', + $db->quoteName('a.publish_up') . ' <= :nowDate1', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_down') . ' IS NULL', + $db->quoteName('a.publish_down') . ' >= :nowDate2', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.imptotal') . ' = 0', + $db->quoteName('a.impmade') . ' < ' . $db->quoteName('a.imptotal'), + ], + 'OR' + ) + ->bind([':nowDate1', ':nowDate2'], $nowDate); + + if ($cid) { + $query->where( + [ + $db->quoteName('a.cid') . ' = :clientId', + $db->quoteName('cl.state') . ' = 1', + ] + ) + ->bind(':clientId', $cid, ParameterType::INTEGER); + } + + // Filter by a single or group of categories + if (is_numeric($categoryId)) { + $categoryId = (int) $categoryId; + $type = $this->getState('filter.category_id.include', true) ? ' = ' : ' <> '; + + // Add subcategory check + if ($this->getState('filter.subcategories', false)) { + $levels = (int) $this->getState('filter.max_category_levels', '1'); + + // Create a subquery for the subcategory list + $subQuery = $db->getQuery(true); + $subQuery->select($db->quoteName('sub.id')) + ->from($db->quoteName('#__categories', 'sub')) + ->join( + 'INNER', + $db->quoteName('#__categories', 'this'), + $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') + . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') + ) + ->where( + [ + $db->quoteName('this.id') . ' = :categoryId1', + $db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :levels', + ] + ); + + // Add the subquery to the main query + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.catid') . $type . ':categoryId2', + $db->quoteName('a.catid') . ' IN (' . $subQuery . ')', + ], + 'OR' + ) + ->bind([':categoryId1', ':categoryId2'], $categoryId, ParameterType::INTEGER) + ->bind(':levels', $levels, ParameterType::INTEGER); + } else { + $query->where($db->quoteName('a.catid') . $type . ':categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + } elseif (is_array($categoryId) && (count($categoryId) > 0)) { + $categoryId = ArrayHelper::toInteger($categoryId); + + if ($this->getState('filter.category_id.include', true)) { + $query->whereIn($db->quoteName('a.catid'), $categoryId); + } else { + $query->whereNotIn($db->quoteName('a.catid'), $categoryId); + } + } + + if ($tagSearch) { + if (!$keywords) { + // No keywords, select nothing. + $query->where('0 != 0'); + } else { + $temp = array(); + $config = ComponentHelper::getParams('com_banners'); + $prefix = $config->get('metakey_prefix'); + + if ($categoryId) { + $query->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('a.catid') . ' = ' . $db->quoteName('cat.id')); + } + + foreach ($keywords as $key => $keyword) { + $regexp = '[[:<:]]' . $keyword . '[[:>:]]'; + $valuesToBind = [$keyword, $keyword, $regexp]; + + if ($cid) { + $valuesToBind[] = $regexp; + } + + if ($categoryId) { + $valuesToBind[] = $regexp; + } + + // Because values to $query->bind() are passed by reference, using $query->bindArray() here instead to prevent overwriting. + $bounded = $query->bindArray($valuesToBind, ParameterType::STRING); + + $condition1 = $db->quoteName('a.own_prefix') . ' = 1' + . ' AND ' . $db->quoteName('a.metakey_prefix') + . ' = SUBSTRING(' . $bounded[0] . ',1,LENGTH(' . $db->quoteName('a.metakey_prefix') . '))' + . ' OR ' . $db->quoteName('a.own_prefix') . ' = 0' + . ' AND ' . $db->quoteName('cl.own_prefix') . ' = 1' + . ' AND ' . $db->quoteName('cl.metakey_prefix') + . ' = SUBSTRING(' . $bounded[1] . ',1,LENGTH(' . $db->quoteName('cl.metakey_prefix') . '))' + . ' OR ' . $db->quoteName('a.own_prefix') . ' = 0' + . ' AND ' . $db->quoteName('cl.own_prefix') . ' = 0' + . ' AND ' . ($prefix == substr($keyword, 0, strlen($prefix)) ? '0 = 0' : '0 != 0'); + + $condition2 = $db->quoteName('a.metakey') . ' ' . $query->regexp($bounded[2]); + + if ($cid) { + $condition2 .= ' OR ' . $db->quoteName('cl.metakey') . ' ' . $query->regexp($bounded[3]) . ' '; + } + + if ($categoryId) { + $condition2 .= ' OR ' . $db->quoteName('cat.metakey') . ' ' . $query->regexp($bounded[4]) . ' '; + } + + $temp[] = "($condition1) AND ($condition2)"; + } + + $query->where('(' . implode(' OR ', $temp) . ')'); + } + } + + // Filter by language + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + $query->order($db->quoteName('a.sticky') . ' DESC, ' . ($randomise ? $query->rand() : $db->quoteName('a.ordering'))); + + return $query; + } + + /** + * Get a list of banners. + * + * @return array + * + * @since 1.6 + */ + public function getItems() + { + if ($this->getState('filter.tag_search')) { + // Filter out empty keywords. + $keywords = array_values(array_filter(array_map('trim', $this->getState('filter.keywords')), 'strlen')); + + // Re-set state before running the query. + $this->setState('filter.keywords', $keywords); + + // If no keywords are provided, avoid running the query. + if (!$keywords) { + $this->cache['items'] = array(); + + return $this->cache['items']; + } + } + + if (!isset($this->cache['items'])) { + $this->cache['items'] = parent::getItems(); + + foreach ($this->cache['items'] as &$item) { + $item->params = new Registry($item->params); + } + } + + return $this->cache['items']; + } + + /** + * Makes impressions on a list of banners + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + public function impress() + { + $trackDate = Factory::getDate()->format('Y-m-d H:00:00'); + $trackDate = Factory::getDate($trackDate)->toSql(); + $items = $this->getItems(); + $db = $this->getDatabase(); + $bid = []; + + if (!count($items)) { + return; + } + + foreach ($items as $item) { + $bid[] = (int) $item->id; + } + + // Increment impression made + $query = $db->getQuery(true); + $query->update($db->quoteName('#__banners')) + ->set($db->quoteName('impmade') . ' = ' . $db->quoteName('impmade') . ' + 1') + ->whereIn($db->quoteName('id'), $bid); + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + throw new \Exception($e->getMessage(), 500); + } + + foreach ($items as $item) { + // Track impressions + $trackImpressions = $item->track_impressions; + + if ($trackImpressions < 0 && $item->cid) { + $trackImpressions = $item->client_track_impressions; + } + + if ($trackImpressions < 0) { + $config = ComponentHelper::getParams('com_banners'); + $trackImpressions = $config->get('track_impressions'); + } + + if ($trackImpressions > 0) { + // Is track already created? + // Update count + $query = $db->getQuery(true); + $query->update($db->quoteName('#__banner_tracks')) + ->set($db->quoteName('count') . ' = ' . $db->quoteName('count') . ' + 1') + ->where( + [ + $db->quoteName('track_type') . ' = 1', + $db->quoteName('banner_id') . ' = :id', + $db->quoteName('track_date') . ' = :trackDate', + ] + ) + ->bind(':id', $item->id, ParameterType::INTEGER) + ->bind(':trackDate', $trackDate); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + throw new \Exception($e->getMessage(), 500); + } + + if ($db->getAffectedRows() === 0) { + // Insert new count + $query = $db->getQuery(true); + $query->insert($db->quoteName('#__banner_tracks')) + ->columns( + [ + $db->quoteName('count'), + $db->quoteName('track_type'), + $db->quoteName('banner_id'), + $db->quoteName('track_date'), + ] + ) + ->values('1, 1, :id, :trackDate') + ->bind(':id', $item->id, ParameterType::INTEGER) + ->bind(':trackDate', $trackDate); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + throw new \Exception($e->getMessage(), 500); + } + } + } + } + } } diff --git a/components/com_banners/src/Service/Category.php b/components/com_banners/src/Service/Category.php index 7c02083006dcb..ff529be6db6e3 100644 --- a/components/com_banners/src/Service/Category.php +++ b/components/com_banners/src/Service/Category.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - } - - /** - * Method to handle cancel - * - * @return void - * - * @since 3.2 - */ - public function cancel() - { - // Redirect back to home(base) page - $this->setRedirect(Uri::base()); - } - - /** - * Method to save global configuration. - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - // Check if the user is authorized to do this. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR')); - $this->app->redirect('index.php'); - } - - // Set FTP credentials, if given. - ClientHelper::setCredentialsFromRequest('ftp'); - - $model = $this->getModel(); - - $form = $model->getForm(); - $data = $this->app->input->post->get('jform', array(), 'array'); - - // Validate the posted data. - $return = $model->validate($form, $data); - - // Check for validation errors. - if ($return === false) - { - /* - * The validate method enqueued all messages for us, so we just need to redirect back. - */ - - // Save the data in the session. - $this->app->setUserState('com_config.config.global.data', $data); - - // Redirect back to the edit screen. - $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); - } - - // Attempt to save the configuration. - $data = $return; - - // Access backend com_config - $saveClass = $this->factory->createController('Application', 'Administrator', [], $this->app, $this->input); - - // Get a document object - $document = $this->app->getDocument(); - - // Set backend required params - $document->setType('json'); - - // Execute backend controller - $return = $saveClass->save(); - - // Reset params back after requesting from service - $document->setType('html'); - - // Check the return value. - if ($return === false) - { - /* - * The save method enqueued all messages for us, so we just need to redirect back. - */ - - // Save the data in the session. - $this->app->setUserState('com_config.config.global.data', $data); - - // Save failed, go back to the screen and display a notice. - $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); - } - - // Redirect back to com_config display - $this->app->enqueueMessage(Text::_('COM_CONFIG_SAVE_SUCCESS')); - $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); - - return true; - } + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The JApplication for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object for the request + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('apply', 'save'); + } + + /** + * Method to handle cancel + * + * @return void + * + * @since 3.2 + */ + public function cancel() + { + // Redirect back to home(base) page + $this->setRedirect(Uri::base()); + } + + /** + * Method to save global configuration. + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + // Check if the user is authorized to do this. + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR')); + $this->app->redirect('index.php'); + } + + // Set FTP credentials, if given. + ClientHelper::setCredentialsFromRequest('ftp'); + + $model = $this->getModel(); + + $form = $model->getForm(); + $data = $this->app->input->post->get('jform', array(), 'array'); + + // Validate the posted data. + $return = $model->validate($form, $data); + + // Check for validation errors. + if ($return === false) { + /* + * The validate method enqueued all messages for us, so we just need to redirect back. + */ + + // Save the data in the session. + $this->app->setUserState('com_config.config.global.data', $data); + + // Redirect back to the edit screen. + $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); + } + + // Attempt to save the configuration. + $data = $return; + + // Access backend com_config + $saveClass = $this->factory->createController('Application', 'Administrator', [], $this->app, $this->input); + + // Get a document object + $document = $this->app->getDocument(); + + // Set backend required params + $document->setType('json'); + + // Execute backend controller + $return = $saveClass->save(); + + // Reset params back after requesting from service + $document->setType('html'); + + // Check the return value. + if ($return === false) { + /* + * The save method enqueued all messages for us, so we just need to redirect back. + */ + + // Save the data in the session. + $this->app->setUserState('com_config.config.global.data', $data); + + // Save failed, go back to the screen and display a notice. + $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); + } + + // Redirect back to com_config display + $this->app->enqueueMessage(Text::_('COM_CONFIG_SAVE_SUCCESS')); + $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); + + return true; + } } diff --git a/components/com_config/src/Controller/DisplayController.php b/components/com_config/src/Controller/DisplayController.php index daadff242b4c9..231d3dc00cc64 100644 --- a/components/com_config/src/Controller/DisplayController.php +++ b/components/com_config/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ setRedirect(Uri::base()); - } + /** + * Method to handle cancel + * + * @return void + * + * @since 3.2 + */ + public function cancel() + { + // Redirect back to home(base) page + $this->setRedirect(Uri::base()); + } } diff --git a/components/com_config/src/Controller/ModulesController.php b/components/com_config/src/Controller/ModulesController.php index 5000e4cc91156..b0f27cd0159eb 100644 --- a/components/com_config/src/Controller/ModulesController.php +++ b/components/com_config/src/Controller/ModulesController.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - } - - /** - * Method to handle cancel - * - * @return void - * - * @since 3.2 - */ - public function cancel() - { - // Redirect back to home(base) page - $this->setRedirect(Uri::base()); - } - - /** - * Method to save module editing. - * - * @return void - * - * @since 3.2 - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - // Check if the user is authorized to do this. - $user = $this->app->getIdentity(); - - if (!$user->authorise('module.edit.frontend', 'com_modules.module.' . $this->input->get('id'))) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $this->app->redirect('index.php'); - } - - // Set FTP credentials, if given. - ClientHelper::setCredentialsFromRequest('ftp'); - - // Get submitted module id - $moduleId = '&id=' . $this->input->getInt('id'); - - // Get returnUri - $returnUri = $this->input->post->get('return', null, 'base64'); - $redirect = ''; - - if (!empty($returnUri)) - { - $redirect = '&return=' . $returnUri; - } - - /** @var AdministratorApplication $app */ - $app = Factory::getContainer()->get(AdministratorApplication::class); - - // Reset Uri cache. - Uri::reset(); - - // Get a document object - $document = $this->app->getDocument(); - - // Load application dependencies. - $app->loadLanguage($this->app->getLanguage()); - $app->loadDocument($document); - $app->loadIdentity($user); - - /** @var \Joomla\CMS\Dispatcher\ComponentDispatcher $dispatcher */ - $dispatcher = $app->bootComponent('com_modules')->getDispatcher($app); - - /** @var ModuleController $controllerClass */ - $controllerClass = $dispatcher->getController('Module'); - - // Set backend required params - $document->setType('json'); - - // Execute backend controller - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/forms'); - $return = $controllerClass->save(); - - // Reset params back after requesting from service - $document->setType('html'); - - // Check the return value. - if ($return === false) - { - // Save the data in the session. - $data = $this->input->post->get('jform', array(), 'array'); - - $this->app->setUserState('com_config.modules.global.data', $data); - - // Save failed, go back to the screen and display a notice. - $this->app->enqueueMessage(Text::_('JERROR_SAVE_FAILED')); - $this->app->redirect(Route::_('index.php?option=com_config&view=modules' . $moduleId . $redirect, false)); - } - - // Redirect back to com_config display - $this->app->enqueueMessage(Text::_('COM_CONFIG_MODULES_SAVE_SUCCESS')); - - // Set the redirect based on the task. - switch ($this->input->getCmd('task')) - { - case 'apply': - $this->app->redirect(Route::_('index.php?option=com_config&view=modules' . $moduleId . $redirect, false)); - break; - - case 'save': - default: - - if (!empty($returnUri)) - { - $redirect = base64_decode(urldecode($returnUri)); - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect)) - { - $redirect = Uri::base(); - } - } - else - { - $redirect = Uri::base(); - } - - $this->setRedirect($redirect); - break; - } - } + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object for the request + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('apply', 'save'); + } + + /** + * Method to handle cancel + * + * @return void + * + * @since 3.2 + */ + public function cancel() + { + // Redirect back to home(base) page + $this->setRedirect(Uri::base()); + } + + /** + * Method to save module editing. + * + * @return void + * + * @since 3.2 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + // Check if the user is authorized to do this. + $user = $this->app->getIdentity(); + + if (!$user->authorise('module.edit.frontend', 'com_modules.module.' . $this->input->get('id'))) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $this->app->redirect('index.php'); + } + + // Set FTP credentials, if given. + ClientHelper::setCredentialsFromRequest('ftp'); + + // Get submitted module id + $moduleId = '&id=' . $this->input->getInt('id'); + + // Get returnUri + $returnUri = $this->input->post->get('return', null, 'base64'); + $redirect = ''; + + if (!empty($returnUri)) { + $redirect = '&return=' . $returnUri; + } + + /** @var AdministratorApplication $app */ + $app = Factory::getContainer()->get(AdministratorApplication::class); + + // Reset Uri cache. + Uri::reset(); + + // Get a document object + $document = $this->app->getDocument(); + + // Load application dependencies. + $app->loadLanguage($this->app->getLanguage()); + $app->loadDocument($document); + $app->loadIdentity($user); + + /** @var \Joomla\CMS\Dispatcher\ComponentDispatcher $dispatcher */ + $dispatcher = $app->bootComponent('com_modules')->getDispatcher($app); + + /** @var ModuleController $controllerClass */ + $controllerClass = $dispatcher->getController('Module'); + + // Set backend required params + $document->setType('json'); + + // Execute backend controller + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/forms'); + $return = $controllerClass->save(); + + // Reset params back after requesting from service + $document->setType('html'); + + // Check the return value. + if ($return === false) { + // Save the data in the session. + $data = $this->input->post->get('jform', array(), 'array'); + + $this->app->setUserState('com_config.modules.global.data', $data); + + // Save failed, go back to the screen and display a notice. + $this->app->enqueueMessage(Text::_('JERROR_SAVE_FAILED')); + $this->app->redirect(Route::_('index.php?option=com_config&view=modules' . $moduleId . $redirect, false)); + } + + // Redirect back to com_config display + $this->app->enqueueMessage(Text::_('COM_CONFIG_MODULES_SAVE_SUCCESS')); + + // Set the redirect based on the task. + switch ($this->input->getCmd('task')) { + case 'apply': + $this->app->redirect(Route::_('index.php?option=com_config&view=modules' . $moduleId . $redirect, false)); + break; + + case 'save': + default: + if (!empty($returnUri)) { + $redirect = base64_decode(urldecode($returnUri)); + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect)) { + $redirect = Uri::base(); + } + } else { + $redirect = Uri::base(); + } + + $this->setRedirect($redirect); + break; + } + } } diff --git a/components/com_config/src/Controller/TemplatesController.php b/components/com_config/src/Controller/TemplatesController.php index 6cc36fcaad69a..74676ba563cbe 100644 --- a/components/com_config/src/Controller/TemplatesController.php +++ b/components/com_config/src/Controller/TemplatesController.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - } - - /** - * Method to handle cancel - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function cancel() - { - // Redirect back to home(base) page - $this->setRedirect(Uri::base()); - } - - /** - * Method to save global configuration. - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - // Check if the user is authorized to do this. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR')); - - return false; - } - - // Set FTP credentials, if given. - ClientHelper::setCredentialsFromRequest('ftp'); - - $app = $this->app; - - // Access backend com_templates - $controllerClass = $app->bootComponent('com_templates') - ->getMVCFactory()->createController('Style', 'Administrator', [], $app, $app->input); - - // Get a document object - $document = $app->getDocument(); - - // Set backend required params - $document->setType('json'); - $this->input->set('id', $app->getTemplate(true)->id); - - // Execute backend controller - $return = $controllerClass->save(); - - // Reset params back after requesting from service - $document->setType('html'); - - // Check the return value. - if ($return === false) - { - // Save failed, go back to the screen and display a notice. - $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED'), 'error'); - $this->setRedirect(Route::_('index.php?option=com_config&view=templates', false)); - - return false; - } - - // Set the success message. - $this->setMessage(Text::_('COM_CONFIG_SAVE_SUCCESS')); - - // Redirect back to com_config display - $this->setRedirect(Route::_('index.php?option=com_config&view=templates', false)); - - return true; - } + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object for the request + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // Apply, Save & New, and Save As copy should be standard on forms. + $this->registerTask('apply', 'save'); + } + + /** + * Method to handle cancel + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function cancel() + { + // Redirect back to home(base) page + $this->setRedirect(Uri::base()); + } + + /** + * Method to save global configuration. + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + // Check if the user is authorized to do this. + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR')); + + return false; + } + + // Set FTP credentials, if given. + ClientHelper::setCredentialsFromRequest('ftp'); + + $app = $this->app; + + // Access backend com_templates + $controllerClass = $app->bootComponent('com_templates') + ->getMVCFactory()->createController('Style', 'Administrator', [], $app, $app->input); + + // Get a document object + $document = $app->getDocument(); + + // Set backend required params + $document->setType('json'); + $this->input->set('id', $app->getTemplate(true)->id); + + // Execute backend controller + $return = $controllerClass->save(); + + // Reset params back after requesting from service + $document->setType('html'); + + // Check the return value. + if ($return === false) { + // Save failed, go back to the screen and display a notice. + $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_config&view=templates', false)); + + return false; + } + + // Set the success message. + $this->setMessage(Text::_('COM_CONFIG_SAVE_SUCCESS')); + + // Redirect back to com_config display + $this->setRedirect(Route::_('index.php?option=com_config&view=templates', false)); + + return true; + } } diff --git a/components/com_config/src/Dispatcher/Dispatcher.php b/components/com_config/src/Dispatcher/Dispatcher.php index 336f520bad3c2..d410cbc691ddd 100644 --- a/components/com_config/src/Dispatcher/Dispatcher.php +++ b/components/com_config/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->getCmd('task', 'display'); - $view = $this->input->get('view'); - $user = $this->app->getIdentity(); + $task = $this->input->getCmd('task', 'display'); + $view = $this->input->get('view'); + $user = $this->app->getIdentity(); - if (substr($task, 0, 8) === 'modules.' || $view === 'modules') - { - if (!$user->authorise('module.edit.frontend', 'com_modules.module.' . $this->input->get('id'))) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } - elseif (!$user->authorise('core.admin')) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + if (substr($task, 0, 8) === 'modules.' || $view === 'modules') { + if (!$user->authorise('module.edit.frontend', 'com_modules.module.' . $this->input->get('id'))) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } elseif (!$user->authorise('core.admin')) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/components/com_config/src/Model/ConfigModel.php b/components/com_config/src/Model/ConfigModel.php index 381ff67a69fed..175f397010ae7 100644 --- a/components/com_config/src/Model/ConfigModel.php +++ b/components/com_config/src/Model/ConfigModel.php @@ -1,4 +1,5 @@ loadForm('com_config.config', 'config', array('control' => 'jform', 'load_data' => $loadData)); + /** + * Method to get a form object. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return mixed A JForm object on success, false on failure + * + * @since 3.2 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_config.config', 'config', array('control' => 'jform', 'load_data' => $loadData)); - if (empty($form)) - { - return false; - } + if (empty($form)) { + return false; + } - return $form; - } + return $form; + } } diff --git a/components/com_config/src/Model/FormModel.php b/components/com_config/src/Model/FormModel.php index ccdbf7116771d..7ab6f22c8ac7c 100644 --- a/components/com_config/src/Model/FormModel.php +++ b/components/com_config/src/Model/FormModel.php @@ -1,4 +1,5 @@ getTable(); - - if (!$table->load($pk)) - { - throw new \RuntimeException($table->getError()); - } - - // Check if this is the user has previously checked out the row. - if (!is_null($table->checked_out) && $table->checked_out != $user->get('id') && !$user->authorise('core.admin', 'com_checkin')) - { - throw new \RuntimeException($table->getError()); - } - - // Attempt to check the row in. - if (!$table->checkIn($pk)) - { - throw new \RuntimeException($table->getError()); - } - } - - return true; - } - - /** - * Method to check-out a row for editing. - * - * @param integer $pk The numeric id of the primary key. - * - * @return boolean False on failure or error, true otherwise. - * - * @since 3.2 - */ - public function checkout($pk = null) - { - // Only attempt to check the row in if it exists. - if ($pk) - { - $user = Factory::getUser(); - - // Get an instance of the row to checkout. - $table = $this->getTable(); - - if (!$table->load($pk)) - { - throw new \RuntimeException($table->getError()); - } - - // Check if this is the user having previously checked out the row. - if (!is_null($table->checked_out) && $table->checked_out != $user->get('id')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CHECKOUT_USER_MISMATCH')); - } - - // Attempt to check the row out. - if (!$table->checkOut($user->get('id'), $pk)) - { - throw new \RuntimeException($table->getError()); - } - } - - return true; - } - - /** - * Method to get a form object. - * - * @param string $name The name of the form. - * @param string $source The form source. Can be XML string if file flag is set to false. - * @param array $options Optional array of options for the form creation. - * @param boolean $clear Optional argument to force load a new form. - * @param string $xpath An optional xpath to search for the fields. - * - * @return mixed JForm object on success, False on error. - * - * @see JForm - * @since 3.2 - */ - protected function loadForm($name, $source = null, $options = array(), $clear = false, $xpath = false) - { - // Handle the optional arguments. - $options['control'] = ArrayHelper::getValue($options, 'control', false); - - // Create a signature hash. - $hash = sha1($source . serialize($options)); - - // Check if we can use a previously loaded form. - if (isset($this->_forms[$hash]) && !$clear) - { - return $this->_forms[$hash]; - } - - // Register the paths for the form. - Form::addFormPath(JPATH_SITE . '/components/com_config/forms'); - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_config/forms'); - - try - { - // Get the form. - $form = Form::getInstance($name, $source, $options, false, $xpath); - - if (isset($options['load_data']) && $options['load_data']) - { - // Get the data for the form. - $data = $this->loadFormData(); - } - else - { - $data = array(); - } - - // Allow for additional modification of the form, and events to be triggered. - // We pass the data because plugins may require it. - $this->preprocessForm($form, $data); - - // Load the data into the form after the plugins have operated. - $form->bind($data); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage()); - - return false; - } - - // Store the form for later. - $this->_forms[$hash] = $form; - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 3.2 - */ - protected function loadFormData() - { - return array(); - } - - /** - * Method to allow derived classes to preprocess the data. - * - * @param string $context The context identifier. - * @param mixed &$data The data to be processed. It gets altered directly. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 3.2 - */ - protected function preprocessData($context, &$data, $group = 'content') - { - // Get the dispatcher and load the users plugins. - PluginHelper::importPlugin('content'); - - // Trigger the data preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareData', array($context, $data)); - } - - /** - * Method to allow derived classes to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @see \Joomla\CMS\Form\FormField - * @since 3.2 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - // Import the appropriate plugin group. - PluginHelper::importPlugin($group); - - // Trigger the form preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareForm', array($form, $data)); - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return mixed Array of filtered data if valid, false otherwise. - * - * @see \Joomla\CMS\Form\FormRule - * @see JFilterInput - * @since 3.2 - */ - public function validate($form, $data, $group = null) - { - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data, $group); - - // Check for an error. - if ($return instanceof \Exception) - { - Factory::getApplication()->enqueueMessage($return->getMessage(), 'error'); - - return false; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $message) - { - if ($message instanceof \Exception) - { - $message = $message->getMessage(); - } - - Factory::getApplication()->enqueueMessage($message, 'error'); - } - - return false; - } - - return $data; - } + /** + * Array of form objects. + * + * @var array + * @since 3.2 + */ + protected $forms = array(); + + /** + * Method to checkin a row. + * + * @param integer $pk The numeric id of the primary key. + * + * @return boolean False on failure or error, true otherwise. + * + * @since 3.2 + * @throws \RuntimeException + */ + public function checkin($pk = null) + { + // Only attempt to check the row in if it exists. + if ($pk) { + $user = Factory::getUser(); + + // Get an instance of the row to checkin. + $table = $this->getTable(); + + if (!$table->load($pk)) { + throw new \RuntimeException($table->getError()); + } + + // Check if this is the user has previously checked out the row. + if (!is_null($table->checked_out) && $table->checked_out != $user->get('id') && !$user->authorise('core.admin', 'com_checkin')) { + throw new \RuntimeException($table->getError()); + } + + // Attempt to check the row in. + if (!$table->checkIn($pk)) { + throw new \RuntimeException($table->getError()); + } + } + + return true; + } + + /** + * Method to check-out a row for editing. + * + * @param integer $pk The numeric id of the primary key. + * + * @return boolean False on failure or error, true otherwise. + * + * @since 3.2 + */ + public function checkout($pk = null) + { + // Only attempt to check the row in if it exists. + if ($pk) { + $user = Factory::getUser(); + + // Get an instance of the row to checkout. + $table = $this->getTable(); + + if (!$table->load($pk)) { + throw new \RuntimeException($table->getError()); + } + + // Check if this is the user having previously checked out the row. + if (!is_null($table->checked_out) && $table->checked_out != $user->get('id')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CHECKOUT_USER_MISMATCH')); + } + + // Attempt to check the row out. + if (!$table->checkOut($user->get('id'), $pk)) { + throw new \RuntimeException($table->getError()); + } + } + + return true; + } + + /** + * Method to get a form object. + * + * @param string $name The name of the form. + * @param string $source The form source. Can be XML string if file flag is set to false. + * @param array $options Optional array of options for the form creation. + * @param boolean $clear Optional argument to force load a new form. + * @param string $xpath An optional xpath to search for the fields. + * + * @return mixed JForm object on success, False on error. + * + * @see JForm + * @since 3.2 + */ + protected function loadForm($name, $source = null, $options = array(), $clear = false, $xpath = false) + { + // Handle the optional arguments. + $options['control'] = ArrayHelper::getValue($options, 'control', false); + + // Create a signature hash. + $hash = sha1($source . serialize($options)); + + // Check if we can use a previously loaded form. + if (isset($this->_forms[$hash]) && !$clear) { + return $this->_forms[$hash]; + } + + // Register the paths for the form. + Form::addFormPath(JPATH_SITE . '/components/com_config/forms'); + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_config/forms'); + + try { + // Get the form. + $form = Form::getInstance($name, $source, $options, false, $xpath); + + if (isset($options['load_data']) && $options['load_data']) { + // Get the data for the form. + $data = $this->loadFormData(); + } else { + $data = array(); + } + + // Allow for additional modification of the form, and events to be triggered. + // We pass the data because plugins may require it. + $this->preprocessForm($form, $data); + + // Load the data into the form after the plugins have operated. + $form->bind($data); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage()); + + return false; + } + + // Store the form for later. + $this->_forms[$hash] = $form; + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 3.2 + */ + protected function loadFormData() + { + return array(); + } + + /** + * Method to allow derived classes to preprocess the data. + * + * @param string $context The context identifier. + * @param mixed &$data The data to be processed. It gets altered directly. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 3.2 + */ + protected function preprocessData($context, &$data, $group = 'content') + { + // Get the dispatcher and load the users plugins. + PluginHelper::importPlugin('content'); + + // Trigger the data preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareData', array($context, $data)); + } + + /** + * Method to allow derived classes to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @see \Joomla\CMS\Form\FormField + * @since 3.2 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + // Import the appropriate plugin group. + PluginHelper::importPlugin($group); + + // Trigger the form preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareForm', array($form, $data)); + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return mixed Array of filtered data if valid, false otherwise. + * + * @see \Joomla\CMS\Form\FormRule + * @see JFilterInput + * @since 3.2 + */ + public function validate($form, $data, $group = null) + { + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data, $group); + + // Check for an error. + if ($return instanceof \Exception) { + Factory::getApplication()->enqueueMessage($return->getMessage(), 'error'); + + return false; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $message) { + if ($message instanceof \Exception) { + $message = $message->getMessage(); + } + + Factory::getApplication()->enqueueMessage($message, 'error'); + } + + return false; + } + + return $data; + } } diff --git a/components/com_config/src/Model/ModulesModel.php b/components/com_config/src/Model/ModulesModel.php index 9c8871187b0f7..66b596af1e900 100644 --- a/components/com_config/src/Model/ModulesModel.php +++ b/components/com_config/src/Model/ModulesModel.php @@ -1,4 +1,5 @@ input->getInt('id'); - - $this->setState('module.id', $pk); - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form A Form object on success, false on failure - * - * @since 3.2 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_config.modules', 'modules', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to preprocess the form - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 3.2 - * @throws \Exception if there is an error loading the form. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $lang = Factory::getLanguage(); - $module = $this->getState()->get('module.name'); - $basePath = JPATH_BASE; - - $formFile = Path::clean($basePath . '/modules/' . $module . '/' . $module . '.xml'); - - // Load the core and/or local language file(s). - $lang->load($module, $basePath) - || $lang->load($module, $basePath . '/modules/' . $module); - - if (file_exists($formFile)) - { - // Get the module form. - if (!$form->loadFile($formFile, false, '//config')) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - - // Load the default advanced params - Form::addFormPath(JPATH_BASE . '/components/com_config/model/form'); - $form->loadFile('modules_advanced', false); - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to get list of module positions in current template - * - * @return array - * - * @since 3.2 - */ - public function getPositions() - { - $lang = Factory::getLanguage(); - $templateName = Factory::getApplication()->getTemplate(); - - // Load templateDetails.xml file - $path = Path::clean(JPATH_BASE . '/templates/' . $templateName . '/templateDetails.xml'); - $currentTemplatePositions = array(); - - if (file_exists($path)) - { - $xml = simplexml_load_file($path); - - if (isset($xml->positions[0])) - { - // Load language files - $lang->load('tpl_' . $templateName . '.sys', JPATH_BASE) - || $lang->load('tpl_' . $templateName . '.sys', JPATH_BASE . '/templates/' . $templateName); - - foreach ($xml->positions[0] as $position) - { - $value = (string) $position; - $text = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'TPL_' . strtoupper($templateName) . '_POSITION_' . strtoupper($value)); - - // Construct list of positions - $currentTemplatePositions[] = self::createOption($value, Text::_($text) . ' [' . $value . ']'); - } - } - } - - $templateGroups = array(); - - // Add an empty value to be able to deselect a module position - $option = self::createOption(); - $templateGroups[''] = self::createOptionGroup('', array($option)); - - $templateGroups[$templateName] = self::createOptionGroup($templateName, $currentTemplatePositions); - - // Add custom position to options - $customGroupText = Text::_('COM_MODULES_CUSTOM_POSITION'); - - $editPositions = true; - $customPositions = self::getActivePositions(0, $editPositions); - $templateGroups[$customGroupText] = self::createOptionGroup($customGroupText, $customPositions); - - return $templateGroups; - } - - /** - * Get a list of modules positions - * - * @param integer $clientId Client ID - * @param boolean $editPositions Allow to edit the positions - * - * @return array A list of positions - * - * @since 3.6.3 - */ - public static function getActivePositions($clientId, $editPositions = false) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('DISTINCT position') - ->from($db->quoteName('#__modules')) - ->where($db->quoteName('client_id') . ' = ' . (int) $clientId) - ->order($db->quoteName('position')); - - $db->setQuery($query); - - try - { - $positions = $db->loadColumn(); - $positions = is_array($positions) ? $positions : array(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return; - } - - // Build the list - $options = array(); - - foreach ($positions as $position) - { - if (!$position && !$editPositions) - { - $options[] = HTMLHelper::_('select.option', 'none', ':: ' . Text::_('JNONE') . ' ::'); - } - else - { - $options[] = HTMLHelper::_('select.option', $position, $position); - } - } - - return $options; - } - - /** - * Create and return a new Option - * - * @param string $value The option value [optional] - * @param string $text The option text [optional] - * - * @return object The option as an object (stdClass instance) - * - * @since 3.6.3 - */ - private static function createOption($value = '', $text = '') - { - if (empty($text)) - { - $text = $value; - } - - $option = new \stdClass; - $option->value = $value; - $option->text = $text; - - return $option; - } - - /** - * Create and return a new Option Group - * - * @param string $label Value and label for group [optional] - * @param array $options Array of options to insert into group [optional] - * - * @return array Return the new group as an array - * - * @since 3.6.3 - */ - private static function createOptionGroup($label = '', $options = array()) - { - $group = array(); - $group['value'] = $label; - $group['text'] = $label; - $group['items'] = $options; - - return $group; - } + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.2 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + + $this->setState('module.id', $pk); + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 3.2 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_config.modules', 'modules', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to preprocess the form + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 3.2 + * @throws \Exception if there is an error loading the form. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $lang = Factory::getLanguage(); + $module = $this->getState()->get('module.name'); + $basePath = JPATH_BASE; + + $formFile = Path::clean($basePath . '/modules/' . $module . '/' . $module . '.xml'); + + // Load the core and/or local language file(s). + $lang->load($module, $basePath) + || $lang->load($module, $basePath . '/modules/' . $module); + + if (file_exists($formFile)) { + // Get the module form. + if (!$form->loadFile($formFile, false, '//config')) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + + // Load the default advanced params + Form::addFormPath(JPATH_BASE . '/components/com_config/model/form'); + $form->loadFile('modules_advanced', false); + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to get list of module positions in current template + * + * @return array + * + * @since 3.2 + */ + public function getPositions() + { + $lang = Factory::getLanguage(); + $templateName = Factory::getApplication()->getTemplate(); + + // Load templateDetails.xml file + $path = Path::clean(JPATH_BASE . '/templates/' . $templateName . '/templateDetails.xml'); + $currentTemplatePositions = array(); + + if (file_exists($path)) { + $xml = simplexml_load_file($path); + + if (isset($xml->positions[0])) { + // Load language files + $lang->load('tpl_' . $templateName . '.sys', JPATH_BASE) + || $lang->load('tpl_' . $templateName . '.sys', JPATH_BASE . '/templates/' . $templateName); + + foreach ($xml->positions[0] as $position) { + $value = (string) $position; + $text = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'TPL_' . strtoupper($templateName) . '_POSITION_' . strtoupper($value)); + + // Construct list of positions + $currentTemplatePositions[] = self::createOption($value, Text::_($text) . ' [' . $value . ']'); + } + } + } + + $templateGroups = array(); + + // Add an empty value to be able to deselect a module position + $option = self::createOption(); + $templateGroups[''] = self::createOptionGroup('', array($option)); + + $templateGroups[$templateName] = self::createOptionGroup($templateName, $currentTemplatePositions); + + // Add custom position to options + $customGroupText = Text::_('COM_MODULES_CUSTOM_POSITION'); + + $editPositions = true; + $customPositions = self::getActivePositions(0, $editPositions); + $templateGroups[$customGroupText] = self::createOptionGroup($customGroupText, $customPositions); + + return $templateGroups; + } + + /** + * Get a list of modules positions + * + * @param integer $clientId Client ID + * @param boolean $editPositions Allow to edit the positions + * + * @return array A list of positions + * + * @since 3.6.3 + */ + public static function getActivePositions($clientId, $editPositions = false) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT position') + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = ' . (int) $clientId) + ->order($db->quoteName('position')); + + $db->setQuery($query); + + try { + $positions = $db->loadColumn(); + $positions = is_array($positions) ? $positions : array(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return; + } + + // Build the list + $options = array(); + + foreach ($positions as $position) { + if (!$position && !$editPositions) { + $options[] = HTMLHelper::_('select.option', 'none', ':: ' . Text::_('JNONE') . ' ::'); + } else { + $options[] = HTMLHelper::_('select.option', $position, $position); + } + } + + return $options; + } + + /** + * Create and return a new Option + * + * @param string $value The option value [optional] + * @param string $text The option text [optional] + * + * @return object The option as an object (stdClass instance) + * + * @since 3.6.3 + */ + private static function createOption($value = '', $text = '') + { + if (empty($text)) { + $text = $value; + } + + $option = new \stdClass(); + $option->value = $value; + $option->text = $text; + + return $option; + } + + /** + * Create and return a new Option Group + * + * @param string $label Value and label for group [optional] + * @param array $options Array of options to insert into group [optional] + * + * @return array Return the new group as an array + * + * @since 3.6.3 + */ + private static function createOptionGroup($label = '', $options = array()) + { + $group = array(); + $group['value'] = $label; + $group['text'] = $label; + $group['items'] = $options; + + return $group; + } } diff --git a/components/com_config/src/Model/TemplatesModel.php b/components/com_config/src/Model/TemplatesModel.php index f9f901679f054..c45adb424d028 100644 --- a/components/com_config/src/Model/TemplatesModel.php +++ b/components/com_config/src/Model/TemplatesModel.php @@ -1,4 +1,5 @@ setState('params', ComponentHelper::getParams('com_templates')); - } - - /** - * Method to get the record form. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A JForm object on success, false on failure - * - * @since 3.2 - */ - public function getForm($data = array(), $loadData = true) - { - try - { - // Get the form. - $form = $this->loadForm('com_config.templates', 'templates', array('load_data' => $loadData)); - - $data = array(); - $this->preprocessForm($form, $data); - - // Load the data into the form - $form->bind($data); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage()); - - return false; - } - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to preprocess the form - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group Plugin group to load - * - * @return void - * - * @since 3.2 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $lang = Factory::getLanguage(); - - $template = Factory::getApplication()->getTemplate(); - - // Load the core and/or local language file(s). - $lang->load('tpl_' . $template, JPATH_BASE) - || $lang->load('tpl_' . $template, JPATH_BASE . '/templates/' . $template); - - // Look for com_config.xml, which contains fields to display - $formFile = Path::clean(JPATH_BASE . '/templates/' . $template . '/com_config.xml'); - - if (!file_exists($formFile)) - { - // If com_config.xml not found, fall back to templateDetails.xml - $formFile = Path::clean(JPATH_BASE . '/templates/' . $template . '/templateDetails.xml'); - } - - // Get the template form. - if (file_exists($formFile) && !$form->loadFile($formFile, false, '//config')) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return null + * + * @since 3.2 + */ + protected function populateState() + { + parent::populateState(); + + $this->setState('params', ComponentHelper::getParams('com_templates')); + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A JForm object on success, false on failure + * + * @since 3.2 + */ + public function getForm($data = array(), $loadData = true) + { + try { + // Get the form. + $form = $this->loadForm('com_config.templates', 'templates', array('load_data' => $loadData)); + + $data = array(); + $this->preprocessForm($form, $data); + + // Load the data into the form + $form->bind($data); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage()); + + return false; + } + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to preprocess the form + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group Plugin group to load + * + * @return void + * + * @since 3.2 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $lang = Factory::getLanguage(); + + $template = Factory::getApplication()->getTemplate(); + + // Load the core and/or local language file(s). + $lang->load('tpl_' . $template, JPATH_BASE) + || $lang->load('tpl_' . $template, JPATH_BASE . '/templates/' . $template); + + // Look for com_config.xml, which contains fields to display + $formFile = Path::clean(JPATH_BASE . '/templates/' . $template . '/com_config.xml'); + + if (!file_exists($formFile)) { + // If com_config.xml not found, fall back to templateDetails.xml + $formFile = Path::clean(JPATH_BASE . '/templates/' . $template . '/templateDetails.xml'); + } + + // Get the template form. + if (file_exists($formFile) && !$form->loadFile($formFile, false, '//config')) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } } diff --git a/components/com_config/src/Service/Router.php b/components/com_config/src/Service/Router.php index a2935a9425925..e79ba4dc65a28 100644 --- a/components/com_config/src/Service/Router.php +++ b/components/com_config/src/Service/Router.php @@ -1,4 +1,5 @@ registerView(new RouterViewConfiguration('config')); - $this->registerView(new RouterViewConfiguration('templates')); + /** + * Config Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + */ + public function __construct(SiteApplication $app, AbstractMenu $menu) + { + $this->registerView(new RouterViewConfiguration('config')); + $this->registerView(new RouterViewConfiguration('templates')); - parent::__construct($app, $menu); + parent::__construct($app, $menu); - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } } diff --git a/components/com_config/src/View/Config/HtmlView.php b/components/com_config/src/View/Config/HtmlView.php index 6cb0b6ff1636e..4848bae9530b7 100644 --- a/components/com_config/src/View/Config/HtmlView.php +++ b/components/com_config/src/View/Config/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser(); - $this->userIsSuperAdmin = $user->authorise('core.admin'); - - // Access backend com_config - $requestController = new RequestController; - - // Execute backend controller - $serviceData = json_decode($requestController->getJson(), true); - - $form = $this->getForm(); - - if ($form) - { - $form->bind($serviceData); - } - - $this->form = $form; - $this->data = $serviceData; - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 4.0.0 - */ - protected function _prepareDocument() - { - $params = Factory::getApplication()->getParams(); - - // Because the application sets a default page title, we need to get it - // right from the menu item itself - - $this->setDocumentTitle($params->get('page_title', '')); - - if ($params->get('menu-meta_description')) - { - $this->document->setDescription($params->get('menu-meta_description')); - } - - if ($params->get('robots')) - { - $this->document->setMetaData('robots', $params->get('robots')); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - $this->params = &$params; - } + /** + * The form object + * + * @var \Joomla\CMS\Form\Form + * + * @since 3.2 + */ + public $form; + + /** + * The data to be displayed in the form + * + * @var array + * + * @since 3.2 + */ + public $data; + + /** + * Is the current user a super administrator? + * + * @var boolean + * + * @since 3.2 + */ + protected $userIsSuperAdmin; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + $this->userIsSuperAdmin = $user->authorise('core.admin'); + + // Access backend com_config + $requestController = new RequestController(); + + // Execute backend controller + $serviceData = json_decode($requestController->getJson(), true); + + $form = $this->getForm(); + + if ($form) { + $form->bind($serviceData); + } + + $this->form = $form; + $this->data = $serviceData; + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 4.0.0 + */ + protected function _prepareDocument() + { + $params = Factory::getApplication()->getParams(); + + // Because the application sets a default page title, we need to get it + // right from the menu item itself + + $this->setDocumentTitle($params->get('page_title', '')); + + if ($params->get('menu-meta_description')) { + $this->document->setDescription($params->get('menu-meta_description')); + } + + if ($params->get('robots')) { + $this->document->setMetaData('robots', $params->get('robots')); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + $this->params = &$params; + } } diff --git a/components/com_config/src/View/Modules/HtmlView.php b/components/com_config/src/View/Modules/HtmlView.php index e5e3f5be92925..e8d83ce0c33f9 100644 --- a/components/com_config/src/View/Modules/HtmlView.php +++ b/components/com_config/src/View/Modules/HtmlView.php @@ -1,4 +1,5 @@ getLanguage(); - $lang->load('', JPATH_ADMINISTRATOR, $lang->getTag()); - $lang->load('com_modules', JPATH_ADMINISTRATOR, $lang->getTag()); - - // @todo Move and clean up - $module = (new \Joomla\Component\Modules\Administrator\Model\ModuleModel)->getItem(Factory::getApplication()->input->getInt('id')); - - $moduleData = $module->getProperties(); - unset($moduleData['xml']); - - /** @var \Joomla\Component\Config\Site\Model\ModulesModel $model */ - $model = $this->getModel(); - - // Need to add module name to the state of model - $model->getState()->set('module.name', $moduleData['module']); - - /** @var Form form */ - $this->form = $this->get('form'); - $this->positions = $this->get('positions'); - $this->item = $moduleData; - - if ($this->form) - { - $this->form->bind($moduleData); - } - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 4.0.0 - */ - protected function _prepareDocument() - { - // There is no menu item for this so we have to use the title from the component - $this->setDocumentTitle(Text::_('COM_CONFIG_MODULES_SETTINGS_TITLE')); - } + /** + * The module to be rendered + * + * @var array + * + * @since 3.2 + */ + public $item; + + /** + * The form object + * + * @var Form + * + * @since 3.2 + */ + public $form; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $lang = Factory::getApplication()->getLanguage(); + $lang->load('', JPATH_ADMINISTRATOR, $lang->getTag()); + $lang->load('com_modules', JPATH_ADMINISTRATOR, $lang->getTag()); + + // @todo Move and clean up + $module = (new \Joomla\Component\Modules\Administrator\Model\ModuleModel())->getItem(Factory::getApplication()->input->getInt('id')); + + $moduleData = $module->getProperties(); + unset($moduleData['xml']); + + /** @var \Joomla\Component\Config\Site\Model\ModulesModel $model */ + $model = $this->getModel(); + + // Need to add module name to the state of model + $model->getState()->set('module.name', $moduleData['module']); + + /** @var Form form */ + $this->form = $this->get('form'); + $this->positions = $this->get('positions'); + $this->item = $moduleData; + + if ($this->form) { + $this->form->bind($moduleData); + } + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 4.0.0 + */ + protected function _prepareDocument() + { + // There is no menu item for this so we have to use the title from the component + $this->setDocumentTitle(Text::_('COM_CONFIG_MODULES_SETTINGS_TITLE')); + } } diff --git a/components/com_config/src/View/Templates/HtmlView.php b/components/com_config/src/View/Templates/HtmlView.php index 39478d9b5d734..a7e4f4282d38e 100644 --- a/components/com_config/src/View/Templates/HtmlView.php +++ b/components/com_config/src/View/Templates/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser(); - $this->userIsSuperAdmin = $user->authorise('core.admin'); - - $app = Factory::getApplication(); - - $app->input->set('id', $app->getTemplate(true)->id); - - /** @var MVCFactory $factory */ - $factory = $app->bootComponent('com_templates')->getMVCFactory(); - - $view = $factory->createView('Style', 'Administrator', 'Json'); - $view->setModel($factory->createModel('Style', 'Administrator'), true); - - $view->document = $this->document; - - $json = $view->display(); - - // Execute backend controller - $serviceData = json_decode($json, true); - - // Access backend com_config - $requestController = new RequestController; - - // Execute backend controller - $configData = json_decode($requestController->getJson(), true); - - $data = array_merge($configData, $serviceData); - - /** @var Form $form */ - $form = $this->getForm(); - - if ($form) - { - $form->bind($data); - } - - $this->form = $form; - - $this->data = $serviceData; - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 4.0.0 - */ - protected function _prepareDocument() - { - $params = Factory::getApplication()->getParams(); - - // Because the application sets a default page title, we need to get it - // right from the menu item itself - $this->setDocumentTitle($params->get('page_title', '')); - - if ($params->get('menu-meta_description')) - { - $this->document->setDescription($params->get('menu-meta_description')); - } - - if ($params->get('robots')) - { - $this->document->setMetaData('robots', $params->get('robots')); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - $this->params = &$params; - } + /** + * The data to be displayed in the form + * + * @var array + * + * @since 3.2 + */ + public $item; + + /** + * The form object + * + * @var Form + * + * @since 3.2 + */ + public $form; + + /** + * Is the current user a super administrator? + * + * @var boolean + * + * @since 3.2 + */ + protected $userIsSuperAdmin; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * Method to render the view. + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + $this->userIsSuperAdmin = $user->authorise('core.admin'); + + $app = Factory::getApplication(); + + $app->input->set('id', $app->getTemplate(true)->id); + + /** @var MVCFactory $factory */ + $factory = $app->bootComponent('com_templates')->getMVCFactory(); + + $view = $factory->createView('Style', 'Administrator', 'Json'); + $view->setModel($factory->createModel('Style', 'Administrator'), true); + + $view->document = $this->document; + + $json = $view->display(); + + // Execute backend controller + $serviceData = json_decode($json, true); + + // Access backend com_config + $requestController = new RequestController(); + + // Execute backend controller + $configData = json_decode($requestController->getJson(), true); + + $data = array_merge($configData, $serviceData); + + /** @var Form $form */ + $form = $this->getForm(); + + if ($form) { + $form->bind($data); + } + + $this->form = $form; + + $this->data = $serviceData; + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 4.0.0 + */ + protected function _prepareDocument() + { + $params = Factory::getApplication()->getParams(); + + // Because the application sets a default page title, we need to get it + // right from the menu item itself + $this->setDocumentTitle($params->get('page_title', '')); + + if ($params->get('menu-meta_description')) { + $this->document->setDescription($params->get('menu-meta_description')); + } + + if ($params->get('robots')) { + $this->document->setMetaData('robots', $params->get('robots')); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + $this->params = &$params; + } } diff --git a/components/com_config/tmpl/config/default.php b/components/com_config/tmpl/config/default.php index 95398edb0da8b..f7f88c3e1e1c0 100644 --- a/components/com_config/tmpl/config/default.php +++ b/components/com_config/tmpl/config/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_config.config') - ->useScript('inlinehelp'); + ->useScript('form.validate') + ->useScript('com_config.config') + ->useScript('inlinehelp'); ?> params->get('show_page_heading')) : ?> - +
    -
    - -
    - - loadTemplate('site'); ?> - loadTemplate('seo'); ?> - loadTemplate('metadata'); ?> - - - - -
    - - -
    +
    + +
    + + loadTemplate('site'); ?> + loadTemplate('seo'); ?> + loadTemplate('metadata'); ?> + + + + +
    + + +
    diff --git a/components/com_config/tmpl/config/default_metadata.php b/components/com_config/tmpl/config/default_metadata.php index 4daf35c503e09..f361d59281319 100644 --- a/components/com_config/tmpl/config/default_metadata.php +++ b/components/com_config/tmpl/config/default_metadata.php @@ -1,4 +1,5 @@
    - + - form->getFieldset('metadata') as $field) : ?> -
    - label; ?> - input; ?> - description): ?> -
    - description) ?> -
    - -
    - + form->getFieldset('metadata') as $field) : ?> +
    + label; ?> + input; ?> + description) : ?> +
    + description) ?> +
    + +
    +
    diff --git a/components/com_config/tmpl/config/default_seo.php b/components/com_config/tmpl/config/default_seo.php index 0c26e03d712d3..911390985f711 100644 --- a/components/com_config/tmpl/config/default_seo.php +++ b/components/com_config/tmpl/config/default_seo.php @@ -1,4 +1,5 @@
    - + - form->getFieldset('seo') as $field) : ?> -
    - label; ?> - input; ?> - description): ?> -
    - description) ?> -
    - -
    - + form->getFieldset('seo') as $field) : ?> +
    + label; ?> + input; ?> + description) : ?> +
    + description) ?> +
    + +
    +
    diff --git a/components/com_config/tmpl/config/default_site.php b/components/com_config/tmpl/config/default_site.php index 1e13994ea8c9e..571b7de73cb3b 100644 --- a/components/com_config/tmpl/config/default_site.php +++ b/components/com_config/tmpl/config/default_site.php @@ -1,4 +1,5 @@
    - + - form->getFieldset('site') as $field) : ?> -
    - label; ?> - input; ?> - description): ?> -
    - description) ?> -
    - -
    - + form->getFieldset('site') as $field) : ?> +
    + label; ?> + input; ?> + description) : ?> +
    + description) ?> +
    + +
    +
    diff --git a/components/com_config/tmpl/modules/default.php b/components/com_config/tmpl/modules/default.php index fe76134ce3394..6fc48f5163366 100644 --- a/components/com_config/tmpl/modules/default.php +++ b/components/com_config/tmpl/modules/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_config.modules'); + ->useScript('form.validate') + ->useScript('com_config.modules'); $editorText = false; $moduleXml = JPATH_SITE . '/modules/' . $this->item['module'] . '/' . $this->item['module'] . '.xml'; -if (File::exists($moduleXml)) -{ - $xml = simplexml_load_file($moduleXml); +if (File::exists($moduleXml)) { + $xml = simplexml_load_file($moduleXml); - if (isset($xml->customContent)) - { - $editorText = true; - } + if (isset($xml->customContent)) { + $editorText = true; + } } // If multi-language site, make language read-only -if (Multilanguage::isEnabled()) -{ - $this->form->setFieldAttribute('language', 'readonly', 'true'); +if (Multilanguage::isEnabled()) { + $this->form->setFieldAttribute('language', 'readonly', 'true'); } ?>
    -
    -
    - - -
    - - item['title']; ?> -    - - item['module']; ?> -
    -
    - -
    -
    - -
    -
    - form->getLabel('title'); ?> -
    -
    - form->getInput('title'); ?> -
    -
    -
    -
    - form->getLabel('showtitle'); ?> -
    -
    - form->getInput('showtitle'); ?> -
    -
    -
    -
    - form->getLabel('position'); ?> -
    -
    - form->getInput('position'); ?> -
    -
    - -
    - - authorise('core.edit.state', 'com_modules.module.' . $this->item['id'])) : ?> -
    -
    - form->getLabel('published'); ?> -
    -
    - form->getInput('published'); ?> -
    -
    - - -
    -
    - form->getLabel('publish_up'); ?> -
    -
    - form->getInput('publish_up'); ?> -
    -
    -
    -
    - form->getLabel('publish_down'); ?> -
    -
    - form->getInput('publish_down'); ?> -
    -
    - -
    -
    - form->getLabel('access'); ?> -
    -
    - form->getInput('access'); ?> -
    -
    -
    -
    - form->getLabel('ordering'); ?> -
    -
    - form->getInput('ordering'); ?> -
    -
    - - -
    -
    - form->getLabel('language'); ?> -
    -
    - form->getInput('language'); ?> -
    -
    - - -
    -
    - form->getLabel('note'); ?> -
    -
    - form->getInput('note'); ?> -
    -
    - -
    - -
    - loadTemplate('options'); ?> -
    - - -
    - form->getInput('content'); ?> -
    - -
    - - - - - -
    -
    - - - -
    -
    -
    +
    +
    + + +
    + + item['title']; ?> +    + + item['module']; ?> +
    +
    + +
    +
    + +
    +
    + form->getLabel('title'); ?> +
    +
    + form->getInput('title'); ?> +
    +
    +
    +
    + form->getLabel('showtitle'); ?> +
    +
    + form->getInput('showtitle'); ?> +
    +
    +
    +
    + form->getLabel('position'); ?> +
    +
    + form->getInput('position'); ?> +
    +
    + +
    + + authorise('core.edit.state', 'com_modules.module.' . $this->item['id'])) : ?> +
    +
    + form->getLabel('published'); ?> +
    +
    + form->getInput('published'); ?> +
    +
    + + +
    +
    + form->getLabel('publish_up'); ?> +
    +
    + form->getInput('publish_up'); ?> +
    +
    +
    +
    + form->getLabel('publish_down'); ?> +
    +
    + form->getInput('publish_down'); ?> +
    +
    + +
    +
    + form->getLabel('access'); ?> +
    +
    + form->getInput('access'); ?> +
    +
    +
    +
    + form->getLabel('ordering'); ?> +
    +
    + form->getInput('ordering'); ?> +
    +
    + + +
    +
    + form->getLabel('language'); ?> +
    +
    + form->getInput('language'); ?> +
    +
    + + +
    +
    + form->getLabel('note'); ?> +
    +
    + form->getInput('note'); ?> +
    +
    + +
    + +
    + loadTemplate('options'); ?> +
    + + +
    + form->getInput('content'); ?> +
    + +
    + + + + + +
    +
    + + + +
    +
    +
    diff --git a/components/com_config/tmpl/modules/default_options.php b/components/com_config/tmpl/modules/default_options.php index 28c4c73007070..e22325152df33 100644 --- a/components/com_config/tmpl/modules/default_options.php +++ b/components/com_config/tmpl/modules/default_options.php @@ -1,4 +1,5 @@ $fieldSet) : - -$label = !empty($fieldSet->label) ? $fieldSet->label : 'COM_MODULES_' . strtoupper($name) . '_FIELDSET_LABEL'; -$class = isset($fieldSet->class) && !empty($fieldSet->class) ? $fieldSet->class : ''; + $label = !empty($fieldSet->label) ? $fieldSet->label : 'COM_MODULES_' . strtoupper($name) . '_FIELDSET_LABEL'; + $class = isset($fieldSet->class) && !empty($fieldSet->class) ? $fieldSet->class : ''; -if (isset($fieldSet->description) && trim($fieldSet->description)) : -echo '

    ' . $this->escape(Text::_($fieldSet->description)) . '

    '; -endif; -?> - + if (isset($fieldSet->description) && trim($fieldSet->description)) : + echo '

    ' . $this->escape(Text::_($fieldSet->description)) . '

    '; + endif; + ?> + - + diff --git a/components/com_config/tmpl/templates/default.php b/components/com_config/tmpl/templates/default.php index 2c0a86135de7e..c3531496379c3 100644 --- a/components/com_config/tmpl/templates/default.php +++ b/components/com_config/tmpl/templates/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_config.templates'); + ->useScript('form.validate') + ->useScript('com_config.templates'); ?> params->get('show_page_heading')) : ?> - +
    -
    -
    -
    - loadTemplate('options'); ?> -
    -
    -
    - - - - -
    - - +
    +
    +
    + loadTemplate('options'); ?> +
    +
    +
    + + + + +
    + +
    diff --git a/components/com_config/tmpl/templates/default_options.php b/components/com_config/tmpl/templates/default_options.php index ca9f12253870a..a8c8be624b923 100644 --- a/components/com_config/tmpl/templates/default_options.php +++ b/components/com_config/tmpl/templates/default_options.php @@ -1,4 +1,5 @@ form->renderFieldset('com_config'); -} -else -{ - // Fall-back to display all in params - foreach ($fieldSets as $name => $fieldSet) - { - $label = !empty($fieldSet->label) ? $fieldSet->label : 'COM_CONFIG_' . $name . '_FIELDSET_LABEL'; - - if (isset($fieldSet->description) && trim($fieldSet->description)) - { - echo '

    ' . $this->escape(Text::_($fieldSet->description)) . '

    '; - } - - echo $this->form->renderFieldset($name); - - } +if (!empty($fieldSets['com_config'])) { + echo $this->form->renderFieldset('com_config'); +} else { + // Fall-back to display all in params + foreach ($fieldSets as $name => $fieldSet) { + $label = !empty($fieldSet->label) ? $fieldSet->label : 'COM_CONFIG_' . $name . '_FIELDSET_LABEL'; + + if (isset($fieldSet->description) && trim($fieldSet->description)) { + echo '

    ' . $this->escape(Text::_($fieldSet->description)) . '

    '; + } + + echo $this->form->renderFieldset($name); + } } diff --git a/components/com_contact/helpers/route.php b/components/com_contact/helpers/route.php index 797298b1193c4..5b67665b640ff 100644 --- a/components/com_contact/helpers/route.php +++ b/components/com_contact/helpers/route.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; -if (!array_key_exists('field', $displayData)) -{ - return; +if (!array_key_exists('field', $displayData)) { + return; } $field = $displayData['field']; @@ -22,26 +23,24 @@ $showLabel = $field->params->get('showlabel'); $labelClass = $field->params->get('label_render_class'); -if ($field->context == 'com_contact.mail') -{ - // Prepare the value for the contact form mail - $value = html_entity_decode($value); +if ($field->context == 'com_contact.mail') { + // Prepare the value for the contact form mail + $value = html_entity_decode($value); - echo ($showLabel ? $label . ': ' : '') . $value . "\r\n"; - return; + echo ($showLabel ? $label . ': ' : '') . $value . "\r\n"; + return; } -if (!strlen($value)) -{ - return; +if (!strlen($value)) { + return; } ?>
    - - : - + + : +
    - +
    diff --git a/components/com_contact/layouts/fields/render.php b/components/com_contact/layouts/fields/render.php index eb0ab9ec70a4c..1f4821b5d5c74 100644 --- a/components/com_contact/layouts/fields/render.php +++ b/components/com_contact/layouts/fields/render.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\Component\Fields\Administrator\Helper\FieldsHelper; // Check if we have all the data -if (!array_key_exists('item', $displayData) || !array_key_exists('context', $displayData)) -{ - return; +if (!array_key_exists('item', $displayData) || !array_key_exists('context', $displayData)) { + return; } // Setting up for display $item = $displayData['item']; -if (!$item) -{ - return; +if (!$item) { + return; } $context = $displayData['context']; -if (!$context) -{ - return; +if (!$context) { + return; } $parts = explode('.', $context); $component = $parts[0]; $fields = null; -if (array_key_exists('fields', $displayData)) -{ - $fields = $displayData['fields']; -} -else -{ - $fields = $item->jcfields ?: FieldsHelper::getFields($context, $item, true); +if (array_key_exists('fields', $displayData)) { + $fields = $displayData['fields']; +} else { + $fields = $item->jcfields ?: FieldsHelper::getFields($context, $item, true); } -if (!$fields) -{ - return; +if (!$fields) { + return; } // Check if we have mail context in first element $isMail = (reset($fields)->context == 'com_contact.mail'); -if (!$isMail) -{ - // Print the container tag - echo '
    '; +if (!$isMail) { + // Print the container tag + echo '
    '; } // Loop through the fields and print them -foreach ($fields as $field) -{ - // If the value is empty do nothing - if (!strlen($field->value) && !$isMail) - { - continue; - } - - $layout = $field->params->get('layout', 'render'); - echo FieldsHelper::render($context, 'field.' . $layout, array('field' => $field)); -} +foreach ($fields as $field) { + // If the value is empty do nothing + if (!strlen($field->value) && !$isMail) { + continue; + } -if (!$isMail) -{ - // Close the container - echo '
    '; + $layout = $field->params->get('layout', 'render'); + echo FieldsHelper::render($context, 'field.' . $layout, array('field' => $field)); } +if (!$isMail) { + // Close the container + echo '
    '; +} diff --git a/components/com_contact/src/Controller/ContactController.php b/components/com_contact/src/Controller/ContactController.php index 5b1ee4dcbb45f..2d69b70b5f68a 100644 --- a/components/com_contact/src/Controller/ContactController.php +++ b/components/com_contact/src/Controller/ContactController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, array('ignore_request' => false)); - } - - /** - * Method to submit the contact form and send an email. - * - * @return boolean True on success sending the email. False on failure. - * - * @since 1.5.19 - */ - public function submit() - { - // Check for request forgeries. - $this->checkToken(); - - $app = $this->app; - $model = $this->getModel('contact'); - $stub = $this->input->getString('id'); - $id = (int) $stub; - - // Get the data from POST - $data = $this->input->post->get('jform', array(), 'array'); - - // Get item - $model->setState('filter.published', 1); - $contact = $model->getItem($id); - - if ($contact === false) - { - $this->setMessage($model->getError(), 'error'); - - return false; - } - - // Get item params, take menu parameters into account if necessary - $active = $app->getMenu()->getActive(); - $stateParams = clone $model->getState()->get('params'); - - // If the current view is the active item and a contact view for this contact, then the menu item params take priority - if ($active && strpos($active->link, 'view=contact') && strpos($active->link, '&id=' . (int) $contact->id)) - { - // $item->params are the contact params, $temp are the menu item params - // Merge so that the menu item params take priority - $contact->params->merge($stateParams); - } - else - { - // Current view is not a single contact, so the contact params take priority here - $stateParams->merge($contact->params); - $contact->params = $stateParams; - } - - // Check if the contact form is enabled - if (!$contact->params->get('show_email_form')) - { - $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); - - return false; - } - - // Check for a valid session cookie - if ($contact->params->get('validate_session', 0)) - { - if (Factory::getSession()->getState() !== 'active') - { - $this->app->enqueueMessage(Text::_('JLIB_ENVIRONMENT_SESSION_INVALID'), 'warning'); - - // Save the data in the session. - $this->app->setUserState('com_contact.contact.data', $data); - - // Redirect back to the contact form. - $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); - - return false; - } - } - - // Contact plugins - PluginHelper::importPlugin('contact'); - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - if (!$model->validate($form, $data)) - { - $errors = $model->getErrors(); - - foreach ($errors as $error) - { - $errorMessage = $error; - - if ($error instanceof \Exception) - { - $errorMessage = $error->getMessage(); - } - - $app->enqueueMessage($errorMessage, 'error'); - } - - $app->setUserState('com_contact.contact.data', $data); - - $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); - - return false; - } - - // Validation succeeded, continue with custom handlers - $results = $this->app->triggerEvent('onValidateContact', array(&$contact, &$data)); - - foreach ($results as $result) - { - if ($result instanceof \Exception) - { - return false; - } - } - - // Passed Validation: Process the contact plugins to integrate with other applications - $this->app->triggerEvent('onSubmitContact', array(&$contact, &$data)); - - // Send the email - $sent = false; - - if (!$contact->params->get('custom_reply')) - { - $sent = $this->_sendEmail($data, $contact, $contact->params->get('show_email_copy', 0)); - } - - $msg = ''; - - // Set the success message if it was a success - if ($sent) - { - $msg = Text::_('COM_CONTACT_EMAIL_THANKS'); - } - - // Flush the data from the session - $this->app->setUserState('com_contact.contact.data', null); - - // Redirect if it is set in the parameters, otherwise redirect back to where we came from - if ($contact->params->get('redirect')) - { - $this->setRedirect($contact->params->get('redirect'), $msg); - } - else - { - $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false), $msg); - } - - return true; - } - - /** - * Method to get a model object, loading it if required. - * - * @param array $data The data to send in the email. - * @param \stdClass $contact The user information to send the email to - * @param boolean $emailCopyToSender True to send a copy of the email to the user. - * - * @return boolean True on success sending the email, false on failure. - * - * @since 1.6.4 - */ - private function _sendEmail($data, $contact, $emailCopyToSender) - { - $app = $this->app; - - if ($contact->email_to == '' && $contact->user_id != 0) - { - $contact_user = User::getInstance($contact->user_id); - $contact->email_to = $contact_user->get('email'); - } - - $templateData = [ - 'sitename' => $app->get('sitename'), - 'name' => $data['contact_name'], - 'contactname' => $contact->name, - 'email' => PunycodeHelper::emailToPunycode($data['contact_email']), - 'subject' => $data['contact_subject'], - 'body' => stripslashes($data['contact_message']), - 'url' => Uri::base(), - 'customfields' => '' - ]; - - // Load the custom fields - if (!empty($data['com_fields']) && $fields = FieldsHelper::getFields('com_contact.mail', $contact, true, $data['com_fields'])) - { - $output = FieldsHelper::render( - 'com_contact.mail', - 'fields.render', - array( - 'context' => 'com_contact.mail', - 'item' => $contact, - 'fields' => $fields, - ) - ); - - if ($output) - { - $templateData['customfields'] = $output; - } - } - - try - { - $mailer = new MailTemplate('com_contact.mail', $app->getLanguage()->getTag()); - $mailer->addRecipient($contact->email_to); - $mailer->setReplyTo($templateData['email'], $templateData['name']); - $mailer->addTemplateData($templateData); - $sent = $mailer->send(); - - // If we are supposed to copy the sender, do so. - if ($emailCopyToSender == true && !empty($data['contact_email_copy'])) - { - $mailer = new MailTemplate('com_contact.mail.copy', $app->getLanguage()->getTag()); - $mailer->addRecipient($templateData['email']); - $mailer->setReplyTo($templateData['email'], $templateData['name']); - $mailer->addTemplateData($templateData); - $sent = $mailer->send(); - } - } - catch (MailDisabledException | phpMailerException $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $sent = false; - } - catch (\RuntimeException $exception) - { - $this->app->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $sent = false; - } - } - - return $sent; - } - - /** - * Method override to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowAdd($data = array()) - { - if ($categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('catid'), 'int')) - { - $user = $this->app->getIdentity(); - - // If the category has been passed in the data or URL check it. - return $user->authorise('core.create', 'com_contact.category.' . $categoryId); - } - - // In the absence of better information, revert to the component permissions. - return parent::allowAdd(); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key; default is id. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - - if (!$recordId) - { - return false; - } - - // Need to do a lookup from the model. - $record = $this->getModel()->getItem($recordId); - $categoryId = (int) $record->catid; - - if ($categoryId) - { - $user = $this->app->getIdentity(); - - // The category has been set. Check the category permissions. - if ($user->authorise('core.edit', $this->option . '.category.' . $categoryId)) - { - return true; - } - - // Fallback on edit.own. - if ($user->authorise('core.edit.own', $this->option . '.category.' . $categoryId)) - { - return ($record->created_by === $user->id); - } - - return false; - } - - // Since there is no asset tracking, revert to the component permissions. - return parent::allowEdit($data, $key); - } - - /** - * Method to cancel an edit. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 4.0.0 - */ - public function cancel($key = null) - { - $result = parent::cancel($key); - - $this->setRedirect(Route::_($this->getReturnPage(), false)); - - return $result; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = 0, $urlVar = 'id') - { - // Need to override the parent method completely. - $tmpl = $this->input->get('tmpl'); - - $append = ''; - - // Setup redirect info. - if ($tmpl) - { - $append .= '&tmpl=' . $tmpl; - } - - $append .= '&layout=edit'; - - $append .= '&' . $urlVar . '=' . (int) $recordId; - - $itemId = $this->input->getInt('Itemid'); - $return = $this->getReturnPage(); - $catId = $this->input->getInt('catid'); - - if ($itemId) - { - $append .= '&Itemid=' . $itemId; - } - - if ($catId) - { - $append .= '&catid=' . $catId; - } - - if ($return) - { - $append .= '&return=' . base64_encode($return); - } - - return $append; - } - - /** - * Get the return URL. - * - * If a "return" variable has been passed in the request - * - * @return string The return URL. - * - * @since 4.0.0 - */ - protected function getReturnPage() - { - $return = $this->input->get('return', null, 'base64'); - - if (empty($return) || !Uri::isInternal(base64_decode($return))) - { - return Uri::base(); - } - - return base64_decode($return); - } + use VersionableControllerTrait; + + /** + * The URL view item variable. + * + * @var string + * @since 4.0.0 + */ + protected $view_item = 'form'; + + /** + * The URL view list variable. + * + * @var string + * @since 4.0.0 + */ + protected $view_list = 'categories'; + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6.4 + */ + public function getModel($name = 'form', $prefix = '', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, array('ignore_request' => false)); + } + + /** + * Method to submit the contact form and send an email. + * + * @return boolean True on success sending the email. False on failure. + * + * @since 1.5.19 + */ + public function submit() + { + // Check for request forgeries. + $this->checkToken(); + + $app = $this->app; + $model = $this->getModel('contact'); + $stub = $this->input->getString('id'); + $id = (int) $stub; + + // Get the data from POST + $data = $this->input->post->get('jform', array(), 'array'); + + // Get item + $model->setState('filter.published', 1); + $contact = $model->getItem($id); + + if ($contact === false) { + $this->setMessage($model->getError(), 'error'); + + return false; + } + + // Get item params, take menu parameters into account if necessary + $active = $app->getMenu()->getActive(); + $stateParams = clone $model->getState()->get('params'); + + // If the current view is the active item and a contact view for this contact, then the menu item params take priority + if ($active && strpos($active->link, 'view=contact') && strpos($active->link, '&id=' . (int) $contact->id)) { + // $item->params are the contact params, $temp are the menu item params + // Merge so that the menu item params take priority + $contact->params->merge($stateParams); + } else { + // Current view is not a single contact, so the contact params take priority here + $stateParams->merge($contact->params); + $contact->params = $stateParams; + } + + // Check if the contact form is enabled + if (!$contact->params->get('show_email_form')) { + $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); + + return false; + } + + // Check for a valid session cookie + if ($contact->params->get('validate_session', 0)) { + if (Factory::getSession()->getState() !== 'active') { + $this->app->enqueueMessage(Text::_('JLIB_ENVIRONMENT_SESSION_INVALID'), 'warning'); + + // Save the data in the session. + $this->app->setUserState('com_contact.contact.data', $data); + + // Redirect back to the contact form. + $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); + + return false; + } + } + + // Contact plugins + PluginHelper::importPlugin('contact'); + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + if (!$model->validate($form, $data)) { + $errors = $model->getErrors(); + + foreach ($errors as $error) { + $errorMessage = $error; + + if ($error instanceof \Exception) { + $errorMessage = $error->getMessage(); + } + + $app->enqueueMessage($errorMessage, 'error'); + } + + $app->setUserState('com_contact.contact.data', $data); + + $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); + + return false; + } + + // Validation succeeded, continue with custom handlers + $results = $this->app->triggerEvent('onValidateContact', array(&$contact, &$data)); + + foreach ($results as $result) { + if ($result instanceof \Exception) { + return false; + } + } + + // Passed Validation: Process the contact plugins to integrate with other applications + $this->app->triggerEvent('onSubmitContact', array(&$contact, &$data)); + + // Send the email + $sent = false; + + if (!$contact->params->get('custom_reply')) { + $sent = $this->_sendEmail($data, $contact, $contact->params->get('show_email_copy', 0)); + } + + $msg = ''; + + // Set the success message if it was a success + if ($sent) { + $msg = Text::_('COM_CONTACT_EMAIL_THANKS'); + } + + // Flush the data from the session + $this->app->setUserState('com_contact.contact.data', null); + + // Redirect if it is set in the parameters, otherwise redirect back to where we came from + if ($contact->params->get('redirect')) { + $this->setRedirect($contact->params->get('redirect'), $msg); + } else { + $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false), $msg); + } + + return true; + } + + /** + * Method to get a model object, loading it if required. + * + * @param array $data The data to send in the email. + * @param \stdClass $contact The user information to send the email to + * @param boolean $emailCopyToSender True to send a copy of the email to the user. + * + * @return boolean True on success sending the email, false on failure. + * + * @since 1.6.4 + */ + private function _sendEmail($data, $contact, $emailCopyToSender) + { + $app = $this->app; + + if ($contact->email_to == '' && $contact->user_id != 0) { + $contact_user = User::getInstance($contact->user_id); + $contact->email_to = $contact_user->get('email'); + } + + $templateData = [ + 'sitename' => $app->get('sitename'), + 'name' => $data['contact_name'], + 'contactname' => $contact->name, + 'email' => PunycodeHelper::emailToPunycode($data['contact_email']), + 'subject' => $data['contact_subject'], + 'body' => stripslashes($data['contact_message']), + 'url' => Uri::base(), + 'customfields' => '' + ]; + + // Load the custom fields + if (!empty($data['com_fields']) && $fields = FieldsHelper::getFields('com_contact.mail', $contact, true, $data['com_fields'])) { + $output = FieldsHelper::render( + 'com_contact.mail', + 'fields.render', + array( + 'context' => 'com_contact.mail', + 'item' => $contact, + 'fields' => $fields, + ) + ); + + if ($output) { + $templateData['customfields'] = $output; + } + } + + try { + $mailer = new MailTemplate('com_contact.mail', $app->getLanguage()->getTag()); + $mailer->addRecipient($contact->email_to); + $mailer->setReplyTo($templateData['email'], $templateData['name']); + $mailer->addTemplateData($templateData); + $sent = $mailer->send(); + + // If we are supposed to copy the sender, do so. + if ($emailCopyToSender == true && !empty($data['contact_email_copy'])) { + $mailer = new MailTemplate('com_contact.mail.copy', $app->getLanguage()->getTag()); + $mailer->addRecipient($templateData['email']); + $mailer->setReplyTo($templateData['email'], $templateData['name']); + $mailer->addTemplateData($templateData); + $sent = $mailer->send(); + } + } catch (MailDisabledException | phpMailerException $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $sent = false; + } catch (\RuntimeException $exception) { + $this->app->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $sent = false; + } + } + + return $sent; + } + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowAdd($data = array()) + { + if ($categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('catid'), 'int')) { + $user = $this->app->getIdentity(); + + // If the category has been passed in the data or URL check it. + return $user->authorise('core.create', 'com_contact.category.' . $categoryId); + } + + // In the absence of better information, revert to the component permissions. + return parent::allowAdd(); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key; default is id. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + + if (!$recordId) { + return false; + } + + // Need to do a lookup from the model. + $record = $this->getModel()->getItem($recordId); + $categoryId = (int) $record->catid; + + if ($categoryId) { + $user = $this->app->getIdentity(); + + // The category has been set. Check the category permissions. + if ($user->authorise('core.edit', $this->option . '.category.' . $categoryId)) { + return true; + } + + // Fallback on edit.own. + if ($user->authorise('core.edit.own', $this->option . '.category.' . $categoryId)) { + return ($record->created_by === $user->id); + } + + return false; + } + + // Since there is no asset tracking, revert to the component permissions. + return parent::allowEdit($data, $key); + } + + /** + * Method to cancel an edit. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 4.0.0 + */ + public function cancel($key = null) + { + $result = parent::cancel($key); + + $this->setRedirect(Route::_($this->getReturnPage(), false)); + + return $result; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = 0, $urlVar = 'id') + { + // Need to override the parent method completely. + $tmpl = $this->input->get('tmpl'); + + $append = ''; + + // Setup redirect info. + if ($tmpl) { + $append .= '&tmpl=' . $tmpl; + } + + $append .= '&layout=edit'; + + $append .= '&' . $urlVar . '=' . (int) $recordId; + + $itemId = $this->input->getInt('Itemid'); + $return = $this->getReturnPage(); + $catId = $this->input->getInt('catid'); + + if ($itemId) { + $append .= '&Itemid=' . $itemId; + } + + if ($catId) { + $append .= '&catid=' . $catId; + } + + if ($return) { + $append .= '&return=' . base64_encode($return); + } + + return $append; + } + + /** + * Get the return URL. + * + * If a "return" variable has been passed in the request + * + * @return string The return URL. + * + * @since 4.0.0 + */ + protected function getReturnPage() + { + $return = $this->input->get('return', null, 'base64'); + + if (empty($return) || !Uri::isInternal(base64_decode($return))) { + return Uri::base(); + } + + return base64_decode($return); + } } diff --git a/components/com_contact/src/Controller/DisplayController.php b/components/com_contact/src/Controller/DisplayController.php index 477d27adfaaca..42671d27d3f26 100644 --- a/components/com_contact/src/Controller/DisplayController.php +++ b/components/com_contact/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input; + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object for the request + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + // Contact frontpage Editor contacts proxying. + $input = Factory::getApplication()->input; - if ($input->get('view') === 'contacts' && $input->get('layout') === 'modal') - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - } + if ($input->get('view') === 'contacts' && $input->get('layout') === 'modal') { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + } - parent::__construct($config, $factory, $app, $input); - } + parent::__construct($config, $factory, $app, $input); + } - /** - * Method to display a view. - * - * @param boolean $cachable If true, the view output will be cached - * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. - * - * @return DisplayController This object to support chaining. - * - * @since 1.5 - */ - public function display($cachable = false, $urlparams = array()) - { - if ($this->app->getUserState('com_contact.contact.data') === null) - { - $cachable = true; - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return DisplayController This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + if ($this->app->getUserState('com_contact.contact.data') === null) { + $cachable = true; + } - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'categories'); - $this->input->set('view', $vName); + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'categories'); + $this->input->set('view', $vName); - if ($this->app->getIdentity()->get('id')) - { - $cachable = false; - } + if ($this->app->getIdentity()->get('id')) { + $cachable = false; + } - $safeurlparams = array('catid' => 'INT', 'id' => 'INT', 'cid' => 'ARRAY', 'year' => 'INT', 'month' => 'INT', - 'limit' => 'UINT', 'limitstart' => 'UINT', 'showall' => 'INT', 'return' => 'BASE64', 'filter' => 'STRING', - 'filter_order' => 'CMD', 'filter_order_Dir' => 'CMD', 'filter-search' => 'STRING', 'print' => 'BOOLEAN', - 'lang' => 'CMD'); + $safeurlparams = array('catid' => 'INT', 'id' => 'INT', 'cid' => 'ARRAY', 'year' => 'INT', 'month' => 'INT', + 'limit' => 'UINT', 'limitstart' => 'UINT', 'showall' => 'INT', 'return' => 'BASE64', 'filter' => 'STRING', + 'filter_order' => 'CMD', 'filter_order_Dir' => 'CMD', 'filter-search' => 'STRING', 'print' => 'BOOLEAN', + 'lang' => 'CMD'); - parent::display($cachable, $safeurlparams); + parent::display($cachable, $safeurlparams); - return $this; - } + return $this; + } } diff --git a/components/com_contact/src/Dispatcher/Dispatcher.php b/components/com_contact/src/Dispatcher/Dispatcher.php index 537798c969180..6b38fa383159b 100644 --- a/components/com_contact/src/Dispatcher/Dispatcher.php +++ b/components/com_contact/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->get('view') === 'contacts' && $this->input->get('layout') === 'modal') - { - if (!$this->app->getIdentity()->authorise('core.create', 'com_contact')) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'warning'); - - return; - } - - $this->app->getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); - } - - parent::dispatch(); - } + /** + * Dispatch a controller task. Redirecting the user if appropriate. + * + * @return void + * + * @since 4.0.0 + */ + public function dispatch() + { + if ($this->input->get('view') === 'contacts' && $this->input->get('layout') === 'modal') { + if (!$this->app->getIdentity()->authorise('core.create', 'com_contact')) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'warning'); + + return; + } + + $this->app->getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); + } + + parent::dispatch(); + } } diff --git a/components/com_contact/src/Helper/AssociationHelper.php b/components/com_contact/src/Helper/AssociationHelper.php index 14e014ee7f844..6c98685fc7b85 100644 --- a/components/com_contact/src/Helper/AssociationHelper.php +++ b/components/com_contact/src/Helper/AssociationHelper.php @@ -1,4 +1,5 @@ input; - $view = $view ?? $jinput->get('view'); - $id = empty($id) ? $jinput->getInt('id') : $id; - - if ($view === 'contact') - { - if ($id) - { - $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $id); - - $return = array(); - - foreach ($associations as $tag => $item) - { - $return[$tag] = RouteHelper::getContactRoute($item->id, (int) $item->catid, $item->language); - } - - return $return; - } - } - - if ($view === 'category' || $view === 'categories') - { - return self::getCategoryAssociations($id, 'com_contact'); - } - - return array(); - - } + /** + * Method to get the associations for a given item + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 3.0 + */ + public static function getAssociations($id = 0, $view = null) + { + $jinput = Factory::getApplication()->input; + $view = $view ?? $jinput->get('view'); + $id = empty($id) ? $jinput->getInt('id') : $id; + + if ($view === 'contact') { + if ($id) { + $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $id); + + $return = array(); + + foreach ($associations as $tag => $item) { + $return[$tag] = RouteHelper::getContactRoute($item->id, (int) $item->catid, $item->language); + } + + return $return; + } + } + + if ($view === 'category' || $view === 'categories') { + return self::getCategoryAssociations($id, 'com_contact'); + } + + return array(); + } } diff --git a/components/com_contact/src/Helper/RouteHelper.php b/components/com_contact/src/Helper/RouteHelper.php index 10937c0dbeeac..6e91065fee010 100644 --- a/components/com_contact/src/Helper/RouteHelper.php +++ b/components/com_contact/src/Helper/RouteHelper.php @@ -1,4 +1,5 @@ 1) - { - $link .= '&catid=' . $catid; - } + if ($catid > 1) { + $link .= '&catid=' . $catid; + } - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } - return $link; - } + return $link; + } - /** - * Get the URL route for a contact category from a contact category ID and language - * - * @param mixed $catid The id of the contact's category either an integer id or an instance of CategoryNode - * @param mixed $language The id of the language being used. - * - * @return string The link to the contact - * - * @since 1.5 - */ - public static function getCategoryRoute($catid, $language = 0) - { - if ($catid instanceof CategoryNode) - { - $id = $catid->id; - } - else - { - $id = (int) $catid; - } + /** + * Get the URL route for a contact category from a contact category ID and language + * + * @param mixed $catid The id of the contact's category either an integer id or an instance of CategoryNode + * @param mixed $language The id of the language being used. + * + * @return string The link to the contact + * + * @since 1.5 + */ + public static function getCategoryRoute($catid, $language = 0) + { + if ($catid instanceof CategoryNode) { + $id = $catid->id; + } else { + $id = (int) $catid; + } - if ($id < 1) - { - $link = ''; - } - else - { - // Create the link - $link = 'index.php?option=com_contact&view=category&id=' . $id; + if ($id < 1) { + $link = ''; + } else { + // Create the link + $link = 'index.php?option=com_contact&view=category&id=' . $id; - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } - } + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } + } - return $link; - } + return $link; + } } diff --git a/components/com_contact/src/Model/CategoriesModel.php b/components/com_contact/src/Model/CategoriesModel.php index d5ee42aa1af3e..a6d2fba75778c 100644 --- a/components/com_contact/src/Model/CategoriesModel.php +++ b/components/com_contact/src/Model/CategoriesModel.php @@ -1,4 +1,5 @@ setState('filter.extension', $this->_extension); - - // Get the parent id if defined. - $parentId = $app->input->getInt('id'); - $this->setState('filter.parentId', $parentId); - - $params = $app->getParams(); - $this->setState('params', $params); - - $this->setState('filter.published', 1); - $this->setState('filter.access', true); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.extension'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.parentId'); - - return parent::getStoreId($id); - } - - /** - * Redefine the function and add some properties to make the styling easier - * - * @return mixed An array of data items on success, false on failure. - */ - public function getItems() - { - if ($this->_items === null) - { - $app = Factory::getApplication(); - $menu = $app->getMenu(); - $active = $menu->getActive(); - - if ($active) - { - $params = $active->getParams(); - } - else - { - $params = new Registry; - } - - $options = array(); - $options['countItems'] = $params->get('show_cat_items_cat', 1) || !$params->get('show_empty_categories_cat', 0); - $categories = Categories::getInstance('Contact', $options); - $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); - - if (is_object($this->_parent)) - { - $this->_items = $this->_parent->getChildren(); - } - else - { - $this->_items = false; - } - } - - return $this->_items; - } - - /** - * Gets the id of the parent category for the selected list of categories - * - * @return integer The id of the parent category - * - * @since 1.6.0 - */ - public function getParent() - { - if (!is_object($this->_parent)) - { - $this->getItems(); - } - - return $this->_parent; - } + /** + * Model context string. + * + * @var string + */ + public $_context = 'com_contact.categories'; + + /** + * The category context (allows other extensions to derived from this model). + * + * @var string + */ + protected $_extension = 'com_contact'; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + private $_parent = null; + + /** + * Array of child-categories + * + * @var CategoryNode[]|null + */ + private $_items = null; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $this->setState('filter.extension', $this->_extension); + + // Get the parent id if defined. + $parentId = $app->input->getInt('id'); + $this->setState('filter.parentId', $parentId); + + $params = $app->getParams(); + $this->setState('params', $params); + + $this->setState('filter.published', 1); + $this->setState('filter.access', true); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.extension'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.parentId'); + + return parent::getStoreId($id); + } + + /** + * Redefine the function and add some properties to make the styling easier + * + * @return mixed An array of data items on success, false on failure. + */ + public function getItems() + { + if ($this->_items === null) { + $app = Factory::getApplication(); + $menu = $app->getMenu(); + $active = $menu->getActive(); + + if ($active) { + $params = $active->getParams(); + } else { + $params = new Registry(); + } + + $options = array(); + $options['countItems'] = $params->get('show_cat_items_cat', 1) || !$params->get('show_empty_categories_cat', 0); + $categories = Categories::getInstance('Contact', $options); + $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); + + if (is_object($this->_parent)) { + $this->_items = $this->_parent->getChildren(); + } else { + $this->_items = false; + } + } + + return $this->_items; + } + + /** + * Gets the id of the parent category for the selected list of categories + * + * @return integer The id of the parent category + * + * @since 1.6.0 + */ + public function getParent() + { + if (!is_object($this->_parent)) { + $this->getItems(); + } + + return $this->_parent; + } } diff --git a/components/com_contact/src/Model/CategoryModel.php b/components/com_contact/src/Model/CategoryModel.php index 93ca40860791e..4b42ad2a9eccd 100644 --- a/components/com_contact/src/Model/CategoryModel.php +++ b/components/com_contact/src/Model/CategoryModel.php @@ -1,4 +1,5 @@ _params)) - { - $item->params = new Registry($item->params); - } - - // Some contexts may not use tags data at all, so we allow callers to disable loading tag data - if ($this->getState('load_tags', true)) - { - $item->tags = new TagsHelper; - $taggedItems[$item->id] = $item; - } - } - - // Load tags of all items. - if ($taggedItems) - { - $tagsHelper = new TagsHelper; - $itemIds = \array_keys($taggedItems); - - foreach ($tagsHelper->getMultipleItemTags('com_contact.contact', $itemIds) as $id => $tags) - { - $taggedItems[$id]->tags->itemTags = $tags; - } - } - - return $items; - } - - /** - * Method to build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - // Create a new query object. - $db = $this->getDatabase(); - - /** @var \Joomla\Database\DatabaseQuery $query */ - $query = $db->getQuery(true); - - $query->select($this->getState('list.select', 'a.*')) - ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') - ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') - /** - * @todo: we actually should be doing it but it's wrong this way - * . ' CASE WHEN CHAR_LENGTH(a.alias) THEN CONCAT_WS(\':\', a.id, a.alias) ELSE a.id END as slug, ' - * . ' CASE WHEN CHAR_LENGTH(c.alias) THEN CONCAT_WS(\':\', c.id, c.alias) ELSE c.id END AS catslug '); - */ - ->from($db->quoteName('#__contact_details', 'a')) - ->leftJoin($db->quoteName('#__categories', 'c') . ' ON c.id = a.catid') - ->whereIn($db->quoteName('a.access'), $groups); - - // Filter by category. - if ($categoryId = $this->getState('category.id')) - { - $query->where($db->quoteName('a.catid') . ' = :acatid') - ->whereIn($db->quoteName('c.access'), $groups); - $query->bind(':acatid', $categoryId, ParameterType::INTEGER); - } - - // Join over the users for the author and modified_by names. - $query->select("CASE WHEN a.created_by_alias > ' ' THEN a.created_by_alias ELSE ua.name END AS author") - ->select('ua.email AS author_email') - ->leftJoin($db->quoteName('#__users', 'ua') . ' ON ua.id = a.created_by') - ->leftJoin($db->quoteName('#__users', 'uam') . ' ON uam.id = a.modified_by'); - - // Filter by state - $state = $this->getState('filter.published'); - - if (is_numeric($state)) - { - $query->where($db->quoteName('a.published') . ' = :published'); - $query->bind(':published', $state, ParameterType::INTEGER); - } - else - { - $query->whereIn($db->quoteName('c.published'), [0,1,2]); - } - - // Filter by start and end dates. - $nowDate = Factory::getDate()->toSql(); - - if ($this->getState('filter.publish_date')) - { - $query->where('(' . $db->quoteName('a.publish_up') - . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)' - ) - ->where('(' . $db->quoteName('a.publish_down') - . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)' - ) - ->bind(':publish_up', $nowDate) - ->bind(':publish_down', $nowDate); - } - - // Filter by search in title - $search = $this->getState('list.filter'); - - if (!empty($search)) - { - $search = '%' . trim($search) . '%'; - $query->where($db->quoteName('a.name') . ' LIKE :name '); - $query->bind(':name', $search); - } - - // Filter on the language. - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getApplication()->getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - // Set sortname ordering if selected - if ($this->getState('list.ordering') === 'sortname') - { - $query->order($db->escape('a.sortname1') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))) - ->order($db->escape('a.sortname2') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))) - ->order($db->escape('a.sortname3') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - } - elseif ($this->getState('list.ordering') === 'featuredordering') - { - $query->order($db->escape('a.featured') . ' DESC') - ->order($db->escape('a.ordering') . ' ASC'); - } - else - { - $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - } - - return $query; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = null, $direction = null) - { - $app = Factory::getApplication(); - $params = ComponentHelper::getParams('com_contact'); - - // Get list ordering default from the parameters - if ($menu = $app->getMenu()->getActive()) - { - $menuParams = $menu->getParams(); - } - else - { - $menuParams = new Registry; - } - - $mergedParams = clone $params; - $mergedParams->merge($menuParams); - - // List state information - $format = $app->input->getWord('format'); - - if ($format === 'feed') - { - $limit = $app->get('feed_limit'); - } - else - { - $limit = $app->getUserStateFromRequest( - 'com_contact.category.list.limit', - 'limit', - $mergedParams->get('contacts_display_num', $app->get('list_limit')), - 'uint' - ); - } - - $this->setState('list.limit', $limit); - - $limitstart = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $limitstart); - - // Optional filter text - $itemid = $app->input->get('Itemid', 0, 'int'); - $search = $app->getUserStateFromRequest('com_contact.category.list.' . $itemid . '.filter-search', 'filter-search', '', 'string'); - $this->setState('list.filter', $search); - - $orderCol = $app->input->get('filter_order', $mergedParams->get('initial_sort', 'ordering')); - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'ordering'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->input->get('filter_order_Dir', 'ASC'); - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $id = $app->input->get('id', 0, 'int'); - $this->setState('category.id', $id); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) - { - // Limit to published for people who can't edit or edit.state. - $this->setState('filter.published', 1); - - // Filter by start and end dates. - $this->setState('filter.publish_date', true); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to get category data for the current category - * - * @return object The category object - * - * @since 1.5 - */ - public function getCategory() - { - if (!is_object($this->_item)) - { - $app = Factory::getApplication(); - $menu = $app->getMenu(); - $active = $menu->getActive(); - - if ($active) - { - $params = $active->getParams(); - } - else - { - $params = new Registry; - } - - $options = array(); - $options['countItems'] = $params->get('show_cat_items', 1) || $params->get('show_empty_categories', 0); - $categories = Categories::getInstance('Contact', $options); - $this->_item = $categories->get($this->getState('category.id', 'root')); - - if (is_object($this->_item)) - { - $this->_children = $this->_item->getChildren(); - $this->_parent = false; - - if ($this->_item->getParent()) - { - $this->_parent = $this->_item->getParent(); - } - - $this->_rightsibling = $this->_item->getSibling(); - $this->_leftsibling = $this->_item->getSibling(false); - } - else - { - $this->_children = false; - $this->_parent = false; - } - } - - return $this->_item; - } - - /** - * Get the parent category. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function getParent() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_parent; - } - - /** - * Get the sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getLeftSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_leftsibling; - } - - /** - * Get the sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getRightSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_rightsibling; - } - - /** - * Get the child categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getChildren() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_children; - } - - /** - * Generate column expression for slug or catslug. - * - * @param \Joomla\Database\DatabaseQuery $query Current query instance. - * @param string $id Column id name. - * @param string $alias Column alias name. - * - * @return string - * - * @since 4.0.0 - */ - private function getSlugColumn($query, $id, $alias) - { - return 'CASE WHEN ' - . $query->charLength($alias, '!=', '0') - . ' THEN ' - . $query->concatenate(array($query->castAsChar($id), $alias), ':') - . ' ELSE ' - . $query->castAsChar($id) . ' END'; - } - - /** - * Increment the hit counter for the category. - * - * @param integer $pk Optional primary key of the category to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - * - * @since 3.2 - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); - - $table = Table::getInstance('Category'); - $table->hit($pk); - } - - return true; - } + /** + * Category item data + * + * @var CategoryNode + */ + protected $_item; + + /** + * Array of contacts in the category + * + * @var \stdClass[] + */ + protected $_articles; + + /** + * Category left and right of this one + * + * @var CategoryNode[]|null + */ + protected $_siblings; + + /** + * Array of child-categories + * + * @var CategoryNode[]|null + */ + protected $_children; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + protected $_parent; + + /** + * The category that applies. + * + * @var object + */ + protected $_category; + + /** + * The list of other contact categories. + * + * @var array + */ + protected $_categories; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'con_position', 'a.con_position', + 'suburb', 'a.suburb', + 'state', 'a.state', + 'country', 'a.country', + 'ordering', 'a.ordering', + 'sortname', + 'sortname1', 'a.sortname1', + 'sortname2', 'a.sortname2', + 'sortname3', 'a.sortname3', + 'featuredordering', 'a.featured' + ); + } + + parent::__construct($config); + } + + /** + * Method to get a list of items. + * + * @return mixed An array of objects on success, false on failure. + */ + public function getItems() + { + // Invoke the parent getItems method to get the main list + $items = parent::getItems(); + + if ($items === false) { + return false; + } + + $taggedItems = []; + + // Convert the params field into an object, saving original in _params + foreach ($items as $item) { + if (!isset($this->_params)) { + $item->params = new Registry($item->params); + } + + // Some contexts may not use tags data at all, so we allow callers to disable loading tag data + if ($this->getState('load_tags', true)) { + $item->tags = new TagsHelper(); + $taggedItems[$item->id] = $item; + } + } + + // Load tags of all items. + if ($taggedItems) { + $tagsHelper = new TagsHelper(); + $itemIds = \array_keys($taggedItems); + + foreach ($tagsHelper->getMultipleItemTags('com_contact.contact', $itemIds) as $id => $tags) { + $taggedItems[$id]->tags->itemTags = $tags; + } + } + + return $items; + } + + /** + * Method to build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + // Create a new query object. + $db = $this->getDatabase(); + + /** @var \Joomla\Database\DatabaseQuery $query */ + $query = $db->getQuery(true); + + $query->select($this->getState('list.select', 'a.*')) + ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') + ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') + /** + * @todo: we actually should be doing it but it's wrong this way + * . ' CASE WHEN CHAR_LENGTH(a.alias) THEN CONCAT_WS(\':\', a.id, a.alias) ELSE a.id END as slug, ' + * . ' CASE WHEN CHAR_LENGTH(c.alias) THEN CONCAT_WS(\':\', c.id, c.alias) ELSE c.id END AS catslug '); + */ + ->from($db->quoteName('#__contact_details', 'a')) + ->leftJoin($db->quoteName('#__categories', 'c') . ' ON c.id = a.catid') + ->whereIn($db->quoteName('a.access'), $groups); + + // Filter by category. + if ($categoryId = $this->getState('category.id')) { + $query->where($db->quoteName('a.catid') . ' = :acatid') + ->whereIn($db->quoteName('c.access'), $groups); + $query->bind(':acatid', $categoryId, ParameterType::INTEGER); + } + + // Join over the users for the author and modified_by names. + $query->select("CASE WHEN a.created_by_alias > ' ' THEN a.created_by_alias ELSE ua.name END AS author") + ->select('ua.email AS author_email') + ->leftJoin($db->quoteName('#__users', 'ua') . ' ON ua.id = a.created_by') + ->leftJoin($db->quoteName('#__users', 'uam') . ' ON uam.id = a.modified_by'); + + // Filter by state + $state = $this->getState('filter.published'); + + if (is_numeric($state)) { + $query->where($db->quoteName('a.published') . ' = :published'); + $query->bind(':published', $state, ParameterType::INTEGER); + } else { + $query->whereIn($db->quoteName('c.published'), [0,1,2]); + } + + // Filter by start and end dates. + $nowDate = Factory::getDate()->toSql(); + + if ($this->getState('filter.publish_date')) { + $query->where('(' . $db->quoteName('a.publish_up') + . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)') + ->where('(' . $db->quoteName('a.publish_down') + . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)') + ->bind(':publish_up', $nowDate) + ->bind(':publish_down', $nowDate); + } + + // Filter by search in title + $search = $this->getState('list.filter'); + + if (!empty($search)) { + $search = '%' . trim($search) . '%'; + $query->where($db->quoteName('a.name') . ' LIKE :name '); + $query->bind(':name', $search); + } + + // Filter on the language. + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getApplication()->getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + // Set sortname ordering if selected + if ($this->getState('list.ordering') === 'sortname') { + $query->order($db->escape('a.sortname1') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))) + ->order($db->escape('a.sortname2') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))) + ->order($db->escape('a.sortname3') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + } elseif ($this->getState('list.ordering') === 'featuredordering') { + $query->order($db->escape('a.featured') . ' DESC') + ->order($db->escape('a.ordering') . ' ASC'); + } else { + $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + } + + return $query; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $params = ComponentHelper::getParams('com_contact'); + + // Get list ordering default from the parameters + if ($menu = $app->getMenu()->getActive()) { + $menuParams = $menu->getParams(); + } else { + $menuParams = new Registry(); + } + + $mergedParams = clone $params; + $mergedParams->merge($menuParams); + + // List state information + $format = $app->input->getWord('format'); + + if ($format === 'feed') { + $limit = $app->get('feed_limit'); + } else { + $limit = $app->getUserStateFromRequest( + 'com_contact.category.list.limit', + 'limit', + $mergedParams->get('contacts_display_num', $app->get('list_limit')), + 'uint' + ); + } + + $this->setState('list.limit', $limit); + + $limitstart = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $limitstart); + + // Optional filter text + $itemid = $app->input->get('Itemid', 0, 'int'); + $search = $app->getUserStateFromRequest('com_contact.category.list.' . $itemid . '.filter-search', 'filter-search', '', 'string'); + $this->setState('list.filter', $search); + + $orderCol = $app->input->get('filter_order', $mergedParams->get('initial_sort', 'ordering')); + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'ordering'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->input->get('filter_order_Dir', 'ASC'); + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $id = $app->input->get('id', 0, 'int'); + $this->setState('category.id', $id); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) { + // Limit to published for people who can't edit or edit.state. + $this->setState('filter.published', 1); + + // Filter by start and end dates. + $this->setState('filter.publish_date', true); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to get category data for the current category + * + * @return object The category object + * + * @since 1.5 + */ + public function getCategory() + { + if (!is_object($this->_item)) { + $app = Factory::getApplication(); + $menu = $app->getMenu(); + $active = $menu->getActive(); + + if ($active) { + $params = $active->getParams(); + } else { + $params = new Registry(); + } + + $options = array(); + $options['countItems'] = $params->get('show_cat_items', 1) || $params->get('show_empty_categories', 0); + $categories = Categories::getInstance('Contact', $options); + $this->_item = $categories->get($this->getState('category.id', 'root')); + + if (is_object($this->_item)) { + $this->_children = $this->_item->getChildren(); + $this->_parent = false; + + if ($this->_item->getParent()) { + $this->_parent = $this->_item->getParent(); + } + + $this->_rightsibling = $this->_item->getSibling(); + $this->_leftsibling = $this->_item->getSibling(false); + } else { + $this->_children = false; + $this->_parent = false; + } + } + + return $this->_item; + } + + /** + * Get the parent category. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function getParent() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_parent; + } + + /** + * Get the sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getLeftSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_leftsibling; + } + + /** + * Get the sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getRightSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_rightsibling; + } + + /** + * Get the child categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getChildren() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_children; + } + + /** + * Generate column expression for slug or catslug. + * + * @param \Joomla\Database\DatabaseQuery $query Current query instance. + * @param string $id Column id name. + * @param string $alias Column alias name. + * + * @return string + * + * @since 4.0.0 + */ + private function getSlugColumn($query, $id, $alias) + { + return 'CASE WHEN ' + . $query->charLength($alias, '!=', '0') + . ' THEN ' + . $query->concatenate(array($query->castAsChar($id), $alias), ':') + . ' ELSE ' + . $query->castAsChar($id) . ' END'; + } + + /** + * Increment the hit counter for the category. + * + * @param integer $pk Optional primary key of the category to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + * + * @since 3.2 + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); + + $table = Table::getInstance('Category'); + $table->hit($pk); + } + + return true; + } } diff --git a/components/com_contact/src/Model/ContactModel.php b/components/com_contact/src/Model/ContactModel.php index 54def0d43450a..f4f9e0d85b3f3 100644 --- a/components/com_contact/src/Model/ContactModel.php +++ b/components/com_contact/src/Model/ContactModel.php @@ -1,4 +1,5 @@ get(SiteApplication::class); - - if (Factory::getApplication()->isClient('api')) - { - // @todo: remove this - $app->loadLanguage(); - $this->setState('contact.id', Factory::getApplication()->input->post->getInt('id')); - } - else - { - $this->setState('contact.id', $app->input->getInt('id')); - } - - $this->setState('params', $app->getParams()); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) - { - $this->setState('filter.published', 1); - $this->setState('filter.archived', 2); - } - } - - /** - * Method to get the contact form. - * The base form is loaded from XML and then an event is fired - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - $form = $this->loadForm('com_contact.contact', 'contact', array('control' => 'jform', 'load_data' => true)); - - if (empty($form)) - { - return false; - } - - $temp = clone $this->getState('params'); - $contact = $this->_item[$this->getState('contact.id')]; - $active = Factory::getContainer()->get(SiteApplication::class)->getMenu()->getActive(); - - if ($active) - { - // If the current view is the active item and a contact view for this contact, then the menu item params take priority - if (strpos($active->link, 'view=contact') && strpos($active->link, '&id=' . (int) $contact->id)) - { - // $contact->params are the contact params, $temp are the menu item params - // Merge so that the menu item params take priority - $contact->params->merge($temp); - } - else - { - // Current view is not a single contact, so the contact params take priority here - // Merge the menu item params with the contact params so that the contact params take priority - $temp->merge($contact->params); - $contact->params = $temp; - } - } - else - { - // Merge so that contact params take priority - $temp->merge($contact->params); - $contact->params = $temp; - } - - if (!$contact->params->get('show_email_copy', 0)) - { - $form->removeField('contact_email_copy'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 1.6.2 - */ - protected function loadFormData() - { - $data = (array) Factory::getApplication()->getUserState('com_contact.contact.data', array()); - - if (empty($data['language']) && Multilanguage::isEnabled()) - { - $data['language'] = Factory::getLanguage()->getTag(); - } - - // Add contact catid to contact form data, so fields plugin can work properly - if (empty($data['catid'])) - { - $data['catid'] = $this->getItem()->catid; - } - - $this->preprocessData('com_contact.contact', $data); - - return $data; - } - - /** - * Gets a contact - * - * @param integer $pk Id for the contact - * - * @return mixed Object or null - * - * @since 1.6.0 - */ - public function getItem($pk = null) - { - $pk = $pk ?: (int) $this->getState('contact.id'); - - if ($this->_item === null) - { - $this->_item = array(); - } - - if (!isset($this->_item[$pk])) - { - try - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select($this->getState('item.select', 'a.*')) - ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') - ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') - ->from($db->quoteName('#__contact_details', 'a')) - - // Join on category table. - ->select('c.title AS category_title, c.alias AS category_alias, c.access AS category_access') - ->leftJoin($db->quoteName('#__categories', 'c'), 'c.id = a.catid') - - // Join over the categories to get parent category titles - ->select('parent.title AS parent_title, parent.id AS parent_id, parent.path AS parent_route, parent.alias AS parent_alias') - ->leftJoin($db->quoteName('#__categories', 'parent'), 'parent.id = c.parent_id') - ->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - // Filter by start and end dates. - $nowDate = Factory::getDate()->toSql(); - - // Filter by published state. - $published = $this->getState('filter.published'); - $archived = $this->getState('filter.archived'); - - if (is_numeric($published)) - { - $queryString = $db->quoteName('a.published') . ' = :published'; - - if ($archived !== null) - { - $queryString = '(' . $queryString . ' OR ' . $db->quoteName('a.published') . ' = :archived)'; - $query->bind(':archived', $archived, ParameterType::INTEGER); - } - - $query->where($queryString) - ->where('(' . $db->quoteName('a.publish_up') . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)') - ->where('(' . $db->quoteName('a.publish_down') . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)') - ->bind(':published', $published, ParameterType::INTEGER) - ->bind(':publish_up', $nowDate) - ->bind(':publish_down', $nowDate); - } - - $db->setQuery($query); - $data = $db->loadObject(); - - if (empty($data)) - { - throw new \Exception(Text::_('COM_CONTACT_ERROR_CONTACT_NOT_FOUND'), 404); - } - - // Check for published state if filter set. - if ((is_numeric($published) || is_numeric($archived)) && (($data->published != $published) && ($data->published != $archived))) - { - throw new \Exception(Text::_('COM_CONTACT_ERROR_CONTACT_NOT_FOUND'), 404); - } - - /** - * In case some entity params have been set to "use global", those are - * represented as an empty string and must be "overridden" by merging - * the component and / or menu params here. - */ - $registry = new Registry($data->params); - - $data->params = clone $this->getState('params'); - $data->params->merge($registry); - - $registry = new Registry($data->metadata); - $data->metadata = $registry; - - // Some contexts may not use tags data at all, so we allow callers to disable loading tag data - if ($this->getState('load_tags', true)) - { - $data->tags = new TagsHelper; - $data->tags->getItemTags('com_contact.contact', $data->id); - } - - // Compute access permissions. - if (($access = $this->getState('filter.access'))) - { - // If the access filter has been set, we already know this user can view. - $data->params->set('access-view', true); - } - else - { - // If no access filter is set, the layout takes some responsibility for display of limited information. - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - if ($data->catid == 0 || $data->category_access === null) - { - $data->params->set('access-view', in_array($data->access, $groups)); - } - else - { - $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); - } - } - - $this->_item[$pk] = $data; - } - catch (\Exception $e) - { - if ($e->getCode() == 404) - { - // Need to go through the error handler to allow Redirect to work. - throw $e; - } - else - { - $this->setError($e); - $this->_item[$pk] = false; - } - } - } - - if ($this->_item[$pk]) - { - $this->buildContactExtendedData($this->_item[$pk]); - } - - return $this->_item[$pk]; - } - - /** - * Load extended data (profile, articles) for a contact - * - * @param object $contact The contact object - * - * @return void - */ - protected function buildContactExtendedData($contact) - { - $db = $this->getDatabase(); - $nowDate = Factory::getDate()->toSql(); - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - $published = $this->getState('filter.published'); - $query = $db->getQuery(true); - - // If we are showing a contact list, then the contact parameters take priority - // So merge the contact parameters with the merged parameters - if ($this->getState('params')->get('show_contact_list')) - { - $this->getState('params')->merge($contact->params); - } - - // Get the com_content articles by the linked user - if ((int) $contact->user_id && $this->getState('params')->get('show_articles')) - { - $query->select('a.id') - ->select('a.title') - ->select('a.state') - ->select('a.access') - ->select('a.catid') - ->select('a.created') - ->select('a.language') - ->select('a.publish_up') - ->select('a.introtext') - ->select('a.images') - ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') - ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') - ->from($db->quoteName('#__content', 'a')) - ->leftJoin($db->quoteName('#__categories', 'c') . ' ON a.catid = c.id') - ->where($db->quoteName('a.created_by') . ' = :created_by') - ->whereIn($db->quoteName('a.access'), $groups) - ->bind(':created_by', $contact->user_id, ParameterType::INTEGER) - ->order('a.publish_up DESC'); - - // Filter per language if plugin published - if (Multilanguage::isEnabled()) - { - $language = [Factory::getLanguage()->getTag(), $db->quote('*')]; - $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); - } - - if (is_numeric($published)) - { - $query->where('a.state IN (1,2)') - ->where('(' . $db->quoteName('a.publish_up') . ' IS NULL' . - ' OR ' . $db->quoteName('a.publish_up') . ' <= :now1)' - ) - ->where('(' . $db->quoteName('a.publish_down') . ' IS NULL' . - ' OR ' . $db->quoteName('a.publish_down') . ' >= :now2)' - ) - ->bind([':now1', ':now2'], $nowDate); - } - - // Number of articles to display from config/menu params - $articles_display_num = $this->getState('params')->get('articles_display_num', 10); - - // Use contact setting? - if ($articles_display_num === 'use_contact') - { - $articles_display_num = $contact->params->get('articles_display_num', 10); - - // Use global? - if ((string) $articles_display_num === '') - { - $articles_display_num = ComponentHelper::getParams('com_contact')->get('articles_display_num', 10); - } - } - - $query->setLimit((int) $articles_display_num); - $db->setQuery($query); - $contact->articles = $db->loadObjectList(); - } - else - { - $contact->articles = null; - } - - // Get the profile information for the linked user - $userModel = $this->bootComponent('com_users')->getMVCFactory() - ->createModel('User', 'Administrator', ['ignore_request' => true]); - $data = $userModel->getItem((int) $contact->user_id); - - PluginHelper::importPlugin('user'); - - // Get the form. - Form::addFormPath(JPATH_SITE . '/components/com_users/forms'); - - $form = Form::getInstance('com_users.profile', 'profile'); - - // Trigger the form preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareForm', array($form, $data)); - - // Trigger the data preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.profile', $data)); - - // Load the data into the form after the plugins have operated. - $form->bind($data); - $contact->profile = $form; - } - - /** - * Generate column expression for slug or catslug. - * - * @param QueryInterface $query Current query instance. - * @param string $id Column id name. - * @param string $alias Column alias name. - * - * @return string - * - * @since 4.0.0 - */ - private function getSlugColumn($query, $id, $alias) - { - return 'CASE WHEN ' - . $query->charLength($alias, '!=', '0') - . ' THEN ' - . $query->concatenate(array($query->castAsChar($id), $alias), ':') - . ' ELSE ' - . $query->castAsChar($id) . ' END'; - } - - /** - * Increment the hit counter for the contact. - * - * @param integer $pk Optional primary key of the contact to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - * - * @since 3.0 - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = $pk ?: (int) $this->getState('contact.id'); - - $table = $this->getTable('Contact'); - $table->hit($pk); - } - - return true; - } + /** + * The name of the view for a single item + * + * @var string + * @since 1.6 + */ + protected $view_item = 'contact'; + + /** + * A loaded item + * + * @var \stdClass + * @since 1.6 + */ + protected $_item = null; + + /** + * Model context string. + * + * @var string + */ + protected $_context = 'com_contact.contact'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + /** @var SiteApplication $app */ + $app = Factory::getContainer()->get(SiteApplication::class); + + if (Factory::getApplication()->isClient('api')) { + // @todo: remove this + $app->loadLanguage(); + $this->setState('contact.id', Factory::getApplication()->input->post->getInt('id')); + } else { + $this->setState('contact.id', $app->input->getInt('id')); + } + + $this->setState('params', $app->getParams()); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) { + $this->setState('filter.published', 1); + $this->setState('filter.archived', 2); + } + } + + /** + * Method to get the contact form. + * The base form is loaded from XML and then an event is fired + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + $form = $this->loadForm('com_contact.contact', 'contact', array('control' => 'jform', 'load_data' => true)); + + if (empty($form)) { + return false; + } + + $temp = clone $this->getState('params'); + $contact = $this->_item[$this->getState('contact.id')]; + $active = Factory::getContainer()->get(SiteApplication::class)->getMenu()->getActive(); + + if ($active) { + // If the current view is the active item and a contact view for this contact, then the menu item params take priority + if (strpos($active->link, 'view=contact') && strpos($active->link, '&id=' . (int) $contact->id)) { + // $contact->params are the contact params, $temp are the menu item params + // Merge so that the menu item params take priority + $contact->params->merge($temp); + } else { + // Current view is not a single contact, so the contact params take priority here + // Merge the menu item params with the contact params so that the contact params take priority + $temp->merge($contact->params); + $contact->params = $temp; + } + } else { + // Merge so that contact params take priority + $temp->merge($contact->params); + $contact->params = $temp; + } + + if (!$contact->params->get('show_email_copy', 0)) { + $form->removeField('contact_email_copy'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 1.6.2 + */ + protected function loadFormData() + { + $data = (array) Factory::getApplication()->getUserState('com_contact.contact.data', array()); + + if (empty($data['language']) && Multilanguage::isEnabled()) { + $data['language'] = Factory::getLanguage()->getTag(); + } + + // Add contact catid to contact form data, so fields plugin can work properly + if (empty($data['catid'])) { + $data['catid'] = $this->getItem()->catid; + } + + $this->preprocessData('com_contact.contact', $data); + + return $data; + } + + /** + * Gets a contact + * + * @param integer $pk Id for the contact + * + * @return mixed Object or null + * + * @since 1.6.0 + */ + public function getItem($pk = null) + { + $pk = $pk ?: (int) $this->getState('contact.id'); + + if ($this->_item === null) { + $this->_item = array(); + } + + if (!isset($this->_item[$pk])) { + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select($this->getState('item.select', 'a.*')) + ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') + ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') + ->from($db->quoteName('#__contact_details', 'a')) + + // Join on category table. + ->select('c.title AS category_title, c.alias AS category_alias, c.access AS category_access') + ->leftJoin($db->quoteName('#__categories', 'c'), 'c.id = a.catid') + + // Join over the categories to get parent category titles + ->select('parent.title AS parent_title, parent.id AS parent_id, parent.path AS parent_route, parent.alias AS parent_alias') + ->leftJoin($db->quoteName('#__categories', 'parent'), 'parent.id = c.parent_id') + ->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + // Filter by start and end dates. + $nowDate = Factory::getDate()->toSql(); + + // Filter by published state. + $published = $this->getState('filter.published'); + $archived = $this->getState('filter.archived'); + + if (is_numeric($published)) { + $queryString = $db->quoteName('a.published') . ' = :published'; + + if ($archived !== null) { + $queryString = '(' . $queryString . ' OR ' . $db->quoteName('a.published') . ' = :archived)'; + $query->bind(':archived', $archived, ParameterType::INTEGER); + } + + $query->where($queryString) + ->where('(' . $db->quoteName('a.publish_up') . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)') + ->where('(' . $db->quoteName('a.publish_down') . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)') + ->bind(':published', $published, ParameterType::INTEGER) + ->bind(':publish_up', $nowDate) + ->bind(':publish_down', $nowDate); + } + + $db->setQuery($query); + $data = $db->loadObject(); + + if (empty($data)) { + throw new \Exception(Text::_('COM_CONTACT_ERROR_CONTACT_NOT_FOUND'), 404); + } + + // Check for published state if filter set. + if ((is_numeric($published) || is_numeric($archived)) && (($data->published != $published) && ($data->published != $archived))) { + throw new \Exception(Text::_('COM_CONTACT_ERROR_CONTACT_NOT_FOUND'), 404); + } + + /** + * In case some entity params have been set to "use global", those are + * represented as an empty string and must be "overridden" by merging + * the component and / or menu params here. + */ + $registry = new Registry($data->params); + + $data->params = clone $this->getState('params'); + $data->params->merge($registry); + + $registry = new Registry($data->metadata); + $data->metadata = $registry; + + // Some contexts may not use tags data at all, so we allow callers to disable loading tag data + if ($this->getState('load_tags', true)) { + $data->tags = new TagsHelper(); + $data->tags->getItemTags('com_contact.contact', $data->id); + } + + // Compute access permissions. + if (($access = $this->getState('filter.access'))) { + // If the access filter has been set, we already know this user can view. + $data->params->set('access-view', true); + } else { + // If no access filter is set, the layout takes some responsibility for display of limited information. + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + if ($data->catid == 0 || $data->category_access === null) { + $data->params->set('access-view', in_array($data->access, $groups)); + } else { + $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); + } + } + + $this->_item[$pk] = $data; + } catch (\Exception $e) { + if ($e->getCode() == 404) { + // Need to go through the error handler to allow Redirect to work. + throw $e; + } else { + $this->setError($e); + $this->_item[$pk] = false; + } + } + } + + if ($this->_item[$pk]) { + $this->buildContactExtendedData($this->_item[$pk]); + } + + return $this->_item[$pk]; + } + + /** + * Load extended data (profile, articles) for a contact + * + * @param object $contact The contact object + * + * @return void + */ + protected function buildContactExtendedData($contact) + { + $db = $this->getDatabase(); + $nowDate = Factory::getDate()->toSql(); + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + $published = $this->getState('filter.published'); + $query = $db->getQuery(true); + + // If we are showing a contact list, then the contact parameters take priority + // So merge the contact parameters with the merged parameters + if ($this->getState('params')->get('show_contact_list')) { + $this->getState('params')->merge($contact->params); + } + + // Get the com_content articles by the linked user + if ((int) $contact->user_id && $this->getState('params')->get('show_articles')) { + $query->select('a.id') + ->select('a.title') + ->select('a.state') + ->select('a.access') + ->select('a.catid') + ->select('a.created') + ->select('a.language') + ->select('a.publish_up') + ->select('a.introtext') + ->select('a.images') + ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') + ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') + ->from($db->quoteName('#__content', 'a')) + ->leftJoin($db->quoteName('#__categories', 'c') . ' ON a.catid = c.id') + ->where($db->quoteName('a.created_by') . ' = :created_by') + ->whereIn($db->quoteName('a.access'), $groups) + ->bind(':created_by', $contact->user_id, ParameterType::INTEGER) + ->order('a.publish_up DESC'); + + // Filter per language if plugin published + if (Multilanguage::isEnabled()) { + $language = [Factory::getLanguage()->getTag(), $db->quote('*')]; + $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); + } + + if (is_numeric($published)) { + $query->where('a.state IN (1,2)') + ->where('(' . $db->quoteName('a.publish_up') . ' IS NULL' . + ' OR ' . $db->quoteName('a.publish_up') . ' <= :now1)') + ->where('(' . $db->quoteName('a.publish_down') . ' IS NULL' . + ' OR ' . $db->quoteName('a.publish_down') . ' >= :now2)') + ->bind([':now1', ':now2'], $nowDate); + } + + // Number of articles to display from config/menu params + $articles_display_num = $this->getState('params')->get('articles_display_num', 10); + + // Use contact setting? + if ($articles_display_num === 'use_contact') { + $articles_display_num = $contact->params->get('articles_display_num', 10); + + // Use global? + if ((string) $articles_display_num === '') { + $articles_display_num = ComponentHelper::getParams('com_contact')->get('articles_display_num', 10); + } + } + + $query->setLimit((int) $articles_display_num); + $db->setQuery($query); + $contact->articles = $db->loadObjectList(); + } else { + $contact->articles = null; + } + + // Get the profile information for the linked user + $userModel = $this->bootComponent('com_users')->getMVCFactory() + ->createModel('User', 'Administrator', ['ignore_request' => true]); + $data = $userModel->getItem((int) $contact->user_id); + + PluginHelper::importPlugin('user'); + + // Get the form. + Form::addFormPath(JPATH_SITE . '/components/com_users/forms'); + + $form = Form::getInstance('com_users.profile', 'profile'); + + // Trigger the form preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareForm', array($form, $data)); + + // Trigger the data preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.profile', $data)); + + // Load the data into the form after the plugins have operated. + $form->bind($data); + $contact->profile = $form; + } + + /** + * Generate column expression for slug or catslug. + * + * @param QueryInterface $query Current query instance. + * @param string $id Column id name. + * @param string $alias Column alias name. + * + * @return string + * + * @since 4.0.0 + */ + private function getSlugColumn($query, $id, $alias) + { + return 'CASE WHEN ' + . $query->charLength($alias, '!=', '0') + . ' THEN ' + . $query->concatenate(array($query->castAsChar($id), $alias), ':') + . ' ELSE ' + . $query->castAsChar($id) . ' END'; + } + + /** + * Increment the hit counter for the contact. + * + * @param integer $pk Optional primary key of the contact to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + * + * @since 3.0 + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = $pk ?: (int) $this->getState('contact.id'); + + $table = $this->getTable('Contact'); + $table->hit($pk); + } + + return true; + } } diff --git a/components/com_contact/src/Model/FeaturedModel.php b/components/com_contact/src/Model/FeaturedModel.php index f654f45cf6f4a..94dc13207c392 100644 --- a/components/com_contact/src/Model/FeaturedModel.php +++ b/components/com_contact/src/Model/FeaturedModel.php @@ -1,4 +1,5 @@ _params)) - { - $item->params = new Registry($item->params); - } - } - - return $items; - } - - /** - * Method to build an SQL query to load the list data. - * - * @return string An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select required fields from the categories. - $query->select($this->getState('list.select', 'a.*')) - ->from($db->quoteName('#__contact_details', 'a')) - ->where($db->quoteName('a.featured') . ' = 1') - ->whereIn($db->quoteName('a.access'), $groups) - ->innerJoin($db->quoteName('#__categories', 'c') . ' ON c.id = a.catid') - ->whereIn($db->quoteName('c.access'), $groups); - - // Filter by category. - if ($categoryId = $this->getState('category.id')) - { - $query->where($db->quoteName('a.catid') . ' = :catid'); - $query->bind(':catid', $categoryId, ParameterType::INTEGER); - } - - $query->select('c.published as cat_published, c.published AS parents_published') - ->where('c.published = 1'); - - // Filter by state - $state = $this->getState('filter.published'); - - if (is_numeric($state)) - { - $query->where($db->quoteName('a.published') . ' = :published'); - $query->bind(':published', $state, ParameterType::INTEGER); - - // Filter by start and end dates. - $nowDate = Factory::getDate()->toSql(); - - $query->where('(' . $db->quoteName('a.publish_up') . - ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)' - ) - ->where('(' . $db->quoteName('a.publish_down') . - ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)' - ) - ->bind(':publish_up', $nowDate) - ->bind(':publish_down', $nowDate); - } - - // Filter by search in title - $search = $this->getState('list.filter'); - - // Filter by search in title - if (!empty($search)) - { - $search = '%' . trim($search) . '%'; - $query->where($db->quoteName('a.name') . ' LIKE :name '); - $query->bind(':name', $search); - } - - // Filter by language - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = null, $direction = null) - { - $app = Factory::getApplication(); - $params = ComponentHelper::getParams('com_contact'); - - // List state information - $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); - $this->setState('list.limit', $limit); - - $limitstart = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $limitstart); - - // Optional filter text - $this->setState('list.filter', $app->input->getString('filter-search')); - - $orderCol = $app->input->get('filter_order', 'ordering'); - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'ordering'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->input->get('filter_order_Dir', 'ASC'); - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) - { - // Limit to published for people who can't edit or edit.state. - $this->setState('filter.published', 1); - - // Filter by start and end dates. - $this->setState('filter.publish_date', true); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - - // Load the parameters. - $this->setState('params', $params); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'con_position', 'a.con_position', + 'suburb', 'a.suburb', + 'state', 'a.state', + 'country', 'a.country', + 'ordering', 'a.ordering', + ); + } + + parent::__construct($config); + } + + /** + * Method to get a list of items. + * + * @return mixed An array of objects on success, false on failure. + */ + public function getItems() + { + // Invoke the parent getItems method to get the main list + $items = parent::getItems(); + + // Convert the params field into an object, saving original in _params + for ($i = 0, $n = count($items); $i < $n; $i++) { + $item = &$items[$i]; + + if (!isset($this->_params)) { + $item->params = new Registry($item->params); + } + } + + return $items; + } + + /** + * Method to build an SQL query to load the list data. + * + * @return string An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select required fields from the categories. + $query->select($this->getState('list.select', 'a.*')) + ->from($db->quoteName('#__contact_details', 'a')) + ->where($db->quoteName('a.featured') . ' = 1') + ->whereIn($db->quoteName('a.access'), $groups) + ->innerJoin($db->quoteName('#__categories', 'c') . ' ON c.id = a.catid') + ->whereIn($db->quoteName('c.access'), $groups); + + // Filter by category. + if ($categoryId = $this->getState('category.id')) { + $query->where($db->quoteName('a.catid') . ' = :catid'); + $query->bind(':catid', $categoryId, ParameterType::INTEGER); + } + + $query->select('c.published as cat_published, c.published AS parents_published') + ->where('c.published = 1'); + + // Filter by state + $state = $this->getState('filter.published'); + + if (is_numeric($state)) { + $query->where($db->quoteName('a.published') . ' = :published'); + $query->bind(':published', $state, ParameterType::INTEGER); + + // Filter by start and end dates. + $nowDate = Factory::getDate()->toSql(); + + $query->where('(' . $db->quoteName('a.publish_up') . + ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)') + ->where('(' . $db->quoteName('a.publish_down') . + ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)') + ->bind(':publish_up', $nowDate) + ->bind(':publish_down', $nowDate); + } + + // Filter by search in title + $search = $this->getState('list.filter'); + + // Filter by search in title + if (!empty($search)) { + $search = '%' . trim($search) . '%'; + $query->where($db->quoteName('a.name') . ' LIKE :name '); + $query->bind(':name', $search); + } + + // Filter by language + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $params = ComponentHelper::getParams('com_contact'); + + // List state information + $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); + $this->setState('list.limit', $limit); + + $limitstart = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $limitstart); + + // Optional filter text + $this->setState('list.filter', $app->input->getString('filter-search')); + + $orderCol = $app->input->get('filter_order', 'ordering'); + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'ordering'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->input->get('filter_order_Dir', 'ASC'); + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) { + // Limit to published for people who can't edit or edit.state. + $this->setState('filter.published', 1); + + // Filter by start and end dates. + $this->setState('filter.publish_date', true); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + + // Load the parameters. + $this->setState('params', $params); + } } diff --git a/components/com_contact/src/Model/FormModel.php b/components/com_contact/src/Model/FormModel.php index 8afefd64f50c9..eb172ca82a495 100644 --- a/components/com_contact/src/Model/FormModel.php +++ b/components/com_contact/src/Model/FormModel.php @@ -1,4 +1,5 @@ getState('contact.id') && Associations::isEnabled()) - { - $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $id); - - // Make fields read only - if (!empty($associations)) - { - $form->setFieldAttribute('language', 'readonly', 'true'); - $form->setFieldAttribute('language', 'filter', 'unset'); - } - } - - return $form; - } - - /** - * Method to get contact data. - * - * @param integer $itemId The id of the contact. - * - * @return mixed Contact item data object on success, false on failure. - * - * @throws Exception - * - * @since 4.0.0 - */ - public function getItem($itemId = null) - { - $itemId = (int) (!empty($itemId)) ? $itemId : $this->getState('contact.id'); - - // Get a row instance. - $table = $this->getTable(); - - // Attempt to load the row. - try - { - if (!$table->load($itemId)) - { - return false; - } - } - catch (Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage()); - - return false; - } - - $properties = $table->getProperties(); - $value = ArrayHelper::toObject($properties, \Joomla\CMS\Object\CMSObject::class); - - // Convert field to Registry. - $value->params = new Registry($value->params); - - // Convert the metadata field to an array. - $registry = new Registry($value->metadata); - $value->metadata = $registry->toArray(); - - if ($itemId) - { - $value->tags = new TagsHelper; - $value->tags->getTagIds($value->id, 'com_contact.contact'); - $value->metadata['tags'] = $value->tags; - } - - return $value; - } - - /** - * Get the return URL. - * - * @return string The return URL. - * - * @since 4.0.0 - */ - public function getReturnPage() - { - return base64_encode($this->getState('return_page', '')); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 4.0.0 - * - * @throws Exception - */ - public function save($data) - { - // Associations are not edited in frontend ATM so we have to inherit them - if (Associations::isEnabled() && !empty($data['id']) - && $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $data['id'])) - { - foreach ($associations as $tag => $associated) - { - $associations[$tag] = (int) $associated->id; - } - - $data['associations'] = $associations; - } - - return parent::save($data); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 4.0.0 - * - * @throws Exception - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load state from the request. - $pk = $app->input->getInt('id'); - $this->setState('contact.id', $pk); - - $this->setState('contact.catid', $app->input->getInt('catid')); - - $return = $app->input->get('return', '', 'base64'); - $this->setState('return_page', base64_decode($return)); - - // Load the parameters. - $params = $app->getParams(); - $this->setState('params', $params); - - $this->setState('layout', $app->input->getString('layout')); - } - - /** - * Allows preprocessing of the JForm object. - * - * @param Form $form The form object - * @param array $data The data to be merged into the form object - * @param string $group The plugin group to be executed - * - * @return void - * - * @since 4.0.0 - */ - protected function preprocessForm(Form $form, $data, $group = 'contact') - { - if (!Multilanguage::isEnabled()) - { - $form->setFieldAttribute('language', 'type', 'hidden'); - $form->setFieldAttribute('language', 'default', '*'); - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return bool|Table A Table object - * - * @since 4.0.0 - - * @throws Exception - */ - public function getTable($name = 'Contact', $prefix = 'Administrator', $options = array()) - { - return parent::getTable($name, $prefix, $options); - } + /** + * Model typeAlias string. Used for version history. + * + * @var string + * + * @since 4.0.0 + */ + public $typeAlias = 'com_contact.contact'; + + /** + * Name of the form + * + * @var string + * + * @since 4.0.0 + */ + protected $formName = 'form'; + + /** + * Method to get the row form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 4.0.0 + */ + public function getForm($data = array(), $loadData = true) + { + $form = parent::getForm($data, $loadData); + + // Prevent messing with article language and category when editing existing contact with associations + if ($id = $this->getState('contact.id') && Associations::isEnabled()) { + $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $id); + + // Make fields read only + if (!empty($associations)) { + $form->setFieldAttribute('language', 'readonly', 'true'); + $form->setFieldAttribute('language', 'filter', 'unset'); + } + } + + return $form; + } + + /** + * Method to get contact data. + * + * @param integer $itemId The id of the contact. + * + * @return mixed Contact item data object on success, false on failure. + * + * @throws Exception + * + * @since 4.0.0 + */ + public function getItem($itemId = null) + { + $itemId = (int) (!empty($itemId)) ? $itemId : $this->getState('contact.id'); + + // Get a row instance. + $table = $this->getTable(); + + // Attempt to load the row. + try { + if (!$table->load($itemId)) { + return false; + } + } catch (Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage()); + + return false; + } + + $properties = $table->getProperties(); + $value = ArrayHelper::toObject($properties, \Joomla\CMS\Object\CMSObject::class); + + // Convert field to Registry. + $value->params = new Registry($value->params); + + // Convert the metadata field to an array. + $registry = new Registry($value->metadata); + $value->metadata = $registry->toArray(); + + if ($itemId) { + $value->tags = new TagsHelper(); + $value->tags->getTagIds($value->id, 'com_contact.contact'); + $value->metadata['tags'] = $value->tags; + } + + return $value; + } + + /** + * Get the return URL. + * + * @return string The return URL. + * + * @since 4.0.0 + */ + public function getReturnPage() + { + return base64_encode($this->getState('return_page', '')); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 4.0.0 + * + * @throws Exception + */ + public function save($data) + { + // Associations are not edited in frontend ATM so we have to inherit them + if ( + Associations::isEnabled() && !empty($data['id']) + && $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $data['id']) + ) { + foreach ($associations as $tag => $associated) { + $associations[$tag] = (int) $associated->id; + } + + $data['associations'] = $associations; + } + + return parent::save($data); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 4.0.0 + * + * @throws Exception + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load state from the request. + $pk = $app->input->getInt('id'); + $this->setState('contact.id', $pk); + + $this->setState('contact.catid', $app->input->getInt('catid')); + + $return = $app->input->get('return', '', 'base64'); + $this->setState('return_page', base64_decode($return)); + + // Load the parameters. + $params = $app->getParams(); + $this->setState('params', $params); + + $this->setState('layout', $app->input->getString('layout')); + } + + /** + * Allows preprocessing of the JForm object. + * + * @param Form $form The form object + * @param array $data The data to be merged into the form object + * @param string $group The plugin group to be executed + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocessForm(Form $form, $data, $group = 'contact') + { + if (!Multilanguage::isEnabled()) { + $form->setFieldAttribute('language', 'type', 'hidden'); + $form->setFieldAttribute('language', 'default', '*'); + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return bool|Table A Table object + * + * @since 4.0.0 + + * @throws Exception + */ + public function getTable($name = 'Contact', $prefix = 'Administrator', $options = array()) + { + return parent::getTable($name, $prefix, $options); + } } diff --git a/components/com_contact/src/Rule/ContactEmailMessageRule.php b/components/com_contact/src/Rule/ContactEmailMessageRule.php index 3bbfb5ea1ad58..32db59edee1f3 100644 --- a/components/com_contact/src/Rule/ContactEmailMessageRule.php +++ b/components/com_contact/src/Rule/ContactEmailMessageRule.php @@ -1,4 +1,5 @@ tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise. - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - $params = ComponentHelper::getParams('com_contact'); - $banned = $params->get('banned_text'); + /** + * Method to test a message for banned words + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise. + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + $params = ComponentHelper::getParams('com_contact'); + $banned = $params->get('banned_text'); - if ($banned) - { - foreach (explode(';', $banned) as $item) - { - if ($item != '' && StringHelper::stristr($value, $item) !== false) - { - return false; - } - } - } + if ($banned) { + foreach (explode(';', $banned) as $item) { + if ($item != '' && StringHelper::stristr($value, $item) !== false) { + return false; + } + } + } - return true; - } + return true; + } } diff --git a/components/com_contact/src/Rule/ContactEmailRule.php b/components/com_contact/src/Rule/ContactEmailRule.php index f9bdf9d986ce4..c78de628e2cac 100644 --- a/components/com_contact/src/Rule/ContactEmailRule.php +++ b/components/com_contact/src/Rule/ContactEmailRule.php @@ -1,4 +1,5 @@ tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise. - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - if (!parent::test($element, $value, $group, $input, $form)) - { - return false; - } - - $params = ComponentHelper::getParams('com_contact'); - $banned = $params->get('banned_email'); - - if ($banned) - { - foreach (explode(';', $banned) as $item) - { - if ($item != '' && StringHelper::stristr($value, $item) !== false) - { - return false; - } - } - } - - return true; - } + /** + * Method to test for banned email addresses + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise. + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + if (!parent::test($element, $value, $group, $input, $form)) { + return false; + } + + $params = ComponentHelper::getParams('com_contact'); + $banned = $params->get('banned_email'); + + if ($banned) { + foreach (explode(';', $banned) as $item) { + if ($item != '' && StringHelper::stristr($value, $item) !== false) { + return false; + } + } + } + + return true; + } } diff --git a/components/com_contact/src/Rule/ContactEmailSubjectRule.php b/components/com_contact/src/Rule/ContactEmailSubjectRule.php index cc3ea2247d182..9954d766423c9 100644 --- a/components/com_contact/src/Rule/ContactEmailSubjectRule.php +++ b/components/com_contact/src/Rule/ContactEmailSubjectRule.php @@ -1,4 +1,5 @@ tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - $params = ComponentHelper::getParams('com_contact'); - $banned = $params->get('banned_subject'); + /** + * Method to test for a banned subject + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + $params = ComponentHelper::getParams('com_contact'); + $banned = $params->get('banned_subject'); - if ($banned) - { - foreach (explode(';', $banned) as $item) - { - if ($item != '' && StringHelper::stristr($value, $item) !== false) - { - return false; - } - } - } + if ($banned) { + foreach (explode(';', $banned) as $item) { + if ($item != '' && StringHelper::stristr($value, $item) !== false) { + return false; + } + } + } - return true; - } + return true; + } } diff --git a/components/com_contact/src/Service/Category.php b/components/com_contact/src/Service/Category.php index 0b45769ec178c..58e4e2b166a29 100644 --- a/components/com_contact/src/Service/Category.php +++ b/components/com_contact/src/Service/Category.php @@ -1,4 +1,5 @@ categoryFactory = $categoryFactory; - $this->db = $db; - - $params = ComponentHelper::getParams('com_contact'); - $this->noIDs = (bool) $params->get('sef_ids'); - $categories = new RouterViewConfiguration('categories'); - $categories->setKey('id'); - $this->registerView($categories); - $category = new RouterViewConfiguration('category'); - $category->setKey('id')->setParent($categories, 'catid')->setNestable(); - $this->registerView($category); - $contact = new RouterViewConfiguration('contact'); - $contact->setKey('id')->setParent($category, 'catid'); - $this->registerView($contact); - $this->registerView(new RouterViewConfiguration('featured')); - $form = new RouterViewConfiguration('form'); - $form->setKey('id'); - $this->registerView($form); - - parent::__construct($app, $menu); - - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategorySegment($id, $query) - { - $category = $this->getCategories()->get($id); - - if ($category) - { - $path = array_reverse($category->getPath(), true); - $path[0] = '1:root'; - - if ($this->noIDs) - { - foreach ($path as &$segment) - { - list($id, $segment) = explode(':', $segment, 2); - } - } - - return $path; - } - - return array(); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategoriesSegment($id, $query) - { - return $this->getCategorySegment($id, $query); - } - - /** - * Method to get the segment(s) for a contact - * - * @param string $id ID of the contact to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getContactSegment($id, $query) - { - if (!strpos($id, ':')) - { - $id = (int) $id; - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('alias')) - ->from($this->db->quoteName('#__contact_details')) - ->where($this->db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - $id .= ':' . $this->db->loadResult(); - } - - if ($this->noIDs) - { - list($void, $segment) = explode(':', $id, 2); - - return array($void => $segment); - } - - return array((int) $id => $id); - } - - /** - * Method to get the segment(s) for a form - * - * @param string $id ID of the contact form to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - * - * @since 4.0.0 - */ - public function getFormSegment($id, $query) - { - return $this->getContactSegment($id, $query); - } - - /** - * Method to get the id for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoryId($segment, $query) - { - if (isset($query['id'])) - { - $category = $this->getCategories(['access' => false])->get($query['id']); - - if ($category) - { - foreach ($category->getChildren() as $child) - { - if ($this->noIDs) - { - if ($child->alias == $segment) - { - return $child->id; - } - } - else - { - if ($child->id == (int) $segment) - { - return $child->id; - } - } - } - } - } - - return false; - } - - /** - * Method to get the segment(s) for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoriesId($segment, $query) - { - return $this->getCategoryId($segment, $query); - } - - /** - * Method to get the segment(s) for a contact - * - * @param string $segment Segment of the contact to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getContactId($segment, $query) - { - if ($this->noIDs) - { - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('id')) - ->from($this->db->quoteName('#__contact_details')) - ->where( - [ - $this->db->quoteName('alias') . ' = :alias', - $this->db->quoteName('catid') . ' = :catid', - ] - ) - ->bind(':alias', $segment) - ->bind(':catid', $query['id'], ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - return (int) $this->db->loadResult(); - } - - return (int) $segment; - } - - /** - * Method to get categories from cache - * - * @param array $options The options for retrieving categories - * - * @return CategoryInterface The object containing categories - * - * @since 4.0.0 - */ - private function getCategories(array $options = []): CategoryInterface - { - $key = serialize($options); - - if (!isset($this->categoryCache[$key])) - { - $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); - } - - return $this->categoryCache[$key]; - } + /** + * Flag to remove IDs + * + * @var boolean + */ + protected $noIDs = false; + + /** + * The category factory + * + * @var CategoryFactoryInterface + * + * @since 4.0.0 + */ + private $categoryFactory; + + /** + * The category cache + * + * @var array + * + * @since 4.0.0 + */ + private $categoryCache = []; + + /** + * The db + * + * @var DatabaseInterface + * + * @since 4.0.0 + */ + private $db; + + /** + * Content Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + * @param CategoryFactoryInterface $categoryFactory The category object + * @param DatabaseInterface $db The database object + */ + public function __construct(SiteApplication $app, AbstractMenu $menu, CategoryFactoryInterface $categoryFactory, DatabaseInterface $db) + { + $this->categoryFactory = $categoryFactory; + $this->db = $db; + + $params = ComponentHelper::getParams('com_contact'); + $this->noIDs = (bool) $params->get('sef_ids'); + $categories = new RouterViewConfiguration('categories'); + $categories->setKey('id'); + $this->registerView($categories); + $category = new RouterViewConfiguration('category'); + $category->setKey('id')->setParent($categories, 'catid')->setNestable(); + $this->registerView($category); + $contact = new RouterViewConfiguration('contact'); + $contact->setKey('id')->setParent($category, 'catid'); + $this->registerView($contact); + $this->registerView(new RouterViewConfiguration('featured')); + $form = new RouterViewConfiguration('form'); + $form->setKey('id'); + $this->registerView($form); + + parent::__construct($app, $menu); + + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategorySegment($id, $query) + { + $category = $this->getCategories()->get($id); + + if ($category) { + $path = array_reverse($category->getPath(), true); + $path[0] = '1:root'; + + if ($this->noIDs) { + foreach ($path as &$segment) { + list($id, $segment) = explode(':', $segment, 2); + } + } + + return $path; + } + + return array(); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategoriesSegment($id, $query) + { + return $this->getCategorySegment($id, $query); + } + + /** + * Method to get the segment(s) for a contact + * + * @param string $id ID of the contact to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getContactSegment($id, $query) + { + if (!strpos($id, ':')) { + $id = (int) $id; + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('alias')) + ->from($this->db->quoteName('#__contact_details')) + ->where($this->db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + $id .= ':' . $this->db->loadResult(); + } + + if ($this->noIDs) { + list($void, $segment) = explode(':', $id, 2); + + return array($void => $segment); + } + + return array((int) $id => $id); + } + + /** + * Method to get the segment(s) for a form + * + * @param string $id ID of the contact form to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + * + * @since 4.0.0 + */ + public function getFormSegment($id, $query) + { + return $this->getContactSegment($id, $query); + } + + /** + * Method to get the id for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoryId($segment, $query) + { + if (isset($query['id'])) { + $category = $this->getCategories(['access' => false])->get($query['id']); + + if ($category) { + foreach ($category->getChildren() as $child) { + if ($this->noIDs) { + if ($child->alias == $segment) { + return $child->id; + } + } else { + if ($child->id == (int) $segment) { + return $child->id; + } + } + } + } + } + + return false; + } + + /** + * Method to get the segment(s) for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoriesId($segment, $query) + { + return $this->getCategoryId($segment, $query); + } + + /** + * Method to get the segment(s) for a contact + * + * @param string $segment Segment of the contact to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getContactId($segment, $query) + { + if ($this->noIDs) { + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__contact_details')) + ->where( + [ + $this->db->quoteName('alias') . ' = :alias', + $this->db->quoteName('catid') . ' = :catid', + ] + ) + ->bind(':alias', $segment) + ->bind(':catid', $query['id'], ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + return (int) $this->db->loadResult(); + } + + return (int) $segment; + } + + /** + * Method to get categories from cache + * + * @param array $options The options for retrieving categories + * + * @return CategoryInterface The object containing categories + * + * @since 4.0.0 + */ + private function getCategories(array $options = []): CategoryInterface + { + $key = serialize($options); + + if (!isset($this->categoryCache[$key])) { + $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); + } + + return $this->categoryCache[$key]; + } } diff --git a/components/com_contact/src/View/Categories/HtmlView.php b/components/com_contact/src/View/Categories/HtmlView.php index 9f447cbe98f68..99565fc18dd7b 100644 --- a/components/com_contact/src/View/Categories/HtmlView.php +++ b/components/com_contact/src/View/Categories/HtmlView.php @@ -1,4 +1,5 @@ description = $item->address; - } + $item->description = $item->address; + } } diff --git a/components/com_contact/src/View/Category/HtmlView.php b/components/com_contact/src/View/Category/HtmlView.php index 74bf75ca8c40c..d023f40b50c34 100644 --- a/components/com_contact/src/View/Category/HtmlView.php +++ b/components/com_contact/src/View/Category/HtmlView.php @@ -1,4 +1,5 @@ pagination->hideEmptyLimitstart = true; - - // Prepare the data. - // Compute the contact slug. - foreach ($this->items as $item) - { - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - $temp = $item->params; - $item->params = clone $this->params; - $item->params->merge($temp); - - if ($item->params->get('show_email_headings', 0) == 1) - { - $item->email_to = trim($item->email_to); - - if (!empty($item->email_to) && MailHelper::isEmailAddress($item->email_to)) - { - $item->email_to = HTMLHelper::_('email.cloak', $item->email_to); - } - else - { - $item->email_to = ''; - } - } - } - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function prepareDocument() - { - parent::prepareDocument(); - - parent::addFeed(); - - if ($this->menuItemMatchCategory) - { - // If the active menu item is linked directly to the category being displayed, no further process is needed - return; - } - - // Get ID of the category from active menu item - $menu = $this->menu; - - if ($menu && $menu->component == 'com_contact' && isset($menu->query['view']) - && in_array($menu->query['view'], ['categories', 'category'])) - { - $id = $menu->query['id']; - } - else - { - $id = 0; - } - - $path = [['title' => $this->category->title, 'link' => '']]; - $category = $this->category->getParent(); - - while ($category !== null && $category->id != $id && $category->id !== 'root') - { - $path[] = ['title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)]; - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $this->pathway->addItem($item['title'], $item['link']); - } - } + /** + * @var string The name of the extension for the category + * @since 3.2 + */ + protected $extension = 'com_contact'; + + /** + * @var string Default title to use for page title + * @since 3.2 + */ + protected $defaultPageTitle = 'COM_CONTACT_DEFAULT_PAGE_TITLE'; + + /** + * @var string The name of the view to link individual items to + * @since 3.2 + */ + protected $viewName = 'contact'; + + /** + * Run the standard Joomla plugins + * + * @var boolean + * @since 3.5 + */ + protected $runPlugins = true; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + parent::commonCategoryDisplay(); + + // Flag indicates to not add limitstart=0 to URL + $this->pagination->hideEmptyLimitstart = true; + + // Prepare the data. + // Compute the contact slug. + foreach ($this->items as $item) { + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + $temp = $item->params; + $item->params = clone $this->params; + $item->params->merge($temp); + + if ($item->params->get('show_email_headings', 0) == 1) { + $item->email_to = trim($item->email_to); + + if (!empty($item->email_to) && MailHelper::isEmailAddress($item->email_to)) { + $item->email_to = HTMLHelper::_('email.cloak', $item->email_to); + } else { + $item->email_to = ''; + } + } + } + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function prepareDocument() + { + parent::prepareDocument(); + + parent::addFeed(); + + if ($this->menuItemMatchCategory) { + // If the active menu item is linked directly to the category being displayed, no further process is needed + return; + } + + // Get ID of the category from active menu item + $menu = $this->menu; + + if ( + $menu && $menu->component == 'com_contact' && isset($menu->query['view']) + && in_array($menu->query['view'], ['categories', 'category']) + ) { + $id = $menu->query['id']; + } else { + $id = 0; + } + + $path = [['title' => $this->category->title, 'link' => '']]; + $category = $this->category->getParent(); + + while ($category !== null && $category->id != $id && $category->id !== 'root') { + $path[] = ['title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)]; + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $this->pathway->addItem($item['title'], $item['link']); + } + } } diff --git a/components/com_contact/src/View/Contact/HtmlView.php b/components/com_contact/src/View/Contact/HtmlView.php index 0419adf686b98..f74bdaaf848b8 100644 --- a/components/com_contact/src/View/Contact/HtmlView.php +++ b/components/com_contact/src/View/Contact/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser(); - $state = $this->get('State'); - $item = $this->get('Item'); - $this->form = $this->get('Form'); - $params = $state->get('params'); - $contacts = []; - - $temp = clone $params; - - $active = $app->getMenu()->getActive(); - - // If the current view is the active item and a contact view for this contact, then the menu item params take priority - if ($active - && $active->component == 'com_contact' - && isset($active->query['view'], $active->query['id']) - && $active->query['view'] == 'contact' - && $active->query['id'] == $item->id) - { - $this->menuItemMatchContact = true; - - // Load layout from active query (in case it is an alternative menu item) - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - // Check for alternative layout of contact - elseif ($layout = $item->params->get('contact_layout')) - { - $this->setLayout($layout); - } - - $item->params->merge($temp); - } - else - { - // Merge so that contact params take priority - $temp->merge($item->params); - $item->params = $temp; - - if ($layout = $item->params->get('contact_layout')) - { - $this->setLayout($layout); - } - } - - // Collect extra contact information when this information is required - if ($item && $item->params->get('show_contact_list')) - { - // Get Category Model data - /** @var \Joomla\Component\Contact\Site\Model\CategoryModel $categoryModel */ - $categoryModel = $app->bootComponent('com_contact')->getMVCFactory() - ->createModel('Category', 'Site', ['ignore_request' => true]); - - $categoryModel->setState('category.id', $item->catid); - $categoryModel->setState('list.ordering', 'a.name'); - $categoryModel->setState('list.direction', 'asc'); - $categoryModel->setState('filter.published', 1); - - $contacts = $categoryModel->getItems(); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check if access is not public - $groups = $user->getAuthorisedViewLevels(); - - if (!in_array($item->access, $groups) || !in_array($item->category_access, $groups)) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return false; - } - - $options['category_id'] = $item->catid; - $options['order by'] = 'a.default_con DESC, a.ordering ASC'; - - /** - * Handle email cloaking - * - * Keep a copy of the raw email address so it can - * still be accessed in the layout if needed. - */ - $item->email_raw = $item->email_to; - - if ($item->email_to && $item->params->get('show_email')) - { - $item->email_to = HTMLHelper::_('email.cloak', $item->email_to, (bool) $item->params->get('add_mailto_link', true)); - } - - if ($item->params->get('show_street_address') || $item->params->get('show_suburb') || $item->params->get('show_state') - || $item->params->get('show_postcode') || $item->params->get('show_country')) - { - if (!empty($item->address) || !empty($item->suburb) || !empty($item->state) || !empty($item->country) || !empty($item->postcode)) - { - $item->params->set('address_check', 1); - } - } - else - { - $item->params->set('address_check', 0); - } - - // Manage the display mode for contact detail groups - switch ($item->params->get('contact_icons')) - { - case 1: - // Text - $item->params->set('marker_address', Text::_('COM_CONTACT_ADDRESS') . ': '); - $item->params->set('marker_email', Text::_('JGLOBAL_EMAIL') . ': '); - $item->params->set('marker_telephone', Text::_('COM_CONTACT_TELEPHONE') . ': '); - $item->params->set('marker_fax', Text::_('COM_CONTACT_FAX') . ': '); - $item->params->set('marker_mobile', Text::_('COM_CONTACT_MOBILE') . ': '); - $item->params->set('marker_webpage', Text::_('COM_CONTACT_WEBPAGE') . ': '); - $item->params->set('marker_misc', Text::_('COM_CONTACT_OTHER_INFORMATION') . ': '); - $item->params->set('marker_class', 'jicons-text'); - break; - - case 2: - // None - $item->params->set('marker_address', ''); - $item->params->set('marker_email', ''); - $item->params->set('marker_telephone', ''); - $item->params->set('marker_mobile', ''); - $item->params->set('marker_fax', ''); - $item->params->set('marker_misc', ''); - $item->params->set('marker_webpage', ''); - $item->params->set('marker_class', 'jicons-none'); - break; - - default: - if ($item->params->get('icon_address')) - { - $item->params->set( - 'marker_address', - HTMLHelper::_('image', $item->params->get('icon_address', ''), Text::_('COM_CONTACT_ADDRESS'), false) - ); - } - - if ($item->params->get('icon_email')) - { - $item->params->set( - 'marker_email', - HTMLHelper::_('image', $item->params->get('icon_email', ''), Text::_('COM_CONTACT_EMAIL'), false) - ); - } - - if ($item->params->get('icon_telephone')) - { - $item->params->set( - 'marker_telephone', - HTMLHelper::_('image', $item->params->get('icon_telephone', ''), Text::_('COM_CONTACT_TELEPHONE'), false) - ); - } - - if ($item->params->get('icon_fax', '')) - { - $item->params->set( - 'marker_fax', - HTMLHelper::_('image', $item->params->get('icon_fax', ''), Text::_('COM_CONTACT_FAX'), false) - ); - } - - if ($item->params->get('icon_misc')) - { - $item->params->set( - 'marker_misc', - HTMLHelper::_('image', $item->params->get('icon_misc', ''), Text::_('COM_CONTACT_OTHER_INFORMATION'), false) - ); - } - - if ($item->params->get('icon_mobile')) - { - $item->params->set( - 'marker_mobile', - HTMLHelper::_('image', $item->params->get('icon_mobile', ''), Text::_('COM_CONTACT_MOBILE'), false) - ); - } - - if ($item->params->get('icon_webpage')) - { - $item->params->set( - 'marker_webpage', - HTMLHelper::_('image', $item->params->get('icon_webpage', ''), Text::_('COM_CONTACT_WEBPAGE'), false) - ); - } - - $item->params->set('marker_class', 'jicons-icons'); - break; - } - - // Add links to contacts - if ($item->params->get('show_contact_list') && count($contacts) > 1) - { - foreach ($contacts as &$contact) - { - $contact->link = Route::_(RouteHelper::getContactRoute($contact->slug, $contact->catid, $contact->language)); - } - - $item->link = Route::_(RouteHelper::getContactRoute($item->slug, $item->catid, $item->language), false); - } - - // Process the content plugins. - PluginHelper::importPlugin('content'); - $offset = $state->get('list.offset'); - - // Fix for where some plugins require a text attribute - $item->text = ''; - - if (!empty($item->misc)) - { - $item->text = $item->misc; - } - - $app->triggerEvent('onContentPrepare', array ('com_contact.contact', &$item, &$item->params, $offset)); - - // Store the events for later - $item->event = new \stdClass; - $results = $app->triggerEvent('onContentAfterTitle', array('com_contact.contact', &$item, &$item->params, $offset)); - $item->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = $app->triggerEvent('onContentBeforeDisplay', array('com_contact.contact', &$item, &$item->params, $offset)); - $item->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = $app->triggerEvent('onContentAfterDisplay', array('com_contact.contact', &$item, &$item->params, $offset)); - $item->event->afterDisplayContent = trim(implode("\n", $results)); - - if (!empty($item->text)) - { - $item->misc = $item->text; - } - - $contactUser = null; - - if ($item->params->get('show_user_custom_fields') && $item->user_id && $contactUser = Factory::getUser($item->user_id)) - { - $contactUser->text = ''; - $app->triggerEvent('onContentPrepare', array ('com_users.user', &$contactUser, &$item->params, 0)); - - if (!isset($contactUser->jcfields)) - { - $contactUser->jcfields = array(); - } - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($item->params->get('pageclass_sfx', '')); - - $this->params = &$item->params; - $this->state = &$state; - $this->item = &$item; - $this->user = &$user; - $this->contacts = &$contacts; - $this->contactUser = $contactUser; - - $model = $this->getModel(); - $model->hit(); - - $captchaSet = $item->params->get('captcha', $app->get('captcha', '0')); - - foreach (PluginHelper::getPlugin('captcha') as $plugin) - { - if ($captchaSet === $plugin->name) - { - $this->captchaEnabled = true; - break; - } - } - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @since 1.6 - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - $pathway = $app->getPathway(); - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_CONTACT_DEFAULT_PAGE_TITLE')); - } - - $title = $this->params->get('page_title', ''); - - // If the menu item does not concern this contact - if (!$this->menuItemMatchContact) - { - // If this is not a single contact menu item, set the page title to the contact title - if ($this->item->name) - { - $title = $this->item->name; - } - - // Get ID of the category from active menu item - if ($menu && $menu->component == 'com_contact' && isset($menu->query['view']) - && in_array($menu->query['view'], ['categories', 'category'])) - { - $id = $menu->query['id']; - } - else - { - $id = 0; - } - - $path = array(array('title' => $this->item->name, 'link' => '')); - $category = Categories::getInstance('Contact')->get($this->item->catid); - - while ($category !== null && $category->id != $id && $category->id !== 'root') - { - $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $pathway->addItem($item['title'], $item['link']); - } - } - - if (empty($title)) - { - $title = $this->item->name; - } - - $this->setDocumentTitle($title); - - if ($this->item->metadesc) - { - $this->document->setDescription($this->item->metadesc); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - $mdata = $this->item->metadata->toArray(); - - foreach ($mdata as $k => $v) - { - if ($v) - { - $this->document->setMetaData($k, $v); - } - } - } + /** + * The item model state + * + * @var \Joomla\Registry\Registry + * + * @since 1.6 + */ + protected $state; + + /** + * The form object for the contact item + * + * @var \Joomla\CMS\Form\Form + * + * @since 1.6 + */ + protected $form; + + /** + * The item object details + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 1.6 + */ + protected $item; + + /** + * The page to return to on submission + * + * @var string + * + * @since 1.6 + * + * @todo Implement this functionality + */ + protected $return_page = ''; + + /** + * Should we show a captcha form for the submission of the contact request? + * + * @var boolean + * + * @since 3.6.3 + */ + protected $captchaEnabled = false; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * The user object + * + * @var \Joomla\CMS\User\User + * + * @since 4.0.0 + */ + protected $user; + + /** + * Other contacts in this contacts category + * + * @var array + * + * @since 4.0.0 + */ + protected $contacts; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The flag to mark if the active menu item is linked to the contact being displayed + * + * @var boolean + * + * @since 4.0.0 + */ + protected $menuItemMatchContact = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void|boolean + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $user = $this->getCurrentUser(); + $state = $this->get('State'); + $item = $this->get('Item'); + $this->form = $this->get('Form'); + $params = $state->get('params'); + $contacts = []; + + $temp = clone $params; + + $active = $app->getMenu()->getActive(); + + // If the current view is the active item and a contact view for this contact, then the menu item params take priority + if ( + $active + && $active->component == 'com_contact' + && isset($active->query['view'], $active->query['id']) + && $active->query['view'] == 'contact' + && $active->query['id'] == $item->id + ) { + $this->menuItemMatchContact = true; + + // Load layout from active query (in case it is an alternative menu item) + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + // Check for alternative layout of contact + elseif ($layout = $item->params->get('contact_layout')) { + $this->setLayout($layout); + } + + $item->params->merge($temp); + } else { + // Merge so that contact params take priority + $temp->merge($item->params); + $item->params = $temp; + + if ($layout = $item->params->get('contact_layout')) { + $this->setLayout($layout); + } + } + + // Collect extra contact information when this information is required + if ($item && $item->params->get('show_contact_list')) { + // Get Category Model data + /** @var \Joomla\Component\Contact\Site\Model\CategoryModel $categoryModel */ + $categoryModel = $app->bootComponent('com_contact')->getMVCFactory() + ->createModel('Category', 'Site', ['ignore_request' => true]); + + $categoryModel->setState('category.id', $item->catid); + $categoryModel->setState('list.ordering', 'a.name'); + $categoryModel->setState('list.direction', 'asc'); + $categoryModel->setState('filter.published', 1); + + $contacts = $categoryModel->getItems(); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check if access is not public + $groups = $user->getAuthorisedViewLevels(); + + if (!in_array($item->access, $groups) || !in_array($item->category_access, $groups)) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return false; + } + + $options['category_id'] = $item->catid; + $options['order by'] = 'a.default_con DESC, a.ordering ASC'; + + /** + * Handle email cloaking + * + * Keep a copy of the raw email address so it can + * still be accessed in the layout if needed. + */ + $item->email_raw = $item->email_to; + + if ($item->email_to && $item->params->get('show_email')) { + $item->email_to = HTMLHelper::_('email.cloak', $item->email_to, (bool) $item->params->get('add_mailto_link', true)); + } + + if ( + $item->params->get('show_street_address') || $item->params->get('show_suburb') || $item->params->get('show_state') + || $item->params->get('show_postcode') || $item->params->get('show_country') + ) { + if (!empty($item->address) || !empty($item->suburb) || !empty($item->state) || !empty($item->country) || !empty($item->postcode)) { + $item->params->set('address_check', 1); + } + } else { + $item->params->set('address_check', 0); + } + + // Manage the display mode for contact detail groups + switch ($item->params->get('contact_icons')) { + case 1: + // Text + $item->params->set('marker_address', Text::_('COM_CONTACT_ADDRESS') . ': '); + $item->params->set('marker_email', Text::_('JGLOBAL_EMAIL') . ': '); + $item->params->set('marker_telephone', Text::_('COM_CONTACT_TELEPHONE') . ': '); + $item->params->set('marker_fax', Text::_('COM_CONTACT_FAX') . ': '); + $item->params->set('marker_mobile', Text::_('COM_CONTACT_MOBILE') . ': '); + $item->params->set('marker_webpage', Text::_('COM_CONTACT_WEBPAGE') . ': '); + $item->params->set('marker_misc', Text::_('COM_CONTACT_OTHER_INFORMATION') . ': '); + $item->params->set('marker_class', 'jicons-text'); + break; + + case 2: + // None + $item->params->set('marker_address', ''); + $item->params->set('marker_email', ''); + $item->params->set('marker_telephone', ''); + $item->params->set('marker_mobile', ''); + $item->params->set('marker_fax', ''); + $item->params->set('marker_misc', ''); + $item->params->set('marker_webpage', ''); + $item->params->set('marker_class', 'jicons-none'); + break; + + default: + if ($item->params->get('icon_address')) { + $item->params->set( + 'marker_address', + HTMLHelper::_('image', $item->params->get('icon_address', ''), Text::_('COM_CONTACT_ADDRESS'), false) + ); + } + + if ($item->params->get('icon_email')) { + $item->params->set( + 'marker_email', + HTMLHelper::_('image', $item->params->get('icon_email', ''), Text::_('COM_CONTACT_EMAIL'), false) + ); + } + + if ($item->params->get('icon_telephone')) { + $item->params->set( + 'marker_telephone', + HTMLHelper::_('image', $item->params->get('icon_telephone', ''), Text::_('COM_CONTACT_TELEPHONE'), false) + ); + } + + if ($item->params->get('icon_fax', '')) { + $item->params->set( + 'marker_fax', + HTMLHelper::_('image', $item->params->get('icon_fax', ''), Text::_('COM_CONTACT_FAX'), false) + ); + } + + if ($item->params->get('icon_misc')) { + $item->params->set( + 'marker_misc', + HTMLHelper::_('image', $item->params->get('icon_misc', ''), Text::_('COM_CONTACT_OTHER_INFORMATION'), false) + ); + } + + if ($item->params->get('icon_mobile')) { + $item->params->set( + 'marker_mobile', + HTMLHelper::_('image', $item->params->get('icon_mobile', ''), Text::_('COM_CONTACT_MOBILE'), false) + ); + } + + if ($item->params->get('icon_webpage')) { + $item->params->set( + 'marker_webpage', + HTMLHelper::_('image', $item->params->get('icon_webpage', ''), Text::_('COM_CONTACT_WEBPAGE'), false) + ); + } + + $item->params->set('marker_class', 'jicons-icons'); + break; + } + + // Add links to contacts + if ($item->params->get('show_contact_list') && count($contacts) > 1) { + foreach ($contacts as &$contact) { + $contact->link = Route::_(RouteHelper::getContactRoute($contact->slug, $contact->catid, $contact->language)); + } + + $item->link = Route::_(RouteHelper::getContactRoute($item->slug, $item->catid, $item->language), false); + } + + // Process the content plugins. + PluginHelper::importPlugin('content'); + $offset = $state->get('list.offset'); + + // Fix for where some plugins require a text attribute + $item->text = ''; + + if (!empty($item->misc)) { + $item->text = $item->misc; + } + + $app->triggerEvent('onContentPrepare', array ('com_contact.contact', &$item, &$item->params, $offset)); + + // Store the events for later + $item->event = new \stdClass(); + $results = $app->triggerEvent('onContentAfterTitle', array('com_contact.contact', &$item, &$item->params, $offset)); + $item->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = $app->triggerEvent('onContentBeforeDisplay', array('com_contact.contact', &$item, &$item->params, $offset)); + $item->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = $app->triggerEvent('onContentAfterDisplay', array('com_contact.contact', &$item, &$item->params, $offset)); + $item->event->afterDisplayContent = trim(implode("\n", $results)); + + if (!empty($item->text)) { + $item->misc = $item->text; + } + + $contactUser = null; + + if ($item->params->get('show_user_custom_fields') && $item->user_id && $contactUser = Factory::getUser($item->user_id)) { + $contactUser->text = ''; + $app->triggerEvent('onContentPrepare', array ('com_users.user', &$contactUser, &$item->params, 0)); + + if (!isset($contactUser->jcfields)) { + $contactUser->jcfields = array(); + } + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($item->params->get('pageclass_sfx', '')); + + $this->params = &$item->params; + $this->state = &$state; + $this->item = &$item; + $this->user = &$user; + $this->contacts = &$contacts; + $this->contactUser = $contactUser; + + $model = $this->getModel(); + $model->hit(); + + $captchaSet = $item->params->get('captcha', $app->get('captcha', '0')); + + foreach (PluginHelper::getPlugin('captcha') as $plugin) { + if ($captchaSet === $plugin->name) { + $this->captchaEnabled = true; + break; + } + } + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @since 1.6 + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + $pathway = $app->getPathway(); + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_CONTACT_DEFAULT_PAGE_TITLE')); + } + + $title = $this->params->get('page_title', ''); + + // If the menu item does not concern this contact + if (!$this->menuItemMatchContact) { + // If this is not a single contact menu item, set the page title to the contact title + if ($this->item->name) { + $title = $this->item->name; + } + + // Get ID of the category from active menu item + if ( + $menu && $menu->component == 'com_contact' && isset($menu->query['view']) + && in_array($menu->query['view'], ['categories', 'category']) + ) { + $id = $menu->query['id']; + } else { + $id = 0; + } + + $path = array(array('title' => $this->item->name, 'link' => '')); + $category = Categories::getInstance('Contact')->get($this->item->catid); + + while ($category !== null && $category->id != $id && $category->id !== 'root') { + $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $pathway->addItem($item['title'], $item['link']); + } + } + + if (empty($title)) { + $title = $this->item->name; + } + + $this->setDocumentTitle($title); + + if ($this->item->metadesc) { + $this->document->setDescription($this->item->metadesc); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + $mdata = $this->item->metadata->toArray(); + + foreach ($mdata as $k => $v) { + if ($v) { + $this->document->setMetaData($k, $v); + } + } + } } diff --git a/components/com_contact/src/View/Contact/VcfView.php b/components/com_contact/src/View/Contact/VcfView.php index 14d05d9745869..f5627d2946593 100644 --- a/components/com_contact/src/View/Contact/VcfView.php +++ b/components/com_contact/src/View/Contact/VcfView.php @@ -1,4 +1,5 @@ get('Item'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->document->setMimeEncoding('text/directory', true); - - // Compute lastname, firstname and middlename - $item->name = trim($item->name); - - // "Lastname, Firstname Middlename" format support - // e.g. "de Gaulle, Charles" - $namearray = explode(',', $item->name); - - if (count($namearray) > 1) - { - $lastname = $namearray[0]; - $card_name = $lastname; - $name_and_midname = trim($namearray[1]); - - $firstname = ''; - - if (!empty($name_and_midname)) - { - $namearray = explode(' ', $name_and_midname); - - $firstname = $namearray[0]; - $middlename = (count($namearray) > 1) ? $namearray[1] : ''; - $card_name = $firstname . ' ' . ($middlename ? $middlename . ' ' : '') . $card_name; - } - } - // "Firstname Middlename Lastname" format support - else - { - $namearray = explode(' ', $item->name); - - $middlename = (count($namearray) > 2) ? $namearray[1] : ''; - $firstname = array_shift($namearray); - $lastname = count($namearray) ? end($namearray) : ''; - $card_name = $firstname . ($middlename ? ' ' . $middlename : '') . ($lastname ? ' ' . $lastname : ''); - } - - $rev = date('c', strtotime($item->modified)); - - Factory::getApplication()->setHeader('Content-disposition', 'attachment; filename="' . $card_name . '.vcf"', true); - - $vcard = []; - $vcard[] .= 'BEGIN:VCARD'; - $vcard[] .= 'VERSION:3.0'; - $vcard[] = 'N:' . $lastname . ';' . $firstname . ';' . $middlename; - $vcard[] = 'FN:' . $item->name; - $vcard[] = 'TITLE:' . $item->con_position; - $vcard[] = 'TEL;TYPE=WORK,VOICE:' . $item->telephone; - $vcard[] = 'TEL;TYPE=WORK,FAX:' . $item->fax; - $vcard[] = 'TEL;TYPE=WORK,MOBILE:' . $item->mobile; - $vcard[] = 'ADR;TYPE=WORK:;;' . $item->address . ';' . $item->suburb . ';' . $item->state . ';' . $item->postcode . ';' . $item->country; - $vcard[] = 'LABEL;TYPE=WORK:' . $item->address . "\n" . $item->suburb . "\n" . $item->state . "\n" . $item->postcode . "\n" . $item->country; - $vcard[] = 'EMAIL;TYPE=PREF,INTERNET:' . $item->email_to; - $vcard[] = 'URL:' . $item->webpage; - $vcard[] = 'REV:' . $rev . 'Z'; - $vcard[] = 'END:VCARD'; - - echo implode("\n", $vcard); - } + /** + * The contact item + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $item; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return string A string if successful + * + * @throws GenericDataException + */ + public function display($tpl = null) + { + // Get model data. + $item = $this->get('Item'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->document->setMimeEncoding('text/directory', true); + + // Compute lastname, firstname and middlename + $item->name = trim($item->name); + + // "Lastname, Firstname Middlename" format support + // e.g. "de Gaulle, Charles" + $namearray = explode(',', $item->name); + + if (count($namearray) > 1) { + $lastname = $namearray[0]; + $card_name = $lastname; + $name_and_midname = trim($namearray[1]); + + $firstname = ''; + + if (!empty($name_and_midname)) { + $namearray = explode(' ', $name_and_midname); + + $firstname = $namearray[0]; + $middlename = (count($namearray) > 1) ? $namearray[1] : ''; + $card_name = $firstname . ' ' . ($middlename ? $middlename . ' ' : '') . $card_name; + } + } + // "Firstname Middlename Lastname" format support + else { + $namearray = explode(' ', $item->name); + + $middlename = (count($namearray) > 2) ? $namearray[1] : ''; + $firstname = array_shift($namearray); + $lastname = count($namearray) ? end($namearray) : ''; + $card_name = $firstname . ($middlename ? ' ' . $middlename : '') . ($lastname ? ' ' . $lastname : ''); + } + + $rev = date('c', strtotime($item->modified)); + + Factory::getApplication()->setHeader('Content-disposition', 'attachment; filename="' . $card_name . '.vcf"', true); + + $vcard = []; + $vcard[] .= 'BEGIN:VCARD'; + $vcard[] .= 'VERSION:3.0'; + $vcard[] = 'N:' . $lastname . ';' . $firstname . ';' . $middlename; + $vcard[] = 'FN:' . $item->name; + $vcard[] = 'TITLE:' . $item->con_position; + $vcard[] = 'TEL;TYPE=WORK,VOICE:' . $item->telephone; + $vcard[] = 'TEL;TYPE=WORK,FAX:' . $item->fax; + $vcard[] = 'TEL;TYPE=WORK,MOBILE:' . $item->mobile; + $vcard[] = 'ADR;TYPE=WORK:;;' . $item->address . ';' . $item->suburb . ';' . $item->state . ';' . $item->postcode . ';' . $item->country; + $vcard[] = 'LABEL;TYPE=WORK:' . $item->address . "\n" . $item->suburb . "\n" . $item->state . "\n" . $item->postcode . "\n" . $item->country; + $vcard[] = 'EMAIL;TYPE=PREF,INTERNET:' . $item->email_to; + $vcard[] = 'URL:' . $item->webpage; + $vcard[] = 'REV:' . $rev . 'Z'; + $vcard[] = 'END:VCARD'; + + echo implode("\n", $vcard); + } } diff --git a/components/com_contact/src/View/Featured/HtmlView.php b/components/com_contact/src/View/Featured/HtmlView.php index 61e370842ed0e..3a5d4a9c0d715 100644 --- a/components/com_contact/src/View/Featured/HtmlView.php +++ b/components/com_contact/src/View/Featured/HtmlView.php @@ -1,4 +1,5 @@ getParams(); - - // Get some data from the models - $state = $this->get('State'); - $items = $this->get('Items'); - $category = $this->get('Category'); - $children = $this->get('Children'); - $parent = $this->get('Parent'); - $pagination = $this->get('Pagination'); - - // Flag indicates to not add limitstart=0 to URL - $pagination->hideEmptyLimitstart = true; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Prepare the data. - // Compute the contact slug. - for ($i = 0, $n = count($items); $i < $n; $i++) - { - $item = &$items[$i]; - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - $temp = $item->params; - $item->params = clone $params; - $item->params->merge($temp); - - if ($item->params->get('show_email', 0) == 1) - { - $item->email_to = trim($item->email_to); - - if (!empty($item->email_to) && MailHelper::isEmailAddress($item->email_to)) - { - $item->email_to = HTMLHelper::_('email.cloak', $item->email_to); - } - else - { - $item->email_to = ''; - } - } - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $maxLevel = $params->get('maxLevel', -1); - $this->maxLevel = &$maxLevel; - $this->state = &$state; - $this->items = &$items; - $this->category = &$category; - $this->children = &$children; - $this->params = &$params; - $this->parent = &$parent; - $this->pagination = &$pagination; - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @since 1.6 - */ - protected function _prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_CONTACT_DEFAULT_PAGE_TITLE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The item model state + * + * @var \Joomla\Registry\Registry + * + * @since 1.6.0 + */ + protected $state; + + /** + * The item details + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 1.6.0 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 1.6.0 + */ + protected $pagination; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * Method to display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $params = $app->getParams(); + + // Get some data from the models + $state = $this->get('State'); + $items = $this->get('Items'); + $category = $this->get('Category'); + $children = $this->get('Children'); + $parent = $this->get('Parent'); + $pagination = $this->get('Pagination'); + + // Flag indicates to not add limitstart=0 to URL + $pagination->hideEmptyLimitstart = true; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Prepare the data. + // Compute the contact slug. + for ($i = 0, $n = count($items); $i < $n; $i++) { + $item = &$items[$i]; + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + $temp = $item->params; + $item->params = clone $params; + $item->params->merge($temp); + + if ($item->params->get('show_email', 0) == 1) { + $item->email_to = trim($item->email_to); + + if (!empty($item->email_to) && MailHelper::isEmailAddress($item->email_to)) { + $item->email_to = HTMLHelper::_('email.cloak', $item->email_to); + } else { + $item->email_to = ''; + } + } + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $maxLevel = $params->get('maxLevel', -1); + $this->maxLevel = &$maxLevel; + $this->state = &$state; + $this->items = &$items; + $this->category = &$category; + $this->children = &$children; + $this->params = &$params; + $this->parent = &$parent; + $this->pagination = &$pagination; + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @since 1.6 + */ + protected function _prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_CONTACT_DEFAULT_PAGE_TITLE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_contact/src/View/Form/HtmlView.php b/components/com_contact/src/View/Form/HtmlView.php index 498db543fae8c..860606f7d3d2e 100644 --- a/components/com_contact/src/View/Form/HtmlView.php +++ b/components/com_contact/src/View/Form/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser(); - $app = Factory::getApplication(); - - // Get model data. - $this->state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - $this->return_page = $this->get('ReturnPage'); - - if (empty($this->item->id)) - { - $authorised = $user->authorise('core.create', 'com_contact') || count($user->getAuthorisedCategories('com_contact', 'core.create')); - } - else - { - // Since we don't track these assets at the item level, use the category id. - $canDo = ContactHelper::getActions('com_contact', 'category', $this->item->catid); - $authorised = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by === $user->id); - } - - if ($authorised !== true) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return false; - } - - $this->item->tags = new TagsHelper; - - if (!empty($this->item->id)) - { - $this->item->tags->getItemTags('com_contact.contact', $this->item->id); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - $app->enqueueMessage(implode("\n", $errors), 'error'); - - return false; - } - - // Create a shortcut to the parameters. - $this->params = $this->state->params; - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); - - // Override global params with contact specific params - $this->params->merge($this->item->params); - - // Propose current language as default when creating new contact - if (empty($this->item->id) && Multilanguage::isEnabled()) - { - $lang = Factory::getLanguage()->getTag(); - $this->form->setFieldAttribute('language', 'default', $lang); - } - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @throws \Exception - * - * @since 4.0.0 - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_CONTACT_FORM_EDIT_CONTACT')); - } - - $title = $this->params->def('page_title', Text::_('COM_CONTACT_FORM_EDIT_CONTACT')); - - $this->setDocumentTitle($title); - - $pathway = $app->getPathWay(); - $pathway->addItem($title, ''); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('menu-meta_keywords')) - { - $this->document->setMetaData('keywords', $this->params->get('menu-meta_keywords')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * @var \Joomla\CMS\Form\Form + * @since 4.0.0 + */ + protected $form; + + /** + * @var object + * @since 4.0.0 + */ + protected $item; + + /** + * @var string + * @since 4.0.0 + */ + protected $return_page; + + /** + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx; + + /** + * @var \Joomla\Registry\Registry + * @since 4.0.0 + */ + protected $state; + + /** + * @var \Joomla\Registry\Registry + * @since 4.0.0 + */ + protected $params; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void|boolean + * + * @throws \Exception + * @since 4.0.0 + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + $app = Factory::getApplication(); + + // Get model data. + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + $this->return_page = $this->get('ReturnPage'); + + if (empty($this->item->id)) { + $authorised = $user->authorise('core.create', 'com_contact') || count($user->getAuthorisedCategories('com_contact', 'core.create')); + } else { + // Since we don't track these assets at the item level, use the category id. + $canDo = ContactHelper::getActions('com_contact', 'category', $this->item->catid); + $authorised = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by === $user->id); + } + + if ($authorised !== true) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return false; + } + + $this->item->tags = new TagsHelper(); + + if (!empty($this->item->id)) { + $this->item->tags->getItemTags('com_contact.contact', $this->item->id); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + $app->enqueueMessage(implode("\n", $errors), 'error'); + + return false; + } + + // Create a shortcut to the parameters. + $this->params = $this->state->params; + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); + + // Override global params with contact specific params + $this->params->merge($this->item->params); + + // Propose current language as default when creating new contact + if (empty($this->item->id) && Multilanguage::isEnabled()) { + $lang = Factory::getLanguage()->getTag(); + $this->form->setFieldAttribute('language', 'default', $lang); + } + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @throws \Exception + * + * @since 4.0.0 + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_CONTACT_FORM_EDIT_CONTACT')); + } + + $title = $this->params->def('page_title', Text::_('COM_CONTACT_FORM_EDIT_CONTACT')); + + $this->setDocumentTitle($title); + + $pathway = $app->getPathWay(); + $pathway->addItem($title, ''); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('menu-meta_keywords')) { + $this->document->setMetaData('keywords', $this->params->get('menu-meta_keywords')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_contact/tmpl/categories/default.php b/components/com_contact/tmpl/categories/default.php index a8c558dafc292..2e23579be8a5b 100644 --- a/components/com_contact/tmpl/categories/default.php +++ b/components/com_contact/tmpl/categories/default.php @@ -1,4 +1,5 @@
    - loadTemplate('items'); - ?> + loadTemplate('items'); + ?>
    diff --git a/components/com_contact/tmpl/categories/default_items.php b/components/com_contact/tmpl/categories/default_items.php index d18c34fc3e59a..5f8a812d86663 100644 --- a/components/com_contact/tmpl/categories/default_items.php +++ b/components/com_contact/tmpl/categories/default_items.php @@ -1,4 +1,5 @@ maxLevelcat != 0 && count($this->items[$this->parent->id]) > 0) : -?> - items[$this->parent->id] as $id => $item) : ?> - params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> -
    - - params->get('show_subcat_desc_cat') == 1) : ?> - description) : ?> -
    - description, '', 'com_contact.categories'); ?> -
    - - + ?> + items[$this->parent->id] as $id => $item) : ?> + params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> +
    + + params->get('show_subcat_desc_cat') == 1) : ?> + description) : ?> +
    + description, '', 'com_contact.categories'); ?> +
    + + - maxLevelcat > 1 && count($item->getChildren()) > 0) : ?> -
    - items[$item->id] = $item->getChildren(); - $this->parent = $item; - $this->maxLevelcat--; - echo $this->loadTemplate('items'); - $this->parent = $item->getParent(); - $this->maxLevelcat++; - ?> -
    - -
    - - + maxLevelcat > 1 && count($item->getChildren()) > 0) : ?> +
    + items[$item->id] = $item->getChildren(); + $this->parent = $item; + $this->maxLevelcat--; + echo $this->loadTemplate('items'); + $this->parent = $item->getParent(); + $this->maxLevelcat++; + ?> +
    + +
    + + diff --git a/components/com_contact/tmpl/category/default.php b/components/com_contact/tmpl/category/default.php index 84581b57c8ece..b25d7830b1637 100644 --- a/components/com_contact/tmpl/category/default.php +++ b/components/com_contact/tmpl/category/default.php @@ -1,4 +1,5 @@
    - subtemplatename = 'items'; - echo LayoutHelper::render('joomla.content.category_default', $this); - ?> + subtemplatename = 'items'; + echo LayoutHelper::render('joomla.content.category_default', $this); + ?>
    diff --git a/components/com_contact/tmpl/category/default_children.php b/components/com_contact/tmpl/category/default_children.php index ed2ed64012e80..0febb97df8dc8 100644 --- a/components/com_contact/tmpl/category/default_children.php +++ b/components/com_contact/tmpl/category/default_children.php @@ -1,4 +1,5 @@ maxLevel != 0 && count($this->children[$this->category->id]) > 0) : -?> + ?>
      -children[$this->category->id] as $id => $child) : ?> - params->get('show_empty_categories') || $child->numitems || count($child->getChildren())) : ?> -
    • -

      - - escape($child->title); ?> - + children[$this->category->id] as $id => $child) : ?> + params->get('show_empty_categories') || $child->numitems || count($child->getChildren())) : ?> +
    • +

      + + escape($child->title); ?> + - params->get('show_cat_items') == 1) : ?> - numitems; ?> - -

      + params->get('show_cat_items') == 1) : ?> + numitems; ?> + +
    • - params->get('show_subcat_desc') == 1) : ?> - description) : ?> -
      - description, '', 'com_contact.category'); ?> -
      - - + params->get('show_subcat_desc') == 1) : ?> + description) : ?> +
      + description, '', 'com_contact.category'); ?> +
      + + - getChildren()) > 0 ) : - $this->children[$child->id] = $child->getChildren(); - $this->category = $child; - $this->maxLevel--; - echo $this->loadTemplate('children'); - $this->category = $child->getParent(); - $this->maxLevel++; - endif; ?> -
    • - - + getChildren()) > 0) : + $this->children[$child->id] = $child->getChildren(); + $this->category = $child; + $this->maxLevel--; + echo $this->loadTemplate('children'); + $this->category = $child->getParent(); + $this->maxLevel++; + endif; ?> + + +
    diff --git a/components/com_contact/tmpl/category/default_items.php b/components/com_contact/tmpl/category/default_items.php index 05229d773dfd7..e073ca252e0dc 100644 --- a/components/com_contact/tmpl/category/default_items.php +++ b/components/com_contact/tmpl/category/default_items.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_contact.contacts-list') - ->useScript('core'); + ->useScript('core'); $canDo = ContactHelper::getActions('com_contact', 'category', $this->category->id); $canEdit = $canDo->get('core.edit'); @@ -31,185 +32,185 @@ $listDirn = $this->escape($this->state->get('list.direction')); ?>
    -
    - params->get('filter_field')) : ?> -
    - - - - -
    - - - params->get('show_pagination_limit')) : ?> -
    - - pagination->getLimitBox(); ?> -
    - - - items)) : ?> - params->get('show_no_contacts', 1)) : ?> -
    - - -
    - - - - - - params->get('show_headings')) : ?> - - - - - get('core.edit.own') && $item->created_by === $userId)) : ?> - - - - - - - items as $i => $item) : ?> - items[$i]->published == 0) : ?> - - - - - - - get('core.edit.own') && $item->created_by === $userId)) : ?> - - - - - - - - get('core.create')) : ?> - category, $this->category->params); ?> - - - params->get('show_pagination', 2)) : ?> -
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - - - pagination->getPagesLinks(); ?> -
    - -
    - - -
    -
    +
    + params->get('filter_field')) : ?> +
    + + + + +
    + + + params->get('show_pagination_limit')) : ?> +
    + + pagination->getLimitBox(); ?> +
    + + + items)) : ?> + params->get('show_no_contacts', 1)) : ?> +
    + + +
    + + + + + + params->get('show_headings')) : ?> + + + + + get('core.edit.own') && $item->created_by === $userId)) : ?> + + + + + + + items as $i => $item) : ?> + items[$i]->published == 0) : ?> + + + + + + + get('core.edit.own') && $item->created_by === $userId)) : ?> + + + + + + + + get('core.create')) : ?> + category, $this->category->params); ?> + + + params->get('show_pagination', 2)) : ?> +
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + + + pagination->getPagesLinks(); ?> +
    + +
    + + +
    +
    diff --git a/components/com_contact/tmpl/contact/default.php b/components/com_contact/tmpl/contact/default.php index 24d326583bdbe..2d216b1e48f8f 100644 --- a/components/com_contact/tmpl/contact/default.php +++ b/components/com_contact/tmpl/contact/default.php @@ -1,4 +1,5 @@
    - get('show_page_heading')) : ?> -

    - escape($tparams->get('page_heading')); ?> -

    - - - item->name && $tparams->get('show_name')) : ?> - - - - -
    -
    -
    - item, $tparams); ?> -
    -
    -
    - - - get('show_contact_category'); ?> - - -

    - item->category_title; ?> -

    - - item->catid, $this->item->language); ?> -

    - - escape($this->item->category_title); ?> - -

    - - - item->event->afterDisplayTitle; ?> - - get('show_contact_list') && count($this->contacts) > 1) : ?> -
    - - contacts, - 'select_contact', - 'class="form-select" onchange="document.location.href = this.value"', 'link', 'name', $this->item->link); - ?> -
    - - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> -
    - item->tagLayout = new FileLayout('joomla.content.tags'); ?> - item->tagLayout->render($this->item->tags->itemTags); ?> -
    - - - item->event->beforeDisplayContent; ?> - - params->get('show_info', 1)) : ?> - -
    - ' . Text::_('COM_CONTACT_DETAILS') . '

    '; ?> - - item->image && $tparams->get('show_image')) : ?> -
    - $this->item->image, - 'alt' => $this->item->name, - 'itemprop' => 'image', - ] - ); ?> -
    - - - item->con_position && $tparams->get('show_position')) : ?> -
    -
    :
    -
    - item->con_position; ?> -
    -
    - - -
    - loadTemplate('address'); ?> - - get('allow_vcard')) : ?> - - - - -
    - - - - - get('show_email_form') && ($this->item->email_to || $this->item->user_id)) : ?> - ' . Text::_('COM_CONTACT_EMAIL_FORM') . '

    '; ?> - - loadTemplate('form'); ?> - - - get('show_links')) : ?> - loadTemplate('links'); ?> - - - get('show_articles') && $this->item->user_id && $this->item->articles) : ?> - ' . Text::_('JGLOBAL_ARTICLES') . ''; ?> - - loadTemplate('articles'); ?> - - - get('show_profile') && $this->item->user_id && PluginHelper::isEnabled('user', 'profile')) : ?> - ' . Text::_('COM_CONTACT_PROFILE') . ''; ?> - - loadTemplate('profile'); ?> - - - get('show_user_custom_fields') && $this->contactUser) : ?> - loadTemplate('user_custom_fields'); ?> - - - item->misc && $tparams->get('show_misc')) : ?> - ' . Text::_('COM_CONTACT_OTHER_INFORMATION') . ''; ?> - -
    -
    -
    - params->get('marker_misc')) : ?> - - - - - params->get('marker_misc'); ?> - - -
    -
    - - item->misc; ?> - -
    -
    -
    - - item->event->afterDisplayContent; ?> + get('show_page_heading')) : ?> +

    + escape($tparams->get('page_heading')); ?> +

    + + + item->name && $tparams->get('show_name')) : ?> + + + + +
    +
    +
    + item, $tparams); ?> +
    +
    +
    + + + get('show_contact_category'); ?> + + +

    + item->category_title; ?> +

    + + item->catid, $this->item->language); ?> +

    + + escape($this->item->category_title); ?> + +

    + + + item->event->afterDisplayTitle; ?> + + get('show_contact_list') && count($this->contacts) > 1) : ?> +
    + + contacts, + 'select_contact', + 'class="form-select" onchange="document.location.href = this.value"', + 'link', + 'name', + $this->item->link + ); + ?> +
    + + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> +
    + item->tagLayout = new FileLayout('joomla.content.tags'); ?> + item->tagLayout->render($this->item->tags->itemTags); ?> +
    + + + item->event->beforeDisplayContent; ?> + + params->get('show_info', 1)) : ?> +
    + ' . Text::_('COM_CONTACT_DETAILS') . ''; ?> + + item->image && $tparams->get('show_image')) : ?> +
    + $this->item->image, + 'alt' => $this->item->name, + 'itemprop' => 'image', + ] + ); ?> +
    + + + item->con_position && $tparams->get('show_position')) : ?> +
    +
    :
    +
    + item->con_position; ?> +
    +
    + + +
    + loadTemplate('address'); ?> + + get('allow_vcard')) : ?> + + + + +
    +
    + + + + get('show_email_form') && ($this->item->email_to || $this->item->user_id)) : ?> + ' . Text::_('COM_CONTACT_EMAIL_FORM') . ''; ?> + + loadTemplate('form'); ?> + + + get('show_links')) : ?> + loadTemplate('links'); ?> + + + get('show_articles') && $this->item->user_id && $this->item->articles) : ?> + ' . Text::_('JGLOBAL_ARTICLES') . ''; ?> + + loadTemplate('articles'); ?> + + + get('show_profile') && $this->item->user_id && PluginHelper::isEnabled('user', 'profile')) : ?> + ' . Text::_('COM_CONTACT_PROFILE') . ''; ?> + + loadTemplate('profile'); ?> + + + get('show_user_custom_fields') && $this->contactUser) : ?> + loadTemplate('user_custom_fields'); ?> + + + item->misc && $tparams->get('show_misc')) : ?> + ' . Text::_('COM_CONTACT_OTHER_INFORMATION') . ''; ?> + +
    +
    +
    + params->get('marker_misc')) : ?> + + + + + params->get('marker_misc'); ?> + + +
    +
    + + item->misc; ?> + +
    +
    +
    + + item->event->afterDisplayContent; ?> diff --git a/components/com_contact/tmpl/contact/default_address.php b/components/com_contact/tmpl/contact/default_address.php index fbec2f6e2a880..c6dfdf110a9b9 100644 --- a/components/com_contact/tmpl/contact/default_address.php +++ b/components/com_contact/tmpl/contact/default_address.php @@ -1,4 +1,5 @@
    - params->get('address_check') > 0) && - ($this->item->address || $this->item->suburb || $this->item->state || $this->item->country || $this->item->postcode)) : ?> -
    - params->get('marker_address')) : ?> - - - - params->get('marker_address'); ?> - - -
    + params->get('address_check') > 0) && + ($this->item->address || $this->item->suburb || $this->item->state || $this->item->country || $this->item->postcode) +) : ?> +
    + params->get('marker_address')) : ?> + + + + params->get('marker_address'); ?> + + +
    - item->address && $this->params->get('show_street_address')) : ?> -
    - - item->address, false); ?> - -
    - + item->address && $this->params->get('show_street_address')) : ?> +
    + + item->address, false); ?> + +
    + - item->suburb && $this->params->get('show_suburb')) : ?> -
    - - item->suburb; ?> - -
    - - item->state && $this->params->get('show_state')) : ?> -
    - - item->state; ?> - -
    - - item->postcode && $this->params->get('show_postcode')) : ?> -
    - - item->postcode; ?> - -
    - - item->country && $this->params->get('show_country')) : ?> -
    - - item->country; ?> - -
    - - + item->suburb && $this->params->get('show_suburb')) : ?> +
    + + item->suburb; ?> + +
    + + item->state && $this->params->get('show_state')) : ?> +
    + + item->state; ?> + +
    + + item->postcode && $this->params->get('show_postcode')) : ?> +
    + + item->postcode; ?> + +
    + + item->country && $this->params->get('show_country')) : ?> +
    + + item->country; ?> + +
    + + item->email_to && $this->params->get('show_email')) : ?> -
    - params->get('marker_email')) : ?> - - - - params->get('marker_email'); ?> - - -
    -
    - - item->email_to; ?> - -
    +
    + params->get('marker_email')) : ?> + + + + params->get('marker_email'); ?> + + +
    +
    + + item->email_to; ?> + +
    item->telephone && $this->params->get('show_telephone')) : ?> -
    - params->get('marker_telephone')) : ?> - - - - params->get('marker_telephone'); ?> - - -
    -
    - - item->telephone; ?> - -
    +
    + params->get('marker_telephone')) : ?> + + + + params->get('marker_telephone'); ?> + + +
    +
    + + item->telephone; ?> + +
    item->fax && $this->params->get('show_fax')) : ?> -
    - params->get('marker_fax')) : ?> - - - - params->get('marker_fax'); ?> - - -
    -
    - - item->fax; ?> - -
    +
    + params->get('marker_fax')) : ?> + + + + params->get('marker_fax'); ?> + + +
    +
    + + item->fax; ?> + +
    item->mobile && $this->params->get('show_mobile')) : ?> -
    - params->get('marker_mobile')) : ?> - - - - params->get('marker_mobile'); ?> - - -
    -
    - - item->mobile; ?> - -
    +
    + params->get('marker_mobile')) : ?> + + + + params->get('marker_mobile'); ?> + + +
    +
    + + item->mobile; ?> + +
    item->webpage && $this->params->get('show_webpage')) : ?> -
    - params->get('marker_webpage')) : ?> - - - - params->get('marker_webpage'); ?> - - -
    -
    - - - -
    +
    + params->get('marker_webpage')) : ?> + + + + params->get('marker_webpage'); ?> + + +
    +
    + + + +
    diff --git a/components/com_contact/tmpl/contact/default_articles.php b/components/com_contact/tmpl/contact/default_articles.php index c16a95e5f8588..c17d00c54ddc0 100644 --- a/components/com_contact/tmpl/contact/default_articles.php +++ b/components/com_contact/tmpl/contact/default_articles.php @@ -1,4 +1,5 @@ params->get('show_articles')) : ?>
    -
      - item->articles as $article) : ?> -
    • - slug, $article->catid, $article->language)), htmlspecialchars($article->title, ENT_COMPAT, 'UTF-8')); ?> -
    • - -
    +
      + item->articles as $article) : ?> +
    • + slug, $article->catid, $article->language)), htmlspecialchars($article->title, ENT_COMPAT, 'UTF-8')); ?> +
    • + +
    diff --git a/components/com_contact/tmpl/contact/default_form.php b/components/com_contact/tmpl/contact/default_form.php index 8bfe273b904b2..303c13f609781 100644 --- a/components/com_contact/tmpl/contact/default_form.php +++ b/components/com_contact/tmpl/contact/default_form.php @@ -1,4 +1,5 @@
    -
    - form->getFieldsets() as $fieldset) : ?> - name === 'captcha' && !$this->captchaEnabled) : ?> - - - form->getFieldset($fieldset->name); ?> - -
    - label) && ($legend = trim(Text::_($fieldset->label))) !== '') : ?> - - - - renderField(); ?> - -
    - - -
    -
    - - - - - - -
    -
    -
    +
    + form->getFieldsets() as $fieldset) : ?> + name === 'captcha' && !$this->captchaEnabled) : ?> + + + form->getFieldset($fieldset->name); ?> + +
    + label) && ($legend = trim(Text::_($fieldset->label))) !== '') : ?> + + + + renderField(); ?> + +
    + + +
    +
    + + + + + + +
    +
    +
    diff --git a/components/com_contact/tmpl/contact/default_links.php b/components/com_contact/tmpl/contact/default_links.php index 64b95ad8d49cf..0b95aae7448cb 100644 --- a/components/com_contact/tmpl/contact/default_links.php +++ b/components/com_contact/tmpl/contact/default_links.php @@ -1,4 +1,5 @@ ' . Text::_('COM_CONTACT_LINKS') . ''; ?> diff --git a/components/com_contact/tmpl/contact/default_profile.php b/components/com_contact/tmpl/contact/default_profile.php index f64d411a3f4dd..b4e0134a04f38 100644 --- a/components/com_contact/tmpl/contact/default_profile.php +++ b/components/com_contact/tmpl/contact/default_profile.php @@ -1,4 +1,5 @@ item->profile->getFieldset('profile'); ?> -
    -
    - value) : - echo '
    ' . $profile->label . '
    '; - $profile->text = htmlspecialchars($profile->value, ENT_COMPAT, 'UTF-8'); - - switch ($profile->id) : - case 'profile_website': - $v_http = substr($profile->value, 0, 4); - - if ($v_http === 'http') : - echo '
    ' . PunycodeHelper::urlToUTF8($profile->text) . '
    '; - else : - echo '
    ' . PunycodeHelper::urlToUTF8($profile->text) . '
    '; - endif; - break; - - case 'profile_dob': - echo '
    ' . HTMLHelper::_('date', $profile->text, Text::_('DATE_FORMAT_LC4'), false) . '
    '; - break; - - default: - echo '
    ' . $profile->text . '
    '; - break; - endswitch; - endif; - endforeach; ?> -
    -
    + $fields = $this->item->profile->getFieldset('profile'); ?> +
    +
    + value) : + echo '
    ' . $profile->label . '
    '; + $profile->text = htmlspecialchars($profile->value, ENT_COMPAT, 'UTF-8'); + + switch ($profile->id) : + case 'profile_website': + $v_http = substr($profile->value, 0, 4); + + if ($v_http === 'http') : + echo '
    ' . PunycodeHelper::urlToUTF8($profile->text) . '
    '; + else : + echo '
    ' . PunycodeHelper::urlToUTF8($profile->text) . '
    '; + endif; + break; + + case 'profile_dob': + echo '
    ' . HTMLHelper::_('date', $profile->text, Text::_('DATE_FORMAT_LC4'), false) . '
    '; + break; + + default: + echo '
    ' . $profile->text . '
    '; + break; + endswitch; + endif; + endforeach; ?> +
    +
    diff --git a/components/com_contact/tmpl/contact/default_user_custom_fields.php b/components/com_contact/tmpl/contact/default_user_custom_fields.php index 839a70c757db7..a00b9412b1450 100644 --- a/components/com_contact/tmpl/contact/default_user_custom_fields.php +++ b/components/com_contact/tmpl/contact/default_user_custom_fields.php @@ -1,4 +1,5 @@ contactUser) : ?> - + contactUser->jcfields as $field) : ?> - value && (in_array('-1', $displayGroups) || in_array($field->group_id, $displayGroups))) : ?> - group_title][] = $field; ?> - + value && (in_array('-1', $displayGroups) || in_array($field->group_id, $displayGroups))) : ?> + group_title][] = $field; ?> + $fields) : ?> - - ' . ($groupTitle ?: Text::_('COM_CONTACT_USER_FIELDS')) . ''; ?> - -
    -
    - - value) : ?> - - - - params->get('showlabel')) : ?> - ' . Text::_($field->label) . ''; ?> - - - ' . $field->value . ''; ?> - -
    -
    + + ' . ($groupTitle ?: Text::_('COM_CONTACT_USER_FIELDS')) . ''; ?> + +
    +
    + + value) : ?> + + + + params->get('showlabel')) : ?> + ' . Text::_($field->label) . ''; ?> + + + ' . $field->value . ''; ?> + +
    +
    diff --git a/components/com_contact/tmpl/featured/default.php b/components/com_contact/tmpl/featured/default.php index f0093ac62c42a..d87e8d6716e95 100644 --- a/components/com_contact/tmpl/featured/default.php +++ b/components/com_contact/tmpl/featured/default.php @@ -1,4 +1,5 @@ diff --git a/components/com_contact/tmpl/featured/default_items.php b/components/com_contact/tmpl/featured/default_items.php index 75b31fe853750..1fd0130ad96ce 100644 --- a/components/com_contact/tmpl/featured/default_items.php +++ b/components/com_contact/tmpl/featured/default_items.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_contact.contacts-list') - ->useScript('core'); + ->useScript('core'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?> diff --git a/components/com_contact/tmpl/form/edit.php b/components/com_contact/tmpl/form/edit.php index cdb20b89ae85d..d5f33d6f52a56 100644 --- a/components/com_contact/tmpl/form/edit.php +++ b/components/com_contact/tmpl/form/edit.php @@ -1,4 +1,5 @@ useCoreUI = true; ?>
    - params->get('show_page_heading')) : ?> - - + params->get('show_page_heading')) : ?> + + -
    -
    - tab_name, ['active' => 'details', 'recall' => true, 'breakpoint' => 768]); ?> - tab_name, 'details', empty($this->item->id) ? Text::_('COM_CONTACT_NEW_CONTACT') : Text::_('COM_CONTACT_EDIT_CONTACT')); ?> - form->renderField('name'); ?> + +
    + tab_name, ['active' => 'details', 'recall' => true, 'breakpoint' => 768]); ?> + tab_name, 'details', empty($this->item->id) ? Text::_('COM_CONTACT_NEW_CONTACT') : Text::_('COM_CONTACT_EDIT_CONTACT')); ?> + form->renderField('name'); ?> - item->id)) : ?> - form->renderField('alias'); ?> - + item->id)) : ?> + form->renderField('alias'); ?> + - form->renderFieldset('details'); ?> - + form->renderFieldset('details'); ?> + - tab_name, 'misc', Text::_('COM_CONTACT_FIELDSET_MISCELLANEOUS')); ?> - form->getInput('misc'); ?> - + tab_name, 'misc', Text::_('COM_CONTACT_FIELDSET_MISCELLANEOUS')); ?> + form->getInput('misc'); ?> + - - tab_name, 'language', Text::_('JFIELD_LANGUAGE_LABEL')); ?> - form->renderField('language'); ?> - - - form->renderField('language'); ?> - + + tab_name, 'language', Text::_('JFIELD_LANGUAGE_LABEL')); ?> + form->renderField('language'); ?> + + + form->renderField('language'); ?> + - - + + - - - -
    -
    - - - params->get('save_history', 0) && $this->item->id) : ?> - form->getInput('contenthistory'); ?> - -
    - + + + +
    +
    + + + params->get('save_history', 0) && $this->item->id) : ?> + form->getInput('contenthistory'); ?> + +
    +
    diff --git a/components/com_content/helpers/icon.php b/components/com_content/helpers/icon.php index 3aa3126a48828..21b4074a9fb8f 100644 --- a/components/com_content/helpers/icon.php +++ b/components/com_content/helpers/icon.php @@ -1,4 +1,5 @@ create($category, $params, $attribs, $legacy); - } + /** + * Method to generate a link to the create item page for the given category + * + * @param object $category The category information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML markup for the create item link + * + * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead + */ + public static function create($category, $params, $attribs = array(), $legacy = false) + { + return self::getIcon()->create($category, $params, $attribs, $legacy); + } - /** - * Display an edit icon for the article. - * - * This icon will not display in a popup window, nor if the article is trashed. - * Edit access checks must be performed in the calling code. - * - * @param object $article The article information - * @param Registry $params The item parameters - * @param array $attribs Optional attributes for the link - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML for the article edit icon. - * - * @since 1.6 - * - * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead - */ - public static function edit($article, $params, $attribs = array(), $legacy = false) - { - return self::getIcon()->edit($article, $params, $attribs, $legacy); - } + /** + * Display an edit icon for the article. + * + * This icon will not display in a popup window, nor if the article is trashed. + * Edit access checks must be performed in the calling code. + * + * @param object $article The article information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML for the article edit icon. + * + * @since 1.6 + * + * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead + */ + public static function edit($article, $params, $attribs = array(), $legacy = false) + { + return self::getIcon()->edit($article, $params, $attribs, $legacy); + } - /** - * Method to generate a popup link to print an article - * - * @param object $article The article information - * @param Registry $params The item parameters - * @param array $attribs Optional attributes for the link - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML markup for the popup link - * - * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead - */ - public static function print_popup($article, $params, $attribs = array(), $legacy = false) - { - throw new \Exception(Text::_('COM_CONTENT_ERROR_PRINT_POPUP')); - } + /** + * Method to generate a popup link to print an article + * + * @param object $article The article information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML markup for the popup link + * + * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead + */ + public static function print_popup($article, $params, $attribs = array(), $legacy = false) + { + throw new \Exception(Text::_('COM_CONTENT_ERROR_PRINT_POPUP')); + } - /** - * Method to generate a link to print an article - * - * @param object $article Not used, @deprecated for 4.0 - * @param Registry $params The item parameters - * @param array $attribs Not used, @deprecated for 4.0 - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML markup for the popup link - * - * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead - */ - public static function print_screen($article, $params, $attribs = array(), $legacy = false) - { - return self::getIcon()->print_screen($params, $legacy); - } + /** + * Method to generate a link to print an article + * + * @param object $article Not used, @deprecated for 4.0 + * @param Registry $params The item parameters + * @param array $attribs Not used, @deprecated for 4.0 + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML markup for the popup link + * + * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead + */ + public static function print_screen($article, $params, $attribs = array(), $legacy = false) + { + return self::getIcon()->print_screen($params, $legacy); + } - /** - * Creates an icon instance. - * - * @return \Joomla\Component\Content\Administrator\Service\HTML\Icon - */ - private static function getIcon() - { - return (new \Joomla\Component\Content\Administrator\Service\HTML\Icon(Joomla\CMS\Factory::getApplication())); - } + /** + * Creates an icon instance. + * + * @return \Joomla\Component\Content\Administrator\Service\HTML\Icon + */ + private static function getIcon() + { + return (new \Joomla\Component\Content\Administrator\Service\HTML\Icon(Joomla\CMS\Factory::getApplication())); + } } diff --git a/components/com_content/src/Controller/ArticleController.php b/components/com_content/src/Controller/ArticleController.php index 0c8f6c5ed70cc..b07af00891c72 100644 --- a/components/com_content/src/Controller/ArticleController.php +++ b/components/com_content/src/Controller/ArticleController.php @@ -1,4 +1,5 @@ setRedirect($this->getReturnPage()); - - return; - } - - // Redirect to the edit screen. - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_item . '&a_id=0' - . $this->getRedirectToItemAppend(), false - ) - ); - - return true; - } - - /** - * Method override to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowAdd($data = array()) - { - $user = $this->app->getIdentity(); - $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('catid'), 'int'); - $allow = null; - - if ($categoryId) - { - // If the category has been passed in the data or URL check it. - $allow = $user->authorise('core.create', 'com_content.category.' . $categoryId); - } - - if ($allow === null) - { - // In the absence of better information, revert to the component permissions. - return parent::allowAdd(); - } - else - { - return $allow; - } - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key; default is id. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $user = $this->app->getIdentity(); - - // Zero record (id:0), return component edit permission by calling parent controller method - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Check edit on the record asset (explicit or inherited) - if ($user->authorise('core.edit', 'com_content.article.' . $recordId)) - { - return true; - } - - // Check edit own on the record asset (explicit or inherited) - if ($user->authorise('core.edit.own', 'com_content.article.' . $recordId)) - { - // Existing record already has an owner, get it - $record = $this->getModel()->getItem($recordId); - - if (empty($record)) - { - return false; - } - - // Grant if current user is owner of the record - return $user->get('id') == $record->created_by; - } - - return false; - } - - /** - * Method to cancel an edit. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 1.6 - */ - public function cancel($key = 'a_id') - { - $result = parent::cancel($key); - - /** @var SiteApplication $app */ - $app = $this->app; - - // Load the parameters. - $params = $app->getParams(); - - $customCancelRedir = (bool) $params->get('custom_cancel_redirect'); - - if ($customCancelRedir) - { - $cancelMenuitemId = (int) $params->get('cancel_redirect_menuitem'); - - if ($cancelMenuitemId > 0) - { - $item = $app->getMenu()->getItem($cancelMenuitemId); - $lang = ''; - - if (Multilanguage::isEnabled()) - { - $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; - } - - // Redirect to the user specified return page. - $redirlink = $item->link . $lang . '&Itemid=' . $cancelMenuitemId; - } - else - { - // Redirect to the same article submission form (clean form). - $redirlink = $app->getMenu()->getActive()->link . '&Itemid=' . $app->getMenu()->getActive()->id; - } - } - else - { - $menuitemId = (int) $params->get('redirect_menuitem'); - - if ($menuitemId > 0) - { - $lang = ''; - $item = $app->getMenu()->getItem($menuitemId); - - if (Multilanguage::isEnabled()) - { - $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; - } - - // Redirect to the general (redirect_menuitem) user specified return page. - $redirlink = $item->link . $lang . '&Itemid=' . $menuitemId; - } - else - { - // Redirect to the return page. - $redirlink = $this->getReturnPage(); - } - } - - $this->setRedirect(Route::_($redirlink, false)); - - return $result; - } - - /** - * Method to edit an existing record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key - * (sometimes required to avoid router collisions). - * - * @return boolean True if access level check and checkout passes, false otherwise. - * - * @since 1.6 - */ - public function edit($key = null, $urlVar = 'a_id') - { - $result = parent::edit($key, $urlVar); - - if (!$result) - { - $this->setRedirect(Route::_($this->getReturnPage(), false)); - } - - return $result; - } - - /** - * Method to get a model object, loading it if required. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return object The model. - * - * @since 1.5 - */ - public function getModel($name = 'Form', $prefix = 'Site', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 1.6 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'a_id') - { - // Need to override the parent method completely. - $tmpl = $this->input->get('tmpl'); - - $append = ''; - - // Setup redirect info. - if ($tmpl) - { - $append .= '&tmpl=' . $tmpl; - } - - // @todo This is a bandaid, not a long term solution. - /** - * if ($layout) - * { - * $append .= '&layout=' . $layout; - * } - */ - - $append .= '&layout=edit'; - - if ($recordId) - { - $append .= '&' . $urlVar . '=' . $recordId; - } - - $itemId = $this->input->getInt('Itemid'); - $return = $this->getReturnPage(); - $catId = $this->input->getInt('catid'); - - if ($itemId) - { - $append .= '&Itemid=' . $itemId; - } - - if ($catId) - { - $append .= '&catid=' . $catId; - } - - if ($return) - { - $append .= '&return=' . base64_encode($return); - } - - return $append; - } - - /** - * Get the return URL. - * - * If a "return" variable has been passed in the request - * - * @return string The return URL. - * - * @since 1.6 - */ - protected function getReturnPage() - { - $return = $this->input->get('return', null, 'base64'); - - if (empty($return) || !Uri::isInternal(base64_decode($return))) - { - return Uri::base(); - } - else - { - return base64_decode($return); - } - } - - /** - * Method to save a record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 1.6 - */ - public function save($key = null, $urlVar = 'a_id') - { - $result = parent::save($key, $urlVar); - - if (\in_array($this->getTask(), ['save2copy', 'apply'], true)) - { - return $result; - } - - $app = $this->app; - $articleId = $app->input->getInt('a_id'); - - // Load the parameters. - $params = $app->getParams(); - $menuitem = (int) $params->get('redirect_menuitem'); - - // Check for redirection after submission when creating a new article only - if ($menuitem > 0 && $articleId == 0) - { - $lang = ''; - - if (Multilanguage::isEnabled()) - { - $item = $app->getMenu()->getItem($menuitem); - $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; - } - - // If ok, redirect to the return page. - if ($result) - { - $this->setRedirect(Route::_('index.php?Itemid=' . $menuitem . $lang, false)); - } - } - elseif ($this->getTask() === 'save2copy') - { - // Redirect to the article page, use the redirect url set from parent controller - } - else - { - // If ok, redirect to the return page. - if ($result) - { - $this->setRedirect(Route::_($this->getReturnPage(), false)); - } - } - - return $result; - } - - /** - * Method to reload a record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return void - * - * @since 3.8.0 - */ - public function reload($key = null, $urlVar = 'a_id') - { - parent::reload($key, $urlVar); - } - - /** - * Method to save a vote. - * - * @return void - * - * @since 1.6 - */ - public function vote() - { - // Check for request forgeries. - $this->checkToken(); - - $user_rating = $this->input->getInt('user_rating', -1); - - if ($user_rating > -1) - { - $url = $this->input->getString('url', ''); - $id = $this->input->getInt('id', 0); - $viewName = $this->input->getString('view', $this->default_view); - $model = $this->getModel($viewName); - - // Don't redirect to an external URL. - if (!Uri::isInternal($url)) - { - $url = Route::_('index.php'); - } - - if ($model->storeVote($id, $user_rating)) - { - $this->setRedirect($url, Text::_('COM_CONTENT_ARTICLE_VOTE_SUCCESS')); - } - else - { - $this->setRedirect($url, Text::_('COM_CONTENT_ARTICLE_VOTE_FAILURE')); - } - } - } + use VersionableControllerTrait; + + /** + * The URL view item variable. + * + * @var string + * @since 1.6 + */ + protected $view_item = 'form'; + + /** + * The URL view list variable. + * + * @var string + * @since 1.6 + */ + protected $view_list = 'categories'; + + /** + * The URL edit variable. + * + * @var string + * @since 3.2 + */ + protected $urlVar = 'a.id'; + + /** + * Method to add a new record. + * + * @return mixed True if the record can be added, an error object if not. + * + * @since 1.6 + */ + public function add() + { + if (!parent::add()) { + // Redirect to the return page. + $this->setRedirect($this->getReturnPage()); + + return; + } + + // Redirect to the edit screen. + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_item . '&a_id=0' + . $this->getRedirectToItemAppend(), + false + ) + ); + + return true; + } + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $user = $this->app->getIdentity(); + $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('catid'), 'int'); + $allow = null; + + if ($categoryId) { + // If the category has been passed in the data or URL check it. + $allow = $user->authorise('core.create', 'com_content.category.' . $categoryId); + } + + if ($allow === null) { + // In the absence of better information, revert to the component permissions. + return parent::allowAdd(); + } else { + return $allow; + } + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key; default is id. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $user = $this->app->getIdentity(); + + // Zero record (id:0), return component edit permission by calling parent controller method + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Check edit on the record asset (explicit or inherited) + if ($user->authorise('core.edit', 'com_content.article.' . $recordId)) { + return true; + } + + // Check edit own on the record asset (explicit or inherited) + if ($user->authorise('core.edit.own', 'com_content.article.' . $recordId)) { + // Existing record already has an owner, get it + $record = $this->getModel()->getItem($recordId); + + if (empty($record)) { + return false; + } + + // Grant if current user is owner of the record + return $user->get('id') == $record->created_by; + } + + return false; + } + + /** + * Method to cancel an edit. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 1.6 + */ + public function cancel($key = 'a_id') + { + $result = parent::cancel($key); + + /** @var SiteApplication $app */ + $app = $this->app; + + // Load the parameters. + $params = $app->getParams(); + + $customCancelRedir = (bool) $params->get('custom_cancel_redirect'); + + if ($customCancelRedir) { + $cancelMenuitemId = (int) $params->get('cancel_redirect_menuitem'); + + if ($cancelMenuitemId > 0) { + $item = $app->getMenu()->getItem($cancelMenuitemId); + $lang = ''; + + if (Multilanguage::isEnabled()) { + $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; + } + + // Redirect to the user specified return page. + $redirlink = $item->link . $lang . '&Itemid=' . $cancelMenuitemId; + } else { + // Redirect to the same article submission form (clean form). + $redirlink = $app->getMenu()->getActive()->link . '&Itemid=' . $app->getMenu()->getActive()->id; + } + } else { + $menuitemId = (int) $params->get('redirect_menuitem'); + + if ($menuitemId > 0) { + $lang = ''; + $item = $app->getMenu()->getItem($menuitemId); + + if (Multilanguage::isEnabled()) { + $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; + } + + // Redirect to the general (redirect_menuitem) user specified return page. + $redirlink = $item->link . $lang . '&Itemid=' . $menuitemId; + } else { + // Redirect to the return page. + $redirlink = $this->getReturnPage(); + } + } + + $this->setRedirect(Route::_($redirlink, false)); + + return $result; + } + + /** + * Method to edit an existing record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key + * (sometimes required to avoid router collisions). + * + * @return boolean True if access level check and checkout passes, false otherwise. + * + * @since 1.6 + */ + public function edit($key = null, $urlVar = 'a_id') + { + $result = parent::edit($key, $urlVar); + + if (!$result) { + $this->setRedirect(Route::_($this->getReturnPage(), false)); + } + + return $result; + } + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.5 + */ + public function getModel($name = 'Form', $prefix = 'Site', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 1.6 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'a_id') + { + // Need to override the parent method completely. + $tmpl = $this->input->get('tmpl'); + + $append = ''; + + // Setup redirect info. + if ($tmpl) { + $append .= '&tmpl=' . $tmpl; + } + + // @todo This is a bandaid, not a long term solution. + /** + * if ($layout) + * { + * $append .= '&layout=' . $layout; + * } + */ + + $append .= '&layout=edit'; + + if ($recordId) { + $append .= '&' . $urlVar . '=' . $recordId; + } + + $itemId = $this->input->getInt('Itemid'); + $return = $this->getReturnPage(); + $catId = $this->input->getInt('catid'); + + if ($itemId) { + $append .= '&Itemid=' . $itemId; + } + + if ($catId) { + $append .= '&catid=' . $catId; + } + + if ($return) { + $append .= '&return=' . base64_encode($return); + } + + return $append; + } + + /** + * Get the return URL. + * + * If a "return" variable has been passed in the request + * + * @return string The return URL. + * + * @since 1.6 + */ + protected function getReturnPage() + { + $return = $this->input->get('return', null, 'base64'); + + if (empty($return) || !Uri::isInternal(base64_decode($return))) { + return Uri::base(); + } else { + return base64_decode($return); + } + } + + /** + * Method to save a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 1.6 + */ + public function save($key = null, $urlVar = 'a_id') + { + $result = parent::save($key, $urlVar); + + if (\in_array($this->getTask(), ['save2copy', 'apply'], true)) { + return $result; + } + + $app = $this->app; + $articleId = $app->input->getInt('a_id'); + + // Load the parameters. + $params = $app->getParams(); + $menuitem = (int) $params->get('redirect_menuitem'); + + // Check for redirection after submission when creating a new article only + if ($menuitem > 0 && $articleId == 0) { + $lang = ''; + + if (Multilanguage::isEnabled()) { + $item = $app->getMenu()->getItem($menuitem); + $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; + } + + // If ok, redirect to the return page. + if ($result) { + $this->setRedirect(Route::_('index.php?Itemid=' . $menuitem . $lang, false)); + } + } elseif ($this->getTask() === 'save2copy') { + // Redirect to the article page, use the redirect url set from parent controller + } else { + // If ok, redirect to the return page. + if ($result) { + $this->setRedirect(Route::_($this->getReturnPage(), false)); + } + } + + return $result; + } + + /** + * Method to reload a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return void + * + * @since 3.8.0 + */ + public function reload($key = null, $urlVar = 'a_id') + { + parent::reload($key, $urlVar); + } + + /** + * Method to save a vote. + * + * @return void + * + * @since 1.6 + */ + public function vote() + { + // Check for request forgeries. + $this->checkToken(); + + $user_rating = $this->input->getInt('user_rating', -1); + + if ($user_rating > -1) { + $url = $this->input->getString('url', ''); + $id = $this->input->getInt('id', 0); + $viewName = $this->input->getString('view', $this->default_view); + $model = $this->getModel($viewName); + + // Don't redirect to an external URL. + if (!Uri::isInternal($url)) { + $url = Route::_('index.php'); + } + + if ($model->storeVote($id, $user_rating)) { + $this->setRedirect($url, Text::_('COM_CONTENT_ARTICLE_VOTE_SUCCESS')); + } else { + $this->setRedirect($url, Text::_('COM_CONTENT_ARTICLE_VOTE_FAILURE')); + } + } + } } diff --git a/components/com_content/src/Controller/DisplayController.php b/components/com_content/src/Controller/DisplayController.php index 498e5760c652e..8237ede661d78 100644 --- a/components/com_content/src/Controller/DisplayController.php +++ b/components/com_content/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input = Factory::getApplication()->input; - - // Article frontpage Editor pagebreak proxying: - if ($this->input->get('view') === 'article' && $this->input->get('layout') === 'pagebreak') - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - } - // Article frontpage Editor article proxying: - elseif ($this->input->get('view') === 'articles' && $this->input->get('layout') === 'modal') - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - } - - parent::__construct($config, $factory, $app, $input); - } - - /** - * Method to display a view. - * - * @param boolean $cachable If true, the view output will be cached. - * @param boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. - * - * @return DisplayController This object to support chaining. - * - * @since 1.5 - */ - public function display($cachable = false, $urlparams = false) - { - $cachable = true; - - /** - * Set the default view name and format from the Request. - * Note we are using a_id to avoid collisions with the router and the return page. - * Frontend is a bit messier than the backend. - */ - $id = $this->input->getInt('a_id'); - $vName = $this->input->getCmd('view', 'categories'); - $this->input->set('view', $vName); - - $user = $this->app->getIdentity(); - - if ($user->get('id') - || ($this->input->getMethod() === 'POST' - && (($vName === 'category' && $this->input->get('layout') !== 'blog') || $vName === 'archive' ))) - { - $cachable = false; - } - - $safeurlparams = array( - 'catid' => 'INT', - 'id' => 'INT', - 'cid' => 'ARRAY', - 'year' => 'INT', - 'month' => 'INT', - 'limit' => 'UINT', - 'limitstart' => 'UINT', - 'showall' => 'INT', - 'return' => 'BASE64', - 'filter' => 'STRING', - 'filter_order' => 'CMD', - 'filter_order_Dir' => 'CMD', - 'filter-search' => 'STRING', - 'print' => 'BOOLEAN', - 'lang' => 'CMD', - 'Itemid' => 'INT'); - - // Check for edit form. - if ($vName === 'form' && !$this->checkEditId('com_content.edit.article', $id)) - { - // Somehow the person just went to the form - we don't allow that. - throw new \Exception(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 403); - } - - if ($vName === 'article') - { - // Get/Create the model - if ($model = $this->getModel($vName)) - { - if (ComponentHelper::getParams('com_content')->get('record_hits', 1) == 1) - { - $model->hit(); - } - } - } - - parent::display($cachable, $safeurlparams); - - return $this; - } + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object for the request + * + * @since 3.0.1 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + $this->input = Factory::getApplication()->input; + + // Article frontpage Editor pagebreak proxying: + if ($this->input->get('view') === 'article' && $this->input->get('layout') === 'pagebreak') { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + } + // Article frontpage Editor article proxying: + elseif ($this->input->get('view') === 'articles' && $this->input->get('layout') === 'modal') { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + } + + parent::__construct($config, $factory, $app, $input); + } + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached. + * @param boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return DisplayController This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $cachable = true; + + /** + * Set the default view name and format from the Request. + * Note we are using a_id to avoid collisions with the router and the return page. + * Frontend is a bit messier than the backend. + */ + $id = $this->input->getInt('a_id'); + $vName = $this->input->getCmd('view', 'categories'); + $this->input->set('view', $vName); + + $user = $this->app->getIdentity(); + + if ( + $user->get('id') + || ($this->input->getMethod() === 'POST' + && (($vName === 'category' && $this->input->get('layout') !== 'blog') || $vName === 'archive' )) + ) { + $cachable = false; + } + + $safeurlparams = array( + 'catid' => 'INT', + 'id' => 'INT', + 'cid' => 'ARRAY', + 'year' => 'INT', + 'month' => 'INT', + 'limit' => 'UINT', + 'limitstart' => 'UINT', + 'showall' => 'INT', + 'return' => 'BASE64', + 'filter' => 'STRING', + 'filter_order' => 'CMD', + 'filter_order_Dir' => 'CMD', + 'filter-search' => 'STRING', + 'print' => 'BOOLEAN', + 'lang' => 'CMD', + 'Itemid' => 'INT'); + + // Check for edit form. + if ($vName === 'form' && !$this->checkEditId('com_content.edit.article', $id)) { + // Somehow the person just went to the form - we don't allow that. + throw new \Exception(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 403); + } + + if ($vName === 'article') { + // Get/Create the model + if ($model = $this->getModel($vName)) { + if (ComponentHelper::getParams('com_content')->get('record_hits', 1) == 1) { + $model->hit(); + } + } + } + + parent::display($cachable, $safeurlparams); + + return $this; + } } diff --git a/components/com_content/src/Dispatcher/Dispatcher.php b/components/com_content/src/Dispatcher/Dispatcher.php index 7efdf0c367e6a..26209b4b20d89 100644 --- a/components/com_content/src/Dispatcher/Dispatcher.php +++ b/components/com_content/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->get('view') === 'articles' && $this->input->get('layout') === 'modal') - || ($this->input->get('view') === 'article' && $this->input->get('layout') === 'pagebreak'); - - if ($checkCreateEdit) - { - // Can create in any category (component permission) or at least in one category - $canCreateRecords = $this->app->getIdentity()->authorise('core.create', 'com_content') - || count($this->app->getIdentity()->getAuthorisedCategories('com_content', 'core.create')) > 0; - - // Instead of checking edit on all records, we can use **same** check as the form editing view - $values = (array) $this->app->getUserState('com_content.edit.article.id'); - $isEditingRecords = count($values); - $hasAccess = $canCreateRecords || $isEditingRecords; - - if (!$hasAccess) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'warning'); - - return; - } - } - - parent::dispatch(); - } + /** + * Dispatch a controller task. Redirecting the user if appropriate. + * + * @return void + * + * @since 4.0.0 + */ + public function dispatch() + { + $checkCreateEdit = ($this->input->get('view') === 'articles' && $this->input->get('layout') === 'modal') + || ($this->input->get('view') === 'article' && $this->input->get('layout') === 'pagebreak'); + + if ($checkCreateEdit) { + // Can create in any category (component permission) or at least in one category + $canCreateRecords = $this->app->getIdentity()->authorise('core.create', 'com_content') + || count($this->app->getIdentity()->getAuthorisedCategories('com_content', 'core.create')) > 0; + + // Instead of checking edit on all records, we can use **same** check as the form editing view + $values = (array) $this->app->getUserState('com_content.edit.article.id'); + $isEditingRecords = count($values); + $hasAccess = $canCreateRecords || $isEditingRecords; + + if (!$hasAccess) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'warning'); + + return; + } + } + + parent::dispatch(); + } } diff --git a/components/com_content/src/Helper/AssociationHelper.php b/components/com_content/src/Helper/AssociationHelper.php index 0699a3c1aeab4..1e4aa124b2fad 100644 --- a/components/com_content/src/Helper/AssociationHelper.php +++ b/components/com_content/src/Helper/AssociationHelper.php @@ -1,4 +1,5 @@ input; - $view = $view ?? $jinput->get('view'); - $component = $jinput->getCmd('option'); - $id = empty($id) ? $jinput->getInt('id') : $id; - - if ($layout === null && $jinput->get('view') == $view && $component == 'com_content') - { - $layout = $jinput->get('layout', '', 'string'); - } - - if ($view === 'article') - { - if ($id) - { - $user = Factory::getUser(); - $groups = implode(',', $user->getAuthorisedViewLevels()); - $db = Factory::getDbo(); - $advClause = array(); - - // Filter by user groups - $advClause[] = 'c2.access IN (' . $groups . ')'; - - // Filter by current language - $advClause[] = 'c2.language != ' . $db->quote(Factory::getLanguage()->getTag()); - - if (!$user->authorise('core.edit.state', 'com_content') && !$user->authorise('core.edit', 'com_content')) - { - // Filter by start and end dates. - $date = Factory::getDate(); - - $nowDate = $db->quote($date->toSql()); - - $advClause[] = '(c2.publish_up IS NULL OR c2.publish_up <= ' . $nowDate . ')'; - $advClause[] = '(c2.publish_down IS NULL OR c2.publish_down >= ' . $nowDate . ')'; - - // Filter by published - $advClause[] = 'c2.state = 1'; - } - - $associations = Associations::getAssociations( - 'com_content', - '#__content', - 'com_content.item', - $id, - 'id', - 'alias', - 'catid', - $advClause - ); - - $return = array(); - - foreach ($associations as $tag => $item) - { - $return[$tag] = RouteHelper::getArticleRoute($item->id, (int) $item->catid, $item->language, $layout); - } - - return $return; - } - } - - if ($view === 'category' || $view === 'categories') - { - return self::getCategoryAssociations($id, 'com_content', $layout); - } - - return array(); - } - - /** - * Method to display in frontend the associations for a given article - * - * @param integer $id Id of the article - * - * @return array An array containing the association URL and the related language object - * - * @since 3.7.0 - */ - public static function displayAssociations($id) - { - $return = array(); - - if ($associations = self::getAssociations($id, 'article')) - { - $levels = Factory::getUser()->getAuthorisedViewLevels(); - $languages = LanguageHelper::getLanguages(); - - foreach ($languages as $language) - { - // Do not display language when no association - if (empty($associations[$language->lang_code])) - { - continue; - } - - // Do not display language without frontend UI - if (!array_key_exists($language->lang_code, LanguageHelper::getInstalledLanguages(0))) - { - continue; - } - - // Do not display language without specific home menu - if (!array_key_exists($language->lang_code, Multilanguage::getSiteHomePages())) - { - continue; - } - - // Do not display language without authorized access level - if (isset($language->access) && $language->access && !in_array($language->access, $levels)) - { - continue; - } - - $return[$language->lang_code] = array('item' => $associations[$language->lang_code], 'language' => $language); - } - } - - return $return; - } + /** + * Method to get the associations for a given item + * + * @param integer $id Id of the item + * @param string $view Name of the view + * @param string $layout View layout + * + * @return array Array of associations for the item + * + * @since 3.0 + */ + public static function getAssociations($id = 0, $view = null, $layout = null) + { + $jinput = Factory::getApplication()->input; + $view = $view ?? $jinput->get('view'); + $component = $jinput->getCmd('option'); + $id = empty($id) ? $jinput->getInt('id') : $id; + + if ($layout === null && $jinput->get('view') == $view && $component == 'com_content') { + $layout = $jinput->get('layout', '', 'string'); + } + + if ($view === 'article') { + if ($id) { + $user = Factory::getUser(); + $groups = implode(',', $user->getAuthorisedViewLevels()); + $db = Factory::getDbo(); + $advClause = array(); + + // Filter by user groups + $advClause[] = 'c2.access IN (' . $groups . ')'; + + // Filter by current language + $advClause[] = 'c2.language != ' . $db->quote(Factory::getLanguage()->getTag()); + + if (!$user->authorise('core.edit.state', 'com_content') && !$user->authorise('core.edit', 'com_content')) { + // Filter by start and end dates. + $date = Factory::getDate(); + + $nowDate = $db->quote($date->toSql()); + + $advClause[] = '(c2.publish_up IS NULL OR c2.publish_up <= ' . $nowDate . ')'; + $advClause[] = '(c2.publish_down IS NULL OR c2.publish_down >= ' . $nowDate . ')'; + + // Filter by published + $advClause[] = 'c2.state = 1'; + } + + $associations = Associations::getAssociations( + 'com_content', + '#__content', + 'com_content.item', + $id, + 'id', + 'alias', + 'catid', + $advClause + ); + + $return = array(); + + foreach ($associations as $tag => $item) { + $return[$tag] = RouteHelper::getArticleRoute($item->id, (int) $item->catid, $item->language, $layout); + } + + return $return; + } + } + + if ($view === 'category' || $view === 'categories') { + return self::getCategoryAssociations($id, 'com_content', $layout); + } + + return array(); + } + + /** + * Method to display in frontend the associations for a given article + * + * @param integer $id Id of the article + * + * @return array An array containing the association URL and the related language object + * + * @since 3.7.0 + */ + public static function displayAssociations($id) + { + $return = array(); + + if ($associations = self::getAssociations($id, 'article')) { + $levels = Factory::getUser()->getAuthorisedViewLevels(); + $languages = LanguageHelper::getLanguages(); + + foreach ($languages as $language) { + // Do not display language when no association + if (empty($associations[$language->lang_code])) { + continue; + } + + // Do not display language without frontend UI + if (!array_key_exists($language->lang_code, LanguageHelper::getInstalledLanguages(0))) { + continue; + } + + // Do not display language without specific home menu + if (!array_key_exists($language->lang_code, Multilanguage::getSiteHomePages())) { + continue; + } + + // Do not display language without authorized access level + if (isset($language->access) && $language->access && !in_array($language->access, $levels)) { + continue; + } + + $return[$language->lang_code] = array('item' => $associations[$language->lang_code], 'language' => $language); + } + } + + return $return; + } } diff --git a/components/com_content/src/Helper/QueryHelper.php b/components/com_content/src/Helper/QueryHelper.php index 3de4456ba6ca4..188ceccccbbae 100644 --- a/components/com_content/src/Helper/QueryHelper.php +++ b/components/com_content/src/Helper/QueryHelper.php @@ -1,4 +1,5 @@ getQuery(true)->rand(); - break; - - case 'vote': - $orderby = 'a.id DESC '; - - if (PluginHelper::isEnabled('content', 'vote')) - { - $orderby = 'rating_count DESC '; - } - break; - - case 'rvote': - $orderby = 'a.id ASC '; - - if (PluginHelper::isEnabled('content', 'vote')) - { - $orderby = 'rating_count ASC '; - } - break; - - case 'rank': - $orderby = 'a.id DESC '; - - if (PluginHelper::isEnabled('content', 'vote')) - { - $orderby = 'rating DESC '; - } - break; - - case 'rrank': - $orderby = 'a.id ASC '; - - if (PluginHelper::isEnabled('content', 'vote')) - { - $orderby = 'rating ASC '; - } - break; - - default: - $orderby = 'a.ordering'; - break; - } - - return $orderby; - } - - /** - * Translate an order code to a field for primary category ordering. - * - * @param string $orderDate The ordering code. - * @param DatabaseInterface $db The database - * - * @return string The SQL field(s) to order by. - * - * @since 1.6 - */ - public static function getQueryDate($orderDate, DatabaseInterface $db = null) - { - $db = $db ?: Factory::getDbo(); - - switch ($orderDate) - { - case 'modified' : - $queryDate = ' CASE WHEN a.modified IS NULL THEN a.created ELSE a.modified END'; - break; - - // Use created if publish_up is not set - case 'published' : - $queryDate = ' CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END '; - break; - - case 'unpublished' : - $queryDate = ' CASE WHEN a.publish_down IS NULL THEN a.created ELSE a.publish_down END '; - break; - case 'created' : - default : - $queryDate = ' a.created '; - break; - } - - return $queryDate; - } - - /** - * Get join information for the voting query. - * - * @param \Joomla\Registry\Registry $params An options object for the article. - * - * @return array A named array with "select" and "join" keys. - * - * @since 1.5 - * - * @deprecated 5.0 Deprecated without replacement, not used in core - */ - public static function buildVotingQuery($params = null) - { - if (!$params) - { - $params = ComponentHelper::getParams('com_content'); - } - - $voting = $params->get('show_vote'); - - if ($voting) - { - // Calculate voting count - $select = ' , ROUND(v.rating_sum / v.rating_count) AS rating, v.rating_count'; - $join = ' LEFT JOIN #__content_rating AS v ON a.id = v.content_id'; - } - else - { - $select = ''; - $join = ''; - } - - return array('select' => $select, 'join' => $join); - } + /** + * Translate an order code to a field for primary category ordering. + * + * @param string $orderby The ordering code. + * + * @return string The SQL field(s) to order by. + * + * @since 1.5 + */ + public static function orderbyPrimary($orderby) + { + switch ($orderby) { + case 'alpha': + $orderby = 'c.path, '; + break; + + case 'ralpha': + $orderby = 'c.path DESC, '; + break; + + case 'order': + $orderby = 'c.lft, '; + break; + + default: + $orderby = ''; + break; + } + + return $orderby; + } + + /** + * Translate an order code to a field for secondary category ordering. + * + * @param string $orderby The ordering code. + * @param string $orderDate The ordering code for the date. + * @param DatabaseInterface $db The database + * + * @return string The SQL field(s) to order by. + * + * @since 1.5 + */ + public static function orderbySecondary($orderby, $orderDate = 'created', DatabaseInterface $db = null) + { + $db = $db ?: Factory::getDbo(); + + $queryDate = self::getQueryDate($orderDate, $db); + + switch ($orderby) { + case 'date': + $orderby = $queryDate; + break; + + case 'rdate': + $orderby = $queryDate . ' DESC '; + break; + + case 'alpha': + $orderby = 'a.title'; + break; + + case 'ralpha': + $orderby = 'a.title DESC'; + break; + + case 'hits': + $orderby = 'a.hits DESC'; + break; + + case 'rhits': + $orderby = 'a.hits'; + break; + + case 'rorder': + $orderby = 'a.ordering DESC'; + break; + + case 'author': + $orderby = 'author'; + break; + + case 'rauthor': + $orderby = 'author DESC'; + break; + + case 'front': + $orderby = 'a.featured DESC, fp.ordering, ' . $queryDate . ' DESC '; + break; + + case 'random': + $orderby = $db->getQuery(true)->rand(); + break; + + case 'vote': + $orderby = 'a.id DESC '; + + if (PluginHelper::isEnabled('content', 'vote')) { + $orderby = 'rating_count DESC '; + } + break; + + case 'rvote': + $orderby = 'a.id ASC '; + + if (PluginHelper::isEnabled('content', 'vote')) { + $orderby = 'rating_count ASC '; + } + break; + + case 'rank': + $orderby = 'a.id DESC '; + + if (PluginHelper::isEnabled('content', 'vote')) { + $orderby = 'rating DESC '; + } + break; + + case 'rrank': + $orderby = 'a.id ASC '; + + if (PluginHelper::isEnabled('content', 'vote')) { + $orderby = 'rating ASC '; + } + break; + + default: + $orderby = 'a.ordering'; + break; + } + + return $orderby; + } + + /** + * Translate an order code to a field for primary category ordering. + * + * @param string $orderDate The ordering code. + * @param DatabaseInterface $db The database + * + * @return string The SQL field(s) to order by. + * + * @since 1.6 + */ + public static function getQueryDate($orderDate, DatabaseInterface $db = null) + { + $db = $db ?: Factory::getDbo(); + + switch ($orderDate) { + case 'modified': + $queryDate = ' CASE WHEN a.modified IS NULL THEN a.created ELSE a.modified END'; + break; + + // Use created if publish_up is not set + case 'published': + $queryDate = ' CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END '; + break; + + case 'unpublished': + $queryDate = ' CASE WHEN a.publish_down IS NULL THEN a.created ELSE a.publish_down END '; + break; + case 'created': + default: + $queryDate = ' a.created '; + break; + } + + return $queryDate; + } + + /** + * Get join information for the voting query. + * + * @param \Joomla\Registry\Registry $params An options object for the article. + * + * @return array A named array with "select" and "join" keys. + * + * @since 1.5 + * + * @deprecated 5.0 Deprecated without replacement, not used in core + */ + public static function buildVotingQuery($params = null) + { + if (!$params) { + $params = ComponentHelper::getParams('com_content'); + } + + $voting = $params->get('show_vote'); + + if ($voting) { + // Calculate voting count + $select = ' , ROUND(v.rating_sum / v.rating_count) AS rating, v.rating_count'; + $join = ' LEFT JOIN #__content_rating AS v ON a.id = v.content_id'; + } else { + $select = ''; + $join = ''; + } + + return array('select' => $select, 'join' => $join); + } } diff --git a/components/com_content/src/Helper/RouteHelper.php b/components/com_content/src/Helper/RouteHelper.php index 91b3441f7533d..c9c807ea18fe2 100644 --- a/components/com_content/src/Helper/RouteHelper.php +++ b/components/com_content/src/Helper/RouteHelper.php @@ -1,4 +1,5 @@ 1) - { - $link .= '&catid=' . $catid; - } - - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } - - if ($layout) - { - $link .= '&layout=' . $layout; - } - - return $link; - } - - /** - * Get the category route. - * - * @param integer $catid The category ID. - * @param integer $language The language code. - * @param string $layout The layout value. - * - * @return string The article route. - * - * @since 1.5 - */ - public static function getCategoryRoute($catid, $language = 0, $layout = null) - { - if ($catid instanceof CategoryNode) - { - $id = $catid->id; - } - else - { - $id = (int) $catid; - } - - if ($id < 1) - { - return ''; - } - - $link = 'index.php?option=com_content&view=category&id=' . $id; - - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } - - if ($layout) - { - $link .= '&layout=' . $layout; - } - - return $link; - } - - /** - * Get the form route. - * - * @param integer $id The form ID. - * - * @return string The article route. - * - * @since 1.5 - */ - public static function getFormRoute($id) - { - return 'index.php?option=com_content&task=article.edit&a_id=' . (int) $id; - } + /** + * Get the article route. + * + * @param integer $id The route of the content item. + * @param integer $catid The category ID. + * @param integer $language The language code. + * @param string $layout The layout value. + * + * @return string The article route. + * + * @since 1.5 + */ + public static function getArticleRoute($id, $catid = 0, $language = 0, $layout = null) + { + // Create the link + $link = 'index.php?option=com_content&view=article&id=' . $id; + + if ((int) $catid > 1) { + $link .= '&catid=' . $catid; + } + + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } + + if ($layout) { + $link .= '&layout=' . $layout; + } + + return $link; + } + + /** + * Get the category route. + * + * @param integer $catid The category ID. + * @param integer $language The language code. + * @param string $layout The layout value. + * + * @return string The article route. + * + * @since 1.5 + */ + public static function getCategoryRoute($catid, $language = 0, $layout = null) + { + if ($catid instanceof CategoryNode) { + $id = $catid->id; + } else { + $id = (int) $catid; + } + + if ($id < 1) { + return ''; + } + + $link = 'index.php?option=com_content&view=category&id=' . $id; + + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } + + if ($layout) { + $link .= '&layout=' . $layout; + } + + return $link; + } + + /** + * Get the form route. + * + * @param integer $id The form ID. + * + * @return string The article route. + * + * @since 1.5 + */ + public static function getFormRoute($id) + { + return 'index.php?option=com_content&task=article.edit&a_id=' . (int) $id; + } } diff --git a/components/com_content/src/Model/ArchiveModel.php b/components/com_content/src/Model/ArchiveModel.php index af8ce8ce263be..e89b2b473afe1 100644 --- a/components/com_content/src/Model/ArchiveModel.php +++ b/components/com_content/src/Model/ArchiveModel.php @@ -1,4 +1,5 @@ state->get('params'); - - // Filter on archived articles - $this->setState('filter.published', ContentComponent::CONDITION_ARCHIVED); - - // Filter on month, year - $this->setState('filter.month', $app->input->getInt('month')); - $this->setState('filter.year', $app->input->getInt('year')); - - // Optional filter text - $this->setState('list.filter', $app->input->getString('filter-search')); - - // Get list limit - $itemid = $app->input->get('Itemid', 0, 'int'); - $limit = $app->getUserStateFromRequest('com_content.archive.list' . $itemid . '.limit', 'limit', $params->get('display_num', 20), 'uint'); - $this->setState('list.limit', $limit); - - // Set the archive ordering - $articleOrderby = $params->get('orderby_sec', 'rdate'); - $articleOrderDate = $params->get('order_date'); - - // No category ordering - $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDatabase()); - - $this->setState('list.ordering', $secondary . ', a.created DESC'); - $this->setState('list.direction', ''); - } - - /** - * Get the master query for retrieving a list of articles subject to the model state. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - $params = $this->state->params; - $app = Factory::getApplication(); - $catids = $app->input->get('catid', array(), 'array'); - $catids = array_values(array_diff($catids, array(''))); - - $articleOrderDate = $params->get('order_date'); - - // Create a new query object. - $db = $this->getDatabase(); - $query = parent::getListQuery(); - - // Add routing for archive - $query->select( - [ - $this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS ' . $db->quoteName('slug'), - $this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS ' . $db->quoteName('catslug'), - ] - ); - - // Filter on month, year - // First, get the date field - $queryDate = QueryHelper::getQueryDate($articleOrderDate, $this->getDatabase()); - - if ($month = (int) $this->getState('filter.month')) - { - $query->where($query->month($queryDate) . ' = :month') - ->bind(':month', $month, ParameterType::INTEGER); - } - - if ($year = (int) $this->getState('filter.year')) - { - $query->where($query->year($queryDate) . ' = :year') - ->bind(':year', $year, ParameterType::INTEGER); - } - - if (count($catids) > 0) - { - $query->whereIn($db->quoteName('c.id'), $catids); - } - - return $query; - } - - /** - * Method to get the archived article list - * - * @access public - * @return array - */ - public function getData() - { - $app = Factory::getApplication(); - - // Lets load the content if it doesn't already exist - if (empty($this->_data)) - { - // Get the page/component configuration - $params = $app->getParams(); - - // Get the pagination request variables - $limit = $app->input->get('limit', $params->get('display_num', 20), 'uint'); - $limitstart = $app->input->get('limitstart', 0, 'uint'); - - $query = $this->_buildQuery(); - - $this->_data = $this->_getList($query, $limitstart, $limit); - } - - return $this->_data; - } - - /** - * Gets the archived articles years - * - * @return array - * - * @since 3.6.0 - */ - public function getYears() - { - $db = $this->getDatabase(); - $nowDate = Factory::getDate()->toSql(); - $query = $db->getQuery(true); - $years = $query->year($db->quoteName('c.created')); - - $query->select('DISTINCT ' . $years) - ->from($db->quoteName('#__content', 'c')) - ->where($db->quoteName('c.state') . ' = ' . ContentComponent::CONDITION_ARCHIVED) - ->extendWhere( - 'AND', - [ - $db->quoteName('c.publish_up') . ' IS NULL', - $db->quoteName('c.publish_up') . ' <= :publishUp', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('c.publish_down') . ' IS NULL', - $db->quoteName('c.publish_down') . ' >= :publishDown', - ], - 'OR' - ) - ->bind(':publishUp', $nowDate) - ->bind(':publishDown', $nowDate) - ->order('1 ASC'); - - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Generate column expression for slug or catslug. - * - * @param \Joomla\Database\DatabaseQuery $query Current query instance. - * @param string $id Column id name. - * @param string $alias Column alias name. - * - * @return string - * - * @since 4.0.0 - */ - private function getSlugColumn($query, $id, $alias) - { - $db = $this->getDatabase(); - - return 'CASE WHEN ' - . $query->charLength($db->quoteName($alias), '!=', '0') - . ' THEN ' - . $query->concatenate([$query->castAsChar($db->quoteName($id)), $db->quoteName($alias)], ':') - . ' ELSE ' - . $query->castAsChar($id) . ' END'; - } + /** + * Model context string. + * + * @var string + */ + public $_context = 'com_content.archive'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering The field to order on. + * @param string $direction The direction to order on. + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + parent::populateState(); + + $app = Factory::getApplication(); + + // Add archive properties + $params = $this->state->get('params'); + + // Filter on archived articles + $this->setState('filter.published', ContentComponent::CONDITION_ARCHIVED); + + // Filter on month, year + $this->setState('filter.month', $app->input->getInt('month')); + $this->setState('filter.year', $app->input->getInt('year')); + + // Optional filter text + $this->setState('list.filter', $app->input->getString('filter-search')); + + // Get list limit + $itemid = $app->input->get('Itemid', 0, 'int'); + $limit = $app->getUserStateFromRequest('com_content.archive.list' . $itemid . '.limit', 'limit', $params->get('display_num', 20), 'uint'); + $this->setState('list.limit', $limit); + + // Set the archive ordering + $articleOrderby = $params->get('orderby_sec', 'rdate'); + $articleOrderDate = $params->get('order_date'); + + // No category ordering + $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDatabase()); + + $this->setState('list.ordering', $secondary . ', a.created DESC'); + $this->setState('list.direction', ''); + } + + /** + * Get the master query for retrieving a list of articles subject to the model state. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + $params = $this->state->params; + $app = Factory::getApplication(); + $catids = $app->input->get('catid', array(), 'array'); + $catids = array_values(array_diff($catids, array(''))); + + $articleOrderDate = $params->get('order_date'); + + // Create a new query object. + $db = $this->getDatabase(); + $query = parent::getListQuery(); + + // Add routing for archive + $query->select( + [ + $this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS ' . $db->quoteName('slug'), + $this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS ' . $db->quoteName('catslug'), + ] + ); + + // Filter on month, year + // First, get the date field + $queryDate = QueryHelper::getQueryDate($articleOrderDate, $this->getDatabase()); + + if ($month = (int) $this->getState('filter.month')) { + $query->where($query->month($queryDate) . ' = :month') + ->bind(':month', $month, ParameterType::INTEGER); + } + + if ($year = (int) $this->getState('filter.year')) { + $query->where($query->year($queryDate) . ' = :year') + ->bind(':year', $year, ParameterType::INTEGER); + } + + if (count($catids) > 0) { + $query->whereIn($db->quoteName('c.id'), $catids); + } + + return $query; + } + + /** + * Method to get the archived article list + * + * @access public + * @return array + */ + public function getData() + { + $app = Factory::getApplication(); + + // Lets load the content if it doesn't already exist + if (empty($this->_data)) { + // Get the page/component configuration + $params = $app->getParams(); + + // Get the pagination request variables + $limit = $app->input->get('limit', $params->get('display_num', 20), 'uint'); + $limitstart = $app->input->get('limitstart', 0, 'uint'); + + $query = $this->_buildQuery(); + + $this->_data = $this->_getList($query, $limitstart, $limit); + } + + return $this->_data; + } + + /** + * Gets the archived articles years + * + * @return array + * + * @since 3.6.0 + */ + public function getYears() + { + $db = $this->getDatabase(); + $nowDate = Factory::getDate()->toSql(); + $query = $db->getQuery(true); + $years = $query->year($db->quoteName('c.created')); + + $query->select('DISTINCT ' . $years) + ->from($db->quoteName('#__content', 'c')) + ->where($db->quoteName('c.state') . ' = ' . ContentComponent::CONDITION_ARCHIVED) + ->extendWhere( + 'AND', + [ + $db->quoteName('c.publish_up') . ' IS NULL', + $db->quoteName('c.publish_up') . ' <= :publishUp', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('c.publish_down') . ' IS NULL', + $db->quoteName('c.publish_down') . ' >= :publishDown', + ], + 'OR' + ) + ->bind(':publishUp', $nowDate) + ->bind(':publishDown', $nowDate) + ->order('1 ASC'); + + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Generate column expression for slug or catslug. + * + * @param \Joomla\Database\DatabaseQuery $query Current query instance. + * @param string $id Column id name. + * @param string $alias Column alias name. + * + * @return string + * + * @since 4.0.0 + */ + private function getSlugColumn($query, $id, $alias) + { + $db = $this->getDatabase(); + + return 'CASE WHEN ' + . $query->charLength($db->quoteName($alias), '!=', '0') + . ' THEN ' + . $query->concatenate([$query->castAsChar($db->quoteName($id)), $db->quoteName($alias)], ':') + . ' ELSE ' + . $query->castAsChar($id) . ' END'; + } } diff --git a/components/com_content/src/Model/ArticleModel.php b/components/com_content/src/Model/ArticleModel.php index b3daabdca6fca..b47a077c3c300 100644 --- a/components/com_content/src/Model/ArticleModel.php +++ b/components/com_content/src/Model/ArticleModel.php @@ -1,4 +1,5 @@ input->getInt('id'); - $this->setState('article.id', $pk); - - $offset = $app->input->getUint('limitstart'); - $this->setState('list.offset', $offset); - - // Load the parameters. - $params = $app->getParams(); - $this->setState('params', $params); - - $user = Factory::getUser(); - - // If $pk is set then authorise on complete asset, else on component only - $asset = empty($pk) ? 'com_content' : 'com_content.article.' . $pk; - - if ((!$user->authorise('core.edit.state', $asset)) && (!$user->authorise('core.edit', $asset))) - { - $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); - $this->setState('filter.archived', ContentComponent::CONDITION_ARCHIVED); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - } - - /** - * Method to get article data. - * - * @param integer $pk The id of the article. - * - * @return object|boolean Menu item data object on success, boolean false - */ - public function getItem($pk = null) - { - $user = Factory::getUser(); - - $pk = (int) ($pk ?: $this->getState('article.id')); - - if ($this->_item === null) - { - $this->_item = array(); - } - - if (!isset($this->_item[$pk])) - { - try - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select( - $this->getState( - 'item.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.asset_id'), - $db->quoteName('a.title'), - $db->quoteName('a.alias'), - $db->quoteName('a.introtext'), - $db->quoteName('a.fulltext'), - $db->quoteName('a.state'), - $db->quoteName('a.catid'), - $db->quoteName('a.created'), - $db->quoteName('a.created_by'), - $db->quoteName('a.created_by_alias'), - $db->quoteName('a.modified'), - $db->quoteName('a.modified_by'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.publish_up'), - $db->quoteName('a.publish_down'), - $db->quoteName('a.images'), - $db->quoteName('a.urls'), - $db->quoteName('a.attribs'), - $db->quoteName('a.version'), - $db->quoteName('a.ordering'), - $db->quoteName('a.metakey'), - $db->quoteName('a.metadesc'), - $db->quoteName('a.access'), - $db->quoteName('a.hits'), - $db->quoteName('a.metadata'), - $db->quoteName('a.featured'), - $db->quoteName('a.language'), - ] - ) - ) - ->select( - [ - $db->quoteName('fp.featured_up'), - $db->quoteName('fp.featured_down'), - $db->quoteName('c.title', 'category_title'), - $db->quoteName('c.alias', 'category_alias'), - $db->quoteName('c.access', 'category_access'), - $db->quoteName('c.language', 'category_language'), - $db->quoteName('fp.ordering'), - $db->quoteName('u.name', 'author'), - $db->quoteName('parent.title', 'parent_title'), - $db->quoteName('parent.id', 'parent_id'), - $db->quoteName('parent.path', 'parent_route'), - $db->quoteName('parent.alias', 'parent_alias'), - $db->quoteName('parent.language', 'parent_language'), - 'ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 1) AS ' - . $db->quoteName('rating'), - $db->quoteName('v.rating_count', 'rating_count'), - ] - ) - ->from($db->quoteName('#__content', 'a')) - ->join( - 'INNER', - $db->quoteName('#__categories', 'c'), - $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid') - ) - ->join('LEFT', $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by')) - ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) - ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')) - ->where( - [ - $db->quoteName('a.id') . ' = :pk', - $db->quoteName('c.published') . ' > 0', - ] - ) - ->bind(':pk', $pk, ParameterType::INTEGER); - - // Filter by language - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - if (!$user->authorise('core.edit.state', 'com_content.article.' . $pk) - && !$user->authorise('core.edit', 'com_content.article.' . $pk) - ) - { - // Filter by start and end dates. - $nowDate = Factory::getDate()->toSql(); - - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_up') . ' IS NULL', - $db->quoteName('a.publish_up') . ' <= :publishUp', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_down') . ' IS NULL', - $db->quoteName('a.publish_down') . ' >= :publishDown', - ], - 'OR' - ) - ->bind([':publishUp', ':publishDown'], $nowDate); - } - - // Filter by published state. - $published = $this->getState('filter.published'); - $archived = $this->getState('filter.archived'); - - if (is_numeric($published)) - { - $query->whereIn($db->quoteName('a.state'), [(int) $published, (int) $archived]); - } - - $db->setQuery($query); - - $data = $db->loadObject(); - - if (empty($data)) - { - throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); - } - - // Check for published state if filter set. - if ((is_numeric($published) || is_numeric($archived)) && ($data->state != $published && $data->state != $archived)) - { - throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); - } - - // Convert parameter fields to objects. - $registry = new Registry($data->attribs); - - $data->params = clone $this->getState('params'); - $data->params->merge($registry); - - $data->metadata = new Registry($data->metadata); - - // Technically guest could edit an article, but lets not check that to improve performance a little. - if (!$user->get('guest')) - { - $userId = $user->get('id'); - $asset = 'com_content.article.' . $data->id; - - // Check general edit permission first. - if ($user->authorise('core.edit', $asset)) - { - $data->params->set('access-edit', true); - } - - // Now check if edit.own is available. - elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) - { - // Check for a valid user and that they are the owner. - if ($userId == $data->created_by) - { - $data->params->set('access-edit', true); - } - } - } - - // Compute view access permissions. - if ($access = $this->getState('filter.access')) - { - // If the access filter has been set, we already know this user can view. - $data->params->set('access-view', true); - } - else - { - // If no access filter is set, the layout takes some responsibility for display of limited information. - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - if ($data->catid == 0 || $data->category_access === null) - { - $data->params->set('access-view', in_array($data->access, $groups)); - } - else - { - $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); - } - } - - $this->_item[$pk] = $data; - } - catch (\Exception $e) - { - if ($e->getCode() == 404) - { - // Need to go through the error handler to allow Redirect to work. - throw $e; - } - else - { - $this->setError($e); - $this->_item[$pk] = false; - } - } - } - - return $this->_item[$pk]; - } - - /** - * Increment the hit counter for the article. - * - * @param integer $pk Optional primary key of the article to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('article.id'); - - $table = Table::getInstance('Content', 'JTable'); - $table->hit($pk); - } - - return true; - } - - /** - * Save user vote on article - * - * @param integer $pk Joomla Article Id - * @param integer $rate Voting rate - * - * @return boolean Return true on success - */ - public function storeVote($pk = 0, $rate = 0) - { - $pk = (int) $pk; - $rate = (int) $rate; - - if ($rate >= 1 && $rate <= 5 && $pk > 0) - { - $userIP = IpHelper::getIp(); - - // Initialize variables. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Create the base select statement. - $query->select('*') - ->from($db->quoteName('#__content_rating')) - ->where($db->quoteName('content_id') . ' = :pk') - ->bind(':pk', $pk, ParameterType::INTEGER); - - // Set the query and load the result. - $db->setQuery($query); - - // Check for a database error. - try - { - $rating = $db->loadObject(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // There are no ratings yet, so lets insert our rating - if (!$rating) - { - $query = $db->getQuery(true); - - // Create the base insert statement. - $query->insert($db->quoteName('#__content_rating')) - ->columns( - [ - $db->quoteName('content_id'), - $db->quoteName('lastip'), - $db->quoteName('rating_sum'), - $db->quoteName('rating_count'), - ] - ) - ->values(':pk, :ip, :rate, 1') - ->bind(':pk', $pk, ParameterType::INTEGER) - ->bind(':ip', $userIP) - ->bind(':rate', $rate, ParameterType::INTEGER); - - // Set the query and execute the insert. - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - } - else - { - if ($userIP != $rating->lastip) - { - $query = $db->getQuery(true); - - // Create the base update statement. - $query->update($db->quoteName('#__content_rating')) - ->set( - [ - $db->quoteName('rating_count') . ' = ' . $db->quoteName('rating_count') . ' + 1', - $db->quoteName('rating_sum') . ' = ' . $db->quoteName('rating_sum') . ' + :rate', - $db->quoteName('lastip') . ' = :ip', - ] - ) - ->where($db->quoteName('content_id') . ' = :pk') - ->bind(':rate', $rate, ParameterType::INTEGER) - ->bind(':ip', $userIP) - ->bind(':pk', $pk, ParameterType::INTEGER); - - // Set the query and execute the update. - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - } - else - { - return false; - } - } - - $this->cleanCache(); - - return true; - } - - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_CONTENT_INVALID_RATING', $rate), 'error'); - - return false; - } - - /** - * Cleans the cache of com_content and content modules - * - * @param string $group The cache group - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 3.9.9 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_content'); - parent::cleanCache('mod_articles_archive'); - parent::cleanCache('mod_articles_categories'); - parent::cleanCache('mod_articles_category'); - parent::cleanCache('mod_articles_latest'); - parent::cleanCache('mod_articles_news'); - parent::cleanCache('mod_articles_popular'); - } + /** + * Model context string. + * + * @var string + */ + protected $_context = 'com_content.article'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @since 1.6 + * + * @return void + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load state from the request. + $pk = $app->input->getInt('id'); + $this->setState('article.id', $pk); + + $offset = $app->input->getUint('limitstart'); + $this->setState('list.offset', $offset); + + // Load the parameters. + $params = $app->getParams(); + $this->setState('params', $params); + + $user = Factory::getUser(); + + // If $pk is set then authorise on complete asset, else on component only + $asset = empty($pk) ? 'com_content' : 'com_content.article.' . $pk; + + if ((!$user->authorise('core.edit.state', $asset)) && (!$user->authorise('core.edit', $asset))) { + $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); + $this->setState('filter.archived', ContentComponent::CONDITION_ARCHIVED); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + } + + /** + * Method to get article data. + * + * @param integer $pk The id of the article. + * + * @return object|boolean Menu item data object on success, boolean false + */ + public function getItem($pk = null) + { + $user = Factory::getUser(); + + $pk = (int) ($pk ?: $this->getState('article.id')); + + if ($this->_item === null) { + $this->_item = array(); + } + + if (!isset($this->_item[$pk])) { + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + $this->getState( + 'item.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.asset_id'), + $db->quoteName('a.title'), + $db->quoteName('a.alias'), + $db->quoteName('a.introtext'), + $db->quoteName('a.fulltext'), + $db->quoteName('a.state'), + $db->quoteName('a.catid'), + $db->quoteName('a.created'), + $db->quoteName('a.created_by'), + $db->quoteName('a.created_by_alias'), + $db->quoteName('a.modified'), + $db->quoteName('a.modified_by'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.publish_up'), + $db->quoteName('a.publish_down'), + $db->quoteName('a.images'), + $db->quoteName('a.urls'), + $db->quoteName('a.attribs'), + $db->quoteName('a.version'), + $db->quoteName('a.ordering'), + $db->quoteName('a.metakey'), + $db->quoteName('a.metadesc'), + $db->quoteName('a.access'), + $db->quoteName('a.hits'), + $db->quoteName('a.metadata'), + $db->quoteName('a.featured'), + $db->quoteName('a.language'), + ] + ) + ) + ->select( + [ + $db->quoteName('fp.featured_up'), + $db->quoteName('fp.featured_down'), + $db->quoteName('c.title', 'category_title'), + $db->quoteName('c.alias', 'category_alias'), + $db->quoteName('c.access', 'category_access'), + $db->quoteName('c.language', 'category_language'), + $db->quoteName('fp.ordering'), + $db->quoteName('u.name', 'author'), + $db->quoteName('parent.title', 'parent_title'), + $db->quoteName('parent.id', 'parent_id'), + $db->quoteName('parent.path', 'parent_route'), + $db->quoteName('parent.alias', 'parent_alias'), + $db->quoteName('parent.language', 'parent_language'), + 'ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 1) AS ' + . $db->quoteName('rating'), + $db->quoteName('v.rating_count', 'rating_count'), + ] + ) + ->from($db->quoteName('#__content', 'a')) + ->join( + 'INNER', + $db->quoteName('#__categories', 'c'), + $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid') + ) + ->join('LEFT', $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by')) + ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) + ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')) + ->where( + [ + $db->quoteName('a.id') . ' = :pk', + $db->quoteName('c.published') . ' > 0', + ] + ) + ->bind(':pk', $pk, ParameterType::INTEGER); + + // Filter by language + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + if ( + !$user->authorise('core.edit.state', 'com_content.article.' . $pk) + && !$user->authorise('core.edit', 'com_content.article.' . $pk) + ) { + // Filter by start and end dates. + $nowDate = Factory::getDate()->toSql(); + + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_up') . ' IS NULL', + $db->quoteName('a.publish_up') . ' <= :publishUp', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_down') . ' IS NULL', + $db->quoteName('a.publish_down') . ' >= :publishDown', + ], + 'OR' + ) + ->bind([':publishUp', ':publishDown'], $nowDate); + } + + // Filter by published state. + $published = $this->getState('filter.published'); + $archived = $this->getState('filter.archived'); + + if (is_numeric($published)) { + $query->whereIn($db->quoteName('a.state'), [(int) $published, (int) $archived]); + } + + $db->setQuery($query); + + $data = $db->loadObject(); + + if (empty($data)) { + throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); + } + + // Check for published state if filter set. + if ((is_numeric($published) || is_numeric($archived)) && ($data->state != $published && $data->state != $archived)) { + throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); + } + + // Convert parameter fields to objects. + $registry = new Registry($data->attribs); + + $data->params = clone $this->getState('params'); + $data->params->merge($registry); + + $data->metadata = new Registry($data->metadata); + + // Technically guest could edit an article, but lets not check that to improve performance a little. + if (!$user->get('guest')) { + $userId = $user->get('id'); + $asset = 'com_content.article.' . $data->id; + + // Check general edit permission first. + if ($user->authorise('core.edit', $asset)) { + $data->params->set('access-edit', true); + } + + // Now check if edit.own is available. + elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) { + // Check for a valid user and that they are the owner. + if ($userId == $data->created_by) { + $data->params->set('access-edit', true); + } + } + } + + // Compute view access permissions. + if ($access = $this->getState('filter.access')) { + // If the access filter has been set, we already know this user can view. + $data->params->set('access-view', true); + } else { + // If no access filter is set, the layout takes some responsibility for display of limited information. + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + if ($data->catid == 0 || $data->category_access === null) { + $data->params->set('access-view', in_array($data->access, $groups)); + } else { + $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); + } + } + + $this->_item[$pk] = $data; + } catch (\Exception $e) { + if ($e->getCode() == 404) { + // Need to go through the error handler to allow Redirect to work. + throw $e; + } else { + $this->setError($e); + $this->_item[$pk] = false; + } + } + } + + return $this->_item[$pk]; + } + + /** + * Increment the hit counter for the article. + * + * @param integer $pk Optional primary key of the article to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('article.id'); + + $table = Table::getInstance('Content', 'JTable'); + $table->hit($pk); + } + + return true; + } + + /** + * Save user vote on article + * + * @param integer $pk Joomla Article Id + * @param integer $rate Voting rate + * + * @return boolean Return true on success + */ + public function storeVote($pk = 0, $rate = 0) + { + $pk = (int) $pk; + $rate = (int) $rate; + + if ($rate >= 1 && $rate <= 5 && $pk > 0) { + $userIP = IpHelper::getIp(); + + // Initialize variables. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Create the base select statement. + $query->select('*') + ->from($db->quoteName('#__content_rating')) + ->where($db->quoteName('content_id') . ' = :pk') + ->bind(':pk', $pk, ParameterType::INTEGER); + + // Set the query and load the result. + $db->setQuery($query); + + // Check for a database error. + try { + $rating = $db->loadObject(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // There are no ratings yet, so lets insert our rating + if (!$rating) { + $query = $db->getQuery(true); + + // Create the base insert statement. + $query->insert($db->quoteName('#__content_rating')) + ->columns( + [ + $db->quoteName('content_id'), + $db->quoteName('lastip'), + $db->quoteName('rating_sum'), + $db->quoteName('rating_count'), + ] + ) + ->values(':pk, :ip, :rate, 1') + ->bind(':pk', $pk, ParameterType::INTEGER) + ->bind(':ip', $userIP) + ->bind(':rate', $rate, ParameterType::INTEGER); + + // Set the query and execute the insert. + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + } else { + if ($userIP != $rating->lastip) { + $query = $db->getQuery(true); + + // Create the base update statement. + $query->update($db->quoteName('#__content_rating')) + ->set( + [ + $db->quoteName('rating_count') . ' = ' . $db->quoteName('rating_count') . ' + 1', + $db->quoteName('rating_sum') . ' = ' . $db->quoteName('rating_sum') . ' + :rate', + $db->quoteName('lastip') . ' = :ip', + ] + ) + ->where($db->quoteName('content_id') . ' = :pk') + ->bind(':rate', $rate, ParameterType::INTEGER) + ->bind(':ip', $userIP) + ->bind(':pk', $pk, ParameterType::INTEGER); + + // Set the query and execute the update. + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + } else { + return false; + } + } + + $this->cleanCache(); + + return true; + } + + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_CONTENT_INVALID_RATING', $rate), 'error'); + + return false; + } + + /** + * Cleans the cache of com_content and content modules + * + * @param string $group The cache group + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 3.9.9 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_content'); + parent::cleanCache('mod_articles_archive'); + parent::cleanCache('mod_articles_categories'); + parent::cleanCache('mod_articles_category'); + parent::cleanCache('mod_articles_latest'); + parent::cleanCache('mod_articles_news'); + parent::cleanCache('mod_articles_popular'); + } } diff --git a/components/com_content/src/Model/ArticlesModel.php b/components/com_content/src/Model/ArticlesModel.php index fe1ab6aefda2c..2650ed4b03ea5 100644 --- a/components/com_content/src/Model/ArticlesModel.php +++ b/components/com_content/src/Model/ArticlesModel.php @@ -1,4 +1,5 @@ input->get('limit', $app->get('list_limit', 0), 'uint'); - $this->setState('list.limit', $value); - - $value = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $value); - - $value = $app->input->get('filter_tag', 0, 'uint'); - $this->setState('filter.tag', $value); - - $orderCol = $app->input->get('filter_order', 'a.ordering'); - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'a.ordering'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->input->get('filter_order_Dir', 'ASC'); - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $params = $app->getParams(); - $this->setState('params', $params); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) - { - // Filter on published for those who do not have edit or edit.state rights. - $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - - // Process show_noauth parameter - if ((!$params->get('show_noauth')) || (!ComponentHelper::getParams('com_content')->get('show_noauth'))) - { - $this->setState('filter.access', true); - } - else - { - $this->setState('filter.access', false); - } - - $this->setState('layout', $app->input->getString('layout')); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . serialize($this->getState('filter.published')); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.featured'); - $id .= ':' . serialize($this->getState('filter.article_id')); - $id .= ':' . $this->getState('filter.article_id.include'); - $id .= ':' . serialize($this->getState('filter.category_id')); - $id .= ':' . $this->getState('filter.category_id.include'); - $id .= ':' . serialize($this->getState('filter.author_id')); - $id .= ':' . $this->getState('filter.author_id.include'); - $id .= ':' . serialize($this->getState('filter.author_alias')); - $id .= ':' . $this->getState('filter.author_alias.include'); - $id .= ':' . $this->getState('filter.date_filtering'); - $id .= ':' . $this->getState('filter.date_field'); - $id .= ':' . $this->getState('filter.start_date_range'); - $id .= ':' . $this->getState('filter.end_date_range'); - $id .= ':' . $this->getState('filter.relative_date'); - $id .= ':' . serialize($this->getState('filter.tag')); - - return parent::getStoreId($id); - } - - /** - * Get the master query for retrieving a list of articles subject to the model state. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - $user = Factory::getUser(); - - // Create a new query object. - $db = $this->getDatabase(); - - /** @var \Joomla\Database\DatabaseQuery $query */ - $query = $db->getQuery(true); - - $nowDate = Factory::getDate()->toSql(); - - $conditionArchived = ContentComponent::CONDITION_ARCHIVED; - $conditionUnpublished = ContentComponent::CONDITION_UNPUBLISHED; - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.title'), - $db->quoteName('a.alias'), - $db->quoteName('a.introtext'), - $db->quoteName('a.fulltext'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.catid'), - $db->quoteName('a.created'), - $db->quoteName('a.created_by'), - $db->quoteName('a.created_by_alias'), - $db->quoteName('a.modified'), - $db->quoteName('a.modified_by'), - // Use created if publish_up is null - 'CASE WHEN ' . $db->quoteName('a.publish_up') . ' IS NULL THEN ' . $db->quoteName('a.created') - . ' ELSE ' . $db->quoteName('a.publish_up') . ' END AS ' . $db->quoteName('publish_up'), - $db->quoteName('a.publish_down'), - $db->quoteName('a.images'), - $db->quoteName('a.urls'), - $db->quoteName('a.attribs'), - $db->quoteName('a.metadata'), - $db->quoteName('a.metakey'), - $db->quoteName('a.metadesc'), - $db->quoteName('a.access'), - $db->quoteName('a.hits'), - $db->quoteName('a.featured'), - $db->quoteName('a.language'), - $query->length($db->quoteName('a.fulltext')) . ' AS ' . $db->quoteName('readmore'), - $db->quoteName('a.ordering'), - ] - ) - ) - ->select( - [ - $db->quoteName('fp.featured_up'), - $db->quoteName('fp.featured_down'), - // Published/archived article in archived category is treated as archived article. If category is not published then force 0. - 'CASE WHEN ' . $db->quoteName('c.published') . ' = 2 AND ' . $db->quoteName('a.state') . ' > 0 THEN ' . $conditionArchived - . ' WHEN ' . $db->quoteName('c.published') . ' != 1 THEN ' . $conditionUnpublished - . ' ELSE ' . $db->quoteName('a.state') . ' END AS ' . $db->quoteName('state'), - $db->quoteName('c.title', 'category_title'), - $db->quoteName('c.path', 'category_route'), - $db->quoteName('c.access', 'category_access'), - $db->quoteName('c.alias', 'category_alias'), - $db->quoteName('c.language', 'category_language'), - $db->quoteName('c.published'), - $db->quoteName('c.published', 'parents_published'), - $db->quoteName('c.lft'), - 'CASE WHEN ' . $db->quoteName('a.created_by_alias') . ' > ' . $db->quote(' ') . ' THEN ' . $db->quoteName('a.created_by_alias') - . ' ELSE ' . $db->quoteName('ua.name') . ' END AS ' . $db->quoteName('author'), - $db->quoteName('ua.email', 'author_email'), - $db->quoteName('uam.name', 'modified_by_name'), - $db->quoteName('parent.title', 'parent_title'), - $db->quoteName('parent.id', 'parent_id'), - $db->quoteName('parent.path', 'parent_route'), - $db->quoteName('parent.alias', 'parent_alias'), - $db->quoteName('parent.language', 'parent_language'), - ] - ) - ->from($db->quoteName('#__content', 'a')) - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) - ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')) - ->join('LEFT', $db->quoteName('#__users', 'uam'), $db->quoteName('uam.id') . ' = ' . $db->quoteName('a.modified_by')) - ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')); - - $params = $this->getState('params'); - $orderby_sec = $params->get('orderby_sec'); - - // Join over the frontpage articles if required. - $frontpageJoin = 'LEFT'; - - if ($this->getState('filter.frontpage')) - { - if ($orderby_sec === 'front') - { - $query->select($db->quoteName('fp.ordering')); - $frontpageJoin = 'INNER'; - } - else - { - $query->where($db->quoteName('a.featured') . ' = 1'); - } - - $query->where( - [ - '(' . $db->quoteName('fp.featured_up') . ' IS NULL OR ' . $db->quoteName('fp.featured_up') . ' <= :frontpageUp)', - '(' . $db->quoteName('fp.featured_down') . ' IS NULL OR ' . $db->quoteName('fp.featured_down') . ' >= :frontpageDown)', - ] - ) - ->bind(':frontpageUp', $nowDate) - ->bind(':frontpageDown', $nowDate); - } - elseif ($orderby_sec === 'front' || $this->getState('list.ordering') === 'fp.ordering') - { - $query->select($db->quoteName('fp.ordering')); - } - - $query->join($frontpageJoin, $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')); - - if (PluginHelper::isEnabled('content', 'vote')) - { - // Join on voting table - $query->select( - [ - 'COALESCE(NULLIF(ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 1), 0), 0)' - . ' AS ' . $db->quoteName('rating'), - 'COALESCE(NULLIF(' . $db->quoteName('v.rating_count') . ', 0), 0) AS ' . $db->quoteName('rating_count'), - ] - ) - ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')); - } - - // Filter by access level. - if ($this->getState('filter.access', true)) - { - $groups = $this->getState('filter.viewlevels', $user->getAuthorisedViewLevels()); - $query->whereIn($db->quoteName('a.access'), $groups) - ->whereIn($db->quoteName('c.access'), $groups); - } - - // Filter by published state - $condition = $this->getState('filter.published'); - - if (is_numeric($condition) && $condition == 2) - { - /** - * If category is archived then article has to be published or archived. - * Or category is published then article has to be archived. - */ - $query->where('((' . $db->quoteName('c.published') . ' = 2 AND ' . $db->quoteName('a.state') . ' > :conditionUnpublished)' - . ' OR (' . $db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') . ' = :conditionArchived))' - ) - ->bind(':conditionUnpublished', $conditionUnpublished, ParameterType::INTEGER) - ->bind(':conditionArchived', $conditionArchived, ParameterType::INTEGER); - } - elseif (is_numeric($condition)) - { - $condition = (int) $condition; - - // Category has to be published - $query->where($db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') . ' = :condition') - ->bind(':condition', $condition, ParameterType::INTEGER); - } - elseif (is_array($condition)) - { - // Category has to be published - $query->where( - $db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') - . ' IN (' . implode(',', $query->bindArray($condition)) . ')' - ); - } - - // Filter by featured state - $featured = $this->getState('filter.featured'); - - switch ($featured) - { - case 'hide': - $query->where($db->quoteName('a.featured') . ' = 0'); - break; - - case 'only': - $query->where( - [ - $db->quoteName('a.featured') . ' = 1', - '(' . $db->quoteName('fp.featured_up') . ' IS NULL OR ' . $db->quoteName('fp.featured_up') . ' <= :featuredUp)', - '(' . $db->quoteName('fp.featured_down') . ' IS NULL OR ' . $db->quoteName('fp.featured_down') . ' >= :featuredDown)', - ] - ) - ->bind(':featuredUp', $nowDate) - ->bind(':featuredDown', $nowDate); - break; - - case 'show': - default: - // Normally we do not discriminate between featured/unfeatured items. - break; - } - - // Filter by a single or group of articles. - $articleId = $this->getState('filter.article_id'); - - if (is_numeric($articleId)) - { - $articleId = (int) $articleId; - $type = $this->getState('filter.article_id.include', true) ? ' = ' : ' <> '; - $query->where($db->quoteName('a.id') . $type . ':articleId') - ->bind(':articleId', $articleId, ParameterType::INTEGER); - } - elseif (is_array($articleId)) - { - $articleId = ArrayHelper::toInteger($articleId); - - if ($this->getState('filter.article_id.include', true)) - { - $query->whereIn($db->quoteName('a.id'), $articleId); - } - else - { - $query->whereNotIn($db->quoteName('a.id'), $articleId); - } - } - - // Filter by a single or group of categories - $categoryId = $this->getState('filter.category_id'); - - if (is_numeric($categoryId)) - { - $type = $this->getState('filter.category_id.include', true) ? ' = ' : ' <> '; - - // Add subcategory check - $includeSubcategories = $this->getState('filter.subcategories', false); - - if ($includeSubcategories) - { - $categoryId = (int) $categoryId; - $levels = (int) $this->getState('filter.max_category_levels', 1); - - // Create a subquery for the subcategory list - $subQuery = $db->getQuery(true) - ->select($db->quoteName('sub.id')) - ->from($db->quoteName('#__categories', 'sub')) - ->join( - 'INNER', - $db->quoteName('#__categories', 'this'), - $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') - . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') - ) - ->where($db->quoteName('this.id') . ' = :subCategoryId'); - - $query->bind(':subCategoryId', $categoryId, ParameterType::INTEGER); - - if ($levels >= 0) - { - $subQuery->where($db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :levels'); - $query->bind(':levels', $levels, ParameterType::INTEGER); - } - - // Add the subquery to the main query - $query->where( - '(' . $db->quoteName('a.catid') . $type . ':categoryId OR ' . $db->quoteName('a.catid') . ' IN (' . $subQuery . '))' - ); - $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - else - { - $query->where($db->quoteName('a.catid') . $type . ':categoryId'); - $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - } - elseif (is_array($categoryId) && (count($categoryId) > 0)) - { - $categoryId = ArrayHelper::toInteger($categoryId); - - if (!empty($categoryId)) - { - if ($this->getState('filter.category_id.include', true)) - { - $query->whereIn($db->quoteName('a.catid'), $categoryId); - } - else - { - $query->whereNotIn($db->quoteName('a.catid'), $categoryId); - } - } - } - - // Filter by author - $authorId = $this->getState('filter.author_id'); - $authorWhere = ''; - - if (is_numeric($authorId)) - { - $authorId = (int) $authorId; - $type = $this->getState('filter.author_id.include', true) ? ' = ' : ' <> '; - $authorWhere = $db->quoteName('a.created_by') . $type . ':authorId'; - $query->bind(':authorId', $authorId, ParameterType::INTEGER); - } - elseif (is_array($authorId)) - { - $authorId = array_values(array_filter($authorId, 'is_numeric')); - - if ($authorId) - { - $type = $this->getState('filter.author_id.include', true) ? ' IN' : ' NOT IN'; - $authorWhere = $db->quoteName('a.created_by') . $type . ' (' . implode(',', $query->bindArray($authorId)) . ')'; - } - } - - // Filter by author alias - $authorAlias = $this->getState('filter.author_alias'); - $authorAliasWhere = ''; - - if (is_string($authorAlias)) - { - $type = $this->getState('filter.author_alias.include', true) ? ' = ' : ' <> '; - $authorAliasWhere = $db->quoteName('a.created_by_alias') . $type . ':authorAlias'; - $query->bind(':authorAlias', $authorAlias); - } - elseif (\is_array($authorAlias) && !empty($authorAlias)) - { - $type = $this->getState('filter.author_alias.include', true) ? ' IN' : ' NOT IN'; - $authorAliasWhere = $db->quoteName('a.created_by_alias') . $type - . ' (' . implode(',', $query->bindArray($authorAlias, ParameterType::STRING)) . ')'; - } - - if (!empty($authorWhere) && !empty($authorAliasWhere)) - { - $query->where('(' . $authorWhere . ' OR ' . $authorAliasWhere . ')'); - } - elseif (!empty($authorWhere) || !empty($authorAliasWhere)) - { - // One of these is empty, the other is not so we just add both - $query->where($authorWhere . $authorAliasWhere); - } - - // Filter by start and end dates. - if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) - { - $query->where( - [ - '(' . $db->quoteName('a.publish_up') . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publishUp)', - '(' . $db->quoteName('a.publish_down') . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publishDown)', - ] - ) - ->bind(':publishUp', $nowDate) - ->bind(':publishDown', $nowDate); - } - - // Filter by Date Range or Relative Date - $dateFiltering = $this->getState('filter.date_filtering', 'off'); - $dateField = $db->escape($this->getState('filter.date_field', 'a.created')); - - switch ($dateFiltering) - { - case 'range': - $startDateRange = $this->getState('filter.start_date_range', ''); - $endDateRange = $this->getState('filter.end_date_range', ''); - - if ($startDateRange || $endDateRange) - { - $query->where($db->quoteName($dateField) . ' IS NOT NULL'); - - if ($startDateRange) - { - $query->where($db->quoteName($dateField) . ' >= :startDateRange') - ->bind(':startDateRange', $startDateRange); - } - - if ($endDateRange) - { - $query->where($db->quoteName($dateField) . ' <= :endDateRange') - ->bind(':endDateRange', $endDateRange); - } - } - - break; - - case 'relative': - $relativeDate = (int) $this->getState('filter.relative_date', 0); - $query->where( - $db->quoteName($dateField) . ' IS NOT NULL AND ' - . $db->quoteName($dateField) . ' >= ' . $query->dateAdd($db->quote($nowDate), -1 * $relativeDate, 'DAY') - ); - break; - - case 'off': - default: - break; - } - - // Process the filter for list views with user-entered filters - if (is_object($params) && ($params->get('filter_field') !== 'hide') && ($filter = $this->getState('list.filter'))) - { - // Clean filter variable - $filter = StringHelper::strtolower($filter); - $monthFilter = $filter; - $hitsFilter = (int) $filter; - $textFilter = '%' . $filter . '%'; - - switch ($params->get('filter_field')) - { - case 'author': - $query->where( - 'LOWER(CASE WHEN ' . $db->quoteName('a.created_by_alias') . ' > ' . $db->quote(' ') - . ' THEN ' . $db->quoteName('a.created_by_alias') . ' ELSE ' . $db->quoteName('ua.name') . ' END) LIKE :search' - ) - ->bind(':search', $textFilter); - break; - - case 'hits': - $query->where($db->quoteName('a.hits') . ' >= :hits') - ->bind(':hits', $hitsFilter, ParameterType::INTEGER); - break; - - case 'month': - if ($monthFilter != '') - { - $monthStart = date("Y-m-d", strtotime($monthFilter)) . ' 00:00:00'; - $monthEnd = date("Y-m-t", strtotime($monthFilter)) . ' 23:59:59'; - - $query->where( - [ - ':monthStart <= CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END', - ':monthEnd >= CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END', - ] - ) - ->bind(':monthStart', $monthStart) - ->bind(':monthEnd', $monthEnd); - } - break; - - case 'title': - default: - // Default to 'title' if parameter is not valid - $query->where('LOWER(' . $db->quoteName('a.title') . ') LIKE :search') - ->bind(':search', $textFilter); - break; - } - } - - // Filter by language - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getApplication()->getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - // Filter by a single or group of tags. - $tagId = $this->getState('filter.tag'); - - if (is_array($tagId) && count($tagId) === 1) - { - $tagId = current($tagId); - } - - if (is_array($tagId)) - { - $tagId = ArrayHelper::toInteger($tagId); - - if ($tagId) - { - $subQuery = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('content_item_id')) - ->from($db->quoteName('#__contentitem_tag_map')) - ->where( - [ - $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tagId)) . ')', - $db->quoteName('type_alias') . ' = ' . $db->quote('com_content.article'), - ] - ); - - $query->join( - 'INNER', - '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ); - } - } - elseif ($tagId = (int) $tagId) - { - $query->join( - 'INNER', - $db->quoteName('#__contentitem_tag_map', 'tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - . ' AND ' . $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_content.article') - ) - ->where($db->quoteName('tagmap.tag_id') . ' = :tagId') - ->bind(':tagId', $tagId, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $query->order( - $db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) - ); - - return $query; - } - - /** - * Method to get a list of articles. - * - * Overridden to inject convert the attribs field into a Registry object. - * - * @return mixed An array of objects on success, false on failure. - * - * @since 1.6 - */ - public function getItems() - { - $items = parent::getItems(); - - $user = Factory::getUser(); - $userId = $user->get('id'); - $guest = $user->get('guest'); - $groups = $user->getAuthorisedViewLevels(); - $input = Factory::getApplication()->input; - - // Get the global params - $globalParams = ComponentHelper::getParams('com_content', true); - - $taggedItems = []; - - // Convert the parameter fields into objects. - foreach ($items as $item) - { - $articleParams = new Registry($item->attribs); - - // Unpack readmore and layout params - $item->alternative_readmore = $articleParams->get('alternative_readmore'); - $item->layout = $articleParams->get('layout'); - - $item->params = clone $this->getState('params'); - - /** - * For blogs, article params override menu item params only if menu param = 'use_article' - * Otherwise, menu item params control the layout - * If menu item is 'use_article' and there is no article param, use global - */ - if (($input->getString('layout') === 'blog') || ($input->getString('view') === 'featured') - || ($this->getState('params')->get('layout_type') === 'blog')) - { - // Create an array of just the params set to 'use_article' - $menuParamsArray = $this->getState('params')->toArray(); - $articleArray = array(); - - foreach ($menuParamsArray as $key => $value) - { - if ($value === 'use_article') - { - // If the article has a value, use it - if ($articleParams->get($key) != '') - { - // Get the value from the article - $articleArray[$key] = $articleParams->get($key); - } - else - { - // Otherwise, use the global value - $articleArray[$key] = $globalParams->get($key); - } - } - } - - // Merge the selected article params - if (count($articleArray) > 0) - { - $articleParams = new Registry($articleArray); - $item->params->merge($articleParams); - } - } - else - { - // For non-blog layouts, merge all of the article params - $item->params->merge($articleParams); - } - - // Get display date - switch ($item->params->get('list_show_date')) - { - case 'modified': - $item->displayDate = $item->modified; - break; - - case 'published': - $item->displayDate = ($item->publish_up == 0) ? $item->created : $item->publish_up; - break; - - default: - case 'created': - $item->displayDate = $item->created; - break; - } - - /** - * Compute the asset access permissions. - * Technically guest could edit an article, but lets not check that to improve performance a little. - */ - if (!$guest) - { - $asset = 'com_content.article.' . $item->id; - - // Check general edit permission first. - if ($user->authorise('core.edit', $asset)) - { - $item->params->set('access-edit', true); - } - - // Now check if edit.own is available. - elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) - { - // Check for a valid user and that they are the owner. - if ($userId == $item->created_by) - { - $item->params->set('access-edit', true); - } - } - } - - $access = $this->getState('filter.access'); - - if ($access) - { - // If the access filter has been set, we already have only the articles this user can view. - $item->params->set('access-view', true); - } - else - { - // If no access filter is set, the layout takes some responsibility for display of limited information. - if ($item->catid == 0 || $item->category_access === null) - { - $item->params->set('access-view', in_array($item->access, $groups)); - } - else - { - $item->params->set('access-view', in_array($item->access, $groups) && in_array($item->category_access, $groups)); - } - } - - // Some contexts may not use tags data at all, so we allow callers to disable loading tag data - if ($this->getState('load_tags', $item->params->get('show_tags', '1'))) - { - $item->tags = new TagsHelper; - $taggedItems[$item->id] = $item; - } - - if (Associations::isEnabled() && $item->params->get('show_associations')) - { - $item->associations = AssociationHelper::displayAssociations($item->id); - } - } - - // Load tags of all items. - if ($taggedItems) - { - $tagsHelper = new TagsHelper; - $itemIds = \array_keys($taggedItems); - - foreach ($tagsHelper->getMultipleItemTags('com_content.article', $itemIds) as $id => $tags) - { - $taggedItems[$id]->tags->itemTags = $tags; - } - } - - return $items; - } - - /** - * Method to get the starting number of items for the data set. - * - * @return integer The starting number of items available in the data set. - * - * @since 3.0.1 - */ - public function getStart() - { - return $this->getState('list.start'); - } - - /** - * Count Items by Month - * - * @return mixed An array of objects on success, false on failure. - * - * @since 3.9.0 - */ - public function countItemsByMonth() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Get the list query. - $listQuery = $this->getListQuery(); - $bounded = $listQuery->getBounded(); - - // Bind list query variables to our new query. - $keys = array_keys($bounded); - $values = array_column($bounded, 'value'); - $dataTypes = array_column($bounded, 'dataType'); - - $query->bind($keys, $values, $dataTypes); - - $query - ->select( - 'DATE(' . - $query->concatenate( - array( - $query->year($db->quoteName('publish_up')), - $db->quote('-'), - $query->month($db->quoteName('publish_up')), - $db->quote('-01') - ) - ) . ') AS ' . $db->quoteName('d') - ) - ->select('COUNT(*) AS ' . $db->quoteName('c')) - ->from('(' . $this->getListQuery() . ') AS ' . $db->quoteName('b')) - ->group($db->quoteName('d')) - ->order($db->quoteName('d') . ' DESC'); - - return $db->setQuery($query)->loadObjectList(); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see \JController + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'alias', 'a.alias', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'catid', 'a.catid', 'category_title', + 'state', 'a.state', + 'access', 'a.access', 'access_level', + 'created', 'a.created', + 'created_by', 'a.created_by', + 'ordering', 'a.ordering', + 'featured', 'a.featured', + 'language', 'a.language', + 'hits', 'a.hits', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'images', 'a.images', + 'urls', 'a.urls', + 'filter_tag', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.0.1 + */ + protected function populateState($ordering = 'ordering', $direction = 'ASC') + { + $app = Factory::getApplication(); + + // List state information + $value = $app->input->get('limit', $app->get('list_limit', 0), 'uint'); + $this->setState('list.limit', $value); + + $value = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $value); + + $value = $app->input->get('filter_tag', 0, 'uint'); + $this->setState('filter.tag', $value); + + $orderCol = $app->input->get('filter_order', 'a.ordering'); + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'a.ordering'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->input->get('filter_order_Dir', 'ASC'); + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $params = $app->getParams(); + $this->setState('params', $params); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) { + // Filter on published for those who do not have edit or edit.state rights. + $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + + // Process show_noauth parameter + if ((!$params->get('show_noauth')) || (!ComponentHelper::getParams('com_content')->get('show_noauth'))) { + $this->setState('filter.access', true); + } else { + $this->setState('filter.access', false); + } + + $this->setState('layout', $app->input->getString('layout')); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . serialize($this->getState('filter.published')); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.featured'); + $id .= ':' . serialize($this->getState('filter.article_id')); + $id .= ':' . $this->getState('filter.article_id.include'); + $id .= ':' . serialize($this->getState('filter.category_id')); + $id .= ':' . $this->getState('filter.category_id.include'); + $id .= ':' . serialize($this->getState('filter.author_id')); + $id .= ':' . $this->getState('filter.author_id.include'); + $id .= ':' . serialize($this->getState('filter.author_alias')); + $id .= ':' . $this->getState('filter.author_alias.include'); + $id .= ':' . $this->getState('filter.date_filtering'); + $id .= ':' . $this->getState('filter.date_field'); + $id .= ':' . $this->getState('filter.start_date_range'); + $id .= ':' . $this->getState('filter.end_date_range'); + $id .= ':' . $this->getState('filter.relative_date'); + $id .= ':' . serialize($this->getState('filter.tag')); + + return parent::getStoreId($id); + } + + /** + * Get the master query for retrieving a list of articles subject to the model state. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + $user = Factory::getUser(); + + // Create a new query object. + $db = $this->getDatabase(); + + /** @var \Joomla\Database\DatabaseQuery $query */ + $query = $db->getQuery(true); + + $nowDate = Factory::getDate()->toSql(); + + $conditionArchived = ContentComponent::CONDITION_ARCHIVED; + $conditionUnpublished = ContentComponent::CONDITION_UNPUBLISHED; + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.title'), + $db->quoteName('a.alias'), + $db->quoteName('a.introtext'), + $db->quoteName('a.fulltext'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.catid'), + $db->quoteName('a.created'), + $db->quoteName('a.created_by'), + $db->quoteName('a.created_by_alias'), + $db->quoteName('a.modified'), + $db->quoteName('a.modified_by'), + // Use created if publish_up is null + 'CASE WHEN ' . $db->quoteName('a.publish_up') . ' IS NULL THEN ' . $db->quoteName('a.created') + . ' ELSE ' . $db->quoteName('a.publish_up') . ' END AS ' . $db->quoteName('publish_up'), + $db->quoteName('a.publish_down'), + $db->quoteName('a.images'), + $db->quoteName('a.urls'), + $db->quoteName('a.attribs'), + $db->quoteName('a.metadata'), + $db->quoteName('a.metakey'), + $db->quoteName('a.metadesc'), + $db->quoteName('a.access'), + $db->quoteName('a.hits'), + $db->quoteName('a.featured'), + $db->quoteName('a.language'), + $query->length($db->quoteName('a.fulltext')) . ' AS ' . $db->quoteName('readmore'), + $db->quoteName('a.ordering'), + ] + ) + ) + ->select( + [ + $db->quoteName('fp.featured_up'), + $db->quoteName('fp.featured_down'), + // Published/archived article in archived category is treated as archived article. If category is not published then force 0. + 'CASE WHEN ' . $db->quoteName('c.published') . ' = 2 AND ' . $db->quoteName('a.state') . ' > 0 THEN ' . $conditionArchived + . ' WHEN ' . $db->quoteName('c.published') . ' != 1 THEN ' . $conditionUnpublished + . ' ELSE ' . $db->quoteName('a.state') . ' END AS ' . $db->quoteName('state'), + $db->quoteName('c.title', 'category_title'), + $db->quoteName('c.path', 'category_route'), + $db->quoteName('c.access', 'category_access'), + $db->quoteName('c.alias', 'category_alias'), + $db->quoteName('c.language', 'category_language'), + $db->quoteName('c.published'), + $db->quoteName('c.published', 'parents_published'), + $db->quoteName('c.lft'), + 'CASE WHEN ' . $db->quoteName('a.created_by_alias') . ' > ' . $db->quote(' ') . ' THEN ' . $db->quoteName('a.created_by_alias') + . ' ELSE ' . $db->quoteName('ua.name') . ' END AS ' . $db->quoteName('author'), + $db->quoteName('ua.email', 'author_email'), + $db->quoteName('uam.name', 'modified_by_name'), + $db->quoteName('parent.title', 'parent_title'), + $db->quoteName('parent.id', 'parent_id'), + $db->quoteName('parent.path', 'parent_route'), + $db->quoteName('parent.alias', 'parent_alias'), + $db->quoteName('parent.language', 'parent_language'), + ] + ) + ->from($db->quoteName('#__content', 'a')) + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) + ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')) + ->join('LEFT', $db->quoteName('#__users', 'uam'), $db->quoteName('uam.id') . ' = ' . $db->quoteName('a.modified_by')) + ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')); + + $params = $this->getState('params'); + $orderby_sec = $params->get('orderby_sec'); + + // Join over the frontpage articles if required. + $frontpageJoin = 'LEFT'; + + if ($this->getState('filter.frontpage')) { + if ($orderby_sec === 'front') { + $query->select($db->quoteName('fp.ordering')); + $frontpageJoin = 'INNER'; + } else { + $query->where($db->quoteName('a.featured') . ' = 1'); + } + + $query->where( + [ + '(' . $db->quoteName('fp.featured_up') . ' IS NULL OR ' . $db->quoteName('fp.featured_up') . ' <= :frontpageUp)', + '(' . $db->quoteName('fp.featured_down') . ' IS NULL OR ' . $db->quoteName('fp.featured_down') . ' >= :frontpageDown)', + ] + ) + ->bind(':frontpageUp', $nowDate) + ->bind(':frontpageDown', $nowDate); + } elseif ($orderby_sec === 'front' || $this->getState('list.ordering') === 'fp.ordering') { + $query->select($db->quoteName('fp.ordering')); + } + + $query->join($frontpageJoin, $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')); + + if (PluginHelper::isEnabled('content', 'vote')) { + // Join on voting table + $query->select( + [ + 'COALESCE(NULLIF(ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 1), 0), 0)' + . ' AS ' . $db->quoteName('rating'), + 'COALESCE(NULLIF(' . $db->quoteName('v.rating_count') . ', 0), 0) AS ' . $db->quoteName('rating_count'), + ] + ) + ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')); + } + + // Filter by access level. + if ($this->getState('filter.access', true)) { + $groups = $this->getState('filter.viewlevels', $user->getAuthorisedViewLevels()); + $query->whereIn($db->quoteName('a.access'), $groups) + ->whereIn($db->quoteName('c.access'), $groups); + } + + // Filter by published state + $condition = $this->getState('filter.published'); + + if (is_numeric($condition) && $condition == 2) { + /** + * If category is archived then article has to be published or archived. + * Or category is published then article has to be archived. + */ + $query->where('((' . $db->quoteName('c.published') . ' = 2 AND ' . $db->quoteName('a.state') . ' > :conditionUnpublished)' + . ' OR (' . $db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') . ' = :conditionArchived))') + ->bind(':conditionUnpublished', $conditionUnpublished, ParameterType::INTEGER) + ->bind(':conditionArchived', $conditionArchived, ParameterType::INTEGER); + } elseif (is_numeric($condition)) { + $condition = (int) $condition; + + // Category has to be published + $query->where($db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') . ' = :condition') + ->bind(':condition', $condition, ParameterType::INTEGER); + } elseif (is_array($condition)) { + // Category has to be published + $query->where( + $db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') + . ' IN (' . implode(',', $query->bindArray($condition)) . ')' + ); + } + + // Filter by featured state + $featured = $this->getState('filter.featured'); + + switch ($featured) { + case 'hide': + $query->where($db->quoteName('a.featured') . ' = 0'); + break; + + case 'only': + $query->where( + [ + $db->quoteName('a.featured') . ' = 1', + '(' . $db->quoteName('fp.featured_up') . ' IS NULL OR ' . $db->quoteName('fp.featured_up') . ' <= :featuredUp)', + '(' . $db->quoteName('fp.featured_down') . ' IS NULL OR ' . $db->quoteName('fp.featured_down') . ' >= :featuredDown)', + ] + ) + ->bind(':featuredUp', $nowDate) + ->bind(':featuredDown', $nowDate); + break; + + case 'show': + default: + // Normally we do not discriminate between featured/unfeatured items. + break; + } + + // Filter by a single or group of articles. + $articleId = $this->getState('filter.article_id'); + + if (is_numeric($articleId)) { + $articleId = (int) $articleId; + $type = $this->getState('filter.article_id.include', true) ? ' = ' : ' <> '; + $query->where($db->quoteName('a.id') . $type . ':articleId') + ->bind(':articleId', $articleId, ParameterType::INTEGER); + } elseif (is_array($articleId)) { + $articleId = ArrayHelper::toInteger($articleId); + + if ($this->getState('filter.article_id.include', true)) { + $query->whereIn($db->quoteName('a.id'), $articleId); + } else { + $query->whereNotIn($db->quoteName('a.id'), $articleId); + } + } + + // Filter by a single or group of categories + $categoryId = $this->getState('filter.category_id'); + + if (is_numeric($categoryId)) { + $type = $this->getState('filter.category_id.include', true) ? ' = ' : ' <> '; + + // Add subcategory check + $includeSubcategories = $this->getState('filter.subcategories', false); + + if ($includeSubcategories) { + $categoryId = (int) $categoryId; + $levels = (int) $this->getState('filter.max_category_levels', 1); + + // Create a subquery for the subcategory list + $subQuery = $db->getQuery(true) + ->select($db->quoteName('sub.id')) + ->from($db->quoteName('#__categories', 'sub')) + ->join( + 'INNER', + $db->quoteName('#__categories', 'this'), + $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') + . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') + ) + ->where($db->quoteName('this.id') . ' = :subCategoryId'); + + $query->bind(':subCategoryId', $categoryId, ParameterType::INTEGER); + + if ($levels >= 0) { + $subQuery->where($db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :levels'); + $query->bind(':levels', $levels, ParameterType::INTEGER); + } + + // Add the subquery to the main query + $query->where( + '(' . $db->quoteName('a.catid') . $type . ':categoryId OR ' . $db->quoteName('a.catid') . ' IN (' . $subQuery . '))' + ); + $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } else { + $query->where($db->quoteName('a.catid') . $type . ':categoryId'); + $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + } elseif (is_array($categoryId) && (count($categoryId) > 0)) { + $categoryId = ArrayHelper::toInteger($categoryId); + + if (!empty($categoryId)) { + if ($this->getState('filter.category_id.include', true)) { + $query->whereIn($db->quoteName('a.catid'), $categoryId); + } else { + $query->whereNotIn($db->quoteName('a.catid'), $categoryId); + } + } + } + + // Filter by author + $authorId = $this->getState('filter.author_id'); + $authorWhere = ''; + + if (is_numeric($authorId)) { + $authorId = (int) $authorId; + $type = $this->getState('filter.author_id.include', true) ? ' = ' : ' <> '; + $authorWhere = $db->quoteName('a.created_by') . $type . ':authorId'; + $query->bind(':authorId', $authorId, ParameterType::INTEGER); + } elseif (is_array($authorId)) { + $authorId = array_values(array_filter($authorId, 'is_numeric')); + + if ($authorId) { + $type = $this->getState('filter.author_id.include', true) ? ' IN' : ' NOT IN'; + $authorWhere = $db->quoteName('a.created_by') . $type . ' (' . implode(',', $query->bindArray($authorId)) . ')'; + } + } + + // Filter by author alias + $authorAlias = $this->getState('filter.author_alias'); + $authorAliasWhere = ''; + + if (is_string($authorAlias)) { + $type = $this->getState('filter.author_alias.include', true) ? ' = ' : ' <> '; + $authorAliasWhere = $db->quoteName('a.created_by_alias') . $type . ':authorAlias'; + $query->bind(':authorAlias', $authorAlias); + } elseif (\is_array($authorAlias) && !empty($authorAlias)) { + $type = $this->getState('filter.author_alias.include', true) ? ' IN' : ' NOT IN'; + $authorAliasWhere = $db->quoteName('a.created_by_alias') . $type + . ' (' . implode(',', $query->bindArray($authorAlias, ParameterType::STRING)) . ')'; + } + + if (!empty($authorWhere) && !empty($authorAliasWhere)) { + $query->where('(' . $authorWhere . ' OR ' . $authorAliasWhere . ')'); + } elseif (!empty($authorWhere) || !empty($authorAliasWhere)) { + // One of these is empty, the other is not so we just add both + $query->where($authorWhere . $authorAliasWhere); + } + + // Filter by start and end dates. + if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) { + $query->where( + [ + '(' . $db->quoteName('a.publish_up') . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publishUp)', + '(' . $db->quoteName('a.publish_down') . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publishDown)', + ] + ) + ->bind(':publishUp', $nowDate) + ->bind(':publishDown', $nowDate); + } + + // Filter by Date Range or Relative Date + $dateFiltering = $this->getState('filter.date_filtering', 'off'); + $dateField = $db->escape($this->getState('filter.date_field', 'a.created')); + + switch ($dateFiltering) { + case 'range': + $startDateRange = $this->getState('filter.start_date_range', ''); + $endDateRange = $this->getState('filter.end_date_range', ''); + + if ($startDateRange || $endDateRange) { + $query->where($db->quoteName($dateField) . ' IS NOT NULL'); + + if ($startDateRange) { + $query->where($db->quoteName($dateField) . ' >= :startDateRange') + ->bind(':startDateRange', $startDateRange); + } + + if ($endDateRange) { + $query->where($db->quoteName($dateField) . ' <= :endDateRange') + ->bind(':endDateRange', $endDateRange); + } + } + + break; + + case 'relative': + $relativeDate = (int) $this->getState('filter.relative_date', 0); + $query->where( + $db->quoteName($dateField) . ' IS NOT NULL AND ' + . $db->quoteName($dateField) . ' >= ' . $query->dateAdd($db->quote($nowDate), -1 * $relativeDate, 'DAY') + ); + break; + + case 'off': + default: + break; + } + + // Process the filter for list views with user-entered filters + if (is_object($params) && ($params->get('filter_field') !== 'hide') && ($filter = $this->getState('list.filter'))) { + // Clean filter variable + $filter = StringHelper::strtolower($filter); + $monthFilter = $filter; + $hitsFilter = (int) $filter; + $textFilter = '%' . $filter . '%'; + + switch ($params->get('filter_field')) { + case 'author': + $query->where( + 'LOWER(CASE WHEN ' . $db->quoteName('a.created_by_alias') . ' > ' . $db->quote(' ') + . ' THEN ' . $db->quoteName('a.created_by_alias') . ' ELSE ' . $db->quoteName('ua.name') . ' END) LIKE :search' + ) + ->bind(':search', $textFilter); + break; + + case 'hits': + $query->where($db->quoteName('a.hits') . ' >= :hits') + ->bind(':hits', $hitsFilter, ParameterType::INTEGER); + break; + + case 'month': + if ($monthFilter != '') { + $monthStart = date("Y-m-d", strtotime($monthFilter)) . ' 00:00:00'; + $monthEnd = date("Y-m-t", strtotime($monthFilter)) . ' 23:59:59'; + + $query->where( + [ + ':monthStart <= CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END', + ':monthEnd >= CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END', + ] + ) + ->bind(':monthStart', $monthStart) + ->bind(':monthEnd', $monthEnd); + } + break; + + case 'title': + default: + // Default to 'title' if parameter is not valid + $query->where('LOWER(' . $db->quoteName('a.title') . ') LIKE :search') + ->bind(':search', $textFilter); + break; + } + } + + // Filter by language + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getApplication()->getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + // Filter by a single or group of tags. + $tagId = $this->getState('filter.tag'); + + if (is_array($tagId) && count($tagId) === 1) { + $tagId = current($tagId); + } + + if (is_array($tagId)) { + $tagId = ArrayHelper::toInteger($tagId); + + if ($tagId) { + $subQuery = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('content_item_id')) + ->from($db->quoteName('#__contentitem_tag_map')) + ->where( + [ + $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tagId)) . ')', + $db->quoteName('type_alias') . ' = ' . $db->quote('com_content.article'), + ] + ); + + $query->join( + 'INNER', + '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ); + } + } elseif ($tagId = (int) $tagId) { + $query->join( + 'INNER', + $db->quoteName('#__contentitem_tag_map', 'tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + . ' AND ' . $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_content.article') + ) + ->where($db->quoteName('tagmap.tag_id') . ' = :tagId') + ->bind(':tagId', $tagId, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $query->order( + $db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) + ); + + return $query; + } + + /** + * Method to get a list of articles. + * + * Overridden to inject convert the attribs field into a Registry object. + * + * @return mixed An array of objects on success, false on failure. + * + * @since 1.6 + */ + public function getItems() + { + $items = parent::getItems(); + + $user = Factory::getUser(); + $userId = $user->get('id'); + $guest = $user->get('guest'); + $groups = $user->getAuthorisedViewLevels(); + $input = Factory::getApplication()->input; + + // Get the global params + $globalParams = ComponentHelper::getParams('com_content', true); + + $taggedItems = []; + + // Convert the parameter fields into objects. + foreach ($items as $item) { + $articleParams = new Registry($item->attribs); + + // Unpack readmore and layout params + $item->alternative_readmore = $articleParams->get('alternative_readmore'); + $item->layout = $articleParams->get('layout'); + + $item->params = clone $this->getState('params'); + + /** + * For blogs, article params override menu item params only if menu param = 'use_article' + * Otherwise, menu item params control the layout + * If menu item is 'use_article' and there is no article param, use global + */ + if ( + ($input->getString('layout') === 'blog') || ($input->getString('view') === 'featured') + || ($this->getState('params')->get('layout_type') === 'blog') + ) { + // Create an array of just the params set to 'use_article' + $menuParamsArray = $this->getState('params')->toArray(); + $articleArray = array(); + + foreach ($menuParamsArray as $key => $value) { + if ($value === 'use_article') { + // If the article has a value, use it + if ($articleParams->get($key) != '') { + // Get the value from the article + $articleArray[$key] = $articleParams->get($key); + } else { + // Otherwise, use the global value + $articleArray[$key] = $globalParams->get($key); + } + } + } + + // Merge the selected article params + if (count($articleArray) > 0) { + $articleParams = new Registry($articleArray); + $item->params->merge($articleParams); + } + } else { + // For non-blog layouts, merge all of the article params + $item->params->merge($articleParams); + } + + // Get display date + switch ($item->params->get('list_show_date')) { + case 'modified': + $item->displayDate = $item->modified; + break; + + case 'published': + $item->displayDate = ($item->publish_up == 0) ? $item->created : $item->publish_up; + break; + + default: + case 'created': + $item->displayDate = $item->created; + break; + } + + /** + * Compute the asset access permissions. + * Technically guest could edit an article, but lets not check that to improve performance a little. + */ + if (!$guest) { + $asset = 'com_content.article.' . $item->id; + + // Check general edit permission first. + if ($user->authorise('core.edit', $asset)) { + $item->params->set('access-edit', true); + } + + // Now check if edit.own is available. + elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) { + // Check for a valid user and that they are the owner. + if ($userId == $item->created_by) { + $item->params->set('access-edit', true); + } + } + } + + $access = $this->getState('filter.access'); + + if ($access) { + // If the access filter has been set, we already have only the articles this user can view. + $item->params->set('access-view', true); + } else { + // If no access filter is set, the layout takes some responsibility for display of limited information. + if ($item->catid == 0 || $item->category_access === null) { + $item->params->set('access-view', in_array($item->access, $groups)); + } else { + $item->params->set('access-view', in_array($item->access, $groups) && in_array($item->category_access, $groups)); + } + } + + // Some contexts may not use tags data at all, so we allow callers to disable loading tag data + if ($this->getState('load_tags', $item->params->get('show_tags', '1'))) { + $item->tags = new TagsHelper(); + $taggedItems[$item->id] = $item; + } + + if (Associations::isEnabled() && $item->params->get('show_associations')) { + $item->associations = AssociationHelper::displayAssociations($item->id); + } + } + + // Load tags of all items. + if ($taggedItems) { + $tagsHelper = new TagsHelper(); + $itemIds = \array_keys($taggedItems); + + foreach ($tagsHelper->getMultipleItemTags('com_content.article', $itemIds) as $id => $tags) { + $taggedItems[$id]->tags->itemTags = $tags; + } + } + + return $items; + } + + /** + * Method to get the starting number of items for the data set. + * + * @return integer The starting number of items available in the data set. + * + * @since 3.0.1 + */ + public function getStart() + { + return $this->getState('list.start'); + } + + /** + * Count Items by Month + * + * @return mixed An array of objects on success, false on failure. + * + * @since 3.9.0 + */ + public function countItemsByMonth() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Get the list query. + $listQuery = $this->getListQuery(); + $bounded = $listQuery->getBounded(); + + // Bind list query variables to our new query. + $keys = array_keys($bounded); + $values = array_column($bounded, 'value'); + $dataTypes = array_column($bounded, 'dataType'); + + $query->bind($keys, $values, $dataTypes); + + $query + ->select( + 'DATE(' . + $query->concatenate( + array( + $query->year($db->quoteName('publish_up')), + $db->quote('-'), + $query->month($db->quoteName('publish_up')), + $db->quote('-01') + ) + ) . ') AS ' . $db->quoteName('d') + ) + ->select('COUNT(*) AS ' . $db->quoteName('c')) + ->from('(' . $this->getListQuery() . ') AS ' . $db->quoteName('b')) + ->group($db->quoteName('d')) + ->order($db->quoteName('d') . ' DESC'); + + return $db->setQuery($query)->loadObjectList(); + } } diff --git a/components/com_content/src/Model/CategoriesModel.php b/components/com_content/src/Model/CategoriesModel.php index b3a3a4ebcac42..86bd9b6e5745d 100644 --- a/components/com_content/src/Model/CategoriesModel.php +++ b/components/com_content/src/Model/CategoriesModel.php @@ -1,4 +1,5 @@ setState('filter.extension', $this->_extension); - - // Get the parent id if defined. - $parentId = $app->input->getInt('id'); - $this->setState('filter.parentId', $parentId); - - $params = $app->getParams(); - $this->setState('params', $params); - - $this->setState('filter.published', 1); - $this->setState('filter.access', true); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.extension'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.parentId'); - - return parent::getStoreId($id); - } - - /** - * Redefine the function and add some properties to make the styling easier - * - * @param bool $recursive True if you want to return children recursively. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 1.6 - */ - public function getItems($recursive = false) - { - $store = $this->getStoreId(); - - if (!isset($this->cache[$store])) - { - $app = Factory::getApplication(); - $menu = $app->getMenu(); - $active = $menu->getActive(); - - if ($active) - { - $params = $active->getParams(); - } - else - { - $params = new Registry; - } - - $options = array(); - $options['countItems'] = $params->get('show_cat_num_articles_cat', 1) || !$params->get('show_empty_categories_cat', 0); - $categories = Categories::getInstance('Content', $options); - $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); - - if (is_object($this->_parent)) - { - $this->cache[$store] = $this->_parent->getChildren($recursive); - } - else - { - $this->cache[$store] = false; - } - } - - return $this->cache[$store]; - } - - /** - * Get the parent. - * - * @return object An array of data items on success, false on failure. - * - * @since 1.6 - */ - public function getParent() - { - if (!is_object($this->_parent)) - { - $this->getItems(); - } - - return $this->_parent; - } + /** + * Model context string. + * + * @var string + */ + public $_context = 'com_content.categories'; + + /** + * The category context (allows other extensions to derived from this model). + * + * @var string + */ + protected $_extension = 'com_content'; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + private $_parent = null; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering The field to order on. + * @param string $direction The direction to order on. + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $this->setState('filter.extension', $this->_extension); + + // Get the parent id if defined. + $parentId = $app->input->getInt('id'); + $this->setState('filter.parentId', $parentId); + + $params = $app->getParams(); + $this->setState('params', $params); + + $this->setState('filter.published', 1); + $this->setState('filter.access', true); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.extension'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.parentId'); + + return parent::getStoreId($id); + } + + /** + * Redefine the function and add some properties to make the styling easier + * + * @param bool $recursive True if you want to return children recursively. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 1.6 + */ + public function getItems($recursive = false) + { + $store = $this->getStoreId(); + + if (!isset($this->cache[$store])) { + $app = Factory::getApplication(); + $menu = $app->getMenu(); + $active = $menu->getActive(); + + if ($active) { + $params = $active->getParams(); + } else { + $params = new Registry(); + } + + $options = array(); + $options['countItems'] = $params->get('show_cat_num_articles_cat', 1) || !$params->get('show_empty_categories_cat', 0); + $categories = Categories::getInstance('Content', $options); + $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); + + if (is_object($this->_parent)) { + $this->cache[$store] = $this->_parent->getChildren($recursive); + } else { + $this->cache[$store] = false; + } + } + + return $this->cache[$store]; + } + + /** + * Get the parent. + * + * @return object An array of data items on success, false on failure. + * + * @since 1.6 + */ + public function getParent() + { + if (!is_object($this->_parent)) { + $this->getItems(); + } + + return $this->_parent; + } } diff --git a/components/com_content/src/Model/CategoryModel.php b/components/com_content/src/Model/CategoryModel.php index ff3c8753cd5ef..f845d5526d977 100644 --- a/components/com_content/src/Model/CategoryModel.php +++ b/components/com_content/src/Model/CategoryModel.php @@ -1,4 +1,5 @@ input->getInt('id'); - - $this->setState('category.id', $pk); - - // Load the parameters. Merge Global and Menu Item params into new object - $params = $app->getParams(); - - if ($menu = $app->getMenu()->getActive()) - { - $menuParams = $menu->getParams(); - } - else - { - $menuParams = new Registry; - } - - $mergedParams = clone $menuParams; - $mergedParams->merge($params); - - $this->setState('params', $mergedParams); - $user = Factory::getUser(); - - $asset = 'com_content'; - - if ($pk) - { - $asset .= '.category.' . $pk; - } - - if ((!$user->authorise('core.edit.state', $asset)) && (!$user->authorise('core.edit', $asset))) - { - // Limit to published for people who can't edit or edit.state. - $this->setState('filter.published', 1); - } - else - { - $this->setState('filter.published', [0, 1]); - } - - // Process show_noauth parameter - if (!$params->get('show_noauth')) - { - $this->setState('filter.access', true); - } - else - { - $this->setState('filter.access', false); - } - - $itemid = $app->input->get('id', 0, 'int') . ':' . $app->input->get('Itemid', 0, 'int'); - - $value = $this->getUserStateFromRequest('com_content.category.filter.' . $itemid . '.tag', 'filter_tag', 0, 'int', false); - $this->setState('filter.tag', $value); - - // Optional filter text - $search = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter-search', 'filter-search', '', 'string'); - $this->setState('list.filter', $search); - - // Filter.order - $orderCol = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'a.ordering'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order_Dir', 'filter_order_Dir', '', 'cmd'); - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $this->setState('list.start', $app->input->get('limitstart', 0, 'uint')); - - // Set limit for query. If list, use parameter. If blog, add blog parameters for limit. - if (($app->input->get('layout') === 'blog') || $params->get('layout_type') === 'blog') - { - $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); - $this->setState('list.links', $params->get('num_links')); - } - else - { - $limit = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.limit', 'limit', $params->get('display_num'), 'uint'); - } - - $this->setState('list.limit', $limit); - - // Set the depth of the category query based on parameter - $showSubcategories = $params->get('show_subcategory_content', '0'); - - if ($showSubcategories) - { - $this->setState('filter.max_category_levels', $params->get('show_subcategory_content', '1')); - $this->setState('filter.subcategories', true); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - - $this->setState('layout', $app->input->getString('layout')); - - // Set the featured articles state - $this->setState('filter.featured', $params->get('show_featured')); - } - - /** - * Get the articles in the category - * - * @return array|bool An array of articles or false if an error occurs. - * - * @since 1.5 - */ - public function getItems() - { - $limit = $this->getState('list.limit'); - - if ($this->_articles === null && $category = $this->getCategory()) - { - $model = $this->bootComponent('com_content')->getMVCFactory() - ->createModel('Articles', 'Site', ['ignore_request' => true]); - $model->setState('params', Factory::getApplication()->getParams()); - $model->setState('filter.category_id', $category->id); - $model->setState('filter.published', $this->getState('filter.published')); - $model->setState('filter.access', $this->getState('filter.access')); - $model->setState('filter.language', $this->getState('filter.language')); - $model->setState('filter.featured', $this->getState('filter.featured')); - $model->setState('list.ordering', $this->_buildContentOrderBy()); - $model->setState('list.start', $this->getState('list.start')); - $model->setState('list.limit', $limit); - $model->setState('list.direction', $this->getState('list.direction')); - $model->setState('list.filter', $this->getState('list.filter')); - $model->setState('filter.tag', $this->getState('filter.tag')); - - // Filter.subcategories indicates whether to include articles from subcategories in the list or blog - $model->setState('filter.subcategories', $this->getState('filter.subcategories')); - $model->setState('filter.max_category_levels', $this->getState('filter.max_category_levels')); - $model->setState('list.links', $this->getState('list.links')); - - if ($limit >= 0) - { - $this->_articles = $model->getItems(); - - if ($this->_articles === false) - { - $this->setError($model->getError()); - } - } - else - { - $this->_articles = array(); - } - - $this->_pagination = $model->getPagination(); - } - - return $this->_articles; - } - - /** - * Build the orderby for the query - * - * @return string $orderby portion of query - * - * @since 1.5 - */ - protected function _buildContentOrderBy() - { - $app = Factory::getApplication(); - $db = $this->getDatabase(); - $params = $this->state->params; - $itemid = $app->input->get('id', 0, 'int') . ':' . $app->input->get('Itemid', 0, 'int'); - $orderCol = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); - $orderDirn = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order_Dir', 'filter_order_Dir', '', 'cmd'); - $orderby = ' '; - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = null; - } - - if (!in_array(strtoupper($orderDirn), array('ASC', 'DESC', ''))) - { - $orderDirn = 'ASC'; - } - - if ($orderCol && $orderDirn) - { - $orderby .= $db->escape($orderCol) . ' ' . $db->escape($orderDirn) . ', '; - } - - $articleOrderby = $params->get('orderby_sec', 'rdate'); - $articleOrderDate = $params->get('order_date'); - $categoryOrderby = $params->def('orderby_pri', ''); - $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDatabase()) . ', '; - $primary = QueryHelper::orderbyPrimary($categoryOrderby); - - $orderby .= $primary . ' ' . $secondary . ' a.created '; - - return $orderby; - } - - /** - * Method to get a JPagination object for the data set. - * - * @return \Joomla\CMS\Pagination\Pagination A JPagination object for the data set. - * - * @since 3.0.1 - */ - public function getPagination() - { - if (empty($this->_pagination)) - { - return null; - } - - return $this->_pagination; - } - - /** - * Method to get category data for the current category - * - * @return object - * - * @since 1.5 - */ - public function getCategory() - { - if (!is_object($this->_item)) - { - if (isset($this->state->params)) - { - $params = $this->state->params; - $options = array(); - $options['countItems'] = $params->get('show_cat_num_articles', 1) || !$params->get('show_empty_categories_cat', 0); - $options['access'] = $params->get('check_access_rights', 1); - } - else - { - $options['countItems'] = 0; - } - - $categories = Categories::getInstance('Content', $options); - $this->_item = $categories->get($this->getState('category.id', 'root')); - - // Compute selected asset permissions. - if (is_object($this->_item)) - { - $user = Factory::getUser(); - $asset = 'com_content.category.' . $this->_item->id; - - // Check general create permission. - if ($user->authorise('core.create', $asset)) - { - $this->_item->getParams()->set('access-create', true); - } - - // @todo: Why aren't we lazy loading the children and siblings? - $this->_children = $this->_item->getChildren(); - $this->_parent = false; - - if ($this->_item->getParent()) - { - $this->_parent = $this->_item->getParent(); - } - - $this->_rightsibling = $this->_item->getSibling(); - $this->_leftsibling = $this->_item->getSibling(false); - } - else - { - $this->_children = false; - $this->_parent = false; - } - } - - return $this->_item; - } - - /** - * Get the parent category. - * - * @return mixed An array of categories or false if an error occurs. - * - * @since 1.6 - */ - public function getParent() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_parent; - } - - /** - * Get the left sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - * - * @since 1.6 - */ - public function &getLeftSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_leftsibling; - } - - /** - * Get the right sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - * - * @since 1.6 - */ - public function &getRightSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_rightsibling; - } - - /** - * Get the child categories. - * - * @return mixed An array of categories or false if an error occurs. - * - * @since 1.6 - */ - public function &getChildren() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - // Order subcategories - if ($this->_children) - { - $params = $this->getState()->get('params'); - - $orderByPri = $params->get('orderby_pri'); - - if ($orderByPri === 'alpha' || $orderByPri === 'ralpha') - { - $this->_children = ArrayHelper::sortObjects($this->_children, 'title', ($orderByPri === 'alpha') ? 1 : (-1)); - } - } - - return $this->_children; - } - - /** - * Increment the hit counter for the category. - * - * @param int $pk Optional primary key of the category to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); - - $table = Table::getInstance('Category', 'JTable'); - $table->hit($pk); - } - - return true; - } + /** + * Category items data + * + * @var array + */ + protected $_item = null; + + /** + * Array of articles in the category + * + * @var \stdClass[] + */ + protected $_articles = null; + + /** + * Category left and right of this one + * + * @var CategoryNode[]|null + */ + protected $_siblings = null; + + /** + * Array of child-categories + * + * @var CategoryNode[]|null + */ + protected $_children = null; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + protected $_parent = null; + + /** + * Model context string. + * + * @var string + */ + protected $_context = 'com_content.category'; + + /** + * The category that applies. + * + * @var object + */ + protected $_category = null; + + /** + * The list of categories. + * + * @var array + */ + protected $_categories = null; + + /** + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'alias', 'a.alias', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'catid', 'a.catid', 'category_title', + 'state', 'a.state', + 'access', 'a.access', 'access_level', + 'created', 'a.created', + 'created_by', 'a.created_by', + 'modified', 'a.modified', + 'ordering', 'a.ordering', + 'featured', 'a.featured', + 'language', 'a.language', + 'hits', 'a.hits', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'author', 'a.author', + 'filter_tag' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering The field to order on. + * @param string $direction The direction to order on. + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $pk = $app->input->getInt('id'); + + $this->setState('category.id', $pk); + + // Load the parameters. Merge Global and Menu Item params into new object + $params = $app->getParams(); + + if ($menu = $app->getMenu()->getActive()) { + $menuParams = $menu->getParams(); + } else { + $menuParams = new Registry(); + } + + $mergedParams = clone $menuParams; + $mergedParams->merge($params); + + $this->setState('params', $mergedParams); + $user = Factory::getUser(); + + $asset = 'com_content'; + + if ($pk) { + $asset .= '.category.' . $pk; + } + + if ((!$user->authorise('core.edit.state', $asset)) && (!$user->authorise('core.edit', $asset))) { + // Limit to published for people who can't edit or edit.state. + $this->setState('filter.published', 1); + } else { + $this->setState('filter.published', [0, 1]); + } + + // Process show_noauth parameter + if (!$params->get('show_noauth')) { + $this->setState('filter.access', true); + } else { + $this->setState('filter.access', false); + } + + $itemid = $app->input->get('id', 0, 'int') . ':' . $app->input->get('Itemid', 0, 'int'); + + $value = $this->getUserStateFromRequest('com_content.category.filter.' . $itemid . '.tag', 'filter_tag', 0, 'int', false); + $this->setState('filter.tag', $value); + + // Optional filter text + $search = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter-search', 'filter-search', '', 'string'); + $this->setState('list.filter', $search); + + // Filter.order + $orderCol = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'a.ordering'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order_Dir', 'filter_order_Dir', '', 'cmd'); + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $this->setState('list.start', $app->input->get('limitstart', 0, 'uint')); + + // Set limit for query. If list, use parameter. If blog, add blog parameters for limit. + if (($app->input->get('layout') === 'blog') || $params->get('layout_type') === 'blog') { + $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); + $this->setState('list.links', $params->get('num_links')); + } else { + $limit = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.limit', 'limit', $params->get('display_num'), 'uint'); + } + + $this->setState('list.limit', $limit); + + // Set the depth of the category query based on parameter + $showSubcategories = $params->get('show_subcategory_content', '0'); + + if ($showSubcategories) { + $this->setState('filter.max_category_levels', $params->get('show_subcategory_content', '1')); + $this->setState('filter.subcategories', true); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + + $this->setState('layout', $app->input->getString('layout')); + + // Set the featured articles state + $this->setState('filter.featured', $params->get('show_featured')); + } + + /** + * Get the articles in the category + * + * @return array|bool An array of articles or false if an error occurs. + * + * @since 1.5 + */ + public function getItems() + { + $limit = $this->getState('list.limit'); + + if ($this->_articles === null && $category = $this->getCategory()) { + $model = $this->bootComponent('com_content')->getMVCFactory() + ->createModel('Articles', 'Site', ['ignore_request' => true]); + $model->setState('params', Factory::getApplication()->getParams()); + $model->setState('filter.category_id', $category->id); + $model->setState('filter.published', $this->getState('filter.published')); + $model->setState('filter.access', $this->getState('filter.access')); + $model->setState('filter.language', $this->getState('filter.language')); + $model->setState('filter.featured', $this->getState('filter.featured')); + $model->setState('list.ordering', $this->_buildContentOrderBy()); + $model->setState('list.start', $this->getState('list.start')); + $model->setState('list.limit', $limit); + $model->setState('list.direction', $this->getState('list.direction')); + $model->setState('list.filter', $this->getState('list.filter')); + $model->setState('filter.tag', $this->getState('filter.tag')); + + // Filter.subcategories indicates whether to include articles from subcategories in the list or blog + $model->setState('filter.subcategories', $this->getState('filter.subcategories')); + $model->setState('filter.max_category_levels', $this->getState('filter.max_category_levels')); + $model->setState('list.links', $this->getState('list.links')); + + if ($limit >= 0) { + $this->_articles = $model->getItems(); + + if ($this->_articles === false) { + $this->setError($model->getError()); + } + } else { + $this->_articles = array(); + } + + $this->_pagination = $model->getPagination(); + } + + return $this->_articles; + } + + /** + * Build the orderby for the query + * + * @return string $orderby portion of query + * + * @since 1.5 + */ + protected function _buildContentOrderBy() + { + $app = Factory::getApplication(); + $db = $this->getDatabase(); + $params = $this->state->params; + $itemid = $app->input->get('id', 0, 'int') . ':' . $app->input->get('Itemid', 0, 'int'); + $orderCol = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); + $orderDirn = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order_Dir', 'filter_order_Dir', '', 'cmd'); + $orderby = ' '; + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = null; + } + + if (!in_array(strtoupper($orderDirn), array('ASC', 'DESC', ''))) { + $orderDirn = 'ASC'; + } + + if ($orderCol && $orderDirn) { + $orderby .= $db->escape($orderCol) . ' ' . $db->escape($orderDirn) . ', '; + } + + $articleOrderby = $params->get('orderby_sec', 'rdate'); + $articleOrderDate = $params->get('order_date'); + $categoryOrderby = $params->def('orderby_pri', ''); + $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDatabase()) . ', '; + $primary = QueryHelper::orderbyPrimary($categoryOrderby); + + $orderby .= $primary . ' ' . $secondary . ' a.created '; + + return $orderby; + } + + /** + * Method to get a JPagination object for the data set. + * + * @return \Joomla\CMS\Pagination\Pagination A JPagination object for the data set. + * + * @since 3.0.1 + */ + public function getPagination() + { + if (empty($this->_pagination)) { + return null; + } + + return $this->_pagination; + } + + /** + * Method to get category data for the current category + * + * @return object + * + * @since 1.5 + */ + public function getCategory() + { + if (!is_object($this->_item)) { + if (isset($this->state->params)) { + $params = $this->state->params; + $options = array(); + $options['countItems'] = $params->get('show_cat_num_articles', 1) || !$params->get('show_empty_categories_cat', 0); + $options['access'] = $params->get('check_access_rights', 1); + } else { + $options['countItems'] = 0; + } + + $categories = Categories::getInstance('Content', $options); + $this->_item = $categories->get($this->getState('category.id', 'root')); + + // Compute selected asset permissions. + if (is_object($this->_item)) { + $user = Factory::getUser(); + $asset = 'com_content.category.' . $this->_item->id; + + // Check general create permission. + if ($user->authorise('core.create', $asset)) { + $this->_item->getParams()->set('access-create', true); + } + + // @todo: Why aren't we lazy loading the children and siblings? + $this->_children = $this->_item->getChildren(); + $this->_parent = false; + + if ($this->_item->getParent()) { + $this->_parent = $this->_item->getParent(); + } + + $this->_rightsibling = $this->_item->getSibling(); + $this->_leftsibling = $this->_item->getSibling(false); + } else { + $this->_children = false; + $this->_parent = false; + } + } + + return $this->_item; + } + + /** + * Get the parent category. + * + * @return mixed An array of categories or false if an error occurs. + * + * @since 1.6 + */ + public function getParent() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_parent; + } + + /** + * Get the left sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + * + * @since 1.6 + */ + public function &getLeftSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_leftsibling; + } + + /** + * Get the right sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + * + * @since 1.6 + */ + public function &getRightSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_rightsibling; + } + + /** + * Get the child categories. + * + * @return mixed An array of categories or false if an error occurs. + * + * @since 1.6 + */ + public function &getChildren() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + // Order subcategories + if ($this->_children) { + $params = $this->getState()->get('params'); + + $orderByPri = $params->get('orderby_pri'); + + if ($orderByPri === 'alpha' || $orderByPri === 'ralpha') { + $this->_children = ArrayHelper::sortObjects($this->_children, 'title', ($orderByPri === 'alpha') ? 1 : (-1)); + } + } + + return $this->_children; + } + + /** + * Increment the hit counter for the category. + * + * @param int $pk Optional primary key of the category to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); + + $table = Table::getInstance('Category', 'JTable'); + $table->hit($pk); + } + + return true; + } } diff --git a/components/com_content/src/Model/FeaturedModel.php b/components/com_content/src/Model/FeaturedModel.php index 9934bc5ce0e97..8e9b04aef81ae 100644 --- a/components/com_content/src/Model/FeaturedModel.php +++ b/components/com_content/src/Model/FeaturedModel.php @@ -1,4 +1,5 @@ input; - $user = $app->getIdentity(); - - // List state information - $limitstart = $input->getUint('limitstart', 0); - $this->setState('list.start', $limitstart); - - $params = $this->state->params; - - if ($menu = $app->getMenu()->getActive()) - { - $menuParams = $menu->getParams(); - } - else - { - $menuParams = new Registry; - } - - $mergedParams = clone $menuParams; - $mergedParams->merge($params); - - $this->setState('params', $mergedParams); - - $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); - $this->setState('list.limit', $limit); - $this->setState('list.links', $params->get('num_links')); - - $this->setState('filter.frontpage', true); - - if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) - { - // Filter on published for those who do not have edit or edit.state rights. - $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); - } - else - { - $this->setState('filter.published', [ContentComponent::CONDITION_UNPUBLISHED, ContentComponent::CONDITION_PUBLISHED]); - } - - // Process show_noauth parameter - if (!$params->get('show_noauth')) - { - $this->setState('filter.access', true); - } - else - { - $this->setState('filter.access', false); - } - - // Check for category selection - if ($params->get('featured_categories') && implode(',', $params->get('featured_categories')) == true) - { - $featuredCategories = $params->get('featured_categories'); - $this->setState('filter.frontpage.categories', $featuredCategories); - } - - $articleOrderby = $params->get('orderby_sec', 'rdate'); - $articleOrderDate = $params->get('order_date'); - $categoryOrderby = $params->def('orderby_pri', ''); - - $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDatabase()); - $primary = QueryHelper::orderbyPrimary($categoryOrderby); - - $this->setState('list.ordering', $primary . $secondary . ', a.created DESC'); - $this->setState('list.direction', ''); - } - - /** - * Method to get a list of articles. - * - * @return mixed An array of objects on success, false on failure. - */ - public function getItems() - { - $params = clone $this->getState('params'); - $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); - - if ($limit > 0) - { - $this->setState('list.limit', $limit); - - return parent::getItems(); - } - - return array(); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= $this->getState('filter.frontpage'); - - return parent::getStoreId($id); - } - - /** - * Get the list of items. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $query = parent::getListQuery(); - - // Filter by categories - $featuredCategories = $this->getState('filter.frontpage.categories'); - - if (is_array($featuredCategories) && !in_array('', $featuredCategories)) - { - $query->where('a.catid IN (' . implode(',', ArrayHelper::toInteger($featuredCategories)) . ')'); - } - - return $query; - } + /** + * Model context string. + * + * @var string + */ + public $_context = 'com_content.frontpage'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering The field to order on. + * @param string $direction The direction to order on. + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + parent::populateState($ordering, $direction); + + $app = Factory::getApplication(); + $input = $app->input; + $user = $app->getIdentity(); + + // List state information + $limitstart = $input->getUint('limitstart', 0); + $this->setState('list.start', $limitstart); + + $params = $this->state->params; + + if ($menu = $app->getMenu()->getActive()) { + $menuParams = $menu->getParams(); + } else { + $menuParams = new Registry(); + } + + $mergedParams = clone $menuParams; + $mergedParams->merge($params); + + $this->setState('params', $mergedParams); + + $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); + $this->setState('list.limit', $limit); + $this->setState('list.links', $params->get('num_links')); + + $this->setState('filter.frontpage', true); + + if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) { + // Filter on published for those who do not have edit or edit.state rights. + $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); + } else { + $this->setState('filter.published', [ContentComponent::CONDITION_UNPUBLISHED, ContentComponent::CONDITION_PUBLISHED]); + } + + // Process show_noauth parameter + if (!$params->get('show_noauth')) { + $this->setState('filter.access', true); + } else { + $this->setState('filter.access', false); + } + + // Check for category selection + if ($params->get('featured_categories') && implode(',', $params->get('featured_categories')) == true) { + $featuredCategories = $params->get('featured_categories'); + $this->setState('filter.frontpage.categories', $featuredCategories); + } + + $articleOrderby = $params->get('orderby_sec', 'rdate'); + $articleOrderDate = $params->get('order_date'); + $categoryOrderby = $params->def('orderby_pri', ''); + + $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDatabase()); + $primary = QueryHelper::orderbyPrimary($categoryOrderby); + + $this->setState('list.ordering', $primary . $secondary . ', a.created DESC'); + $this->setState('list.direction', ''); + } + + /** + * Method to get a list of articles. + * + * @return mixed An array of objects on success, false on failure. + */ + public function getItems() + { + $params = clone $this->getState('params'); + $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); + + if ($limit > 0) { + $this->setState('list.limit', $limit); + + return parent::getItems(); + } + + return array(); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= $this->getState('filter.frontpage'); + + return parent::getStoreId($id); + } + + /** + * Get the list of items. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $query = parent::getListQuery(); + + // Filter by categories + $featuredCategories = $this->getState('filter.frontpage.categories'); + + if (is_array($featuredCategories) && !in_array('', $featuredCategories)) { + $query->where('a.catid IN (' . implode(',', ArrayHelper::toInteger($featuredCategories)) . ')'); + } + + return $query; + } } diff --git a/components/com_content/src/Model/FormModel.php b/components/com_content/src/Model/FormModel.php index a5368ba1fef53..0675f99871564 100644 --- a/components/com_content/src/Model/FormModel.php +++ b/components/com_content/src/Model/FormModel.php @@ -1,4 +1,5 @@ getParams(); - $this->setState('params', $params); - - if ($params && $params->get('enable_category') == 1 && $params->get('catid')) - { - $catId = $params->get('catid'); - } - else - { - $catId = 0; - } - - // Load state from the request. - $pk = $app->input->getInt('a_id'); - $this->setState('article.id', $pk); - - $this->setState('article.catid', $app->input->getInt('catid', $catId)); - - $return = $app->input->get('return', '', 'base64'); - $this->setState('return_page', base64_decode($return)); - - $this->setState('layout', $app->input->getString('layout')); - } - - /** - * Method to get article data. - * - * @param integer $itemId The id of the article. - * - * @return mixed Content item data object on success, false on failure. - */ - public function getItem($itemId = null) - { - $itemId = (int) (!empty($itemId)) ? $itemId : $this->getState('article.id'); - - // Get a row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load($itemId); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - - $properties = $table->getProperties(1); - $value = ArrayHelper::toObject($properties, CMSObject::class); - - // Convert attrib field to Registry. - $value->params = new Registry($value->attribs); - - // Compute selected asset permissions. - $user = Factory::getUser(); - $userId = $user->get('id'); - $asset = 'com_content.article.' . $value->id; - - // Check general edit permission first. - if ($user->authorise('core.edit', $asset)) - { - $value->params->set('access-edit', true); - } - - // Now check if edit.own is available. - elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) - { - // Check for a valid user and that they are the owner. - if ($userId == $value->created_by) - { - $value->params->set('access-edit', true); - } - } - - // Check edit state permission. - if ($itemId) - { - // Existing item - $value->params->set('access-change', $user->authorise('core.edit.state', $asset)); - } - else - { - // New item. - $catId = (int) $this->getState('article.catid'); - - if ($catId) - { - $value->params->set('access-change', $user->authorise('core.edit.state', 'com_content.category.' . $catId)); - $value->catid = $catId; - } - else - { - $value->params->set('access-change', $user->authorise('core.edit.state', 'com_content')); - } - } - - $value->articletext = $value->introtext; - - if (!empty($value->fulltext)) - { - $value->articletext .= '
    ' . $value->fulltext; - } - - // Convert the metadata field to an array. - $registry = new Registry($value->metadata); - $value->metadata = $registry->toArray(); - - if ($itemId) - { - $value->tags = new TagsHelper; - $value->tags->getTagIds($value->id, 'com_content.article'); - $value->metadata['tags'] = $value->tags; - } - - return $value; - } - - /** - * Get the return URL. - * - * @return string The return URL. - * - * @since 1.6 - */ - public function getReturnPage() - { - return base64_encode($this->getState('return_page', '')); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function save($data) - { - // Associations are not edited in frontend ATM so we have to inherit them - if (Associations::isEnabled() && !empty($data['id']) - && $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $data['id'])) - { - foreach ($associations as $tag => $associated) - { - $associations[$tag] = (int) $associated->id; - } - - $data['associations'] = $associations; - } - - if (!Multilanguage::isEnabled()) - { - $data['language'] = '*'; - } - - return parent::save($data); - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = [], $loadData = true) - { - $form = parent::getForm($data, $loadData); - - if (empty($form)) - { - return false; - } - - $app = Factory::getApplication(); - $user = $app->getIdentity(); - - // On edit article, we get ID of article from article.id state, but on save, we use data from input - $id = (int) $this->getState('article.id', $app->input->getInt('a_id')); - - // Existing record. We can't edit the category in frontend if not edit.state. - if ($id > 0 && !$user->authorise('core.edit.state', 'com_content.article.' . $id)) - { - $form->setFieldAttribute('catid', 'readonly', 'true'); - $form->setFieldAttribute('catid', 'required', 'false'); - $form->setFieldAttribute('catid', 'filter', 'unset'); - } - - // Prevent messing with article language and category when editing existing article with associations - if ($this->getState('article.id') && Associations::isEnabled()) - { - $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $id); - - // Make fields read only - if (!empty($associations)) - { - $form->setFieldAttribute('language', 'readonly', 'true'); - $form->setFieldAttribute('catid', 'readonly', 'true'); - $form->setFieldAttribute('language', 'filter', 'unset'); - $form->setFieldAttribute('catid', 'filter', 'unset'); - } - } - - return $form; - } - - /** - * Allows preprocessing of the JForm object. - * - * @param Form $form The form object - * @param array $data The data to be merged into the form object - * @param string $group The plugin group to be executed - * - * @return void - * - * @since 3.7.0 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $params = $this->getState()->get('params'); - - if ($params && $params->get('enable_category') == 1 && $params->get('catid')) - { - $form->setFieldAttribute('catid', 'default', $params->get('catid')); - $form->setFieldAttribute('catid', 'readonly', 'true'); - - if (Multilanguage::isEnabled()) - { - $categoryId = (int) $params->get('catid'); - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('language')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('id') . ' = :categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - $db->setQuery($query); - - $result = $db->loadResult(); - - if ($result != '*') - { - $form->setFieldAttribute('language', 'readonly', 'true'); - $form->setFieldAttribute('language', 'default', $result); - } - } - } - - if (!Multilanguage::isEnabled()) - { - $form->setFieldAttribute('language', 'type', 'hidden'); - $form->setFieldAttribute('language', 'default', '*'); - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 4.0.0 - * @throws \Exception - */ - public function getTable($name = 'Article', $prefix = 'Administrator', $options = array()) - { - return parent::getTable($name, $prefix, $options); - } + /** + * Model typeAlias string. Used for version history. + * + * @var string + */ + public $typeAlias = 'com_content.article'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the parameters. + $params = $app->getParams(); + $this->setState('params', $params); + + if ($params && $params->get('enable_category') == 1 && $params->get('catid')) { + $catId = $params->get('catid'); + } else { + $catId = 0; + } + + // Load state from the request. + $pk = $app->input->getInt('a_id'); + $this->setState('article.id', $pk); + + $this->setState('article.catid', $app->input->getInt('catid', $catId)); + + $return = $app->input->get('return', '', 'base64'); + $this->setState('return_page', base64_decode($return)); + + $this->setState('layout', $app->input->getString('layout')); + } + + /** + * Method to get article data. + * + * @param integer $itemId The id of the article. + * + * @return mixed Content item data object on success, false on failure. + */ + public function getItem($itemId = null) + { + $itemId = (int) (!empty($itemId)) ? $itemId : $this->getState('article.id'); + + // Get a row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load($itemId); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + + $properties = $table->getProperties(1); + $value = ArrayHelper::toObject($properties, CMSObject::class); + + // Convert attrib field to Registry. + $value->params = new Registry($value->attribs); + + // Compute selected asset permissions. + $user = Factory::getUser(); + $userId = $user->get('id'); + $asset = 'com_content.article.' . $value->id; + + // Check general edit permission first. + if ($user->authorise('core.edit', $asset)) { + $value->params->set('access-edit', true); + } + + // Now check if edit.own is available. + elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) { + // Check for a valid user and that they are the owner. + if ($userId == $value->created_by) { + $value->params->set('access-edit', true); + } + } + + // Check edit state permission. + if ($itemId) { + // Existing item + $value->params->set('access-change', $user->authorise('core.edit.state', $asset)); + } else { + // New item. + $catId = (int) $this->getState('article.catid'); + + if ($catId) { + $value->params->set('access-change', $user->authorise('core.edit.state', 'com_content.category.' . $catId)); + $value->catid = $catId; + } else { + $value->params->set('access-change', $user->authorise('core.edit.state', 'com_content')); + } + } + + $value->articletext = $value->introtext; + + if (!empty($value->fulltext)) { + $value->articletext .= '
    ' . $value->fulltext; + } + + // Convert the metadata field to an array. + $registry = new Registry($value->metadata); + $value->metadata = $registry->toArray(); + + if ($itemId) { + $value->tags = new TagsHelper(); + $value->tags->getTagIds($value->id, 'com_content.article'); + $value->metadata['tags'] = $value->tags; + } + + return $value; + } + + /** + * Get the return URL. + * + * @return string The return URL. + * + * @since 1.6 + */ + public function getReturnPage() + { + return base64_encode($this->getState('return_page', '')); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function save($data) + { + // Associations are not edited in frontend ATM so we have to inherit them + if ( + Associations::isEnabled() && !empty($data['id']) + && $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $data['id']) + ) { + foreach ($associations as $tag => $associated) { + $associations[$tag] = (int) $associated->id; + } + + $data['associations'] = $associations; + } + + if (!Multilanguage::isEnabled()) { + $data['language'] = '*'; + } + + return parent::save($data); + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = [], $loadData = true) + { + $form = parent::getForm($data, $loadData); + + if (empty($form)) { + return false; + } + + $app = Factory::getApplication(); + $user = $app->getIdentity(); + + // On edit article, we get ID of article from article.id state, but on save, we use data from input + $id = (int) $this->getState('article.id', $app->input->getInt('a_id')); + + // Existing record. We can't edit the category in frontend if not edit.state. + if ($id > 0 && !$user->authorise('core.edit.state', 'com_content.article.' . $id)) { + $form->setFieldAttribute('catid', 'readonly', 'true'); + $form->setFieldAttribute('catid', 'required', 'false'); + $form->setFieldAttribute('catid', 'filter', 'unset'); + } + + // Prevent messing with article language and category when editing existing article with associations + if ($this->getState('article.id') && Associations::isEnabled()) { + $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $id); + + // Make fields read only + if (!empty($associations)) { + $form->setFieldAttribute('language', 'readonly', 'true'); + $form->setFieldAttribute('catid', 'readonly', 'true'); + $form->setFieldAttribute('language', 'filter', 'unset'); + $form->setFieldAttribute('catid', 'filter', 'unset'); + } + } + + return $form; + } + + /** + * Allows preprocessing of the JForm object. + * + * @param Form $form The form object + * @param array $data The data to be merged into the form object + * @param string $group The plugin group to be executed + * + * @return void + * + * @since 3.7.0 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $params = $this->getState()->get('params'); + + if ($params && $params->get('enable_category') == 1 && $params->get('catid')) { + $form->setFieldAttribute('catid', 'default', $params->get('catid')); + $form->setFieldAttribute('catid', 'readonly', 'true'); + + if (Multilanguage::isEnabled()) { + $categoryId = (int) $params->get('catid'); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('language')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('id') . ' = :categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + $db->setQuery($query); + + $result = $db->loadResult(); + + if ($result != '*') { + $form->setFieldAttribute('language', 'readonly', 'true'); + $form->setFieldAttribute('language', 'default', $result); + } + } + } + + if (!Multilanguage::isEnabled()) { + $form->setFieldAttribute('language', 'type', 'hidden'); + $form->setFieldAttribute('language', 'default', '*'); + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 4.0.0 + * @throws \Exception + */ + public function getTable($name = 'Article', $prefix = 'Administrator', $options = array()) + { + return parent::getTable($name, $prefix, $options); + } } diff --git a/components/com_content/src/Service/Category.php b/components/com_content/src/Service/Category.php index 106f19b56b225..ea6b2432e6b48 100644 --- a/components/com_content/src/Service/Category.php +++ b/components/com_content/src/Service/Category.php @@ -1,4 +1,5 @@ categoryFactory = $categoryFactory; - $this->db = $db; - - $params = ComponentHelper::getParams('com_content'); - $this->noIDs = (bool) $params->get('sef_ids'); - $categories = new RouterViewConfiguration('categories'); - $categories->setKey('id'); - $this->registerView($categories); - $category = new RouterViewConfiguration('category'); - $category->setKey('id')->setParent($categories, 'catid')->setNestable()->addLayout('blog'); - $this->registerView($category); - $article = new RouterViewConfiguration('article'); - $article->setKey('id')->setParent($category, 'catid'); - $this->registerView($article); - $this->registerView(new RouterViewConfiguration('archive')); - $this->registerView(new RouterViewConfiguration('featured')); - $form = new RouterViewConfiguration('form'); - $form->setKey('a_id'); - $this->registerView($form); - - parent::__construct($app, $menu); - - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategorySegment($id, $query) - { - $category = $this->getCategories(['access' => true])->get($id); - - if ($category) - { - $path = array_reverse($category->getPath(), true); - $path[0] = '1:root'; - - if ($this->noIDs) - { - foreach ($path as &$segment) - { - list($id, $segment) = explode(':', $segment, 2); - } - } - - return $path; - } - - return array(); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategoriesSegment($id, $query) - { - return $this->getCategorySegment($id, $query); - } - - /** - * Method to get the segment(s) for an article - * - * @param string $id ID of the article to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getArticleSegment($id, $query) - { - if (!strpos($id, ':')) - { - $id = (int) $id; - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('alias')) - ->from($this->db->quoteName('#__content')) - ->where($this->db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - $id .= ':' . $this->db->loadResult(); - } - - if ($this->noIDs) - { - list($void, $segment) = explode(':', $id, 2); - - return array($void => $segment); - } - - return array((int) $id => $id); - } - - /** - * Method to get the segment(s) for a form - * - * @param string $id ID of the article form to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - * - * @since 3.7.3 - */ - public function getFormSegment($id, $query) - { - return $this->getArticleSegment($id, $query); - } - - /** - * Method to get the id for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoryId($segment, $query) - { - if (isset($query['id'])) - { - $category = $this->getCategories(['access' => false])->get($query['id']); - - if ($category) - { - foreach ($category->getChildren() as $child) - { - if ($this->noIDs) - { - if ($child->alias == $segment) - { - return $child->id; - } - } - else - { - if ($child->id == (int) $segment) - { - return $child->id; - } - } - } - } - } - - return false; - } - - /** - * Method to get the segment(s) for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoriesId($segment, $query) - { - return $this->getCategoryId($segment, $query); - } - - /** - * Method to get the segment(s) for an article - * - * @param string $segment Segment of the article to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getArticleId($segment, $query) - { - if ($this->noIDs) - { - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('id')) - ->from($this->db->quoteName('#__content')) - ->where( - [ - $this->db->quoteName('alias') . ' = :alias', - $this->db->quoteName('catid') . ' = :catid', - ] - ) - ->bind(':alias', $segment) - ->bind(':catid', $query['id'], ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - return (int) $this->db->loadResult(); - } - - return (int) $segment; - } - - /** - * Method to get categories from cache - * - * @param array $options The options for retrieving categories - * - * @return CategoryInterface The object containing categories - * - * @since 4.0.0 - */ - private function getCategories(array $options = []): CategoryInterface - { - $key = serialize($options); - - if (!isset($this->categoryCache[$key])) - { - $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); - } - - return $this->categoryCache[$key]; - } + /** + * Flag to remove IDs + * + * @var boolean + */ + protected $noIDs = false; + + /** + * The category factory + * + * @var CategoryFactoryInterface + * + * @since 4.0.0 + */ + private $categoryFactory; + + /** + * The category cache + * + * @var array + * + * @since 4.0.0 + */ + private $categoryCache = []; + + /** + * The db + * + * @var DatabaseInterface + * + * @since 4.0.0 + */ + private $db; + + /** + * Content Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + * @param CategoryFactoryInterface $categoryFactory The category object + * @param DatabaseInterface $db The database object + */ + public function __construct(SiteApplication $app, AbstractMenu $menu, CategoryFactoryInterface $categoryFactory, DatabaseInterface $db) + { + $this->categoryFactory = $categoryFactory; + $this->db = $db; + + $params = ComponentHelper::getParams('com_content'); + $this->noIDs = (bool) $params->get('sef_ids'); + $categories = new RouterViewConfiguration('categories'); + $categories->setKey('id'); + $this->registerView($categories); + $category = new RouterViewConfiguration('category'); + $category->setKey('id')->setParent($categories, 'catid')->setNestable()->addLayout('blog'); + $this->registerView($category); + $article = new RouterViewConfiguration('article'); + $article->setKey('id')->setParent($category, 'catid'); + $this->registerView($article); + $this->registerView(new RouterViewConfiguration('archive')); + $this->registerView(new RouterViewConfiguration('featured')); + $form = new RouterViewConfiguration('form'); + $form->setKey('a_id'); + $this->registerView($form); + + parent::__construct($app, $menu); + + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategorySegment($id, $query) + { + $category = $this->getCategories(['access' => true])->get($id); + + if ($category) { + $path = array_reverse($category->getPath(), true); + $path[0] = '1:root'; + + if ($this->noIDs) { + foreach ($path as &$segment) { + list($id, $segment) = explode(':', $segment, 2); + } + } + + return $path; + } + + return array(); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategoriesSegment($id, $query) + { + return $this->getCategorySegment($id, $query); + } + + /** + * Method to get the segment(s) for an article + * + * @param string $id ID of the article to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getArticleSegment($id, $query) + { + if (!strpos($id, ':')) { + $id = (int) $id; + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('alias')) + ->from($this->db->quoteName('#__content')) + ->where($this->db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + $id .= ':' . $this->db->loadResult(); + } + + if ($this->noIDs) { + list($void, $segment) = explode(':', $id, 2); + + return array($void => $segment); + } + + return array((int) $id => $id); + } + + /** + * Method to get the segment(s) for a form + * + * @param string $id ID of the article form to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + * + * @since 3.7.3 + */ + public function getFormSegment($id, $query) + { + return $this->getArticleSegment($id, $query); + } + + /** + * Method to get the id for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoryId($segment, $query) + { + if (isset($query['id'])) { + $category = $this->getCategories(['access' => false])->get($query['id']); + + if ($category) { + foreach ($category->getChildren() as $child) { + if ($this->noIDs) { + if ($child->alias == $segment) { + return $child->id; + } + } else { + if ($child->id == (int) $segment) { + return $child->id; + } + } + } + } + } + + return false; + } + + /** + * Method to get the segment(s) for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoriesId($segment, $query) + { + return $this->getCategoryId($segment, $query); + } + + /** + * Method to get the segment(s) for an article + * + * @param string $segment Segment of the article to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getArticleId($segment, $query) + { + if ($this->noIDs) { + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__content')) + ->where( + [ + $this->db->quoteName('alias') . ' = :alias', + $this->db->quoteName('catid') . ' = :catid', + ] + ) + ->bind(':alias', $segment) + ->bind(':catid', $query['id'], ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + return (int) $this->db->loadResult(); + } + + return (int) $segment; + } + + /** + * Method to get categories from cache + * + * @param array $options The options for retrieving categories + * + * @return CategoryInterface The object containing categories + * + * @since 4.0.0 + */ + private function getCategories(array $options = []): CategoryInterface + { + $key = serialize($options); + + if (!isset($this->categoryCache[$key])) { + $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); + } + + return $this->categoryCache[$key]; + } } diff --git a/components/com_content/src/View/Archive/HtmlView.php b/components/com_content/src/View/Archive/HtmlView.php index ff86fccf5d37e..12836c21677d7 100644 --- a/components/com_content/src/View/Archive/HtmlView.php +++ b/components/com_content/src/View/Archive/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser(); - $state = $this->get('State'); - $items = $this->get('Items'); - $pagination = $this->get('Pagination'); - - if ($errors = $this->getModel()->getErrors()) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Flag indicates to not add limitstart=0 to URL - $pagination->hideEmptyLimitstart = true; - - // Get the page/component configuration - $params = &$state->params; - - PluginHelper::importPlugin('content'); - - foreach ($items as $item) - { - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - - // No link for ROOT category - if ($item->parent_alias === 'root') - { - $item->parent_id = null; - } - - $item->event = new \stdClass; - - // Old plugins: Ensure that text property is available - if (!isset($item->text)) - { - $item->text = $item->introtext; - } - - Factory::getApplication()->triggerEvent('onContentPrepare', array('com_content.archive', &$item, &$item->params, 0)); - - // Old plugins: Use processed text as introtext - $item->introtext = $item->text; - - $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.archive', &$item, &$item->params, 0)); - $item->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.archive', &$item, &$item->params, 0)); - $item->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.archive', &$item, &$item->params, 0)); - $item->event->afterDisplayContent = trim(implode("\n", $results)); - } - - $form = new \stdClass; - - // Month Field - $months = array( - '' => Text::_('COM_CONTENT_MONTH'), - '1' => Text::_('JANUARY_SHORT'), - '2' => Text::_('FEBRUARY_SHORT'), - '3' => Text::_('MARCH_SHORT'), - '4' => Text::_('APRIL_SHORT'), - '5' => Text::_('MAY_SHORT'), - '6' => Text::_('JUNE_SHORT'), - '7' => Text::_('JULY_SHORT'), - '8' => Text::_('AUGUST_SHORT'), - '9' => Text::_('SEPTEMBER_SHORT'), - '10' => Text::_('OCTOBER_SHORT'), - '11' => Text::_('NOVEMBER_SHORT'), - '12' => Text::_('DECEMBER_SHORT') - ); - $form->monthField = HTMLHelper::_( - 'select.genericlist', - $months, - 'month', - array( - 'list.attr' => 'class="form-select"', - 'list.select' => $state->get('filter.month'), - 'option.key' => null - ) - ); - - // Year Field - $this->years = $this->getModel()->getYears(); - $years = array(); - $years[] = HTMLHelper::_('select.option', null, Text::_('JYEAR')); - - for ($i = 0, $iMax = count($this->years); $i < $iMax; $i++) - { - $years[] = HTMLHelper::_('select.option', $this->years[$i], $this->years[$i]); - } - - $form->yearField = HTMLHelper::_( - 'select.genericlist', - $years, - 'year', - array('list.attr' => 'class="form-select"', 'list.select' => $state->get('filter.year')) - ); - $form->limitField = $pagination->getLimitBox(); - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - - $this->filter = $state->get('list.filter'); - $this->form = &$form; - $this->items = &$items; - $this->params = &$params; - $this->user = &$user; - $this->pagination = &$pagination; - $this->pagination->setAdditionalUrlParam('month', $state->get('filter.month')); - $this->pagination->setAdditionalUrlParam('year', $state->get('filter.year')); - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function _prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state = null; + + /** + * An array containing archived articles + * + * @var \stdClass[] + */ + protected $items = array(); + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination|null + */ + protected $pagination = null; + + /** + * The years that are available to filter on. + * + * @var array + * + * @since 3.6.0 + */ + protected $years = array(); + + /** + * Object containing the year, month and limit field to be displayed + * + * @var \stdClass|null + * + * @since 4.0.0 + */ + protected $form = null; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * The search query used on any archived articles (note this may not be displayed depending on the value of the + * filter_field component parameter) + * + * @var string + * + * @since 4.0.0 + */ + protected $filter = ''; + + /** + * The user object + * + * @var \Joomla\CMS\User\User + * + * @since 4.0.0 + */ + protected $user = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @throws GenericDataException + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + $state = $this->get('State'); + $items = $this->get('Items'); + $pagination = $this->get('Pagination'); + + if ($errors = $this->getModel()->getErrors()) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Flag indicates to not add limitstart=0 to URL + $pagination->hideEmptyLimitstart = true; + + // Get the page/component configuration + $params = &$state->params; + + PluginHelper::importPlugin('content'); + + foreach ($items as $item) { + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + + // No link for ROOT category + if ($item->parent_alias === 'root') { + $item->parent_id = null; + } + + $item->event = new \stdClass(); + + // Old plugins: Ensure that text property is available + if (!isset($item->text)) { + $item->text = $item->introtext; + } + + Factory::getApplication()->triggerEvent('onContentPrepare', array('com_content.archive', &$item, &$item->params, 0)); + + // Old plugins: Use processed text as introtext + $item->introtext = $item->text; + + $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.archive', &$item, &$item->params, 0)); + $item->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.archive', &$item, &$item->params, 0)); + $item->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.archive', &$item, &$item->params, 0)); + $item->event->afterDisplayContent = trim(implode("\n", $results)); + } + + $form = new \stdClass(); + + // Month Field + $months = array( + '' => Text::_('COM_CONTENT_MONTH'), + '1' => Text::_('JANUARY_SHORT'), + '2' => Text::_('FEBRUARY_SHORT'), + '3' => Text::_('MARCH_SHORT'), + '4' => Text::_('APRIL_SHORT'), + '5' => Text::_('MAY_SHORT'), + '6' => Text::_('JUNE_SHORT'), + '7' => Text::_('JULY_SHORT'), + '8' => Text::_('AUGUST_SHORT'), + '9' => Text::_('SEPTEMBER_SHORT'), + '10' => Text::_('OCTOBER_SHORT'), + '11' => Text::_('NOVEMBER_SHORT'), + '12' => Text::_('DECEMBER_SHORT') + ); + $form->monthField = HTMLHelper::_( + 'select.genericlist', + $months, + 'month', + array( + 'list.attr' => 'class="form-select"', + 'list.select' => $state->get('filter.month'), + 'option.key' => null + ) + ); + + // Year Field + $this->years = $this->getModel()->getYears(); + $years = array(); + $years[] = HTMLHelper::_('select.option', null, Text::_('JYEAR')); + + for ($i = 0, $iMax = count($this->years); $i < $iMax; $i++) { + $years[] = HTMLHelper::_('select.option', $this->years[$i], $this->years[$i]); + } + + $form->yearField = HTMLHelper::_( + 'select.genericlist', + $years, + 'year', + array('list.attr' => 'class="form-select"', 'list.select' => $state->get('filter.year')) + ); + $form->limitField = $pagination->getLimitBox(); + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + + $this->filter = $state->get('list.filter'); + $this->form = &$form; + $this->items = &$items; + $this->params = &$params; + $this->user = &$user; + $this->pagination = &$pagination; + $this->pagination->setAdditionalUrlParam('month', $state->get('filter.month')); + $this->pagination->setAdditionalUrlParam('year', $state->get('filter.year')); + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function _prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_content/src/View/Article/HtmlView.php b/components/com_content/src/View/Article/HtmlView.php index 737561243eaea..9a834735c1158 100644 --- a/components/com_content/src/View/Article/HtmlView.php +++ b/components/com_content/src/View/Article/HtmlView.php @@ -1,4 +1,5 @@ getLayout() == 'pagebreak') - { - parent::display($tpl); - - return; - } - - $app = Factory::getApplication(); - $user = $this->getCurrentUser(); - - $this->item = $this->get('Item'); - $this->print = $app->input->getBool('print', false); - $this->state = $this->get('State'); - $this->user = $user; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Create a shortcut for $item. - $item = $this->item; - $item->tagLayout = new FileLayout('joomla.content.tags'); - - // Add router helpers. - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - - // No link for ROOT category - if ($item->parent_alias === 'root') - { - $item->parent_id = null; - } - - // @todo Change based on shownoauth - $item->readmore_link = Route::_(RouteHelper::getArticleRoute($item->slug, $item->catid, $item->language)); - - // Merge article params. If this is single-article view, menu params override article params - // Otherwise, article params override menu item params - $this->params = $this->state->get('params'); - $active = $app->getMenu()->getActive(); - $temp = clone $this->params; - - // Check to see which parameters should take priority. If the active menu item link to the current article, then - // the menu item params take priority - if ($active - && $active->component == 'com_content' - && isset($active->query['view'], $active->query['id']) - && $active->query['view'] == 'article' - && $active->query['id'] == $item->id) - { - $this->menuItemMatchArticle = true; - - // Load layout from active query (in case it is an alternative menu item) - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - // Check for alternative layout of article - elseif ($layout = $item->params->get('article_layout')) - { - $this->setLayout($layout); - } - - // $item->params are the article params, $temp are the menu item params - // Merge so that the menu item params take priority - $item->params->merge($temp); - } - else - { - // The active menu item is not linked to this article, so the article params take priority here - // Merge the menu item params with the article params so that the article params take priority - $temp->merge($item->params); - $item->params = $temp; - - // Check for alternative layouts (since we are not in a single-article menu item) - // Single-article menu item layout takes priority over alt layout for an article - if ($layout = $item->params->get('article_layout')) - { - $this->setLayout($layout); - } - } - - $offset = $this->state->get('list.offset'); - - // Check the view access to the article (the model has already computed the values). - if ($item->params->get('access-view') == false && ($item->params->get('show_noauth', '0') == '0')) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return; - } - - /** - * Check for no 'access-view' and empty fulltext, - * - Redirect guest users to login - * - Deny access to logged users with 403 code - * NOTE: we do not recheck for no access-view + show_noauth disabled ... since it was checked above - */ - if ($item->params->get('access-view') == false && !strlen($item->fulltext)) - { - if ($this->user->get('guest')) - { - $return = base64_encode(Uri::getInstance()); - $login_url_with_return = Route::_('index.php?option=com_users&view=login&return=' . $return); - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'notice'); - $app->redirect($login_url_with_return, 403); - } - else - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return; - } - } - - /** - * NOTE: The following code (usually) sets the text to contain the fulltext, but it is the - * responsibility of the layout to check 'access-view' and only use "introtext" for guests - */ - if ($item->params->get('show_intro', '1') == '1') - { - $item->text = $item->introtext . ' ' . $item->fulltext; - } - elseif ($item->fulltext) - { - $item->text = $item->fulltext; - } - else - { - $item->text = $item->introtext; - } - - $item->tags = new TagsHelper; - $item->tags->getItemTags('com_content.article', $this->item->id); - - if (Associations::isEnabled() && $item->params->get('show_associations')) - { - $item->associations = AssociationHelper::displayAssociations($item->id); - } - - // Process the content plugins. - PluginHelper::importPlugin('content'); - $this->dispatchEvent(new Event('onContentPrepare', array('com_content.article', &$item, &$item->params, $offset))); - - $item->event = new \stdClass; - $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.article', &$item, &$item->params, $offset)); - $item->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.article', &$item, &$item->params, $offset)); - $item->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.article', &$item, &$item->params, $offset)); - $item->event->afterDisplayContent = trim(implode("\n", $results)); - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->item->params->get('pageclass_sfx', '')); - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - $pathway = $app->getPathway(); - - /** - * Because the application sets a default page title, - * we need to get it from the menu item itself - */ - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); - } - - $title = $this->params->get('page_title', ''); - - // If the menu item is not linked to this article - if (!$this->menuItemMatchArticle) - { - // If a browser page title is defined, use that, then fall back to the article title if set, then fall back to the page_title option - $title = $this->item->params->get('article_page_title', $this->item->title ?: $title); - - // Get ID of the category from active menu item - if ($menu && $menu->component == 'com_content' && isset($menu->query['view']) - && in_array($menu->query['view'], ['categories', 'category'])) - { - $id = $menu->query['id']; - } - else - { - $id = 0; - } - - $path = array(array('title' => $this->item->title, 'link' => '')); - $category = Categories::getInstance('Content')->get($this->item->catid); - - while ($category !== null && $category->id != $id && $category->id !== 'root') - { - $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $pathway->addItem($item['title'], $item['link']); - } - } - - if (empty($title)) - { - $title = $this->item->title; - } - - $this->setDocumentTitle($title); - - if ($this->item->metadesc) - { - $this->document->setDescription($this->item->metadesc); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - if ($app->get('MetaAuthor') == '1') - { - $author = $this->item->created_by_alias ?: $this->item->author; - $this->document->setMetaData('author', $author); - } - - $mdata = $this->item->metadata->toArray(); - - foreach ($mdata as $k => $v) - { - if ($v) - { - $this->document->setMetaData($k, $v); - } - } - - // If there is a pagebreak heading or title, add it to the page title - if (!empty($this->item->page_title)) - { - $this->item->title = $this->item->title . ' - ' . $this->item->page_title; - $this->setDocumentTitle( - $this->item->page_title . ' - ' . Text::sprintf('PLG_CONTENT_PAGEBREAK_PAGE_NUM', $this->state->get('list.offset') + 1) - ); - } - - if ($this->print) - { - $this->document->setMetaData('robots', 'noindex, nofollow'); - } - } + /** + * The article object + * + * @var \stdClass + */ + protected $item; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * Should the print button be displayed or not? + * + * @var boolean + */ + protected $print = false; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * The user object + * + * @var \Joomla\CMS\User\User|null + */ + protected $user = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The flag to mark if the active menu item is linked to the being displayed article + * + * @var boolean + */ + protected $menuItemMatchArticle = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + if ($this->getLayout() == 'pagebreak') { + parent::display($tpl); + + return; + } + + $app = Factory::getApplication(); + $user = $this->getCurrentUser(); + + $this->item = $this->get('Item'); + $this->print = $app->input->getBool('print', false); + $this->state = $this->get('State'); + $this->user = $user; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Create a shortcut for $item. + $item = $this->item; + $item->tagLayout = new FileLayout('joomla.content.tags'); + + // Add router helpers. + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + + // No link for ROOT category + if ($item->parent_alias === 'root') { + $item->parent_id = null; + } + + // @todo Change based on shownoauth + $item->readmore_link = Route::_(RouteHelper::getArticleRoute($item->slug, $item->catid, $item->language)); + + // Merge article params. If this is single-article view, menu params override article params + // Otherwise, article params override menu item params + $this->params = $this->state->get('params'); + $active = $app->getMenu()->getActive(); + $temp = clone $this->params; + + // Check to see which parameters should take priority. If the active menu item link to the current article, then + // the menu item params take priority + if ( + $active + && $active->component == 'com_content' + && isset($active->query['view'], $active->query['id']) + && $active->query['view'] == 'article' + && $active->query['id'] == $item->id + ) { + $this->menuItemMatchArticle = true; + + // Load layout from active query (in case it is an alternative menu item) + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + // Check for alternative layout of article + elseif ($layout = $item->params->get('article_layout')) { + $this->setLayout($layout); + } + + // $item->params are the article params, $temp are the menu item params + // Merge so that the menu item params take priority + $item->params->merge($temp); + } else { + // The active menu item is not linked to this article, so the article params take priority here + // Merge the menu item params with the article params so that the article params take priority + $temp->merge($item->params); + $item->params = $temp; + + // Check for alternative layouts (since we are not in a single-article menu item) + // Single-article menu item layout takes priority over alt layout for an article + if ($layout = $item->params->get('article_layout')) { + $this->setLayout($layout); + } + } + + $offset = $this->state->get('list.offset'); + + // Check the view access to the article (the model has already computed the values). + if ($item->params->get('access-view') == false && ($item->params->get('show_noauth', '0') == '0')) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return; + } + + /** + * Check for no 'access-view' and empty fulltext, + * - Redirect guest users to login + * - Deny access to logged users with 403 code + * NOTE: we do not recheck for no access-view + show_noauth disabled ... since it was checked above + */ + if ($item->params->get('access-view') == false && !strlen($item->fulltext)) { + if ($this->user->get('guest')) { + $return = base64_encode(Uri::getInstance()); + $login_url_with_return = Route::_('index.php?option=com_users&view=login&return=' . $return); + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'notice'); + $app->redirect($login_url_with_return, 403); + } else { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return; + } + } + + /** + * NOTE: The following code (usually) sets the text to contain the fulltext, but it is the + * responsibility of the layout to check 'access-view' and only use "introtext" for guests + */ + if ($item->params->get('show_intro', '1') == '1') { + $item->text = $item->introtext . ' ' . $item->fulltext; + } elseif ($item->fulltext) { + $item->text = $item->fulltext; + } else { + $item->text = $item->introtext; + } + + $item->tags = new TagsHelper(); + $item->tags->getItemTags('com_content.article', $this->item->id); + + if (Associations::isEnabled() && $item->params->get('show_associations')) { + $item->associations = AssociationHelper::displayAssociations($item->id); + } + + // Process the content plugins. + PluginHelper::importPlugin('content'); + $this->dispatchEvent(new Event('onContentPrepare', array('com_content.article', &$item, &$item->params, $offset))); + + $item->event = new \stdClass(); + $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.article', &$item, &$item->params, $offset)); + $item->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.article', &$item, &$item->params, $offset)); + $item->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.article', &$item, &$item->params, $offset)); + $item->event->afterDisplayContent = trim(implode("\n", $results)); + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->item->params->get('pageclass_sfx', '')); + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + $pathway = $app->getPathway(); + + /** + * Because the application sets a default page title, + * we need to get it from the menu item itself + */ + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); + } + + $title = $this->params->get('page_title', ''); + + // If the menu item is not linked to this article + if (!$this->menuItemMatchArticle) { + // If a browser page title is defined, use that, then fall back to the article title if set, then fall back to the page_title option + $title = $this->item->params->get('article_page_title', $this->item->title ?: $title); + + // Get ID of the category from active menu item + if ( + $menu && $menu->component == 'com_content' && isset($menu->query['view']) + && in_array($menu->query['view'], ['categories', 'category']) + ) { + $id = $menu->query['id']; + } else { + $id = 0; + } + + $path = array(array('title' => $this->item->title, 'link' => '')); + $category = Categories::getInstance('Content')->get($this->item->catid); + + while ($category !== null && $category->id != $id && $category->id !== 'root') { + $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $pathway->addItem($item['title'], $item['link']); + } + } + + if (empty($title)) { + $title = $this->item->title; + } + + $this->setDocumentTitle($title); + + if ($this->item->metadesc) { + $this->document->setDescription($this->item->metadesc); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + if ($app->get('MetaAuthor') == '1') { + $author = $this->item->created_by_alias ?: $this->item->author; + $this->document->setMetaData('author', $author); + } + + $mdata = $this->item->metadata->toArray(); + + foreach ($mdata as $k => $v) { + if ($v) { + $this->document->setMetaData($k, $v); + } + } + + // If there is a pagebreak heading or title, add it to the page title + if (!empty($this->item->page_title)) { + $this->item->title = $this->item->title . ' - ' . $this->item->page_title; + $this->setDocumentTitle( + $this->item->page_title . ' - ' . Text::sprintf('PLG_CONTENT_PAGEBREAK_PAGE_NUM', $this->state->get('list.offset') + 1) + ); + } + + if ($this->print) { + $this->document->setMetaData('robots', 'noindex, nofollow'); + } + } } diff --git a/components/com_content/src/View/Categories/HtmlView.php b/components/com_content/src/View/Categories/HtmlView.php index f16ff6d57e27e..70928399408cb 100644 --- a/components/com_content/src/View/Categories/HtmlView.php +++ b/components/com_content/src/View/Categories/HtmlView.php @@ -1,4 +1,5 @@ getParams(); - $item->description = ''; - $obj = json_decode($item->images); + /** + * Method to reconcile non-standard names from components to usage in this class. + * Typically overridden in the component feed view class. + * + * @param object $item The item for a feed, an element of the $items array. + * + * @return void + * + * @since 3.2 + */ + protected function reconcileNames($item) + { + // Get description, intro_image, author and date + $app = Factory::getApplication(); + $params = $app->getParams(); + $item->description = ''; + $obj = json_decode($item->images); - if (!empty($obj->image_intro)) - { - $item->description = '

    ' . HTMLHelper::_('image', $obj->image_intro, $obj->image_intro_alt) . '

    '; - } + if (!empty($obj->image_intro)) { + $item->description = '

    ' . HTMLHelper::_('image', $obj->image_intro, $obj->image_intro_alt) . '

    '; + } - $item->description .= ($params->get('feed_summary', 0) ? $item->introtext . $item->fulltext : $item->introtext); + $item->description .= ($params->get('feed_summary', 0) ? $item->introtext . $item->fulltext : $item->introtext); - // Add readmore link to description if introtext is shown, show_readmore is true and fulltext exists - if (!$item->params->get('feed_summary', 0) && $item->params->get('feed_show_readmore', 0) && $item->fulltext) - { - // Compute the article slug - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + // Add readmore link to description if introtext is shown, show_readmore is true and fulltext exists + if (!$item->params->get('feed_summary', 0) && $item->params->get('feed_show_readmore', 0) && $item->fulltext) { + // Compute the article slug + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - // URL link to article - $link = Route::_( - RouteHelper::getArticleRoute($item->slug, $item->catid, $item->language), - true, - $app->get('force_ssl') == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE, - true - ); + // URL link to article + $link = Route::_( + RouteHelper::getArticleRoute($item->slug, $item->catid, $item->language), + true, + $app->get('force_ssl') == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE, + true + ); - $item->description .= '

    ' - . Text::_('COM_CONTENT_FEED_READMORE') . '

    '; - } + $item->description .= '

    ' + . Text::_('COM_CONTENT_FEED_READMORE') . '

    '; + } - $item->author = $item->created_by_alias ?: $item->author; - } + $item->author = $item->created_by_alias ?: $item->author; + } } diff --git a/components/com_content/src/View/Category/HtmlView.php b/components/com_content/src/View/Category/HtmlView.php index 90e31bf226918..cac3b3eafa72d 100644 --- a/components/com_content/src/View/Category/HtmlView.php +++ b/components/com_content/src/View/Category/HtmlView.php @@ -1,4 +1,5 @@ pagination->hideEmptyLimitstart = true; - - // Prepare the data - // Get the metrics for the structural page layout. - $params = $this->params; - $numLeading = $params->def('num_leading_articles', 1); - $numIntro = $params->def('num_intro_articles', 4); - $numLinks = $params->def('num_links', 4); - $this->vote = PluginHelper::isEnabled('content', 'vote'); - - PluginHelper::importPlugin('content'); - - $app = Factory::getApplication(); - - // Compute the article slugs and prepare introtext (runs content plugins). - foreach ($this->items as $item) - { - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - - // No link for ROOT category - if ($item->parent_alias === 'root') - { - $item->parent_id = null; - } - - $item->event = new \stdClass; - - // Old plugins: Ensure that text property is available - if (!isset($item->text)) - { - $item->text = $item->introtext; - } - - $app->triggerEvent('onContentPrepare', array('com_content.category', &$item, &$item->params, 0)); - - // Old plugins: Use processed text as introtext - $item->introtext = $item->text; - - $results = $app->triggerEvent('onContentAfterTitle', array('com_content.category', &$item, &$item->params, 0)); - $item->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = $app->triggerEvent('onContentBeforeDisplay', array('com_content.category', &$item, &$item->params, 0)); - $item->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = $app->triggerEvent('onContentAfterDisplay', array('com_content.category', &$item, &$item->params, 0)); - $item->event->afterDisplayContent = trim(implode("\n", $results)); - } - - // For blog layouts, preprocess the breakdown of leading, intro and linked articles. - // This makes it much easier for the designer to just interrogate the arrays. - if ($params->get('layout_type') === 'blog' || $this->getLayout() === 'blog') - { - foreach ($this->items as $i => $item) - { - if ($i < $numLeading) - { - $this->lead_items[] = $item; - } - - elseif ($i >= $numLeading && $i < $numLeading + $numIntro) - { - $this->intro_items[] = $item; - } - - elseif ($i < $numLeading + $numIntro + $numLinks) - { - $this->link_items[] = $item; - } - } - } - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $active = $app->getMenu()->getActive(); - - if ($this->menuItemMatchCategory) - { - $this->params->def('page_heading', $this->params->get('page_title', $active->title)); - $title = $this->params->get('page_title', $active->title); - } - else - { - $this->params->def('page_heading', $this->category->title); - $title = $this->category->title; - $this->params->set('page_title', $title); - } - - if (empty($title)) - { - $title = $this->category->title; - } - - $this->setDocumentTitle($title); - - if ($this->category->metadesc) - { - $this->document->setDescription($this->category->metadesc); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - if (!is_object($this->category->metadata)) - { - $this->category->metadata = new Registry($this->category->metadata); - } - - if (($app->get('MetaAuthor') == '1') && $this->category->get('author', '')) - { - $this->document->setMetaData('author', $this->category->get('author', '')); - } - - $mdata = $this->category->metadata->toArray(); - - foreach ($mdata as $k => $v) - { - if ($v) - { - $this->document->setMetaData($k, $v); - } - } - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function prepareDocument() - { - parent::prepareDocument(); - - parent::addFeed(); - - if ($this->menuItemMatchCategory) - { - // If the active menu item is linked directly to the category being displayed, no further process is needed - return; - } - - // Get ID of the category from active menu item - $menu = $this->menu; - - if ($menu && $menu->component == 'com_content' && isset($menu->query['view']) - && in_array($menu->query['view'], ['categories', 'category'])) - { - $id = $menu->query['id']; - } - else - { - $id = 0; - } - - $path = [['title' => $this->category->title, 'link' => '']]; - $category = $this->category->getParent(); - - while ($category !== null && $category->id !== 'root' && $category->id != $id) - { - $path[] = ['title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)]; - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $this->pathway->addItem($item['title'], $item['link']); - } - } + /** + * @var array Array of leading items for blog display + * @since 3.2 + */ + protected $lead_items = array(); + + /** + * @var array Array of intro items for blog display + * @since 3.2 + */ + protected $intro_items = array(); + + /** + * @var array Array of links in blog display + * @since 3.2 + */ + protected $link_items = array(); + + /** + * @var string The name of the extension for the category + * @since 3.2 + */ + protected $extension = 'com_content'; + + /** + * @var string Default title to use for page title + * @since 3.2 + */ + protected $defaultPageTitle = 'JGLOBAL_ARTICLES'; + + /** + * @var string The name of the view to link individual items to + * @since 3.2 + */ + protected $viewName = 'article'; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + parent::commonCategoryDisplay(); + + // Flag indicates to not add limitstart=0 to URL + $this->pagination->hideEmptyLimitstart = true; + + // Prepare the data + // Get the metrics for the structural page layout. + $params = $this->params; + $numLeading = $params->def('num_leading_articles', 1); + $numIntro = $params->def('num_intro_articles', 4); + $numLinks = $params->def('num_links', 4); + $this->vote = PluginHelper::isEnabled('content', 'vote'); + + PluginHelper::importPlugin('content'); + + $app = Factory::getApplication(); + + // Compute the article slugs and prepare introtext (runs content plugins). + foreach ($this->items as $item) { + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + + // No link for ROOT category + if ($item->parent_alias === 'root') { + $item->parent_id = null; + } + + $item->event = new \stdClass(); + + // Old plugins: Ensure that text property is available + if (!isset($item->text)) { + $item->text = $item->introtext; + } + + $app->triggerEvent('onContentPrepare', array('com_content.category', &$item, &$item->params, 0)); + + // Old plugins: Use processed text as introtext + $item->introtext = $item->text; + + $results = $app->triggerEvent('onContentAfterTitle', array('com_content.category', &$item, &$item->params, 0)); + $item->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = $app->triggerEvent('onContentBeforeDisplay', array('com_content.category', &$item, &$item->params, 0)); + $item->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = $app->triggerEvent('onContentAfterDisplay', array('com_content.category', &$item, &$item->params, 0)); + $item->event->afterDisplayContent = trim(implode("\n", $results)); + } + + // For blog layouts, preprocess the breakdown of leading, intro and linked articles. + // This makes it much easier for the designer to just interrogate the arrays. + if ($params->get('layout_type') === 'blog' || $this->getLayout() === 'blog') { + foreach ($this->items as $i => $item) { + if ($i < $numLeading) { + $this->lead_items[] = $item; + } elseif ($i >= $numLeading && $i < $numLeading + $numIntro) { + $this->intro_items[] = $item; + } elseif ($i < $numLeading + $numIntro + $numLinks) { + $this->link_items[] = $item; + } + } + } + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $active = $app->getMenu()->getActive(); + + if ($this->menuItemMatchCategory) { + $this->params->def('page_heading', $this->params->get('page_title', $active->title)); + $title = $this->params->get('page_title', $active->title); + } else { + $this->params->def('page_heading', $this->category->title); + $title = $this->category->title; + $this->params->set('page_title', $title); + } + + if (empty($title)) { + $title = $this->category->title; + } + + $this->setDocumentTitle($title); + + if ($this->category->metadesc) { + $this->document->setDescription($this->category->metadesc); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + if (!is_object($this->category->metadata)) { + $this->category->metadata = new Registry($this->category->metadata); + } + + if (($app->get('MetaAuthor') == '1') && $this->category->get('author', '')) { + $this->document->setMetaData('author', $this->category->get('author', '')); + } + + $mdata = $this->category->metadata->toArray(); + + foreach ($mdata as $k => $v) { + if ($v) { + $this->document->setMetaData($k, $v); + } + } + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function prepareDocument() + { + parent::prepareDocument(); + + parent::addFeed(); + + if ($this->menuItemMatchCategory) { + // If the active menu item is linked directly to the category being displayed, no further process is needed + return; + } + + // Get ID of the category from active menu item + $menu = $this->menu; + + if ( + $menu && $menu->component == 'com_content' && isset($menu->query['view']) + && in_array($menu->query['view'], ['categories', 'category']) + ) { + $id = $menu->query['id']; + } else { + $id = 0; + } + + $path = [['title' => $this->category->title, 'link' => '']]; + $category = $this->category->getParent(); + + while ($category !== null && $category->id !== 'root' && $category->id != $id) { + $path[] = ['title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)]; + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $this->pathway->addItem($item['title'], $item['link']); + } + } } diff --git a/components/com_content/src/View/Featured/FeedView.php b/components/com_content/src/View/Featured/FeedView.php index 1a037b07c135f..86a21e7f9d418 100644 --- a/components/com_content/src/View/Featured/FeedView.php +++ b/components/com_content/src/View/Featured/FeedView.php @@ -1,4 +1,5 @@ getParams(); - $feedEmail = $app->get('feed_email', 'none'); - $siteEmail = $app->get('mailfrom'); - - $this->document->link = Route::_('index.php?option=com_content&view=featured'); - - // Get some data from the model - $app->input->set('limit', $app->get('feed_limit')); - $categories = Categories::getInstance('Content'); - $rows = $this->get('Items'); - - foreach ($rows as $row) - { - // Strip html from feed item title - $title = htmlspecialchars($row->title, ENT_QUOTES, 'UTF-8'); - $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); - - // Compute the article slug - $row->slug = $row->alias ? ($row->id . ':' . $row->alias) : $row->id; - - // URL link to article - $link = RouteHelper::getArticleRoute($row->slug, $row->catid, $row->language); - - $description = ''; - $obj = json_decode($row->images); - - if (!empty($obj->image_intro)) - { - $description = '

    ' . HTMLHelper::_('image', $obj->image_intro, $obj->image_intro_alt) . '

    '; - } - - $description .= ($params->get('feed_summary', 0) ? $row->introtext . $row->fulltext : $row->introtext); - $author = $row->created_by_alias ?: $row->author; - - // Load individual item creator class - $item = new FeedItem; - $item->title = $title; - $item->link = Route::_($link); - $item->date = $row->publish_up; - $item->category = array(); - - // All featured articles are categorized as "Featured" - $item->category[] = Text::_('JFEATURED'); - - for ($item_category = $categories->get($row->catid); $item_category !== null; $item_category = $item_category->getParent()) - { - // Only add non-root categories - if ($item_category->id > 1) - { - $item->category[] = $item_category->title; - } - } - - $item->author = $author; - - if ($feedEmail === 'site') - { - $item->authorEmail = $siteEmail; - } - elseif ($feedEmail === 'author') - { - $item->authorEmail = $row->author_email; - } - - // Add readmore link to description if introtext is shown, show_readmore is true and fulltext exists - if (!$params->get('feed_summary', 0) && $params->get('feed_show_readmore', 0) && $row->fulltext) - { - $link = Route::_($link, true, $app->get('force_ssl') == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE, true); - $description .= '

    ' - . Text::_('COM_CONTENT_FEED_READMORE') . '

    '; - } - - // Load item description and add div - $item->description = '
    ' . $description . '
    '; - - // Loads item info into rss array - $this->document->addItem($item); - } - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + */ + public function display($tpl = null) + { + // Parameters + $app = Factory::getApplication(); + $params = $app->getParams(); + $feedEmail = $app->get('feed_email', 'none'); + $siteEmail = $app->get('mailfrom'); + + $this->document->link = Route::_('index.php?option=com_content&view=featured'); + + // Get some data from the model + $app->input->set('limit', $app->get('feed_limit')); + $categories = Categories::getInstance('Content'); + $rows = $this->get('Items'); + + foreach ($rows as $row) { + // Strip html from feed item title + $title = htmlspecialchars($row->title, ENT_QUOTES, 'UTF-8'); + $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); + + // Compute the article slug + $row->slug = $row->alias ? ($row->id . ':' . $row->alias) : $row->id; + + // URL link to article + $link = RouteHelper::getArticleRoute($row->slug, $row->catid, $row->language); + + $description = ''; + $obj = json_decode($row->images); + + if (!empty($obj->image_intro)) { + $description = '

    ' . HTMLHelper::_('image', $obj->image_intro, $obj->image_intro_alt) . '

    '; + } + + $description .= ($params->get('feed_summary', 0) ? $row->introtext . $row->fulltext : $row->introtext); + $author = $row->created_by_alias ?: $row->author; + + // Load individual item creator class + $item = new FeedItem(); + $item->title = $title; + $item->link = Route::_($link); + $item->date = $row->publish_up; + $item->category = array(); + + // All featured articles are categorized as "Featured" + $item->category[] = Text::_('JFEATURED'); + + for ($item_category = $categories->get($row->catid); $item_category !== null; $item_category = $item_category->getParent()) { + // Only add non-root categories + if ($item_category->id > 1) { + $item->category[] = $item_category->title; + } + } + + $item->author = $author; + + if ($feedEmail === 'site') { + $item->authorEmail = $siteEmail; + } elseif ($feedEmail === 'author') { + $item->authorEmail = $row->author_email; + } + + // Add readmore link to description if introtext is shown, show_readmore is true and fulltext exists + if (!$params->get('feed_summary', 0) && $params->get('feed_show_readmore', 0) && $row->fulltext) { + $link = Route::_($link, true, $app->get('force_ssl') == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE, true); + $description .= '

    ' + . Text::_('COM_CONTENT_FEED_READMORE') . '

    '; + } + + // Load item description and add div + $item->description = '
    ' . $description . '
    '; + + // Loads item info into rss array + $this->document->addItem($item); + } + } } diff --git a/components/com_content/src/View/Featured/HtmlView.php b/components/com_content/src/View/Featured/HtmlView.php index e84d4d554da0a..c345e77d350ce 100644 --- a/components/com_content/src/View/Featured/HtmlView.php +++ b/components/com_content/src/View/Featured/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser(); - - $state = $this->get('State'); - $items = $this->get('Items'); - $pagination = $this->get('Pagination'); - - // Flag indicates to not add limitstart=0 to URL - $pagination->hideEmptyLimitstart = true; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - /** @var \Joomla\Registry\Registry $params */ - $params = &$state->params; - - // PREPARE THE DATA - - // Get the metrics for the structural page layout. - $numLeading = (int) $params->def('num_leading_articles', 1); - $numIntro = (int) $params->def('num_intro_articles', 4); - - PluginHelper::importPlugin('content'); - - // Compute the article slugs and prepare introtext (runs content plugins). - foreach ($items as &$item) - { - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - - // No link for ROOT category - if ($item->parent_alias === 'root') - { - $item->parent_id = null; - } - - $item->event = new \stdClass; - - // Old plugins: Ensure that text property is available - if (!isset($item->text)) - { - $item->text = $item->introtext; - } - - Factory::getApplication()->triggerEvent('onContentPrepare', array('com_content.featured', &$item, &$item->params, 0)); - - // Old plugins: Use processed text as introtext - $item->introtext = $item->text; - - $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.featured', &$item, &$item->params, 0)); - $item->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.featured', &$item, &$item->params, 0)); - $item->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.featured', &$item, &$item->params, 0)); - $item->event->afterDisplayContent = trim(implode("\n", $results)); - } - - // Preprocess the breakdown of leading, intro and linked articles. - // This makes it much easier for the designer to just integrate the arrays. - $max = count($items); - - // The first group is the leading articles. - $limit = $numLeading; - - for ($i = 0; $i < $limit && $i < $max; $i++) - { - $this->lead_items[$i] = &$items[$i]; - } - - // The second group is the intro articles. - $limit = $numLeading + $numIntro; - - // Order articles across, then down (or single column mode) - for ($i = $numLeading; $i < $limit && $i < $max; $i++) - { - $this->intro_items[$i] = &$items[$i]; - } - - // The remainder are the links. - for ($i = $numLeading + $numIntro; $i < $max; $i++) - { - $this->link_items[$i] = &$items[$i]; - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - - $this->params = &$params; - $this->items = &$items; - $this->pagination = &$pagination; - $this->user = &$user; - $this->db = Factory::getDbo(); - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - */ - protected function _prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - // Add feed links - if ($this->params->get('show_feed_link', 1)) - { - $link = '&format=feed&limitstart='; - $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); - $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); - $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); - $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); - } - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state = null; + + /** + * The featured articles array + * + * @var \stdClass[] + */ + protected $items = null; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination = null; + + /** + * The featured articles to be displayed as lead items. + * + * @var \stdClass[] + */ + protected $lead_items = array(); + + /** + * The featured articles to be displayed as intro items. + * + * @var \stdClass[] + */ + protected $intro_items = array(); + + /** + * The featured articles to be displayed as link items. + * + * @var \stdClass[] + */ + protected $link_items = array(); + + /** + * @var \Joomla\Database\DatabaseDriver + * + * @since 3.6.3 + * + * @deprecated 5.0 Will be removed without replacement + */ + protected $db; + + /** + * The user object + * + * @var \Joomla\CMS\User\User|null + */ + protected $user = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + + $state = $this->get('State'); + $items = $this->get('Items'); + $pagination = $this->get('Pagination'); + + // Flag indicates to not add limitstart=0 to URL + $pagination->hideEmptyLimitstart = true; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + /** @var \Joomla\Registry\Registry $params */ + $params = &$state->params; + + // PREPARE THE DATA + + // Get the metrics for the structural page layout. + $numLeading = (int) $params->def('num_leading_articles', 1); + $numIntro = (int) $params->def('num_intro_articles', 4); + + PluginHelper::importPlugin('content'); + + // Compute the article slugs and prepare introtext (runs content plugins). + foreach ($items as &$item) { + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + + // No link for ROOT category + if ($item->parent_alias === 'root') { + $item->parent_id = null; + } + + $item->event = new \stdClass(); + + // Old plugins: Ensure that text property is available + if (!isset($item->text)) { + $item->text = $item->introtext; + } + + Factory::getApplication()->triggerEvent('onContentPrepare', array('com_content.featured', &$item, &$item->params, 0)); + + // Old plugins: Use processed text as introtext + $item->introtext = $item->text; + + $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.featured', &$item, &$item->params, 0)); + $item->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.featured', &$item, &$item->params, 0)); + $item->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.featured', &$item, &$item->params, 0)); + $item->event->afterDisplayContent = trim(implode("\n", $results)); + } + + // Preprocess the breakdown of leading, intro and linked articles. + // This makes it much easier for the designer to just integrate the arrays. + $max = count($items); + + // The first group is the leading articles. + $limit = $numLeading; + + for ($i = 0; $i < $limit && $i < $max; $i++) { + $this->lead_items[$i] = &$items[$i]; + } + + // The second group is the intro articles. + $limit = $numLeading + $numIntro; + + // Order articles across, then down (or single column mode) + for ($i = $numLeading; $i < $limit && $i < $max; $i++) { + $this->intro_items[$i] = &$items[$i]; + } + + // The remainder are the links. + for ($i = $numLeading + $numIntro; $i < $max; $i++) { + $this->link_items[$i] = &$items[$i]; + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + + $this->params = &$params; + $this->items = &$items; + $this->pagination = &$pagination; + $this->user = &$user; + $this->db = Factory::getDbo(); + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + */ + protected function _prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + // Add feed links + if ($this->params->get('show_feed_link', 1)) { + $link = '&format=feed&limitstart='; + $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); + $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); + $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); + $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); + } + } } diff --git a/components/com_content/src/View/Form/HtmlView.php b/components/com_content/src/View/Form/HtmlView.php index eb685043ea54d..e64a10189a568 100644 --- a/components/com_content/src/View/Form/HtmlView.php +++ b/components/com_content/src/View/Form/HtmlView.php @@ -1,4 +1,5 @@ getIdentity(); - - // Get model data. - $this->state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - $this->return_page = $this->get('ReturnPage'); - - if (empty($this->item->id)) - { - $catid = $this->state->params->get('catid'); - - if ($this->state->params->get('enable_category') == 1 && $catid) - { - $authorised = $user->authorise('core.create', 'com_content.category.' . $catid); - } - else - { - $authorised = $user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')); - } - } - else - { - $authorised = $this->item->params->get('access-edit'); - } - - if ($authorised !== true) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return false; - } - - $this->item->tags = new TagsHelper; - - if (!empty($this->item->id)) - { - $this->item->tags->getItemTags('com_content.article', $this->item->id); - - $this->item->images = json_decode($this->item->images); - $this->item->urls = json_decode($this->item->urls); - - $tmp = new \stdClass; - $tmp->images = $this->item->images; - $tmp->urls = $this->item->urls; - $this->form->bind($tmp); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Create a shortcut to the parameters. - $params = &$this->state->params; - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - - $this->params = $params; - - // Override global params with article specific params - $this->params->merge($this->item->params); - $this->user = $user; - - // Propose current language as default when creating new article - if (empty($this->item->id) && Multilanguage::isEnabled() && $params->get('enable_category') != 1) - { - $lang = Factory::getLanguage()->getTag(); - $this->form->setFieldAttribute('language', 'default', $lang); - } - - $captchaSet = $params->get('captcha', Factory::getApplication()->get('captcha', '0')); - - foreach (PluginHelper::getPlugin('captcha') as $plugin) - { - if ($captchaSet === $plugin->name) - { - $this->captchaEnabled = true; - break; - } - } - - // If the article is being edited and the current user has permission to create article - if ($this->item->id - && ($user->authorise('core.create', 'com_content') || \count($user->getAuthorisedCategories('com_content', 'core.create')))) - { - $this->showSaveAsCopy = true; - } - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_CONTENT_FORM_EDIT_ARTICLE')); - } - - $title = $this->params->def('page_title', Text::_('COM_CONTENT_FORM_EDIT_ARTICLE')); - - $this->setDocumentTitle($title); - - $app->getPathway()->addItem($title); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The item being created + * + * @var \stdClass + */ + protected $item; + + /** + * The page to return to after the article is submitted + * + * @var string + */ + protected $return_page = ''; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The user object + * + * @var \Joomla\CMS\User\User + * + * @since 4.0.0 + */ + protected $user = null; + + /** + * Should we show a captcha form for the submission of the article? + * + * @var boolean + * + * @since 3.7.0 + */ + protected $captchaEnabled = false; + + /** + * Should we show Save As Copy button? + * + * @var boolean + * @since 4.1.0 + */ + protected $showSaveAsCopy = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void|boolean + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $user = $app->getIdentity(); + + // Get model data. + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + $this->return_page = $this->get('ReturnPage'); + + if (empty($this->item->id)) { + $catid = $this->state->params->get('catid'); + + if ($this->state->params->get('enable_category') == 1 && $catid) { + $authorised = $user->authorise('core.create', 'com_content.category.' . $catid); + } else { + $authorised = $user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')); + } + } else { + $authorised = $this->item->params->get('access-edit'); + } + + if ($authorised !== true) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return false; + } + + $this->item->tags = new TagsHelper(); + + if (!empty($this->item->id)) { + $this->item->tags->getItemTags('com_content.article', $this->item->id); + + $this->item->images = json_decode($this->item->images); + $this->item->urls = json_decode($this->item->urls); + + $tmp = new \stdClass(); + $tmp->images = $this->item->images; + $tmp->urls = $this->item->urls; + $this->form->bind($tmp); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Create a shortcut to the parameters. + $params = &$this->state->params; + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + + $this->params = $params; + + // Override global params with article specific params + $this->params->merge($this->item->params); + $this->user = $user; + + // Propose current language as default when creating new article + if (empty($this->item->id) && Multilanguage::isEnabled() && $params->get('enable_category') != 1) { + $lang = Factory::getLanguage()->getTag(); + $this->form->setFieldAttribute('language', 'default', $lang); + } + + $captchaSet = $params->get('captcha', Factory::getApplication()->get('captcha', '0')); + + foreach (PluginHelper::getPlugin('captcha') as $plugin) { + if ($captchaSet === $plugin->name) { + $this->captchaEnabled = true; + break; + } + } + + // If the article is being edited and the current user has permission to create article + if ( + $this->item->id + && ($user->authorise('core.create', 'com_content') || \count($user->getAuthorisedCategories('com_content', 'core.create'))) + ) { + $this->showSaveAsCopy = true; + } + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_CONTENT_FORM_EDIT_ARTICLE')); + } + + $title = $this->params->def('page_title', Text::_('COM_CONTENT_FORM_EDIT_ARTICLE')); + + $this->setDocumentTitle($title); + + $app->getPathway()->addItem($title); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_content/tmpl/archive/default.php b/components/com_content/tmpl/archive/default.php index 95d7a2f98b094..e749e93a4554b 100644 --- a/components/com_content/tmpl/archive/default.php +++ b/components/com_content/tmpl/archive/default.php @@ -1,4 +1,5 @@
    params->get('show_page_heading')) : ?> - +
    -
    - +
    loadTemplate('items'); ?>
    diff --git a/components/com_content/tmpl/archive/default_items.php b/components/com_content/tmpl/archive/default_items.php index 5e83c2b56262d..e080d636878a7 100644 --- a/components/com_content/tmpl/archive/default_items.php +++ b/components/com_content/tmpl/archive/default_items.php @@ -1,4 +1,5 @@ params; ?>
    - items as $i => $item) : ?> - params->get('info_block_position', 0); ?> -
    - + + + event->afterDisplayContent; ?> +
    +
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - -
    - pagination->getPagesLinks(); ?> -
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + +
    + pagination->getPagesLinks(); ?> +
    diff --git a/components/com_content/tmpl/article/default.php b/components/com_content/tmpl/article/default.php index 160dae0ef1ada..41e4d6394caf0 100644 --- a/components/com_content/tmpl/article/default.php +++ b/components/com_content/tmpl/article/default.php @@ -1,4 +1,5 @@ item->publish_down) && $this->item->publish_down < $currentDate; ?>
    - - params->get('show_page_heading')) : ?> - - item->pagination) && !$this->item->paginationposition && $this->item->paginationrelative) - { - echo $this->item->pagination; - } - ?> + + params->get('show_page_heading')) : ?> + + item->pagination) && !$this->item->paginationposition && $this->item->paginationrelative) { + echo $this->item->pagination; + } + ?> - get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') - || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam; ?> + get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') + || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam; ?> - get('show_title')) : ?> - - - - $params, 'item' => $this->item)); ?> - + get('show_title')) : ?> + + + + $params, 'item' => $this->item)); ?> + - - item->event->afterDisplayTitle; ?> + + item->event->afterDisplayTitle; ?> - - $this->item, 'params' => $params, 'position' => 'above')); ?> - + + $this->item, 'params' => $params, 'position' => 'above')); ?> + - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tagLayout = new FileLayout('joomla.content.tags'); ?> + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tagLayout = new FileLayout('joomla.content.tags'); ?> - item->tagLayout->render($this->item->tags->itemTags); ?> - + item->tagLayout->render($this->item->tags->itemTags); ?> + - - item->event->beforeDisplayContent; ?> + + item->event->beforeDisplayContent; ?> - get('urls_position', 0) === 0) : ?> - loadTemplate('links'); ?> - - get('access-view')) : ?> - item); ?> - item->pagination) && !$this->item->paginationposition && !$this->item->paginationrelative) : - echo $this->item->pagination; - endif; - ?> - item->toc)) : - echo $this->item->toc; - endif; ?> -
    - item->text; ?> -
    + get('urls_position', 0) === 0) : ?> + loadTemplate('links'); ?> + + get('access-view')) : ?> + item); ?> + item->pagination) && !$this->item->paginationposition && !$this->item->paginationrelative) : + echo $this->item->pagination; + endif; + ?> + item->toc)) : + echo $this->item->toc; + endif; ?> +
    + item->text; ?> +
    - - - $this->item, 'params' => $params, 'position' => 'below')); ?> - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tagLayout = new FileLayout('joomla.content.tags'); ?> - item->tagLayout->render($this->item->tags->itemTags); ?> - - + + + $this->item, 'params' => $params, 'position' => 'below')); ?> + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tagLayout = new FileLayout('joomla.content.tags'); ?> + item->tagLayout->render($this->item->tags->itemTags); ?> + + - item->pagination) && $this->item->paginationposition && !$this->item->paginationrelative) : - echo $this->item->pagination; - ?> - - get('urls_position', 0) === 1) : ?> - loadTemplate('links'); ?> - - - get('show_noauth') == true && $user->get('guest')) : ?> - item); ?> - item->introtext); ?> - - get('show_readmore') && $this->item->fulltext != null) : ?> - getMenu(); ?> - getActive(); ?> - id; ?> - - setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); ?> - $this->item, 'params' => $params, 'link' => $link)); ?> - - - item->pagination) && $this->item->paginationposition && $this->item->paginationrelative) : - echo $this->item->pagination; - ?> - - - item->event->afterDisplayContent; ?> + item->pagination) && $this->item->paginationposition && !$this->item->paginationrelative) : + echo $this->item->pagination; + ?> + + get('urls_position', 0) === 1) : ?> + loadTemplate('links'); ?> + + + get('show_noauth') == true && $user->get('guest')) : ?> + item); ?> + item->introtext); ?> + + get('show_readmore') && $this->item->fulltext != null) : ?> + getMenu(); ?> + getActive(); ?> + id; ?> + + setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); ?> + $this->item, 'params' => $params, 'link' => $link)); ?> + + + item->pagination) && $this->item->paginationposition && $this->item->paginationrelative) : + echo $this->item->pagination; + ?> + + + item->event->afterDisplayContent; ?>
    diff --git a/components/com_content/tmpl/article/default_links.php b/components/com_content/tmpl/article/default_links.php index 51e25ed33cca2..a8e4c427406b9 100644 --- a/components/com_content/tmpl/article/default_links.php +++ b/components/com_content/tmpl/article/default_links.php @@ -1,4 +1,5 @@ item->params; if ($urls && (!empty($urls->urla) || !empty($urls->urlb) || !empty($urls->urlc))) : -?> + ?>
    -
      - urla, $urls->urlatext, $urls->targeta, 'a'), - array($urls->urlb, $urls->urlbtext, $urls->targetb, 'b'), - array($urls->urlc, $urls->urlctext, $urls->targetc, 'c') - ); - foreach ($urlarray as $url) : - $link = $url[0]; - $label = $url[1]; - $target = $url[2]; - $id = $url[3]; +
        + urla, $urls->urlatext, $urls->targeta, 'a'), + array($urls->urlb, $urls->urlbtext, $urls->targetb, 'b'), + array($urls->urlc, $urls->urlctext, $urls->targetc, 'c') + ); + foreach ($urlarray as $url) : + $link = $url[0]; + $label = $url[1]; + $target = $url[2]; + $id = $url[3]; - if ( ! $link) : - continue; - endif; + if (! $link) : + continue; + endif; - // If no label is present, take the link - $label = $label ?: $link; + // If no label is present, take the link + $label = $label ?: $link; - // If no target is present, use the default - $target = $target ?: $params->get('target' . $id); - ?> -
      • - get('target' . $id); + ?> +
      • + ' . - htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ''; - break; + switch ($target) { + case 1: + // Open in a new window + echo '' . + htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ''; + break; - case 2: - // Open in a popup window - $attribs = 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=600,height=600'; - echo "" . - htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ''; - break; - case 3: - echo '' . - htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ' '; - echo HTMLHelper::_( - 'bootstrap.renderModal', - 'linkModal', - array( - 'url' => $link, - 'title' => $label, - 'height' => '100%', - 'width' => '100%', - 'modalWidth' => '500', - 'bodyHeight' => '500', - 'footer' => '' - ) - ); - break; + case 2: + // Open in a popup window + $attribs = 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=600,height=600'; + echo "" . + htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ''; + break; + case 3: + echo '' . + htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ' '; + echo HTMLHelper::_( + 'bootstrap.renderModal', + 'linkModal', + array( + 'url' => $link, + 'title' => $label, + 'height' => '100%', + 'width' => '100%', + 'modalWidth' => '500', + 'bodyHeight' => '500', + 'footer' => '' + ) + ); + break; - default: - // Open in parent window - echo '' . - htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ' '; - break; - } - ?> -
      • - -
      + default: + // Open in parent window + echo '' . + htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ' '; + break; + } + ?> + + +
    diff --git a/components/com_content/tmpl/categories/default.php b/components/com_content/tmpl/categories/default.php index 19a6961ac357e..a1bd6b924c19b 100644 --- a/components/com_content/tmpl/categories/default.php +++ b/components/com_content/tmpl/categories/default.php @@ -1,4 +1,5 @@
    - loadTemplate('items'); - ?> + loadTemplate('items'); + ?>
    diff --git a/components/com_content/tmpl/categories/default_items.php b/components/com_content/tmpl/categories/default_items.php index a591a4a8e4fb8..2ed17e9fbaa03 100644 --- a/components/com_content/tmpl/categories/default_items.php +++ b/components/com_content/tmpl/categories/default_items.php @@ -1,4 +1,5 @@ maxLevelcat != 0 && count($this->items[$this->parent->id]) > 0) : -?> -
    - items[$this->parent->id] as $id => $item) : ?> - params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> -
    -
    -
    - - escape($item->title); ?> - params->get('show_cat_num_articles_cat') == 1) :?> - -   - numitems; ?> - - -
    - getChildren()) > 0 && $this->maxLevelcat > 1) : ?> - - -
    - params->get('show_description_image') && $item->getParams()->get('image')) : ?> - getParams()->get('image'), $item->getParams()->get('image_alt')); ?> - - params->get('show_subcat_desc_cat') == 1) : ?> - description) : ?> -
    - description, '', 'com_content.categories'); ?> -
    - - + ?> +
    + items[$this->parent->id] as $id => $item) : ?> + params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> +
    +
    +
    + + escape($item->title); ?> + params->get('show_cat_num_articles_cat') == 1) :?> + +   + numitems; ?> + + +
    + getChildren()) > 0 && $this->maxLevelcat > 1) : ?> + + +
    + params->get('show_description_image') && $item->getParams()->get('image')) : ?> + getParams()->get('image'), $item->getParams()->get('image_alt')); ?> + + params->get('show_subcat_desc_cat') == 1) : ?> + description) : ?> +
    + description, '', 'com_content.categories'); ?> +
    + + - getChildren()) > 0 && $this->maxLevelcat > 1) : ?> - - -
    - - -
    + getChildren()) > 0 && $this->maxLevelcat > 1) : ?> + + +
    + + +
    diff --git a/components/com_content/tmpl/category/blog.php b/components/com_content/tmpl/category/blog.php index cd6d656572c82..d25185dd63ddf 100644 --- a/components/com_content/tmpl/category/blog.php +++ b/components/com_content/tmpl/category/blog.php @@ -1,4 +1,5 @@ diff --git a/components/com_content/tmpl/category/blog_children.php b/components/com_content/tmpl/category/blog_children.php index 7f4c921b13df8..6757330021bb8 100644 --- a/components/com_content/tmpl/category/blog_children.php +++ b/components/com_content/tmpl/category/blog_children.php @@ -1,4 +1,5 @@ getAuthorisedViewLevels(); if ($this->maxLevel != 0 && count($this->children[$this->category->id]) > 0) : ?> + children[$this->category->id] as $id => $child) : ?> + + access, $groups)) : ?> + params->get('show_empty_categories') || $child->numitems || count($child->getChildren())) : ?> + - - - + maxLevel > 1 && count($child->getChildren()) > 0) : ?> + + + + + + format('Y-m-d H:i:s'); $isUnpublished = ($this->item->state == ContentComponent::CONDITION_UNPUBLISHED || $this->item->publish_up > $currentDate) - || ($this->item->publish_down < $currentDate && $this->item->publish_down !== null); + || ($this->item->publish_down < $currentDate && $this->item->publish_down !== null); ?> item); ?>
    - -
    - - - item); ?> - - - $params, 'item' => $this->item)); ?> - - - - get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') - || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam); ?> - - - $this->item, 'params' => $params, 'position' => 'above')); ?> - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tags->itemTags); ?> - - - get('show_intro')) : ?> - - item->event->afterDisplayTitle; ?> - - - - item->event->beforeDisplayContent; ?> - - item->introtext; ?> - - - - $this->item, 'params' => $params, 'position' => 'below')); ?> - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tags->itemTags); ?> - - - - get('show_readmore') && $this->item->readmore) : - if ($params->get('access-view')) : - $link = Route::_(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language)); - else : - $menu = Factory::getApplication()->getMenu(); - $active = $menu->getActive(); - $itemId = $active->id; - $link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false)); - $link->setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); - endif; ?> - - $this->item, 'params' => $params, 'link' => $link)); ?> - - - - -
    - - - - item->event->afterDisplayContent; ?> + +
    + + + item); ?> + + + $params, 'item' => $this->item)); ?> + + + + get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') + || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam); ?> + + + $this->item, 'params' => $params, 'position' => 'above')); ?> + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tags->itemTags); ?> + + + get('show_intro')) : ?> + + item->event->afterDisplayTitle; ?> + + + + item->event->beforeDisplayContent; ?> + + item->introtext; ?> + + + + $this->item, 'params' => $params, 'position' => 'below')); ?> + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tags->itemTags); ?> + + + + get('show_readmore') && $this->item->readmore) : + if ($params->get('access-view')) : + $link = Route::_(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language)); + else : + $menu = Factory::getApplication()->getMenu(); + $active = $menu->getActive(); + $itemId = $active->id; + $link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false)); + $link->setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); + endif; ?> + + $this->item, 'params' => $params, 'link' => $link)); ?> + + + + +
    + + + + item->event->afterDisplayContent; ?>
    diff --git a/components/com_content/tmpl/category/blog_links.php b/components/com_content/tmpl/category/blog_links.php index 0027a357a6f1b..83250ff6b89c8 100644 --- a/components/com_content/tmpl/category/blog_links.php +++ b/components/com_content/tmpl/category/blog_links.php @@ -1,4 +1,5 @@ diff --git a/components/com_content/tmpl/category/default.php b/components/com_content/tmpl/category/default.php index ab946a6c69bf2..d2e30dc61c26c 100644 --- a/components/com_content/tmpl/category/default.php +++ b/components/com_content/tmpl/category/default.php @@ -1,4 +1,5 @@ params->get('filter_field') === 'tag') && (Multilanguage::isEnabled())) -{ - $tagfilter = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter'); +if (($this->params->get('filter_field') === 'tag') && (Multilanguage::isEnabled())) { + $tagfilter = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter'); - switch ($tagfilter) - { - case 'current_language': - $langFilter = Factory::getApplication()->getLanguage()->getTag(); - break; + switch ($tagfilter) { + case 'current_language': + $langFilter = Factory::getApplication()->getLanguage()->getTag(); + break; - case 'all': - $langFilter = false; - break; + case 'all': + $langFilter = false; + break; - default: - $langFilter = $tagfilter; - } + default: + $langFilter = $tagfilter; + } } // Check for at least one editable article $isEditable = false; -if (!empty($this->items)) -{ - foreach ($this->items as $article) - { - if ($article->params->get('access-edit')) - { - $isEditable = true; - break; - } - } +if (!empty($this->items)) { + foreach ($this->items as $article) { + if ($article->params->get('access-edit')) { + $isEditable = true; + break; + } + } } $currentDate = Factory::getDate()->format('Y-m-d H:i:s'); ?> diff --git a/components/com_content/tmpl/category/default_children.php b/components/com_content/tmpl/category/default_children.php index bb8b5334f1ce3..21d741a432e5d 100644 --- a/components/com_content/tmpl/category/default_children.php +++ b/components/com_content/tmpl/category/default_children.php @@ -1,4 +1,5 @@ children[$this->category->id]) > 0) : ?> - children[$this->category->id] as $id => $child) : ?> - - access, $groups)) : ?> - params->get('show_empty_categories') || $child->getNumItems(true) || count($child->getChildren())) : ?> - - - - + + + + diff --git a/components/com_content/tmpl/featured/default.php b/components/com_content/tmpl/featured/default.php index fd46e43cd6258..04bc1bdd8ef14 100644 --- a/components/com_content/tmpl/featured/default.php +++ b/components/com_content/tmpl/featured/default.php @@ -1,4 +1,5 @@ diff --git a/components/com_content/tmpl/featured/default_item.php b/components/com_content/tmpl/featured/default_item.php index 250764ebd9204..1d43493d89273 100644 --- a/components/com_content/tmpl/featured/default_item.php +++ b/components/com_content/tmpl/featured/default_item.php @@ -1,4 +1,5 @@ item); ?>
    - -
    - - - get('show_title')) : ?> -

    - get('link_titles') && $params->get('access-view')) : ?> - - - escape($this->item->title); ?> - -

    - - - item->state == ContentComponent::CONDITION_UNPUBLISHED) : ?> - - - - - - - - - - - $params, 'item' => $this->item)); ?> - - - - item->event->afterDisplayTitle; ?> - - - get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') - || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam); ?> - - - $this->item, 'params' => $params, 'position' => 'above')); ?> - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tags->itemTags); ?> - - - - item->event->beforeDisplayContent; ?> - - item->introtext; ?> - - - - $this->item, 'params' => $params, 'position' => 'below')); ?> - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tags->itemTags); ?> - - - - get('show_readmore') && $this->item->readmore) : - if ($params->get('access-view')) : - $link = Route::_(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language)); - else : - $menu = Factory::getApplication()->getMenu(); - $active = $menu->getActive(); - $itemId = $active->id; - $link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false)); - $link->setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); - endif; ?> - - $this->item, 'params' => $params, 'link' => $link)); ?> - - - - -
    - + +
    + + + get('show_title')) : ?> +

    + get('link_titles') && $params->get('access-view')) : ?> + + + escape($this->item->title); ?> + +

    + + + item->state == ContentComponent::CONDITION_UNPUBLISHED) : ?> + + + + + + + + + + + $params, 'item' => $this->item)); ?> + + + + item->event->afterDisplayTitle; ?> + + + get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') + || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam); ?> + + + $this->item, 'params' => $params, 'position' => 'above')); ?> + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tags->itemTags); ?> + + + + item->event->beforeDisplayContent; ?> + + item->introtext; ?> + + + + $this->item, 'params' => $params, 'position' => 'below')); ?> + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tags->itemTags); ?> + + + + get('show_readmore') && $this->item->readmore) : + if ($params->get('access-view')) : + $link = Route::_(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language)); + else : + $menu = Factory::getApplication()->getMenu(); + $active = $menu->getActive(); + $itemId = $active->id; + $link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false)); + $link->setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); + endif; ?> + + $this->item, 'params' => $params, 'link' => $link)); ?> + + + + +
    +
    diff --git a/components/com_content/tmpl/featured/default_links.php b/components/com_content/tmpl/featured/default_links.php index 3f87a6826ace4..083f2b9f89afc 100644 --- a/components/com_content/tmpl/featured/default_links.php +++ b/components/com_content/tmpl/featured/default_links.php @@ -1,4 +1,5 @@ diff --git a/components/com_content/tmpl/form/edit.php b/components/com_content/tmpl/form/edit.php index 1437c88d14546..7477afd63e7a6 100644 --- a/components/com_content/tmpl/form/edit.php +++ b/components/com_content/tmpl/form/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_content.form-edit'); + ->useScript('form.validate') + ->useScript('com_content.form-edit'); $this->tab_name = 'com-content-form'; $this->ignore_fieldsets = array('image-intro', 'image-full', 'jmetadata', 'item_associations'); @@ -31,153 +32,152 @@ // This checks if the editor config options have ever been saved. If they haven't they will fall back to the original settings. $editoroptions = isset($params->show_publishing_options); -if (!$editoroptions) -{ - $params->show_urls_images_frontend = '0'; +if (!$editoroptions) { + $params->show_urls_images_frontend = '0'; } ?>
    - get('show_page_heading')) : ?> - - - -
    -
    - tab_name, ['active' => 'editor', 'recall' => true, 'breakpoint' => 768]); ?> - - tab_name, 'editor', Text::_('COM_CONTENT_ARTICLE_CONTENT')); ?> - form->renderField('title'); ?> - - item->id)) : ?> - form->renderField('alias'); ?> - - - form->renderField('articletext'); ?> - - captchaEnabled) : ?> - form->renderField('captcha'); ?> - - - - get('show_urls_images_frontend')) : ?> - tab_name, 'images', Text::_('COM_CONTENT_IMAGES_AND_URLS')); ?> - form->renderField('image_intro', 'images'); ?> - form->renderField('image_intro_alt', 'images'); ?> - form->renderField('image_intro_alt_empty', 'images'); ?> - form->renderField('image_intro_caption', 'images'); ?> - form->renderField('float_intro', 'images'); ?> - form->renderField('image_fulltext', 'images'); ?> - form->renderField('image_fulltext_alt', 'images'); ?> - form->renderField('image_fulltext_alt_empty', 'images'); ?> - form->renderField('image_fulltext_caption', 'images'); ?> - form->renderField('float_fulltext', 'images'); ?> - form->renderField('urla', 'urls'); ?> - form->renderField('urlatext', 'urls'); ?> -
    -
    - form->getInput('targeta', 'urls'); ?> -
    -
    - form->renderField('urlb', 'urls'); ?> - form->renderField('urlbtext', 'urls'); ?> -
    -
    - form->getInput('targetb', 'urls'); ?> -
    -
    - form->renderField('urlc', 'urls'); ?> - form->renderField('urlctext', 'urls'); ?> -
    -
    - form->getInput('targetc', 'urls'); ?> -
    -
    - - - - - - tab_name, 'publishing', Text::_('COM_CONTENT_PUBLISHING')); ?> - - form->renderField('transition'); ?> - form->renderField('state'); ?> - form->renderField('catid'); ?> - form->renderField('tags'); ?> - form->renderField('note'); ?> - get('save_history', 0)) : ?> - form->renderField('version_note'); ?> - - get('show_publishing_options', 1) == 1) : ?> - form->renderField('created_by_alias'); ?> - - item->params->get('access-change')) : ?> - form->renderField('featured'); ?> - get('show_publishing_options', 1) == 1) : ?> - form->renderField('featured_up'); ?> - form->renderField('featured_down'); ?> - form->renderField('publish_up'); ?> - form->renderField('publish_down'); ?> - - - form->renderField('access'); ?> - item->id)) : ?> -
    -
    -
    -
    - -
    -
    - - - - - tab_name, 'language', Text::_('JFIELD_LANGUAGE_LABEL')); ?> - form->renderField('language'); ?> - - - form->renderField('language'); ?> - - - get('show_publishing_options', 1) == 1) : ?> - tab_name, 'metadata', Text::_('COM_CONTENT_METADATA')); ?> - form->renderField('metadesc'); ?> - form->renderField('metakey'); ?> - - - - - - - - -
    -
    - - - showSaveAsCopy) : ?> - - - - get('save_history', 0) && $this->item->id) : ?> - form->getInput('contenthistory'); ?> - -
    -
    + get('show_page_heading')) : ?> + + + +
    +
    + tab_name, ['active' => 'editor', 'recall' => true, 'breakpoint' => 768]); ?> + + tab_name, 'editor', Text::_('COM_CONTENT_ARTICLE_CONTENT')); ?> + form->renderField('title'); ?> + + item->id)) : ?> + form->renderField('alias'); ?> + + + form->renderField('articletext'); ?> + + captchaEnabled) : ?> + form->renderField('captcha'); ?> + + + + get('show_urls_images_frontend')) : ?> + tab_name, 'images', Text::_('COM_CONTENT_IMAGES_AND_URLS')); ?> + form->renderField('image_intro', 'images'); ?> + form->renderField('image_intro_alt', 'images'); ?> + form->renderField('image_intro_alt_empty', 'images'); ?> + form->renderField('image_intro_caption', 'images'); ?> + form->renderField('float_intro', 'images'); ?> + form->renderField('image_fulltext', 'images'); ?> + form->renderField('image_fulltext_alt', 'images'); ?> + form->renderField('image_fulltext_alt_empty', 'images'); ?> + form->renderField('image_fulltext_caption', 'images'); ?> + form->renderField('float_fulltext', 'images'); ?> + form->renderField('urla', 'urls'); ?> + form->renderField('urlatext', 'urls'); ?> +
    +
    + form->getInput('targeta', 'urls'); ?> +
    +
    + form->renderField('urlb', 'urls'); ?> + form->renderField('urlbtext', 'urls'); ?> +
    +
    + form->getInput('targetb', 'urls'); ?> +
    +
    + form->renderField('urlc', 'urls'); ?> + form->renderField('urlctext', 'urls'); ?> +
    +
    + form->getInput('targetc', 'urls'); ?> +
    +
    + + + + + + tab_name, 'publishing', Text::_('COM_CONTENT_PUBLISHING')); ?> + + form->renderField('transition'); ?> + form->renderField('state'); ?> + form->renderField('catid'); ?> + form->renderField('tags'); ?> + form->renderField('note'); ?> + get('save_history', 0)) : ?> + form->renderField('version_note'); ?> + + get('show_publishing_options', 1) == 1) : ?> + form->renderField('created_by_alias'); ?> + + item->params->get('access-change')) : ?> + form->renderField('featured'); ?> + get('show_publishing_options', 1) == 1) : ?> + form->renderField('featured_up'); ?> + form->renderField('featured_down'); ?> + form->renderField('publish_up'); ?> + form->renderField('publish_down'); ?> + + + form->renderField('access'); ?> + item->id)) : ?> +
    +
    +
    +
    + +
    +
    + + + + + tab_name, 'language', Text::_('JFIELD_LANGUAGE_LABEL')); ?> + form->renderField('language'); ?> + + + form->renderField('language'); ?> + + + get('show_publishing_options', 1) == 1) : ?> + tab_name, 'metadata', Text::_('COM_CONTENT_METADATA')); ?> + form->renderField('metadesc'); ?> + form->renderField('metakey'); ?> + + + + + + + + +
    +
    + + + showSaveAsCopy) : ?> + + + + get('save_history', 0) && $this->item->id) : ?> + form->getInput('contenthistory'); ?> + +
    +
    diff --git a/components/com_contenthistory/src/Controller/DisplayController.php b/components/com_contenthistory/src/Controller/DisplayController.php index 29bf90b0df382..e04ee9057d4e9 100644 --- a/components/com_contenthistory/src/Controller/DisplayController.php +++ b/components/com_contenthistory/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getLanguage()->load($this->option, JPATH_ADMINISTRATOR) || - $this->app->getLanguage()->load($this->option, JPATH_SITE); - } + /** + * Load the language + * + * @since 4.0.0 + * + * @return void + */ + protected function loadLanguage() + { + // Load common and local language files. + $this->app->getLanguage()->load($this->option, JPATH_ADMINISTRATOR) || + $this->app->getLanguage()->load($this->option, JPATH_SITE); + } - /** - * Method to check component access permission - * - * @since 4.0.0 - * - * @return void - * - * @throws \Exception|NotAllowed - */ - protected function checkAccess() - { - // Check the user has permission to access this component if in the backend - if ($this->app->getIdentity()->guest) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + * + * @throws \Exception|NotAllowed + */ + protected function checkAccess() + { + // Check the user has permission to access this component if in the backend + if ($this->app->getIdentity()->guest) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } - /** - * Get a controller from the component - * - * @param string $name Controller name - * @param string $client Optional client (like Administrator, Site etc.) - * @param array $config Optional controller config - * - * @return BaseController - * - * @since 4.0.0 - */ - public function getController(string $name, string $client = '', array $config = array()): BaseController - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - $client = 'Administrator'; + /** + * Get a controller from the component + * + * @param string $name Controller name + * @param string $client Optional client (like Administrator, Site etc.) + * @param array $config Optional controller config + * + * @return BaseController + * + * @since 4.0.0 + */ + public function getController(string $name, string $client = '', array $config = array()): BaseController + { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + $client = 'Administrator'; - return parent::getController($name, $client, $config); - } + return parent::getController($name, $client, $config); + } } diff --git a/components/com_fields/layouts/field/render.php b/components/com_fields/layouts/field/render.php index 0623fd7355a39..cd6e4144e044f 100644 --- a/components/com_fields/layouts/field/render.php +++ b/components/com_fields/layouts/field/render.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; -if (!array_key_exists('field', $displayData)) -{ - return; +if (!array_key_exists('field', $displayData)) { + return; } $field = $displayData['field']; @@ -24,19 +25,18 @@ $labelClass = $field->params->get('label_render_class'); $valueClass = $field->params->get('value_render_class'); -if ($value == '') -{ - return; +if ($value == '') { + return; } ?> - : + : - + - + diff --git a/components/com_fields/layouts/fields/render.php b/components/com_fields/layouts/fields/render.php index c84630be6bed9..80a8a5c4a0801 100644 --- a/components/com_fields/layouts/fields/render.php +++ b/components/com_fields/layouts/fields/render.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\Component\Fields\Administrator\Helper\FieldsHelper; // Check if we have all the data -if (!array_key_exists('item', $displayData) || !array_key_exists('context', $displayData)) -{ - return; +if (!array_key_exists('item', $displayData) || !array_key_exists('context', $displayData)) { + return; } // Setting up for display $item = $displayData['item']; -if (!$item) -{ - return; +if (!$item) { + return; } $context = $displayData['context']; -if (!$context) -{ - return; +if (!$context) { + return; } $parts = explode('.', $context); $component = $parts[0]; $fields = null; -if (array_key_exists('fields', $displayData)) -{ - $fields = $displayData['fields']; -} -else -{ - $fields = $item->jcfields ?: FieldsHelper::getFields($context, $item, true); +if (array_key_exists('fields', $displayData)) { + $fields = $displayData['fields']; +} else { + $fields = $item->jcfields ?: FieldsHelper::getFields($context, $item, true); } -if (empty($fields)) -{ - return; +if (empty($fields)) { + return; } $output = array(); -foreach ($fields as $field) -{ - // If the value is empty do nothing - if (!isset($field->value) || trim($field->value) === '') - { - continue; - } - - $class = $field->name . ' ' . $field->params->get('render_class'); - $layout = $field->params->get('layout', 'render'); - $content = FieldsHelper::render($context, 'field.' . $layout, array('field' => $field)); - - // If the content is empty do nothing - if (trim($content) === '') - { - continue; - } - - $output[] = '
  • ' . $content . '
  • '; +foreach ($fields as $field) { + // If the value is empty do nothing + if (!isset($field->value) || trim($field->value) === '') { + continue; + } + + $class = $field->name . ' ' . $field->params->get('render_class'); + $layout = $field->params->get('layout', 'render'); + $content = FieldsHelper::render($context, 'field.' . $layout, array('field' => $field)); + + // If the content is empty do nothing + if (trim($content) === '') { + continue; + } + + $output[] = '
  • ' . $content . '
  • '; } -if (empty($output)) -{ - return; +if (empty($output)) { + return; } ?>
      - +
    diff --git a/components/com_fields/src/Controller/DisplayController.php b/components/com_fields/src/Controller/DisplayController.php index d96c9257159eb..2ac3a9501f7ad 100644 --- a/components/com_fields/src/Controller/DisplayController.php +++ b/components/com_fields/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ get('view') === 'fields' && $input->get('layout') === 'modal') { + // Load the backend language file. + $app->getLanguage()->load('com_fields', JPATH_ADMINISTRATOR); - /** - * @param array $config An optional associative array of configuration settings. - * Recognized key values include 'name', 'default_task', 'model_path', and - * 'view_path' (this list is not meant to be comprehensive). - * @param MVCFactoryInterface|null $factory The factory. - * @param CMSApplication|null $app The Application for the dispatcher - * @param \Joomla\CMS\Input\Input|null $input The request's input object - * - * @since 3.7.0 - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - // Frontpage Editor Fields Button proxying. - if ($input->get('view') === 'fields' && $input->get('layout') === 'modal') - { - // Load the backend language file. - $app->getLanguage()->load('com_fields', JPATH_ADMINISTRATOR); - - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - } + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + } - parent::__construct($config, $factory, $app, $input); - } + parent::__construct($config, $factory, $app, $input); + } } diff --git a/components/com_fields/src/Dispatcher/Dispatcher.php b/components/com_fields/src/Dispatcher/Dispatcher.php index af013bbc662be..de892387551e3 100644 --- a/components/com_fields/src/Dispatcher/Dispatcher.php +++ b/components/com_fields/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->get('view') !== 'fields' || $this->input->get('layout') !== 'modal') - { - return; - } - - $context = $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'); - $parts = FieldsHelper::extract($context); - - if (!$this->app->getIdentity()->authorise('core.create', $parts[0]) - || !$this->app->getIdentity()->authorise('core.edit', $parts[0])) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR')); - } - } + /** + * Method to check component access permission + * + * @return void + * + * @since 4.0.0 + */ + protected function checkAccess() + { + parent::checkAccess(); + + if ($this->input->get('view') !== 'fields' || $this->input->get('layout') !== 'modal') { + return; + } + + $context = $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'); + $parts = FieldsHelper::extract($context); + + if ( + !$this->app->getIdentity()->authorise('core.create', $parts[0]) + || !$this->app->getIdentity()->authorise('core.edit', $parts[0]) + ) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR')); + } + } } diff --git a/components/com_finder/helpers/route.php b/components/com_finder/helpers/route.php index 4cfe9367583a9..ee93fd8395cf0 100644 --- a/components/com_finder/helpers/route.php +++ b/components/com_finder/helpers/route.php @@ -1,4 +1,5 @@ app->input; - $cachable = true; - - // Load plugin language files. - LanguageHelper::loadPluginLanguage(); - - // Set the default view name and format from the Request. - $viewName = $input->get('view', 'search', 'word'); - $input->set('view', $viewName); - - // Don't cache view for search queries - if ($input->get('q', null, 'string') || $input->get('f', null, 'int') || $input->get('t', null, 'array')) - { - $cachable = false; - } - - $safeurlparams = array( - 'f' => 'INT', - 'lang' => 'CMD' - ); - - return parent::display($cachable, $safeurlparams); - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached. [optional] + * @param array $urlparams An array of safe URL parameters and their variable types, + * for valid values see {@link \JFilterInput::clean()}. [optional] + * + * @return static This object is to support chaining. + * + * @since 2.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $input = $this->app->input; + $cachable = true; + + // Load plugin language files. + LanguageHelper::loadPluginLanguage(); + + // Set the default view name and format from the Request. + $viewName = $input->get('view', 'search', 'word'); + $input->set('view', $viewName); + + // Don't cache view for search queries + if ($input->get('q', null, 'string') || $input->get('f', null, 'int') || $input->get('t', null, 'array')) { + $cachable = false; + } + + $safeurlparams = array( + 'f' => 'INT', + 'lang' => 'CMD' + ); + + return parent::display($cachable, $safeurlparams); + } } diff --git a/components/com_finder/src/Controller/SuggestionsController.php b/components/com_finder/src/Controller/SuggestionsController.php index e8f04b7be641c..5fb9700175335 100644 --- a/components/com_finder/src/Controller/SuggestionsController.php +++ b/components/com_finder/src/Controller/SuggestionsController.php @@ -1,4 +1,5 @@ app; - $app->mimeType = 'application/json'; - - // Ensure caching is disabled as it depends on the query param in the model - $app->allowCache(false); - - $suggestions = $this->getSuggestions(); - - // Send the response. - $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); - $app->sendHeaders(); - echo '{ "suggestions": ' . json_encode($suggestions) . ' }'; - } - - /** - * Method to find search query suggestions for OpenSearch - * - * @return void - * - * @since 4.0.0 - */ - public function opensearchsuggest() - { - $app = $this->app; - $app->mimeType = 'application/json'; - $result = array(); - $result[] = $app->input->request->get('q', '', 'string'); - - $result[] = $this->getSuggestions(); - - // Ensure caching is disabled as it depends on the query param in the model - $app->allowCache(false); - - // Send the response. - $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); - $app->sendHeaders(); - echo json_encode($result); - } - - /** - * Method to retrieve the data from the database - * - * @return array The suggested words - * - * @since 3.4 - */ - protected function getSuggestions() - { - $return = array(); - - $params = ComponentHelper::getParams('com_finder'); - - if ($params->get('show_autosuggest', 1)) - { - // Get the suggestions. - $model = $this->getModel('Suggestions'); - $return = $model->getItems(); - } - - // Check the data. - if (empty($return)) - { - $return = array(); - } - - return $return; - } + /** + * Method to find search query suggestions. Uses awesomplete + * + * @return void + * + * @since 3.4 + */ + public function suggest() + { + $app = $this->app; + $app->mimeType = 'application/json'; + + // Ensure caching is disabled as it depends on the query param in the model + $app->allowCache(false); + + $suggestions = $this->getSuggestions(); + + // Send the response. + $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); + $app->sendHeaders(); + echo '{ "suggestions": ' . json_encode($suggestions) . ' }'; + } + + /** + * Method to find search query suggestions for OpenSearch + * + * @return void + * + * @since 4.0.0 + */ + public function opensearchsuggest() + { + $app = $this->app; + $app->mimeType = 'application/json'; + $result = array(); + $result[] = $app->input->request->get('q', '', 'string'); + + $result[] = $this->getSuggestions(); + + // Ensure caching is disabled as it depends on the query param in the model + $app->allowCache(false); + + // Send the response. + $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); + $app->sendHeaders(); + echo json_encode($result); + } + + /** + * Method to retrieve the data from the database + * + * @return array The suggested words + * + * @since 3.4 + */ + protected function getSuggestions() + { + $return = array(); + + $params = ComponentHelper::getParams('com_finder'); + + if ($params->get('show_autosuggest', 1)) { + // Get the suggestions. + $model = $this->getModel('Suggestions'); + $return = $model->getItems(); + } + + // Check the data. + if (empty($return)) { + $return = array(); + } + + return $return; + } } diff --git a/components/com_finder/src/Helper/FinderHelper.php b/components/com_finder/src/Helper/FinderHelper.php index e32f6cd026804..9eb044076d496 100644 --- a/components/com_finder/src/Helper/FinderHelper.php +++ b/components/com_finder/src/Helper/FinderHelper.php @@ -1,4 +1,5 @@ get('gather_search_statistics', 0)) - { - return; - } + /** + * Method to log searches to the database + * + * @param Query $searchquery The search query + * @param integer $resultCount The number of results for this search + * + * @return void + * + * @since 4.0.0 + */ + public static function logSearch(Query $searchquery, $resultCount = 0) + { + if (!ComponentHelper::getParams('com_finder')->get('gather_search_statistics', 0)) { + return; + } - if (trim($searchquery->input) == '' && !$searchquery->empty) - { - return; - } + if (trim($searchquery->input) == '' && !$searchquery->empty) { + return; + } - // Initialise our variables - $db = Factory::getDbo(); - $query = $db->getQuery(true); + // Initialise our variables + $db = Factory::getDbo(); + $query = $db->getQuery(true); - // Sanitise the term for the database - $temp = unserialize(serialize($searchquery)); - $temp->input = trim(strtolower($searchquery->input)); - $entry = new \stdClass; - $entry->searchterm = $temp->input; - $entry->query = serialize($temp); - $entry->md5sum = md5($entry->query); - $entry->hits = 1; - $entry->results = $resultCount; + // Sanitise the term for the database + $temp = unserialize(serialize($searchquery)); + $temp->input = trim(strtolower($searchquery->input)); + $entry = new \stdClass(); + $entry->searchterm = $temp->input; + $entry->query = serialize($temp); + $entry->md5sum = md5($entry->query); + $entry->hits = 1; + $entry->results = $resultCount; - // Query the table to determine if the term has been searched previously - $query->select($db->quoteName('hits')) - ->from($db->quoteName('#__finder_logging')) - ->where($db->quoteName('md5sum') . ' = ' . $db->quote($entry->md5sum)); - $db->setQuery($query); - $hits = (int) $db->loadResult(); + // Query the table to determine if the term has been searched previously + $query->select($db->quoteName('hits')) + ->from($db->quoteName('#__finder_logging')) + ->where($db->quoteName('md5sum') . ' = ' . $db->quote($entry->md5sum)); + $db->setQuery($query); + $hits = (int) $db->loadResult(); - // Reset the $query object - $query->clear(); + // Reset the $query object + $query->clear(); - // Update the table based on the results - if ($hits) - { - $query->update($db->quoteName('#__finder_logging')) - ->set('hits = (hits + 1)') - ->where($db->quoteName('md5sum') . ' = ' . $db->quote($entry->md5sum)); - $db->setQuery($query); - $db->execute(); - } - else - { - $query->insert($db->quoteName('#__finder_logging')) - ->columns( - [ - $db->quoteName('searchterm'), - $db->quoteName('query'), - $db->quoteName('md5sum'), - $db->quoteName('hits'), - $db->quoteName('results'), - ] - ) - ->values('?, ?, ?, ?, ?') - ->bind(1, $entry->searchterm) - ->bind(2, $entry->query, ParameterType::LARGE_OBJECT) - ->bind(3, $entry->md5sum) - ->bind(4, $entry->hits, ParameterType::INTEGER) - ->bind(5, $entry->results, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - } - } + // Update the table based on the results + if ($hits) { + $query->update($db->quoteName('#__finder_logging')) + ->set('hits = (hits + 1)') + ->where($db->quoteName('md5sum') . ' = ' . $db->quote($entry->md5sum)); + $db->setQuery($query); + $db->execute(); + } else { + $query->insert($db->quoteName('#__finder_logging')) + ->columns( + [ + $db->quoteName('searchterm'), + $db->quoteName('query'), + $db->quoteName('md5sum'), + $db->quoteName('hits'), + $db->quoteName('results'), + ] + ) + ->values('?, ?, ?, ?, ?') + ->bind(1, $entry->searchterm) + ->bind(2, $entry->query, ParameterType::LARGE_OBJECT) + ->bind(3, $entry->md5sum) + ->bind(4, $entry->hits, ParameterType::INTEGER) + ->bind(5, $entry->results, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + } + } } diff --git a/components/com_finder/src/Helper/RouteHelper.php b/components/com_finder/src/Helper/RouteHelper.php index 275df8c470c62..65794e3a3c006 100644 --- a/components/com_finder/src/Helper/RouteHelper.php +++ b/components/com_finder/src/Helper/RouteHelper.php @@ -1,4 +1,5 @@ 'search', 'q' => $q, 'f' => $f); - $item = self::getItemid($query); - - // Get the base route. - $uri = clone Uri::getInstance('index.php?option=com_finder&view=search'); - - // Add the pre-defined search filter if present. - if ($f !== null) - { - $uri->setVar('f', $f); - } - - // Add the search query string if present. - if ($q !== null) - { - $uri->setVar('q', $q); - } - - // Add the menu item id if present. - if ($item !== null) - { - $uri->setVar('Itemid', $item); - } - - return $uri->toString(array('path', 'query')); - } - - /** - * Method to get the route for an advanced search page. - * - * @param integer $f The search filter id. [optional] - * @param string $q The search query string. [optional] - * - * @return string The advanced search route. - * - * @since 2.5 - */ - public static function getAdvancedRoute($f = null, $q = null) - { - // Get the menu item id. - $query = array('view' => 'advanced', 'q' => $q, 'f' => $f); - $item = self::getItemid($query); - - // Get the base route. - $uri = clone Uri::getInstance('index.php?option=com_finder&view=advanced'); - - // Add the pre-defined search filter if present. - if ($q !== null) - { - $uri->setVar('f', $f); - } - - // Add the search query string if present. - if ($q !== null) - { - $uri->setVar('q', $q); - } - - // Add the menu item id if present. - if ($item !== null) - { - $uri->setVar('Itemid', $item); - } - - return $uri->toString(array('path', 'query')); - } - - /** - * Method to get the most appropriate menu item for the route based on the - * supplied query needles. - * - * @param array $query An array of URL parameters. - * - * @return mixed An integer on success, null otherwise. - * - * @since 2.5 - */ - public static function getItemid($query) - { - static $items, $active; - - // Get the menu items for com_finder. - if (!$items || !$active) - { - $app = Factory::getApplication(); - $com = ComponentHelper::getComponent('com_finder'); - $menu = $app->getMenu(); - $active = $menu->getActive(); - $items = $menu->getItems('component_id', $com->id); - $items = is_array($items) ? $items : array(); - } - - // Try to match the active view and filter. - if ($active && @$active->query['view'] == @$query['view'] && @$active->query['f'] == @$query['f']) - { - return $active->id; - } - - // Try to match the view, query, and filter. - foreach ($items as $item) - { - if (@$item->query['view'] == @$query['view'] && @$item->query['q'] == @$query['q'] && @$item->query['f'] == @$query['f']) - { - return $item->id; - } - } - - // Try to match the view and filter. - foreach ($items as $item) - { - if (@$item->query['view'] == @$query['view'] && @$item->query['f'] == @$query['f']) - { - return $item->id; - } - } - - // Try to match the view. - foreach ($items as $item) - { - if (@$item->query['view'] == @$query['view']) - { - return $item->id; - } - } - - return null; - } + /** + * Method to get the route for a search page. + * + * @param integer $f The search filter id. [optional] + * @param string $q The search query string. [optional] + * + * @return string The search route. + * + * @since 2.5 + */ + public static function getSearchRoute($f = null, $q = null) + { + // Get the menu item id. + $query = array('view' => 'search', 'q' => $q, 'f' => $f); + $item = self::getItemid($query); + + // Get the base route. + $uri = clone Uri::getInstance('index.php?option=com_finder&view=search'); + + // Add the pre-defined search filter if present. + if ($f !== null) { + $uri->setVar('f', $f); + } + + // Add the search query string if present. + if ($q !== null) { + $uri->setVar('q', $q); + } + + // Add the menu item id if present. + if ($item !== null) { + $uri->setVar('Itemid', $item); + } + + return $uri->toString(array('path', 'query')); + } + + /** + * Method to get the route for an advanced search page. + * + * @param integer $f The search filter id. [optional] + * @param string $q The search query string. [optional] + * + * @return string The advanced search route. + * + * @since 2.5 + */ + public static function getAdvancedRoute($f = null, $q = null) + { + // Get the menu item id. + $query = array('view' => 'advanced', 'q' => $q, 'f' => $f); + $item = self::getItemid($query); + + // Get the base route. + $uri = clone Uri::getInstance('index.php?option=com_finder&view=advanced'); + + // Add the pre-defined search filter if present. + if ($q !== null) { + $uri->setVar('f', $f); + } + + // Add the search query string if present. + if ($q !== null) { + $uri->setVar('q', $q); + } + + // Add the menu item id if present. + if ($item !== null) { + $uri->setVar('Itemid', $item); + } + + return $uri->toString(array('path', 'query')); + } + + /** + * Method to get the most appropriate menu item for the route based on the + * supplied query needles. + * + * @param array $query An array of URL parameters. + * + * @return mixed An integer on success, null otherwise. + * + * @since 2.5 + */ + public static function getItemid($query) + { + static $items, $active; + + // Get the menu items for com_finder. + if (!$items || !$active) { + $app = Factory::getApplication(); + $com = ComponentHelper::getComponent('com_finder'); + $menu = $app->getMenu(); + $active = $menu->getActive(); + $items = $menu->getItems('component_id', $com->id); + $items = is_array($items) ? $items : array(); + } + + // Try to match the active view and filter. + if ($active && @$active->query['view'] == @$query['view'] && @$active->query['f'] == @$query['f']) { + return $active->id; + } + + // Try to match the view, query, and filter. + foreach ($items as $item) { + if (@$item->query['view'] == @$query['view'] && @$item->query['q'] == @$query['q'] && @$item->query['f'] == @$query['f']) { + return $item->id; + } + } + + // Try to match the view and filter. + foreach ($items as $item) { + if (@$item->query['view'] == @$query['view'] && @$item->query['f'] == @$query['f']) { + return $item->id; + } + } + + // Try to match the view. + foreach ($items as $item) { + if (@$item->query['view'] == @$query['view']) { + return $item->id; + } + } + + return null; + } } diff --git a/components/com_finder/src/Model/SearchModel.php b/components/com_finder/src/Model/SearchModel.php index cc248493436dd..61777bfa48f7a 100644 --- a/components/com_finder/src/Model/SearchModel.php +++ b/components/com_finder/src/Model/SearchModel.php @@ -1,4 +1,5 @@ $row) - { - // Build the result object. - if (is_resource($row->object)) - { - $result = unserialize(stream_get_contents($row->object)); - } - else - { - $result = unserialize($row->object); - } - - $result->cleanURL = $result->route; - - // Add the result back to the stack. - $results[] = $result; - } - - // Return the results. - return $results; - } - - /** - * Method to get the query object. - * - * @return Query A query object. - * - * @since 2.5 - */ - public function getQuery() - { - // Return the query object. - return $this->searchquery; - } - - /** - * Method to build a database query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery A database query. - * - * @since 2.5 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'l.link_id, l.object' - ) - ); - - $query->from('#__finder_links AS l'); - - $user = Factory::getUser(); - $groups = $this->getState('user.groups', $user->getAuthorisedViewLevels()); - $query->whereIn($db->quoteName('l.access'), $groups) - ->where('l.state = 1') - ->where('l.published = 1'); - - // Get the current date, minus seconds. - $nowDate = $db->quote(substr_replace(Factory::getDate()->toSql(), '00', -2)); - - // Add the publish up and publish down filters. - $query->where('(l.publish_start_date IS NULL OR l.publish_start_date <= ' . $nowDate . ')') - ->where('(l.publish_end_date IS NULL OR l.publish_end_date >= ' . $nowDate . ')'); - - $query->group('l.link_id'); - $query->group('l.object'); - - /* - * Add the taxonomy filters to the query. We have to join the taxonomy - * map table for each group so that we can use AND clauses across - * groups. Within each group there can be an array of values that will - * use OR clauses. - */ - if (!empty($this->searchquery->filters)) - { - // Convert the associative array to a numerically indexed array. - $groups = array_values($this->searchquery->filters); - $taxonomies = call_user_func_array('array_merge', array_values($this->searchquery->filters)); - - $query->join('INNER', $db->quoteName('#__finder_taxonomy_map') . ' AS t ON t.link_id = l.link_id') - ->where('t.node_id IN (' . implode(',', array_unique($taxonomies)) . ')'); - - // Iterate through each taxonomy group. - for ($i = 0, $c = count($groups); $i < $c; $i++) - { - $query->having('SUM(CASE WHEN t.node_id IN (' . implode(',', $groups[$i]) . ') THEN 1 ELSE 0 END) > 0'); - } - } - - // Add the start date filter to the query. - if (!empty($this->searchquery->date1)) - { - // Escape the date. - $date1 = $db->quote($this->searchquery->date1); - - // Add the appropriate WHERE condition. - if ($this->searchquery->when1 === 'before') - { - $query->where($db->quoteName('l.start_date') . ' <= ' . $date1); - } - elseif ($this->searchquery->when1 === 'after') - { - $query->where($db->quoteName('l.start_date') . ' >= ' . $date1); - } - else - { - $query->where($db->quoteName('l.start_date') . ' = ' . $date1); - } - } - - // Add the end date filter to the query. - if (!empty($this->searchquery->date2)) - { - // Escape the date. - $date2 = $db->quote($this->searchquery->date2); - - // Add the appropriate WHERE condition. - if ($this->searchquery->when2 === 'before') - { - $query->where($db->quoteName('l.start_date') . ' <= ' . $date2); - } - elseif ($this->searchquery->when2 === 'after') - { - $query->where($db->quoteName('l.start_date') . ' >= ' . $date2); - } - else - { - $query->where($db->quoteName('l.start_date') . ' = ' . $date2); - } - } - - // Filter by language - if ($this->getState('filter.language')) - { - $query->where('l.language IN (' . $db->quote(Factory::getLanguage()->getTag()) . ', ' . $db->quote('*') . ')'); - } - - // Get the result ordering and direction. - $ordering = $this->getState('list.ordering', 'm.weight'); - $direction = $this->getState('list.direction', 'DESC'); - - /* - * If we are ordering by relevance we have to add up the relevance - * scores that are contained in the ordering field. - */ - if ($ordering === 'm.weight') - { - // Get the base query and add the ordering information. - $query->select('SUM(' . $db->escape($ordering) . ') AS ordering'); - } - /* - * If we are not ordering by relevance, we just have to add - * the unique items to the set. - */ - else - { - // Get the base query and add the ordering information. - $query->select($db->escape($ordering) . ' AS ordering'); - } - - $query->order('ordering ' . $db->escape($direction)); - - /* - * If there are no optional or required search terms in the query, we - * can get the results in one relatively simple database query. - */ - if (empty($this->includedTerms) && $this->searchquery->empty && $this->searchquery->input == '') - { - // Return the results. - return $query; - } - - /* - * If there are no optional or required search terms in the query and - * empty searches are not allowed, we return an empty query. - * If the search term is not empty and empty searches are allowed, - * but no terms were found, we return an empty query as well. - */ - if (empty($this->includedTerms) - && (!$this->searchquery->empty || ($this->searchquery->empty && $this->searchquery->input != ''))) - { - // Since we need to return a query, we simplify this one. - $query->clear('join') - ->clear('where') - ->clear('bounded') - ->clear('having') - ->clear('group') - ->where('false'); - - return $query; - } - - $included = call_user_func_array('array_merge', array_values($this->includedTerms)); - $query->join('INNER', $db->quoteName('#__finder_links_terms') . ' AS m ON m.link_id = l.link_id') - ->where('m.term_id IN (' . implode(',', $included) . ')'); - - // Check if there are any excluded terms to deal with. - if (count($this->excludedTerms)) - { - $query2 = $db->getQuery(true); - $query2->select('e.link_id') - ->from($db->quoteName('#__finder_links_terms', 'e')) - ->where('e.term_id IN (' . implode(',', $this->excludedTerms) . ')'); - $query->where('l.link_id NOT IN (' . $query2 . ')'); - } - - /* - * The query contains required search terms. - */ - if (count($this->requiredTerms)) - { - foreach ($this->requiredTerms as $terms) - { - if (count($terms)) - { - $query->having('SUM(CASE WHEN m.term_id IN (' . implode(',', $terms) . ') THEN 1 ELSE 0 END) > 0'); - } - else - { - $query->where('false'); - break; - } - } - } - - return $query; - } - - /** - * Method to get a store id based on model the configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id An identifier string to generate the store id. [optional] - * @param boolean $page True to store the data paged, false to store all data. [optional] - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '', $page = true) - { - // Get the query object. - $query = $this->getQuery(); - - // Add the search query state. - $id .= ':' . $query->input; - $id .= ':' . $query->language; - $id .= ':' . $query->filter; - $id .= ':' . serialize($query->filters); - $id .= ':' . $query->date1; - $id .= ':' . $query->date2; - $id .= ':' . $query->when1; - $id .= ':' . $query->when2; - - if ($page) - { - // Add the list state for page specific data. - $id .= ':' . $this->getState('list.start'); - $id .= ':' . $this->getState('list.limit'); - $id .= ':' . $this->getState('list.ordering'); - $id .= ':' . $this->getState('list.direction'); - } - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. [optional] - * @param string $direction An optional direction. [optional] - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = null, $direction = null) - { - // Get the configuration options. - $app = Factory::getApplication(); - $input = $app->input; - $params = $app->getParams(); - $user = Factory::getUser(); - $language = Factory::getLanguage(); - - $this->setState('filter.language', Multilanguage::isEnabled()); - - $request = $input->request; - $options = array(); - - // Get the empty query setting. - $options['empty'] = $params->get('allow_empty_query', 0); - - // Get the static taxonomy filters. - $options['filter'] = $request->getInt('f', $params->get('f', '')); - - // Get the dynamic taxonomy filters. - $options['filters'] = $request->get('t', $params->get('t', array()), 'array'); - - // Get the query string. - $options['input'] = $request->getString('q', $params->get('q', '')); - - // Get the query language. - $options['language'] = $request->getCmd('l', $params->get('l', $language->getTag())); - - // Set the word match mode - $options['word_match'] = $params->get('word_match', 'exact'); - - // Get the start date and start date modifier filters. - $options['date1'] = $request->getString('d1', $params->get('d1', '')); - $options['when1'] = $request->getString('w1', $params->get('w1', '')); - - // Get the end date and end date modifier filters. - $options['date2'] = $request->getString('d2', $params->get('d2', '')); - $options['when2'] = $request->getString('w2', $params->get('w2', '')); - - // Load the query object. - $this->searchquery = new Query($options, $this->getDatabase()); - - // Load the query token data. - $this->excludedTerms = $this->searchquery->getExcludedTermIds(); - $this->includedTerms = $this->searchquery->getIncludedTermIds(); - $this->requiredTerms = $this->searchquery->getRequiredTermIds(); - - // Load the list state. - $this->setState('list.start', $input->get('limitstart', 0, 'uint')); - $this->setState('list.limit', $input->get('limit', $params->get('list_limit', $app->get('list_limit', 20)), 'uint')); - - /* - * Load the sort ordering. - * Currently this is 'hard' coded via menu item parameter but may not satisfy a users need. - * More flexibility was way more user friendly. So we allow the user to pass a custom value - * from the pool of fields that are indexed like the 'title' field. - * Also, we allow this parameter to be passed in either case (lower/upper). - */ - $order = $input->getWord('o', $params->get('sort_order', 'relevance')); - $order = StringHelper::strtolower($order); - $this->setState('list.raworder', $order); - - switch ($order) - { - case 'date': - $this->setState('list.ordering', 'l.start_date'); - break; - - case 'price': - $this->setState('list.ordering', 'l.list_price'); - break; - - case ($order === 'relevance' && !empty($this->includedTerms)) : - $this->setState('list.ordering', 'm.weight'); - break; - - case 'title': - $this->setState('list.ordering', 'l.title'); - break; - - default: - $this->setState('list.ordering', 'l.link_id'); - $this->setState('list.raworder'); - break; - } - - /* - * Load the sort direction. - * Currently this is 'hard' coded via menu item parameter but may not satisfy a users need. - * More flexibility was way more user friendly. So we allow to be inverted. - * Also, we allow this parameter to be passed in either case (lower/upper). - */ - $dirn = $input->getWord('od', $params->get('sort_direction', 'desc')); - $dirn = StringHelper::strtolower($dirn); - - switch ($dirn) - { - case 'asc': - $this->setState('list.direction', 'ASC'); - break; - - default: - $this->setState('list.direction', 'DESC'); - break; - } - - // Set the match limit. - $this->setState('match.limit', 1000); - - // Load the parameters. - $this->setState('params', $params); - - // Load the user state. - $this->setState('user.id', (int) $user->get('id')); - $this->setState('user.groups', $user->getAuthorisedViewLevels()); - } + /** + * Context string for the model type + * + * @var string + * @since 2.5 + */ + protected $context = 'com_finder.search'; + + /** + * The query object is an instance of Query which contains and + * models the entire search query including the text input; static and + * dynamic taxonomy filters; date filters; etc. + * + * @var Query + * @since 2.5 + */ + protected $searchquery; + + /** + * An array of all excluded terms ids. + * + * @var array + * @since 2.5 + */ + protected $excludedTerms = array(); + + /** + * An array of all included terms ids. + * + * @var array + * @since 2.5 + */ + protected $includedTerms = array(); + + /** + * An array of all required terms ids. + * + * @var array + * @since 2.5 + */ + protected $requiredTerms = array(); + + /** + * Method to get the results of the query. + * + * @return array An array of Result objects. + * + * @since 2.5 + * @throws \Exception on database error. + */ + public function getItems() + { + $items = parent::getItems(); + + // Check the data. + if (empty($items)) { + return null; + } + + $results = array(); + + // Convert the rows to result objects. + foreach ($items as $rk => $row) { + // Build the result object. + if (is_resource($row->object)) { + $result = unserialize(stream_get_contents($row->object)); + } else { + $result = unserialize($row->object); + } + + $result->cleanURL = $result->route; + + // Add the result back to the stack. + $results[] = $result; + } + + // Return the results. + return $results; + } + + /** + * Method to get the query object. + * + * @return Query A query object. + * + * @since 2.5 + */ + public function getQuery() + { + // Return the query object. + return $this->searchquery; + } + + /** + * Method to build a database query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery A database query. + * + * @since 2.5 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'l.link_id, l.object' + ) + ); + + $query->from('#__finder_links AS l'); + + $user = Factory::getUser(); + $groups = $this->getState('user.groups', $user->getAuthorisedViewLevels()); + $query->whereIn($db->quoteName('l.access'), $groups) + ->where('l.state = 1') + ->where('l.published = 1'); + + // Get the current date, minus seconds. + $nowDate = $db->quote(substr_replace(Factory::getDate()->toSql(), '00', -2)); + + // Add the publish up and publish down filters. + $query->where('(l.publish_start_date IS NULL OR l.publish_start_date <= ' . $nowDate . ')') + ->where('(l.publish_end_date IS NULL OR l.publish_end_date >= ' . $nowDate . ')'); + + $query->group('l.link_id'); + $query->group('l.object'); + + /* + * Add the taxonomy filters to the query. We have to join the taxonomy + * map table for each group so that we can use AND clauses across + * groups. Within each group there can be an array of values that will + * use OR clauses. + */ + if (!empty($this->searchquery->filters)) { + // Convert the associative array to a numerically indexed array. + $groups = array_values($this->searchquery->filters); + $taxonomies = call_user_func_array('array_merge', array_values($this->searchquery->filters)); + + $query->join('INNER', $db->quoteName('#__finder_taxonomy_map') . ' AS t ON t.link_id = l.link_id') + ->where('t.node_id IN (' . implode(',', array_unique($taxonomies)) . ')'); + + // Iterate through each taxonomy group. + for ($i = 0, $c = count($groups); $i < $c; $i++) { + $query->having('SUM(CASE WHEN t.node_id IN (' . implode(',', $groups[$i]) . ') THEN 1 ELSE 0 END) > 0'); + } + } + + // Add the start date filter to the query. + if (!empty($this->searchquery->date1)) { + // Escape the date. + $date1 = $db->quote($this->searchquery->date1); + + // Add the appropriate WHERE condition. + if ($this->searchquery->when1 === 'before') { + $query->where($db->quoteName('l.start_date') . ' <= ' . $date1); + } elseif ($this->searchquery->when1 === 'after') { + $query->where($db->quoteName('l.start_date') . ' >= ' . $date1); + } else { + $query->where($db->quoteName('l.start_date') . ' = ' . $date1); + } + } + + // Add the end date filter to the query. + if (!empty($this->searchquery->date2)) { + // Escape the date. + $date2 = $db->quote($this->searchquery->date2); + + // Add the appropriate WHERE condition. + if ($this->searchquery->when2 === 'before') { + $query->where($db->quoteName('l.start_date') . ' <= ' . $date2); + } elseif ($this->searchquery->when2 === 'after') { + $query->where($db->quoteName('l.start_date') . ' >= ' . $date2); + } else { + $query->where($db->quoteName('l.start_date') . ' = ' . $date2); + } + } + + // Filter by language + if ($this->getState('filter.language')) { + $query->where('l.language IN (' . $db->quote(Factory::getLanguage()->getTag()) . ', ' . $db->quote('*') . ')'); + } + + // Get the result ordering and direction. + $ordering = $this->getState('list.ordering', 'm.weight'); + $direction = $this->getState('list.direction', 'DESC'); + + /* + * If we are ordering by relevance we have to add up the relevance + * scores that are contained in the ordering field. + */ + if ($ordering === 'm.weight') { + // Get the base query and add the ordering information. + $query->select('SUM(' . $db->escape($ordering) . ') AS ordering'); + } + /* + * If we are not ordering by relevance, we just have to add + * the unique items to the set. + */ + else { + // Get the base query and add the ordering information. + $query->select($db->escape($ordering) . ' AS ordering'); + } + + $query->order('ordering ' . $db->escape($direction)); + + /* + * If there are no optional or required search terms in the query, we + * can get the results in one relatively simple database query. + */ + if (empty($this->includedTerms) && $this->searchquery->empty && $this->searchquery->input == '') { + // Return the results. + return $query; + } + + /* + * If there are no optional or required search terms in the query and + * empty searches are not allowed, we return an empty query. + * If the search term is not empty and empty searches are allowed, + * but no terms were found, we return an empty query as well. + */ + if ( + empty($this->includedTerms) + && (!$this->searchquery->empty || ($this->searchquery->empty && $this->searchquery->input != '')) + ) { + // Since we need to return a query, we simplify this one. + $query->clear('join') + ->clear('where') + ->clear('bounded') + ->clear('having') + ->clear('group') + ->where('false'); + + return $query; + } + + $included = call_user_func_array('array_merge', array_values($this->includedTerms)); + $query->join('INNER', $db->quoteName('#__finder_links_terms') . ' AS m ON m.link_id = l.link_id') + ->where('m.term_id IN (' . implode(',', $included) . ')'); + + // Check if there are any excluded terms to deal with. + if (count($this->excludedTerms)) { + $query2 = $db->getQuery(true); + $query2->select('e.link_id') + ->from($db->quoteName('#__finder_links_terms', 'e')) + ->where('e.term_id IN (' . implode(',', $this->excludedTerms) . ')'); + $query->where('l.link_id NOT IN (' . $query2 . ')'); + } + + /* + * The query contains required search terms. + */ + if (count($this->requiredTerms)) { + foreach ($this->requiredTerms as $terms) { + if (count($terms)) { + $query->having('SUM(CASE WHEN m.term_id IN (' . implode(',', $terms) . ') THEN 1 ELSE 0 END) > 0'); + } else { + $query->where('false'); + break; + } + } + } + + return $query; + } + + /** + * Method to get a store id based on model the configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id An identifier string to generate the store id. [optional] + * @param boolean $page True to store the data paged, false to store all data. [optional] + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '', $page = true) + { + // Get the query object. + $query = $this->getQuery(); + + // Add the search query state. + $id .= ':' . $query->input; + $id .= ':' . $query->language; + $id .= ':' . $query->filter; + $id .= ':' . serialize($query->filters); + $id .= ':' . $query->date1; + $id .= ':' . $query->date2; + $id .= ':' . $query->when1; + $id .= ':' . $query->when2; + + if ($page) { + // Add the list state for page specific data. + $id .= ':' . $this->getState('list.start'); + $id .= ':' . $this->getState('list.limit'); + $id .= ':' . $this->getState('list.ordering'); + $id .= ':' . $this->getState('list.direction'); + } + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. [optional] + * @param string $direction An optional direction. [optional] + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = null, $direction = null) + { + // Get the configuration options. + $app = Factory::getApplication(); + $input = $app->input; + $params = $app->getParams(); + $user = Factory::getUser(); + $language = Factory::getLanguage(); + + $this->setState('filter.language', Multilanguage::isEnabled()); + + $request = $input->request; + $options = array(); + + // Get the empty query setting. + $options['empty'] = $params->get('allow_empty_query', 0); + + // Get the static taxonomy filters. + $options['filter'] = $request->getInt('f', $params->get('f', '')); + + // Get the dynamic taxonomy filters. + $options['filters'] = $request->get('t', $params->get('t', array()), 'array'); + + // Get the query string. + $options['input'] = $request->getString('q', $params->get('q', '')); + + // Get the query language. + $options['language'] = $request->getCmd('l', $params->get('l', $language->getTag())); + + // Set the word match mode + $options['word_match'] = $params->get('word_match', 'exact'); + + // Get the start date and start date modifier filters. + $options['date1'] = $request->getString('d1', $params->get('d1', '')); + $options['when1'] = $request->getString('w1', $params->get('w1', '')); + + // Get the end date and end date modifier filters. + $options['date2'] = $request->getString('d2', $params->get('d2', '')); + $options['when2'] = $request->getString('w2', $params->get('w2', '')); + + // Load the query object. + $this->searchquery = new Query($options, $this->getDatabase()); + + // Load the query token data. + $this->excludedTerms = $this->searchquery->getExcludedTermIds(); + $this->includedTerms = $this->searchquery->getIncludedTermIds(); + $this->requiredTerms = $this->searchquery->getRequiredTermIds(); + + // Load the list state. + $this->setState('list.start', $input->get('limitstart', 0, 'uint')); + $this->setState('list.limit', $input->get('limit', $params->get('list_limit', $app->get('list_limit', 20)), 'uint')); + + /* + * Load the sort ordering. + * Currently this is 'hard' coded via menu item parameter but may not satisfy a users need. + * More flexibility was way more user friendly. So we allow the user to pass a custom value + * from the pool of fields that are indexed like the 'title' field. + * Also, we allow this parameter to be passed in either case (lower/upper). + */ + $order = $input->getWord('o', $params->get('sort_order', 'relevance')); + $order = StringHelper::strtolower($order); + $this->setState('list.raworder', $order); + + switch ($order) { + case 'date': + $this->setState('list.ordering', 'l.start_date'); + break; + + case 'price': + $this->setState('list.ordering', 'l.list_price'); + break; + + case ($order === 'relevance' && !empty($this->includedTerms)): + $this->setState('list.ordering', 'm.weight'); + break; + + case 'title': + $this->setState('list.ordering', 'l.title'); + break; + + default: + $this->setState('list.ordering', 'l.link_id'); + $this->setState('list.raworder'); + break; + } + + /* + * Load the sort direction. + * Currently this is 'hard' coded via menu item parameter but may not satisfy a users need. + * More flexibility was way more user friendly. So we allow to be inverted. + * Also, we allow this parameter to be passed in either case (lower/upper). + */ + $dirn = $input->getWord('od', $params->get('sort_direction', 'desc')); + $dirn = StringHelper::strtolower($dirn); + + switch ($dirn) { + case 'asc': + $this->setState('list.direction', 'ASC'); + break; + + default: + $this->setState('list.direction', 'DESC'); + break; + } + + // Set the match limit. + $this->setState('match.limit', 1000); + + // Load the parameters. + $this->setState('params', $params); + + // Load the user state. + $this->setState('user.id', (int) $user->get('id')); + $this->setState('user.groups', $user->getAuthorisedViewLevels()); + } } diff --git a/components/com_finder/src/Model/SuggestionsModel.php b/components/com_finder/src/Model/SuggestionsModel.php index b0232e3d13655..e7483969fe3ff 100644 --- a/components/com_finder/src/Model/SuggestionsModel.php +++ b/components/com_finder/src/Model/SuggestionsModel.php @@ -1,4 +1,5 @@ $v) - { - $items[$k] = $v->term; - } - - return $items; - } - - /** - * Method to build a database query to load the list data. - * - * @return DatabaseQuery A database query - * - * @since 2.5 - */ - protected function getListQuery() - { - $user = Factory::getUser(); - $groups = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); - $lang = Helper::getPrimaryLanguage($this->getState('language')); - - // Create a new query object. - $db = $this->getDatabase(); - $termIdQuery = $db->getQuery(true); - $termQuery = $db->getQuery(true); - - // Limit term count to a reasonable number of results to reduce main query join size - $termIdQuery->select('ti.term_id') - ->from($db->quoteName('#__finder_terms', 'ti')) - ->where('ti.term LIKE ' . $db->quote($db->escape(StringHelper::strtolower($this->getState('input')), true) . '%', false)) - ->where('ti.common = 0') - ->where('ti.language IN (' . $db->quote($lang) . ', ' . $db->quote('*') . ')') - ->order('ti.links DESC') - ->order('ti.weight DESC'); - - $termIds = $db->setQuery($termIdQuery, 0, 100)->loadColumn(); - - // Early return on term mismatch - if (!count($termIds)) - { - return $termIdQuery; - } - - // Select required fields - $termQuery->select('DISTINCT(t.term)') - ->from($db->quoteName('#__finder_terms', 't')) - ->whereIn('t.term_id', $termIds) - ->order('t.links DESC') - ->order('t.weight DESC'); - - // Join mapping table for term <-> link relation - $mappingTable = $db->quoteName('#__finder_links_terms', 'tm'); - $termQuery->join('INNER', $mappingTable . ' ON tm.term_id = t.term_id'); - - // Join links table - $termQuery->join('INNER', $db->quoteName('#__finder_links', 'l') . ' ON (tm.link_id = l.link_id)') - ->where('l.access IN (' . implode(',', $groups) . ')') - ->where('l.state = 1') - ->where('l.published = 1'); - - return $termQuery; - } - - /** - * Method to get a store id based on model the configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id An identifier string to generate the store id. [optional] - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '') - { - // Add the search query state. - $id .= ':' . $this->getState('input'); - $id .= ':' . $this->getState('language'); - - // Add the list state. - $id .= ':' . $this->getState('list.start'); - $id .= ':' . $this->getState('list.limit'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = null, $direction = null) - { - // Get the configuration options. - $app = Factory::getApplication(); - $input = $app->input; - $params = ComponentHelper::getParams('com_finder'); - $user = Factory::getUser(); - - // Get the query input. - $this->setState('input', $input->request->get('q', '', 'string')); - - // Set the query language - if (Multilanguage::isEnabled()) - { - $lang = Factory::getLanguage()->getTag(); - } - else - { - $lang = Helper::getDefaultLanguage(); - } - - $this->setState('language', $lang); - - // Load the list state. - $this->setState('list.start', 0); - $this->setState('list.limit', 10); - - // Load the parameters. - $this->setState('params', $params); - - // Load the user state. - $this->setState('user.id', (int) $user->get('id')); - } + /** + * Context string for the model type. + * + * @var string + * @since 2.5 + */ + protected $context = 'com_finder.suggestions'; + + /** + * Method to get an array of data items. + * + * @return array An array of data items. + * + * @since 2.5 + */ + public function getItems() + { + // Get the items. + $items = parent::getItems(); + + // Convert them to a simple array. + foreach ($items as $k => $v) { + $items[$k] = $v->term; + } + + return $items; + } + + /** + * Method to build a database query to load the list data. + * + * @return DatabaseQuery A database query + * + * @since 2.5 + */ + protected function getListQuery() + { + $user = Factory::getUser(); + $groups = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); + $lang = Helper::getPrimaryLanguage($this->getState('language')); + + // Create a new query object. + $db = $this->getDatabase(); + $termIdQuery = $db->getQuery(true); + $termQuery = $db->getQuery(true); + + // Limit term count to a reasonable number of results to reduce main query join size + $termIdQuery->select('ti.term_id') + ->from($db->quoteName('#__finder_terms', 'ti')) + ->where('ti.term LIKE ' . $db->quote($db->escape(StringHelper::strtolower($this->getState('input')), true) . '%', false)) + ->where('ti.common = 0') + ->where('ti.language IN (' . $db->quote($lang) . ', ' . $db->quote('*') . ')') + ->order('ti.links DESC') + ->order('ti.weight DESC'); + + $termIds = $db->setQuery($termIdQuery, 0, 100)->loadColumn(); + + // Early return on term mismatch + if (!count($termIds)) { + return $termIdQuery; + } + + // Select required fields + $termQuery->select('DISTINCT(t.term)') + ->from($db->quoteName('#__finder_terms', 't')) + ->whereIn('t.term_id', $termIds) + ->order('t.links DESC') + ->order('t.weight DESC'); + + // Join mapping table for term <-> link relation + $mappingTable = $db->quoteName('#__finder_links_terms', 'tm'); + $termQuery->join('INNER', $mappingTable . ' ON tm.term_id = t.term_id'); + + // Join links table + $termQuery->join('INNER', $db->quoteName('#__finder_links', 'l') . ' ON (tm.link_id = l.link_id)') + ->where('l.access IN (' . implode(',', $groups) . ')') + ->where('l.state = 1') + ->where('l.published = 1'); + + return $termQuery; + } + + /** + * Method to get a store id based on model the configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id An identifier string to generate the store id. [optional] + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '') + { + // Add the search query state. + $id .= ':' . $this->getState('input'); + $id .= ':' . $this->getState('language'); + + // Add the list state. + $id .= ':' . $this->getState('list.start'); + $id .= ':' . $this->getState('list.limit'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = null, $direction = null) + { + // Get the configuration options. + $app = Factory::getApplication(); + $input = $app->input; + $params = ComponentHelper::getParams('com_finder'); + $user = Factory::getUser(); + + // Get the query input. + $this->setState('input', $input->request->get('q', '', 'string')); + + // Set the query language + if (Multilanguage::isEnabled()) { + $lang = Factory::getLanguage()->getTag(); + } else { + $lang = Helper::getDefaultLanguage(); + } + + $this->setState('language', $lang); + + // Load the list state. + $this->setState('list.start', 0); + $this->setState('list.limit', 10); + + // Load the parameters. + $this->setState('params', $params); + + // Load the user state. + $this->setState('user.id', (int) $user->get('id')); + } } diff --git a/components/com_finder/src/Service/Router.php b/components/com_finder/src/Service/Router.php index 024fa4b3b8d88..c203f352b987f 100644 --- a/components/com_finder/src/Service/Router.php +++ b/components/com_finder/src/Service/Router.php @@ -1,4 +1,5 @@ registerView($search); + /** + * Finder Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + */ + public function __construct(SiteApplication $app, AbstractMenu $menu) + { + $search = new RouterViewConfiguration('search'); + $this->registerView($search); - parent::__construct($app, $menu); + parent::__construct($app, $menu); - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } } diff --git a/components/com_finder/src/View/Search/FeedView.php b/components/com_finder/src/View/Search/FeedView.php index 4e642595d4517..67ac17d2abe6a 100644 --- a/components/com_finder/src/View/Search/FeedView.php +++ b/components/com_finder/src/View/Search/FeedView.php @@ -1,4 +1,5 @@ input->set('limit', $app->get('feed_limit')); + // Adjust the list limit to the feed limit. + $app->input->set('limit', $app->get('feed_limit')); - // Get view data. - $state = $this->get('State'); - $params = $state->get('params'); - $query = $this->get('Query'); - $results = $this->get('Items'); - $total = $this->get('Total'); + // Get view data. + $state = $this->get('State'); + $params = $state->get('params'); + $query = $this->get('Query'); + $results = $this->get('Items'); + $total = $this->get('Total'); - // Push out the query data. - $explained = HTMLHelper::_('query.explained', $query); + // Push out the query data. + $explained = HTMLHelper::_('query.explained', $query); - // Set the document title. - $this->setDocumentTitle($params->get('page_title', '')); + // Set the document title. + $this->setDocumentTitle($params->get('page_title', '')); - // Configure the document description. - if (!empty($explained)) - { - $this->document->setDescription(html_entity_decode(strip_tags($explained), ENT_QUOTES, 'UTF-8')); - } + // Configure the document description. + if (!empty($explained)) { + $this->document->setDescription(html_entity_decode(strip_tags($explained), ENT_QUOTES, 'UTF-8')); + } - // Set the document link. - $this->document->link = Route::_($query->toUri()); + // Set the document link. + $this->document->link = Route::_($query->toUri()); - // If we don't have any results, we are done. - if (empty($results)) - { - return; - } + // If we don't have any results, we are done. + if (empty($results)) { + return; + } - // Convert the results to feed entries. - foreach ($results as $result) - { - // Convert the result to a feed entry. - $item = new FeedItem; - $item->title = $result->title; - $item->link = Route::_($result->route); - $item->description = $result->description; + // Convert the results to feed entries. + foreach ($results as $result) { + // Convert the result to a feed entry. + $item = new FeedItem(); + $item->title = $result->title; + $item->link = Route::_($result->route); + $item->description = $result->description; - // Use Unix date to cope for non-english languages - $item->date = (int) $result->start_date ? HTMLHelper::_('date', $result->start_date, 'U') : $result->indexdate; + // Use Unix date to cope for non-english languages + $item->date = (int) $result->start_date ? HTMLHelper::_('date', $result->start_date, 'U') : $result->indexdate; - // Loads item info into RSS array - $this->document->addItem($item); - } - } + // Loads item info into RSS array + $this->document->addItem($item); + } + } } diff --git a/components/com_finder/src/View/Search/HtmlView.php b/components/com_finder/src/View/Search/HtmlView.php index a7494962e7da8..b2aaf2de85b96 100644 --- a/components/com_finder/src/View/Search/HtmlView.php +++ b/components/com_finder/src/View/Search/HtmlView.php @@ -1,4 +1,5 @@ params = $app->getParams(); - - // Get view data. - $this->state = $this->get('State'); - $this->query = $this->get('Query'); - \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderQuery') : null; - $this->results = $this->get('Items'); - \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderResults') : null; - $this->total = $this->get('Total'); - \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderTotal') : null; - $this->pagination = $this->get('Pagination'); - \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderPagination') : null; - - // Flag indicates to not add limitstart=0 to URL - $this->pagination->hideEmptyLimitstart = true; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Configure the pathway. - if (!empty($this->query->input)) - { - $app->getPathway()->addItem($this->escape($this->query->input)); - } - - // Check for a double quote in the query string. - if (strpos($this->query->input, '"')) - { - $router = $this->getSiteRouter(); - - // Fix the q variable in the URL. - if ($router->getVar('q') !== $this->query->input) - { - $router->setVar('q', $this->query->input); - } - } - - // Run an event on each result item - if (is_array($this->results)) - { - // Import Finder plugins - PluginHelper::importPlugin('finder'); - - foreach ($this->results as $result) - { - $app->triggerEvent('onFinderResult', array(&$result, &$this->query)); - } - } - - // Log the search - FinderHelper::logSearch($this->query, $this->total); - - // Push out the query data. - $this->suggested = HTMLHelper::_('query.suggested', $this->query); - $this->explained = HTMLHelper::_('query.explained', $this->query); - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); - - // Check for layout override only if this is not the active menu item - // If it is the active menu item, then the view and category id will match - $active = $app->getMenu()->getActive(); - - if (isset($active->query['layout'])) - { - // We need to set the layout in case this is an alternative menu item (with an alternative layout) - $this->setLayout($active->query['layout']); - } - - $this->prepareDocument(); - - \JDEBUG ? Profiler::getInstance('Application')->mark('beforeFinderLayout') : null; - - parent::display($tpl); - - \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderLayout') : null; - } - - /** - * Method to get hidden input fields for a get form so that control variables - * are not lost upon form submission - * - * @return string A string of hidden input form fields - * - * @since 2.5 - */ - protected function getFields() - { - $fields = null; - - // Get the URI. - $uri = Uri::getInstance(Route::_($this->query->toUri())); - $uri->delVar('q'); - $uri->delVar('o'); - $uri->delVar('t'); - $uri->delVar('d1'); - $uri->delVar('d2'); - $uri->delVar('w1'); - $uri->delVar('w2'); - $elements = $uri->getQuery(true); - - // Create hidden input elements for each part of the URI. - foreach ($elements as $n => $v) - { - if (is_scalar($v)) - { - $fields .= ''; - } - } - - return $fields; - } - - /** - * Method to get the layout file for a search result object. - * - * @param string $layout The layout file to check. [optional] - * - * @return string The layout file to use. - * - * @since 2.5 - */ - protected function getLayoutFile($layout = null) - { - // Create and sanitize the file name. - $file = $this->_layout . '_' . preg_replace('/[^A-Z0-9_\.-]/i', '', $layout); - - // Check if the file exists. - $filetofind = $this->_createFileName('template', array('name' => $file)); - $exists = Path::find($this->_path['template'], $filetofind); - - return ($exists ? $layout : 'result'); - } - - /** - * Prepares the document - * - * @return void - * - * @since 2.5 - */ - protected function prepareDocument() - { - $app = Factory::getApplication(); - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_FINDER_DEFAULT_PAGE_TITLE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($layout = $this->params->get('article_layout')) - { - $this->setLayout($layout); - } - - // Configure the document meta-description. - if (!empty($this->explained)) - { - $explained = $this->escape(html_entity_decode(strip_tags($this->explained), ENT_QUOTES, 'UTF-8')); - $this->document->setDescription($explained); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - // Check for OpenSearch - if ($this->params->get('opensearch', 1)) - { - $ostitle = $this->params->get('opensearch_name', - Text::_('COM_FINDER_OPENSEARCH_NAME') . ' ' . $app->get('sitename') - ); - $this->document->addHeadLink( - Uri::getInstance()->toString(array('scheme', 'host', 'port')) . Route::_('index.php?option=com_finder&view=search&format=opensearch'), - 'search', 'rel', array('title' => $ostitle, 'type' => 'application/opensearchdescription+xml') - ); - } - - // Add feed link to the document head. - if ($this->params->get('show_feed_link', 1) == 1) - { - // Add the RSS link. - $props = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); - $route = Route::_($this->query->toUri() . '&format=feed&type=rss'); - $this->document->addHeadLink($route, 'alternate', 'rel', $props); - - // Add the ATOM link. - $props = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); - $route = Route::_($this->query->toUri() . '&format=feed&type=atom'); - $this->document->addHeadLink($route, 'alternate', 'rel', $props); - } - } + use SiteRouterAwareTrait; + + /** + * The query indexer object + * + * @var Query + * + * @since 4.0.0 + */ + protected $query; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params = null; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * The logged in user + * + * @var \Joomla\CMS\User\User|null + */ + protected $user = null; + + /** + * The suggested search query + * + * @var string|false + * + * @since 4.0.0 + */ + protected $suggested = false; + + /** + * The explained (human-readable) search query + * + * @var string|null + * + * @since 4.0.0 + */ + protected $explained = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * An array of results + * + * @var array + * + * @since 3.8.0 + */ + protected $results; + + /** + * The total number of items + * + * @var integer + * + * @since 3.8.0 + */ + protected $total; + + /** + * The pagination object + * + * @var Pagination + * + * @since 3.8.0 + */ + protected $pagination; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $this->params = $app->getParams(); + + // Get view data. + $this->state = $this->get('State'); + $this->query = $this->get('Query'); + \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderQuery') : null; + $this->results = $this->get('Items'); + \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderResults') : null; + $this->total = $this->get('Total'); + \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderTotal') : null; + $this->pagination = $this->get('Pagination'); + \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderPagination') : null; + + // Flag indicates to not add limitstart=0 to URL + $this->pagination->hideEmptyLimitstart = true; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Configure the pathway. + if (!empty($this->query->input)) { + $app->getPathway()->addItem($this->escape($this->query->input)); + } + + // Check for a double quote in the query string. + if (strpos($this->query->input, '"')) { + $router = $this->getSiteRouter(); + + // Fix the q variable in the URL. + if ($router->getVar('q') !== $this->query->input) { + $router->setVar('q', $this->query->input); + } + } + + // Run an event on each result item + if (is_array($this->results)) { + // Import Finder plugins + PluginHelper::importPlugin('finder'); + + foreach ($this->results as $result) { + $app->triggerEvent('onFinderResult', array(&$result, &$this->query)); + } + } + + // Log the search + FinderHelper::logSearch($this->query, $this->total); + + // Push out the query data. + $this->suggested = HTMLHelper::_('query.suggested', $this->query); + $this->explained = HTMLHelper::_('query.explained', $this->query); + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); + + // Check for layout override only if this is not the active menu item + // If it is the active menu item, then the view and category id will match + $active = $app->getMenu()->getActive(); + + if (isset($active->query['layout'])) { + // We need to set the layout in case this is an alternative menu item (with an alternative layout) + $this->setLayout($active->query['layout']); + } + + $this->prepareDocument(); + + \JDEBUG ? Profiler::getInstance('Application')->mark('beforeFinderLayout') : null; + + parent::display($tpl); + + \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderLayout') : null; + } + + /** + * Method to get hidden input fields for a get form so that control variables + * are not lost upon form submission + * + * @return string A string of hidden input form fields + * + * @since 2.5 + */ + protected function getFields() + { + $fields = null; + + // Get the URI. + $uri = Uri::getInstance(Route::_($this->query->toUri())); + $uri->delVar('q'); + $uri->delVar('o'); + $uri->delVar('t'); + $uri->delVar('d1'); + $uri->delVar('d2'); + $uri->delVar('w1'); + $uri->delVar('w2'); + $elements = $uri->getQuery(true); + + // Create hidden input elements for each part of the URI. + foreach ($elements as $n => $v) { + if (is_scalar($v)) { + $fields .= ''; + } + } + + return $fields; + } + + /** + * Method to get the layout file for a search result object. + * + * @param string $layout The layout file to check. [optional] + * + * @return string The layout file to use. + * + * @since 2.5 + */ + protected function getLayoutFile($layout = null) + { + // Create and sanitize the file name. + $file = $this->_layout . '_' . preg_replace('/[^A-Z0-9_\.-]/i', '', $layout); + + // Check if the file exists. + $filetofind = $this->_createFileName('template', array('name' => $file)); + $exists = Path::find($this->_path['template'], $filetofind); + + return ($exists ? $layout : 'result'); + } + + /** + * Prepares the document + * + * @return void + * + * @since 2.5 + */ + protected function prepareDocument() + { + $app = Factory::getApplication(); + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_FINDER_DEFAULT_PAGE_TITLE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($layout = $this->params->get('article_layout')) { + $this->setLayout($layout); + } + + // Configure the document meta-description. + if (!empty($this->explained)) { + $explained = $this->escape(html_entity_decode(strip_tags($this->explained), ENT_QUOTES, 'UTF-8')); + $this->document->setDescription($explained); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + // Check for OpenSearch + if ($this->params->get('opensearch', 1)) { + $ostitle = $this->params->get( + 'opensearch_name', + Text::_('COM_FINDER_OPENSEARCH_NAME') . ' ' . $app->get('sitename') + ); + $this->document->addHeadLink( + Uri::getInstance()->toString(array('scheme', 'host', 'port')) . Route::_('index.php?option=com_finder&view=search&format=opensearch'), + 'search', + 'rel', + array('title' => $ostitle, 'type' => 'application/opensearchdescription+xml') + ); + } + + // Add feed link to the document head. + if ($this->params->get('show_feed_link', 1) == 1) { + // Add the RSS link. + $props = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); + $route = Route::_($this->query->toUri() . '&format=feed&type=rss'); + $this->document->addHeadLink($route, 'alternate', 'rel', $props); + + // Add the ATOM link. + $props = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); + $route = Route::_($this->query->toUri() . '&format=feed&type=atom'); + $this->document->addHeadLink($route, 'alternate', 'rel', $props); + } + } } diff --git a/components/com_finder/src/View/Search/OpensearchView.php b/components/com_finder/src/View/Search/OpensearchView.php index b8eac3f9de54a..a13b3f412b28c 100644 --- a/components/com_finder/src/View/Search/OpensearchView.php +++ b/components/com_finder/src/View/Search/OpensearchView.php @@ -1,4 +1,5 @@ document->setShortName($params->get('opensearch_name', $app->get('sitename'))); - $this->document->setDescription($params->get('opensearch_description', $app->get('MetaDesc'))); + $params = ComponentHelper::getParams('com_finder'); + $this->document->setShortName($params->get('opensearch_name', $app->get('sitename'))); + $this->document->setDescription($params->get('opensearch_description', $app->get('MetaDesc'))); - // Prevent any output when OpenSearch Support is disabled - if (!$params->get('opensearch', 1)) - { - return; - } + // Prevent any output when OpenSearch Support is disabled + if (!$params->get('opensearch', 1)) { + return; + } - // Add the URL for the search - $searchUri = 'index.php?option=com_finder&view=search&q={searchTerms}'; - $suggestionsUri = 'index.php?option=com_finder&task=suggestions.opensearchsuggest&format=json&q={searchTerms}'; - $baseUrl = Uri::getInstance()->toString(array('host', 'port', 'scheme')); - $active = $app->getMenu()->getActive(); + // Add the URL for the search + $searchUri = 'index.php?option=com_finder&view=search&q={searchTerms}'; + $suggestionsUri = 'index.php?option=com_finder&task=suggestions.opensearchsuggest&format=json&q={searchTerms}'; + $baseUrl = Uri::getInstance()->toString(array('host', 'port', 'scheme')); + $active = $app->getMenu()->getActive(); - if ($active->component == 'com_finder') - { - $searchUri .= '&Itemid=' . $active->id; - $suggestionsUri .= '&Itemid=' . $active->id; - } + if ($active->component == 'com_finder') { + $searchUri .= '&Itemid=' . $active->id; + $suggestionsUri .= '&Itemid=' . $active->id; + } - // Add the HTML result view - $htmlSearch = new OpensearchUrl; - $htmlSearch->template = $baseUrl . Route::_($searchUri, false); - $this->document->addUrl($htmlSearch); + // Add the HTML result view + $htmlSearch = new OpensearchUrl(); + $htmlSearch->template = $baseUrl . Route::_($searchUri, false); + $this->document->addUrl($htmlSearch); - // Add the RSS result view - $htmlSearch = new OpensearchUrl; - $htmlSearch->template = $baseUrl . Route::_($searchUri . '&format=feed&type=rss', false); - $htmlSearch->type = 'application/rss+xml'; - $this->document->addUrl($htmlSearch); + // Add the RSS result view + $htmlSearch = new OpensearchUrl(); + $htmlSearch->template = $baseUrl . Route::_($searchUri . '&format=feed&type=rss', false); + $htmlSearch->type = 'application/rss+xml'; + $this->document->addUrl($htmlSearch); - // Add the Atom result view - $htmlSearch = new OpensearchUrl; - $htmlSearch->template = $baseUrl . Route::_($searchUri . '&format=feed&type=atom', false); - $htmlSearch->type = 'application/atom+xml'; - $this->document->addUrl($htmlSearch); + // Add the Atom result view + $htmlSearch = new OpensearchUrl(); + $htmlSearch->template = $baseUrl . Route::_($searchUri . '&format=feed&type=atom', false); + $htmlSearch->type = 'application/atom+xml'; + $this->document->addUrl($htmlSearch); - // Add suggestions URL - if ($params->get('show_autosuggest', 1)) - { - $htmlSearch = new OpensearchUrl; - $htmlSearch->template = $baseUrl . Route::_($suggestionsUri, false); - $htmlSearch->type = 'application/x-suggestions+json'; - $this->document->addUrl($htmlSearch); - } - } + // Add suggestions URL + if ($params->get('show_autosuggest', 1)) { + $htmlSearch = new OpensearchUrl(); + $htmlSearch->template = $baseUrl . Route::_($suggestionsUri, false); + $htmlSearch->type = 'application/x-suggestions+json'; + $this->document->addUrl($htmlSearch); + } + } } diff --git a/components/com_finder/tmpl/search/default.php b/components/com_finder/tmpl/search/default.php index c83d4b72b5adf..419f1443c9e95 100644 --- a/components/com_finder/tmpl/search/default.php +++ b/components/com_finder/tmpl/search/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager() - ->useStyle('com_finder.finder') - ->useScript('com_finder.finder'); + ->useStyle('com_finder.finder') + ->useScript('com_finder.finder'); ?>
    - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading'))) : ?> - escape($this->params->get('page_heading')); ?> - - escape($this->params->get('page_title')); ?> - -

    - -
    - loadTemplate('form'); ?> -
    - - query->search === true) : ?> -
    - loadTemplate('results'); ?> -
    - + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading'))) : ?> + escape($this->params->get('page_heading')); ?> + + escape($this->params->get('page_title')); ?> + +

    + +
    + loadTemplate('form'); ?> +
    + + query->search === true) : ?> +
    + loadTemplate('results'); ?> +
    +
    diff --git a/components/com_finder/tmpl/search/default_form.php b/components/com_finder/tmpl/search/default_form.php index cfba0b5a9b912..5c7677391d849 100644 --- a/components/com_finder/tmpl/search/default_form.php +++ b/components/com_finder/tmpl/search/default_form.php @@ -1,4 +1,5 @@ params->get('show_autosuggest', 1)) -{ - $this->document->getWebAssetManager()->usePreset('awesomplete'); - $this->document->addScriptOptions('finder-search', array('url' => Route::_('index.php?option=com_finder&task=suggestions.suggest&format=json&tmpl=component', false))); +if ($this->params->get('show_autosuggest', 1)) { + $this->document->getWebAssetManager()->usePreset('awesomplete'); + $this->document->addScriptOptions('finder-search', array('url' => Route::_('index.php?option=com_finder&task=suggestions.suggest&format=json&tmpl=component', false))); } ?>
    - getFields(); ?> - + getFields(); ?> + - params->get('show_advanced', 1)) : ?> -
    - - - - params->get('show_advanced_tips', 1)) : ?> -
    -
    - - - - - params->get('tuplecount', 1) > 1) : ?> - - - -
    -
    - -
    - query, $this->params); ?> -
    -
    - + params->get('show_advanced', 1)) : ?> +
    + + + + params->get('show_advanced_tips', 1)) : ?> +
    +
    + + + + + params->get('tuplecount', 1) > 1) : ?> + + + +
    +
    + +
    + query, $this->params); ?> +
    +
    +
    diff --git a/components/com_finder/tmpl/search/default_result.php b/components/com_finder/tmpl/search/default_result.php index 8338a98121872..c04d30a3b8e58 100644 --- a/components/com_finder/tmpl/search/default_result.php +++ b/components/com_finder/tmpl/search/default_result.php @@ -1,4 +1,5 @@ getIdentity(); $show_description = $this->params->get('show_description', 1); -if ($show_description) -{ - // Calculate number of characters to display around the result - $term_length = StringHelper::strlen($this->query->input); - $desc_length = $this->params->get('description_length', 255); - $pad_length = $term_length < $desc_length ? (int) floor(($desc_length - $term_length) / 2) : 0; +if ($show_description) { + // Calculate number of characters to display around the result + $term_length = StringHelper::strlen($this->query->input); + $desc_length = $this->params->get('description_length', 255); + $pad_length = $term_length < $desc_length ? (int) floor(($desc_length - $term_length) / 2) : 0; - // Make sure we highlight term both in introtext and fulltext - $full_description = $this->result->description; - if (!empty($this->result->summary) && !empty($this->result->body)) - { - $full_description = Helper::parse($this->result->summary . $this->result->body); - } + // Make sure we highlight term both in introtext and fulltext + $full_description = $this->result->description; + if (!empty($this->result->summary) && !empty($this->result->body)) { + $full_description = Helper::parse($this->result->summary . $this->result->body); + } - // Find the position of the search term - $pos = $term_length ? StringHelper::strpos(StringHelper::strtolower($full_description), StringHelper::strtolower($this->query->input)) : false; + // Find the position of the search term + $pos = $term_length ? StringHelper::strpos(StringHelper::strtolower($full_description), StringHelper::strtolower($this->query->input)) : false; - // Find a potential start point - $start = ($pos && $pos > $pad_length) ? $pos - $pad_length : 0; + // Find a potential start point + $start = ($pos && $pos > $pad_length) ? $pos - $pad_length : 0; - // Find a space between $start and $pos, start right after it. - $space = StringHelper::strpos($full_description, ' ', $start > 0 ? $start - 1 : 0); - $start = ($space && $space < $pos) ? $space + 1 : $start; + // Find a space between $start and $pos, start right after it. + $space = StringHelper::strpos($full_description, ' ', $start > 0 ? $start - 1 : 0); + $start = ($space && $space < $pos) ? $space + 1 : $start; - $description = HTMLHelper::_('string.truncate', StringHelper::substr($full_description, $start), $desc_length, true); + $description = HTMLHelper::_('string.truncate', StringHelper::substr($full_description, $start), $desc_length, true); } $showImage = $this->params->get('show_image', 0); $imageClass = $this->params->get('image_class', ''); $extraAttr = []; -if ($showImage && !empty($this->result->imageUrl) && $imageClass !== '') -{ - $extraAttr['class'] = $imageClass; +if ($showImage && !empty($this->result->imageUrl) && $imageClass !== '') { + $extraAttr['class'] = $imageClass; } $icon = ''; -if (!empty($this->result->mime)) -{ - $icon = ' '; +if (!empty($this->result->mime)) { + $icon = ' '; } $show_url = ''; -if ($this->params->get('show_url', 1)) -{ - $show_url = '' . $this->baseUrl . Route::_($this->result->cleanURL) . ''; +if ($this->params->get('show_url', 1)) { + $show_url = '' . $this->baseUrl . Route::_($this->result->cleanURL) . ''; } ?>
  • - result->imageUrl)) : ?> -
    - params->get('link_image') && $this->result->route) : ?> - - result->imageUrl, $this->result->imageAlt, $extraAttr); ?> - - - result->imageUrl, $this->result->imageAlt, $extraAttr); ?> - -
    - -

    - result->route) : ?> - result->route), - '' . $icon . $this->result->title . '' . $show_url, - [ - 'class' => 'result__title-link' - ] - ); ?> - - result->title; ?> - -

    - -

    - result->start_date && $this->params->get('show_date', 1)) : ?> - - - -

    - - result->getTaxonomy(); ?> - params->get('show_taxonomy', 1)) : ?> -
      - $taxonomy) : ?> - - state == 1 && in_array($branch->access, $user->getAuthorisedViewLevels())) : ?> - - - state == 1 && in_array($node->access, $user->getAuthorisedViewLevels())) : ?> - title; ?> - - - -
    • - : -
    • - - - -
    - + result->imageUrl)) : ?> +
    + params->get('link_image') && $this->result->route) : ?> + + result->imageUrl, $this->result->imageAlt, $extraAttr); ?> + + + result->imageUrl, $this->result->imageAlt, $extraAttr); ?> + +
    + +

    + result->route) : ?> + result->route), + '' . $icon . $this->result->title . '' . $show_url, + [ + 'class' => 'result__title-link' + ] + ); ?> + + result->title; ?> + +

    + +

    + result->start_date && $this->params->get('show_date', 1)) : ?> + + + +

    + + result->getTaxonomy(); ?> + params->get('show_taxonomy', 1)) : ?> +
      + $taxonomy) : ?> + + state == 1 && in_array($branch->access, $user->getAuthorisedViewLevels())) : ?> + + + state == 1 && in_array($node->access, $user->getAuthorisedViewLevels())) : ?> + title; ?> + + + +
    • + : +
    • + + + +
    +
  • diff --git a/components/com_finder/tmpl/search/default_results.php b/components/com_finder/tmpl/search/default_results.php index 691fe7224b3b0..7e283441d5a9b 100644 --- a/components/com_finder/tmpl/search/default_results.php +++ b/components/com_finder/tmpl/search/default_results.php @@ -1,4 +1,5 @@ suggested && $this->params->get('show_suggested_query', 1)) || ($this->explained && $this->params->get('show_explained_query', 1))) : ?> -
    - - suggested && $this->params->get('show_suggested_query', 1)) : ?> - - query->toUri()); ?> - setVar('q', $this->suggested); ?> - - toString(array('path', 'query'))); ?> - ' . $this->escape($this->suggested) . ''; ?> - - explained && $this->params->get('show_explained_query', 1)) : ?> - -

    - total, $this->explained); ?> -

    - -
    +
    + + suggested && $this->params->get('show_suggested_query', 1)) : ?> + + query->toUri()); ?> + setVar('q', $this->suggested); ?> + + toString(array('path', 'query'))); ?> + ' . $this->escape($this->suggested) . ''; ?> + + explained && $this->params->get('show_explained_query', 1)) : ?> + +

    + total, $this->explained); ?> +

    + +
    total === 0) || ($this->total === null)) : ?> -
    -

    - getLanguageFilter() ? '_MULTILANG' : ''; ?> -

    escape($this->query->input)); ?>

    -
    - - +
    +

    + getLanguageFilter() ? '_MULTILANG' : ''; ?> +

    escape($this->query->input)); ?>

    +
    + + query->highlight) && $this->params->get('highlight_terms', 1)) : ?> - document->getWebAssetManager()->useScript('highlight'); - $this->document->addScriptOptions( - 'highlight', - [[ - 'class' => 'js-highlight', - 'highLight' => $this->query->highlight, - ]] - ); - ?> + document->getWebAssetManager()->useScript('highlight'); + $this->document->addScriptOptions( + 'highlight', + [[ + 'class' => 'js-highlight', + 'highLight' => $this->query->highlight, + ]] + ); + ?>
      - baseUrl = Uri::getInstance()->toString(array('scheme', 'host', 'port')); ?> - results as $i => $result) : ?> - result = &$result; ?> - result->counter = $i + 1; ?> - getLayoutFile($this->result->layout); ?> - loadTemplate($layout); ?> - + baseUrl = Uri::getInstance()->toString(array('scheme', 'host', 'port')); ?> + results as $i => $result) : ?> + result = &$result; ?> + result->counter = $i + 1; ?> + getLayoutFile($this->result->layout); ?> + loadTemplate($layout); ?> +
    - params->get('show_pagination', 1) > 0) : ?> -
    - pagination->getPagesLinks(); ?> -
    - - params->get('show_pagination_results', 1) > 0) : ?> -
    - - pagination->limitstart + 1; ?> - pagination->total; ?> - pagination->limit * $this->pagination->pagesCurrent; ?> - $total ? $total : $limit); ?> - -
    - + params->get('show_pagination', 1) > 0) : ?> +
    + pagination->getPagesLinks(); ?> +
    + + params->get('show_pagination_results', 1) > 0) : ?> +
    + + pagination->limitstart + 1; ?> + pagination->total; ?> + pagination->limit * $this->pagination->pagesCurrent; ?> + $total ? $total : $limit); ?> + +
    +
    diff --git a/components/com_media/src/Dispatcher/Dispatcher.php b/components/com_media/src/Dispatcher/Dispatcher.php index 893d134777743..40c738335d785 100644 --- a/components/com_media/src/Dispatcher/Dispatcher.php +++ b/components/com_media/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getLanguage()->load('', JPATH_ADMINISTRATOR); - $this->app->getLanguage()->load($this->option, JPATH_ADMINISTRATOR); + /** + * Load the language + * + * @since 4.0.0 + * + * @return void + */ + protected function loadLanguage() + { + // Load the administrator languages needed for the media manager + $this->app->getLanguage()->load('', JPATH_ADMINISTRATOR); + $this->app->getLanguage()->load($this->option, JPATH_ADMINISTRATOR); - parent::loadLanguage(); - } + parent::loadLanguage(); + } - /** - * Method to check component access permission - * - * @since 4.0.0 - * - * @return void - */ - protected function checkAccess() - { - $user = $this->app->getIdentity(); + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + */ + protected function checkAccess() + { + $user = $this->app->getIdentity(); - // Access check - if (!$user->authorise('core.manage', 'com_media') - && !$user->authorise('core.create', 'com_media')) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + // Access check + if ( + !$user->authorise('core.manage', 'com_media') + && !$user->authorise('core.create', 'com_media') + ) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } - /** - * Get a controller from the component - * - * @param string $name Controller name - * @param string $client Optional client (like Administrator, Site etc.) - * @param array $config Optional controller config - * - * @return BaseController - * - * @since 4.0.0 - */ - public function getController(string $name, string $client = '', array $config = array()): BaseController - { - $config['base_path'] = JPATH_ADMINISTRATOR . '/components/com_media'; + /** + * Get a controller from the component + * + * @param string $name Controller name + * @param string $client Optional client (like Administrator, Site etc.) + * @param array $config Optional controller config + * + * @return BaseController + * + * @since 4.0.0 + */ + public function getController(string $name, string $client = '', array $config = array()): BaseController + { + $config['base_path'] = JPATH_ADMINISTRATOR . '/components/com_media'; - // Force to load the admin controller - return parent::getController($name, 'Administrator', $config); - } + // Force to load the admin controller + return parent::getController($name, 'Administrator', $config); + } } diff --git a/components/com_menus/layouts/joomla/searchtools/default.php b/components/com_menus/layouts/joomla/searchtools/default.php index 456ea9279fe11..0d7cd07e2d764 100644 --- a/components/com_menus/layouts/joomla/searchtools/default.php +++ b/components/com_menus/layouts/joomla/searchtools/default.php @@ -1,4 +1,5 @@ filterForm) && !empty($data['view']->filterForm)) -{ - // Checks if a selector (e.g. client_id) exists. - if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) - { - $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; - - // Checks if a selector should be shown in the current layout. - if (isset($data['view']->layout)) - { - $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; - } - - // Unset the selector field from active filters group. - unset($data['view']->activeFilters[$selectorFieldName]); - } - - if ($data['view'] instanceof \Joomla\Component\Menus\Administrator\View\Items\HtmlView) : - unset($data['view']->activeFilters['client_id']); - endif; - - // Checks if the filters button should exist. - $filters = $data['view']->filterForm->getGroup('filter'); - $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; - - // Checks if it should show the be hidden. - $hideActiveFilters = empty($data['view']->activeFilters); - - // Check if the no results message should appear. - if (isset($data['view']->total) && (int) $data['view']->total === 0) - { - $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); - if (!empty($noResults)) - { - $noResultsText = Text::_($noResults); - } - } +if (isset($data['view']->filterForm) && !empty($data['view']->filterForm)) { + // Checks if a selector (e.g. client_id) exists. + if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) { + $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; + + // Checks if a selector should be shown in the current layout. + if (isset($data['view']->layout)) { + $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; + } + + // Unset the selector field from active filters group. + unset($data['view']->activeFilters[$selectorFieldName]); + } + + if ($data['view'] instanceof \Joomla\Component\Menus\Administrator\View\Items\HtmlView) : + unset($data['view']->activeFilters['client_id']); + endif; + + // Checks if the filters button should exist. + $filters = $data['view']->filterForm->getGroup('filter'); + $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; + + // Checks if it should show the be hidden. + $hideActiveFilters = empty($data['view']->activeFilters); + + // Check if the no results message should appear. + if (isset($data['view']->total) && (int) $data['view']->total === 0) { + $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); + if (!empty($noResults)) { + $noResultsText = Text::_($noResults); + } + } } // Set some basic options. $customOptions = array( - 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, - 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, - 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), - 'searchFieldSelector' => '#filter_search', - 'selectorFieldName' => $selectorFieldName, - 'showSelector' => $showSelector, - 'orderFieldSelector' => '#list_fullordering', - 'showNoResults' => !empty($noResultsText), - 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', - 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', + 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, + 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, + 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), + 'searchFieldSelector' => '#filter_search', + 'selectorFieldName' => $selectorFieldName, + 'showSelector' => $showSelector, + 'orderFieldSelector' => '#list_fullordering', + 'showNoResults' => !empty($noResultsText), + 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', + 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', ); // Merge custom options in the options array. @@ -89,39 +85,39 @@ HTMLHelper::_('searchtools.form', $data['options']['formSelector'], $data['options']); ?> - sublayout('noitems', $data); ?> + sublayout('noitems', $data); ?> diff --git a/components/com_menus/src/Dispatcher/Dispatcher.php b/components/com_menus/src/Dispatcher/Dispatcher.php index 4a87d0ac39111..ca72006a04eed 100644 --- a/components/com_menus/src/Dispatcher/Dispatcher.php +++ b/components/com_menus/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getLanguage()->load('com_menus', JPATH_ADMINISTRATOR); - } + /** + * Load the language + * + * @since 4.0.0 + * + * @return void + */ + protected function loadLanguage() + { + $this->app->getLanguage()->load('com_menus', JPATH_ADMINISTRATOR); + } - /** - * Dispatch a controller task. Redirecting the user if appropriate. - * - * @return void - * - * @since 4.0.0 - */ - public function checkAccess() - { - parent::checkAccess(); + /** + * Dispatch a controller task. Redirecting the user if appropriate. + * + * @return void + * + * @since 4.0.0 + */ + public function checkAccess() + { + parent::checkAccess(); - if ($this->input->get('view') !== 'items' - || $this->input->get('layout') !== 'modal' - || !$this->app->getIdentity()->authorise('core.create', 'com_menus')) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + if ( + $this->input->get('view') !== 'items' + || $this->input->get('layout') !== 'modal' + || !$this->app->getIdentity()->authorise('core.create', 'com_menus') + ) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } - /** - * Get a controller from the component - * - * @param string $name Controller name - * @param string $client Optional client (like Administrator, Site etc.) - * @param array $config Optional controller config - * - * @return \Joomla\CMS\MVC\Controller\BaseController - * - * @since 4.0.0 - */ - public function getController(string $name, string $client = '', array $config = array()): BaseController - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - $client = 'Administrator'; + /** + * Get a controller from the component + * + * @param string $name Controller name + * @param string $client Optional client (like Administrator, Site etc.) + * @param array $config Optional controller config + * + * @return \Joomla\CMS\MVC\Controller\BaseController + * + * @since 4.0.0 + */ + public function getController(string $name, string $client = '', array $config = array()): BaseController + { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + $client = 'Administrator'; - return parent::getController($name, $client, $config); - } + return parent::getController($name, $client, $config); + } } diff --git a/components/com_modules/src/Controller/DisplayController.php b/components/com_modules/src/Controller/DisplayController.php index e0f008346b87a..c32d25e7cf03b 100644 --- a/components/com_modules/src/Controller/DisplayController.php +++ b/components/com_modules/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input = Factory::getApplication()->input; + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param Input|null $input The Input object for the request + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + $this->input = Factory::getApplication()->input; - // Modules frontpage Editor Module proxying. - if ($this->input->get('view') === 'modules' && $this->input->get('layout') === 'modal') - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - } + // Modules frontpage Editor Module proxying. + if ($this->input->get('view') === 'modules' && $this->input->get('layout') === 'modal') { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + } - parent::__construct($config, $factory, $app, $input); - } + parent::__construct($config, $factory, $app, $input); + } } diff --git a/components/com_modules/src/Dispatcher/Dispatcher.php b/components/com_modules/src/Dispatcher/Dispatcher.php index daead79a2b2d8..fe1e93f0fbc68 100644 --- a/components/com_modules/src/Dispatcher/Dispatcher.php +++ b/components/com_modules/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getLanguage()->load('com_modules', JPATH_ADMINISTRATOR); - } + /** + * Load the language + * + * @since 4.0.0 + * + * @return void + */ + protected function loadLanguage() + { + $this->app->getLanguage()->load('com_modules', JPATH_ADMINISTRATOR); + } - /** - * Dispatch a controller task. Redirecting the user if appropriate. - * - * @return void - * - * @since 4.0.0 - */ - public function checkAccess() - { - parent::checkAccess(); + /** + * Dispatch a controller task. Redirecting the user if appropriate. + * + * @return void + * + * @since 4.0.0 + */ + public function checkAccess() + { + parent::checkAccess(); - if ($this->input->get('view') === 'modules' - && $this->input->get('layout') === 'modal' - && !$this->app->getIdentity()->authorise('core.create', 'com_modules')) - { - throw new NotAllowed; - } - } + if ( + $this->input->get('view') === 'modules' + && $this->input->get('layout') === 'modal' + && !$this->app->getIdentity()->authorise('core.create', 'com_modules') + ) { + throw new NotAllowed(); + } + } - /** - * Get a controller from the component - * - * @param string $name Controller name - * @param string $client Optional client (like Administrator, Site etc.) - * @param array $config Optional controller config - * - * @return \Joomla\CMS\MVC\Controller\BaseController - * - * @since 4.0.0 - */ - public function getController(string $name, string $client = '', array $config = array()): BaseController - { - if ($this->input->get('task') === 'orderPosition') - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - $client = 'Administrator'; - } + /** + * Get a controller from the component + * + * @param string $name Controller name + * @param string $client Optional client (like Administrator, Site etc.) + * @param array $config Optional controller config + * + * @return \Joomla\CMS\MVC\Controller\BaseController + * + * @since 4.0.0 + */ + public function getController(string $name, string $client = '', array $config = array()): BaseController + { + if ($this->input->get('task') === 'orderPosition') { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + $client = 'Administrator'; + } - return parent::getController($name, $client, $config); - } + return parent::getController($name, $client, $config); + } } diff --git a/components/com_newsfeeds/helpers/route.php b/components/com_newsfeeds/helpers/route.php index 6ca8fecb9eae3..67a893a704d77 100644 --- a/components/com_newsfeeds/helpers/route.php +++ b/components/com_newsfeeds/helpers/route.php @@ -1,4 +1,5 @@ input->get('view', 'categories'); - $this->input->set('view', $vName); - - if ($this->app->getIdentity()->get('id') || ($this->input->getMethod() === 'POST' && $vName === 'category' )) - { - $cachable = false; - } - - $safeurlparams = array('id' => 'INT', 'limit' => 'UINT', 'limitstart' => 'UINT', - 'filter_order' => 'CMD', 'filter_order_Dir' => 'CMD', 'lang' => 'CMD'); - - return parent::display($cachable, $safeurlparams); - } + /** + * Method to show a newsfeeds view + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $cachable = true; + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'categories'); + $this->input->set('view', $vName); + + if ($this->app->getIdentity()->get('id') || ($this->input->getMethod() === 'POST' && $vName === 'category' )) { + $cachable = false; + } + + $safeurlparams = array('id' => 'INT', 'limit' => 'UINT', 'limitstart' => 'UINT', + 'filter_order' => 'CMD', 'filter_order_Dir' => 'CMD', 'lang' => 'CMD'); + + return parent::display($cachable, $safeurlparams); + } } diff --git a/components/com_newsfeeds/src/Helper/AssociationHelper.php b/components/com_newsfeeds/src/Helper/AssociationHelper.php index c751582630221..612c1a209c081 100644 --- a/components/com_newsfeeds/src/Helper/AssociationHelper.php +++ b/components/com_newsfeeds/src/Helper/AssociationHelper.php @@ -1,4 +1,5 @@ input; - $view = $view ?? $jinput->get('view'); - $id = empty($id) ? $jinput->getInt('id') : $id; - - if ($view === 'newsfeed') - { - if ($id) - { - $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', $id); - - $return = array(); - - foreach ($associations as $tag => $item) - { - $return[$tag] = RouteHelper::getNewsfeedRoute($item->id, (int) $item->catid, $item->language); - } - - return $return; - } - } - - if ($view === 'category' || $view === 'categories') - { - return self::getCategoryAssociations($id, 'com_newsfeeds'); - } - - return array(); - } + /** + * Method to get the associations for a given item + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 3.0 + */ + public static function getAssociations($id = 0, $view = null) + { + $jinput = Factory::getApplication()->input; + $view = $view ?? $jinput->get('view'); + $id = empty($id) ? $jinput->getInt('id') : $id; + + if ($view === 'newsfeed') { + if ($id) { + $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', $id); + + $return = array(); + + foreach ($associations as $tag => $item) { + $return[$tag] = RouteHelper::getNewsfeedRoute($item->id, (int) $item->catid, $item->language); + } + + return $return; + } + } + + if ($view === 'category' || $view === 'categories') { + return self::getCategoryAssociations($id, 'com_newsfeeds'); + } + + return array(); + } } diff --git a/components/com_newsfeeds/src/Helper/RouteHelper.php b/components/com_newsfeeds/src/Helper/RouteHelper.php index 6f3be804299f0..ca74df78e616a 100644 --- a/components/com_newsfeeds/src/Helper/RouteHelper.php +++ b/components/com_newsfeeds/src/Helper/RouteHelper.php @@ -1,4 +1,5 @@ 1) - { - $link .= '&catid=' . $catid; - } + if ((int) $catid > 1) { + $link .= '&catid=' . $catid; + } - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } - return $link; - } + return $link; + } - /** - * getCategoryRoute - * - * @param int $catid category id - * @param int $language language - * - * @return string - */ - public static function getCategoryRoute($catid, $language = 0) - { - if ($catid instanceof CategoryNode) - { - $id = $catid->id; - } - else - { - $id = (int) $catid; - } + /** + * getCategoryRoute + * + * @param int $catid category id + * @param int $language language + * + * @return string + */ + public static function getCategoryRoute($catid, $language = 0) + { + if ($catid instanceof CategoryNode) { + $id = $catid->id; + } else { + $id = (int) $catid; + } - if ($id < 1) - { - $link = ''; - } - else - { - // Create the link - $link = 'index.php?option=com_newsfeeds&view=category&id=' . $id; + if ($id < 1) { + $link = ''; + } else { + // Create the link + $link = 'index.php?option=com_newsfeeds&view=category&id=' . $id; - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } - } + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } + } - return $link; - } + return $link; + } } diff --git a/components/com_newsfeeds/src/Model/CategoriesModel.php b/components/com_newsfeeds/src/Model/CategoriesModel.php index 964ab8b39b618..756bf4751bb42 100644 --- a/components/com_newsfeeds/src/Model/CategoriesModel.php +++ b/components/com_newsfeeds/src/Model/CategoriesModel.php @@ -1,4 +1,5 @@ setState('filter.extension', $this->_extension); - - // Get the parent id if defined. - $parentId = $app->input->getInt('id'); - $this->setState('filter.parentId', $parentId); - - $params = $app->getParams(); - $this->setState('params', $params); - - $this->setState('filter.published', 1); - $this->setState('filter.access', true); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.extension'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.parentId'); - - return parent::getStoreId($id); - } - - /** - * redefine the function and add some properties to make the styling easier - * - * @return mixed An array of data items on success, false on failure. - */ - public function getItems() - { - if ($this->_items === null) - { - $app = Factory::getApplication(); - $menu = $app->getMenu(); - $active = $menu->getActive(); - - if ($active) - { - $params = $active->getParams(); - } - else - { - $params = new Registry; - } - - $options = array(); - $options['countItems'] = $params->get('show_cat_items_cat', 1) || !$params->get('show_empty_categories_cat', 0); - $categories = Categories::getInstance('Newsfeeds', $options); - $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); - - if (is_object($this->_parent)) - { - $this->_items = $this->_parent->getChildren(); - } - else - { - $this->_items = false; - } - } - - return $this->_items; - } - - /** - * get the Parent - * - * @return null - */ - public function getParent() - { - if (!is_object($this->_parent)) - { - $this->getItems(); - } - - return $this->_parent; - } + /** + * Model context string. + * + * @var string + */ + public $_context = 'com_newsfeeds.categories'; + + /** + * The category context (allows other extensions to derived from this model). + * + * @var string + */ + protected $_extension = 'com_newsfeeds'; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + private $_parent = null; + + /** + * Array of child-categories + * + * @var CategoryNode[]|null + */ + private $_items = null; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field + * @param string $direction An optional direction [asc|desc] + * + * @return void + * + * @throws \Exception + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $this->setState('filter.extension', $this->_extension); + + // Get the parent id if defined. + $parentId = $app->input->getInt('id'); + $this->setState('filter.parentId', $parentId); + + $params = $app->getParams(); + $this->setState('params', $params); + + $this->setState('filter.published', 1); + $this->setState('filter.access', true); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.extension'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.parentId'); + + return parent::getStoreId($id); + } + + /** + * redefine the function and add some properties to make the styling easier + * + * @return mixed An array of data items on success, false on failure. + */ + public function getItems() + { + if ($this->_items === null) { + $app = Factory::getApplication(); + $menu = $app->getMenu(); + $active = $menu->getActive(); + + if ($active) { + $params = $active->getParams(); + } else { + $params = new Registry(); + } + + $options = array(); + $options['countItems'] = $params->get('show_cat_items_cat', 1) || !$params->get('show_empty_categories_cat', 0); + $categories = Categories::getInstance('Newsfeeds', $options); + $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); + + if (is_object($this->_parent)) { + $this->_items = $this->_parent->getChildren(); + } else { + $this->_items = false; + } + } + + return $this->_items; + } + + /** + * get the Parent + * + * @return null + */ + public function getParent() + { + if (!is_object($this->_parent)) { + $this->getItems(); + } + + return $this->_parent; + } } diff --git a/components/com_newsfeeds/src/Model/CategoryModel.php b/components/com_newsfeeds/src/Model/CategoryModel.php index 6ba828b2d4cd0..f21ac1e29ca6f 100644 --- a/components/com_newsfeeds/src/Model/CategoryModel.php +++ b/components/com_newsfeeds/src/Model/CategoryModel.php @@ -1,4 +1,5 @@ _params)) - { - $item->params = new Registry($item->params); - } - - // Some contexts may not use tags data at all, so we allow callers to disable loading tag data - if ($this->getState('load_tags', true)) - { - $item->tags = new TagsHelper; - $taggedItems[$item->id] = $item; - } - } - - // Load tags of all items. - if ($taggedItems) - { - $tagsHelper = new TagsHelper; - $itemIds = \array_keys($taggedItems); - - foreach ($tagsHelper->getMultipleItemTags('com_newsfeeds.newsfeed', $itemIds) as $id => $tags) - { - $taggedItems[$id]->tags->itemTags = $tags; - } - } - - return $items; - } - - /** - * Method to build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - // Create a new query object. - $db = $this->getDatabase(); - - /** @var \Joomla\Database\DatabaseQuery $query */ - $query = $db->getQuery(true); - - // Select required fields from the categories. - $query->select($this->getState('list.select', $db->quoteName('a') . '.*')) - ->from($db->quoteName('#__newsfeeds', 'a')) - ->whereIn($db->quoteName('a.access'), $groups); - - // Filter by category. - if ($categoryId = (int) $this->getState('category.id')) - { - $query->where($db->quoteName('a.catid') . ' = :categoryId') - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) - ->whereIn($db->quoteName('c.access'), $groups) - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - - // Filter by state - $state = $this->getState('filter.published'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.published') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - else - { - $query->where($db->quoteName('a.published') . ' IN (0,1,2)'); - } - - // Filter by start and end dates. - if ($this->getState('filter.publish_date')) - { - $nowDate = Factory::getDate()->toSql(); - - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_up') . ' IS NULL', - $db->quoteName('a.publish_up') . ' <= :nowDate1', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_down') . ' IS NULL', - $db->quoteName('a.publish_down') . ' >= :nowDate2', - ], - 'OR' - ) - ->bind([':nowDate1', ':nowDate2'], $nowDate); - } - - // Filter by search in title - if ($search = $this->getState('list.filter')) - { - $search = '%' . $search . '%'; - $query->where($db->quoteName('a.name') . ' LIKE :search') - ->bind(':search', $search); - } - - // Filter by language - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getApplication()->getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field - * @param string $direction An optional direction [asc|desc] - * - * @return void - * - * @since 1.6 - * - * @throws \Exception - */ - protected function populateState($ordering = null, $direction = null) - { - $app = Factory::getApplication(); - $params = ComponentHelper::getParams('com_newsfeeds'); - - // List state information - $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); - $this->setState('list.limit', $limit); - - $limitstart = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $limitstart); - - // Optional filter text - $this->setState('list.filter', $app->input->getString('filter-search')); - - $orderCol = $app->input->get('filter_order', 'ordering'); - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'ordering'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->input->get('filter_order_Dir', 'ASC'); - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $id = $app->input->get('id', 0, 'int'); - $this->setState('category.id', $id); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_newsfeeds')) && (!$user->authorise('core.edit', 'com_newsfeeds'))) - { - // Limit to published for people who can't edit or edit.state. - $this->setState('filter.published', 1); - - // Filter by start and end dates. - $this->setState('filter.publish_date', true); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to get category data for the current category - * - * @return object - * - * @since 1.5 - */ - public function getCategory() - { - if (!is_object($this->_item)) - { - $app = Factory::getApplication(); - $menu = $app->getMenu(); - $active = $menu->getActive(); - - if ($active) - { - $params = $active->getParams(); - } - else - { - $params = new Registry; - } - - $options = array(); - $options['countItems'] = $params->get('show_cat_items', 1) || $params->get('show_empty_categories', 0); - $categories = Categories::getInstance('Newsfeeds', $options); - $this->_item = $categories->get($this->getState('category.id', 'root')); - - if (is_object($this->_item)) - { - $this->_children = $this->_item->getChildren(); - $this->_parent = false; - - if ($this->_item->getParent()) - { - $this->_parent = $this->_item->getParent(); - } - - $this->_rightsibling = $this->_item->getSibling(); - $this->_leftsibling = $this->_item->getSibling(false); - } - else - { - $this->_children = false; - $this->_parent = false; - } - } - - return $this->_item; - } - - /** - * Get the parent category. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function getParent() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_parent; - } - - /** - * Get the sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getLeftSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_leftsibling; - } - - /** - * Get the sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getRightSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_rightsibling; - } - - /** - * Get the child categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getChildren() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_children; - } - - /** - * Increment the hit counter for the category. - * - * @param int $pk Optional primary key of the category to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); - $table = Table::getInstance('Category', 'JTable'); - $table->hit($pk); - } - - return true; - } + /** + * Category items data + * + * @var array + */ + protected $_item; + + /** + * Array of newsfeeds in the category + * + * @var \stdClass[] + */ + protected $_articles; + + /** + * Category left and right of this one + * + * @var CategoryNode[]|null + */ + protected $_siblings; + + /** + * Array of child-categories + * + * @var CategoryNode[]|null + */ + protected $_children; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + protected $_parent; + + /** + * The category that applies. + * + * @var object + */ + protected $_category; + + /** + * The list of other newsfeed categories. + * + * @var array + */ + protected $_categories; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'numarticles', 'a.numarticles', + 'link', 'a.link', + 'ordering', 'a.ordering', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to get a list of items. + * + * @return mixed An array of objects on success, false on failure. + */ + public function getItems() + { + // Invoke the parent getItems method to get the main list + $items = parent::getItems(); + + $taggedItems = []; + + // Convert the params field into an object, saving original in _params + foreach ($items as $item) { + if (!isset($this->_params)) { + $item->params = new Registry($item->params); + } + + // Some contexts may not use tags data at all, so we allow callers to disable loading tag data + if ($this->getState('load_tags', true)) { + $item->tags = new TagsHelper(); + $taggedItems[$item->id] = $item; + } + } + + // Load tags of all items. + if ($taggedItems) { + $tagsHelper = new TagsHelper(); + $itemIds = \array_keys($taggedItems); + + foreach ($tagsHelper->getMultipleItemTags('com_newsfeeds.newsfeed', $itemIds) as $id => $tags) { + $taggedItems[$id]->tags->itemTags = $tags; + } + } + + return $items; + } + + /** + * Method to build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + // Create a new query object. + $db = $this->getDatabase(); + + /** @var \Joomla\Database\DatabaseQuery $query */ + $query = $db->getQuery(true); + + // Select required fields from the categories. + $query->select($this->getState('list.select', $db->quoteName('a') . '.*')) + ->from($db->quoteName('#__newsfeeds', 'a')) + ->whereIn($db->quoteName('a.access'), $groups); + + // Filter by category. + if ($categoryId = (int) $this->getState('category.id')) { + $query->where($db->quoteName('a.catid') . ' = :categoryId') + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) + ->whereIn($db->quoteName('c.access'), $groups) + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + + // Filter by state + $state = $this->getState('filter.published'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.published') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } else { + $query->where($db->quoteName('a.published') . ' IN (0,1,2)'); + } + + // Filter by start and end dates. + if ($this->getState('filter.publish_date')) { + $nowDate = Factory::getDate()->toSql(); + + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_up') . ' IS NULL', + $db->quoteName('a.publish_up') . ' <= :nowDate1', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_down') . ' IS NULL', + $db->quoteName('a.publish_down') . ' >= :nowDate2', + ], + 'OR' + ) + ->bind([':nowDate1', ':nowDate2'], $nowDate); + } + + // Filter by search in title + if ($search = $this->getState('list.filter')) { + $search = '%' . $search . '%'; + $query->where($db->quoteName('a.name') . ' LIKE :search') + ->bind(':search', $search); + } + + // Filter by language + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getApplication()->getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field + * @param string $direction An optional direction [asc|desc] + * + * @return void + * + * @since 1.6 + * + * @throws \Exception + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $params = ComponentHelper::getParams('com_newsfeeds'); + + // List state information + $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); + $this->setState('list.limit', $limit); + + $limitstart = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $limitstart); + + // Optional filter text + $this->setState('list.filter', $app->input->getString('filter-search')); + + $orderCol = $app->input->get('filter_order', 'ordering'); + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'ordering'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->input->get('filter_order_Dir', 'ASC'); + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $id = $app->input->get('id', 0, 'int'); + $this->setState('category.id', $id); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_newsfeeds')) && (!$user->authorise('core.edit', 'com_newsfeeds'))) { + // Limit to published for people who can't edit or edit.state. + $this->setState('filter.published', 1); + + // Filter by start and end dates. + $this->setState('filter.publish_date', true); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to get category data for the current category + * + * @return object + * + * @since 1.5 + */ + public function getCategory() + { + if (!is_object($this->_item)) { + $app = Factory::getApplication(); + $menu = $app->getMenu(); + $active = $menu->getActive(); + + if ($active) { + $params = $active->getParams(); + } else { + $params = new Registry(); + } + + $options = array(); + $options['countItems'] = $params->get('show_cat_items', 1) || $params->get('show_empty_categories', 0); + $categories = Categories::getInstance('Newsfeeds', $options); + $this->_item = $categories->get($this->getState('category.id', 'root')); + + if (is_object($this->_item)) { + $this->_children = $this->_item->getChildren(); + $this->_parent = false; + + if ($this->_item->getParent()) { + $this->_parent = $this->_item->getParent(); + } + + $this->_rightsibling = $this->_item->getSibling(); + $this->_leftsibling = $this->_item->getSibling(false); + } else { + $this->_children = false; + $this->_parent = false; + } + } + + return $this->_item; + } + + /** + * Get the parent category. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function getParent() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_parent; + } + + /** + * Get the sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getLeftSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_leftsibling; + } + + /** + * Get the sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getRightSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_rightsibling; + } + + /** + * Get the child categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getChildren() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_children; + } + + /** + * Increment the hit counter for the category. + * + * @param int $pk Optional primary key of the category to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); + $table = Table::getInstance('Category', 'JTable'); + $table->hit($pk); + } + + return true; + } } diff --git a/components/com_newsfeeds/src/Model/NewsfeedModel.php b/components/com_newsfeeds/src/Model/NewsfeedModel.php index f63e671d6a450..390830c2bb5aa 100644 --- a/components/com_newsfeeds/src/Model/NewsfeedModel.php +++ b/components/com_newsfeeds/src/Model/NewsfeedModel.php @@ -1,4 +1,5 @@ input->getInt('id'); - $this->setState('newsfeed.id', $pk); - - $offset = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.offset', $offset); - - // Load the parameters. - $params = $app->getParams(); - $this->setState('params', $params); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_newsfeeds')) && (!$user->authorise('core.edit', 'com_newsfeeds'))) - { - $this->setState('filter.published', 1); - $this->setState('filter.archived', 2); - } - } - - /** - * Method to get newsfeed data. - * - * @param integer $pk The id of the newsfeed. - * - * @return mixed Menu item data object on success, false on failure. - * - * @since 1.6 - */ - public function &getItem($pk = null) - { - $pk = (int) $pk ?: (int) $this->getState('newsfeed.id'); - - if ($this->_item === null) - { - $this->_item = array(); - } - - if (!isset($this->_item[$pk])) - { - try - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - $this->getState('item.select', $db->quoteName('a') . '.*'), - $db->quoteName('c.title', 'category_title'), - $db->quoteName('c.alias', 'category_alias'), - $db->quoteName('c.access', 'category_access'), - $db->quoteName('u.name', 'author'), - $db->quoteName('parent.title', 'parent_title'), - $db->quoteName('parent.id', 'parent_id'), - $db->quoteName('parent.path', 'parent_route'), - $db->quoteName('parent.alias', 'parent_alias'), - ] - ) - ->from($db->quoteName('#__newsfeeds', 'a')) - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by')) - ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) - ->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - // Filter by published state. - $published = $this->getState('filter.published'); - $archived = $this->getState('filter.archived'); - - if (is_numeric($published)) - { - // Filter by start and end dates. - $nowDate = Factory::getDate()->toSql(); - - $published = (int) $published; - $archived = (int) $archived; - - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.published') . ' = :published1', - $db->quoteName('a.published') . ' = :archived1', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_up') . ' IS NULL', - $db->quoteName('a.publish_up') . ' <= :nowDate1', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_down') . ' IS NULL', - $db->quoteName('a.publish_down') . ' >= :nowDate2', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('c.published') . ' = :published2', - $db->quoteName('c.published') . ' = :archived2', - ], - 'OR' - ) - ->bind([':published1', ':published2'], $published, ParameterType::INTEGER) - ->bind([':archived1', ':archived2'], $archived, ParameterType::INTEGER) - ->bind([':nowDate1', ':nowDate2'], $nowDate); - } - - $db->setQuery($query); - - $data = $db->loadObject(); - - if ($data === null) - { - throw new \Exception(Text::_('COM_NEWSFEEDS_ERROR_FEED_NOT_FOUND'), 404); - } - - // Check for published state if filter set. - - if ((is_numeric($published) || is_numeric($archived)) && $data->published != $published && $data->published != $archived) - { - throw new \Exception(Text::_('COM_NEWSFEEDS_ERROR_FEED_NOT_FOUND'), 404); - } - - // Convert parameter fields to objects. - $registry = new Registry($data->params); - $data->params = clone $this->getState('params'); - $data->params->merge($registry); - - $data->metadata = new Registry($data->metadata); - - // Compute access permissions. - - if ($access = $this->getState('filter.access')) - { - // If the access filter has been set, we already know this user can view. - $data->params->set('access-view', true); - } - else - { - // If no access filter is set, the layout takes some responsibility for display of limited information. - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); - } - - $this->_item[$pk] = $data; - } - catch (\Exception $e) - { - $this->setError($e); - $this->_item[$pk] = false; - } - } - - return $this->_item[$pk]; - } - - /** - * Increment the hit counter for the newsfeed. - * - * @param int $pk Optional primary key of the item to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - * - * @since 3.0 - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('newsfeed.id'); - - $table = $this->getTable('Newsfeed', 'Administrator'); - $table->hit($pk); - } - - return true; - } + /** + * Model context string. + * + * @var string + * @since 1.6 + */ + protected $_context = 'com_newsfeeds.newsfeed'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load state from the request. + $pk = $app->input->getInt('id'); + $this->setState('newsfeed.id', $pk); + + $offset = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.offset', $offset); + + // Load the parameters. + $params = $app->getParams(); + $this->setState('params', $params); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_newsfeeds')) && (!$user->authorise('core.edit', 'com_newsfeeds'))) { + $this->setState('filter.published', 1); + $this->setState('filter.archived', 2); + } + } + + /** + * Method to get newsfeed data. + * + * @param integer $pk The id of the newsfeed. + * + * @return mixed Menu item data object on success, false on failure. + * + * @since 1.6 + */ + public function &getItem($pk = null) + { + $pk = (int) $pk ?: (int) $this->getState('newsfeed.id'); + + if ($this->_item === null) { + $this->_item = array(); + } + + if (!isset($this->_item[$pk])) { + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $this->getState('item.select', $db->quoteName('a') . '.*'), + $db->quoteName('c.title', 'category_title'), + $db->quoteName('c.alias', 'category_alias'), + $db->quoteName('c.access', 'category_access'), + $db->quoteName('u.name', 'author'), + $db->quoteName('parent.title', 'parent_title'), + $db->quoteName('parent.id', 'parent_id'), + $db->quoteName('parent.path', 'parent_route'), + $db->quoteName('parent.alias', 'parent_alias'), + ] + ) + ->from($db->quoteName('#__newsfeeds', 'a')) + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by')) + ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) + ->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + // Filter by published state. + $published = $this->getState('filter.published'); + $archived = $this->getState('filter.archived'); + + if (is_numeric($published)) { + // Filter by start and end dates. + $nowDate = Factory::getDate()->toSql(); + + $published = (int) $published; + $archived = (int) $archived; + + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.published') . ' = :published1', + $db->quoteName('a.published') . ' = :archived1', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_up') . ' IS NULL', + $db->quoteName('a.publish_up') . ' <= :nowDate1', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_down') . ' IS NULL', + $db->quoteName('a.publish_down') . ' >= :nowDate2', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('c.published') . ' = :published2', + $db->quoteName('c.published') . ' = :archived2', + ], + 'OR' + ) + ->bind([':published1', ':published2'], $published, ParameterType::INTEGER) + ->bind([':archived1', ':archived2'], $archived, ParameterType::INTEGER) + ->bind([':nowDate1', ':nowDate2'], $nowDate); + } + + $db->setQuery($query); + + $data = $db->loadObject(); + + if ($data === null) { + throw new \Exception(Text::_('COM_NEWSFEEDS_ERROR_FEED_NOT_FOUND'), 404); + } + + // Check for published state if filter set. + + if ((is_numeric($published) || is_numeric($archived)) && $data->published != $published && $data->published != $archived) { + throw new \Exception(Text::_('COM_NEWSFEEDS_ERROR_FEED_NOT_FOUND'), 404); + } + + // Convert parameter fields to objects. + $registry = new Registry($data->params); + $data->params = clone $this->getState('params'); + $data->params->merge($registry); + + $data->metadata = new Registry($data->metadata); + + // Compute access permissions. + + if ($access = $this->getState('filter.access')) { + // If the access filter has been set, we already know this user can view. + $data->params->set('access-view', true); + } else { + // If no access filter is set, the layout takes some responsibility for display of limited information. + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); + } + + $this->_item[$pk] = $data; + } catch (\Exception $e) { + $this->setError($e); + $this->_item[$pk] = false; + } + } + + return $this->_item[$pk]; + } + + /** + * Increment the hit counter for the newsfeed. + * + * @param int $pk Optional primary key of the item to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + * + * @since 3.0 + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('newsfeed.id'); + + $table = $this->getTable('Newsfeed', 'Administrator'); + $table->hit($pk); + } + + return true; + } } diff --git a/components/com_newsfeeds/src/Service/Category.php b/components/com_newsfeeds/src/Service/Category.php index 2c2ae1c0a1b04..04ca3380ea5fc 100644 --- a/components/com_newsfeeds/src/Service/Category.php +++ b/components/com_newsfeeds/src/Service/Category.php @@ -1,4 +1,5 @@ categoryFactory = $categoryFactory; - $this->db = $db; - - $params = ComponentHelper::getParams('com_newsfeeds'); - $this->noIDs = (bool) $params->get('sef_ids'); - $categories = new RouterViewConfiguration('categories'); - $categories->setKey('id'); - $this->registerView($categories); - $category = new RouterViewConfiguration('category'); - $category->setKey('id')->setParent($categories, 'catid')->setNestable(); - $this->registerView($category); - $newsfeed = new RouterViewConfiguration('newsfeed'); - $newsfeed->setKey('id')->setParent($category, 'catid'); - $this->registerView($newsfeed); - - parent::__construct($app, $menu); - - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategorySegment($id, $query) - { - $category = $this->getCategories()->get($id); - - if ($category) - { - $path = array_reverse($category->getPath(), true); - $path[0] = '1:root'; - - if ($this->noIDs) - { - foreach ($path as &$segment) - { - list($id, $segment) = explode(':', $segment, 2); - } - } - - return $path; - } - - return array(); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategoriesSegment($id, $query) - { - return $this->getCategorySegment($id, $query); - } - - /** - * Method to get the segment(s) for a newsfeed - * - * @param string $id ID of the newsfeed to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getNewsfeedSegment($id, $query) - { - if (!strpos($id, ':')) - { - $id = (int) $id; - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('alias')) - ->from($this->db->quoteName('#__newsfeeds')) - ->where($this->db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - $id .= ':' . $this->db->loadResult(); - } - - if ($this->noIDs) - { - list($void, $segment) = explode(':', $id, 2); - - return array($void => $segment); - } - - return array((int) $id => $id); - } - - /** - * Method to get the id for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoryId($segment, $query) - { - if (isset($query['id'])) - { - $category = $this->getCategories(['access' => false])->get($query['id']); - - if ($category) - { - foreach ($category->getChildren() as $child) - { - if ($this->noIDs) - { - if ($child->alias === $segment) - { - return $child->id; - } - } - else - { - if ($child->id == (int) $segment) - { - return $child->id; - } - } - } - } - } - - return false; - } - - /** - * Method to get the segment(s) for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoriesId($segment, $query) - { - return $this->getCategoryId($segment, $query); - } - - /** - * Method to get the segment(s) for a newsfeed - * - * @param string $segment Segment of the newsfeed to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getNewsfeedId($segment, $query) - { - if ($this->noIDs) - { - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('id')) - ->from($this->db->quoteName('#__newsfeeds')) - ->where( - [ - $this->db->quoteName('alias') . ' = :segment', - $this->db->quoteName('catid') . ' = :id', - ] - ) - ->bind(':segment', $segment) - ->bind(':id', $query['id'], ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - return (int) $this->db->loadResult(); - } - - return (int) $segment; - } - - /** - * Method to get categories from cache - * - * @param array $options The options for retrieving categories - * - * @return CategoryInterface The object containing categories - * - * @since 4.0.0 - */ - private function getCategories(array $options = []): CategoryInterface - { - $key = serialize($options); - - if (!isset($this->categoryCache[$key])) - { - $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); - } - - return $this->categoryCache[$key]; - } + /** + * Flag to remove IDs + * + * @var boolean + */ + protected $noIDs = false; + + /** + * The category factory + * + * @var CategoryFactoryInterface + * + * @since 4.0.0 + */ + private $categoryFactory; + + /** + * The category cache + * + * @var array + * + * @since 4.0.0 + */ + private $categoryCache = []; + + /** + * The db + * + * @var DatabaseInterface + * + * @since 4.0.0 + */ + private $db; + + /** + * Newsfeeds Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + * @param CategoryFactoryInterface $categoryFactory The category object + * @param DatabaseInterface $db The database object + */ + public function __construct(SiteApplication $app, AbstractMenu $menu, CategoryFactoryInterface $categoryFactory, DatabaseInterface $db) + { + $this->categoryFactory = $categoryFactory; + $this->db = $db; + + $params = ComponentHelper::getParams('com_newsfeeds'); + $this->noIDs = (bool) $params->get('sef_ids'); + $categories = new RouterViewConfiguration('categories'); + $categories->setKey('id'); + $this->registerView($categories); + $category = new RouterViewConfiguration('category'); + $category->setKey('id')->setParent($categories, 'catid')->setNestable(); + $this->registerView($category); + $newsfeed = new RouterViewConfiguration('newsfeed'); + $newsfeed->setKey('id')->setParent($category, 'catid'); + $this->registerView($newsfeed); + + parent::__construct($app, $menu); + + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategorySegment($id, $query) + { + $category = $this->getCategories()->get($id); + + if ($category) { + $path = array_reverse($category->getPath(), true); + $path[0] = '1:root'; + + if ($this->noIDs) { + foreach ($path as &$segment) { + list($id, $segment) = explode(':', $segment, 2); + } + } + + return $path; + } + + return array(); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategoriesSegment($id, $query) + { + return $this->getCategorySegment($id, $query); + } + + /** + * Method to get the segment(s) for a newsfeed + * + * @param string $id ID of the newsfeed to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getNewsfeedSegment($id, $query) + { + if (!strpos($id, ':')) { + $id = (int) $id; + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('alias')) + ->from($this->db->quoteName('#__newsfeeds')) + ->where($this->db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + $id .= ':' . $this->db->loadResult(); + } + + if ($this->noIDs) { + list($void, $segment) = explode(':', $id, 2); + + return array($void => $segment); + } + + return array((int) $id => $id); + } + + /** + * Method to get the id for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoryId($segment, $query) + { + if (isset($query['id'])) { + $category = $this->getCategories(['access' => false])->get($query['id']); + + if ($category) { + foreach ($category->getChildren() as $child) { + if ($this->noIDs) { + if ($child->alias === $segment) { + return $child->id; + } + } else { + if ($child->id == (int) $segment) { + return $child->id; + } + } + } + } + } + + return false; + } + + /** + * Method to get the segment(s) for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoriesId($segment, $query) + { + return $this->getCategoryId($segment, $query); + } + + /** + * Method to get the segment(s) for a newsfeed + * + * @param string $segment Segment of the newsfeed to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getNewsfeedId($segment, $query) + { + if ($this->noIDs) { + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__newsfeeds')) + ->where( + [ + $this->db->quoteName('alias') . ' = :segment', + $this->db->quoteName('catid') . ' = :id', + ] + ) + ->bind(':segment', $segment) + ->bind(':id', $query['id'], ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + return (int) $this->db->loadResult(); + } + + return (int) $segment; + } + + /** + * Method to get categories from cache + * + * @param array $options The options for retrieving categories + * + * @return CategoryInterface The object containing categories + * + * @since 4.0.0 + */ + private function getCategories(array $options = []): CategoryInterface + { + $key = serialize($options); + + if (!isset($this->categoryCache[$key])) { + $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); + } + + return $this->categoryCache[$key]; + } } diff --git a/components/com_newsfeeds/src/View/Categories/HtmlView.php b/components/com_newsfeeds/src/View/Categories/HtmlView.php index e8076475251c2..5ba4144307754 100644 --- a/components/com_newsfeeds/src/View/Categories/HtmlView.php +++ b/components/com_newsfeeds/src/View/Categories/HtmlView.php @@ -1,4 +1,5 @@ commonCategoryDisplay(); - - // Flag indicates to not add limitstart=0 to URL - $this->pagination->hideEmptyLimitstart = true; - - // Prepare the data. - // Compute the newsfeed slug. - foreach ($this->items as $item) - { - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - $temp = $item->params; - $item->params = clone $this->params; - $item->params->merge($temp); - } - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function prepareDocument() - { - parent::prepareDocument(); - - $menu = $this->menu; - $id = (int) @$menu->query['id']; - - if ($menu && (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' - || $id != $this->category->id)) - { - $path = array(array('title' => $this->category->title, 'link' => '')); - $category = $this->category->getParent(); - - while ((!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' - || $id != $category->id) && $category->id > 1) - { - $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $this->pathway->addItem($item['title'], $item['link']); - } - } - } + /** + * @var string Default title to use for page title + * @since 3.2 + */ + protected $defaultPageTitle = 'COM_NEWSFEEDS_DEFAULT_PAGE_TITLE'; + + /** + * @var string The name of the extension for the category + * @since 3.2 + */ + protected $extension = 'com_newsfeeds'; + + /** + * @var string The name of the view to link individual items to + * @since 3.2 + */ + protected $viewName = 'newsfeed'; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->commonCategoryDisplay(); + + // Flag indicates to not add limitstart=0 to URL + $this->pagination->hideEmptyLimitstart = true; + + // Prepare the data. + // Compute the newsfeed slug. + foreach ($this->items as $item) { + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + $temp = $item->params; + $item->params = clone $this->params; + $item->params->merge($temp); + } + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function prepareDocument() + { + parent::prepareDocument(); + + $menu = $this->menu; + $id = (int) @$menu->query['id']; + + if ( + $menu && (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' + || $id != $this->category->id) + ) { + $path = array(array('title' => $this->category->title, 'link' => '')); + $category = $this->category->getParent(); + + while ( + (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' + || $id != $category->id) && $category->id > 1 + ) { + $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $this->pathway->addItem($item['title'], $item['link']); + } + } + } } diff --git a/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php b/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php index 92675cc012365..e7762100ebc59 100644 --- a/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php +++ b/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser(); - - // Get view related request variables. - $print = $app->input->getBool('print'); - - // Get model data. - $state = $this->get('State'); - $item = $this->get('Item'); - - // Check for errors. - // @TODO: Maybe this could go into ComponentHelper::raiseErrors($this->get('Errors')) - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Add router helpers. - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - $item->catslug = $item->category_alias ? ($item->catid . ':' . $item->category_alias) : $item->catid; - $item->parent_slug = $item->category_alias ? ($item->parent_id . ':' . $item->parent_alias) : $item->parent_id; - - // Merge newsfeed params. If this is single-newsfeed view, menu params override newsfeed params - // Otherwise, newsfeed params override menu item params - $params = $state->get('params'); - $newsfeed_params = clone $item->params; - $active = $app->getMenu()->getActive(); - $temp = clone $params; - - // Check to see which parameters should take priority - if ($active) - { - $currentLink = $active->link; - - // If the current view is the active item and a newsfeed view for this feed, then the menu item params take priority - if (strpos($currentLink, 'view=newsfeed') && strpos($currentLink, '&id=' . (string) $item->id)) - { - // $item->params are the newsfeed params, $temp are the menu item params - // Merge so that the menu item params take priority - $newsfeed_params->merge($temp); - $item->params = $newsfeed_params; - - // Load layout from active query (in case it is an alternative menu item) - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - } - else - { - // Current view is not a single newsfeed, so the newsfeed params take priority here - // Merge the menu item params with the newsfeed params so that the newsfeed params take priority - $temp->merge($newsfeed_params); - $item->params = $temp; - - // Check for alternative layouts (since we are not in a single-newsfeed menu item) - if ($layout = $item->params->get('newsfeed_layout')) - { - $this->setLayout($layout); - } - } - } - else - { - // Merge so that newsfeed params take priority - $temp->merge($newsfeed_params); - $item->params = $temp; - - // Check for alternative layouts (since we are not in a single-newsfeed menu item) - if ($layout = $item->params->get('newsfeed_layout')) - { - $this->setLayout($layout); - } - } - - // Check the access to the newsfeed - $levels = $user->getAuthorisedViewLevels(); - - if (!in_array($item->access, $levels) || (in_array($item->access, $levels) && (!in_array($item->category_access, $levels)))) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return; - } - - // Get the current menu item - $params = $app->getParams(); - - $params->merge($item->params); - - try - { - $feed = new FeedFactory; - $this->rssDoc = $feed->getFeed($item->link); - } - catch (\InvalidArgumentException $e) - { - $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); - } - catch (\RuntimeException $e) - { - $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); - } - - if (empty($this->rssDoc)) - { - $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); - } - - $feed_display_order = $params->get('feed_display_order', 'des'); - - if ($feed_display_order === 'asc') - { - $this->rssDoc->reverseItems(); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - - $this->params = $params; - $this->state = $state; - $this->item = $item; - $this->user = $user; - - if (!empty($msg)) - { - $this->msg = $msg; - } - - $this->print = $print; - - $item->tags = new TagsHelper; - $item->tags->getItemTags('com_newsfeeds.newsfeed', $item->id); - - // Increment the hit counter of the newsfeed. - $model = $this->getModel(); - $model->hit(); - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @since 1.6 - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - $pathway = $app->getPathway(); - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_NEWSFEEDS_DEFAULT_PAGE_TITLE')); - } - - $title = $this->params->get('page_title', ''); - - $id = (int) @$menu->query['id']; - - // If the menu item does not concern this newsfeed - if ($menu && (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] !== 'newsfeed' - || $id != $this->item->id)) - { - // If this is not a single newsfeed menu item, set the page title to the newsfeed title - if ($this->item->name) - { - $title = $this->item->name; - } - - $path = array(array('title' => $this->item->name, 'link' => '')); - $category = Categories::getInstance('Newsfeeds')->get($this->item->catid); - - while ((!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' - || $id != $category->id) && $category->id > 1) - { - $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id)); - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $pathway->addItem($item['title'], $item['link']); - } - } - - if (empty($title)) - { - $title = $this->item->name; - } - - $this->setDocumentTitle($title); - - if ($this->item->metadesc) - { - $this->document->setDescription($this->item->metadesc); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - if ($app->get('MetaAuthor') == '1') - { - $this->document->setMetaData('author', $this->item->author); - } - - $mdata = $this->item->metadata->toArray(); - - foreach ($mdata as $k => $v) - { - if ($v) - { - $this->document->setMetaData($k, $v); - } - } - } + /** + * The model state + * + * @var object + * + * @since 1.6 + */ + protected $state; + + /** + * The newsfeed item + * + * @var object + * + * @since 1.6 + */ + protected $item; + + /** + * UNUSED? + * + * @var boolean + * + * @since 1.6 + */ + protected $print; + + /** + * The current user instance + * + * @var \Joomla\CMS\User\User|null + * + * @since 4.0.0 + */ + protected $user = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $user = $this->getCurrentUser(); + + // Get view related request variables. + $print = $app->input->getBool('print'); + + // Get model data. + $state = $this->get('State'); + $item = $this->get('Item'); + + // Check for errors. + // @TODO: Maybe this could go into ComponentHelper::raiseErrors($this->get('Errors')) + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Add router helpers. + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + $item->catslug = $item->category_alias ? ($item->catid . ':' . $item->category_alias) : $item->catid; + $item->parent_slug = $item->category_alias ? ($item->parent_id . ':' . $item->parent_alias) : $item->parent_id; + + // Merge newsfeed params. If this is single-newsfeed view, menu params override newsfeed params + // Otherwise, newsfeed params override menu item params + $params = $state->get('params'); + $newsfeed_params = clone $item->params; + $active = $app->getMenu()->getActive(); + $temp = clone $params; + + // Check to see which parameters should take priority + if ($active) { + $currentLink = $active->link; + + // If the current view is the active item and a newsfeed view for this feed, then the menu item params take priority + if (strpos($currentLink, 'view=newsfeed') && strpos($currentLink, '&id=' . (string) $item->id)) { + // $item->params are the newsfeed params, $temp are the menu item params + // Merge so that the menu item params take priority + $newsfeed_params->merge($temp); + $item->params = $newsfeed_params; + + // Load layout from active query (in case it is an alternative menu item) + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + } else { + // Current view is not a single newsfeed, so the newsfeed params take priority here + // Merge the menu item params with the newsfeed params so that the newsfeed params take priority + $temp->merge($newsfeed_params); + $item->params = $temp; + + // Check for alternative layouts (since we are not in a single-newsfeed menu item) + if ($layout = $item->params->get('newsfeed_layout')) { + $this->setLayout($layout); + } + } + } else { + // Merge so that newsfeed params take priority + $temp->merge($newsfeed_params); + $item->params = $temp; + + // Check for alternative layouts (since we are not in a single-newsfeed menu item) + if ($layout = $item->params->get('newsfeed_layout')) { + $this->setLayout($layout); + } + } + + // Check the access to the newsfeed + $levels = $user->getAuthorisedViewLevels(); + + if (!in_array($item->access, $levels) || (in_array($item->access, $levels) && (!in_array($item->category_access, $levels)))) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return; + } + + // Get the current menu item + $params = $app->getParams(); + + $params->merge($item->params); + + try { + $feed = new FeedFactory(); + $this->rssDoc = $feed->getFeed($item->link); + } catch (\InvalidArgumentException $e) { + $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); + } catch (\RuntimeException $e) { + $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); + } + + if (empty($this->rssDoc)) { + $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); + } + + $feed_display_order = $params->get('feed_display_order', 'des'); + + if ($feed_display_order === 'asc') { + $this->rssDoc->reverseItems(); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + + $this->params = $params; + $this->state = $state; + $this->item = $item; + $this->user = $user; + + if (!empty($msg)) { + $this->msg = $msg; + } + + $this->print = $print; + + $item->tags = new TagsHelper(); + $item->tags->getItemTags('com_newsfeeds.newsfeed', $item->id); + + // Increment the hit counter of the newsfeed. + $model = $this->getModel(); + $model->hit(); + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @since 1.6 + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + $pathway = $app->getPathway(); + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_NEWSFEEDS_DEFAULT_PAGE_TITLE')); + } + + $title = $this->params->get('page_title', ''); + + $id = (int) @$menu->query['id']; + + // If the menu item does not concern this newsfeed + if ( + $menu && (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] !== 'newsfeed' + || $id != $this->item->id) + ) { + // If this is not a single newsfeed menu item, set the page title to the newsfeed title + if ($this->item->name) { + $title = $this->item->name; + } + + $path = array(array('title' => $this->item->name, 'link' => '')); + $category = Categories::getInstance('Newsfeeds')->get($this->item->catid); + + while ( + (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' + || $id != $category->id) && $category->id > 1 + ) { + $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id)); + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $pathway->addItem($item['title'], $item['link']); + } + } + + if (empty($title)) { + $title = $this->item->name; + } + + $this->setDocumentTitle($title); + + if ($this->item->metadesc) { + $this->document->setDescription($this->item->metadesc); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + if ($app->get('MetaAuthor') == '1') { + $this->document->setMetaData('author', $this->item->author); + } + + $mdata = $this->item->metadata->toArray(); + + foreach ($mdata as $k => $v) { + if ($v) { + $this->document->setMetaData($k, $v); + } + } + } } diff --git a/components/com_newsfeeds/tmpl/categories/default.php b/components/com_newsfeeds/tmpl/categories/default.php index 1a90ac23d72b0..3335f6061d218 100644 --- a/components/com_newsfeeds/tmpl/categories/default.php +++ b/components/com_newsfeeds/tmpl/categories/default.php @@ -1,4 +1,5 @@
    - - loadTemplate('items'); ?> + + loadTemplate('items'); ?>
    diff --git a/components/com_newsfeeds/tmpl/categories/default_items.php b/components/com_newsfeeds/tmpl/categories/default_items.php index a762475053450..0b3d6aff68a7f 100644 --- a/components/com_newsfeeds/tmpl/categories/default_items.php +++ b/components/com_newsfeeds/tmpl/categories/default_items.php @@ -1,4 +1,5 @@ maxLevelcat != 0 && count($this->items[$this->parent->id]) > 0) : ?> - items[$this->parent->id] as $id => $item) : ?> - params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> -
    - - params->get('show_subcat_desc_cat') == 1) : ?> - description) : ?> -
    - description, '', 'com_newsfeeds.categories'); ?> -
    - - - getChildren()) > 0 && $this->maxLevelcat > 1) : ?> -
    - items[$item->id] = $item->getChildren(); ?> - parent = $item; ?> - maxLevelcat--; ?> - loadTemplate('items'); ?> - parent = $item->getParent(); ?> - maxLevelcat++; ?> -
    - -
    - - + items[$this->parent->id] as $id => $item) : ?> + params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> +
    + + params->get('show_subcat_desc_cat') == 1) : ?> + description) : ?> +
    + description, '', 'com_newsfeeds.categories'); ?> +
    + + + getChildren()) > 0 && $this->maxLevelcat > 1) : ?> +
    + items[$item->id] = $item->getChildren(); ?> + parent = $item; ?> + maxLevelcat--; ?> + loadTemplate('items'); ?> + parent = $item->getParent(); ?> + maxLevelcat++; ?> +
    + +
    + + diff --git a/components/com_newsfeeds/tmpl/category/default.php b/components/com_newsfeeds/tmpl/category/default.php index 12ce5fafcdcb7..f3852a4e7ee20 100644 --- a/components/com_newsfeeds/tmpl/category/default.php +++ b/components/com_newsfeeds/tmpl/category/default.php @@ -1,4 +1,5 @@ params->get('show_page_heading') ? 'h2' : 'h1'; ?>
    - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - - params->get('show_category_title', 1)) : ?> - <> - category->title, '', 'com_newsfeeds.category.title'); ?> - > - - params->get('show_tags', 1) && !empty($this->category->tags->itemTags)) : ?> - category->tagLayout = new FileLayout('joomla.content.tags'); ?> - category->tagLayout->render($this->category->tags->itemTags); ?> - - params->get('show_description', 1) || $this->params->def('show_description_image', 1)) : ?> -
    - params->get('show_description_image') && $this->category->getParams()->get('image')) : ?> - $this->category->getParams()->get('image'), - 'alt' => empty($this->category->getParams()->get('image_alt')) && empty($this->category->getParams()->get('image_alt_empty')) ? false : $this->category->getParams()->get('image_alt'), - ] - ); ?> - - params->get('show_description') && $this->category->description) : ?> - category->description, '', 'com_newsfeeds.category'); ?> - -
    -
    - - loadTemplate('items'); ?> - maxLevel != 0 && !empty($this->children[$this->category->id])) : ?> -
    -

    - -

    - loadTemplate('children'); ?> -
    - + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    + + params->get('show_category_title', 1)) : ?> + <> + category->title, '', 'com_newsfeeds.category.title'); ?> + > + + params->get('show_tags', 1) && !empty($this->category->tags->itemTags)) : ?> + category->tagLayout = new FileLayout('joomla.content.tags'); ?> + category->tagLayout->render($this->category->tags->itemTags); ?> + + params->get('show_description', 1) || $this->params->def('show_description_image', 1)) : ?> +
    + params->get('show_description_image') && $this->category->getParams()->get('image')) : ?> + $this->category->getParams()->get('image'), + 'alt' => empty($this->category->getParams()->get('image_alt')) && empty($this->category->getParams()->get('image_alt_empty')) ? false : $this->category->getParams()->get('image_alt'), + ] + ); ?> + + params->get('show_description') && $this->category->description) : ?> + category->description, '', 'com_newsfeeds.category'); ?> + +
    +
    + + loadTemplate('items'); ?> + maxLevel != 0 && !empty($this->children[$this->category->id])) : ?> +
    +

    + +

    + loadTemplate('children'); ?> +
    +
    diff --git a/components/com_newsfeeds/tmpl/category/default_children.php b/components/com_newsfeeds/tmpl/category/default_children.php index 7c7e074964c60..09b81be97867d 100644 --- a/components/com_newsfeeds/tmpl/category/default_children.php +++ b/components/com_newsfeeds/tmpl/category/default_children.php @@ -1,4 +1,5 @@ maxLevel != 0 && count($this->children[$this->category->id]) > 0) : ?> -
      - children[$this->category->id] as $id => $child) : ?> - params->get('show_empty_categories') || $child->numitems || count($child->getChildren())) : ?> -
    • - - - escape($child->title); ?> - - - params->get('show_subcat_desc') == 1) : ?> - description) : ?> -
      - description, '', 'com_newsfeeds.category'); ?> -
      - - - params->get('show_cat_items') == 1) : ?> - -   - numitems; ?> - - - getChildren()) > 0) : ?> - children[$child->id] = $child->getChildren(); ?> - category = $child; ?> - maxLevel--; ?> - loadTemplate('children'); ?> - category = $child->getParent(); ?> - maxLevel++; ?> - -
    • - - -
    +
      + children[$this->category->id] as $id => $child) : ?> + params->get('show_empty_categories') || $child->numitems || count($child->getChildren())) : ?> +
    • + + + escape($child->title); ?> + + + params->get('show_subcat_desc') == 1) : ?> + description) : ?> +
      + description, '', 'com_newsfeeds.category'); ?> +
      + + + params->get('show_cat_items') == 1) : ?> + +   + numitems; ?> + + + getChildren()) > 0) : ?> + children[$child->id] = $child->getChildren(); ?> + category = $child; ?> + maxLevel--; ?> + loadTemplate('children'); ?> + category = $child->getParent(); ?> + maxLevel++; ?> + +
    • + + +
    - items)) : ?> -

    - -
    - params->get('filter_field') !== 'hide' || $this->params->get('show_pagination_limit')) : ?> -
    - params->get('filter_field') !== 'hide' && $this->params->get('filter_field') == '1') : ?> -
    - - -
    - - params->get('show_pagination_limit')) : ?> -
    - - pagination->getLimitBox(); ?> -
    - -
    - -
      - items as $item) : ?> -
    • - params->get('show_articles')) : ?> - - numarticles); ?> - - - - - - published == 0) : ?> - - - - -
      - params->get('show_link')) : ?> - link); ?> - - - - - -
      - -
    • - -
    - - items)) : ?> - params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> -
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - - pagination->getPagesLinks(); ?> -
    - - -
    - + items)) : ?> +

    + +
    + params->get('filter_field') !== 'hide' || $this->params->get('show_pagination_limit')) : ?> +
    + params->get('filter_field') !== 'hide' && $this->params->get('filter_field') == '1') : ?> +
    + + +
    + + params->get('show_pagination_limit')) : ?> +
    + + pagination->getLimitBox(); ?> +
    + +
    + +
      + items as $item) : ?> +
    • + params->get('show_articles')) : ?> + + numarticles); ?> + + + + + + published == 0) : ?> + + + + +
      + params->get('show_link')) : ?> + link); ?> + + + + + +
      + +
    • + +
    + + items)) : ?> + params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> +
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + + pagination->getPagesLinks(); ?> +
    + + +
    +
    diff --git a/components/com_newsfeeds/tmpl/newsfeed/default.php b/components/com_newsfeeds/tmpl/newsfeed/default.php index 99cf1315060ea..6ee290eec2e7d 100644 --- a/components/com_newsfeeds/tmpl/newsfeed/default.php +++ b/components/com_newsfeeds/tmpl/newsfeed/default.php @@ -1,4 +1,5 @@ msg)) : ?> - msg; ?> + msg; ?> - - item->rtl; ?> - - isRtl(); ?> - - - - - - - - - - - - - - item->images); ?> -
    - params->get('display_num')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - -

    - item->published == 0) : ?> - - - - item->name); ?> - -

    + + item->rtl; ?> + + isRtl(); ?> + + + + + + + + + + + + + + item->images); ?> +
    + params->get('display_num')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    + +

    + item->published == 0) : ?> + + + + item->name); ?> + +

    - params->get('show_tags', 1)) : ?> - item->tagLayout = new FileLayout('joomla.content.tags'); ?> - item->tagLayout->render($this->item->tags->itemTags); ?> - + params->get('show_tags', 1)) : ?> + item->tagLayout = new FileLayout('joomla.content.tags'); ?> + item->tagLayout->render($this->item->tags->itemTags); ?> + - - image_first) && !empty($images->image_first)) : ?> - float_first) ? $this->params->get('float_first') : $images->float_first; ?> -
    -
    - $images->image_first, - 'alt' => empty($images->image_first_alt) && empty($images->image_first_alt_empty) ? false : $images->image_first_alt, - ] - ); ?> - image_first_caption) : ?> -
    escape($images->image_first_caption); ?>
    - -
    -
    - + + image_first) && !empty($images->image_first)) : ?> + float_first) ? $this->params->get('float_first') : $images->float_first; ?> +
    +
    + $images->image_first, + 'alt' => empty($images->image_first_alt) && empty($images->image_first_alt_empty) ? false : $images->image_first_alt, + ] + ); ?> + image_first_caption) : ?> +
    escape($images->image_first_caption); ?>
    + +
    +
    + - image_second) and !empty($images->image_second)) : ?> - float_second) ? $this->params->get('float_second') : $images->float_second; ?> -
    -
    - $images->image_second, - 'alt' => empty($images->image_second_alt) && empty($images->image_second_alt_empty) ? false : $images->image_second_alt, - ] - ); ?> - image_second_caption) : ?> -
    escape($images->image_second_caption); ?>
    - -
    -
    - - - item->description; ?> - + image_second) and !empty($images->image_second)) : ?> + float_second) ? $this->params->get('float_second') : $images->float_second; ?> +
    +
    + $images->image_second, + 'alt' => empty($images->image_second_alt) && empty($images->image_second_alt_empty) ? false : $images->image_second_alt, + ] + ); ?> + image_second_caption) : ?> +
    escape($images->image_second_caption); ?>
    + +
    +
    + + + item->description; ?> + - params->get('show_feed_description')) : ?> -
    - rssDoc->description); ?> -
    - + params->get('show_feed_description')) : ?> +
    + rssDoc->description); ?> +
    + - - rssDoc->image && $this->params->get('show_feed_image')) : ?> -
    - $this->rssDoc->image->uri, - 'alt' => $this->rssDoc->image->title, - ] - ); ?> -
    - + + rssDoc->image && $this->params->get('show_feed_image')) : ?> +
    + $this->rssDoc->image->uri, + 'alt' => $this->rssDoc->image->title, + ] + ); ?> +
    + - - rssDoc[0])) : ?> -
      - item->numarticles; $i++) : ?> - rssDoc[$i])) : ?> - - - rssDoc[$i]->uri || !$this->rssDoc[$i]->isPermaLink ? trim($this->rssDoc[$i]->uri) : trim($this->rssDoc[$i]->guid); ?> - item->link : $uri; ?> - rssDoc[$i]->content !== '' ? trim($this->rssDoc[$i]->content) : ''; ?> -
    1. - - - - - + + rssDoc[0])) : ?> +
        + item->numarticles; $i++) : ?> + rssDoc[$i])) : ?> + + + rssDoc[$i]->uri || !$this->rssDoc[$i]->isPermaLink ? trim($this->rssDoc[$i]->uri) : trim($this->rssDoc[$i]->guid); ?> + item->link : $uri; ?> + rssDoc[$i]->content !== '' ? trim($this->rssDoc[$i]->content) : ''; ?> +
      1. + + + + + - params->get('show_item_description') && $text !== '') : ?> -
        - params->get('show_feed_image', 0) == 0) : ?> - - - params->get('feed_character_count')); ?> - -
        - -
      2. - -
      - -
    + params->get('show_item_description') && $text !== '') : ?> +
    + params->get('show_feed_image', 0) == 0) : ?> + + + params->get('feed_character_count')); ?> + +
    + + + + + +
    diff --git a/components/com_privacy/src/Controller/DisplayController.php b/components/com_privacy/src/Controller/DisplayController.php index a428c60f856c7..03fd975c9c929 100644 --- a/components/com_privacy/src/Controller/DisplayController.php +++ b/components/com_privacy/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', $this->default_view); - - // Submitting information requests and confirmation through the frontend is restricted to authenticated users at this time - if (in_array($view, ['confirm', 'request']) && $this->app->getIdentity()->guest) - { - $this->setRedirect( - Route::_('index.php?option=com_users&view=login&return=' . base64_encode('index.php?option=com_privacy&view=' . $view), false) - ); - - return $this; - } - - // Set a Referrer-Policy header for views which require it - if (in_array($view, ['confirm', 'remind'])) - { - $this->app->setHeader('Referrer-Policy', 'no-referrer', true); - } - - return parent::display($cachable, $urlparams); - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return $this + * + * @since 3.9.0 + */ + public function display($cachable = false, $urlparams = []) + { + $view = $this->input->get('view', $this->default_view); + + // Submitting information requests and confirmation through the frontend is restricted to authenticated users at this time + if (in_array($view, ['confirm', 'request']) && $this->app->getIdentity()->guest) { + $this->setRedirect( + Route::_('index.php?option=com_users&view=login&return=' . base64_encode('index.php?option=com_privacy&view=' . $view), false) + ); + + return $this; + } + + // Set a Referrer-Policy header for views which require it + if (in_array($view, ['confirm', 'remind'])) { + $this->app->setHeader('Referrer-Policy', 'no-referrer', true); + } + + return parent::display($cachable, $urlparams); + } } diff --git a/components/com_privacy/src/Controller/RequestController.php b/components/com_privacy/src/Controller/RequestController.php index 1611fe222346d..cc033df04bd44 100644 --- a/components/com_privacy/src/Controller/RequestController.php +++ b/components/com_privacy/src/Controller/RequestController.php @@ -1,4 +1,5 @@ checkToken('post'); - - /** @var ConfirmModel $model */ - $model = $this->getModel('Confirm', 'Site'); - $data = $this->input->post->get('jform', [], 'array'); - - $return = $model->confirmRequest($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - if ($this->app->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_PRIVACY_ERROR_CONFIRMING_REQUEST'); - } - - // Go back to the confirm form. - $this->setRedirect(Route::_('index.php?option=com_privacy&view=confirm', false), $message, 'error'); - - return false; - } - elseif ($return === false) - { - // Confirm failed. - // Go back to the confirm form. - $message = Text::sprintf('COM_PRIVACY_ERROR_CONFIRMING_REQUEST_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_privacy&view=confirm', false), $message, 'notice'); - - return false; - } - else - { - // Confirm succeeded. - $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CONFIRM_REQUEST_SUCCEEDED'), 'info'); - - return true; - } - } - - /** - * Method to submit an information request. - * - * @return boolean - * - * @since 3.9.0 - */ - public function submit() - { - // Check the request token. - $this->checkToken('post'); - - /** @var RequestModel $model */ - $model = $this->getModel('Request', 'Site'); - $data = $this->input->post->get('jform', [], 'array'); - - $return = $model->createRequest($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - if ($this->app->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_PRIVACY_ERROR_CREATING_REQUEST'); - } - - // Go back to the confirm form. - $this->setRedirect(Route::_('index.php?option=com_privacy&view=request', false), $message, 'error'); - - return false; - } - elseif ($return === false) - { - // Confirm failed. - // Go back to the confirm form. - $message = Text::sprintf('COM_PRIVACY_ERROR_CREATING_REQUEST_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_privacy&view=request', false), $message, 'notice'); - - return false; - } - else - { - // Confirm succeeded. - $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CREATE_REQUEST_SUCCEEDED'), 'info'); - - return true; - } - } - - /** - * Method to extend the privacy consent. - * - * @return boolean - * - * @since 3.9.0 - */ - public function remind() - { - // Check the request token. - $this->checkToken('post'); - - /** @var ConfirmModel $model */ - $model = $this->getModel('Remind', 'Site'); - $data = $this->input->post->get('jform', [], 'array'); - - $return = $model->remindRequest($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - if ($this->app->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_PRIVACY_ERROR_REMIND_REQUEST'); - } - - // Go back to the confirm form. - $this->setRedirect(Route::_('index.php?option=com_privacy&view=remind', false), $message, 'error'); - - return false; - } - elseif ($return === false) - { - // Confirm failed. - // Go back to the confirm form. - $message = Text::sprintf('COM_PRIVACY_ERROR_CONFIRMING_REMIND_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_privacy&view=remind', false), $message, 'notice'); - - return false; - } - else - { - // Confirm succeeded. - $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CONFIRM_REMIND_SUCCEEDED'), 'info'); - - return true; - } - } + /** + * Method to confirm the information request. + * + * @return boolean + * + * @since 3.9.0 + */ + public function confirm() + { + // Check the request token. + $this->checkToken('post'); + + /** @var ConfirmModel $model */ + $model = $this->getModel('Confirm', 'Site'); + $data = $this->input->post->get('jform', [], 'array'); + + $return = $model->confirmRequest($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + if ($this->app->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_PRIVACY_ERROR_CONFIRMING_REQUEST'); + } + + // Go back to the confirm form. + $this->setRedirect(Route::_('index.php?option=com_privacy&view=confirm', false), $message, 'error'); + + return false; + } elseif ($return === false) { + // Confirm failed. + // Go back to the confirm form. + $message = Text::sprintf('COM_PRIVACY_ERROR_CONFIRMING_REQUEST_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_privacy&view=confirm', false), $message, 'notice'); + + return false; + } else { + // Confirm succeeded. + $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CONFIRM_REQUEST_SUCCEEDED'), 'info'); + + return true; + } + } + + /** + * Method to submit an information request. + * + * @return boolean + * + * @since 3.9.0 + */ + public function submit() + { + // Check the request token. + $this->checkToken('post'); + + /** @var RequestModel $model */ + $model = $this->getModel('Request', 'Site'); + $data = $this->input->post->get('jform', [], 'array'); + + $return = $model->createRequest($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + if ($this->app->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_PRIVACY_ERROR_CREATING_REQUEST'); + } + + // Go back to the confirm form. + $this->setRedirect(Route::_('index.php?option=com_privacy&view=request', false), $message, 'error'); + + return false; + } elseif ($return === false) { + // Confirm failed. + // Go back to the confirm form. + $message = Text::sprintf('COM_PRIVACY_ERROR_CREATING_REQUEST_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_privacy&view=request', false), $message, 'notice'); + + return false; + } else { + // Confirm succeeded. + $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CREATE_REQUEST_SUCCEEDED'), 'info'); + + return true; + } + } + + /** + * Method to extend the privacy consent. + * + * @return boolean + * + * @since 3.9.0 + */ + public function remind() + { + // Check the request token. + $this->checkToken('post'); + + /** @var ConfirmModel $model */ + $model = $this->getModel('Remind', 'Site'); + $data = $this->input->post->get('jform', [], 'array'); + + $return = $model->remindRequest($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + if ($this->app->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_PRIVACY_ERROR_REMIND_REQUEST'); + } + + // Go back to the confirm form. + $this->setRedirect(Route::_('index.php?option=com_privacy&view=remind', false), $message, 'error'); + + return false; + } elseif ($return === false) { + // Confirm failed. + // Go back to the confirm form. + $message = Text::sprintf('COM_PRIVACY_ERROR_CONFIRMING_REMIND_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_privacy&view=remind', false), $message, 'notice'); + + return false; + } else { + // Confirm succeeded. + $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CONFIRM_REMIND_SUCCEEDED'), 'info'); + + return true; + } + } } diff --git a/components/com_privacy/src/Model/ConfirmModel.php b/components/com_privacy/src/Model/ConfirmModel.php index d47cb8fc1a03b..9912d888aad37 100644 --- a/components/com_privacy/src/Model/ConfirmModel.php +++ b/components/com_privacy/src/Model/ConfirmModel.php @@ -1,4 +1,5 @@ getForm(); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - // Get the user email address - $email = Factory::getUser()->email; - - // Search for the information request - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load(['email' => $email, 'status' => 0])) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); - - return false; - } - - // A request can only be confirmed if it is in a pending status and has a confirmation token - if ($table->status != '0' || !$table->confirm_token || $table->confirm_token_created_at === null) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); - - return false; - } - - // A request can only be confirmed if the token is less than 24 hours old - $confirmTokenCreatedAt = new Date($table->confirm_token_created_at); - $confirmTokenCreatedAt->add(new \DateInterval('P1D')); - - $now = new Date('now'); - - if ($now > $confirmTokenCreatedAt) - { - // Invalidate the request - $table->status = -1; - $table->confirm_token = ''; - $table->confirm_token_created_at = null; - - try - { - $table->store(); - } - catch (ExecutionFailureException $exception) - { - // The error will be logged in the database API, we just need to catch it here to not let things fatal out - } - - $this->setError(Text::_('COM_PRIVACY_ERROR_CONFIRM_TOKEN_EXPIRED')); - - return false; - } - - // Verify the token - if (!UserHelper::verifyPassword($data['confirm_token'], $table->confirm_token)) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); - - return false; - } - - // Everything is good to go, transition the request to confirmed - $saved = $this->save( - [ - 'id' => $table->id, - 'status' => 1, - 'confirm_token' => '', - ] - ); - - if (!$saved) - { - // Error was set by the save method - return false; - } - - // Push a notification to the site's super users, deliberately ignoring if this process fails so the below message goes out - /** @var MessageModel $messageModel */ - $messageModel = Factory::getApplication()->bootComponent('com_messages')->getMVCFactory()->createModel('Message', 'Administrator'); - - $messageModel->notifySuperUsers( - Text::_('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CONFIRMED_REQUEST_SUBJECT'), - Text::sprintf('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CONFIRMED_REQUEST_MESSAGE', $table->email) - ); - - $message = [ - 'action' => 'request-confirmed', - 'subjectemail' => $table->email, - 'id' => $table->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_CONFIRMED_REQUEST', 'com_privacy.request'); - - return true; - } - - /** - * Method for getting the form from the model. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 3.9.0 - */ - public function getForm($data = [], $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_privacy.confirm', 'confirm', ['control' => 'jform']); - - if (empty($form)) - { - return false; - } - - $input = Factory::getApplication()->input; - - if ($input->getMethod() === 'GET') - { - $form->setValue('confirm_token', '', $input->get->getAlnum('confirm_token')); - } - - return $form; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.9.0 - * @throws \Exception - */ - public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_privacy'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to fetch an instance of the action log model. - * - * @return ActionlogModel - * - * @since 4.0.0 - */ - private function getActionlogModel(): ActionlogModel - { - return Factory::getApplication()->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - } + /** + * Confirms the information request. + * + * @param array $data The data expected for the form. + * + * @return mixed Exception | boolean + * + * @since 3.9.0 + */ + public function confirmRequest($data) + { + // Get the form. + $form = $this->getForm(); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + // Get the user email address + $email = Factory::getUser()->email; + + // Search for the information request + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load(['email' => $email, 'status' => 0])) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); + + return false; + } + + // A request can only be confirmed if it is in a pending status and has a confirmation token + if ($table->status != '0' || !$table->confirm_token || $table->confirm_token_created_at === null) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); + + return false; + } + + // A request can only be confirmed if the token is less than 24 hours old + $confirmTokenCreatedAt = new Date($table->confirm_token_created_at); + $confirmTokenCreatedAt->add(new \DateInterval('P1D')); + + $now = new Date('now'); + + if ($now > $confirmTokenCreatedAt) { + // Invalidate the request + $table->status = -1; + $table->confirm_token = ''; + $table->confirm_token_created_at = null; + + try { + $table->store(); + } catch (ExecutionFailureException $exception) { + // The error will be logged in the database API, we just need to catch it here to not let things fatal out + } + + $this->setError(Text::_('COM_PRIVACY_ERROR_CONFIRM_TOKEN_EXPIRED')); + + return false; + } + + // Verify the token + if (!UserHelper::verifyPassword($data['confirm_token'], $table->confirm_token)) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); + + return false; + } + + // Everything is good to go, transition the request to confirmed + $saved = $this->save( + [ + 'id' => $table->id, + 'status' => 1, + 'confirm_token' => '', + ] + ); + + if (!$saved) { + // Error was set by the save method + return false; + } + + // Push a notification to the site's super users, deliberately ignoring if this process fails so the below message goes out + /** @var MessageModel $messageModel */ + $messageModel = Factory::getApplication()->bootComponent('com_messages')->getMVCFactory()->createModel('Message', 'Administrator'); + + $messageModel->notifySuperUsers( + Text::_('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CONFIRMED_REQUEST_SUBJECT'), + Text::sprintf('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CONFIRMED_REQUEST_MESSAGE', $table->email) + ); + + $message = [ + 'action' => 'request-confirmed', + 'subjectemail' => $table->email, + 'id' => $table->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_CONFIRMED_REQUEST', 'com_privacy.request'); + + return true; + } + + /** + * Method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 3.9.0 + */ + public function getForm($data = [], $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_privacy.confirm', 'confirm', ['control' => 'jform']); + + if (empty($form)) { + return false; + } + + $input = Factory::getApplication()->input; + + if ($input->getMethod() === 'GET') { + $form->setValue('confirm_token', '', $input->get->getAlnum('confirm_token')); + } + + return $form; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.9.0 + * @throws \Exception + */ + public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_privacy'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to fetch an instance of the action log model. + * + * @return ActionlogModel + * + * @since 4.0.0 + */ + private function getActionlogModel(): ActionlogModel + { + return Factory::getApplication()->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + } } diff --git a/components/com_privacy/src/Model/RemindModel.php b/components/com_privacy/src/Model/RemindModel.php index 68b88eee6f29a..886b4b1b888a1 100644 --- a/components/com_privacy/src/Model/RemindModel.php +++ b/components/com_privacy/src/Model/RemindModel.php @@ -1,4 +1,5 @@ getForm(); - $data['email'] = PunycodeHelper::emailToPunycode($data['email']); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - /** @var ConsentTable $table */ - $table = $this->getTable(); - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['r.id', 'r.user_id', 'r.token'])); - $query->from($db->quoteName('#__privacy_consents', 'r')); - $query->join('LEFT', $db->quoteName('#__users', 'u'), - $db->quoteName('u.id') . ' = ' . $db->quoteName('r.user_id') - ); - $query->where($db->quoteName('u.email') . ' = :email') - ->bind(':email', $data['email']); - $query->where($db->quoteName('r.remind') . ' = 1'); - $db->setQuery($query); - - try - { - $remind = $db->loadObject(); - } - catch (ExecutionFailureException $e) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REMIND')); - - return false; - } - - if (!$remind) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REMIND')); - - return false; - } - - // Verify the token - if (!UserHelper::verifyPassword($data['remind_token'], $remind->token)) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_REMIND_REQUESTS')); - - return false; - } - - // Everything is good to go, transition the request to extended - $saved = $this->save( - [ - 'id' => $remind->id, - 'remind' => 0, - 'token' => '', - 'created' => Factory::getDate()->toSql(), - ] - ); - - if (!$saved) - { - // Error was set by the save method - return false; - } - - return true; - } - - /** - * Method for getting the form from the model. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 3.9.0 - */ - public function getForm($data = [], $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_privacy.remind', 'remind', ['control' => 'jform']); - - if (empty($form)) - { - return false; - } - - $input = Factory::getApplication()->input; - - if ($input->getMethod() === 'GET') - { - $form->setValue('remind_token', '', $input->get->getAlnum('remind_token')); - } - - return $form; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @throws \Exception - * @since 3.9.0 - */ - public function getTable($name = 'Consent', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_privacy'); - - // Load the parameters. - $this->setState('params', $params); - } + /** + * Confirms the remind request. + * + * @param array $data The data expected for the form. + * + * @return mixed \Exception | JException | boolean + * + * @since 3.9.0 + */ + public function remindRequest($data) + { + // Get the form. + $form = $this->getForm(); + $data['email'] = PunycodeHelper::emailToPunycode($data['email']); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + /** @var ConsentTable $table */ + $table = $this->getTable(); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['r.id', 'r.user_id', 'r.token'])); + $query->from($db->quoteName('#__privacy_consents', 'r')); + $query->join( + 'LEFT', + $db->quoteName('#__users', 'u'), + $db->quoteName('u.id') . ' = ' . $db->quoteName('r.user_id') + ); + $query->where($db->quoteName('u.email') . ' = :email') + ->bind(':email', $data['email']); + $query->where($db->quoteName('r.remind') . ' = 1'); + $db->setQuery($query); + + try { + $remind = $db->loadObject(); + } catch (ExecutionFailureException $e) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REMIND')); + + return false; + } + + if (!$remind) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REMIND')); + + return false; + } + + // Verify the token + if (!UserHelper::verifyPassword($data['remind_token'], $remind->token)) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_REMIND_REQUESTS')); + + return false; + } + + // Everything is good to go, transition the request to extended + $saved = $this->save( + [ + 'id' => $remind->id, + 'remind' => 0, + 'token' => '', + 'created' => Factory::getDate()->toSql(), + ] + ); + + if (!$saved) { + // Error was set by the save method + return false; + } + + return true; + } + + /** + * Method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 3.9.0 + */ + public function getForm($data = [], $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_privacy.remind', 'remind', ['control' => 'jform']); + + if (empty($form)) { + return false; + } + + $input = Factory::getApplication()->input; + + if ($input->getMethod() === 'GET') { + $form->setValue('remind_token', '', $input->get->getAlnum('remind_token')); + } + + return $form; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @throws \Exception + * @since 3.9.0 + */ + public function getTable($name = 'Consent', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_privacy'); + + // Load the parameters. + $this->setState('params', $params); + } } diff --git a/components/com_privacy/src/Model/RequestModel.php b/components/com_privacy/src/Model/RequestModel.php index cebcccca630a8..eaaa82d01a77e 100644 --- a/components/com_privacy/src/Model/RequestModel.php +++ b/components/com_privacy/src/Model/RequestModel.php @@ -1,4 +1,5 @@ get('mailonline', 1)) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED')); - - return false; - } - - // Get the form. - $form = $this->getForm(); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - $data['email'] = Factory::getUser()->email; - - // Search for an open information request matching the email and type - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('COUNT(id)') - ->from($db->quoteName('#__privacy_requests')) - ->where($db->quoteName('email') . ' = :email') - ->where($db->quoteName('request_type') . ' = :requesttype') - ->whereIn($db->quoteName('status'), [0, 1]) - ->bind(':email', $data['email']) - ->bind(':requesttype', $data['request_type']); - - try - { - $result = (int) $db->setQuery($query)->loadResult(); - } - catch (ExecutionFailureException $exception) - { - // Can't check for existing requests, so don't create a new one - $this->setError(Text::_('COM_PRIVACY_ERROR_CHECKING_FOR_EXISTING_REQUESTS')); - - return false; - } - - if ($result > 0) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_PENDING_REQUEST_OPEN')); - - return false; - } - - // Everything is good to go, create the request - $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); - $hashedToken = UserHelper::hashPassword($token); - - $data['confirm_token'] = $hashedToken; - $data['confirm_token_created_at'] = Factory::getDate()->toSql(); - - if (!$this->save($data)) - { - // The save function will set the error message, so just return here - return false; - } - - // Push a notification to the site's super users, deliberately ignoring if this process fails so the below message goes out - /** @var MessageModel $messageModel */ - $messageModel = $app->bootComponent('com_messages')->getMVCFactory()->createModel('Message', 'Administrator'); - - $messageModel->notifySuperUsers( - Text::_('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CREATED_REQUEST_SUBJECT'), - Text::sprintf('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CREATED_REQUEST_MESSAGE', $data['email']) - ); - - // The mailer can be set to either throw Exceptions or return boolean false, account for both - try - { - $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - $templateData = [ - 'sitename' => $app->get('sitename'), - 'url' => Uri::root(), - 'tokenurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm&confirm_token=' . $token, false, $linkMode, true), - 'formurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm', false, $linkMode, true), - 'token' => $token, - ]; - - switch ($data['request_type']) - { - case 'export': - $mailer = new MailTemplate('com_privacy.notification.export', $app->getLanguage()->getTag()); - - break; - - case 'remove': - $mailer = new MailTemplate('com_privacy.notification.remove', $app->getLanguage()->getTag()); - - break; - - default: - $this->setError(Text::_('COM_PRIVACY_ERROR_UNKNOWN_REQUEST_TYPE')); - - return false; - } - - $mailer->addTemplateData($templateData); - $mailer->addRecipient($data['email']); - - $mailer->send(); - - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($this->getState($this->getName() . '.id'))) - { - $this->setError($table->getError()); - - return false; - } - - // Log the request's creation - $message = [ - 'action' => 'request-created', - 'requesttype' => $table->request_type, - 'subjectemail' => $table->email, - 'id' => $table->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_CREATED_REQUEST', 'com_privacy.request'); - - // The email sent and the record is saved, everything is good to go from here - return true; - } - catch (MailDisabledException | phpmailerException $exception) - { - $this->setError($exception->getMessage()); - - return false; - } - } - - /** - * Method for getting the form from the model. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 3.9.0 - */ - public function getForm($data = [], $loadData = true) - { - return $this->loadForm('com_privacy.request', 'request', ['control' => 'jform']); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @throws \Exception - * @since 3.9.0 - */ - public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_privacy'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to fetch an instance of the action log model. - * - * @return ActionlogModel - * - * @since 4.0.0 - */ - private function getActionlogModel(): ActionlogModel - { - return Factory::getApplication()->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - } + /** + * Creates an information request. + * + * @param array $data The data expected for the form. + * + * @return mixed Exception | boolean + * + * @since 3.9.0 + */ + public function createRequest($data) + { + $app = Factory::getApplication(); + + // Creating requests requires the site's email sending be enabled + if (!$app->get('mailonline', 1)) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED')); + + return false; + } + + // Get the form. + $form = $this->getForm(); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + $data['email'] = Factory::getUser()->email; + + // Search for an open information request matching the email and type + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('COUNT(id)') + ->from($db->quoteName('#__privacy_requests')) + ->where($db->quoteName('email') . ' = :email') + ->where($db->quoteName('request_type') . ' = :requesttype') + ->whereIn($db->quoteName('status'), [0, 1]) + ->bind(':email', $data['email']) + ->bind(':requesttype', $data['request_type']); + + try { + $result = (int) $db->setQuery($query)->loadResult(); + } catch (ExecutionFailureException $exception) { + // Can't check for existing requests, so don't create a new one + $this->setError(Text::_('COM_PRIVACY_ERROR_CHECKING_FOR_EXISTING_REQUESTS')); + + return false; + } + + if ($result > 0) { + $this->setError(Text::_('COM_PRIVACY_ERROR_PENDING_REQUEST_OPEN')); + + return false; + } + + // Everything is good to go, create the request + $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); + $hashedToken = UserHelper::hashPassword($token); + + $data['confirm_token'] = $hashedToken; + $data['confirm_token_created_at'] = Factory::getDate()->toSql(); + + if (!$this->save($data)) { + // The save function will set the error message, so just return here + return false; + } + + // Push a notification to the site's super users, deliberately ignoring if this process fails so the below message goes out + /** @var MessageModel $messageModel */ + $messageModel = $app->bootComponent('com_messages')->getMVCFactory()->createModel('Message', 'Administrator'); + + $messageModel->notifySuperUsers( + Text::_('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CREATED_REQUEST_SUBJECT'), + Text::sprintf('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CREATED_REQUEST_MESSAGE', $data['email']) + ); + + // The mailer can be set to either throw Exceptions or return boolean false, account for both + try { + $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + $templateData = [ + 'sitename' => $app->get('sitename'), + 'url' => Uri::root(), + 'tokenurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm&confirm_token=' . $token, false, $linkMode, true), + 'formurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm', false, $linkMode, true), + 'token' => $token, + ]; + + switch ($data['request_type']) { + case 'export': + $mailer = new MailTemplate('com_privacy.notification.export', $app->getLanguage()->getTag()); + + break; + + case 'remove': + $mailer = new MailTemplate('com_privacy.notification.remove', $app->getLanguage()->getTag()); + + break; + + default: + $this->setError(Text::_('COM_PRIVACY_ERROR_UNKNOWN_REQUEST_TYPE')); + + return false; + } + + $mailer->addTemplateData($templateData); + $mailer->addRecipient($data['email']); + + $mailer->send(); + + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($this->getState($this->getName() . '.id'))) { + $this->setError($table->getError()); + + return false; + } + + // Log the request's creation + $message = [ + 'action' => 'request-created', + 'requesttype' => $table->request_type, + 'subjectemail' => $table->email, + 'id' => $table->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_CREATED_REQUEST', 'com_privacy.request'); + + // The email sent and the record is saved, everything is good to go from here + return true; + } catch (MailDisabledException | phpmailerException $exception) { + $this->setError($exception->getMessage()); + + return false; + } + } + + /** + * Method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 3.9.0 + */ + public function getForm($data = [], $loadData = true) + { + return $this->loadForm('com_privacy.request', 'request', ['control' => 'jform']); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @throws \Exception + * @since 3.9.0 + */ + public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_privacy'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to fetch an instance of the action log model. + * + * @return ActionlogModel + * + * @since 4.0.0 + */ + private function getActionlogModel(): ActionlogModel + { + return Factory::getApplication()->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + } } diff --git a/components/com_privacy/src/Service/Router.php b/components/com_privacy/src/Service/Router.php index 6b6a7ddbfbb5c..269e1e533b2d1 100644 --- a/components/com_privacy/src/Service/Router.php +++ b/components/com_privacy/src/Service/Router.php @@ -1,4 +1,5 @@ registerView(new RouterViewConfiguration('confirm')); - $this->registerView(new RouterViewConfiguration('request')); - $this->registerView(new RouterViewConfiguration('remind')); + /** + * Privacy Component router constructor + * + * @param CMSApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + * + * @since 3.9.0 + */ + public function __construct($app = null, $menu = null) + { + $this->registerView(new RouterViewConfiguration('confirm')); + $this->registerView(new RouterViewConfiguration('request')); + $this->registerView(new RouterViewConfiguration('remind')); - parent::__construct($app, $menu); + parent::__construct($app, $menu); - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } } diff --git a/components/com_privacy/src/View/Confirm/HtmlView.php b/components/com_privacy/src/View/Confirm/HtmlView.php index 1d3fc2fef02df..1d34d52ef2744 100644 --- a/components/com_privacy/src/View/Confirm/HtmlView.php +++ b/components/com_privacy/src/View/Confirm/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->state = $this->get('State'); - $this->params = $this->state->params; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 3.9.0 - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_CONFIRM_PAGE_TITLE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The form object + * + * @var Form + * @since 3.9.0 + */ + protected $form; + + /** + * The CSS class suffix to append to the view container + * + * @var string + * @since 3.9.0 + */ + protected $pageclass_sfx; + + /** + * The view parameters + * + * @var Registry + * @since 3.9.0 + */ + protected $params; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + // Initialise variables. + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->params = $this->state->params; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 3.9.0 + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_CONFIRM_PAGE_TITLE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_privacy/src/View/Remind/HtmlView.php b/components/com_privacy/src/View/Remind/HtmlView.php index 2664844f35ac2..4918feabcb2bd 100644 --- a/components/com_privacy/src/View/Remind/HtmlView.php +++ b/components/com_privacy/src/View/Remind/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->state = $this->get('State'); - $this->params = $this->state->params; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 3.9.0 - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_REMIND_PAGE_TITLE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The form object + * + * @var Form + * @since 3.9.0 + */ + protected $form; + + /** + * The CSS class suffix to append to the view container + * + * @var string + * @since 3.9.0 + */ + protected $pageclass_sfx; + + /** + * The view parameters + * + * @var Registry + * @since 3.9.0 + */ + protected $params; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + // Initialise variables. + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->params = $this->state->params; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 3.9.0 + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_REMIND_PAGE_TITLE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_privacy/src/View/Request/HtmlView.php b/components/com_privacy/src/View/Request/HtmlView.php index 23c97e2ac7bae..824102b912073 100644 --- a/components/com_privacy/src/View/Request/HtmlView.php +++ b/components/com_privacy/src/View/Request/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->state = $this->get('State'); - $this->params = $this->state->params; - $this->sendMailEnabled = (bool) Factory::getApplication()->get('mailonline', 1); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 3.9.0 - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_REQUEST_PAGE_TITLE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The form object + * + * @var Form + * @since 3.9.0 + */ + protected $form; + + /** + * The CSS class suffix to append to the view container + * + * @var string + * @since 3.9.0 + */ + protected $pageclass_sfx; + + /** + * The view parameters + * + * @var Registry + * @since 3.9.0 + */ + protected $params; + + /** + * Flag indicating the site supports sending email + * + * @var boolean + * @since 3.9.0 + */ + protected $sendMailEnabled; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + // Initialise variables. + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->params = $this->state->params; + $this->sendMailEnabled = (bool) Factory::getApplication()->get('mailonline', 1); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 3.9.0 + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_REQUEST_PAGE_TITLE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_privacy/tmpl/confirm/default.php b/components/com_privacy/tmpl/confirm/default.php index 2f80026cadb9b..7a74c0c893ac5 100644 --- a/components/com_privacy/tmpl/confirm/default.php +++ b/components/com_privacy/tmpl/confirm/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/components/com_privacy/tmpl/remind/default.php b/components/com_privacy/tmpl/remind/default.php index aa311d365ca9c..6e501f6f23c86 100644 --- a/components/com_privacy/tmpl/remind/default.php +++ b/components/com_privacy/tmpl/remind/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/components/com_privacy/tmpl/request/default.php b/components/com_privacy/tmpl/request/default.php index 44afcbb8d0a6d..d203a87d107de 100644 --- a/components/com_privacy/tmpl/request/default.php +++ b/components/com_privacy/tmpl/request/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - - sendMailEnabled) : ?> -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    - -
    - - -
    - + params->get('show_page_heading')) : ?> + + + sendMailEnabled) : ?> +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    + +
    + + +
    +
    diff --git a/components/com_tags/helpers/route.php b/components/com_tags/helpers/route.php index 55cb605e086d5..e62e4ebfb240a 100644 --- a/components/com_tags/helpers/route.php +++ b/components/com_tags/helpers/route.php @@ -1,4 +1,5 @@ app->getIdentity(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'tags'); - $this->input->set('view', $vName); - - if ($user->get('id') || ($this->input->getMethod() === 'POST' && $vName === 'tags')) - { - $cachable = false; - } - - $safeurlparams = array( - 'id' => 'ARRAY', - 'type' => 'ARRAY', - 'limit' => 'UINT', - 'limitstart' => 'UINT', - 'filter_order' => 'CMD', - 'filter_order_Dir' => 'CMD', - 'lang' => 'CMD' - ); - - return parent::display($cachable, $safeurlparams); - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param mixed|boolean $urlparams An array of safe URL parameters and their + * variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 3.1 + */ + public function display($cachable = false, $urlparams = false) + { + $user = $this->app->getIdentity(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'tags'); + $this->input->set('view', $vName); + + if ($user->get('id') || ($this->input->getMethod() === 'POST' && $vName === 'tags')) { + $cachable = false; + } + + $safeurlparams = array( + 'id' => 'ARRAY', + 'type' => 'ARRAY', + 'limit' => 'UINT', + 'limitstart' => 'UINT', + 'filter_order' => 'CMD', + 'filter_order_Dir' => 'CMD', + 'lang' => 'CMD' + ); + + return parent::display($cachable, $safeurlparams); + } } diff --git a/components/com_tags/src/Controller/TagsController.php b/components/com_tags/src/Controller/TagsController.php index 293227bc7dbe5..4a5a248e1873b 100644 --- a/components/com_tags/src/Controller/TagsController.php +++ b/components/com_tags/src/Controller/TagsController.php @@ -1,4 +1,5 @@ app->getIdentity(); - - // Receive request data - $filters = array( - 'like' => trim($this->input->get('like', null, 'string')), - 'title' => trim($this->input->get('title', null, 'string')), - 'flanguage' => $this->input->get('flanguage', null, 'word'), - 'published' => $this->input->get('published', 1, 'int'), - 'parent_id' => $this->input->get('parent_id', 0, 'int'), - 'access' => $user->getAuthorisedViewLevels(), - ); - - if ((!$user->authorise('core.edit.state', 'com_tags')) && (!$user->authorise('core.edit', 'com_tags'))) - { - // Filter on published for those who do not have edit or edit.state rights. - $filters['published'] = 1; - } - - $results = TagsHelper::searchTags($filters); - - if ($results) - { - // Output a JSON object - echo json_encode($results); - } - - $this->app->close(); - } + /** + * Method to search tags with AJAX + * + * @return void + */ + public function searchAjax() + { + $user = $this->app->getIdentity(); + + // Receive request data + $filters = array( + 'like' => trim($this->input->get('like', null, 'string')), + 'title' => trim($this->input->get('title', null, 'string')), + 'flanguage' => $this->input->get('flanguage', null, 'word'), + 'published' => $this->input->get('published', 1, 'int'), + 'parent_id' => $this->input->get('parent_id', 0, 'int'), + 'access' => $user->getAuthorisedViewLevels(), + ); + + if ((!$user->authorise('core.edit.state', 'com_tags')) && (!$user->authorise('core.edit', 'com_tags'))) { + // Filter on published for those who do not have edit or edit.state rights. + $filters['published'] = 1; + } + + $results = TagsHelper::searchTags($filters); + + if ($results) { + // Output a JSON object + echo json_encode($results); + } + + $this->app->close(); + } } diff --git a/components/com_tags/src/Helper/RouteHelper.php b/components/com_tags/src/Helper/RouteHelper.php index 99354fcb481bd..d79ba3d751e16 100644 --- a/components/com_tags/src/Helper/RouteHelper.php +++ b/components/com_tags/src/Helper/RouteHelper.php @@ -1,4 +1,5 @@ getRoute($contentItemId, $typeAlias, $link, $language, $contentCatId); - } - - return $link; - } - - /** - * Tries to load the router for the component and calls it. Otherwise calls getRoute. - * - * @param integer $id The ID of the tag - * - * @return string URL link to pass to the router - * - * @since 3.1 - * @throws Exception - * @deprecated 5.0.0 Use getComponentTagRoute() instead - */ - public static function getTagRoute($id) - { - @trigger_error('This function is replaced by the getComponentTagRoute()', E_USER_DEPRECATED); - - return self::getComponentTagRoute($id); - } - - /** - * Tries to load the router for the component and calls it. Otherwise calls getRoute. - * - * @param string $id The ID of the tag in the format TAG_ID:TAG_ALIAS - * @param string $language The language of the tag - * - * @return string URL link to pass to the router - * - * @since 4.2.0 - * @throws Exception - */ - public static function getComponentTagRoute(string $id, string $language = '*'): string - { - $needles = [ - 'tag' => [(int) $id], - 'language' => $language, - ]; - - if ($id < 1) - { - $link = ''; - } - else - { - $link = 'index.php?option=com_tags&view=tag&id=' . $id; - - if ($item = self::_findItem($needles)) - { - $link .= '&Itemid=' . $item; - } - else - { - $needles = [ - 'tags' => [1, 0], - 'language' => $language, - ]; - - if ($item = self::_findItem($needles)) - { - $link .= '&Itemid=' . $item; - } - } - } - - return $link; - } - - /** - * Tries to load the router for the tags view. - * - * @return string URL link to pass to the router - * - * @since 3.7 - * @throws Exception - * @deprecated 5.0.0 - */ - public static function getTagsRoute() - { - @trigger_error('This function is replaced by the getComponentTagsRoute()', E_USER_DEPRECATED); - - return self::getComponentTagsRoute(); - } - - /** - * Tries to load the router for the tags view. - * - * @param string $language The language of the tag - * - * @return string URL link to pass to the router - * - * @since 4.2.0 - * @throws Exception - */ - public static function getComponentTagsRoute(string $language = '*'): string - { - $needles = [ - 'tags' => [0], - 'language' => $language, - ]; - - $link = 'index.php?option=com_tags&view=tags'; - - if ($item = self::_findItem($needles)) - { - $link .= '&Itemid=' . $item; - } - - return $link; - } - - /** - * Find Item static function - * - * @param array $needles Array used to get the language value - * - * @return null - * - * @throws Exception - */ - protected static function _findItem($needles = null) - { - $menus = AbstractMenu::getInstance('site'); - $language = $needles['language'] ?? '*'; - - // Prepare the reverse lookup array. - if (self::$lookup === null) - { - self::$lookup = array(); - - $component = ComponentHelper::getComponent('com_tags'); - $items = $menus->getItems('component_id', $component->id); - - if ($items) - { - foreach ($items as $item) - { - if (isset($item->query, $item->query['view'])) - { - $lang = ($item->language != '' ? $item->language : '*'); - - if (!isset(self::$lookup[$lang])) - { - self::$lookup[$lang] = array(); - } - - $view = $item->query['view']; - - if (!isset(self::$lookup[$lang][$view])) - { - self::$lookup[$lang][$view] = array(); - } - - // Only match menu items that list one tag - if (isset($item->query['id']) && is_array($item->query['id'])) - { - foreach ($item->query['id'] as $position => $tagId) - { - if (!isset(self::$lookup[$lang][$view][$item->query['id'][$position]]) || count($item->query['id']) == 1) - { - self::$lookup[$lang][$view][$item->query['id'][$position]] = $item->id; - } - } - } - elseif ($view == 'tags') - { - self::$lookup[$lang]['tags'][] = $item->id; - } - } - } - } - } - - if ($needles) - { - foreach ($needles as $view => $ids) - { - if (isset(self::$lookup[$language][$view])) - { - foreach ($ids as $id) - { - if (isset(self::$lookup[$language][$view][(int) $id])) - { - return self::$lookup[$language][$view][(int) $id]; - } - } - } - } - } - else - { - $active = $menus->getActive(); - - if ($active) - { - return $active->id; - } - } - - return null; - } + /** + * Lookup-table for menu items + * + * @var array + */ + protected static $lookup; + + /** + * Tries to load the router for the component and calls it. Otherwise uses getTagRoute. + * + * @param integer $contentItemId Component item id + * @param string $contentItemAlias Component item alias + * @param integer $contentCatId Component item category id + * @param string $language Component item language + * @param string $typeAlias Component type alias + * @param string $routerName Component router + * + * @return string URL link to pass to the router + * + * @since 3.1 + */ + public static function getItemRoute($contentItemId, $contentItemAlias, $contentCatId, $language, $typeAlias, $routerName) + { + $link = ''; + $explodedAlias = explode('.', $typeAlias); + $explodedRouter = explode('::', $routerName); + + if (file_exists($routerFile = JPATH_BASE . '/components/' . $explodedAlias[0] . '/helpers/route.php')) { + \JLoader::register($explodedRouter[0], $routerFile); + $routerClass = $explodedRouter[0]; + $routerMethod = $explodedRouter[1]; + + if (class_exists($routerClass) && method_exists($routerClass, $routerMethod)) { + if ($routerMethod === 'getCategoryRoute') { + $link = $routerClass::$routerMethod($contentItemId, $language); + } else { + $link = $routerClass::$routerMethod($contentItemId . ':' . $contentItemAlias, $contentCatId, $language); + } + } + } + + if ($link === '') { + // Create a fallback link in case we can't find the component router + $router = new CMSRouteHelper(); + $link = $router->getRoute($contentItemId, $typeAlias, $link, $language, $contentCatId); + } + + return $link; + } + + /** + * Tries to load the router for the component and calls it. Otherwise calls getRoute. + * + * @param integer $id The ID of the tag + * + * @return string URL link to pass to the router + * + * @since 3.1 + * @throws Exception + * @deprecated 5.0.0 Use getComponentTagRoute() instead + */ + public static function getTagRoute($id) + { + @trigger_error('This function is replaced by the getComponentTagRoute()', E_USER_DEPRECATED); + + return self::getComponentTagRoute($id); + } + + /** + * Tries to load the router for the component and calls it. Otherwise calls getRoute. + * + * @param string $id The ID of the tag in the format TAG_ID:TAG_ALIAS + * @param string $language The language of the tag + * + * @return string URL link to pass to the router + * + * @since 4.2.0 + * @throws Exception + */ + public static function getComponentTagRoute(string $id, string $language = '*'): string + { + $needles = [ + 'tag' => [(int) $id], + 'language' => $language, + ]; + + if ($id < 1) { + $link = ''; + } else { + $link = 'index.php?option=com_tags&view=tag&id=' . $id; + + if ($item = self::_findItem($needles)) { + $link .= '&Itemid=' . $item; + } else { + $needles = [ + 'tags' => [1, 0], + 'language' => $language, + ]; + + if ($item = self::_findItem($needles)) { + $link .= '&Itemid=' . $item; + } + } + } + + return $link; + } + + /** + * Tries to load the router for the tags view. + * + * @return string URL link to pass to the router + * + * @since 3.7 + * @throws Exception + * @deprecated 5.0.0 + */ + public static function getTagsRoute() + { + @trigger_error('This function is replaced by the getComponentTagsRoute()', E_USER_DEPRECATED); + + return self::getComponentTagsRoute(); + } + + /** + * Tries to load the router for the tags view. + * + * @param string $language The language of the tag + * + * @return string URL link to pass to the router + * + * @since 4.2.0 + * @throws Exception + */ + public static function getComponentTagsRoute(string $language = '*'): string + { + $needles = [ + 'tags' => [0], + 'language' => $language, + ]; + + $link = 'index.php?option=com_tags&view=tags'; + + if ($item = self::_findItem($needles)) { + $link .= '&Itemid=' . $item; + } + + return $link; + } + + /** + * Find Item static function + * + * @param array $needles Array used to get the language value + * + * @return null + * + * @throws Exception + */ + protected static function _findItem($needles = null) + { + $menus = AbstractMenu::getInstance('site'); + $language = $needles['language'] ?? '*'; + + // Prepare the reverse lookup array. + if (self::$lookup === null) { + self::$lookup = array(); + + $component = ComponentHelper::getComponent('com_tags'); + $items = $menus->getItems('component_id', $component->id); + + if ($items) { + foreach ($items as $item) { + if (isset($item->query, $item->query['view'])) { + $lang = ($item->language != '' ? $item->language : '*'); + + if (!isset(self::$lookup[$lang])) { + self::$lookup[$lang] = array(); + } + + $view = $item->query['view']; + + if (!isset(self::$lookup[$lang][$view])) { + self::$lookup[$lang][$view] = array(); + } + + // Only match menu items that list one tag + if (isset($item->query['id']) && is_array($item->query['id'])) { + foreach ($item->query['id'] as $position => $tagId) { + if (!isset(self::$lookup[$lang][$view][$item->query['id'][$position]]) || count($item->query['id']) == 1) { + self::$lookup[$lang][$view][$item->query['id'][$position]] = $item->id; + } + } + } elseif ($view == 'tags') { + self::$lookup[$lang]['tags'][] = $item->id; + } + } + } + } + } + + if ($needles) { + foreach ($needles as $view => $ids) { + if (isset(self::$lookup[$language][$view])) { + foreach ($ids as $id) { + if (isset(self::$lookup[$language][$view][(int) $id])) { + return self::$lookup[$language][$view][(int) $id]; + } + } + } + } + } else { + $active = $menus->getActive(); + + if ($active) { + return $active->id; + } + } + + return null; + } } diff --git a/components/com_tags/src/Model/TagModel.php b/components/com_tags/src/Model/TagModel.php index af2c4ad3a6415..8059ec6b2bd31 100644 --- a/components/com_tags/src/Model/TagModel.php +++ b/components/com_tags/src/Model/TagModel.php @@ -1,4 +1,5 @@ link = RouteHelper::getItemRoute( - $item->content_item_id, - $item->core_alias, - $item->core_catid, - $item->core_language, - $item->type_alias, - $item->router - ); - - // Get display date - switch ($this->state->params->get('tag_list_show_date')) - { - case 'modified': - $item->displayDate = $item->core_modified_time; - break; - - case 'created': - $item->displayDate = $item->core_created_time; - break; - - default: - $item->displayDate = ($item->core_publish_up == 0) ? $item->core_created_time : $item->core_publish_up; - break; - } - } - } - - return $items; - } - - /** - * Method to build an SQL query to load the list data of all items with a given tag. - * - * @return string An SQL query - * - * @since 3.1 - */ - protected function getListQuery() - { - $tagId = $this->getState('tag.id') ? : ''; - - $typesr = $this->getState('tag.typesr'); - $orderByOption = $this->getState('list.ordering', 'c.core_title'); - $includeChildren = $this->state->params->get('include_children', 0); - $orderDir = $this->getState('list.direction', 'ASC'); - $matchAll = $this->getState('params')->get('return_any_or_all', 1); - $language = $this->getState('tag.language'); - $stateFilter = $this->getState('tag.state'); - - // Optionally filter on language - if (empty($language)) - { - $language = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter', 'all'); - } - - $query = (new TagsHelper)->getTagItemsQuery($tagId, $typesr, $includeChildren, $orderByOption, $orderDir, $matchAll, $language, $stateFilter); - - if ($this->state->get('list.filter')) - { - $db = $this->getDatabase(); - $query->where($db->quoteName('c.core_title') . ' LIKE ' . $db->quote('%' . $this->state->get('list.filter') . '%')); - } - - return $query; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 3.1 - */ - protected function populateState($ordering = 'c.core_title', $direction = 'ASC') - { - $app = Factory::getApplication(); - - // Load the parameters. - $params = $app->isClient('administrator') ? ComponentHelper::getParams('com_tags') : $app->getParams(); - - $this->setState('params', $params); - - // Load state from the request. - $ids = (array) $app->input->get('id', array(), 'string'); - - if (count($ids) == 1) - { - $ids = explode(',', $ids[0]); - } - - $ids = ArrayHelper::toInteger($ids); - - // Remove zero values resulting from bad input - $ids = array_filter($ids); - - $pkString = implode(',', $ids); - - $this->setState('tag.id', $pkString); - - // Get the selected list of types from the request. If none are specified all are used. - $typesr = $app->input->get('types', array(), 'array'); - - if ($typesr) - { - // Implode is needed because the array can contain a string with a coma separated list of ids - $typesr = implode(',', $typesr); - - // Sanitise - $typesr = explode(',', $typesr); - $typesr = ArrayHelper::toInteger($typesr); - - $this->setState('tag.typesr', $typesr); - } - - $language = $app->input->getString('tag_list_language_filter'); - $this->setState('tag.language', $language); - - // List state information - $format = $app->input->getWord('format'); - - if ($format === 'feed') - { - $limit = $app->get('feed_limit'); - } - else - { - $limit = $params->get('display_num', $app->get('list_limit', 20)); - $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $limit, 'uint'); - } - - $this->setState('list.limit', $limit); - - $offset = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $offset); - - $itemid = $pkString . ':' . $app->input->get('Itemid', 0, 'int'); - $orderCol = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); - $orderCol = !$orderCol ? $this->state->params->get('tag_list_orderby', 'c.core_title') : $orderCol; - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'c.core_title'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_order_direction', 'filter_order_Dir', '', 'string'); - $listOrder = !$listOrder ? $this->state->params->get('tag_list_orderby_direction', 'ASC') : $listOrder; - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $this->setState('tag.state', 1); - - // Optional filter text - $filterSearch = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_search', 'filter-search', '', 'string'); - $this->setState('list.filter', $filterSearch); - } - - /** - * Method to get tag data for the current tag or tags - * - * @param integer $pk An optional ID - * - * @return array - * - * @since 3.1 - */ - public function getItem($pk = null) - { - if (!isset($this->item)) - { - $this->item = []; - - if (empty($pk)) - { - $pk = $this->getState('tag.id'); - } - - // Get a level row instance. - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - $table = $this->getTable(); - - $idsArray = explode(',', $pk); - - // Attempt to load the rows into an array. - foreach ($idsArray as $id) - { - try - { - $table->load($id); - - // Check published state. - if ($published = $this->getState('tag.state')) - { - if ($table->published != $published) - { - continue; - } - } - - if (!in_array($table->access, Factory::getUser()->getAuthorisedViewLevels())) - { - continue; - } - - // Convert the Table to a clean CMSObject. - $properties = $table->getProperties(1); - $this->item[] = ArrayHelper::toObject($properties, CMSObject::class); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - } - - if (!$this->item) - { - throw new \Exception(Text::_('COM_TAGS_TAG_NOT_FOUND'), 404); - } - - return $this->item; - } - - /** - * Increment the hit counter. - * - * @param integer $pk Optional primary key of the article to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - * - * @since 3.2 - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('tag.id'); - - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - $table = $this->getTable(); - $table->hit($pk); - - // Load the table data for later - $table->load($pk); - - if (!$table->hasPrimaryKey()) - { - throw new \Exception(Text::_('COM_TAGS_TAG_NOT_FOUND'), 404); - } - } - - return true; - } + /** + * The tags that apply. + * + * @var object + * @since 3.1 + */ + protected $tag = null; + + /** + * The list of items associated with the tags. + * + * @var array + * @since 3.1 + */ + protected $items = null; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'core_content_id', 'c.core_content_id', + 'core_title', 'c.core_title', + 'core_type_alias', 'c.core_type_alias', + 'core_checked_out_user_id', 'c.core_checked_out_user_id', + 'core_checked_out_time', 'c.core_checked_out_time', + 'core_catid', 'c.core_catid', + 'core_state', 'c.core_state', + 'core_access', 'c.core_access', + 'core_created_user_id', 'c.core_created_user_id', + 'core_created_time', 'c.core_created_time', + 'core_modified_time', 'c.core_modified_time', + 'core_ordering', 'c.core_ordering', + 'core_featured', 'c.core_featured', + 'core_language', 'c.core_language', + 'core_hits', 'c.core_hits', + 'core_publish_up', 'c.core_publish_up', + 'core_publish_down', 'c.core_publish_down', + 'core_images', 'c.core_images', + 'core_urls', 'c.core_urls', + 'match_count', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to get a list of items for a list of tags. + * + * @return mixed An array of objects on success, false on failure. + * + * @since 3.1 + */ + public function getItems() + { + // Invoke the parent getItems method to get the main list + $items = parent::getItems(); + + if (!empty($items)) { + foreach ($items as $item) { + $item->link = RouteHelper::getItemRoute( + $item->content_item_id, + $item->core_alias, + $item->core_catid, + $item->core_language, + $item->type_alias, + $item->router + ); + + // Get display date + switch ($this->state->params->get('tag_list_show_date')) { + case 'modified': + $item->displayDate = $item->core_modified_time; + break; + + case 'created': + $item->displayDate = $item->core_created_time; + break; + + default: + $item->displayDate = ($item->core_publish_up == 0) ? $item->core_created_time : $item->core_publish_up; + break; + } + } + } + + return $items; + } + + /** + * Method to build an SQL query to load the list data of all items with a given tag. + * + * @return string An SQL query + * + * @since 3.1 + */ + protected function getListQuery() + { + $tagId = $this->getState('tag.id') ? : ''; + + $typesr = $this->getState('tag.typesr'); + $orderByOption = $this->getState('list.ordering', 'c.core_title'); + $includeChildren = $this->state->params->get('include_children', 0); + $orderDir = $this->getState('list.direction', 'ASC'); + $matchAll = $this->getState('params')->get('return_any_or_all', 1); + $language = $this->getState('tag.language'); + $stateFilter = $this->getState('tag.state'); + + // Optionally filter on language + if (empty($language)) { + $language = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter', 'all'); + } + + $query = (new TagsHelper())->getTagItemsQuery($tagId, $typesr, $includeChildren, $orderByOption, $orderDir, $matchAll, $language, $stateFilter); + + if ($this->state->get('list.filter')) { + $db = $this->getDatabase(); + $query->where($db->quoteName('c.core_title') . ' LIKE ' . $db->quote('%' . $this->state->get('list.filter') . '%')); + } + + return $query; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.1 + */ + protected function populateState($ordering = 'c.core_title', $direction = 'ASC') + { + $app = Factory::getApplication(); + + // Load the parameters. + $params = $app->isClient('administrator') ? ComponentHelper::getParams('com_tags') : $app->getParams(); + + $this->setState('params', $params); + + // Load state from the request. + $ids = (array) $app->input->get('id', array(), 'string'); + + if (count($ids) == 1) { + $ids = explode(',', $ids[0]); + } + + $ids = ArrayHelper::toInteger($ids); + + // Remove zero values resulting from bad input + $ids = array_filter($ids); + + $pkString = implode(',', $ids); + + $this->setState('tag.id', $pkString); + + // Get the selected list of types from the request. If none are specified all are used. + $typesr = $app->input->get('types', array(), 'array'); + + if ($typesr) { + // Implode is needed because the array can contain a string with a coma separated list of ids + $typesr = implode(',', $typesr); + + // Sanitise + $typesr = explode(',', $typesr); + $typesr = ArrayHelper::toInteger($typesr); + + $this->setState('tag.typesr', $typesr); + } + + $language = $app->input->getString('tag_list_language_filter'); + $this->setState('tag.language', $language); + + // List state information + $format = $app->input->getWord('format'); + + if ($format === 'feed') { + $limit = $app->get('feed_limit'); + } else { + $limit = $params->get('display_num', $app->get('list_limit', 20)); + $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $limit, 'uint'); + } + + $this->setState('list.limit', $limit); + + $offset = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $offset); + + $itemid = $pkString . ':' . $app->input->get('Itemid', 0, 'int'); + $orderCol = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); + $orderCol = !$orderCol ? $this->state->params->get('tag_list_orderby', 'c.core_title') : $orderCol; + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'c.core_title'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_order_direction', 'filter_order_Dir', '', 'string'); + $listOrder = !$listOrder ? $this->state->params->get('tag_list_orderby_direction', 'ASC') : $listOrder; + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $this->setState('tag.state', 1); + + // Optional filter text + $filterSearch = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_search', 'filter-search', '', 'string'); + $this->setState('list.filter', $filterSearch); + } + + /** + * Method to get tag data for the current tag or tags + * + * @param integer $pk An optional ID + * + * @return array + * + * @since 3.1 + */ + public function getItem($pk = null) + { + if (!isset($this->item)) { + $this->item = []; + + if (empty($pk)) { + $pk = $this->getState('tag.id'); + } + + // Get a level row instance. + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + $table = $this->getTable(); + + $idsArray = explode(',', $pk); + + // Attempt to load the rows into an array. + foreach ($idsArray as $id) { + try { + $table->load($id); + + // Check published state. + if ($published = $this->getState('tag.state')) { + if ($table->published != $published) { + continue; + } + } + + if (!in_array($table->access, Factory::getUser()->getAuthorisedViewLevels())) { + continue; + } + + // Convert the Table to a clean CMSObject. + $properties = $table->getProperties(1); + $this->item[] = ArrayHelper::toObject($properties, CMSObject::class); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + } + + if (!$this->item) { + throw new \Exception(Text::_('COM_TAGS_TAG_NOT_FOUND'), 404); + } + + return $this->item; + } + + /** + * Increment the hit counter. + * + * @param integer $pk Optional primary key of the article to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + * + * @since 3.2 + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('tag.id'); + + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + $table = $this->getTable(); + $table->hit($pk); + + // Load the table data for later + $table->load($pk); + + if (!$table->hasPrimaryKey()) { + throw new \Exception(Text::_('COM_TAGS_TAG_NOT_FOUND'), 404); + } + } + + return true; + } } diff --git a/components/com_tags/src/Model/TagsModel.php b/components/com_tags/src/Model/TagsModel.php index 06d7486bbdcbd..f55d9f596acf9 100644 --- a/components/com_tags/src/Model/TagsModel.php +++ b/components/com_tags/src/Model/TagsModel.php @@ -1,4 +1,5 @@ input->getInt('parent_id'); - $this->setState('tag.parent_id', $pid); - - $language = $app->input->getString('tag_list_language_filter'); - $this->setState('tag.language', $language); - - $offset = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.offset', $offset); - $app = Factory::getApplication(); - - $params = $app->getParams(); - $this->setState('params', $params); - - $this->setState('list.limit', $params->get('maximum', 200)); - - $this->setState('filter.published', 1); - $this->setState('filter.access', true); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_tags')) && (!$user->authorise('core.edit', 'com_tags'))) - { - $this->setState('filter.published', 1); - } - - // Optional filter text - $itemid = $pid . ':' . $app->input->getInt('Itemid', 0); - $filterSearch = $app->getUserStateFromRequest('com_tags.tags.list.' . $itemid . '.filter_search', 'filter-search', '', 'string'); - $this->setState('list.filter', $filterSearch); - } - - /** - * Method to build an SQL query to load the list data. - * - * @return string An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - $app = Factory::getApplication(); - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - $pid = (int) $this->getState('tag.parent_id'); - $orderby = $this->state->params->get('all_tags_orderby', 'title'); - $published = (int) $this->state->params->get('published', 1); - $orderDirection = $this->state->params->get('all_tags_orderby_direction', 'ASC'); - $language = $this->getState('tag.language'); - - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select required fields from the tags. - $query->select('a.*, u.name as created_by_user_name, u.email') - ->from($db->quoteName('#__tags', 'a')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('a.created_user_id') . ' = ' . $db->quoteName('u.id')) - ->whereIn($db->quoteName('a.access'), $groups); - - if (!empty($pid)) - { - $query->where($db->quoteName('a.parent_id') . ' = :pid') - ->bind(':pid', $pid, ParameterType::INTEGER); - } - - // Exclude the root. - $query->where($db->quoteName('a.parent_id') . ' <> 0'); - - // Optionally filter on language - if (empty($language)) - { - $language = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter', 'all'); - } - - if ($language !== 'all') - { - if ($language === 'current_language') - { - $language = ContentHelper::getCurrentLanguage(); - } - - $query->whereIn($db->quoteName('language'), [$language, '*'], ParameterType::STRING); - } - - // List state information - $format = $app->input->getWord('format'); - - if ($format === 'feed') - { - $limit = $app->get('feed_limit'); - } - else - { - if ($this->state->params->get('show_pagination_limit')) - { - $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); - } - else - { - $limit = $this->state->params->get('maximum', 20); - } - } - - $this->setState('list.limit', $limit); - - $offset = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $offset); - - // Optionally filter on entered value - if ($this->state->get('list.filter')) - { - $title = '%' . $this->state->get('list.filter') . '%'; - $query->where($db->quoteName('a.title') . ' LIKE :title') - ->bind(':title', $title); - } - - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - - $query->order($db->quoteName($orderby) . ' ' . $orderDirection . ', a.title ASC'); - - return $query; - } + /** + * Model context string. + * + * @var string + * @since 3.1 + */ + public $_context = 'com_tags.tags'; + + /** + * Method to auto-populate the model state. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @note Calling getState in this method will result in recursion. + * + * @since 3.1 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + + // Load state from the request. + $pid = $app->input->getInt('parent_id'); + $this->setState('tag.parent_id', $pid); + + $language = $app->input->getString('tag_list_language_filter'); + $this->setState('tag.language', $language); + + $offset = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.offset', $offset); + $app = Factory::getApplication(); + + $params = $app->getParams(); + $this->setState('params', $params); + + $this->setState('list.limit', $params->get('maximum', 200)); + + $this->setState('filter.published', 1); + $this->setState('filter.access', true); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_tags')) && (!$user->authorise('core.edit', 'com_tags'))) { + $this->setState('filter.published', 1); + } + + // Optional filter text + $itemid = $pid . ':' . $app->input->getInt('Itemid', 0); + $filterSearch = $app->getUserStateFromRequest('com_tags.tags.list.' . $itemid . '.filter_search', 'filter-search', '', 'string'); + $this->setState('list.filter', $filterSearch); + } + + /** + * Method to build an SQL query to load the list data. + * + * @return string An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + $app = Factory::getApplication(); + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + $pid = (int) $this->getState('tag.parent_id'); + $orderby = $this->state->params->get('all_tags_orderby', 'title'); + $published = (int) $this->state->params->get('published', 1); + $orderDirection = $this->state->params->get('all_tags_orderby_direction', 'ASC'); + $language = $this->getState('tag.language'); + + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select required fields from the tags. + $query->select('a.*, u.name as created_by_user_name, u.email') + ->from($db->quoteName('#__tags', 'a')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('a.created_user_id') . ' = ' . $db->quoteName('u.id')) + ->whereIn($db->quoteName('a.access'), $groups); + + if (!empty($pid)) { + $query->where($db->quoteName('a.parent_id') . ' = :pid') + ->bind(':pid', $pid, ParameterType::INTEGER); + } + + // Exclude the root. + $query->where($db->quoteName('a.parent_id') . ' <> 0'); + + // Optionally filter on language + if (empty($language)) { + $language = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter', 'all'); + } + + if ($language !== 'all') { + if ($language === 'current_language') { + $language = ContentHelper::getCurrentLanguage(); + } + + $query->whereIn($db->quoteName('language'), [$language, '*'], ParameterType::STRING); + } + + // List state information + $format = $app->input->getWord('format'); + + if ($format === 'feed') { + $limit = $app->get('feed_limit'); + } else { + if ($this->state->params->get('show_pagination_limit')) { + $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); + } else { + $limit = $this->state->params->get('maximum', 20); + } + } + + $this->setState('list.limit', $limit); + + $offset = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $offset); + + // Optionally filter on entered value + if ($this->state->get('list.filter')) { + $title = '%' . $this->state->get('list.filter') . '%'; + $query->where($db->quoteName('a.title') . ' LIKE :title') + ->bind(':title', $title); + } + + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + + $query->order($db->quoteName($orderby) . ' ' . $orderDirection . ', a.title ASC'); + + return $query; + } } diff --git a/components/com_tags/src/Service/Router.php b/components/com_tags/src/Service/Router.php index d840c208f8a59..dabbd2f4f9526 100644 --- a/components/com_tags/src/Service/Router.php +++ b/components/com_tags/src/Service/Router.php @@ -1,4 +1,5 @@ db = $db; - - parent::__construct($app, $menu); - } - - /** - * Build the route for the com_tags component - * - * @param array &$query An array of URL arguments - * - * @return array The URL arguments to use to assemble the subsequent URL. - * - * @since 3.3 - */ - public function build(&$query) - { - $segments = array(); - - // Get a menu item based on Itemid or currently active - - // We need a menu item. Either the one specified in the query, or the current active one if none specified - if (empty($query['Itemid'])) - { - $menuItem = $this->menu->getActive(); - } - else - { - $menuItem = $this->menu->getItem($query['Itemid']); - } - - $mView = empty($menuItem->query['view']) ? null : $menuItem->query['view']; - $mId = empty($menuItem->query['id']) ? null : $menuItem->query['id']; - - if (is_array($mId)) - { - $mId = ArrayHelper::toInteger($mId); - } - - $view = ''; - - if (isset($query['view'])) - { - $view = $query['view']; - - if (empty($query['Itemid'])) - { - $segments[] = $view; - } - - unset($query['view']); - } - - // Are we dealing with a tag that is attached to a menu item? - if ($mView == $view && isset($query['id']) && $mId == $query['id']) - { - unset($query['id']); - - return $segments; - } - - if ($view === 'tag') - { - $notActiveTag = is_array($mId) ? (count($mId) > 1 || $mId[0] != (int) $query['id']) : ($mId != (int) $query['id']); - - if ($notActiveTag || $mView != $view) - { - // ID in com_tags can be either an integer, a string or an array of IDs - $id = is_array($query['id']) ? implode(',', $query['id']) : $query['id']; - $segments[] = $id; - } - - unset($query['id']); - } - - if (isset($query['layout'])) - { - if ((!empty($query['Itemid']) && isset($menuItem->query['layout']) - && $query['layout'] == $menuItem->query['layout']) - || $query['layout'] === 'default') - { - unset($query['layout']); - } - } - - $total = count($segments); - - for ($i = 0; $i < $total; $i++) - { - $segments[$i] = str_replace(':', '-', $segments[$i]); - $position = strpos($segments[$i], '-'); - - if ($position) - { - // Remove id from segment - $segments[$i] = substr($segments[$i], $position + 1); - } - } - - return $segments; - } - - /** - * Parse the segments of a URL. - * - * @param array &$segments The segments of the URL to parse. - * - * @return array The URL attributes to be used by the application. - * - * @since 3.3 - */ - public function parse(&$segments) - { - $total = count($segments); - $vars = array(); - - for ($i = 0; $i < $total; $i++) - { - $segments[$i] = preg_replace('/-/', ':', $segments[$i], 1); - } - - // Get the active menu item. - $item = $this->menu->getActive(); - - // Count route segments - $count = count($segments); - - // Standard routing for tags. - if (!isset($item)) - { - $vars['view'] = $segments[0]; - $vars['id'] = $this->fixSegment($segments[$count - 1]); - unset($segments[0]); - unset($segments[$count - 1]); - - return $vars; - } - - $vars['id'] = $this->fixSegment($segments[0]); - $vars['view'] = 'tag'; - unset($segments[0]); - - return $vars; - } - - /** - * Try to add missing id to segment - * - * @param string $segment One piece of segment of the URL to parse - * - * @return string The segment with founded id - * - * @since 3.7 - */ - protected function fixSegment($segment) - { - // Try to find tag id - $alias = str_replace(':', '-', $segment); - - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('id')) - ->from($this->db->quoteName('#__tags')) - ->where($this->db->quoteName('alias') . ' = :alias') - ->bind(':alias', $alias); - - $id = $this->db->setQuery($query)->loadResult(); - - if ($id) - { - $segment = "$id:$alias"; - } - - return $segment; - } + /** + * The db + * + * @var DatabaseInterface + * + * @since 4.0.0 + */ + private $db; + + /** + * Tags Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + * @param CategoryFactoryInterface $categoryFactory The category object + * @param DatabaseInterface $db The database object + * + * @since 4.0.0 + */ + public function __construct(SiteApplication $app, AbstractMenu $menu, ?CategoryFactoryInterface $categoryFactory, DatabaseInterface $db) + { + $this->db = $db; + + parent::__construct($app, $menu); + } + + /** + * Build the route for the com_tags component + * + * @param array &$query An array of URL arguments + * + * @return array The URL arguments to use to assemble the subsequent URL. + * + * @since 3.3 + */ + public function build(&$query) + { + $segments = array(); + + // Get a menu item based on Itemid or currently active + + // We need a menu item. Either the one specified in the query, or the current active one if none specified + if (empty($query['Itemid'])) { + $menuItem = $this->menu->getActive(); + } else { + $menuItem = $this->menu->getItem($query['Itemid']); + } + + $mView = empty($menuItem->query['view']) ? null : $menuItem->query['view']; + $mId = empty($menuItem->query['id']) ? null : $menuItem->query['id']; + + if (is_array($mId)) { + $mId = ArrayHelper::toInteger($mId); + } + + $view = ''; + + if (isset($query['view'])) { + $view = $query['view']; + + if (empty($query['Itemid'])) { + $segments[] = $view; + } + + unset($query['view']); + } + + // Are we dealing with a tag that is attached to a menu item? + if ($mView == $view && isset($query['id']) && $mId == $query['id']) { + unset($query['id']); + + return $segments; + } + + if ($view === 'tag') { + $notActiveTag = is_array($mId) ? (count($mId) > 1 || $mId[0] != (int) $query['id']) : ($mId != (int) $query['id']); + + if ($notActiveTag || $mView != $view) { + // ID in com_tags can be either an integer, a string or an array of IDs + $id = is_array($query['id']) ? implode(',', $query['id']) : $query['id']; + $segments[] = $id; + } + + unset($query['id']); + } + + if (isset($query['layout'])) { + if ( + (!empty($query['Itemid']) && isset($menuItem->query['layout']) + && $query['layout'] == $menuItem->query['layout']) + || $query['layout'] === 'default' + ) { + unset($query['layout']); + } + } + + $total = count($segments); + + for ($i = 0; $i < $total; $i++) { + $segments[$i] = str_replace(':', '-', $segments[$i]); + $position = strpos($segments[$i], '-'); + + if ($position) { + // Remove id from segment + $segments[$i] = substr($segments[$i], $position + 1); + } + } + + return $segments; + } + + /** + * Parse the segments of a URL. + * + * @param array &$segments The segments of the URL to parse. + * + * @return array The URL attributes to be used by the application. + * + * @since 3.3 + */ + public function parse(&$segments) + { + $total = count($segments); + $vars = array(); + + for ($i = 0; $i < $total; $i++) { + $segments[$i] = preg_replace('/-/', ':', $segments[$i], 1); + } + + // Get the active menu item. + $item = $this->menu->getActive(); + + // Count route segments + $count = count($segments); + + // Standard routing for tags. + if (!isset($item)) { + $vars['view'] = $segments[0]; + $vars['id'] = $this->fixSegment($segments[$count - 1]); + unset($segments[0]); + unset($segments[$count - 1]); + + return $vars; + } + + $vars['id'] = $this->fixSegment($segments[0]); + $vars['view'] = 'tag'; + unset($segments[0]); + + return $vars; + } + + /** + * Try to add missing id to segment + * + * @param string $segment One piece of segment of the URL to parse + * + * @return string The segment with founded id + * + * @since 3.7 + */ + protected function fixSegment($segment) + { + // Try to find tag id + $alias = str_replace(':', '-', $segment); + + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__tags')) + ->where($this->db->quoteName('alias') . ' = :alias') + ->bind(':alias', $alias); + + $id = $this->db->setQuery($query)->loadResult(); + + if ($id) { + $segment = "$id:$alias"; + } + + return $segment; + } } diff --git a/components/com_tags/src/View/Tag/FeedView.php b/components/com_tags/src/View/Tag/FeedView.php index 7fa154cd44916..6356ff8719cfd 100644 --- a/components/com_tags/src/View/Tag/FeedView.php +++ b/components/com_tags/src/View/Tag/FeedView.php @@ -1,4 +1,5 @@ input->get('id', array(), 'int'); - $i = 0; - $tagIds = ''; - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - foreach ($ids as $id) - { - if ($i !== 0) - { - $tagIds .= '&'; - } - - $tagIds .= 'id[' . $i . ']=' . $id; - - $i++; - } - - $this->document->link = Route::_('index.php?option=com_tags&view=tag&' . $tagIds); - - $app->input->set('limit', $app->get('feed_limit')); - $siteEmail = $app->get('mailfrom'); - $fromName = $app->get('fromname'); - $feedEmail = $app->get('feed_email', 'none'); - - $this->document->editor = $fromName; - - if ($feedEmail !== 'none') - { - $this->document->editorEmail = $siteEmail; - } - - // Get some data from the model - $items = $this->get('Items'); - - if ($items !== false) - { - foreach ($items as $item) - { - // Strip HTML from feed item title - $title = $this->escape($item->core_title); - $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); - - // Strip HTML from feed item description text - $description = $item->core_body; - $author = $item->core_created_by_alias ?: $item->author; - $date = ($item->displayDate ? date('r', strtotime($item->displayDate)) : ''); - - // Load individual item creator class - $feeditem = new FeedItem; - $feeditem->title = $title; - $feeditem->link = Route::_($item->link); - $feeditem->description = $description; - $feeditem->date = $date; - $feeditem->category = $title; - $feeditem->author = $author; - - if ($feedEmail === 'site') - { - $item->authorEmail = $siteEmail; - } - elseif ($feedEmail === 'author') - { - $item->authorEmail = $item->author_email; - } - - // Loads item info into RSS array - $this->document->addItem($feeditem); - } - } - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $ids = (array) $app->input->get('id', array(), 'int'); + $i = 0; + $tagIds = ''; + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + foreach ($ids as $id) { + if ($i !== 0) { + $tagIds .= '&'; + } + + $tagIds .= 'id[' . $i . ']=' . $id; + + $i++; + } + + $this->document->link = Route::_('index.php?option=com_tags&view=tag&' . $tagIds); + + $app->input->set('limit', $app->get('feed_limit')); + $siteEmail = $app->get('mailfrom'); + $fromName = $app->get('fromname'); + $feedEmail = $app->get('feed_email', 'none'); + + $this->document->editor = $fromName; + + if ($feedEmail !== 'none') { + $this->document->editorEmail = $siteEmail; + } + + // Get some data from the model + $items = $this->get('Items'); + + if ($items !== false) { + foreach ($items as $item) { + // Strip HTML from feed item title + $title = $this->escape($item->core_title); + $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); + + // Strip HTML from feed item description text + $description = $item->core_body; + $author = $item->core_created_by_alias ?: $item->author; + $date = ($item->displayDate ? date('r', strtotime($item->displayDate)) : ''); + + // Load individual item creator class + $feeditem = new FeedItem(); + $feeditem->title = $title; + $feeditem->link = Route::_($item->link); + $feeditem->description = $description; + $feeditem->date = $date; + $feeditem->category = $title; + $feeditem->author = $author; + + if ($feedEmail === 'site') { + $item->authorEmail = $siteEmail; + } elseif ($feedEmail === 'author') { + $item->authorEmail = $item->author_email; + } + + // Loads item info into RSS array + $this->document->addItem($feeditem); + } + } + } } diff --git a/components/com_tags/src/View/Tag/HtmlView.php b/components/com_tags/src/View/Tag/HtmlView.php index c700751e65fea..b85e4f5e5c576 100644 --- a/components/com_tags/src/View/Tag/HtmlView.php +++ b/components/com_tags/src/View/Tag/HtmlView.php @@ -1,4 +1,5 @@ getParams(); - - // Get some data from the models - $state = $this->get('State'); - $items = $this->get('Items'); - $item = $this->get('Item'); - $children = $this->get('Children'); - $parent = $this->get('Parent'); - $pagination = $this->get('Pagination'); - - // Flag indicates to not add limitstart=0 to URL - $pagination->hideEmptyLimitstart = true; - - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check whether access level allows access. - // @TODO: Should already be computed in $item->params->get('access-view') - $user = $this->getCurrentUser(); - $groups = $user->getAuthorisedViewLevels(); - - foreach ($item as $itemElement) - { - if (!in_array($itemElement->access, $groups)) - { - unset($itemElement); - } - - // Prepare the data. - if (!empty($itemElement)) - { - $temp = new Registry($itemElement->params); - $itemElement->params = clone $params; - $itemElement->params->merge($temp); - $itemElement->params = (array) json_decode($itemElement->params); - $itemElement->metadata = new Registry($itemElement->metadata); - } - } - - if ($items !== false) - { - PluginHelper::importPlugin('content'); - - foreach ($items as $itemElement) - { - $itemElement->event = new \stdClass; - - // For some plugins. - !empty($itemElement->core_body) ? $itemElement->text = $itemElement->core_body : $itemElement->text = null; - - $itemElement->core_params = new Registry($itemElement->core_params); - - Factory::getApplication()->triggerEvent('onContentPrepare', ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0]); - - $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', - ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] - ); - $itemElement->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', - ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] - ); - $itemElement->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', - ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] - ); - $itemElement->event->afterDisplayContent = trim(implode("\n", $results)); - - // Write the results back into the body - if (!empty($itemElement->core_body)) - { - $itemElement->core_body = $itemElement->text; - } - - // Categories store the images differently so lets re-map it so the display is correct - if ($itemElement->type_alias === 'com_content.category') - { - $itemElement->core_images = json_encode( - array( - 'image_intro' => $itemElement->core_params->get('image', ''), - 'image_intro_alt' => $itemElement->core_params->get('image_alt', '') - ) - ); - } - } - } - - $this->state = $state; - $this->items = $items; - $this->children = $children; - $this->parent = $parent; - $this->pagination = $pagination; - $this->user = $user; - $this->item = $item; - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - - // Merge tag params. If this is single-tag view, menu params override tag params - // Otherwise, article params override menu item params - $this->params = $this->state->get('params'); - $active = $app->getMenu()->getActive(); - $temp = clone $this->params; - - // Convert item params to a Registry object - $item[0]->params = new Registry($item[0]->params); - - // Check to see which parameters should take priority - if ($active) - { - $currentLink = $active->link; - - // If the current view is the active item and a tag view for one tag, then the menu item params take priority - if (strpos($currentLink, 'view=tag') && strpos($currentLink, '&id[0]=' . (string) $item[0]->id)) - { - // $item[0]->params are the tag params, $temp are the menu item params - // Merge so that the menu item params take priority - $item[0]->params->merge($temp); - - // Load layout from active query (in case it is an alternative menu item) - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - } - else - { - // Current menuitem is not a single tag view, so the tag params take priority. - // Merge the menu item params with the tag params so that the tag params take priority - $temp->merge($item[0]->params); - $item[0]->params = $temp; - - // Check for alternative layouts (since we are not in a single-article menu item) - // Single-article menu item layout takes priority over alt layout for an article - if ($layout = $item[0]->params->get('tag_layout')) - { - $this->setLayout($layout); - } - } - } - else - { - // Merge so that item params take priority - $temp->merge($item[0]->params); - $item[0]->params = $temp; - - // Check for alternative layouts (since we are not in a single-tag menu item) - // Single-tag menu item layout takes priority over alt layout for an article - if ($layout = $item[0]->params->get('tag_layout')) - { - $this->setLayout($layout); - } - } - - // Increment the hit counter - $model = $this->getModel(); - $model->hit(); - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - $menu = $app->getMenu()->getActive(); - $this->tags_title = $this->getTagsTitle(); - $pathway = $app->getPathway(); - $title = ''; - - // Highest priority for "Browser Page Title". - if ($menu) - { - $title = $menu->getParams()->get('page_title', ''); - } - - if ($this->tags_title) - { - $this->params->def('page_heading', $this->tags_title); - $title = $title ?: $this->tags_title; - } - elseif ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - $title = $title ?: $this->params->get('page_title', $menu->title); - } - - $this->setDocumentTitle($title); - $pathway->addItem($title); - - foreach ($this->item as $itemElement) - { - if ($itemElement->metadesc) - { - $this->document->setDescription($itemElement->metadesc); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } - - if (count($this->item) === 1) - { - foreach ($this->item[0]->metadata->toArray() as $k => $v) - { - if ($v) - { - $this->document->setMetaData($k, $v); - } - } - } - - if ($this->params->get('show_feed_link', 1) == 1) - { - $link = '&format=feed&limitstart='; - $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); - $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); - $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); - $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); - } - } - - /** - * Creates the tags title for the output - * - * @return string - * - * @since 3.1 - */ - protected function getTagsTitle() - { - $tags_title = array(); - - if (!empty($this->item)) - { - $user = $this->getCurrentUser(); - $groups = $user->getAuthorisedViewLevels(); - - foreach ($this->item as $item) - { - if (in_array($item->access, $groups)) - { - $tags_title[] = $item->title; - } - } - } - - return implode(' ', $tags_title); - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.1 + */ + protected $state; + + /** + * List of items associated with the tag + * + * @var \stdClass[]|false + * + * @since 3.1 + */ + protected $items; + + /** + * Tag data for the current tag or tags (on success, false on failure) + * + * @var \Joomla\CMS\Object\CMSObject|boolean + * + * @since 3.1 + */ + protected $item; + + /** + * UNUSED + * + * @var null + * + * @since 3.1 + */ + protected $children; + + /** + * UNUSED + * + * @var null + * + * @since 3.1 + */ + protected $parent; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.1 + */ + protected $pagination; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 3.1 + */ + protected $params; + + /** + * Array of tags title + * + * @var array + * + * @since 3.1 + */ + protected $tags_title; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The logged in user + * + * @var User|null + * + * @since 4.0.0 + */ + protected $user = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.1 + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $params = $app->getParams(); + + // Get some data from the models + $state = $this->get('State'); + $items = $this->get('Items'); + $item = $this->get('Item'); + $children = $this->get('Children'); + $parent = $this->get('Parent'); + $pagination = $this->get('Pagination'); + + // Flag indicates to not add limitstart=0 to URL + $pagination->hideEmptyLimitstart = true; + + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check whether access level allows access. + // @TODO: Should already be computed in $item->params->get('access-view') + $user = $this->getCurrentUser(); + $groups = $user->getAuthorisedViewLevels(); + + foreach ($item as $itemElement) { + if (!in_array($itemElement->access, $groups)) { + unset($itemElement); + } + + // Prepare the data. + if (!empty($itemElement)) { + $temp = new Registry($itemElement->params); + $itemElement->params = clone $params; + $itemElement->params->merge($temp); + $itemElement->params = (array) json_decode($itemElement->params); + $itemElement->metadata = new Registry($itemElement->metadata); + } + } + + if ($items !== false) { + PluginHelper::importPlugin('content'); + + foreach ($items as $itemElement) { + $itemElement->event = new \stdClass(); + + // For some plugins. + !empty($itemElement->core_body) ? $itemElement->text = $itemElement->core_body : $itemElement->text = null; + + $itemElement->core_params = new Registry($itemElement->core_params); + + Factory::getApplication()->triggerEvent('onContentPrepare', ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0]); + + $results = Factory::getApplication()->triggerEvent( + 'onContentAfterTitle', + ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] + ); + $itemElement->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent( + 'onContentBeforeDisplay', + ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] + ); + $itemElement->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent( + 'onContentAfterDisplay', + ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] + ); + $itemElement->event->afterDisplayContent = trim(implode("\n", $results)); + + // Write the results back into the body + if (!empty($itemElement->core_body)) { + $itemElement->core_body = $itemElement->text; + } + + // Categories store the images differently so lets re-map it so the display is correct + if ($itemElement->type_alias === 'com_content.category') { + $itemElement->core_images = json_encode( + array( + 'image_intro' => $itemElement->core_params->get('image', ''), + 'image_intro_alt' => $itemElement->core_params->get('image_alt', '') + ) + ); + } + } + } + + $this->state = $state; + $this->items = $items; + $this->children = $children; + $this->parent = $parent; + $this->pagination = $pagination; + $this->user = $user; + $this->item = $item; + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + + // Merge tag params. If this is single-tag view, menu params override tag params + // Otherwise, article params override menu item params + $this->params = $this->state->get('params'); + $active = $app->getMenu()->getActive(); + $temp = clone $this->params; + + // Convert item params to a Registry object + $item[0]->params = new Registry($item[0]->params); + + // Check to see which parameters should take priority + if ($active) { + $currentLink = $active->link; + + // If the current view is the active item and a tag view for one tag, then the menu item params take priority + if (strpos($currentLink, 'view=tag') && strpos($currentLink, '&id[0]=' . (string) $item[0]->id)) { + // $item[0]->params are the tag params, $temp are the menu item params + // Merge so that the menu item params take priority + $item[0]->params->merge($temp); + + // Load layout from active query (in case it is an alternative menu item) + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + } else { + // Current menuitem is not a single tag view, so the tag params take priority. + // Merge the menu item params with the tag params so that the tag params take priority + $temp->merge($item[0]->params); + $item[0]->params = $temp; + + // Check for alternative layouts (since we are not in a single-article menu item) + // Single-article menu item layout takes priority over alt layout for an article + if ($layout = $item[0]->params->get('tag_layout')) { + $this->setLayout($layout); + } + } + } else { + // Merge so that item params take priority + $temp->merge($item[0]->params); + $item[0]->params = $temp; + + // Check for alternative layouts (since we are not in a single-tag menu item) + // Single-tag menu item layout takes priority over alt layout for an article + if ($layout = $item[0]->params->get('tag_layout')) { + $this->setLayout($layout); + } + } + + // Increment the hit counter + $model = $this->getModel(); + $model->hit(); + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + $menu = $app->getMenu()->getActive(); + $this->tags_title = $this->getTagsTitle(); + $pathway = $app->getPathway(); + $title = ''; + + // Highest priority for "Browser Page Title". + if ($menu) { + $title = $menu->getParams()->get('page_title', ''); + } + + if ($this->tags_title) { + $this->params->def('page_heading', $this->tags_title); + $title = $title ?: $this->tags_title; + } elseif ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + $title = $title ?: $this->params->get('page_title', $menu->title); + } + + $this->setDocumentTitle($title); + $pathway->addItem($title); + + foreach ($this->item as $itemElement) { + if ($itemElement->metadesc) { + $this->document->setDescription($itemElement->metadesc); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } + + if (count($this->item) === 1) { + foreach ($this->item[0]->metadata->toArray() as $k => $v) { + if ($v) { + $this->document->setMetaData($k, $v); + } + } + } + + if ($this->params->get('show_feed_link', 1) == 1) { + $link = '&format=feed&limitstart='; + $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); + $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); + $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); + $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); + } + } + + /** + * Creates the tags title for the output + * + * @return string + * + * @since 3.1 + */ + protected function getTagsTitle() + { + $tags_title = array(); + + if (!empty($this->item)) { + $user = $this->getCurrentUser(); + $groups = $user->getAuthorisedViewLevels(); + + foreach ($this->item as $item) { + if (in_array($item->access, $groups)) { + $tags_title[] = $item->title; + } + } + } + + return implode(' ', $tags_title); + } } diff --git a/components/com_tags/src/View/Tags/FeedView.php b/components/com_tags/src/View/Tags/FeedView.php index 48fdbb3866b89..b5079b0f12647 100644 --- a/components/com_tags/src/View/Tags/FeedView.php +++ b/components/com_tags/src/View/Tags/FeedView.php @@ -1,4 +1,5 @@ document->link = Route::_('index.php?option=com_tags&view=tags'); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $this->document->link = Route::_('index.php?option=com_tags&view=tags'); - $app->input->set('limit', $app->get('feed_limit')); - $siteEmail = $app->get('mailfrom'); - $fromName = $app->get('fromname'); - $feedEmail = $app->get('feed_email', 'none'); + $app->input->set('limit', $app->get('feed_limit')); + $siteEmail = $app->get('mailfrom'); + $fromName = $app->get('fromname'); + $feedEmail = $app->get('feed_email', 'none'); - $this->document->editor = $fromName; + $this->document->editor = $fromName; - if ($feedEmail !== 'none') - { - $this->document->editorEmail = $siteEmail; - } + if ($feedEmail !== 'none') { + $this->document->editorEmail = $siteEmail; + } - // Get some data from the model - $items = $this->get('Items'); + // Get some data from the model + $items = $this->get('Items'); - foreach ($items as $item) - { - // Strip HTML from feed item title - $title = $this->escape($item->title); - $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); + foreach ($items as $item) { + // Strip HTML from feed item title + $title = $this->escape($item->title); + $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); - // Strip HTML from feed item description text - $description = $item->description; - $author = $item->created_by_alias ?: $item->created_by_user_name; - $date = $item->created_time ? date('r', strtotime($item->created_time)) : ''; + // Strip HTML from feed item description text + $description = $item->description; + $author = $item->created_by_alias ?: $item->created_by_user_name; + $date = $item->created_time ? date('r', strtotime($item->created_time)) : ''; - // Load individual item creator class - $feeditem = new FeedItem; - $feeditem->title = $title; - $feeditem->link = '/index.php?option=com_tags&view=tag&id=' . (int) $item->id; - $feeditem->description = $description; - $feeditem->date = $date; - $feeditem->category = 'All Tags'; - $feeditem->author = $author; + // Load individual item creator class + $feeditem = new FeedItem(); + $feeditem->title = $title; + $feeditem->link = '/index.php?option=com_tags&view=tag&id=' . (int) $item->id; + $feeditem->description = $description; + $feeditem->date = $date; + $feeditem->category = 'All Tags'; + $feeditem->author = $author; - if ($feedEmail === 'site') - { - $feeditem->authorEmail = $siteEmail; - } + if ($feedEmail === 'site') { + $feeditem->authorEmail = $siteEmail; + } - if ($feedEmail === 'author') - { - $feeditem->authorEmail = $item->email; - } + if ($feedEmail === 'author') { + $feeditem->authorEmail = $item->email; + } - // Loads item info into RSS array - $this->document->addItem($feeditem); - } - } + // Loads item info into RSS array + $this->document->addItem($feeditem); + } + } } diff --git a/components/com_tags/src/View/Tags/HtmlView.php b/components/com_tags/src/View/Tags/HtmlView.php index 4797969d18291..9492c8e753fda 100644 --- a/components/com_tags/src/View/Tags/HtmlView.php +++ b/components/com_tags/src/View/Tags/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->params = $this->state->get('params'); - $this->user = $this->getCurrentUser(); - - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Flag indicates to not add limitstart=0 to URL - $this->pagination->hideEmptyLimitstart = true; - - if (!empty($this->items)) - { - foreach ($this->items as $itemElement) - { - // Prepare the data. - $temp = new Registry($itemElement->params); - $itemElement->params = clone $this->params; - $itemElement->params->merge($temp); - $itemElement->params = (array) json_decode($itemElement->params); - } - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); - - $active = Factory::getApplication()->getMenu()->getActive(); - - // Load layout from active query (in case it is an alternative menu item) - if ($active && isset($active->query['option']) && $active->query['option'] === 'com_tags' && $active->query['view'] === 'tags') - { - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - } - else - { - // Load default All Tags layout from component - if ($layout = $this->params->get('tags_layout')) - { - $this->setLayout($layout); - } - } - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function _prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_TAGS_DEFAULT_PAGE_TITLE')); - } - - // Set metadata for all tags menu item - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - // Respect configuration Sitename Before/After for TITLE in views All Tags. - $this->setDocumentTitle($this->document->getTitle()); - - // Add alternative feed link - if ($this->params->get('show_feed_link', 1) == 1) - { - $link = '&format=feed&limitstart='; - $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); - $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); - $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); - $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); - } - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.1 + */ + protected $state; + + /** + * The list of tags + * + * @var array|false + * @since 3.1 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 3.1 + */ + protected $pagination; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * @since 3.1 + */ + protected $params = null; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The logged in user + * + * @var \Joomla\CMS\User\User|null + * @since 4.0.0 + */ + protected $user = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + */ + public function display($tpl = null) + { + // Get some data from the models + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->params = $this->state->get('params'); + $this->user = $this->getCurrentUser(); + + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Flag indicates to not add limitstart=0 to URL + $this->pagination->hideEmptyLimitstart = true; + + if (!empty($this->items)) { + foreach ($this->items as $itemElement) { + // Prepare the data. + $temp = new Registry($itemElement->params); + $itemElement->params = clone $this->params; + $itemElement->params->merge($temp); + $itemElement->params = (array) json_decode($itemElement->params); + } + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); + + $active = Factory::getApplication()->getMenu()->getActive(); + + // Load layout from active query (in case it is an alternative menu item) + if ($active && isset($active->query['option']) && $active->query['option'] === 'com_tags' && $active->query['view'] === 'tags') { + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + } else { + // Load default All Tags layout from component + if ($layout = $this->params->get('tags_layout')) { + $this->setLayout($layout); + } + } + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function _prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_TAGS_DEFAULT_PAGE_TITLE')); + } + + // Set metadata for all tags menu item + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + // Respect configuration Sitename Before/After for TITLE in views All Tags. + $this->setDocumentTitle($this->document->getTitle()); + + // Add alternative feed link + if ($this->params->get('show_feed_link', 1) == 1) { + $link = '&format=feed&limitstart='; + $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); + $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); + $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); + $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); + } + } } diff --git a/components/com_tags/tmpl/tag/default.php b/components/com_tags/tmpl/tag/default.php index 5e17225a6ed76..fabeaa5114f56 100644 --- a/components/com_tags/tmpl/tag/default.php +++ b/components/com_tags/tmpl/tag/default.php @@ -1,4 +1,5 @@ - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    + - params->get('show_tag_title', 1)) : ?> - <> - tags_title, '', 'com_tag.tag'); ?> - > - + params->get('show_tag_title', 1)) : ?> + <> + tags_title, '', 'com_tag.tag'); ?> + > + - - item) === 1 && ($this->params->get('tag_list_show_tag_image', 1) || $this->params->get('tag_list_show_tag_description', 1))) : ?> -
    - item[0]->images); ?> - params->get('tag_list_show_tag_image', 1) == 1 && !empty($images->image_fulltext)) : ?> - image_fulltext, $images->image_fulltext_alt); ?> - - params->get('tag_list_show_tag_description') == 1 && $this->item[0]->description) : ?> - item[0]->description, '', 'com_tags.tag'); ?> - -
    - + + item) === 1 && ($this->params->get('tag_list_show_tag_image', 1) || $this->params->get('tag_list_show_tag_description', 1))) : ?> +
    + item[0]->images); ?> + params->get('tag_list_show_tag_image', 1) == 1 && !empty($images->image_fulltext)) : ?> + image_fulltext, $images->image_fulltext_alt); ?> + + params->get('tag_list_show_tag_description') == 1 && $this->item[0]->description) : ?> + item[0]->description, '', 'com_tags.tag'); ?> + +
    + - - params->get('tag_list_show_tag_description', 1) || $this->params->get('show_description_image', 1)) : ?> - params->get('show_description_image', 1) == 1 && $this->params->get('tag_list_image')) : ?> - params->get('tag_list_image'), empty($this->params->get('tag_list_image_alt')) && empty($this->params->get('tag_list_image_alt_empty')) ? false : $this->params->get('tag_list_image_alt')); ?> - - params->get('tag_list_description', '') > '') : ?> - params->get('tag_list_description'), '', 'com_tags.tag'); ?> - - - loadTemplate('items'); ?> + + params->get('tag_list_show_tag_description', 1) || $this->params->get('show_description_image', 1)) : ?> + params->get('show_description_image', 1) == 1 && $this->params->get('tag_list_image')) : ?> + params->get('tag_list_image'), empty($this->params->get('tag_list_image_alt')) && empty($this->params->get('tag_list_image_alt_empty')) ? false : $this->params->get('tag_list_image_alt')); ?> + + params->get('tag_list_description', '') > '') : ?> + params->get('tag_list_description'), '', 'com_tags.tag'); ?> + + + loadTemplate('items'); ?> - params->def('show_pagination', 1) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> -
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - - pagination->getPagesLinks(); ?> -
    - + params->def('show_pagination', 1) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> +
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + + pagination->getPagesLinks(); ?> +
    + diff --git a/components/com_tags/tmpl/tag/default_items.php b/components/com_tags/tmpl/tag/default_items.php index ce51281cc7649..62e5185845fa2 100644 --- a/components/com_tags/tmpl/tag/default_items.php +++ b/components/com_tags/tmpl/tag/default_items.php @@ -1,4 +1,5 @@ authorise('core.edit.state', 'com_tags'); ?>
    -
    - params->get('filter_field') || $this->params->get('show_pagination_limit')) : ?> - params->get('filter_field')) : ?> -
    - - - - -
    - - params->get('show_pagination_limit')) : ?> -
    - - pagination->getLimitBox(); ?> -
    - + + params->get('filter_field') || $this->params->get('show_pagination_limit')) : ?> + params->get('filter_field')) : ?> +
    + + + + +
    + + params->get('show_pagination_limit')) : ?> +
    + + pagination->getLimitBox(); ?> +
    + - - - -
    + + + + - items)) : ?> -
    - - -
    - -
      - items as $i => $item) : ?> - core_state == 0) : ?> -
    • - -
    • - - type_alias === 'com_users.category') || ($item->type_alias === 'com_banners.category')) : ?> -

      - escape($item->core_title); ?> -

      - -

      - - escape($item->core_title); ?> - -

      - - - event->afterDisplayTitle; ?> - core_images); ?> - params->get('tag_list_show_item_image', 1) == 1 && !empty($images->image_intro)) : ?> - - image_intro, $images->image_intro_alt); ?> - - - params->get('tag_list_show_item_description', 1)) : ?> - - event->beforeDisplayContent; ?> - - core_body, $this->params->get('tag_list_item_maximum_characters')); ?> - - - event->afterDisplayContent; ?> - -
    • - -
    - + items)) : ?> +
    + + +
    + +
      + items as $i => $item) : ?> + core_state == 0) : ?> +
    • + +
    • + + type_alias === 'com_users.category') || ($item->type_alias === 'com_banners.category')) : ?> +

      + escape($item->core_title); ?> +

      + +

      + + escape($item->core_title); ?> + +

      + + + event->afterDisplayTitle; ?> + core_images); ?> + params->get('tag_list_show_item_image', 1) == 1 && !empty($images->image_intro)) : ?> + + image_intro, $images->image_intro_alt); ?> + + + params->get('tag_list_show_item_description', 1)) : ?> + + event->beforeDisplayContent; ?> + + core_body, $this->params->get('tag_list_item_maximum_characters')); ?> + + + event->afterDisplayContent; ?> + +
    • + +
    +
    diff --git a/components/com_tags/tmpl/tag/list.php b/components/com_tags/tmpl/tag/list.php index 05bbafc2a2394..c60afcfaa107b 100644 --- a/components/com_tags/tmpl/tag/list.php +++ b/components/com_tags/tmpl/tag/list.php @@ -1,4 +1,5 @@ - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - - - params->get('show_tag_title', 1)) : ?> - <> - tags_title, '', 'com_tag.tag'); ?> - > - - - - item) === 1 && ($this->params->get('tag_list_show_tag_image', 1) || $this->params->get('tag_list_show_tag_description', 1))) : ?> -
    - item[0]->images); ?> - params->get('tag_list_show_tag_image', 1) == 1 && !empty($images->image_fulltext)) : ?> - image_fulltext, ''); ?> - - params->get('tag_list_show_tag_description') == 1 && $this->item[0]->description) : ?> - item[0]->description, '', 'com_tags.tag'); ?> - -
    - - - - params->get('tag_list_show_tag_description', 1) || $this->params->get('show_description_image', 1)) : ?> - params->get('show_description_image', 1) == 1 && $this->params->get('tag_list_image')) : ?> - params->get('tag_list_image'), empty($this->params->get('tag_list_image_alt')) && empty($this->params->get('tag_list_image_alt_empty')) ? false : $this->params->get('tag_list_image_alt')); ?> - - params->get('tag_list_description', '') > '') : ?> - params->get('tag_list_description'), '', 'com_tags.tag'); ?> - - - loadTemplate('items'); ?> + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    + + + params->get('show_tag_title', 1)) : ?> + <> + tags_title, '', 'com_tag.tag'); ?> + > + + + + item) === 1 && ($this->params->get('tag_list_show_tag_image', 1) || $this->params->get('tag_list_show_tag_description', 1))) : ?> +
    + item[0]->images); ?> + params->get('tag_list_show_tag_image', 1) == 1 && !empty($images->image_fulltext)) : ?> + image_fulltext, ''); ?> + + params->get('tag_list_show_tag_description') == 1 && $this->item[0]->description) : ?> + item[0]->description, '', 'com_tags.tag'); ?> + +
    + + + + params->get('tag_list_show_tag_description', 1) || $this->params->get('show_description_image', 1)) : ?> + params->get('show_description_image', 1) == 1 && $this->params->get('tag_list_image')) : ?> + params->get('tag_list_image'), empty($this->params->get('tag_list_image_alt')) && empty($this->params->get('tag_list_image_alt_empty')) ? false : $this->params->get('tag_list_image_alt')); ?> + + params->get('tag_list_description', '') > '') : ?> + params->get('tag_list_description'), '', 'com_tags.tag'); ?> + + + loadTemplate('items'); ?> diff --git a/components/com_tags/tmpl/tag/list_items.php b/components/com_tags/tmpl/tag/list_items.php index 3d84e9fb793db..777091a953cad 100644 --- a/components/com_tags/tmpl/tag/list_items.php +++ b/components/com_tags/tmpl/tag/list_items.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
    -
    - params->get('filter_field')) : ?> -
    - - - - -
    - - params->get('show_pagination_limit')) : ?> -
    - - pagination->getLimitBox(); ?> -
    - + + params->get('filter_field')) : ?> +
    + + + + +
    + + params->get('show_pagination_limit')) : ?> +
    + + pagination->getLimitBox(); ?> +
    + - items)) : ?> -
    - - -
    - - - params->get('show_headings')) : ?> - - - - params->get('tag_list_show_date')) : ?> - - - - - - - items as $i => $item) : ?> - core_state == 0) : ?> - - - - - - params->get('tag_list_show_date')) : ?> - - - - - -
    - - - - - - - - - -
    - type_alias === 'com_users.category') || ($item->type_alias === 'com_banners.category')) : ?> - escape($item->core_title); ?> - - - escape($item->core_title); ?> - - - core_state == 0) : ?> - - - - - - displayDate, - $this->escape($this->params->get('date_format', Text::_('DATE_FORMAT_LC3'))) - ); ?> -
    - + items)) : ?> +
    + + +
    + + + params->get('show_headings')) : ?> + + + + params->get('tag_list_show_date')) : ?> + + + + + + + items as $i => $item) : ?> + core_state == 0) : ?> + + + + + + params->get('tag_list_show_date')) : ?> + + + + + +
    + + + + + + + + + +
    + type_alias === 'com_users.category') || ($item->type_alias === 'com_banners.category')) : ?> + escape($item->core_title); ?> + + + escape($item->core_title); ?> + + + core_state == 0) : ?> + + + + + + displayDate, + $this->escape($this->params->get('date_format', Text::_('DATE_FORMAT_LC3'))) + ); ?> +
    + - - params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> -
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - - pagination->getPagesLinks(); ?> -
    - - - - - -
    + + params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> +
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + + pagination->getPagesLinks(); ?> +
    + + + + + +
    diff --git a/components/com_tags/tmpl/tags/default.php b/components/com_tags/tmpl/tags/default.php index fe98a64e1b1c6..c45171f41202f 100644 --- a/components/com_tags/tmpl/tags/default.php +++ b/components/com_tags/tmpl/tags/default.php @@ -1,4 +1,5 @@ params->get('all_tags_description_image'); ?>
    - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - - params->get('all_tags_show_description_image') && !empty($descriptionImage)) : ?> -
    - params->get('all_tags_description_image_alt')) && empty($this->params->get('all_tags_description_image_alt_empty')) ? false : $this->params->get('all_tags_description_image_alt')); ?> -
    - - -
    - -
    - - loadTemplate('items'); ?> + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    + + params->get('all_tags_show_description_image') && !empty($descriptionImage)) : ?> +
    + params->get('all_tags_description_image_alt')) && empty($this->params->get('all_tags_description_image_alt_empty')) ? false : $this->params->get('all_tags_description_image_alt')); ?> +
    + + +
    + +
    + + loadTemplate('items'); ?>
    diff --git a/components/com_tags/tmpl/tags/default_items.php b/components/com_tags/tmpl/tags/default_items.php index a54d944d9cd54..4d115e68e2af4 100644 --- a/components/com_tags/tmpl/tags/default_items.php +++ b/components/com_tags/tmpl/tags/default_items.php @@ -1,4 +1,5 @@ params->get('tag_columns', 1); // Avoid division by 0 and negative columns. -if ($columns < 1) -{ - $columns = 1; +if ($columns < 1) { + $columns = 1; } $bsspans = floor(12 / $columns); -if ($bsspans < 1) -{ - $bsspans = 1; +if ($bsspans < 1) { + $bsspans = 1; } $bscolumns = min($columns, floor(12 / $bsspans)); @@ -48,110 +47,110 @@ ?>
    -
    - params->get('filter_field') || $this->params->get('show_pagination_limit')) : ?> - params->get('filter_field')) : ?> -
    - - - - -
    - - params->get('show_pagination_limit')) : ?> -
    - - pagination->getLimitBox(); ?> -
    - - - - - -
    - - items == false || $n === 0) : ?> -
    - - -
    - - items as $i => $item) : ?> - -
      - - -
    • - access)) && in_array($item->access, $this->user->getAuthorisedViewLevels())) : ?> -

      - - escape($item->title); ?> - -

      - - - params->get('all_tags_show_tag_image') && !empty($item->images)) : ?> - images); ?> - - image_intro)) : ?> - float_intro) ? $this->params->get('float_intro') : $images->float_intro; ?> -
      - - image_intro_caption) : ?> - image_intro_caption; ?> - - - image_intro, $images->image_intro_alt, $imageOptions); ?> -
      - -
      - - - params->get('all_tags_show_tag_description', 1) && !empty($item->description)) || $this->params->get('all_tags_show_tag_hits')) : ?> -
      - params->get('all_tags_show_tag_description', 1) && !empty($item->description)) : ?> - - description, $this->params->get('all_tags_tag_maximum_characters')); ?> - - - params->get('all_tags_show_tag_hits')) : ?> - - hits); ?> - - -
      - -
    • - - -
    - - - - - - - items)) : ?> - params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> -
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - - pagination->getPagesLinks(); ?> -
    - - +
    + params->get('filter_field') || $this->params->get('show_pagination_limit')) : ?> + params->get('filter_field')) : ?> +
    + + + + +
    + + params->get('show_pagination_limit')) : ?> +
    + + pagination->getLimitBox(); ?> +
    + + + + + +
    + + items == false || $n === 0) : ?> +
    + + +
    + + items as $i => $item) : ?> + +
      + + +
    • + access)) && in_array($item->access, $this->user->getAuthorisedViewLevels())) : ?> +

      + + escape($item->title); ?> + +

      + + + params->get('all_tags_show_tag_image') && !empty($item->images)) : ?> + images); ?> + + image_intro)) : ?> + float_intro) ? $this->params->get('float_intro') : $images->float_intro; ?> +
      + + image_intro_caption) : ?> + image_intro_caption; ?> + + + image_intro, $images->image_intro_alt, $imageOptions); ?> +
      + +
      + + + params->get('all_tags_show_tag_description', 1) && !empty($item->description)) || $this->params->get('all_tags_show_tag_hits')) : ?> +
      + params->get('all_tags_show_tag_description', 1) && !empty($item->description)) : ?> + + description, $this->params->get('all_tags_tag_maximum_characters')); ?> + + + params->get('all_tags_show_tag_hits')) : ?> + + hits); ?> + + +
      + +
    • + + +
    + + + + + + + items)) : ?> + params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> +
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + + pagination->getPagesLinks(); ?> +
    + +
    diff --git a/components/com_users/src/Controller/CallbackController.php b/components/com_users/src/Controller/CallbackController.php index de8508032f249..39fb085c16be4 100644 --- a/components/com_users/src/Controller/CallbackController.php +++ b/components/com_users/src/Controller/CallbackController.php @@ -1,4 +1,5 @@ getCode() !== 403) - { - throw $e; - } + /** + * Execute a task by triggering a Method in the derived class. + * + * @param string $task The task to perform. + * + * @return mixed The value returned by the called Method. + * + * @throws \Exception + * @since 4.2.0 + */ + public function execute($task) + { + try { + return parent::execute($task); + } catch (\Exception $e) { + if ($e->getCode() !== 403) { + throw $e; + } - if ($this->app->getIdentity()->guest) - { - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + if ($this->app->getIdentity()->guest) { + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - return null; - } - } + return null; + } + } - return null; - } + return null; + } } diff --git a/components/com_users/src/Controller/DisplayController.php b/components/com_users/src/Controller/DisplayController.php index d5ed593109557..d4c06e6caa655 100644 --- a/components/com_users/src/Controller/DisplayController.php +++ b/components/com_users/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->getCmd('view', 'login'); - $vFormat = $document->getType(); - $lName = $this->input->getCmd('layout', 'default'); - - if ($view = $this->getView($vName, $vFormat)) - { - // Do any specific processing by view. - switch ($vName) - { - case 'registration': - // If the user is already logged in, redirect to the profile page. - $user = $this->app->getIdentity(); - - if ($user->get('guest') != 1) - { - // Redirect to profile page. - $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); - - return; - } - - // Check if user registration is enabled - if (ComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0) - { - // Registration is disabled - Redirect to login page. - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - - return; - } - - // The user is a guest, load the registration model and show the registration page. - $model = $this->getModel('Registration'); - break; - - // Handle view specific models. - case 'profile': - - // If the user is a guest, redirect to the login page. - $user = $this->app->getIdentity(); - - if ($user->get('guest') == 1) - { - // Redirect to login page. - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - - return; - } - - $model = $this->getModel($vName); - break; - - // Handle the default views. - case 'login': - $model = $this->getModel($vName); - break; - - case 'remind': - case 'reset': - // If the user is already logged in, redirect to the profile page. - $user = $this->app->getIdentity(); - - if ($user->get('guest') != 1) - { - // Redirect to profile page. - $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); - - return; - } - - $model = $this->getModel($vName); - break; - - case 'captive': - case 'methods': - case 'method': - $controller = $this->factory->createController($vName, 'Site', [], $this->app, $this->input); - $task = $this->input->get('task', ''); - - return $controller->execute($task); - - break; - - default: - $model = $this->getModel('Login'); - break; - } - - // Make sure we don't send a referer - if (in_array($vName, array('remind', 'reset'))) - { - $this->app->setHeader('Referrer-Policy', 'no-referrer', true); - } - - // Push the model into the view (as default). - $view->setModel($model, true); - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - - $view->display(); - } - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array|boolean $urlparams An array of safe URL parameters and their variable types, + * for valid values see {@link \Joomla\CMS\Filter\InputFilter::clean()}. + * + * @return void + * + * @since 1.5 + * @throws \Exception + */ + public function display($cachable = false, $urlparams = false) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->getCmd('view', 'login'); + $vFormat = $document->getType(); + $lName = $this->input->getCmd('layout', 'default'); + + if ($view = $this->getView($vName, $vFormat)) { + // Do any specific processing by view. + switch ($vName) { + case 'registration': + // If the user is already logged in, redirect to the profile page. + $user = $this->app->getIdentity(); + + if ($user->get('guest') != 1) { + // Redirect to profile page. + $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); + + return; + } + + // Check if user registration is enabled + if (ComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0) { + // Registration is disabled - Redirect to login page. + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return; + } + + // The user is a guest, load the registration model and show the registration page. + $model = $this->getModel('Registration'); + break; + + // Handle view specific models. + case 'profile': + // If the user is a guest, redirect to the login page. + $user = $this->app->getIdentity(); + + if ($user->get('guest') == 1) { + // Redirect to login page. + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return; + } + + $model = $this->getModel($vName); + break; + + // Handle the default views. + case 'login': + $model = $this->getModel($vName); + break; + + case 'remind': + case 'reset': + // If the user is already logged in, redirect to the profile page. + $user = $this->app->getIdentity(); + + if ($user->get('guest') != 1) { + // Redirect to profile page. + $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); + + return; + } + + $model = $this->getModel($vName); + break; + + case 'captive': + case 'methods': + case 'method': + $controller = $this->factory->createController($vName, 'Site', [], $this->app, $this->input); + $task = $this->input->get('task', ''); + + return $controller->execute($task); + + break; + + default: + $model = $this->getModel('Login'); + break; + } + + // Make sure we don't send a referer + if (in_array($vName, array('remind', 'reset'))) { + $this->app->setHeader('Referrer-Policy', 'no-referrer', true); + } + + // Push the model into the view (as default). + $view->setModel($model, true); + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + + $view->display(); + } + } } diff --git a/components/com_users/src/Controller/MethodController.php b/components/com_users/src/Controller/MethodController.php index 59a0a7bee255e..282b2d7cb1d8c 100644 --- a/components/com_users/src/Controller/MethodController.php +++ b/components/com_users/src/Controller/MethodController.php @@ -1,4 +1,5 @@ getCode() !== 403) - { - throw $e; - } + /** + * Execute a task by triggering a Method in the derived class. + * + * @param string $task The task to perform. + * + * @return mixed The value returned by the called Method. + * + * @throws \Exception + * @since 4.2.0 + */ + public function execute($task) + { + try { + return parent::execute($task); + } catch (\Exception $e) { + if ($e->getCode() !== 403) { + throw $e; + } - if ($this->app->getIdentity()->guest) - { - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + if ($this->app->getIdentity()->guest) { + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - return null; - } - } + return null; + } + } - return null; - } + return null; + } } diff --git a/components/com_users/src/Controller/MethodsController.php b/components/com_users/src/Controller/MethodsController.php index 8e742c1534071..5f1cfbdef819b 100644 --- a/components/com_users/src/Controller/MethodsController.php +++ b/components/com_users/src/Controller/MethodsController.php @@ -1,4 +1,5 @@ getCode() !== 403) - { - throw $e; - } + /** + * Execute a task by triggering a Method in the derived class. + * + * @param string $task The task to perform. + * + * @return mixed The value returned by the called Method. + * + * @throws \Exception + * @since 4.2.0 + */ + public function execute($task) + { + try { + return parent::execute($task); + } catch (\Exception $e) { + if ($e->getCode() !== 403) { + throw $e; + } - if ($this->app->getIdentity()->guest) - { - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + if ($this->app->getIdentity()->guest) { + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - return null; - } - } + return null; + } + } - return null; - } + return null; + } } diff --git a/components/com_users/src/Controller/ProfileController.php b/components/com_users/src/Controller/ProfileController.php index e53fd9ef1d5c8..e3b4fd223d32c 100644 --- a/components/com_users/src/Controller/ProfileController.php +++ b/components/com_users/src/Controller/ProfileController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Users\Site\Controller; \defined('_JEXEC') or die; @@ -23,215 +25,201 @@ */ class ProfileController extends BaseController { - /** - * Method to check out a user for editing and redirect to the edit form. - * - * @return boolean - * - * @since 1.6 - */ - public function edit() - { - $app = $this->app; - $user = $this->app->getIdentity(); - $loginUserId = (int) $user->get('id'); - - // Get the current user id. - $userId = $this->input->getInt('user_id'); - - // Check if the user is trying to edit another users profile. - if ($userId != $loginUserId) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return false; - } - - $cookieLogin = $user->get('cookieLogin'); - - // Check if the user logged in with a cookie - if (!empty($cookieLogin)) - { - // If so, the user must login to edit the password and other data. - $app->enqueueMessage(Text::_('JGLOBAL_REMEMBER_MUST_LOGIN'), 'message'); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - - return false; - } - - // Set the user id for the user to edit in the session. - $app->setUserState('com_users.edit.profile.id', $userId); - - // Redirect to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit', false)); - - return true; - } - - /** - * Method to save a user's profile data. - * - * @return void|boolean - * - * @since 1.6 - * @throws \Exception - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\ProfileModel $model */ - $model = $this->getModel('Profile', 'Site'); - $user = $this->app->getIdentity(); - $userId = (int) $user->get('id'); - - // Get the user data. - $requestData = $app->input->post->get('jform', array(), 'array'); - - // Force the ID to this user. - $requestData['id'] = $userId; - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - // Send an object which can be modified through the plugin event - $objData = (object) $requestData; - $app->triggerEvent( - 'onContentNormaliseRequestData', - array('com_users.user', $objData, $form) - ); - $requestData = (array) $objData; - - // Validate the posted data. - $data = $model->validate($form, $requestData); - - // Check for errors. - if ($data === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Unset the passwords. - unset($requestData['password1'], $requestData['password2']); - - // Save the data in the session. - $app->setUserState('com_users.edit.profile.data', $requestData); - - // Redirect back to the edit screen. - $userId = (int) $app->getUserState('com_users.edit.profile.id'); - $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit&user_id=' . $userId, false)); - - return false; - } - - // Attempt to save the data. - $return = $model->save($data); - - // Check for errors. - if ($return === false) - { - // Save the data in the session. - $app->setUserState('com_users.edit.profile.data', $data); - - // Redirect back to the edit screen. - $userId = (int) $app->getUserState('com_users.edit.profile.id'); - $this->setMessage(Text::sprintf('COM_USERS_PROFILE_SAVE_FAILED', $model->getError()), 'warning'); - $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit&user_id=' . $userId, false)); - - return false; - } - - // Redirect the user and adjust session state based on the chosen task. - switch ($this->getTask()) - { - case 'apply': - // Check out the profile. - $app->setUserState('com_users.edit.profile.id', $return); - - // Redirect back to the edit screen. - $this->setMessage(Text::_('COM_USERS_PROFILE_SAVE_SUCCESS')); - - $redirect = $app->getUserState('com_users.edit.profile.redirect'); - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect)) - { - $redirect = null; - } - - if (!$redirect) - { - $redirect = 'index.php?option=com_users&view=profile&layout=edit&hidemainmenu=1'; - } - - $this->setRedirect(Route::_($redirect, false)); - break; - - default: - // Clear the profile id from the session. - $app->setUserState('com_users.edit.profile.id', null); - - $redirect = $app->getUserState('com_users.edit.profile.redirect'); - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect)) - { - $redirect = null; - } - - if (!$redirect) - { - $redirect = 'index.php?option=com_users&view=profile&user_id=' . $return; - } - - // Redirect to the list screen. - $this->setMessage(Text::_('COM_USERS_PROFILE_SAVE_SUCCESS')); - $this->setRedirect(Route::_($redirect, false)); - break; - } - - // Flush the data from the session. - $app->setUserState('com_users.edit.profile.data', null); - } - - /** - * Method to cancel an edit. - * - * @return void - * - * @since 4.0.0 - */ - public function cancel() - { - // Check for request forgeries. - $this->checkToken(); - - // Flush the data from the session. - $this->app->setUserState('com_users.edit.profile', null); - - // Redirect to user profile. - $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); - } + /** + * Method to check out a user for editing and redirect to the edit form. + * + * @return boolean + * + * @since 1.6 + */ + public function edit() + { + $app = $this->app; + $user = $this->app->getIdentity(); + $loginUserId = (int) $user->get('id'); + + // Get the current user id. + $userId = $this->input->getInt('user_id'); + + // Check if the user is trying to edit another users profile. + if ($userId != $loginUserId) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return false; + } + + $cookieLogin = $user->get('cookieLogin'); + + // Check if the user logged in with a cookie + if (!empty($cookieLogin)) { + // If so, the user must login to edit the password and other data. + $app->enqueueMessage(Text::_('JGLOBAL_REMEMBER_MUST_LOGIN'), 'message'); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return false; + } + + // Set the user id for the user to edit in the session. + $app->setUserState('com_users.edit.profile.id', $userId); + + // Redirect to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit', false)); + + return true; + } + + /** + * Method to save a user's profile data. + * + * @return void|boolean + * + * @since 1.6 + * @throws \Exception + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\ProfileModel $model */ + $model = $this->getModel('Profile', 'Site'); + $user = $this->app->getIdentity(); + $userId = (int) $user->get('id'); + + // Get the user data. + $requestData = $app->input->post->get('jform', array(), 'array'); + + // Force the ID to this user. + $requestData['id'] = $userId; + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + // Send an object which can be modified through the plugin event + $objData = (object) $requestData; + $app->triggerEvent( + 'onContentNormaliseRequestData', + array('com_users.user', $objData, $form) + ); + $requestData = (array) $objData; + + // Validate the posted data. + $data = $model->validate($form, $requestData); + + // Check for errors. + if ($data === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Unset the passwords. + unset($requestData['password1'], $requestData['password2']); + + // Save the data in the session. + $app->setUserState('com_users.edit.profile.data', $requestData); + + // Redirect back to the edit screen. + $userId = (int) $app->getUserState('com_users.edit.profile.id'); + $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit&user_id=' . $userId, false)); + + return false; + } + + // Attempt to save the data. + $return = $model->save($data); + + // Check for errors. + if ($return === false) { + // Save the data in the session. + $app->setUserState('com_users.edit.profile.data', $data); + + // Redirect back to the edit screen. + $userId = (int) $app->getUserState('com_users.edit.profile.id'); + $this->setMessage(Text::sprintf('COM_USERS_PROFILE_SAVE_FAILED', $model->getError()), 'warning'); + $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit&user_id=' . $userId, false)); + + return false; + } + + // Redirect the user and adjust session state based on the chosen task. + switch ($this->getTask()) { + case 'apply': + // Check out the profile. + $app->setUserState('com_users.edit.profile.id', $return); + + // Redirect back to the edit screen. + $this->setMessage(Text::_('COM_USERS_PROFILE_SAVE_SUCCESS')); + + $redirect = $app->getUserState('com_users.edit.profile.redirect'); + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect)) { + $redirect = null; + } + + if (!$redirect) { + $redirect = 'index.php?option=com_users&view=profile&layout=edit&hidemainmenu=1'; + } + + $this->setRedirect(Route::_($redirect, false)); + break; + + default: + // Clear the profile id from the session. + $app->setUserState('com_users.edit.profile.id', null); + + $redirect = $app->getUserState('com_users.edit.profile.redirect'); + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect)) { + $redirect = null; + } + + if (!$redirect) { + $redirect = 'index.php?option=com_users&view=profile&user_id=' . $return; + } + + // Redirect to the list screen. + $this->setMessage(Text::_('COM_USERS_PROFILE_SAVE_SUCCESS')); + $this->setRedirect(Route::_($redirect, false)); + break; + } + + // Flush the data from the session. + $app->setUserState('com_users.edit.profile.data', null); + } + + /** + * Method to cancel an edit. + * + * @return void + * + * @since 4.0.0 + */ + public function cancel() + { + // Check for request forgeries. + $this->checkToken(); + + // Flush the data from the session. + $this->app->setUserState('com_users.edit.profile', null); + + // Redirect to user profile. + $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); + } } diff --git a/components/com_users/src/Controller/RegistrationController.php b/components/com_users/src/Controller/RegistrationController.php index 7328f0df01bac..c5ffdd77846a2 100644 --- a/components/com_users/src/Controller/RegistrationController.php +++ b/components/com_users/src/Controller/RegistrationController.php @@ -1,4 +1,5 @@ app->getIdentity(); - $input = $this->input; - $uParams = ComponentHelper::getParams('com_users'); - - // Check for admin activation. Don't allow non-super-admin to delete a super admin - if ($uParams->get('useractivation') != 2 && $user->get('id')) - { - $this->setRedirect('index.php'); - - return true; - } - - // If user registration or account activation is disabled, throw a 403. - if ($uParams->get('useractivation') == 0 || $uParams->get('allowUserRegistration') == 0) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - /** @var \Joomla\Component\Users\Site\Model\RegistrationModel $model */ - $model = $this->getModel('Registration', 'Site'); - $token = $input->getAlnum('token'); - - // Check that the token is in a valid format. - if ($token === null || strlen($token) !== 32) - { - throw new \Exception(Text::_('JINVALID_TOKEN'), 403); - } - - // Get the User ID - $userIdToActivate = $model->getUserIdFromToken($token); - - if (!$userIdToActivate) - { - $this->setMessage(Text::_('COM_USERS_ACTIVATION_TOKEN_NOT_FOUND')); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - - return false; - } - - // Get the user we want to activate - $userToActivate = Factory::getUser($userIdToActivate); - - // Admin activation is on and admin is activating the account - if (($uParams->get('useractivation') == 2) && $userToActivate->getParam('activate', 0)) - { - // If a user admin is not logged in, redirect them to the login page with an error message - if (!$user->authorise('core.create', 'com_users') || !$user->authorise('core.manage', 'com_users')) - { - $activationUrl = 'index.php?option=com_users&task=registration.activate&token=' . $token; - $loginUrl = 'index.php?option=com_users&view=login&return=' . base64_encode($activationUrl); - - // In case we still run into this in the second step the user does not have the right permissions - $message = Text::_('COM_USERS_REGISTRATION_ACL_ADMIN_ACTIVATION_PERMISSIONS'); - - // When we are not logged in we should login - if ($user->guest) - { - $message = Text::_('COM_USERS_REGISTRATION_ACL_ADMIN_ACTIVATION'); - } - - $this->setMessage($message); - $this->setRedirect(Route::_($loginUrl, false)); - - return false; - } - } - - // Attempt to activate the user. - $return = $model->activate($token); - - // Check for errors. - if ($return === false) - { - // Redirect back to the home page. - $this->setMessage(Text::sprintf('COM_USERS_REGISTRATION_SAVE_FAILED', $model->getError()), 'error'); - $this->setRedirect('index.php'); - - return false; - } - - $useractivation = $uParams->get('useractivation'); - - // Redirect to the login screen. - if ($useractivation == 0) - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_SAVE_SUCCESS')); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - } - elseif ($useractivation == 1) - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_ACTIVATE_SUCCESS')); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - } - elseif ($return->getParam('activate')) - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_VERIFY_SUCCESS')); - $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); - } - else - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_ADMINACTIVATE_SUCCESS')); - $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); - } - - return true; - } - - /** - * Method to register a user. - * - * @return boolean True on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function register() - { - // Check for request forgeries. - $this->checkToken(); - - // If registration is disabled - Redirect to login page. - if (ComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0) - { - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - - return false; - } - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\RegistrationModel $model */ - $model = $this->getModel('Registration', 'Site'); - - // Get the user data. - $requestData = $this->input->post->get('jform', array(), 'array'); - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - $data = $model->validate($form, $requestData); - - // Check for validation errors. - if ($data === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $app->enqueueMessage($errors[$i]->getMessage(), 'error'); - } - else - { - $app->enqueueMessage($errors[$i], 'error'); - } - } - - // Save the data in the session. - $app->setUserState('com_users.registration.data', $requestData); - - // Redirect back to the registration screen. - $this->setRedirect(Route::_('index.php?option=com_users&view=registration', false)); - - return false; - } - - // Attempt to save the data. - $return = $model->register($data); - - // Check for errors. - if ($return === false) - { - // Save the data in the session. - $app->setUserState('com_users.registration.data', $data); - - // Redirect back to the edit screen. - $this->setMessage($model->getError(), 'error'); - $this->setRedirect(Route::_('index.php?option=com_users&view=registration', false)); - - return false; - } - - // Flush the data from the session. - $app->setUserState('com_users.registration.data', null); - - // Redirect to the profile screen. - if ($return === 'adminactivate') - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_COMPLETE_VERIFY')); - $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); - } - elseif ($return === 'useractivate') - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_COMPLETE_ACTIVATE')); - $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); - } - else - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_SAVE_SUCCESS')); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - } - - return true; - } + /** + * Method to activate a user. + * + * @return boolean True on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function activate() + { + $user = $this->app->getIdentity(); + $input = $this->input; + $uParams = ComponentHelper::getParams('com_users'); + + // Check for admin activation. Don't allow non-super-admin to delete a super admin + if ($uParams->get('useractivation') != 2 && $user->get('id')) { + $this->setRedirect('index.php'); + + return true; + } + + // If user registration or account activation is disabled, throw a 403. + if ($uParams->get('useractivation') == 0 || $uParams->get('allowUserRegistration') == 0) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + /** @var \Joomla\Component\Users\Site\Model\RegistrationModel $model */ + $model = $this->getModel('Registration', 'Site'); + $token = $input->getAlnum('token'); + + // Check that the token is in a valid format. + if ($token === null || strlen($token) !== 32) { + throw new \Exception(Text::_('JINVALID_TOKEN'), 403); + } + + // Get the User ID + $userIdToActivate = $model->getUserIdFromToken($token); + + if (!$userIdToActivate) { + $this->setMessage(Text::_('COM_USERS_ACTIVATION_TOKEN_NOT_FOUND')); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return false; + } + + // Get the user we want to activate + $userToActivate = Factory::getUser($userIdToActivate); + + // Admin activation is on and admin is activating the account + if (($uParams->get('useractivation') == 2) && $userToActivate->getParam('activate', 0)) { + // If a user admin is not logged in, redirect them to the login page with an error message + if (!$user->authorise('core.create', 'com_users') || !$user->authorise('core.manage', 'com_users')) { + $activationUrl = 'index.php?option=com_users&task=registration.activate&token=' . $token; + $loginUrl = 'index.php?option=com_users&view=login&return=' . base64_encode($activationUrl); + + // In case we still run into this in the second step the user does not have the right permissions + $message = Text::_('COM_USERS_REGISTRATION_ACL_ADMIN_ACTIVATION_PERMISSIONS'); + + // When we are not logged in we should login + if ($user->guest) { + $message = Text::_('COM_USERS_REGISTRATION_ACL_ADMIN_ACTIVATION'); + } + + $this->setMessage($message); + $this->setRedirect(Route::_($loginUrl, false)); + + return false; + } + } + + // Attempt to activate the user. + $return = $model->activate($token); + + // Check for errors. + if ($return === false) { + // Redirect back to the home page. + $this->setMessage(Text::sprintf('COM_USERS_REGISTRATION_SAVE_FAILED', $model->getError()), 'error'); + $this->setRedirect('index.php'); + + return false; + } + + $useractivation = $uParams->get('useractivation'); + + // Redirect to the login screen. + if ($useractivation == 0) { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_SAVE_SUCCESS')); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + } elseif ($useractivation == 1) { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_ACTIVATE_SUCCESS')); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + } elseif ($return->getParam('activate')) { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_VERIFY_SUCCESS')); + $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); + } else { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_ADMINACTIVATE_SUCCESS')); + $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); + } + + return true; + } + + /** + * Method to register a user. + * + * @return boolean True on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function register() + { + // Check for request forgeries. + $this->checkToken(); + + // If registration is disabled - Redirect to login page. + if (ComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0) { + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return false; + } + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\RegistrationModel $model */ + $model = $this->getModel('Registration', 'Site'); + + // Get the user data. + $requestData = $this->input->post->get('jform', array(), 'array'); + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + $data = $model->validate($form, $requestData); + + // Check for validation errors. + if ($data === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $app->enqueueMessage($errors[$i]->getMessage(), 'error'); + } else { + $app->enqueueMessage($errors[$i], 'error'); + } + } + + // Save the data in the session. + $app->setUserState('com_users.registration.data', $requestData); + + // Redirect back to the registration screen. + $this->setRedirect(Route::_('index.php?option=com_users&view=registration', false)); + + return false; + } + + // Attempt to save the data. + $return = $model->register($data); + + // Check for errors. + if ($return === false) { + // Save the data in the session. + $app->setUserState('com_users.registration.data', $data); + + // Redirect back to the edit screen. + $this->setMessage($model->getError(), 'error'); + $this->setRedirect(Route::_('index.php?option=com_users&view=registration', false)); + + return false; + } + + // Flush the data from the session. + $app->setUserState('com_users.registration.data', null); + + // Redirect to the profile screen. + if ($return === 'adminactivate') { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_COMPLETE_VERIFY')); + $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); + } elseif ($return === 'useractivate') { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_COMPLETE_ACTIVATE')); + $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); + } else { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_SAVE_SUCCESS')); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + } + + return true; + } } diff --git a/components/com_users/src/Controller/RemindController.php b/components/com_users/src/Controller/RemindController.php index f3d4aff83ceba..a7de799de8597 100644 --- a/components/com_users/src/Controller/RemindController.php +++ b/components/com_users/src/Controller/RemindController.php @@ -1,4 +1,5 @@ checkToken('post'); - - /** @var \Joomla\Component\Users\Site\Model\RemindModel $model */ - $model = $this->getModel('Remind', 'Site'); - $data = $this->input->post->get('jform', array(), 'array'); - - // Submit the password reset request. - $return = $model->processRemindRequest($data); - - // Check for a hard error. - if ($return == false && JDEBUG) - { - // The request failed. - // Go back to the request form. - $message = Text::sprintf('COM_USERS_REMIND_REQUEST_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'notice'); - - return false; - } - - // To not expose if the user exists or not we send a generic message. - $message = Text::_('COM_USERS_REMIND_REQUEST'); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message, 'notice'); - - return true; - } + /** + * Method to request a username reminder. + * + * @return boolean + * + * @since 1.6 + */ + public function remind() + { + // Check the request token. + $this->checkToken('post'); + + /** @var \Joomla\Component\Users\Site\Model\RemindModel $model */ + $model = $this->getModel('Remind', 'Site'); + $data = $this->input->post->get('jform', array(), 'array'); + + // Submit the password reset request. + $return = $model->processRemindRequest($data); + + // Check for a hard error. + if ($return == false && JDEBUG) { + // The request failed. + // Go back to the request form. + $message = Text::sprintf('COM_USERS_REMIND_REQUEST_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'notice'); + + return false; + } + + // To not expose if the user exists or not we send a generic message. + $message = Text::_('COM_USERS_REMIND_REQUEST'); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message, 'notice'); + + return true; + } } diff --git a/components/com_users/src/Controller/ResetController.php b/components/com_users/src/Controller/ResetController.php index f856676509ce6..64f76cd415c1e 100644 --- a/components/com_users/src/Controller/ResetController.php +++ b/components/com_users/src/Controller/ResetController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Users\Site\Controller; \defined('_JEXEC') or die; @@ -21,177 +23,155 @@ */ class ResetController extends BaseController { - /** - * Method to request a password reset. - * - * @return boolean - * - * @since 1.6 - */ - public function request() - { - // Check the request token. - $this->checkToken('post'); - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ - $model = $this->getModel('Reset', 'Site'); - $data = $this->input->post->get('jform', array(), 'array'); - - // Submit the password reset request. - $return = $model->processResetRequest($data); - - // Check for a hard error. - if ($return instanceof \Exception && JDEBUG) - { - // Get the error message to display. - if ($app->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_USERS_RESET_REQUEST_ERROR'); - } - - // Go back to the request form. - $this->setRedirect(Route::_('index.php?option=com_users&view=reset', false), $message, 'error'); - - return false; - } - elseif ($return === false && JDEBUG) - { - // The request failed. - // Go back to the request form. - $message = Text::sprintf('COM_USERS_RESET_REQUEST_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_users&view=reset', false), $message, 'notice'); - - return false; - } - - // To not expose if the user exists or not we send a generic message. - $message = Text::_('COM_USERS_RESET_REQUEST'); - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'notice'); - - return true; - } - - /** - * Method to confirm the password request. - * - * @return boolean - * - * @access public - * @since 1.6 - */ - public function confirm() - { - // Check the request token. - $this->checkToken('request'); - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ - $model = $this->getModel('Reset', 'Site'); - $data = $this->input->get('jform', array(), 'array'); - - // Confirm the password reset request. - $return = $model->processResetConfirm($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - if ($app->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_USERS_RESET_CONFIRM_ERROR'); - } - - // Go back to the confirm form. - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'error'); - - return false; - } - elseif ($return === false) - { - // Confirm failed. - // Go back to the confirm form. - $message = Text::sprintf('COM_USERS_RESET_CONFIRM_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'notice'); - - return false; - } - else - { - // Confirm succeeded. - // Proceed to step three. - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false)); - - return true; - } - } - - /** - * Method to complete the password reset process. - * - * @return boolean - * - * @since 1.6 - */ - public function complete() - { - // Check for request forgeries - $this->checkToken('post'); - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ - $model = $this->getModel('Reset', 'Site'); - $data = $this->input->post->get('jform', array(), 'array'); - - // Complete the password reset request. - $return = $model->processResetComplete($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - if ($app->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_USERS_RESET_COMPLETE_ERROR'); - } - - // Go back to the complete form. - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false), $message, 'error'); - - return false; - } - elseif ($return === false) - { - // Complete failed. - // Go back to the complete form. - $message = Text::sprintf('COM_USERS_RESET_COMPLETE_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false), $message, 'notice'); - - return false; - } - else - { - // Complete succeeded. - // Proceed to the login form. - $message = Text::_('COM_USERS_RESET_COMPLETE_SUCCESS'); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message); - - return true; - } - } + /** + * Method to request a password reset. + * + * @return boolean + * + * @since 1.6 + */ + public function request() + { + // Check the request token. + $this->checkToken('post'); + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ + $model = $this->getModel('Reset', 'Site'); + $data = $this->input->post->get('jform', array(), 'array'); + + // Submit the password reset request. + $return = $model->processResetRequest($data); + + // Check for a hard error. + if ($return instanceof \Exception && JDEBUG) { + // Get the error message to display. + if ($app->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_USERS_RESET_REQUEST_ERROR'); + } + + // Go back to the request form. + $this->setRedirect(Route::_('index.php?option=com_users&view=reset', false), $message, 'error'); + + return false; + } elseif ($return === false && JDEBUG) { + // The request failed. + // Go back to the request form. + $message = Text::sprintf('COM_USERS_RESET_REQUEST_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_users&view=reset', false), $message, 'notice'); + + return false; + } + + // To not expose if the user exists or not we send a generic message. + $message = Text::_('COM_USERS_RESET_REQUEST'); + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'notice'); + + return true; + } + + /** + * Method to confirm the password request. + * + * @return boolean + * + * @access public + * @since 1.6 + */ + public function confirm() + { + // Check the request token. + $this->checkToken('request'); + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ + $model = $this->getModel('Reset', 'Site'); + $data = $this->input->get('jform', array(), 'array'); + + // Confirm the password reset request. + $return = $model->processResetConfirm($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + if ($app->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_USERS_RESET_CONFIRM_ERROR'); + } + + // Go back to the confirm form. + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'error'); + + return false; + } elseif ($return === false) { + // Confirm failed. + // Go back to the confirm form. + $message = Text::sprintf('COM_USERS_RESET_CONFIRM_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'notice'); + + return false; + } else { + // Confirm succeeded. + // Proceed to step three. + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false)); + + return true; + } + } + + /** + * Method to complete the password reset process. + * + * @return boolean + * + * @since 1.6 + */ + public function complete() + { + // Check for request forgeries + $this->checkToken('post'); + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ + $model = $this->getModel('Reset', 'Site'); + $data = $this->input->post->get('jform', array(), 'array'); + + // Complete the password reset request. + $return = $model->processResetComplete($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + if ($app->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_USERS_RESET_COMPLETE_ERROR'); + } + + // Go back to the complete form. + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false), $message, 'error'); + + return false; + } elseif ($return === false) { + // Complete failed. + // Go back to the complete form. + $message = Text::sprintf('COM_USERS_RESET_COMPLETE_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false), $message, 'notice'); + + return false; + } else { + // Complete succeeded. + // Proceed to the login form. + $message = Text::_('COM_USERS_RESET_COMPLETE_SUCCESS'); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message); + + return true; + } + } } diff --git a/components/com_users/src/Controller/UserController.php b/components/com_users/src/Controller/UserController.php index 0239d4c8833ed..cc93044955e6e 100644 --- a/components/com_users/src/Controller/UserController.php +++ b/components/com_users/src/Controller/UserController.php @@ -1,4 +1,5 @@ checkToken('post'); - - $input = $this->input->getInputForRequestMethod(); - - // Populate the data array: - $data = array(); - - $data['return'] = base64_decode($input->get('return', '', 'BASE64')); - $data['username'] = $input->get('username', '', 'USERNAME'); - $data['password'] = $input->get('password', '', 'RAW'); - $data['secretkey'] = $input->get('secretkey', '', 'RAW'); - - // Check for a simple menu item id - if (is_numeric($data['return'])) - { - $language = $this->getModel('Login', 'Site')->getMenuLanguage($data['return']); - $data['return'] = 'index.php?Itemid=' . $data['return'] . ($language !== '*' ? '&lang=' . $language : ''); - } - // Don't redirect to an external URL. - elseif (!Uri::isInternal($data['return'])) - { - $data['return'] = ''; - } - - // Set the return URL if empty. - if (empty($data['return'])) - { - $data['return'] = 'index.php?option=com_users&view=profile'; - } - - // Set the return URL in the user state to allow modification by plugins - $this->app->setUserState('users.login.form.return', $data['return']); - - // Get the log in options. - $options = array(); - $options['remember'] = $this->input->getBool('remember', false); - $options['return'] = $data['return']; - - // Get the log in credentials. - $credentials = array(); - $credentials['username'] = $data['username']; - $credentials['password'] = $data['password']; - $credentials['secretkey'] = $data['secretkey']; - - // Perform the log in. - if (true !== $this->app->login($credentials, $options)) - { - // Login failed ! - // Clear user name, password and secret key before sending the login form back to the user. - $data['remember'] = (int) $options['remember']; - $data['username'] = ''; - $data['password'] = ''; - $data['secretkey'] = ''; - $this->app->setUserState('users.login.form.data', $data); - $this->app->redirect(Route::_('index.php?option=com_users&view=login', false)); - } - - // Success - if ($options['remember'] == true) - { - $this->app->setUserState('rememberLogin', true); - } - - $this->app->setUserState('users.login.form.data', array()); - $this->app->redirect(Route::_($this->app->getUserState('users.login.form.return'), false)); - } - - /** - * Method to log out a user. - * - * @return void - * - * @since 1.6 - */ - public function logout() - { - $this->checkToken('request'); - - $app = $this->app; - - // Prepare the logout options. - $options = array( - 'clientid' => $app->get('shared_session', '0') ? null : 0, - ); - - // Perform the log out. - $error = $app->logout(null, $options); - $input = $app->input->getInputForRequestMethod(); - - // Check if the log out succeeded. - if ($error instanceof \Exception) - { - $app->redirect(Route::_('index.php?option=com_users&view=login', false)); - } - - // Get the return URL from the request and validate that it is internal. - $return = $input->get('return', '', 'BASE64'); - $return = base64_decode($return); - - // Check for a simple menu item id - if (is_numeric($return)) - { - $language = $this->getModel('Login', 'Site')->getMenuLanguage($return); - $return = 'index.php?Itemid=' . $return . ($language !== '*' ? '&lang=' . $language : ''); - } - elseif (!Uri::isInternal($return)) - { - $return = ''; - } - - // In case redirect url is not set, redirect user to homepage - if (empty($return)) - { - $return = Uri::root(); - } - - // Redirect the user. - $app->redirect(Route::_($return, false)); - } - - /** - * Method to logout directly and redirect to page. - * - * @return void - * - * @since 3.5 - */ - public function menulogout() - { - // Get the ItemID of the page to redirect after logout - $app = $this->app; - $active = $app->getMenu()->getActive(); - $itemid = $active ? $active->getParams()->get('logout') : 0; - - // Get the language of the page when multilang is on - if (Multilanguage::isEnabled()) - { - if ($itemid) - { - $language = $this->getModel('Login', 'Site')->getMenuLanguage($itemid); - - // URL to redirect after logout - $url = 'index.php?Itemid=' . $itemid . ($language !== '*' ? '&lang=' . $language : ''); - } - else - { - // Logout is set to default. Get the home page ItemID - $lang_code = $app->input->cookie->getString(ApplicationHelper::getHash('language')); - $item = $app->getMenu()->getDefault($lang_code); - $itemid = $item->id; - - // Redirect to Home page after logout - $url = 'index.php?Itemid=' . $itemid; - } - } - else - { - // URL to redirect after logout, default page if no ItemID is set - $url = $itemid ? 'index.php?Itemid=' . $itemid : Uri::root(); - } - - // Logout and redirect - $this->setRedirect('index.php?option=com_users&task=user.logout&' . Session::getFormToken() . '=1&return=' . base64_encode($url)); - } - - /** - * Method to request a username reminder. - * - * @return boolean - * - * @since 1.6 - */ - public function remind() - { - // Check the request token. - $this->checkToken('post'); - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\RemindModel $model */ - $model = $this->getModel('Remind', 'Site'); - $data = $this->input->post->get('jform', array(), 'array'); - - // Submit the username remind request. - $return = $model->processRemindRequest($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - $message = $app->get('error_reporting') - ? $return->getMessage() - : Text::_('COM_USERS_REMIND_REQUEST_ERROR'); - - // Go back to the complete form. - $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'error'); - - return false; - } - - if ($return === false) - { - // Go back to the complete form. - $message = Text::sprintf('COM_USERS_REMIND_REQUEST_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'notice'); - - return false; - } - - // Proceed to the login form. - $message = Text::_('COM_USERS_REMIND_REQUEST_SUCCESS'); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message); - - return true; - } - - /** - * Method to resend a user. - * - * @return void - * - * @since 1.6 - */ - public function resend() - { - // Check for request forgeries - // $this->checkToken('post'); - } + /** + * Method to log in a user. + * + * @return void + * + * @since 1.6 + */ + public function login() + { + $this->checkToken('post'); + + $input = $this->input->getInputForRequestMethod(); + + // Populate the data array: + $data = array(); + + $data['return'] = base64_decode($input->get('return', '', 'BASE64')); + $data['username'] = $input->get('username', '', 'USERNAME'); + $data['password'] = $input->get('password', '', 'RAW'); + $data['secretkey'] = $input->get('secretkey', '', 'RAW'); + + // Check for a simple menu item id + if (is_numeric($data['return'])) { + $language = $this->getModel('Login', 'Site')->getMenuLanguage($data['return']); + $data['return'] = 'index.php?Itemid=' . $data['return'] . ($language !== '*' ? '&lang=' . $language : ''); + } + // Don't redirect to an external URL. + elseif (!Uri::isInternal($data['return'])) { + $data['return'] = ''; + } + + // Set the return URL if empty. + if (empty($data['return'])) { + $data['return'] = 'index.php?option=com_users&view=profile'; + } + + // Set the return URL in the user state to allow modification by plugins + $this->app->setUserState('users.login.form.return', $data['return']); + + // Get the log in options. + $options = array(); + $options['remember'] = $this->input->getBool('remember', false); + $options['return'] = $data['return']; + + // Get the log in credentials. + $credentials = array(); + $credentials['username'] = $data['username']; + $credentials['password'] = $data['password']; + $credentials['secretkey'] = $data['secretkey']; + + // Perform the log in. + if (true !== $this->app->login($credentials, $options)) { + // Login failed ! + // Clear user name, password and secret key before sending the login form back to the user. + $data['remember'] = (int) $options['remember']; + $data['username'] = ''; + $data['password'] = ''; + $data['secretkey'] = ''; + $this->app->setUserState('users.login.form.data', $data); + $this->app->redirect(Route::_('index.php?option=com_users&view=login', false)); + } + + // Success + if ($options['remember'] == true) { + $this->app->setUserState('rememberLogin', true); + } + + $this->app->setUserState('users.login.form.data', array()); + $this->app->redirect(Route::_($this->app->getUserState('users.login.form.return'), false)); + } + + /** + * Method to log out a user. + * + * @return void + * + * @since 1.6 + */ + public function logout() + { + $this->checkToken('request'); + + $app = $this->app; + + // Prepare the logout options. + $options = array( + 'clientid' => $app->get('shared_session', '0') ? null : 0, + ); + + // Perform the log out. + $error = $app->logout(null, $options); + $input = $app->input->getInputForRequestMethod(); + + // Check if the log out succeeded. + if ($error instanceof \Exception) { + $app->redirect(Route::_('index.php?option=com_users&view=login', false)); + } + + // Get the return URL from the request and validate that it is internal. + $return = $input->get('return', '', 'BASE64'); + $return = base64_decode($return); + + // Check for a simple menu item id + if (is_numeric($return)) { + $language = $this->getModel('Login', 'Site')->getMenuLanguage($return); + $return = 'index.php?Itemid=' . $return . ($language !== '*' ? '&lang=' . $language : ''); + } elseif (!Uri::isInternal($return)) { + $return = ''; + } + + // In case redirect url is not set, redirect user to homepage + if (empty($return)) { + $return = Uri::root(); + } + + // Redirect the user. + $app->redirect(Route::_($return, false)); + } + + /** + * Method to logout directly and redirect to page. + * + * @return void + * + * @since 3.5 + */ + public function menulogout() + { + // Get the ItemID of the page to redirect after logout + $app = $this->app; + $active = $app->getMenu()->getActive(); + $itemid = $active ? $active->getParams()->get('logout') : 0; + + // Get the language of the page when multilang is on + if (Multilanguage::isEnabled()) { + if ($itemid) { + $language = $this->getModel('Login', 'Site')->getMenuLanguage($itemid); + + // URL to redirect after logout + $url = 'index.php?Itemid=' . $itemid . ($language !== '*' ? '&lang=' . $language : ''); + } else { + // Logout is set to default. Get the home page ItemID + $lang_code = $app->input->cookie->getString(ApplicationHelper::getHash('language')); + $item = $app->getMenu()->getDefault($lang_code); + $itemid = $item->id; + + // Redirect to Home page after logout + $url = 'index.php?Itemid=' . $itemid; + } + } else { + // URL to redirect after logout, default page if no ItemID is set + $url = $itemid ? 'index.php?Itemid=' . $itemid : Uri::root(); + } + + // Logout and redirect + $this->setRedirect('index.php?option=com_users&task=user.logout&' . Session::getFormToken() . '=1&return=' . base64_encode($url)); + } + + /** + * Method to request a username reminder. + * + * @return boolean + * + * @since 1.6 + */ + public function remind() + { + // Check the request token. + $this->checkToken('post'); + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\RemindModel $model */ + $model = $this->getModel('Remind', 'Site'); + $data = $this->input->post->get('jform', array(), 'array'); + + // Submit the username remind request. + $return = $model->processRemindRequest($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + $message = $app->get('error_reporting') + ? $return->getMessage() + : Text::_('COM_USERS_REMIND_REQUEST_ERROR'); + + // Go back to the complete form. + $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'error'); + + return false; + } + + if ($return === false) { + // Go back to the complete form. + $message = Text::sprintf('COM_USERS_REMIND_REQUEST_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'notice'); + + return false; + } + + // Proceed to the login form. + $message = Text::_('COM_USERS_REMIND_REQUEST_SUCCESS'); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message); + + return true; + } + + /** + * Method to resend a user. + * + * @return void + * + * @since 1.6 + */ + public function resend() + { + // Check for request forgeries + // $this->checkToken('post'); + } } diff --git a/components/com_users/src/Model/BackupcodesModel.php b/components/com_users/src/Model/BackupcodesModel.php index 0f84826b7940d..a0ad57313a908 100644 --- a/components/com_users/src/Model/BackupcodesModel.php +++ b/components/com_users/src/Model/BackupcodesModel.php @@ -1,4 +1,5 @@ loadForm('com_users.login', 'login', array('load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Check the session for previously entered login form data. - $app = Factory::getApplication(); - $data = $app->getUserState('users.login.form.data', array()); - - $input = $app->input->getInputForRequestMethod(); - - // Check for return URL from the request first - if ($return = $input->get('return', '', 'BASE64')) - { - $data['return'] = base64_decode($return); - - if (!Uri::isInternal($data['return'])) - { - $data['return'] = ''; - } - } - - $app->setUserState('users.login.form.data', $data); - - $this->preprocessData('com_users.login', $data); - - return $data; - } - - /** - * Method to auto-populate the model state. - * - * Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_users'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Override Joomla\CMS\MVC\Model\AdminModel::preprocessForm to ensure the correct plugin group is loaded. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - parent::preprocessForm($form, $data, $group); - } - - /** - * Returns the language for the given menu id. - * - * @param int $id The menu id - * - * @return string - * - * @since 4.2.0 - */ - public function getMenuLanguage(int $id): string - { - if (!Multilanguage::isEnabled()) - { - return ''; - } - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('language')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('client_id') . ' = 0') - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - return $db->loadResult(); - } - catch (\RuntimeException $e) - { - return ''; - } - } + /** + * Method to get the login form. + * + * The base form is loaded from XML and then an event is fired + * for users plugins to extend the form with extra fields. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.login', 'login', array('load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Check the session for previously entered login form data. + $app = Factory::getApplication(); + $data = $app->getUserState('users.login.form.data', array()); + + $input = $app->input->getInputForRequestMethod(); + + // Check for return URL from the request first + if ($return = $input->get('return', '', 'BASE64')) { + $data['return'] = base64_decode($return); + + if (!Uri::isInternal($data['return'])) { + $data['return'] = ''; + } + } + + $app->setUserState('users.login.form.data', $data); + + $this->preprocessData('com_users.login', $data); + + return $data; + } + + /** + * Method to auto-populate the model state. + * + * Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_users'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Override Joomla\CMS\MVC\Model\AdminModel::preprocessForm to ensure the correct plugin group is loaded. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + parent::preprocessForm($form, $data, $group); + } + + /** + * Returns the language for the given menu id. + * + * @param int $id The menu id + * + * @return string + * + * @since 4.2.0 + */ + public function getMenuLanguage(int $id): string + { + if (!Multilanguage::isEnabled()) { + return ''; + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('language')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('client_id') . ' = 0') + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + return $db->loadResult(); + } catch (\RuntimeException $e) { + return ''; + } + } } diff --git a/components/com_users/src/Model/MethodModel.php b/components/com_users/src/Model/MethodModel.php index 867155b060f40..516784da2854c 100644 --- a/components/com_users/src/Model/MethodModel.php +++ b/components/com_users/src/Model/MethodModel.php @@ -1,4 +1,5 @@ array('validate' => 'user') - ), $config - ); - - parent::__construct($config, $factory, $formFactory); - } - - /** - * Method to get the profile form data. - * - * The base form data is loaded and then an event is fired - * for users plugins to extend the data. - * - * @return User - * - * @since 1.6 - * @throws \Exception - */ - public function getData() - { - if ($this->data === null) - { - $userId = $this->getState('user.id'); - - // Initialise the table with Joomla\CMS\User\User. - $this->data = new User($userId); - - // Set the base user data. - $this->data->email1 = $this->data->get('email'); - - // Override the base user data with any data in the session. - $temp = (array) Factory::getApplication()->getUserState('com_users.edit.profile.data', array()); - - foreach ($temp as $k => $v) - { - $this->data->$k = $v; - } - - // Unset the passwords. - unset($this->data->password1, $this->data->password2); - - $registry = new Registry($this->data->params); - $this->data->params = $registry->toArray(); - } - - return $this->data; - } - - /** - * Method to get the profile form. - * - * The base form is loaded from XML and then an event is fired - * for users plugins to extend the form with extra fields. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.profile', 'profile', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Check for username compliance and parameter set - $isUsernameCompliant = true; - $username = $loadData ? $form->getValue('username') : $this->loadFormData()->username; - - if ($username) - { - $isUsernameCompliant = !(preg_match('#[<>"\'%;()&\\\\]|\\.\\./#', $username) || strlen(utf8_decode($username)) < 2 - || trim($username) !== $username); - } - - $this->setState('user.username.compliant', $isUsernameCompliant); - - if ($isUsernameCompliant && !ComponentHelper::getParams('com_users')->get('change_login_name')) - { - $form->setFieldAttribute('username', 'class', ''); - $form->setFieldAttribute('username', 'filter', ''); - $form->setFieldAttribute('username', 'description', 'COM_USERS_PROFILE_NOCHANGE_USERNAME_DESC'); - $form->setFieldAttribute('username', 'validate', ''); - $form->setFieldAttribute('username', 'message', ''); - $form->setFieldAttribute('username', 'readonly', 'true'); - $form->setFieldAttribute('username', 'required', 'false'); - } - - // When multilanguage is set, a user's default site language should also be a Content Language - if (Multilanguage::isEnabled()) - { - $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); - } - - // If the user needs to change their password, mark the password fields as required - if (Factory::getUser()->requireReset) - { - $form->setFieldAttribute('password1', 'required', 'true'); - $form->setFieldAttribute('password2', 'required', 'true'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $data = $this->getData(); - - $this->preprocessData('com_users.profile', $data, 'user'); - - return $data; - } - - /** - * Override preprocessForm to load the user plugin group instead of content. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @throws \Exception if there is an error in the form event. - * - * @since 1.6 - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - if (ComponentHelper::getParams('com_users')->get('frontend_userparams')) - { - $form->loadFile('frontend', false); - - if (Factory::getUser()->authorise('core.login.admin')) - { - $form->loadFile('frontend_admin', false); - } - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_users'); - - // Get the user id. - $userId = Factory::getApplication()->getUserState('com_users.edit.profile.id'); - $userId = !empty($userId) ? $userId : (int) Factory::getUser()->get('id'); - - // Set the user id. - $this->setState('user.id', $userId); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return mixed The user id on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function save($data) - { - $userId = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('user.id'); - - $user = new User($userId); - - // Prepare the data for the user object. - $data['email'] = PunycodeHelper::emailToPunycode($data['email1']); - $data['password'] = $data['password1']; - - // Unset the username if it should not be overwritten - $isUsernameCompliant = $this->getState('user.username.compliant'); - - if ($isUsernameCompliant && !ComponentHelper::getParams('com_users')->get('change_login_name')) - { - unset($data['username']); - } - - // Unset block and sendEmail so they do not get overwritten - unset($data['block'], $data['sendEmail']); - - // Bind the data. - if (!$user->bind($data)) - { - $this->setError($user->getError()); - - return false; - } - - // Load the users plugin group. - PluginHelper::importPlugin('user'); - - // Retrieve the user groups so they don't get overwritten - unset($user->groups); - $user->groups = Access::getGroupsByUser($user->id, false); - - // Store the data. - if (!$user->save()) - { - $this->setError($user->getError()); - - return false; - } - - // Destroy all active sessions for the user after changing the password - if ($data['password']) - { - UserHelper::destroyUserSessions($user->id, true); - } - - return $user->id; - } - - /** - * Gets the configuration forms for all two-factor authentication methods - * in an array. - * - * @param integer $userId The user ID to load the forms for (optional) - * - * @return array - * - * @since 3.2 - * @deprecated 4.2.0 Will be removed in 5.0. - */ - public function getTwofactorform($userId = null) - { - return []; - } - - /** - * No longer used - * - * @param integer $userId Ignored - * - * @return \stdClass - * - * @since 3.2 - * @deprecated 4.2.0 Will be removed in 5.0 - */ - public function getOtpConfig($userId = null) - { - @trigger_error( - sprintf( - '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getUserMfaRecords() instead.', - __METHOD__ - ), - E_USER_DEPRECATED - ); - - /** @var UserModel $model */ - $model = $this->bootComponent('com_users') - ->getMVCFactory()->createModel('User', 'Administrator'); - - return $model->getOtpConfig(); - } + /** + * @var object The user profile data. + * @since 1.6 + */ + protected $data; + + /** + * Constructor. + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * @param FormFactoryInterface $formFactory The form factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null) + { + $config = array_merge( + array( + 'events_map' => array('validate' => 'user') + ), + $config + ); + + parent::__construct($config, $factory, $formFactory); + } + + /** + * Method to get the profile form data. + * + * The base form data is loaded and then an event is fired + * for users plugins to extend the data. + * + * @return User + * + * @since 1.6 + * @throws \Exception + */ + public function getData() + { + if ($this->data === null) { + $userId = $this->getState('user.id'); + + // Initialise the table with Joomla\CMS\User\User. + $this->data = new User($userId); + + // Set the base user data. + $this->data->email1 = $this->data->get('email'); + + // Override the base user data with any data in the session. + $temp = (array) Factory::getApplication()->getUserState('com_users.edit.profile.data', array()); + + foreach ($temp as $k => $v) { + $this->data->$k = $v; + } + + // Unset the passwords. + unset($this->data->password1, $this->data->password2); + + $registry = new Registry($this->data->params); + $this->data->params = $registry->toArray(); + } + + return $this->data; + } + + /** + * Method to get the profile form. + * + * The base form is loaded from XML and then an event is fired + * for users plugins to extend the form with extra fields. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.profile', 'profile', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Check for username compliance and parameter set + $isUsernameCompliant = true; + $username = $loadData ? $form->getValue('username') : $this->loadFormData()->username; + + if ($username) { + $isUsernameCompliant = !(preg_match('#[<>"\'%;()&\\\\]|\\.\\./#', $username) || strlen(utf8_decode($username)) < 2 + || trim($username) !== $username); + } + + $this->setState('user.username.compliant', $isUsernameCompliant); + + if ($isUsernameCompliant && !ComponentHelper::getParams('com_users')->get('change_login_name')) { + $form->setFieldAttribute('username', 'class', ''); + $form->setFieldAttribute('username', 'filter', ''); + $form->setFieldAttribute('username', 'description', 'COM_USERS_PROFILE_NOCHANGE_USERNAME_DESC'); + $form->setFieldAttribute('username', 'validate', ''); + $form->setFieldAttribute('username', 'message', ''); + $form->setFieldAttribute('username', 'readonly', 'true'); + $form->setFieldAttribute('username', 'required', 'false'); + } + + // When multilanguage is set, a user's default site language should also be a Content Language + if (Multilanguage::isEnabled()) { + $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); + } + + // If the user needs to change their password, mark the password fields as required + if (Factory::getUser()->requireReset) { + $form->setFieldAttribute('password1', 'required', 'true'); + $form->setFieldAttribute('password2', 'required', 'true'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $data = $this->getData(); + + $this->preprocessData('com_users.profile', $data, 'user'); + + return $data; + } + + /** + * Override preprocessForm to load the user plugin group instead of content. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @throws \Exception if there is an error in the form event. + * + * @since 1.6 + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + if (ComponentHelper::getParams('com_users')->get('frontend_userparams')) { + $form->loadFile('frontend', false); + + if (Factory::getUser()->authorise('core.login.admin')) { + $form->loadFile('frontend_admin', false); + } + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_users'); + + // Get the user id. + $userId = Factory::getApplication()->getUserState('com_users.edit.profile.id'); + $userId = !empty($userId) ? $userId : (int) Factory::getUser()->get('id'); + + // Set the user id. + $this->setState('user.id', $userId); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return mixed The user id on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function save($data) + { + $userId = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('user.id'); + + $user = new User($userId); + + // Prepare the data for the user object. + $data['email'] = PunycodeHelper::emailToPunycode($data['email1']); + $data['password'] = $data['password1']; + + // Unset the username if it should not be overwritten + $isUsernameCompliant = $this->getState('user.username.compliant'); + + if ($isUsernameCompliant && !ComponentHelper::getParams('com_users')->get('change_login_name')) { + unset($data['username']); + } + + // Unset block and sendEmail so they do not get overwritten + unset($data['block'], $data['sendEmail']); + + // Bind the data. + if (!$user->bind($data)) { + $this->setError($user->getError()); + + return false; + } + + // Load the users plugin group. + PluginHelper::importPlugin('user'); + + // Retrieve the user groups so they don't get overwritten + unset($user->groups); + $user->groups = Access::getGroupsByUser($user->id, false); + + // Store the data. + if (!$user->save()) { + $this->setError($user->getError()); + + return false; + } + + // Destroy all active sessions for the user after changing the password + if ($data['password']) { + UserHelper::destroyUserSessions($user->id, true); + } + + return $user->id; + } + + /** + * Gets the configuration forms for all two-factor authentication methods + * in an array. + * + * @param integer $userId The user ID to load the forms for (optional) + * + * @return array + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0. + */ + public function getTwofactorform($userId = null) + { + return []; + } + + /** + * No longer used + * + * @param integer $userId Ignored + * + * @return \stdClass + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public function getOtpConfig($userId = null) + { + @trigger_error( + sprintf( + '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getUserMfaRecords() instead.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + /** @var UserModel $model */ + $model = $this->bootComponent('com_users') + ->getMVCFactory()->createModel('User', 'Administrator'); + + return $model->getOtpConfig(); + } } diff --git a/components/com_users/src/Model/RegistrationModel.php b/components/com_users/src/Model/RegistrationModel.php index 1f8d331584e1a..0c3d0553c07ce 100644 --- a/components/com_users/src/Model/RegistrationModel.php +++ b/components/com_users/src/Model/RegistrationModel.php @@ -1,4 +1,5 @@ array('validate' => 'user') - ), $config - ); - - parent::__construct($config, $factory, $formFactory); - } - - /** - * Method to get the user ID from the given token - * - * @param string $token The activation token. - * - * @return mixed False on failure, id of the user on success - * - * @since 3.8.13 - */ - public function getUserIdFromToken($token) - { - $db = $this->getDatabase(); - - // Get the user id based on the token. - $query = $db->getQuery(true); - $query->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('activation') . ' = :activation') - ->where($db->quoteName('block') . ' = 1') - ->where($db->quoteName('lastvisitDate') . ' IS NULL') - ->bind(':activation', $token); - $db->setQuery($query); - - try - { - return (int) $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - } - - /** - * Method to activate a user account. - * - * @param string $token The activation token. - * - * @return mixed False on failure, user object on success. - * - * @since 1.6 - */ - public function activate($token) - { - $app = Factory::getApplication(); - $userParams = ComponentHelper::getParams('com_users'); - $userId = $this->getUserIdFromToken($token); - - // Check for a valid user id. - if (!$userId) - { - $this->setError(Text::_('COM_USERS_ACTIVATION_TOKEN_NOT_FOUND')); - - return false; - } - - // Load the users plugin group. - PluginHelper::importPlugin('user'); - - // Activate the user. - $user = Factory::getUser($userId); - - // Admin activation is on and user is verifying their email - if (($userParams->get('useractivation') == 2) && !$user->getParam('activate', 0)) - { - $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - // Compile the admin notification mail values. - $data = $user->getProperties(); - $data['activation'] = ApplicationHelper::getHash(UserHelper::genRandomPassword()); - $user->set('activation', $data['activation']); - $data['siteurl'] = Uri::base(); - $data['activate'] = Route::link( - 'site', - 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], - false, - $linkMode, - true - ); - - $data['fromname'] = $app->get('fromname'); - $data['mailfrom'] = $app->get('mailfrom'); - $data['sitename'] = $app->get('sitename'); - $user->setParam('activate', 1); - - // Get all admin users - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(array('name', 'email', 'sendEmail', 'id'))) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('sendEmail') . ' = 1') - ->where($db->quoteName('block') . ' = 0'); - - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - - // Send mail to all users with users creating permissions and receiving system emails - foreach ($rows as $row) - { - $usercreator = Factory::getUser($row->id); - - if ($usercreator->authorise('core.create', 'com_users') && $usercreator->authorise('core.manage', 'com_users')) - { - try - { - $mailer = new MailTemplate('com_users.registration.admin.verification_request', $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($row->email); - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $return = false; - } - } - - // Check for an error. - if ($return !== true) - { - $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); - - return false; - } - } - } - } - // Admin activation is on and admin is activating the account - elseif (($userParams->get('useractivation') == 2) && $user->getParam('activate', 0)) - { - $user->set('activation', ''); - $user->set('block', '0'); - - // Compile the user activated notification mail values. - $data = $user->getProperties(); - $user->setParam('activate', 0); - $data['fromname'] = $app->get('fromname'); - $data['mailfrom'] = $app->get('mailfrom'); - $data['sitename'] = $app->get('sitename'); - $data['siteurl'] = Uri::base(); - $mailer = new MailTemplate('com_users.registration.user.admin_activated', $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($data['email']); - - try - { - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $return = false; - } - } - - // Check for an error. - if ($return !== true) - { - $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); - - return false; - } - } - else - { - $user->set('activation', ''); - $user->set('block', '0'); - } - - // Store the user object. - if (!$user->save()) - { - $this->setError(Text::sprintf('COM_USERS_REGISTRATION_ACTIVATION_SAVE_FAILED', $user->getError())); - - return false; - } - - return $user; - } - - /** - * Method to get the registration form data. - * - * The base form data is loaded and then an event is fired - * for users plugins to extend the data. - * - * @return mixed Data object on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function getData() - { - if ($this->data === null) - { - $this->data = new \stdClass; - $app = Factory::getApplication(); - $params = ComponentHelper::getParams('com_users'); - - // Override the base user data with any data in the session. - $temp = (array) $app->getUserState('com_users.registration.data', array()); - - // Don't load the data in this getForm call, or we'll call ourself - $form = $this->getForm(array(), false); - - foreach ($temp as $k => $v) - { - // Here we could have a grouped field, let's check it - if (is_array($v)) - { - $this->data->$k = new \stdClass; - - foreach ($v as $key => $val) - { - if ($form->getField($key, $k) !== false) - { - $this->data->$k->$key = $val; - } - } - } - // Only merge the field if it exists in the form. - elseif ($form->getField($k) !== false) - { - $this->data->$k = $v; - } - } - - // Get the groups the user should be added to after registration. - $this->data->groups = array(); - - // Get the default new user group, guest or public group if not specified. - $system = $params->get('new_usertype', $params->get('guest_usergroup', 1)); - - $this->data->groups[] = $system; - - // Unset the passwords. - unset($this->data->password1, $this->data->password2); - - // Get the dispatcher and load the users plugins. - PluginHelper::importPlugin('user'); - - // Trigger the data preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.registration', $this->data)); - } - - return $this->data; - } - - /** - * Method to get the registration form. - * - * The base form is loaded from XML and then an event is fired - * for users plugins to extend the form with extra fields. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.registration', 'registration', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // When multilanguage is set, a user's default site language should also be a Content Language - if (Multilanguage::isEnabled()) - { - $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $data = $this->getData(); - - if (Multilanguage::isEnabled() && empty($data->language)) - { - $data->language = Factory::getLanguage()->getTag(); - } - - $this->preprocessData('com_users.registration', $data); - - return $data; - } - - /** - * Override preprocessForm to load the user plugin group instead of content. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - $userParams = ComponentHelper::getParams('com_users'); - - // Add the choice for site language at registration time - if ($userParams->get('site_language') == 1 && $userParams->get('frontend_userparams') == 1) - { - $form->loadFile('sitelang', false); - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState() - { - // Get the application object. - $app = Factory::getApplication(); - $params = $app->getParams('com_users'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to save the form data. - * - * @param array $temp The form data. - * - * @return mixed The user id on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function register($temp) - { - $params = ComponentHelper::getParams('com_users'); - - // Initialise the table with Joomla\CMS\User\User. - $user = new User; - $data = (array) $this->getData(); - - // Merge in the registration data. - foreach ($temp as $k => $v) - { - $data[$k] = $v; - } - - // Prepare the data for the user object. - $data['email'] = PunycodeHelper::emailToPunycode($data['email1']); - $data['password'] = $data['password1']; - $useractivation = $params->get('useractivation'); - $sendpassword = $params->get('sendpassword', 1); - - // Check if the user needs to activate their account. - if (($useractivation == 1) || ($useractivation == 2)) - { - $data['activation'] = ApplicationHelper::getHash(UserHelper::genRandomPassword()); - $data['block'] = 1; - } - - // Bind the data. - if (!$user->bind($data)) - { - $this->setError($user->getError()); - - return false; - } - - // Load the users plugin group. - PluginHelper::importPlugin('user'); - - // Store the data. - if (!$user->save()) - { - $this->setError(Text::sprintf('COM_USERS_REGISTRATION_SAVE_FAILED', $user->getError())); - - return false; - } - - $app = Factory::getApplication(); - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Compile the notification mail values. - $data = $user->getProperties(); - $data['fromname'] = $app->get('fromname'); - $data['mailfrom'] = $app->get('mailfrom'); - $data['sitename'] = $app->get('sitename'); - $data['siteurl'] = Uri::root(); - - // Handle account activation/confirmation emails. - if ($useractivation == 2) - { - // Set the link to confirm the user email. - $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - $data['activate'] = Route::link( - 'site', - 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], - false, - $linkMode, - true - ); - - $mailtemplate = 'com_users.registration.user.admin_activation'; - } - elseif ($useractivation == 1) - { - // Set the link to activate the user account. - $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - $data['activate'] = Route::link( - 'site', - 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], - false, - $linkMode, - true - ); - - $mailtemplate = 'com_users.registration.user.self_activation'; - } - else - { - $mailtemplate = 'com_users.registration.user.registration_mail'; - } - - if ($sendpassword) - { - $mailtemplate .= '_w_pw'; - } - - // Try to send the registration email. - try - { - $mailer = new MailTemplate($mailtemplate, $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($data['email']); - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); - - $return = false; - } - } - - // Send mail to all users with user creating permissions and receiving system emails - if (($params->get('useractivation') < 2) && ($params->get('mail_to_admin') == 1)) - { - // Get all admin users - $query->clear() - ->select($db->quoteName(array('name', 'email', 'sendEmail', 'id'))) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('sendEmail') . ' = 1') - ->where($db->quoteName('block') . ' = 0'); - - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - - // Send mail to all superadministrators id - foreach ($rows as $row) - { - $usercreator = Factory::getUser($row->id); - - if (!$usercreator->authorise('core.create', 'com_users') || !$usercreator->authorise('core.manage', 'com_users')) - { - continue; - } - - try - { - $mailer = new MailTemplate('com_users.registration.admin.new_notification', $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($row->email); - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $return = false; - } - } - - // Check for an error. - if ($return !== true) - { - $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); - - return false; - } - } - } - - // Check for an error. - if ($return !== true) - { - $this->setError(Text::_('COM_USERS_REGISTRATION_SEND_MAIL_FAILED')); - - // Send a system message to administrators receiving system mails - $db = $this->getDatabase(); - $query->clear() - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('block') . ' = 0') - ->where($db->quoteName('sendEmail') . ' = 1'); - $db->setQuery($query); - - try - { - $userids = $db->loadColumn(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - - if (count($userids) > 0) - { - $jdate = new Date; - $dateToSql = $jdate->toSql(); - $subject = Text::_('COM_USERS_MAIL_SEND_FAILURE_SUBJECT'); - $message = Text::sprintf('COM_USERS_MAIL_SEND_FAILURE_BODY', $data['username']); - - // Build the query to add the messages - foreach ($userids as $userid) - { - $values = [ - ':user_id_from', - ':user_id_to', - ':date_time', - ':subject', - ':message', - ]; - $query->clear() - ->insert($db->quoteName('#__messages')) - ->columns($db->quoteName(['user_id_from', 'user_id_to', 'date_time', 'subject', 'message'])) - ->values(implode(',', $values)); - $query->bind(':user_id_from', $userid, ParameterType::INTEGER) - ->bind(':user_id_to', $userid, ParameterType::INTEGER) - ->bind(':date_time', $dateToSql) - ->bind(':subject', $subject) - ->bind(':message', $message); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - } - } - - return false; - } - - if ($useractivation == 1) - { - return 'useractivate'; - } - elseif ($useractivation == 2) - { - return 'adminactivate'; - } - else - { - return $user->id; - } - } + /** + * @var object The user registration data. + * @since 1.6 + */ + protected $data; + + /** + * Constructor. + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * @param FormFactoryInterface $formFactory The form factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null) + { + $config = array_merge( + array( + 'events_map' => array('validate' => 'user') + ), + $config + ); + + parent::__construct($config, $factory, $formFactory); + } + + /** + * Method to get the user ID from the given token + * + * @param string $token The activation token. + * + * @return mixed False on failure, id of the user on success + * + * @since 3.8.13 + */ + public function getUserIdFromToken($token) + { + $db = $this->getDatabase(); + + // Get the user id based on the token. + $query = $db->getQuery(true); + $query->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('activation') . ' = :activation') + ->where($db->quoteName('block') . ' = 1') + ->where($db->quoteName('lastvisitDate') . ' IS NULL') + ->bind(':activation', $token); + $db->setQuery($query); + + try { + return (int) $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + } + + /** + * Method to activate a user account. + * + * @param string $token The activation token. + * + * @return mixed False on failure, user object on success. + * + * @since 1.6 + */ + public function activate($token) + { + $app = Factory::getApplication(); + $userParams = ComponentHelper::getParams('com_users'); + $userId = $this->getUserIdFromToken($token); + + // Check for a valid user id. + if (!$userId) { + $this->setError(Text::_('COM_USERS_ACTIVATION_TOKEN_NOT_FOUND')); + + return false; + } + + // Load the users plugin group. + PluginHelper::importPlugin('user'); + + // Activate the user. + $user = Factory::getUser($userId); + + // Admin activation is on and user is verifying their email + if (($userParams->get('useractivation') == 2) && !$user->getParam('activate', 0)) { + $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + // Compile the admin notification mail values. + $data = $user->getProperties(); + $data['activation'] = ApplicationHelper::getHash(UserHelper::genRandomPassword()); + $user->set('activation', $data['activation']); + $data['siteurl'] = Uri::base(); + $data['activate'] = Route::link( + 'site', + 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], + false, + $linkMode, + true + ); + + $data['fromname'] = $app->get('fromname'); + $data['mailfrom'] = $app->get('mailfrom'); + $data['sitename'] = $app->get('sitename'); + $user->setParam('activate', 1); + + // Get all admin users + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(array('name', 'email', 'sendEmail', 'id'))) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('sendEmail') . ' = 1') + ->where($db->quoteName('block') . ' = 0'); + + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + + // Send mail to all users with users creating permissions and receiving system emails + foreach ($rows as $row) { + $usercreator = Factory::getUser($row->id); + + if ($usercreator->authorise('core.create', 'com_users') && $usercreator->authorise('core.manage', 'com_users')) { + try { + $mailer = new MailTemplate('com_users.registration.admin.verification_request', $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($row->email); + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $return = false; + } + } + + // Check for an error. + if ($return !== true) { + $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); + + return false; + } + } + } + } + // Admin activation is on and admin is activating the account + elseif (($userParams->get('useractivation') == 2) && $user->getParam('activate', 0)) { + $user->set('activation', ''); + $user->set('block', '0'); + + // Compile the user activated notification mail values. + $data = $user->getProperties(); + $user->setParam('activate', 0); + $data['fromname'] = $app->get('fromname'); + $data['mailfrom'] = $app->get('mailfrom'); + $data['sitename'] = $app->get('sitename'); + $data['siteurl'] = Uri::base(); + $mailer = new MailTemplate('com_users.registration.user.admin_activated', $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($data['email']); + + try { + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $return = false; + } + } + + // Check for an error. + if ($return !== true) { + $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); + + return false; + } + } else { + $user->set('activation', ''); + $user->set('block', '0'); + } + + // Store the user object. + if (!$user->save()) { + $this->setError(Text::sprintf('COM_USERS_REGISTRATION_ACTIVATION_SAVE_FAILED', $user->getError())); + + return false; + } + + return $user; + } + + /** + * Method to get the registration form data. + * + * The base form data is loaded and then an event is fired + * for users plugins to extend the data. + * + * @return mixed Data object on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function getData() + { + if ($this->data === null) { + $this->data = new \stdClass(); + $app = Factory::getApplication(); + $params = ComponentHelper::getParams('com_users'); + + // Override the base user data with any data in the session. + $temp = (array) $app->getUserState('com_users.registration.data', array()); + + // Don't load the data in this getForm call, or we'll call ourself + $form = $this->getForm(array(), false); + + foreach ($temp as $k => $v) { + // Here we could have a grouped field, let's check it + if (is_array($v)) { + $this->data->$k = new \stdClass(); + + foreach ($v as $key => $val) { + if ($form->getField($key, $k) !== false) { + $this->data->$k->$key = $val; + } + } + } + // Only merge the field if it exists in the form. + elseif ($form->getField($k) !== false) { + $this->data->$k = $v; + } + } + + // Get the groups the user should be added to after registration. + $this->data->groups = array(); + + // Get the default new user group, guest or public group if not specified. + $system = $params->get('new_usertype', $params->get('guest_usergroup', 1)); + + $this->data->groups[] = $system; + + // Unset the passwords. + unset($this->data->password1, $this->data->password2); + + // Get the dispatcher and load the users plugins. + PluginHelper::importPlugin('user'); + + // Trigger the data preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.registration', $this->data)); + } + + return $this->data; + } + + /** + * Method to get the registration form. + * + * The base form is loaded from XML and then an event is fired + * for users plugins to extend the form with extra fields. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.registration', 'registration', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // When multilanguage is set, a user's default site language should also be a Content Language + if (Multilanguage::isEnabled()) { + $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $data = $this->getData(); + + if (Multilanguage::isEnabled() && empty($data->language)) { + $data->language = Factory::getLanguage()->getTag(); + } + + $this->preprocessData('com_users.registration', $data); + + return $data; + } + + /** + * Override preprocessForm to load the user plugin group instead of content. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + $userParams = ComponentHelper::getParams('com_users'); + + // Add the choice for site language at registration time + if ($userParams->get('site_language') == 1 && $userParams->get('frontend_userparams') == 1) { + $form->loadFile('sitelang', false); + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState() + { + // Get the application object. + $app = Factory::getApplication(); + $params = $app->getParams('com_users'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to save the form data. + * + * @param array $temp The form data. + * + * @return mixed The user id on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function register($temp) + { + $params = ComponentHelper::getParams('com_users'); + + // Initialise the table with Joomla\CMS\User\User. + $user = new User(); + $data = (array) $this->getData(); + + // Merge in the registration data. + foreach ($temp as $k => $v) { + $data[$k] = $v; + } + + // Prepare the data for the user object. + $data['email'] = PunycodeHelper::emailToPunycode($data['email1']); + $data['password'] = $data['password1']; + $useractivation = $params->get('useractivation'); + $sendpassword = $params->get('sendpassword', 1); + + // Check if the user needs to activate their account. + if (($useractivation == 1) || ($useractivation == 2)) { + $data['activation'] = ApplicationHelper::getHash(UserHelper::genRandomPassword()); + $data['block'] = 1; + } + + // Bind the data. + if (!$user->bind($data)) { + $this->setError($user->getError()); + + return false; + } + + // Load the users plugin group. + PluginHelper::importPlugin('user'); + + // Store the data. + if (!$user->save()) { + $this->setError(Text::sprintf('COM_USERS_REGISTRATION_SAVE_FAILED', $user->getError())); + + return false; + } + + $app = Factory::getApplication(); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Compile the notification mail values. + $data = $user->getProperties(); + $data['fromname'] = $app->get('fromname'); + $data['mailfrom'] = $app->get('mailfrom'); + $data['sitename'] = $app->get('sitename'); + $data['siteurl'] = Uri::root(); + + // Handle account activation/confirmation emails. + if ($useractivation == 2) { + // Set the link to confirm the user email. + $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + $data['activate'] = Route::link( + 'site', + 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], + false, + $linkMode, + true + ); + + $mailtemplate = 'com_users.registration.user.admin_activation'; + } elseif ($useractivation == 1) { + // Set the link to activate the user account. + $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + $data['activate'] = Route::link( + 'site', + 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], + false, + $linkMode, + true + ); + + $mailtemplate = 'com_users.registration.user.self_activation'; + } else { + $mailtemplate = 'com_users.registration.user.registration_mail'; + } + + if ($sendpassword) { + $mailtemplate .= '_w_pw'; + } + + // Try to send the registration email. + try { + $mailer = new MailTemplate($mailtemplate, $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($data['email']); + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); + + $return = false; + } + } + + // Send mail to all users with user creating permissions and receiving system emails + if (($params->get('useractivation') < 2) && ($params->get('mail_to_admin') == 1)) { + // Get all admin users + $query->clear() + ->select($db->quoteName(array('name', 'email', 'sendEmail', 'id'))) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('sendEmail') . ' = 1') + ->where($db->quoteName('block') . ' = 0'); + + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + + // Send mail to all superadministrators id + foreach ($rows as $row) { + $usercreator = Factory::getUser($row->id); + + if (!$usercreator->authorise('core.create', 'com_users') || !$usercreator->authorise('core.manage', 'com_users')) { + continue; + } + + try { + $mailer = new MailTemplate('com_users.registration.admin.new_notification', $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($row->email); + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $return = false; + } + } + + // Check for an error. + if ($return !== true) { + $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); + + return false; + } + } + } + + // Check for an error. + if ($return !== true) { + $this->setError(Text::_('COM_USERS_REGISTRATION_SEND_MAIL_FAILED')); + + // Send a system message to administrators receiving system mails + $db = $this->getDatabase(); + $query->clear() + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('block') . ' = 0') + ->where($db->quoteName('sendEmail') . ' = 1'); + $db->setQuery($query); + + try { + $userids = $db->loadColumn(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + + if (count($userids) > 0) { + $jdate = new Date(); + $dateToSql = $jdate->toSql(); + $subject = Text::_('COM_USERS_MAIL_SEND_FAILURE_SUBJECT'); + $message = Text::sprintf('COM_USERS_MAIL_SEND_FAILURE_BODY', $data['username']); + + // Build the query to add the messages + foreach ($userids as $userid) { + $values = [ + ':user_id_from', + ':user_id_to', + ':date_time', + ':subject', + ':message', + ]; + $query->clear() + ->insert($db->quoteName('#__messages')) + ->columns($db->quoteName(['user_id_from', 'user_id_to', 'date_time', 'subject', 'message'])) + ->values(implode(',', $values)); + $query->bind(':user_id_from', $userid, ParameterType::INTEGER) + ->bind(':user_id_to', $userid, ParameterType::INTEGER) + ->bind(':date_time', $dateToSql) + ->bind(':subject', $subject) + ->bind(':message', $message); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + } + } + + return false; + } + + if ($useractivation == 1) { + return 'useractivate'; + } elseif ($useractivation == 2) { + return 'adminactivate'; + } else { + return $user->id; + } + } } diff --git a/components/com_users/src/Model/RemindModel.php b/components/com_users/src/Model/RemindModel.php index 9fad52db00f4d..5e9e9252e72d2 100644 --- a/components/com_users/src/Model/RemindModel.php +++ b/components/com_users/src/Model/RemindModel.php @@ -1,4 +1,5 @@ loadForm('com_users.remind', 'remind', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Override preprocessForm to load the user plugin group instead of content. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @throws \Exception if there is an error in the form event. - * - * @since 1.6 - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - parent::preprocessForm($form, $data, 'user'); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - * - * @throws \Exception - */ - protected function populateState() - { - // Get the application object. - $app = Factory::getApplication(); - $params = $app->getParams('com_users'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Send the remind username email - * - * @param array $data Array with the data received from the form - * - * @return boolean - * - * @since 1.6 - */ - public function processRemindRequest($data) - { - // Get the form. - $form = $this->getForm(); - $data['email'] = PunycodeHelper::emailToPunycode($data['email']); - - // Check for an error. - if (empty($form)) - { - return false; - } - - // Validate the data. - $data = $this->validate($form, $data); - - // Check for an error. - if ($data instanceof \Exception) - { - return false; - } - - // Check the validation results. - if ($data === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - // Find the user id for the given email address. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $data['email']); - - // Get the user id. - $db->setQuery($query); - - try - { - $user = $db->loadObject(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - - // Check for a user. - if (empty($user)) - { - $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); - - return false; - } - - // Make sure the user isn't blocked. - if ($user->block) - { - $this->setError(Text::_('COM_USERS_USER_BLOCKED')); - - return false; - } - - $app = Factory::getApplication(); - - // Assemble the login link. - $link = 'index.php?option=com_users&view=login'; - $mode = $app->get('force_ssl', 0) == 2 ? 1 : (-1); - - // Put together the email template data. - $data = ArrayHelper::fromObject($user); - $data['sitename'] = $app->get('sitename'); - $data['link_text'] = Route::_($link, false, $mode); - $data['link_html'] = Route::_($link, true, $mode); - - $mailer = new MailTemplate('com_users.reminder', $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($user->email, $user->name); - - // Try to send the password reset request email. - try - { - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $return = false; - } - } - - // Check for an error. - if ($return !== true) - { - $this->setError(Text::_('COM_USERS_MAIL_FAILED')); - - return false; - } - - Factory::getApplication()->triggerEvent('onUserAfterRemind', array($user)); - - return true; - } + /** + * Method to get the username remind request form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.remind', 'remind', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Override preprocessForm to load the user plugin group instead of content. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @throws \Exception if there is an error in the form event. + * + * @since 1.6 + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + parent::preprocessForm($form, $data, 'user'); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + * + * @throws \Exception + */ + protected function populateState() + { + // Get the application object. + $app = Factory::getApplication(); + $params = $app->getParams('com_users'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Send the remind username email + * + * @param array $data Array with the data received from the form + * + * @return boolean + * + * @since 1.6 + */ + public function processRemindRequest($data) + { + // Get the form. + $form = $this->getForm(); + $data['email'] = PunycodeHelper::emailToPunycode($data['email']); + + // Check for an error. + if (empty($form)) { + return false; + } + + // Validate the data. + $data = $this->validate($form, $data); + + // Check for an error. + if ($data instanceof \Exception) { + return false; + } + + // Check the validation results. + if ($data === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + // Find the user id for the given email address. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $data['email']); + + // Get the user id. + $db->setQuery($query); + + try { + $user = $db->loadObject(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + + // Check for a user. + if (empty($user)) { + $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); + + return false; + } + + // Make sure the user isn't blocked. + if ($user->block) { + $this->setError(Text::_('COM_USERS_USER_BLOCKED')); + + return false; + } + + $app = Factory::getApplication(); + + // Assemble the login link. + $link = 'index.php?option=com_users&view=login'; + $mode = $app->get('force_ssl', 0) == 2 ? 1 : (-1); + + // Put together the email template data. + $data = ArrayHelper::fromObject($user); + $data['sitename'] = $app->get('sitename'); + $data['link_text'] = Route::_($link, false, $mode); + $data['link_html'] = Route::_($link, true, $mode); + + $mailer = new MailTemplate('com_users.reminder', $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($user->email, $user->name); + + // Try to send the password reset request email. + try { + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $return = false; + } + } + + // Check for an error. + if ($return !== true) { + $this->setError(Text::_('COM_USERS_MAIL_FAILED')); + + return false; + } + + Factory::getApplication()->triggerEvent('onUserAfterRemind', array($user)); + + return true; + } } diff --git a/components/com_users/src/Model/ResetModel.php b/components/com_users/src/Model/ResetModel.php index 53d8bed74b8f0..bb7dda76c94dd 100644 --- a/components/com_users/src/Model/ResetModel.php +++ b/components/com_users/src/Model/ResetModel.php @@ -1,4 +1,5 @@ loadForm('com_users.reset_request', 'reset_request', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the password reset complete form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form A Form object on success, false on failure - * - * @since 1.6 - */ - public function getResetCompleteForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.reset_complete', 'reset_complete', $options = array('control' => 'jform')); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the password reset confirm form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form A Form object on success, false on failure - * - * @since 1.6 - * @throws \Exception - */ - public function getResetConfirmForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.reset_confirm', 'reset_confirm', $options = array('control' => 'jform')); - - if (empty($form)) - { - return false; - } - else - { - $form->setValue('token', '', Factory::getApplication()->input->get('token')); - } - - return $form; - } - - /** - * Override preprocessForm to load the user plugin group instead of content. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @throws \Exception if there is an error in the form event. - * - * @since 1.6 - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_users'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Save the new password after reset is done - * - * @param array $data The data expected for the form. - * - * @return mixed \Exception | boolean - * - * @since 1.6 - * @throws \Exception - */ - public function processResetComplete($data) - { - // Get the form. - $form = $this->getResetCompleteForm(); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - // Get the token and user id from the confirmation process. - $app = Factory::getApplication(); - $token = $app->getUserState('com_users.reset.token', null); - $userId = $app->getUserState('com_users.reset.user', null); - - // Check the token and user id. - if (empty($token) || empty($userId)) - { - return new \Exception(Text::_('COM_USERS_RESET_COMPLETE_TOKENS_MISSING'), 403); - } - - // Get the user object. - $user = User::getInstance($userId); - - // Check for a user and that the tokens match. - if (empty($user) || $user->activation !== $token) - { - $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); - - return false; - } - - // Make sure the user isn't blocked. - if ($user->block) - { - $this->setError(Text::_('COM_USERS_USER_BLOCKED')); - - return false; - } - - // Check if the user is reusing the current password if required to reset their password - if ($user->requireReset == 1 && UserHelper::verifyPassword($data['password1'], $user->password)) - { - $this->setError(Text::_('JLIB_USER_ERROR_CANNOT_REUSE_PASSWORD')); - - return false; - } - - // Prepare user data. - $data['password'] = $data['password1']; - $data['activation'] = ''; - - // Update the user object. - if (!$user->bind($data)) - { - return new \Exception($user->getError(), 500); - } - - // Save the user to the database. - if (!$user->save(true)) - { - return new \Exception(Text::sprintf('COM_USERS_USER_SAVE_FAILED', $user->getError()), 500); - } - - // Destroy all active sessions for the user - UserHelper::destroyUserSessions($user->id); - - // Flush the user data from the session. - $app->setUserState('com_users.reset.token', null); - $app->setUserState('com_users.reset.user', null); - - return true; - } - - /** - * Receive the reset password request - * - * @param array $data The data expected for the form. - * - * @return mixed \Exception | boolean - * - * @since 1.6 - * @throws \Exception - */ - public function processResetConfirm($data) - { - // Get the form. - $form = $this->getResetConfirmForm(); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - // Find the user id for the given token. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['activation', 'id', 'block'])) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('username') . ' = :username') - ->bind(':username', $data['username']); - - // Get the user id. - $db->setQuery($query); - - try - { - $user = $db->loadObject(); - } - catch (\RuntimeException $e) - { - return new \Exception(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage()), 500); - } - - // Check for a user. - if (empty($user)) - { - $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); - - return false; - } - - if (!$user->activation) - { - $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); - - return false; - } - - // Verify the token - if (!UserHelper::verifyPassword($data['token'], $user->activation)) - { - $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); - - return false; - } - - // Make sure the user isn't blocked. - if ($user->block) - { - $this->setError(Text::_('COM_USERS_USER_BLOCKED')); - - return false; - } - - // Push the user data into the session. - $app = Factory::getApplication(); - $app->setUserState('com_users.reset.token', $user->activation); - $app->setUserState('com_users.reset.user', $user->id); - - return true; - } - - /** - * Method to start the password reset process. - * - * @param array $data The data expected for the form. - * - * @return mixed \Exception | boolean - * - * @since 1.6 - * @throws \Exception - */ - public function processResetRequest($data) - { - $app = Factory::getApplication(); - - // Get the form. - $form = $this->getForm(); - - $data['email'] = PunycodeHelper::emailToPunycode($data['email']); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - // Find the user id for the given email address. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $data['email']); - - // Get the user object. - $db->setQuery($query); - - try - { - $userId = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - - // Check for a user. - if (empty($userId)) - { - $this->setError(Text::_('COM_USERS_INVALID_EMAIL')); - - return false; - } - - // Get the user object. - $user = User::getInstance($userId); - - // Make sure the user isn't blocked. - if ($user->block) - { - $this->setError(Text::_('COM_USERS_USER_BLOCKED')); - - return false; - } - - // Make sure the user isn't a Super Admin. - if ($user->authorise('core.admin')) - { - $this->setError(Text::_('COM_USERS_REMIND_SUPERADMIN_ERROR')); - - return false; - } - - // Make sure the user has not exceeded the reset limit - if (!$this->checkResetLimit($user)) - { - $resetLimit = (int) Factory::getApplication()->getParams()->get('reset_time'); - $this->setError(Text::plural('COM_USERS_REMIND_LIMIT_ERROR_N_HOURS', $resetLimit)); - - return false; - } - - // Set the confirmation token. - $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); - $hashedToken = UserHelper::hashPassword($token); - - $user->activation = $hashedToken; - - // Save the user to the database. - if (!$user->save(true)) - { - return new \Exception(Text::sprintf('COM_USERS_USER_SAVE_FAILED', $user->getError()), 500); - } - - // Assemble the password reset confirmation link. - $mode = $app->get('force_ssl', 0) == 2 ? 1 : (-1); - $link = 'index.php?option=com_users&view=reset&layout=confirm&token=' . $token; - - // Put together the email template data. - $data = $user->getProperties(); - $data['sitename'] = $app->get('sitename'); - $data['link_text'] = Route::_($link, false, $mode); - $data['link_html'] = Route::_($link, true, $mode); - $data['token'] = $token; - - $mailer = new MailTemplate('com_users.password_reset', $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($user->email, $user->name); - - // Try to send the password reset request email. - try - { - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $return = false; - } - } - - // Check for an error. - if ($return !== true) - { - return new \Exception(Text::_('COM_USERS_MAIL_FAILED'), 500); - } - else - { - return true; - } - } - - /** - * Method to check if user reset limit has been exceeded within the allowed time period. - * - * @param User $user User doing the password reset - * - * @return boolean true if user can do the reset, false if limit exceeded - * - * @since 2.5 - * @throws \Exception - */ - public function checkResetLimit($user) - { - $params = Factory::getApplication()->getParams(); - $maxCount = (int) $params->get('reset_count'); - $resetHours = (int) $params->get('reset_time'); - $result = true; - - $lastResetTime = strtotime($user->lastResetTime) ?: 0; - $hoursSinceLastReset = (strtotime(Factory::getDate()->toSql()) - $lastResetTime) / 3600; - - if ($hoursSinceLastReset > $resetHours) - { - // If it's been long enough, start a new reset count - $user->lastResetTime = Factory::getDate()->toSql(); - $user->resetCount = 1; - } - elseif ($user->resetCount < $maxCount) - { - // If we are under the max count, just increment the counter - ++$user->resetCount; - } - else - { - // At this point, we know we have exceeded the maximum resets for the time period - $result = false; - } - - return $result; - } + /** + * Method to get the password reset request form. + * + * The base form is loaded from XML and then an event is fired + * for users plugins to extend the form with extra fields. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.reset_request', 'reset_request', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the password reset complete form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getResetCompleteForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.reset_complete', 'reset_complete', $options = array('control' => 'jform')); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the password reset confirm form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + * @throws \Exception + */ + public function getResetConfirmForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.reset_confirm', 'reset_confirm', $options = array('control' => 'jform')); + + if (empty($form)) { + return false; + } else { + $form->setValue('token', '', Factory::getApplication()->input->get('token')); + } + + return $form; + } + + /** + * Override preprocessForm to load the user plugin group instead of content. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @throws \Exception if there is an error in the form event. + * + * @since 1.6 + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_users'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Save the new password after reset is done + * + * @param array $data The data expected for the form. + * + * @return mixed \Exception | boolean + * + * @since 1.6 + * @throws \Exception + */ + public function processResetComplete($data) + { + // Get the form. + $form = $this->getResetCompleteForm(); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + // Get the token and user id from the confirmation process. + $app = Factory::getApplication(); + $token = $app->getUserState('com_users.reset.token', null); + $userId = $app->getUserState('com_users.reset.user', null); + + // Check the token and user id. + if (empty($token) || empty($userId)) { + return new \Exception(Text::_('COM_USERS_RESET_COMPLETE_TOKENS_MISSING'), 403); + } + + // Get the user object. + $user = User::getInstance($userId); + + // Check for a user and that the tokens match. + if (empty($user) || $user->activation !== $token) { + $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); + + return false; + } + + // Make sure the user isn't blocked. + if ($user->block) { + $this->setError(Text::_('COM_USERS_USER_BLOCKED')); + + return false; + } + + // Check if the user is reusing the current password if required to reset their password + if ($user->requireReset == 1 && UserHelper::verifyPassword($data['password1'], $user->password)) { + $this->setError(Text::_('JLIB_USER_ERROR_CANNOT_REUSE_PASSWORD')); + + return false; + } + + // Prepare user data. + $data['password'] = $data['password1']; + $data['activation'] = ''; + + // Update the user object. + if (!$user->bind($data)) { + return new \Exception($user->getError(), 500); + } + + // Save the user to the database. + if (!$user->save(true)) { + return new \Exception(Text::sprintf('COM_USERS_USER_SAVE_FAILED', $user->getError()), 500); + } + + // Destroy all active sessions for the user + UserHelper::destroyUserSessions($user->id); + + // Flush the user data from the session. + $app->setUserState('com_users.reset.token', null); + $app->setUserState('com_users.reset.user', null); + + return true; + } + + /** + * Receive the reset password request + * + * @param array $data The data expected for the form. + * + * @return mixed \Exception | boolean + * + * @since 1.6 + * @throws \Exception + */ + public function processResetConfirm($data) + { + // Get the form. + $form = $this->getResetConfirmForm(); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + // Find the user id for the given token. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['activation', 'id', 'block'])) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('username') . ' = :username') + ->bind(':username', $data['username']); + + // Get the user id. + $db->setQuery($query); + + try { + $user = $db->loadObject(); + } catch (\RuntimeException $e) { + return new \Exception(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage()), 500); + } + + // Check for a user. + if (empty($user)) { + $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); + + return false; + } + + if (!$user->activation) { + $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); + + return false; + } + + // Verify the token + if (!UserHelper::verifyPassword($data['token'], $user->activation)) { + $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); + + return false; + } + + // Make sure the user isn't blocked. + if ($user->block) { + $this->setError(Text::_('COM_USERS_USER_BLOCKED')); + + return false; + } + + // Push the user data into the session. + $app = Factory::getApplication(); + $app->setUserState('com_users.reset.token', $user->activation); + $app->setUserState('com_users.reset.user', $user->id); + + return true; + } + + /** + * Method to start the password reset process. + * + * @param array $data The data expected for the form. + * + * @return mixed \Exception | boolean + * + * @since 1.6 + * @throws \Exception + */ + public function processResetRequest($data) + { + $app = Factory::getApplication(); + + // Get the form. + $form = $this->getForm(); + + $data['email'] = PunycodeHelper::emailToPunycode($data['email']); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + // Find the user id for the given email address. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $data['email']); + + // Get the user object. + $db->setQuery($query); + + try { + $userId = $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + + // Check for a user. + if (empty($userId)) { + $this->setError(Text::_('COM_USERS_INVALID_EMAIL')); + + return false; + } + + // Get the user object. + $user = User::getInstance($userId); + + // Make sure the user isn't blocked. + if ($user->block) { + $this->setError(Text::_('COM_USERS_USER_BLOCKED')); + + return false; + } + + // Make sure the user isn't a Super Admin. + if ($user->authorise('core.admin')) { + $this->setError(Text::_('COM_USERS_REMIND_SUPERADMIN_ERROR')); + + return false; + } + + // Make sure the user has not exceeded the reset limit + if (!$this->checkResetLimit($user)) { + $resetLimit = (int) Factory::getApplication()->getParams()->get('reset_time'); + $this->setError(Text::plural('COM_USERS_REMIND_LIMIT_ERROR_N_HOURS', $resetLimit)); + + return false; + } + + // Set the confirmation token. + $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); + $hashedToken = UserHelper::hashPassword($token); + + $user->activation = $hashedToken; + + // Save the user to the database. + if (!$user->save(true)) { + return new \Exception(Text::sprintf('COM_USERS_USER_SAVE_FAILED', $user->getError()), 500); + } + + // Assemble the password reset confirmation link. + $mode = $app->get('force_ssl', 0) == 2 ? 1 : (-1); + $link = 'index.php?option=com_users&view=reset&layout=confirm&token=' . $token; + + // Put together the email template data. + $data = $user->getProperties(); + $data['sitename'] = $app->get('sitename'); + $data['link_text'] = Route::_($link, false, $mode); + $data['link_html'] = Route::_($link, true, $mode); + $data['token'] = $token; + + $mailer = new MailTemplate('com_users.password_reset', $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($user->email, $user->name); + + // Try to send the password reset request email. + try { + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $return = false; + } + } + + // Check for an error. + if ($return !== true) { + return new \Exception(Text::_('COM_USERS_MAIL_FAILED'), 500); + } else { + return true; + } + } + + /** + * Method to check if user reset limit has been exceeded within the allowed time period. + * + * @param User $user User doing the password reset + * + * @return boolean true if user can do the reset, false if limit exceeded + * + * @since 2.5 + * @throws \Exception + */ + public function checkResetLimit($user) + { + $params = Factory::getApplication()->getParams(); + $maxCount = (int) $params->get('reset_count'); + $resetHours = (int) $params->get('reset_time'); + $result = true; + + $lastResetTime = strtotime($user->lastResetTime) ?: 0; + $hoursSinceLastReset = (strtotime(Factory::getDate()->toSql()) - $lastResetTime) / 3600; + + if ($hoursSinceLastReset > $resetHours) { + // If it's been long enough, start a new reset count + $user->lastResetTime = Factory::getDate()->toSql(); + $user->resetCount = 1; + } elseif ($user->resetCount < $maxCount) { + // If we are under the max count, just increment the counter + ++$user->resetCount; + } else { + // At this point, we know we have exceeded the maximum resets for the time period + $result = false; + } + + return $result; + } } diff --git a/components/com_users/src/Rule/LoginUniqueFieldRule.php b/components/com_users/src/Rule/LoginUniqueFieldRule.php index 04310e7dc23ab..d42d874012d9c 100644 --- a/components/com_users/src/Rule/LoginUniqueFieldRule.php +++ b/components/com_users/src/Rule/LoginUniqueFieldRule.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise. - * - * @since 3.6 - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - $loginRedirectUrl = $input['params']->login_redirect_url; - $loginRedirectMenuitem = $input['params']->login_redirect_menuitem; + /** + * Method to test if two fields have a value in order to use only one field. + * To use this rule, the form + * XML needs a validate attribute of loginuniquefield and a field attribute + * that is equal to the field to test against. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise. + * + * @since 3.6 + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + $loginRedirectUrl = $input['params']->login_redirect_url; + $loginRedirectMenuitem = $input['params']->login_redirect_menuitem; - if ($form === null) - { - throw new \InvalidArgumentException(sprintf('The value for $form must not be null in %s', get_class($this))); - } + if ($form === null) { + throw new \InvalidArgumentException(sprintf('The value for $form must not be null in %s', get_class($this))); + } - if ($input === null) - { - throw new \InvalidArgumentException(sprintf('The value for $input must not be null in %s', get_class($this))); - } + if ($input === null) { + throw new \InvalidArgumentException(sprintf('The value for $input must not be null in %s', get_class($this))); + } - // Test the input values for login. - if ($loginRedirectUrl != '' && $loginRedirectMenuitem != '') - { - return false; - } + // Test the input values for login. + if ($loginRedirectUrl != '' && $loginRedirectMenuitem != '') { + return false; + } - return true; - } + return true; + } } diff --git a/components/com_users/src/Rule/LogoutUniqueFieldRule.php b/components/com_users/src/Rule/LogoutUniqueFieldRule.php index 89dd4eaf95fd7..d2e8d09c88f6a 100644 --- a/components/com_users/src/Rule/LogoutUniqueFieldRule.php +++ b/components/com_users/src/Rule/LogoutUniqueFieldRule.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise. - * - * @since 3.6 - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - $logoutRedirectUrl = $input['params']->logout_redirect_url; - $logoutRedirectMenuitem = $input['params']->logout_redirect_menuitem; + /** + * Method to test if two fields have a value in order to use only one field. + * To use this rule, the form + * XML needs a validate attribute of logoutuniquefield and a field attribute + * that is equal to the field to test against. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise. + * + * @since 3.6 + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + $logoutRedirectUrl = $input['params']->logout_redirect_url; + $logoutRedirectMenuitem = $input['params']->logout_redirect_menuitem; - if ($form === null) - { - throw new \InvalidArgumentException(sprintf('The value for $form must not be null in %s', get_class($this))); - } + if ($form === null) { + throw new \InvalidArgumentException(sprintf('The value for $form must not be null in %s', get_class($this))); + } - if ($input === null) - { - throw new \InvalidArgumentException(sprintf('The value for $input must not be null in %s', get_class($this))); - } + if ($input === null) { + throw new \InvalidArgumentException(sprintf('The value for $input must not be null in %s', get_class($this))); + } - // Test the input values for logout. - if ($logoutRedirectUrl != '' && $logoutRedirectMenuitem != '') - { - return false; - } + // Test the input values for logout. + if ($logoutRedirectUrl != '' && $logoutRedirectMenuitem != '') { + return false; + } - return true; - } + return true; + } } diff --git a/components/com_users/src/Service/Router.php b/components/com_users/src/Service/Router.php index 43a85cf4ebe86..33d0fd3e40b28 100644 --- a/components/com_users/src/Service/Router.php +++ b/components/com_users/src/Service/Router.php @@ -1,4 +1,5 @@ registerView(new RouterViewConfiguration('login')); - $profile = new RouterViewConfiguration('profile'); - $profile->addLayout('edit'); - $this->registerView($profile); - $this->registerView(new RouterViewConfiguration('registration')); - $this->registerView(new RouterViewConfiguration('remind')); - $this->registerView(new RouterViewConfiguration('reset')); - $this->registerView(new RouterViewConfiguration('callback')); - $this->registerView(new RouterViewConfiguration('captive')); - $this->registerView(new RouterViewConfiguration('methods')); + /** + * Users Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + */ + public function __construct(SiteApplication $app, AbstractMenu $menu) + { + $this->registerView(new RouterViewConfiguration('login')); + $profile = new RouterViewConfiguration('profile'); + $profile->addLayout('edit'); + $this->registerView($profile); + $this->registerView(new RouterViewConfiguration('registration')); + $this->registerView(new RouterViewConfiguration('remind')); + $this->registerView(new RouterViewConfiguration('reset')); + $this->registerView(new RouterViewConfiguration('callback')); + $this->registerView(new RouterViewConfiguration('captive')); + $this->registerView(new RouterViewConfiguration('methods')); - $method = new RouterViewConfiguration('method'); - $method->setKey('id'); - $this->registerView($method); + $method = new RouterViewConfiguration('method'); + $method->setKey('id'); + $this->registerView($method); - parent::__construct($app, $menu); + parent::__construct($app, $menu); - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } - /** - * Get the method ID from a URL segment - * - * @param string $segment The URL segment - * @param array $query The URL query parameters - * - * @return integer - * @since 4.2.0 - */ - public function getMethodId($segment, $query) - { - return (int) $segment; - } + /** + * Get the method ID from a URL segment + * + * @param string $segment The URL segment + * @param array $query The URL query parameters + * + * @return integer + * @since 4.2.0 + */ + public function getMethodId($segment, $query) + { + return (int) $segment; + } - /** - * Get a segment from a method ID - * - * @param integer $id The method ID - * @param array $query The URL query parameters - * - * @return int[] - * @since 4.2.0 - */ - public function getMethodSegment($id, $query) - { - return [$id => (int) $id]; - } + /** + * Get a segment from a method ID + * + * @param integer $id The method ID + * @param array $query The URL query parameters + * + * @return int[] + * @since 4.2.0 + */ + public function getMethodSegment($id, $query) + { + return [$id => (int) $id]; + } } diff --git a/components/com_users/src/View/Captive/HtmlView.php b/components/com_users/src/View/Captive/HtmlView.php index d43d325412bf1..d6caf754d4c35 100644 --- a/components/com_users/src/View/Captive/HtmlView.php +++ b/components/com_users/src/View/Captive/HtmlView.php @@ -1,4 +1,5 @@ user = $this->getCurrentUser(); - $this->form = $this->get('Form'); - $this->state = $this->get('State'); - $this->params = $this->state->get('params'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check for layout override - $active = Factory::getApplication()->getMenu()->getActive(); - - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - - $this->extraButtons = AuthenticationHelper::getLoginButtons('com-users-login__form'); - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function prepareDocument() - { - $login = $this->getCurrentUser()->get('guest') ? true : false; - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', $login ? Text::_('JLOGIN') : Text::_('JLOGOUT')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The logged in user + * + * @var User + */ + protected $user; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * No longer used + * + * @var boolean + * @since 4.0.0 + * @deprecated 4.2.0 Will be removed in 5.0. + */ + protected $tfa = false; + + /** + * Additional buttons to show on the login page + * + * @var array + * @since 4.0.0 + */ + protected $extraButtons = []; + + /** + * Method to display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.5 + * @throws \Exception + */ + public function display($tpl = null) + { + // Get the view data. + $this->user = $this->getCurrentUser(); + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->params = $this->state->get('params'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check for layout override + $active = Factory::getApplication()->getMenu()->getActive(); + + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + + $this->extraButtons = AuthenticationHelper::getLoginButtons('com-users-login__form'); + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function prepareDocument() + { + $login = $this->getCurrentUser()->get('guest') ? true : false; + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', $login ? Text::_('JLOGIN') : Text::_('JLOGOUT')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_users/src/View/Method/HtmlView.php b/components/com_users/src/View/Method/HtmlView.php index c5e837fb01bf6..e1722c59dd406 100644 --- a/components/com_users/src/View/Method/HtmlView.php +++ b/components/com_users/src/View/Method/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser(); - - // Get the view data. - $this->data = $this->get('Data'); - $this->form = $this->getModel()->getForm(new CMSObject(['id' => $user->id])); - $this->state = $this->get('State'); - $this->params = $this->state->get('params'); - $this->mfaConfigurationUI = Mfa::getConfigurationInterface($user); - $this->db = Factory::getDbo(); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // View also takes responsibility for checking if the user logged in with remember me. - $cookieLogin = $user->get('cookieLogin'); - - if (!empty($cookieLogin)) - { - // If so, the user must login to edit the password and other data. - // What should happen here? Should we force a logout which destroys the cookies? - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('JGLOBAL_REMEMBER_MUST_LOGIN'), 'message'); - $app->redirect(Route::_('index.php?option=com_users&view=login', false)); - - return false; - } - - // Check if a user was found. - if (!$this->data->id) - { - throw new \Exception(Text::_('JERROR_USERS_PROFILE_NOT_FOUND'), 404); - } - - PluginHelper::importPlugin('content'); - $this->data->text = ''; - Factory::getApplication()->triggerEvent('onContentPrepare', array ('com_users.user', &$this->data, &$this->data->params, 0)); - unset($this->data->text); - - // Check for layout from menu item. - $query = Factory::getApplication()->getMenu()->getActive()->query; - - if (isset($query['layout']) && isset($query['option']) && $query['option'] === 'com_users' - && isset($query['view']) && $query['view'] === 'profile') - { - $this->setLayout($query['layout']); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $this->getCurrentUser()->name)); - } - else - { - $this->params->def('page_heading', Text::_('COM_USERS_PROFILE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * Profile form data for the user + * + * @var User + */ + protected $data; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * An instance of DatabaseDriver. + * + * @var DatabaseDriver + * @since 3.6.3 + * + * @deprecated 5.0 Will be removed without replacement + */ + protected $db; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The Multi-factor Authentication configuration interface for the user. + * + * @var string|null + * @since 4.2.0 + */ + protected $mfaConfigurationUI; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void|boolean + * + * @since 1.6 + * @throws \Exception + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + + // Get the view data. + $this->data = $this->get('Data'); + $this->form = $this->getModel()->getForm(new CMSObject(['id' => $user->id])); + $this->state = $this->get('State'); + $this->params = $this->state->get('params'); + $this->mfaConfigurationUI = Mfa::getConfigurationInterface($user); + $this->db = Factory::getDbo(); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // View also takes responsibility for checking if the user logged in with remember me. + $cookieLogin = $user->get('cookieLogin'); + + if (!empty($cookieLogin)) { + // If so, the user must login to edit the password and other data. + // What should happen here? Should we force a logout which destroys the cookies? + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('JGLOBAL_REMEMBER_MUST_LOGIN'), 'message'); + $app->redirect(Route::_('index.php?option=com_users&view=login', false)); + + return false; + } + + // Check if a user was found. + if (!$this->data->id) { + throw new \Exception(Text::_('JERROR_USERS_PROFILE_NOT_FOUND'), 404); + } + + PluginHelper::importPlugin('content'); + $this->data->text = ''; + Factory::getApplication()->triggerEvent('onContentPrepare', array ('com_users.user', &$this->data, &$this->data->params, 0)); + unset($this->data->text); + + // Check for layout from menu item. + $query = Factory::getApplication()->getMenu()->getActive()->query; + + if ( + isset($query['layout']) && isset($query['option']) && $query['option'] === 'com_users' + && isset($query['view']) && $query['view'] === 'profile' + ) { + $this->setLayout($query['layout']); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $this->getCurrentUser()->name)); + } else { + $this->params->def('page_heading', Text::_('COM_USERS_PROFILE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_users/src/View/Registration/HtmlView.php b/components/com_users/src/View/Registration/HtmlView.php index 98741cff3f6ab..217a8b7a96bcb 100644 --- a/components/com_users/src/View/Registration/HtmlView.php +++ b/components/com_users/src/View/Registration/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->data = $this->get('Data'); - $this->state = $this->get('State'); - $this->params = $this->state->get('params'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check for layout override - $active = Factory::getApplication()->getMenu()->getActive(); - - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_USERS_REGISTRATION')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * Registration form data + * + * @var \stdClass|false + */ + protected $data; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The HtmlDocument instance + * + * @var HtmlDocument + */ + public $document; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * Method to display the view. + * + * @param string $tpl The template file to include + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + public function display($tpl = null) + { + // Get the view data. + $this->form = $this->get('Form'); + $this->data = $this->get('Data'); + $this->state = $this->get('State'); + $this->params = $this->state->get('params'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check for layout override + $active = Factory::getApplication()->getMenu()->getActive(); + + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_USERS_REGISTRATION')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_users/src/View/Remind/HtmlView.php b/components/com_users/src/View/Remind/HtmlView.php index 8855d9d1c80e9..ff33c908f1201 100644 --- a/components/com_users/src/View/Remind/HtmlView.php +++ b/components/com_users/src/View/Remind/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->state = $this->get('State'); - $this->params = $this->state->params; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check for layout override - $active = Factory::getApplication()->getMenu()->getActive(); - - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_USERS_REMIND')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * Method to display the view. + * + * @param string $tpl The template file to include + * + * @return mixed + * + * @since 1.5 + * @throws \Exception + */ + public function display($tpl = null) + { + // Get the view data. + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->params = $this->state->params; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check for layout override + $active = Factory::getApplication()->getMenu()->getActive(); + + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_USERS_REMIND')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_users/src/View/Reset/HtmlView.php b/components/com_users/src/View/Reset/HtmlView.php index c5266911f93ed..83b985dee3134 100644 --- a/components/com_users/src/View/Reset/HtmlView.php +++ b/components/com_users/src/View/Reset/HtmlView.php @@ -1,4 +1,5 @@ getLayout(); - - // Check that the name is valid - has an associated model. - if (!in_array($name, array('confirm', 'complete'))) - { - $name = 'default'; - } - - if ('default' === $name) - { - $formname = 'Form'; - } - else - { - $formname = ucfirst($this->_name) . ucfirst($name) . 'Form'; - } - - // Get the view data. - $this->form = $this->get($formname); - $this->state = $this->get('State'); - $this->params = $this->state->params; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_USERS_RESET')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * Method to display the view. + * + * @param string $tpl The template file to include + * + * @return mixed + * + * @since 1.5 + */ + public function display($tpl = null) + { + // This name will be used to get the model + $name = $this->getLayout(); + + // Check that the name is valid - has an associated model. + if (!in_array($name, array('confirm', 'complete'))) { + $name = 'default'; + } + + if ('default' === $name) { + $formname = 'Form'; + } else { + $formname = ucfirst($this->_name) . ucfirst($name) . 'Form'; + } + + // Get the view data. + $this->form = $this->get($formname); + $this->state = $this->get('State'); + $this->params = $this->state->params; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_USERS_RESET')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_users/tmpl/login/default.php b/components/com_users/tmpl/login/default.php index a40eba9805255..0fbcc008e8855 100644 --- a/components/com_users/tmpl/login/default.php +++ b/components/com_users/tmpl/login/default.php @@ -1,4 +1,5 @@ user->get('cookieLogin'); -if (!empty($cookieLogin) || $this->user->get('guest')) -{ - // The user is not logged in or needs to provide a password. - echo $this->loadTemplate('login'); -} -else -{ - // The user is already logged in. - echo $this->loadTemplate('logout'); +if (!empty($cookieLogin) || $this->user->get('guest')) { + // The user is not logged in or needs to provide a password. + echo $this->loadTemplate('login'); +} else { + // The user is already logged in. + echo $this->loadTemplate('logout'); } diff --git a/components/com_users/tmpl/login/default_login.php b/components/com_users/tmpl/login/default_login.php index 8219bafdea841..ec60bda63bee5 100644 --- a/components/com_users/tmpl/login/default_login.php +++ b/components/com_users/tmpl/login/default_login.php @@ -1,4 +1,5 @@ diff --git a/components/com_users/tmpl/login/default_logout.php b/components/com_users/tmpl/login/default_logout.php index 0f199854dd435..099ab56f5cb1e 100644 --- a/components/com_users/tmpl/login/default_logout.php +++ b/components/com_users/tmpl/login/default_logout.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - - - params->get('logoutdescription_show') == 1 && str_replace(' ', '', $this->params->get('logout_description')) != '')|| $this->params->get('logout_image') != '') : ?> -
    - - - params->get('logoutdescription_show') == 1) : ?> - params->get('logout_description'); ?> - - - params->get('logout_image') != '') : ?> - params->get('logout_image'), empty($this->params->get('logout_image_alt')) && empty($this->params->get('logout_image_alt_empty')) ? false : $this->params->get('logout_image_alt'), ['class' => 'com-users-logout__image thumbnail float-end logout-image']); ?> - - - params->get('logoutdescription_show') == 1 && str_replace(' ', '', $this->params->get('logout_description')) != '')|| $this->params->get('logout_image') != '') : ?> -
    - - -
    -
    -
    - -
    -
    - params->get('logout_redirect_url')) : ?> - - - - - -
    + params->get('show_page_heading')) : ?> + + + + params->get('logoutdescription_show') == 1 && str_replace(' ', '', $this->params->get('logout_description')) != '') || $this->params->get('logout_image') != '') : ?> +
    + + + params->get('logoutdescription_show') == 1) : ?> + params->get('logout_description'); ?> + + + params->get('logout_image') != '') : ?> + params->get('logout_image'), empty($this->params->get('logout_image_alt')) && empty($this->params->get('logout_image_alt_empty')) ? false : $this->params->get('logout_image_alt'), ['class' => 'com-users-logout__image thumbnail float-end logout-image']); ?> + + + params->get('logoutdescription_show') == 1 && str_replace(' ', '', $this->params->get('logout_description')) != '') || $this->params->get('logout_image') != '') : ?> +
    + + +
    +
    +
    + +
    +
    + params->get('logout_redirect_url')) : ?> + + + + + +
    diff --git a/components/com_users/tmpl/profile/default.php b/components/com_users/tmpl/profile/default.php index e6b2af7c92a82..a534e5b458e82 100644 --- a/components/com_users/tmpl/profile/default.php +++ b/components/com_users/tmpl/profile/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - + params->get('show_page_heading')) : ?> + + - id == $this->data->id) : ?> - - + id == $this->data->id) : ?> + + - loadTemplate('core'); ?> - loadTemplate('params'); ?> - loadTemplate('custom'); ?> + loadTemplate('core'); ?> + loadTemplate('params'); ?> + loadTemplate('custom'); ?>
    diff --git a/components/com_users/tmpl/profile/default_core.php b/components/com_users/tmpl/profile/default_core.php index 01d085e257bf1..066094aaf0db3 100644 --- a/components/com_users/tmpl/profile/default_core.php +++ b/components/com_users/tmpl/profile/default_core.php @@ -1,4 +1,5 @@
    - - - -
    -
    - -
    -
    - escape($this->data->name); ?> -
    -
    - -
    -
    - escape($this->data->username); ?> -
    -
    - -
    -
    - data->registerDate, Text::_('DATE_FORMAT_LC1')); ?> -
    -
    - -
    - data->lastvisitDate !== null) : ?> -
    - data->lastvisitDate, Text::_('DATE_FORMAT_LC1')); ?> -
    - -
    - -
    - -
    + + + +
    +
    + +
    +
    + escape($this->data->name); ?> +
    +
    + +
    +
    + escape($this->data->username); ?> +
    +
    + +
    +
    + data->registerDate, Text::_('DATE_FORMAT_LC1')); ?> +
    +
    + +
    + data->lastvisitDate !== null) : ?> +
    + data->lastvisitDate, Text::_('DATE_FORMAT_LC1')); ?> +
    + +
    + +
    + +
    diff --git a/components/com_users/tmpl/profile/default_custom.php b/components/com_users/tmpl/profile/default_custom.php index 5106b09f5ef2f..6bf103ecd34b1 100644 --- a/components/com_users/tmpl/profile/default_custom.php +++ b/components/com_users/tmpl/profile/default_custom.php @@ -1,4 +1,5 @@ form->getFieldsets(); -if (isset($fieldsets['core'])) -{ - unset($fieldsets['core']); +if (isset($fieldsets['core'])) { + unset($fieldsets['core']); } -if (isset($fieldsets['params'])) -{ - unset($fieldsets['params']); +if (isset($fieldsets['params'])) { + unset($fieldsets['params']); } $tmp = $this->data->jcfields ?? array(); $customFields = array(); -foreach ($tmp as $customField) -{ - $customFields[$customField->name] = $customField; +foreach ($tmp as $customField) { + $customFields[$customField->name] = $customField; } ?> $fieldset) : ?> - form->getFieldset($group); ?> - -
    - label) && ($legend = trim(Text::_($fieldset->label))) !== '') : ?> - - - description) && trim($fieldset->description)) : ?> -

    escape(Text::_($fieldset->description)); ?>

    - -
    - - hidden && $field->type !== 'Spacer') : ?> -
    - title; ?> -
    -
    - fieldname, $customFields)) : ?> - fieldname]->value) ? $customFields[$field->fieldname]->value : Text::_('COM_USERS_PROFILE_VALUE_NOT_FOUND'); ?> - id)) : ?> - id, $field->value); ?> - fieldname)) : ?> - fieldname, $field->value); ?> - type)) : ?> - type, $field->value); ?> - - value); ?> - -
    - - -
    -
    - + form->getFieldset($group); ?> + +
    + label) && ($legend = trim(Text::_($fieldset->label))) !== '') : ?> + + + description) && trim($fieldset->description)) : ?> +

    escape(Text::_($fieldset->description)); ?>

    + +
    + + hidden && $field->type !== 'Spacer') : ?> +
    + title; ?> +
    +
    + fieldname, $customFields)) : ?> + fieldname]->value) ? $customFields[$field->fieldname]->value : Text::_('COM_USERS_PROFILE_VALUE_NOT_FOUND'); ?> + id)) : ?> + id, $field->value); ?> + fieldname)) : ?> + fieldname, $field->value); ?> + type)) : ?> + type, $field->value); ?> + + value); ?> + +
    + + +
    +
    + diff --git a/components/com_users/tmpl/profile/default_params.php b/components/com_users/tmpl/profile/default_params.php index 80aa06bffe944..f7906946fdfd0 100644 --- a/components/com_users/tmpl/profile/default_params.php +++ b/components/com_users/tmpl/profile/default_params.php @@ -1,4 +1,5 @@ form->getFieldset('params'); ?> -
    - -
    - - hidden) : ?> -
    - title; ?> -
    -
    - id)) : ?> - id, $field->value); ?> - fieldname)) : ?> - fieldname, $field->value); ?> - type)) : ?> - type, $field->value); ?> - - value); ?> - -
    - - -
    -
    +
    + +
    + + hidden) : ?> +
    + title; ?> +
    +
    + id)) : ?> + id, $field->value); ?> + fieldname)) : ?> + fieldname, $field->value); ?> + type)) : ?> + type, $field->value); ?> + + value); ?> + +
    + + +
    +
    diff --git a/components/com_users/tmpl/profile/edit.php b/components/com_users/tmpl/profile/edit.php index e0a9abcb0a651..357a4f52d426f 100644 --- a/components/com_users/tmpl/profile/edit.php +++ b/components/com_users/tmpl/profile/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
    - params->get('show_page_heading')) : ?> - - + params->get('show_page_heading')) : ?> + + -
    - - form->getFieldsets() as $group => $fieldset) : ?> - form->getFieldset($group); ?> - -
    - - label)) : ?> - - label); ?> - - - description) && trim($fieldset->description)) : ?> -

    - escape(Text::_($fieldset->description)); ?> -

    - - - - renderField(); ?> - -
    - - + + + form->getFieldsets() as $group => $fieldset) : ?> + form->getFieldset($group); ?> + +
    + + label)) : ?> + + label); ?> + + + description) && trim($fieldset->description)) : ?> +

    + escape(Text::_($fieldset->description)); ?> +

    + + + + renderField(); ?> + +
    + + - mfaConfigurationUI): ?> -
    - - mfaConfigurationUI ?> -
    - + mfaConfigurationUI) : ?> +
    + + mfaConfigurationUI ?> +
    + -
    -
    - - - -
    -
    - -
    +
    +
    + + + +
    +
    + +
    diff --git a/components/com_users/tmpl/registration/complete.php b/components/com_users/tmpl/registration/complete.php index 36ab26988cb59..15c5efc41667f 100644 --- a/components/com_users/tmpl/registration/complete.php +++ b/components/com_users/tmpl/registration/complete.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    +
    diff --git a/components/com_users/tmpl/registration/default.php b/components/com_users/tmpl/registration/default.php index e9b3093bc07eb..79fcbd67faa4f 100644 --- a/components/com_users/tmpl/registration/default.php +++ b/components/com_users/tmpl/registration/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - + params->get('show_page_heading')) : ?> + + -
    - - form->getFieldsets() as $fieldset) : ?> - form->getFieldset($fieldset->name); ?> - -
    - - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - - -
    -
    - - - -
    -
    - -
    +
    + + form->getFieldsets() as $fieldset) : ?> + form->getFieldset($fieldset->name); ?> + +
    + + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + + +
    +
    + + + +
    +
    + +
    diff --git a/components/com_users/tmpl/remind/default.php b/components/com_users/tmpl/remind/default.php index cff4f4dce489b..e1b0ecf11df72 100644 --- a/components/com_users/tmpl/remind/default.php +++ b/components/com_users/tmpl/remind/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/components/com_users/tmpl/reset/complete.php b/components/com_users/tmpl/reset/complete.php index 463c6b06131bf..9ca759f0645ac 100644 --- a/components/com_users/tmpl/reset/complete.php +++ b/components/com_users/tmpl/reset/complete.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/components/com_users/tmpl/reset/confirm.php b/components/com_users/tmpl/reset/confirm.php index b77b17bbb5611..c1bf30e65a7fc 100644 --- a/components/com_users/tmpl/reset/confirm.php +++ b/components/com_users/tmpl/reset/confirm.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/components/com_users/tmpl/reset/default.php b/components/com_users/tmpl/reset/default.php index d56e2181a0cce..4c8687ec76ea3 100644 --- a/components/com_users/tmpl/reset/default.php +++ b/components/com_users/tmpl/reset/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/components/com_wrapper/src/Controller/DisplayController.php b/components/com_wrapper/src/Controller/DisplayController.php index 2062abda7cc17..1e90c2e35dff9 100644 --- a/components/com_wrapper/src/Controller/DisplayController.php +++ b/components/com_wrapper/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'wrapper'); - $this->input->set('view', $vName); + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'wrapper'); + $this->input->set('view', $vName); - return parent::display($cachable, array('Itemid' => 'INT')); - } + return parent::display($cachable, array('Itemid' => 'INT')); + } } diff --git a/components/com_wrapper/src/Service/Router.php b/components/com_wrapper/src/Service/Router.php index 32b3c111cad2a..cfd64d702acd4 100644 --- a/components/com_wrapper/src/Service/Router.php +++ b/components/com_wrapper/src/Service/Router.php @@ -1,4 +1,5 @@ 'wrapper'); - } + /** + * Parse the segments of a URL. + * + * @param array $segments The segments of the URL to parse. + * + * @return array The URL attributes to be used by the application. + * + * @since 3.3 + */ + public function parse(&$segments) + { + return array('view' => 'wrapper'); + } } diff --git a/components/com_wrapper/src/View/Wrapper/HtmlView.php b/components/com_wrapper/src/View/Wrapper/HtmlView.php index 6e5b41fe5ab93..666a555874faf 100644 --- a/components/com_wrapper/src/View/Wrapper/HtmlView.php +++ b/components/com_wrapper/src/View/Wrapper/HtmlView.php @@ -1,4 +1,5 @@ getParams(); - - // Because the application sets a default page title, we need to get it - // right from the menu item itself - - $this->setDocumentTitle($params->get('page_title', '')); - - if ($params->get('menu-meta_description')) - { - $this->document->setDescription($params->get('menu-meta_description')); - } - - if ($params->get('robots')) - { - $this->document->setMetaData('robots', $params->get('robots')); - } - - $wrapper = new \stdClass; - - // Auto height control - if ($params->def('height_auto')) - { - $wrapper->load = 'onload="iFrameHeight(this)"'; - } - else - { - $wrapper->load = ''; - } - - $url = $params->def('url', ''); - - if ($params->def('add_scheme', 1)) - { - // Adds 'http://' or 'https://' if none is set - if (strpos($url, '//') === 0) - { - // URL without scheme in component. Prepend current scheme. - $wrapper->url = Uri::getInstance()->toString(array('scheme')) . substr($url, 2); - } - elseif (strpos($url, '/') === 0) - { - // Relative URL in component. Use scheme + host + port. - $wrapper->url = Uri::getInstance()->toString(array('scheme', 'host', 'port')) . $url; - } - elseif (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) - { - // URL doesn't start with either 'http://' or 'https://'. Add current scheme. - $wrapper->url = Uri::getInstance()->toString(array('scheme')) . $url; - } - else - { - // URL starts with either 'http://' or 'https://'. Do not change it. - $wrapper->url = $url; - } - } - else - { - $wrapper->url = $url; - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - $this->params = &$params; - $this->wrapper = &$wrapper; - - parent::display($tpl); - } + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * @since 4.0.0 + */ + protected $params = null; + + /** + * The page parameters + * + * @var \stdClass + * @since 4.0.0 + */ + protected $wrapper = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.5 + */ + public function display($tpl = null) + { + $params = Factory::getApplication()->getParams(); + + // Because the application sets a default page title, we need to get it + // right from the menu item itself + + $this->setDocumentTitle($params->get('page_title', '')); + + if ($params->get('menu-meta_description')) { + $this->document->setDescription($params->get('menu-meta_description')); + } + + if ($params->get('robots')) { + $this->document->setMetaData('robots', $params->get('robots')); + } + + $wrapper = new \stdClass(); + + // Auto height control + if ($params->def('height_auto')) { + $wrapper->load = 'onload="iFrameHeight(this)"'; + } else { + $wrapper->load = ''; + } + + $url = $params->def('url', ''); + + if ($params->def('add_scheme', 1)) { + // Adds 'http://' or 'https://' if none is set + if (strpos($url, '//') === 0) { + // URL without scheme in component. Prepend current scheme. + $wrapper->url = Uri::getInstance()->toString(array('scheme')) . substr($url, 2); + } elseif (strpos($url, '/') === 0) { + // Relative URL in component. Use scheme + host + port. + $wrapper->url = Uri::getInstance()->toString(array('scheme', 'host', 'port')) . $url; + } elseif (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) { + // URL doesn't start with either 'http://' or 'https://'. Add current scheme. + $wrapper->url = Uri::getInstance()->toString(array('scheme')) . $url; + } else { + // URL starts with either 'http://' or 'https://'. Do not change it. + $wrapper->url = $url; + } + } else { + $wrapper->url = $url; + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + $this->params = &$params; + $this->wrapper = &$wrapper; + + parent::display($tpl); + } } diff --git a/components/com_wrapper/tmpl/wrapper/default.php b/components/com_wrapper/tmpl/wrapper/default.php index 73fdd91bf0eb3..5e69c5bf06acf 100644 --- a/components/com_wrapper/tmpl/wrapper/default.php +++ b/components/com_wrapper/tmpl/wrapper/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager() - ->registerAndUseScript('com_wrapper.iframe', 'com_wrapper/iframe-height.min.js', [], ['defer' => true]); + ->registerAndUseScript('com_wrapper.iframe', 'com_wrapper/iframe-height.min.js', [], ['defer' => true]); ?>
    - params->get('show_page_heading')) : ?> - - - + params->get('show_page_heading')) : ?> + + +
    diff --git a/includes/app.php b/includes/app.php index 15fdbddf75e80..b50005044fb99 100644 --- a/includes/app.php +++ b/includes/app.php @@ -1,4 +1,5 @@ alias('session.web', 'session.web.site') - ->alias('session', 'session.web.site') - ->alias('JSession', 'session.web.site') - ->alias(\Joomla\CMS\Session\Session::class, 'session.web.site') - ->alias(\Joomla\Session\Session::class, 'session.web.site') - ->alias(\Joomla\Session\SessionInterface::class, 'session.web.site'); + ->alias('session', 'session.web.site') + ->alias('JSession', 'session.web.site') + ->alias(\Joomla\CMS\Session\Session::class, 'session.web.site') + ->alias(\Joomla\Session\Session::class, 'session.web.site') + ->alias(\Joomla\Session\SessionInterface::class, 'session.web.site'); // Instantiate the application. $app = $container->get(\Joomla\CMS\Application\SiteApplication::class); diff --git a/includes/defines.php b/includes/defines.php index 1b89ecbd003aa..a0d6d924b5516 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -1,4 +1,5 @@ isInDevelopmentState()))) -{ - if (file_exists(JPATH_INSTALLATION . '/index.php')) - { - header('Location: ' . substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], 'index.php')) . 'installation/index.php'); - - exit; - } - else - { - echo 'No configuration file found and no installation code available. Exiting...'; - - exit; - } +if ( + !file_exists(JPATH_CONFIGURATION . '/configuration.php') + || (filesize(JPATH_CONFIGURATION . '/configuration.php') < 10) + || (file_exists(JPATH_INSTALLATION . '/index.php') && (false === (new Version())->isInDevelopmentState())) +) { + if (file_exists(JPATH_INSTALLATION . '/index.php')) { + header('Location: ' . substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], 'index.php')) . 'installation/index.php'); + + exit; + } else { + echo 'No configuration file found and no installation code available. Exiting...'; + + exit; + } } // Pre-Load configuration. Don't remove the Output Buffering due to BOM issues, see JCode 26026 @@ -39,68 +38,61 @@ ob_end_clean(); // System configuration. -$config = new JConfig; +$config = new JConfig(); // Set the error_reporting, and adjust a global Error Handler -switch ($config->error_reporting) -{ - case 'default': - case '-1': - - break; +switch ($config->error_reporting) { + case 'default': + case '-1': + break; - case 'none': - case '0': - error_reporting(0); + case 'none': + case '0': + error_reporting(0); - break; + break; - case 'simple': - error_reporting(E_ERROR | E_WARNING | E_PARSE); - ini_set('display_errors', 1); + case 'simple': + error_reporting(E_ERROR | E_WARNING | E_PARSE); + ini_set('display_errors', 1); - break; + break; - case 'maximum': - case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 - error_reporting(E_ALL); - ini_set('display_errors', 1); + case 'maximum': + case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 + error_reporting(E_ALL); + ini_set('display_errors', 1); - break; + break; - default: - error_reporting($config->error_reporting); - ini_set('display_errors', 1); + default: + error_reporting($config->error_reporting); + ini_set('display_errors', 1); - break; + break; } -if (!defined('JDEBUG')) -{ - define('JDEBUG', $config->debug); +if (!defined('JDEBUG')) { + define('JDEBUG', $config->debug); } // Check deprecation logging -if (empty($config->log_deprecated)) -{ - // Reset handler for E_USER_DEPRECATED - set_error_handler(null, E_USER_DEPRECATED); -} -else -{ - // Make sure handler for E_USER_DEPRECATED is registered - set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); +if (empty($config->log_deprecated)) { + // Reset handler for E_USER_DEPRECATED + set_error_handler(null, E_USER_DEPRECATED); +} else { + // Make sure handler for E_USER_DEPRECATED is registered + set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); } -if (JDEBUG || $config->error_reporting === 'maximum') -{ - // Set new Exception handler with debug enabled - $errorHandler->setExceptionHandler( - [ - new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), - 'renderException' - ] - ); +if (JDEBUG || $config->error_reporting === 'maximum') { + // Set new Exception handler with debug enabled + $errorHandler->setExceptionHandler( + [ + new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), + 'renderException' + ] + ); } /** @@ -109,15 +101,12 @@ * We need to do this as high up the stack as we can, as the default in \Joomla\Utilities\IpHelper is to * $allowIpOverride = true which is the wrong default for a generic site NOT behind a trusted proxy/load balancer. */ -if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) -{ - // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR - IpHelper::setAllowIpOverrides(true); -} -else -{ - // We disable the allowing of IP overriding using headers by default. - IpHelper::setAllowIpOverrides(false); +if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) { + // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR + IpHelper::setAllowIpOverrides(true); +} else { + // We disable the allowing of IP overriding using headers by default. + IpHelper::setAllowIpOverrides(false); } unset($config); diff --git a/index.php b/index.php index c2b6893a435f6..59acdd25df0d2 100644 --- a/index.php +++ b/index.php @@ -1,4 +1,5 @@ '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}', - 'text_file' => 'error.php' - ], - \Joomla\CMS\Log\Log::ALL, - ['error'] - ); +if (is_writable(JPATH_ADMINISTRATOR . '/logs')) { + \Joomla\CMS\Log\Log::addLogger( + [ + 'format' => '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}', + 'text_file' => 'error.php' + ], + \Joomla\CMS\Log\Log::ALL, + ['error'] + ); } // Register the Installation application @@ -45,7 +44,7 @@ // Get the dependency injection container $container = \Joomla\CMS\Factory::getContainer(); -$container->registerServiceProvider(new \Joomla\CMS\Installation\Service\Provider\Application); +$container->registerServiceProvider(new \Joomla\CMS\Installation\Service\Provider\Application()); /* * Alias the session service keys to the web session service as that is the primary session backend for this application @@ -55,11 +54,11 @@ * deprecated to be removed when the class name alias is removed as well. */ $container->alias('session.web', 'session.web.installation') - ->alias('session', 'session.web.installation') - ->alias('JSession', 'session.web.installation') - ->alias(\Joomla\CMS\Session\Session::class, 'session.web.installation') - ->alias(\Joomla\Session\Session::class, 'session.web.installation') - ->alias(\Joomla\Session\SessionInterface::class, 'session.web.installation'); + ->alias('session', 'session.web.installation') + ->alias('JSession', 'session.web.installation') + ->alias(\Joomla\CMS\Session\Session::class, 'session.web.installation') + ->alias(\Joomla\Session\Session::class, 'session.web.installation') + ->alias(\Joomla\Session\SessionInterface::class, 'session.web.installation'); // Instantiate and execute the application $container->get(\Joomla\CMS\Installation\Application\InstallationApplication::class)->execute(); diff --git a/installation/includes/defines.php b/installation/includes/defines.php index e3c481ed1a173..9ae71a84dc859 100644 --- a/installation/includes/defines.php +++ b/installation/includes/defines.php @@ -1,4 +1,5 @@ 10) - && !file_exists(JPATH_INSTALLATION . '/index.php')) -{ - header('Location: ../index.php'); - exit(); +if ( + file_exists(JPATH_CONFIGURATION . '/configuration.php') + && (filesize(JPATH_CONFIGURATION . '/configuration.php') > 10) + && !file_exists(JPATH_INSTALLATION . '/index.php') +) { + header('Location: ../index.php'); + exit(); } // Import the Joomla Platform. require_once JPATH_LIBRARIES . '/bootstrap.php'; // If debug mode enabled, set new Exception handler with debug enabled. -if (JDEBUG) -{ - $errorHandler->setExceptionHandler( - [ - new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), - 'renderException' - ] - ); +if (JDEBUG) { + $errorHandler->setExceptionHandler( + [ + new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), + 'renderException' + ] + ); } diff --git a/installation/index.php b/installation/index.php index dc7ebdce1b082..25c1eeeb53e5e 100644 --- a/installation/index.php +++ b/installation/index.php @@ -1,4 +1,5 @@ name = 'installation'; - - // Register the client ID. - $this->clientId = 2; - - // Run the parent constructor. - parent::__construct($input, $config, $client, $container); - - // Store the debug value to config based on the JDEBUG flag. - $this->config->set('debug', JDEBUG); - - // Register the config to Factory. - Factory::$config = $this->config; - - // Set the root in the URI one level up. - $parts = explode('/', Uri::base(true)); - array_pop($parts); - Uri::root(null, implode('/', $parts)); - } - - /** - * After the session has been started we need to populate it with some default values. - * - * @param SessionEvent $event Session event being triggered - * - * @return void - * - * @since 4.0.0 - */ - public function afterSessionStart(SessionEvent $event) - { - $session = $event->getSession(); - - if ($session->isNew()) - { - $session->set('registry', new Registry('session')); - } - } - - /** - * Method to display errors in language parsing. - * - * @return string Language debug output. - * - * @since 3.1 - */ - public function debugLanguage() - { - if ($this->getDocument()->getType() != 'html') - { - return ''; - } - - $lang = Factory::getLanguage(); - $output = '

    ' . Text::_('JDEBUG_LANGUAGE_FILES_IN_ERROR') . '

    '; - - $errorfiles = $lang->getErrorFiles(); - - if (count($errorfiles)) - { - $output .= '
      '; - - foreach ($errorfiles as $error) - { - $output .= "
    • $error
    • "; - } - - $output .= '
    '; - } - else - { - $output .= '
    ' . Text::_('JNONE') . '
    '; - } - - $output .= '

    ' . Text::_('JDEBUG_LANGUAGE_UNTRANSLATED_STRING') . '

    '; - $output .= '
    ';
    -		$orphans = $lang->getOrphans();
    -
    -		if (count($orphans))
    -		{
    -			ksort($orphans, SORT_STRING);
    -
    -			$guesses = array();
    -
    -			foreach ($orphans as $key => $occurrence)
    -			{
    -				$guess = str_replace('_', ' ', $key);
    -
    -				$parts = explode(' ', $guess);
    -
    -				if (count($parts) > 1)
    -				{
    -					array_shift($parts);
    -					$guess = implode(' ', $parts);
    -				}
    -
    -				$guess = trim($guess);
    -
    -				$key = strtoupper(trim($key));
    -				$key = preg_replace('#\s+#', '_', $key);
    -				$key = preg_replace('#\W#', '', $key);
    -
    -				// Prepare the text.
    -				$guesses[] = $key . '="' . $guess . '"';
    -			}
    -
    -			$output .= implode("\n", $guesses);
    -		}
    -		else
    -		{
    -			$output .= '
    ' . Text::_('JNONE') . '
    '; - } - - $output .= '
    '; - - return $output; - } - - /** - * Dispatch the application. - * - * @return void - * - * @since 3.1 - */ - public function dispatch() - { - // Load the document to the API. - $this->loadDocument(); - - // Set up the params - $document = $this->getDocument(); - - // Register the document object with Factory. - Factory::$document = $document; - - // Define component path. - \define('JPATH_COMPONENT', JPATH_BASE); - \define('JPATH_COMPONENT_SITE', JPATH_SITE); - \define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR); - - // Execute the task. - ob_start(); - $this->executeController(); - $contents = ob_get_clean(); - - // If debug language is set, append its output to the contents. - if ($this->config->get('debug_lang')) - { - $contents .= $this->debugLanguage(); - } - - // Set the content on the document - $this->getDocument()->setBuffer($contents, 'component'); - - // Set the document title - $document->setTitle(Text::_('INSTL_PAGE_TITLE')); - } - - /** - * Method to run the Web application routines. - * - * @return void - * - * @since 3.1 - */ - protected function doExecute() - { - // Ensure we load the namespace loader - $this->createExtensionNamespaceMap(); - - // Initialise the application. - $this->initialiseApp(); - - // Dispatch the application. - $this->dispatch(); - } - - /** - * Execute the application. - * - * @return void - * - * @since 4.0.0 - */ - public function execute() - { - try - { - // Perform application routines. - $this->doExecute(); - - // If we have an application document object, render it. - if ($this->document instanceof Document) - { - // Render the application output. - $this->render(); - } - - // If gzip compression is enabled in configuration and the server is compliant, compress the output. - if ($this->get('gzip') && !ini_get('zlib.output_compression') && (ini_get('output_handler') != 'ob_gzhandler')) - { - $this->compress(); - } - } - catch (\Throwable $throwable) - { - ExceptionHandler::render($throwable); - } - - // Send the application response. - $this->respond(); - } - - /** - * Method to load a PHP configuration class file based on convention and return the instantiated data object. You - * will extend this method in child classes to provide configuration data from whatever data source is relevant - * for your specific application. - * - * @param string $file The path and filename of the configuration file. If not provided, configuration.php - * in JPATH_BASE will be used. - * @param string $class The class name to instantiate. - * - * @return mixed Either an array or object to be loaded into the configuration object. - * - * @since 1.7.3 - * @throws \RuntimeException - */ - protected function fetchConfigurationData($file = '', $class = 'JConfig') - { - return array(); - } - - /** - * Executed a controller from the input task. - * - * @return void - * - * @since 4.0.0 - */ - private function executeController() - { - $task = $this->input->getCmd('task', ''); - - // The name of the controller - $controllerName = 'display'; - - // Parse task in format controller.task - if ($task !== '') - { - list($controllerName, $task) = explode('.', $task, 2); - } - - $factory = new MVCFactory('Joomla\\CMS'); - $factory->setDatabase($this->getContainer()->get(DatabaseInterface::class)); - - // Create the instance - $controller = $factory->createController($controllerName, 'Installation', [], $this, $this->input); - - // Execute the task - $controller->execute($task); - } - - /** - * Returns the language code and help URL set in the localise.xml file. - * Used for forcing a particular language in localised releases. - * - * @return mixed False on failure, array on success. - * - * @since 3.1 - */ - public function getLocalise() - { - $xml = simplexml_load_file(JPATH_INSTALLATION . '/localise.xml'); - - if (!$xml) - { - return false; - } - - // Check that it's a localise file. - if ($xml->getName() !== 'localise') - { - return false; - } - - $ret = array(); - - $ret['language'] = (string) $xml->forceLang; - $ret['debug'] = (string) $xml->debug; - $ret['sampledata'] = (string) $xml->sampledata; - - return $ret; - } - - /** - * Returns the installed language files in the administrative and frontend area. - * - * @param DatabaseInterface|null $db Database driver. - * - * @return array Array with installed language packs in admin and site area. - * - * @since 3.1 - */ - public function getLocaliseAdmin(DatabaseInterface $db = null) - { - $langfiles = array(); - - // If db connection, fetch them from the database. - if ($db) - { - foreach (LanguageHelper::getInstalledLanguages() as $clientId => $language) - { - $clientName = $clientId === 0 ? 'site' : 'admin'; - - foreach ($language as $languageCode => $lang) - { - $langfiles[$clientName][] = $lang->element; - } - } - } - // Read the folder names in the site and admin area. - else - { - $langfiles['site'] = Folder::folders(LanguageHelper::getLanguagePath(JPATH_SITE)); - $langfiles['admin'] = Folder::folders(LanguageHelper::getLanguagePath(JPATH_ADMINISTRATOR)); - } - - return $langfiles; - } - - /** - * Gets the name of the current template. - * - * @param boolean $params True to return the template parameters - * - * @return string|\stdClass The name of the template. - * - * @since 3.1 - */ - public function getTemplate($params = false) - { - if ($params) - { - $template = new \stdClass; - $template->template = 'template'; - $template->params = new Registry; - $template->inheritable = 0; - $template->parent = ''; - - return $template; - } - - return 'template'; - } - - /** - * Initialise the application. - * - * @param array $options An optional associative array of configuration settings. - * - * @return void - * - * @since 3.1 - */ - protected function initialiseApp($options = array()) - { - // Get the localisation information provided in the localise.xml file. - $forced = $this->getLocalise(); - - // Check the request data for the language. - if (empty($options['language'])) - { - $requestLang = $this->input->getCmd('lang', null); - - if ($requestLang !== null) - { - $options['language'] = $requestLang; - } - } - - // Check the session for the language. - if (empty($options['language'])) - { - $sessionOptions = $this->getSession()->get('setup.options'); - - if (isset($sessionOptions['language'])) - { - $options['language'] = $sessionOptions['language']; - } - } - - // This could be a first-time visit - try to determine what the client accepts. - if (empty($options['language'])) - { - if (!empty($forced['language'])) - { - $options['language'] = $forced['language']; - } - else - { - $options['language'] = LanguageHelper::detectLanguage(); - - if (empty($options['language'])) - { - $options['language'] = 'en-GB'; - } - } - } - - // Give the user English. - if (empty($options['language'])) - { - $options['language'] = 'en-GB'; - } - - // Set the official helpurl. - $options['helpurl'] = 'https://help.joomla.org/proxy?keyref=Help{major}{minor}:{keyref}&lang={langcode}'; - - // Store helpurl in the session. - $this->getSession()->set('setup.helpurl', $options['helpurl']); - - // Set the language in the class. - $this->config->set('language', $options['language']); - $this->config->set('debug_lang', $forced['debug']); - $this->config->set('sampledata', $forced['sampledata']); - $this->config->set('helpurl', $options['helpurl']); - } - - /** - * Allows the application to load a custom or default document. - * - * The logic and options for creating this object are adequately generic for default cases - * but for many applications it will make sense to override this method and create a document, - * if required, based on more specific needs. - * - * @param Document|null $document An optional document object. If omitted, the factory document is created. - * - * @return InstallationApplication This method is chainable. - * - * @since 3.2 - */ - public function loadDocument(Document $document = null) - { - if ($document === null) - { - $lang = Factory::getLanguage(); - $type = $this->input->get('format', 'html', 'word'); - $date = new Date('now'); - - $attributes = array( - 'charset' => 'utf-8', - 'lineend' => 'unix', - 'tab' => "\t", - 'language' => $lang->getTag(), - 'direction' => $lang->isRtl() ? 'rtl' : 'ltr', - 'mediaversion' => md5($date->format('YmdHi')), - ); - - $document = $this->getContainer()->get(FactoryInterface::class)->createDocument($type, $attributes); - - // Register the instance to Factory. - Factory::$document = $document; - } - - $this->document = $document; - - return $this; - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 3.1 - */ - public function render() - { - $options = []; - - if ($this->document instanceof HtmlDocument) - { - $file = $this->input->getCmd('tmpl', 'index'); - - $options = [ - 'template' => 'template', - 'file' => $file . '.php', - 'directory' => JPATH_THEMES, - 'params' => '{}', - "templateInherits" => '' - ]; - } - - // Parse the document. - $this->document->parse($options); - - // Render the document. - $data = $this->document->render($this->get('cache_enabled'), $options); - - // Set the application output data. - $this->setBody($data); - } - - /** - * Set configuration values. - * - * @param array $vars Array of configuration values - * @param string $namespace The namespace - * - * @return void - * - * @since 3.1 - */ - public function setCfg(array $vars = array(), $namespace = 'config') - { - $this->config->loadArray($vars, $namespace); - } - - /** - * Returns the application \JMenu object. - * - * @param string|null $name The name of the application/client. - * @param array $options An optional associative array of configuration settings. - * - * @return null - * - * @since 3.2 - */ - public function getMenu($name = null, $options = array()) - { - return null; - } + use \Joomla\CMS\Application\ExtensionNamespaceMapper; + + /** + * Class constructor. + * + * @param Input|null $input An optional argument to provide dependency injection for the application's input + * object. If the argument is a JInput object that object will become the + * application's input object, otherwise a default input object is created. + * @param Registry|null $config An optional argument to provide dependency injection for the application's + * config object. If the argument is a Registry object that object will become + * the application's config object, otherwise a default config object is created. + * @param WebClient|null $client An optional argument to provide dependency injection for the application's + * client object. If the argument is a WebClient object that object will become the + * application's client object, otherwise a default client object is created. + * @param Container|null $container Dependency injection container. + * + * @since 3.1 + */ + public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, Container $container = null) + { + // Register the application name. + $this->name = 'installation'; + + // Register the client ID. + $this->clientId = 2; + + // Run the parent constructor. + parent::__construct($input, $config, $client, $container); + + // Store the debug value to config based on the JDEBUG flag. + $this->config->set('debug', JDEBUG); + + // Register the config to Factory. + Factory::$config = $this->config; + + // Set the root in the URI one level up. + $parts = explode('/', Uri::base(true)); + array_pop($parts); + Uri::root(null, implode('/', $parts)); + } + + /** + * After the session has been started we need to populate it with some default values. + * + * @param SessionEvent $event Session event being triggered + * + * @return void + * + * @since 4.0.0 + */ + public function afterSessionStart(SessionEvent $event) + { + $session = $event->getSession(); + + if ($session->isNew()) { + $session->set('registry', new Registry('session')); + } + } + + /** + * Method to display errors in language parsing. + * + * @return string Language debug output. + * + * @since 3.1 + */ + public function debugLanguage() + { + if ($this->getDocument()->getType() != 'html') { + return ''; + } + + $lang = Factory::getLanguage(); + $output = '

    ' . Text::_('JDEBUG_LANGUAGE_FILES_IN_ERROR') . '

    '; + + $errorfiles = $lang->getErrorFiles(); + + if (count($errorfiles)) { + $output .= '
      '; + + foreach ($errorfiles as $error) { + $output .= "
    • $error
    • "; + } + + $output .= '
    '; + } else { + $output .= '
    ' . Text::_('JNONE') . '
    '; + } + + $output .= '

    ' . Text::_('JDEBUG_LANGUAGE_UNTRANSLATED_STRING') . '

    '; + $output .= '
    ';
    +        $orphans = $lang->getOrphans();
    +
    +        if (count($orphans)) {
    +            ksort($orphans, SORT_STRING);
    +
    +            $guesses = array();
    +
    +            foreach ($orphans as $key => $occurrence) {
    +                $guess = str_replace('_', ' ', $key);
    +
    +                $parts = explode(' ', $guess);
    +
    +                if (count($parts) > 1) {
    +                    array_shift($parts);
    +                    $guess = implode(' ', $parts);
    +                }
    +
    +                $guess = trim($guess);
    +
    +                $key = strtoupper(trim($key));
    +                $key = preg_replace('#\s+#', '_', $key);
    +                $key = preg_replace('#\W#', '', $key);
    +
    +                // Prepare the text.
    +                $guesses[] = $key . '="' . $guess . '"';
    +            }
    +
    +            $output .= implode("\n", $guesses);
    +        } else {
    +            $output .= '
    ' . Text::_('JNONE') . '
    '; + } + + $output .= '
    '; + + return $output; + } + + /** + * Dispatch the application. + * + * @return void + * + * @since 3.1 + */ + public function dispatch() + { + // Load the document to the API. + $this->loadDocument(); + + // Set up the params + $document = $this->getDocument(); + + // Register the document object with Factory. + Factory::$document = $document; + + // Define component path. + \define('JPATH_COMPONENT', JPATH_BASE); + \define('JPATH_COMPONENT_SITE', JPATH_SITE); + \define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR); + + // Execute the task. + ob_start(); + $this->executeController(); + $contents = ob_get_clean(); + + // If debug language is set, append its output to the contents. + if ($this->config->get('debug_lang')) { + $contents .= $this->debugLanguage(); + } + + // Set the content on the document + $this->getDocument()->setBuffer($contents, 'component'); + + // Set the document title + $document->setTitle(Text::_('INSTL_PAGE_TITLE')); + } + + /** + * Method to run the Web application routines. + * + * @return void + * + * @since 3.1 + */ + protected function doExecute() + { + // Ensure we load the namespace loader + $this->createExtensionNamespaceMap(); + + // Initialise the application. + $this->initialiseApp(); + + // Dispatch the application. + $this->dispatch(); + } + + /** + * Execute the application. + * + * @return void + * + * @since 4.0.0 + */ + public function execute() + { + try { + // Perform application routines. + $this->doExecute(); + + // If we have an application document object, render it. + if ($this->document instanceof Document) { + // Render the application output. + $this->render(); + } + + // If gzip compression is enabled in configuration and the server is compliant, compress the output. + if ($this->get('gzip') && !ini_get('zlib.output_compression') && (ini_get('output_handler') != 'ob_gzhandler')) { + $this->compress(); + } + } catch (\Throwable $throwable) { + ExceptionHandler::render($throwable); + } + + // Send the application response. + $this->respond(); + } + + /** + * Method to load a PHP configuration class file based on convention and return the instantiated data object. You + * will extend this method in child classes to provide configuration data from whatever data source is relevant + * for your specific application. + * + * @param string $file The path and filename of the configuration file. If not provided, configuration.php + * in JPATH_BASE will be used. + * @param string $class The class name to instantiate. + * + * @return mixed Either an array or object to be loaded into the configuration object. + * + * @since 1.7.3 + * @throws \RuntimeException + */ + protected function fetchConfigurationData($file = '', $class = 'JConfig') + { + return array(); + } + + /** + * Executed a controller from the input task. + * + * @return void + * + * @since 4.0.0 + */ + private function executeController() + { + $task = $this->input->getCmd('task', ''); + + // The name of the controller + $controllerName = 'display'; + + // Parse task in format controller.task + if ($task !== '') { + list($controllerName, $task) = explode('.', $task, 2); + } + + $factory = new MVCFactory('Joomla\\CMS'); + $factory->setDatabase($this->getContainer()->get(DatabaseInterface::class)); + + // Create the instance + $controller = $factory->createController($controllerName, 'Installation', [], $this, $this->input); + + // Execute the task + $controller->execute($task); + } + + /** + * Returns the language code and help URL set in the localise.xml file. + * Used for forcing a particular language in localised releases. + * + * @return mixed False on failure, array on success. + * + * @since 3.1 + */ + public function getLocalise() + { + $xml = simplexml_load_file(JPATH_INSTALLATION . '/localise.xml'); + + if (!$xml) { + return false; + } + + // Check that it's a localise file. + if ($xml->getName() !== 'localise') { + return false; + } + + $ret = array(); + + $ret['language'] = (string) $xml->forceLang; + $ret['debug'] = (string) $xml->debug; + $ret['sampledata'] = (string) $xml->sampledata; + + return $ret; + } + + /** + * Returns the installed language files in the administrative and frontend area. + * + * @param DatabaseInterface|null $db Database driver. + * + * @return array Array with installed language packs in admin and site area. + * + * @since 3.1 + */ + public function getLocaliseAdmin(DatabaseInterface $db = null) + { + $langfiles = array(); + + // If db connection, fetch them from the database. + if ($db) { + foreach (LanguageHelper::getInstalledLanguages() as $clientId => $language) { + $clientName = $clientId === 0 ? 'site' : 'admin'; + + foreach ($language as $languageCode => $lang) { + $langfiles[$clientName][] = $lang->element; + } + } + } + // Read the folder names in the site and admin area. + else { + $langfiles['site'] = Folder::folders(LanguageHelper::getLanguagePath(JPATH_SITE)); + $langfiles['admin'] = Folder::folders(LanguageHelper::getLanguagePath(JPATH_ADMINISTRATOR)); + } + + return $langfiles; + } + + /** + * Gets the name of the current template. + * + * @param boolean $params True to return the template parameters + * + * @return string|\stdClass The name of the template. + * + * @since 3.1 + */ + public function getTemplate($params = false) + { + if ($params) { + $template = new \stdClass(); + $template->template = 'template'; + $template->params = new Registry(); + $template->inheritable = 0; + $template->parent = ''; + + return $template; + } + + return 'template'; + } + + /** + * Initialise the application. + * + * @param array $options An optional associative array of configuration settings. + * + * @return void + * + * @since 3.1 + */ + protected function initialiseApp($options = array()) + { + // Get the localisation information provided in the localise.xml file. + $forced = $this->getLocalise(); + + // Check the request data for the language. + if (empty($options['language'])) { + $requestLang = $this->input->getCmd('lang', null); + + if ($requestLang !== null) { + $options['language'] = $requestLang; + } + } + + // Check the session for the language. + if (empty($options['language'])) { + $sessionOptions = $this->getSession()->get('setup.options'); + + if (isset($sessionOptions['language'])) { + $options['language'] = $sessionOptions['language']; + } + } + + // This could be a first-time visit - try to determine what the client accepts. + if (empty($options['language'])) { + if (!empty($forced['language'])) { + $options['language'] = $forced['language']; + } else { + $options['language'] = LanguageHelper::detectLanguage(); + + if (empty($options['language'])) { + $options['language'] = 'en-GB'; + } + } + } + + // Give the user English. + if (empty($options['language'])) { + $options['language'] = 'en-GB'; + } + + // Set the official helpurl. + $options['helpurl'] = 'https://help.joomla.org/proxy?keyref=Help{major}{minor}:{keyref}&lang={langcode}'; + + // Store helpurl in the session. + $this->getSession()->set('setup.helpurl', $options['helpurl']); + + // Set the language in the class. + $this->config->set('language', $options['language']); + $this->config->set('debug_lang', $forced['debug']); + $this->config->set('sampledata', $forced['sampledata']); + $this->config->set('helpurl', $options['helpurl']); + } + + /** + * Allows the application to load a custom or default document. + * + * The logic and options for creating this object are adequately generic for default cases + * but for many applications it will make sense to override this method and create a document, + * if required, based on more specific needs. + * + * @param Document|null $document An optional document object. If omitted, the factory document is created. + * + * @return InstallationApplication This method is chainable. + * + * @since 3.2 + */ + public function loadDocument(Document $document = null) + { + if ($document === null) { + $lang = Factory::getLanguage(); + $type = $this->input->get('format', 'html', 'word'); + $date = new Date('now'); + + $attributes = array( + 'charset' => 'utf-8', + 'lineend' => 'unix', + 'tab' => "\t", + 'language' => $lang->getTag(), + 'direction' => $lang->isRtl() ? 'rtl' : 'ltr', + 'mediaversion' => md5($date->format('YmdHi')), + ); + + $document = $this->getContainer()->get(FactoryInterface::class)->createDocument($type, $attributes); + + // Register the instance to Factory. + Factory::$document = $document; + } + + $this->document = $document; + + return $this; + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 3.1 + */ + public function render() + { + $options = []; + + if ($this->document instanceof HtmlDocument) { + $file = $this->input->getCmd('tmpl', 'index'); + + $options = [ + 'template' => 'template', + 'file' => $file . '.php', + 'directory' => JPATH_THEMES, + 'params' => '{}', + "templateInherits" => '' + ]; + } + + // Parse the document. + $this->document->parse($options); + + // Render the document. + $data = $this->document->render($this->get('cache_enabled'), $options); + + // Set the application output data. + $this->setBody($data); + } + + /** + * Set configuration values. + * + * @param array $vars Array of configuration values + * @param string $namespace The namespace + * + * @return void + * + * @since 3.1 + */ + public function setCfg(array $vars = array(), $namespace = 'config') + { + $this->config->loadArray($vars, $namespace); + } + + /** + * Returns the application \JMenu object. + * + * @param string|null $name The name of the application/client. + * @param array $options An optional associative array of configuration settings. + * + * @return null + * + * @since 3.2 + */ + public function getMenu($name = null, $options = array()) + { + return null; + } } diff --git a/installation/src/Controller/DisplayController.php b/installation/src/Controller/DisplayController.php index 4968a764ab14a..80cc8303d54b0 100644 --- a/installation/src/Controller/DisplayController.php +++ b/installation/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app; - - $defaultView = 'setup'; - - // If the app has already been installed, default to the remove view - if (file_exists(JPATH_CONFIGURATION . '/configuration.php') - && filesize(JPATH_CONFIGURATION . '/configuration.php') > 10 - && file_exists(JPATH_INSTALLATION . '/index.php')) - { - $defaultView = 'remove'; - } - - /** @var \Joomla\CMS\Installation\Model\ChecksModel $model */ - $model = $this->getModel('Checks'); - - $vName = $this->input->getWord('view', $defaultView); - - if (!$model->getPhpOptionsSufficient() && $defaultView !== 'remove') - { - if ($vName !== 'preinstall') - { - $app->redirect('index.php?view=preinstall'); - } - - $vName = 'preinstall'; - } - else - { - if ($vName === 'preinstall') - { - $app->redirect('index.php?view=setup'); - } - - if ($vName === 'remove' && !file_exists(JPATH_CONFIGURATION . '/configuration.php')) - { - $app->redirect('index.php?view=setup'); - } - - if ($vName !== $defaultView && !$model->getOptions() && $defaultView !== 'remove') - { - $app->redirect('index.php'); - } - } - - $this->input->set('view', $vName); - - return parent::display($cachable, $urlparams); - } - - /** - * Method to get a reference to the current view and load it if necessary. - * - * @param string $name The view name. Optional, defaults to the controller name. - * @param string $type The view type. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for view. Optional. - * - * @return AbstractView Reference to the view or an error. - * - * @since 3.0 - * @throws \Exception - */ - public function getView($name = '', $type = '', $prefix = '', $config = array()) - { - $view = parent::getView($name, $type, $prefix, $config); - - if ($view instanceof AbstractView) - { - // Set some models, used by various views - $view->setModel($this->getModel('Checks')); - $view->setModel($this->getModel('Languages')); - } - - return $view; - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached. + * @param boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return \Joomla\CMS\MVC\Controller\BaseController This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $app = $this->app; + + $defaultView = 'setup'; + + // If the app has already been installed, default to the remove view + if ( + file_exists(JPATH_CONFIGURATION . '/configuration.php') + && filesize(JPATH_CONFIGURATION . '/configuration.php') > 10 + && file_exists(JPATH_INSTALLATION . '/index.php') + ) { + $defaultView = 'remove'; + } + + /** @var \Joomla\CMS\Installation\Model\ChecksModel $model */ + $model = $this->getModel('Checks'); + + $vName = $this->input->getWord('view', $defaultView); + + if (!$model->getPhpOptionsSufficient() && $defaultView !== 'remove') { + if ($vName !== 'preinstall') { + $app->redirect('index.php?view=preinstall'); + } + + $vName = 'preinstall'; + } else { + if ($vName === 'preinstall') { + $app->redirect('index.php?view=setup'); + } + + if ($vName === 'remove' && !file_exists(JPATH_CONFIGURATION . '/configuration.php')) { + $app->redirect('index.php?view=setup'); + } + + if ($vName !== $defaultView && !$model->getOptions() && $defaultView !== 'remove') { + $app->redirect('index.php'); + } + } + + $this->input->set('view', $vName); + + return parent::display($cachable, $urlparams); + } + + /** + * Method to get a reference to the current view and load it if necessary. + * + * @param string $name The view name. Optional, defaults to the controller name. + * @param string $type The view type. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for view. Optional. + * + * @return AbstractView Reference to the view or an error. + * + * @since 3.0 + * @throws \Exception + */ + public function getView($name = '', $type = '', $prefix = '', $config = array()) + { + $view = parent::getView($name, $type, $prefix, $config); + + if ($view instanceof AbstractView) { + // Set some models, used by various views + $view->setModel($this->getModel('Checks')); + $view->setModel($this->getModel('Languages')); + } + + return $view; + } } diff --git a/installation/src/Controller/InstallationController.php b/installation/src/Controller/InstallationController.php index 87ea82c249c50..141bcc6824109 100644 --- a/installation/src/Controller/InstallationController.php +++ b/installation/src/Controller/InstallationController.php @@ -1,4 +1,5 @@ registerTask('populate1', 'populate'); - $this->registerTask('populate2', 'populate'); - $this->registerTask('populate3', 'populate'); - $this->registerTask('custom1', 'populate'); - $this->registerTask('custom2', 'populate'); - $this->registerTask('removeFolder', 'delete'); - } - - /** - * Database check task. - * - * @return void - * - * @since 4.0.0 - */ - public function dbcheck() - { - $this->checkValidToken(); - - // Redirect to the page. - $r = new \stdClass; - $r->view = 'setup'; - - // Check the form - /** @var \Joomla\CMS\Installation\Model\SetupModel $model */ - $model = $this->getModel('Setup'); - - if ($model->checkForm('setup') === false) - { - $this->app->enqueueMessage(Text::_('INSTL_DATABASE_VALIDATION_ERROR'), 'error'); - $r->validated = false; - $this->sendJsonResponse($r); - - return; - } - - $r->validated = $model->validateDbConnection(); - - $this->sendJsonResponse($r); - } - - /** - * Create DB task. - * - * @return void - * - * @since 4.0.0 - */ - public function create() - { - $this->checkValidToken(); - - $r = new \stdClass; - - /** @var \Joomla\CMS\Installation\Model\DatabaseModel $databaseModel */ - $databaseModel = $this->getModel('Database'); - - // Create Db - try - { - $dbCreated = $databaseModel->createDatabase(); - } - catch (\RuntimeException $e) - { - $this->app->enqueueMessage($e->getMessage(), 'error'); - - $dbCreated = false; - } - - if (!$dbCreated) - { - $r->view = 'setup'; - } - else - { - if (!$databaseModel->handleOldDatabase()) - { - $r->view = 'setup'; - } - } - - $this->sendJsonResponse($r); - } - - /** - * Populate the database. - * - * @return void - * - * @since 4.0.0 - */ - public function populate() - { - $this->checkValidToken(); - $step = $this->getTask(); - /** @var \Joomla\CMS\Installation\Model\DatabaseModel $model */ - $model = $this->getModel('Database'); - - $r = new \stdClass; - $db = $model->initialise(); - $files = [ - 'populate1' => 'base', - 'populate2' => 'supports', - 'populate3' => 'extensions', - 'custom1' => 'localise', - 'custom2' => 'custom' - ]; - - $schema = $files[$step]; - $serverType = $db->getServerType(); - - if (in_array($step, ['custom1', 'custom2']) && !is_file('sql/' . $serverType . '/' . $schema . '.sql')) - { - $this->sendJsonResponse($r); - - return; - } - - if (!isset($files[$step])) - { - $r->view = 'setup'; - Factory::getApplication()->enqueueMessage(Text::_('INSTL_SAMPLE_DATA_NOT_FOUND'), 'error'); - $this->sendJsonResponse($r); - } - - // Attempt to populate the database with the given file. - if (!$model->createTables($schema)) - { - $r->view = 'setup'; - } - - $this->sendJsonResponse($r); - } - - /** - * Config task. - * - * @return void - * - * @since 4.0.0 - */ - public function config() - { - $this->checkValidToken(); - - /** @var \Joomla\CMS\Installation\Model\SetupModel $setUpModel */ - $setUpModel = $this->getModel('Setup'); - - // Get the options from the session - $options = $setUpModel->getOptions(); - - $r = new \stdClass; - $r->view = 'remove'; - - /** @var \Joomla\CMS\Installation\Model\ConfigurationModel $configurationModel */ - $configurationModel = $this->getModel('Configuration'); - - // Attempt to setup the configuration. - if (!$configurationModel->setup($options)) - { - $r->view = 'setup'; - } - - $this->sendJsonResponse($r); - } - - /** - * Languages task. - * - * @return void - * - * @since 4.0.0 - */ - public function languages() - { - $this->checkValidToken(); - - // Get array of selected languages - $lids = (array) $this->input->get('cid', [], 'int'); - - // Remove zero values resulting from input filter - $lids = array_filter($lids); - - if (empty($lids)) - { - // No languages have been selected - $this->app->enqueueMessage(Text::_('INSTL_LANGUAGES_NO_LANGUAGE_SELECTED'), 'warning'); - } - else - { - // Get the languages model. - /** @var \Joomla\CMS\Installation\Model\LanguagesModel $model */ - $model = $this->getModel('Languages'); - - // Install selected languages - $model->install($lids); - } - - // Redirect to the page. - $r = new \stdClass; - $r->view = 'remove'; - - $this->sendJsonResponse($r); - } - - /** - * Delete installation folder task. - * - * @return void - * - * @since 4.0.0 - */ - public function delete() - { - $this->checkValidToken(); - - /** @var \Joomla\CMS\Installation\Model\CleanupModel $model */ - $model = $this->getModel('Cleanup'); - - if (!$model->deleteInstallationFolder()) - { - // We can't send a response with sendJsonResponse because our installation classes might not now exist - $error = [ - 'token' => Session::getFormToken(true), - 'error' => true, - 'data' => [ - 'view' => 'remove' - ], - 'messages' => [ - 'warning' => [ - Text::sprintf('INSTL_COMPLETE_ERROR_FOLDER_DELETE', 'installation') - ] - ] - ]; - - echo json_encode($error); - - return; - } - - $this->app->getSession()->destroy(); - - // We can't send a response with sendJsonResponse because our installation classes now do not exist - echo json_encode(['error' => false]); - } + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object. + * + * @since 3.0 + */ + public function __construct($config = [], MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('populate1', 'populate'); + $this->registerTask('populate2', 'populate'); + $this->registerTask('populate3', 'populate'); + $this->registerTask('custom1', 'populate'); + $this->registerTask('custom2', 'populate'); + $this->registerTask('removeFolder', 'delete'); + } + + /** + * Database check task. + * + * @return void + * + * @since 4.0.0 + */ + public function dbcheck() + { + $this->checkValidToken(); + + // Redirect to the page. + $r = new \stdClass(); + $r->view = 'setup'; + + // Check the form + /** @var \Joomla\CMS\Installation\Model\SetupModel $model */ + $model = $this->getModel('Setup'); + + if ($model->checkForm('setup') === false) { + $this->app->enqueueMessage(Text::_('INSTL_DATABASE_VALIDATION_ERROR'), 'error'); + $r->validated = false; + $this->sendJsonResponse($r); + + return; + } + + $r->validated = $model->validateDbConnection(); + + $this->sendJsonResponse($r); + } + + /** + * Create DB task. + * + * @return void + * + * @since 4.0.0 + */ + public function create() + { + $this->checkValidToken(); + + $r = new \stdClass(); + + /** @var \Joomla\CMS\Installation\Model\DatabaseModel $databaseModel */ + $databaseModel = $this->getModel('Database'); + + // Create Db + try { + $dbCreated = $databaseModel->createDatabase(); + } catch (\RuntimeException $e) { + $this->app->enqueueMessage($e->getMessage(), 'error'); + + $dbCreated = false; + } + + if (!$dbCreated) { + $r->view = 'setup'; + } else { + if (!$databaseModel->handleOldDatabase()) { + $r->view = 'setup'; + } + } + + $this->sendJsonResponse($r); + } + + /** + * Populate the database. + * + * @return void + * + * @since 4.0.0 + */ + public function populate() + { + $this->checkValidToken(); + $step = $this->getTask(); + /** @var \Joomla\CMS\Installation\Model\DatabaseModel $model */ + $model = $this->getModel('Database'); + + $r = new \stdClass(); + $db = $model->initialise(); + $files = [ + 'populate1' => 'base', + 'populate2' => 'supports', + 'populate3' => 'extensions', + 'custom1' => 'localise', + 'custom2' => 'custom' + ]; + + $schema = $files[$step]; + $serverType = $db->getServerType(); + + if (in_array($step, ['custom1', 'custom2']) && !is_file('sql/' . $serverType . '/' . $schema . '.sql')) { + $this->sendJsonResponse($r); + + return; + } + + if (!isset($files[$step])) { + $r->view = 'setup'; + Factory::getApplication()->enqueueMessage(Text::_('INSTL_SAMPLE_DATA_NOT_FOUND'), 'error'); + $this->sendJsonResponse($r); + } + + // Attempt to populate the database with the given file. + if (!$model->createTables($schema)) { + $r->view = 'setup'; + } + + $this->sendJsonResponse($r); + } + + /** + * Config task. + * + * @return void + * + * @since 4.0.0 + */ + public function config() + { + $this->checkValidToken(); + + /** @var \Joomla\CMS\Installation\Model\SetupModel $setUpModel */ + $setUpModel = $this->getModel('Setup'); + + // Get the options from the session + $options = $setUpModel->getOptions(); + + $r = new \stdClass(); + $r->view = 'remove'; + + /** @var \Joomla\CMS\Installation\Model\ConfigurationModel $configurationModel */ + $configurationModel = $this->getModel('Configuration'); + + // Attempt to setup the configuration. + if (!$configurationModel->setup($options)) { + $r->view = 'setup'; + } + + $this->sendJsonResponse($r); + } + + /** + * Languages task. + * + * @return void + * + * @since 4.0.0 + */ + public function languages() + { + $this->checkValidToken(); + + // Get array of selected languages + $lids = (array) $this->input->get('cid', [], 'int'); + + // Remove zero values resulting from input filter + $lids = array_filter($lids); + + if (empty($lids)) { + // No languages have been selected + $this->app->enqueueMessage(Text::_('INSTL_LANGUAGES_NO_LANGUAGE_SELECTED'), 'warning'); + } else { + // Get the languages model. + /** @var \Joomla\CMS\Installation\Model\LanguagesModel $model */ + $model = $this->getModel('Languages'); + + // Install selected languages + $model->install($lids); + } + + // Redirect to the page. + $r = new \stdClass(); + $r->view = 'remove'; + + $this->sendJsonResponse($r); + } + + /** + * Delete installation folder task. + * + * @return void + * + * @since 4.0.0 + */ + public function delete() + { + $this->checkValidToken(); + + /** @var \Joomla\CMS\Installation\Model\CleanupModel $model */ + $model = $this->getModel('Cleanup'); + + if (!$model->deleteInstallationFolder()) { + // We can't send a response with sendJsonResponse because our installation classes might not now exist + $error = [ + 'token' => Session::getFormToken(true), + 'error' => true, + 'data' => [ + 'view' => 'remove' + ], + 'messages' => [ + 'warning' => [ + Text::sprintf('INSTL_COMPLETE_ERROR_FOLDER_DELETE', 'installation') + ] + ] + ]; + + echo json_encode($error); + + return; + } + + $this->app->getSession()->destroy(); + + // We can't send a response with sendJsonResponse because our installation classes now do not exist + echo json_encode(['error' => false]); + } } diff --git a/installation/src/Controller/JSONController.php b/installation/src/Controller/JSONController.php index f288c83d8d764..8b19a21ab7cb8 100644 --- a/installation/src/Controller/JSONController.php +++ b/installation/src/Controller/JSONController.php @@ -1,4 +1,5 @@ app->mimeType = 'application/json'; + /** + * Method to send a JSON response. The data parameter + * can be an Exception object for when an error has occurred or + * a JsonResponse for a good response. + * + * @param mixed $response JsonResponse on success, Exception on failure. + * + * @return void + * + * @since 4.0.0 + */ + protected function sendJsonResponse($response) + { + $this->app->mimeType = 'application/json'; - // Very crude workaround to give an error message when JSON is disabled - if (!function_exists('json_encode') || !function_exists('json_decode')) - { - $this->app->setHeader('status', 500); - echo '{"token":"' . Session::getFormToken(true) . '","lang":"' . Factory::getLanguage()->getTag() - . '","error":true,"header":"' . Text::_('INSTL_HEADER_ERROR') . '","message":"' . Text::_('INSTL_WARNJSON') . '"}'; + // Very crude workaround to give an error message when JSON is disabled + if (!function_exists('json_encode') || !function_exists('json_decode')) { + $this->app->setHeader('status', 500); + echo '{"token":"' . Session::getFormToken(true) . '","lang":"' . Factory::getLanguage()->getTag() + . '","error":true,"header":"' . Text::_('INSTL_HEADER_ERROR') . '","message":"' . Text::_('INSTL_WARNJSON') . '"}'; - return; - } + return; + } - // Check if we need to send an error code. - if ($response instanceof \Exception) - { - // Send the appropriate error code response. - $this->app->setHeader('status', $response->getCode(), true); - } + // Check if we need to send an error code. + if ($response instanceof \Exception) { + // Send the appropriate error code response. + $this->app->setHeader('status', $response->getCode(), true); + } - // Send the JSON response. - echo json_encode(new JsonResponse($response)); - } + // Send the JSON response. + echo json_encode(new JsonResponse($response)); + } - /** - * Checks for a form token, if it is invalid a JSON response with the error code 403 is sent. - * - * @return void - * - * @since 4.0.0 - * @see Session::checkToken() - */ - public function checkValidToken() - { - // Check for request forgeries. - if (!Session::checkToken()) - { - $this->sendJsonResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); + /** + * Checks for a form token, if it is invalid a JSON response with the error code 403 is sent. + * + * @return void + * + * @since 4.0.0 + * @see Session::checkToken() + */ + public function checkValidToken() + { + // Check for request forgeries. + if (!Session::checkToken()) { + $this->sendJsonResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); - $this->app->close(); - } - } + $this->app->close(); + } + } } diff --git a/installation/src/Controller/LanguageController.php b/installation/src/Controller/LanguageController.php index 031363d399f60..90744d261a8cc 100644 --- a/installation/src/Controller/LanguageController.php +++ b/installation/src/Controller/LanguageController.php @@ -1,4 +1,5 @@ checkValidToken(); - - // Check for potentially unwritable session - $session = $this->app->getSession(); - - if ($session->isNew()) - { - $this->sendJsonResponse(new \Exception(Text::_('INSTL_COOKIES_NOT_ENABLED'), 500)); - } - - /** @var SetupModel $model */ - $model = $this->getModel('Setup'); - - // Get the posted values from the request and validate them. - $data = $this->input->post->get('jform', [], 'array'); - $return = $model->validate($data, 'language'); - - $r = new \stdClass; - - // Check for validation errors. - if ($return === false) - { - /* - * The validate method enqueued all messages for us, so we just need to - * redirect back to the site setup screen. - */ - $r->view = $this->input->getWord('view', 'setup'); - $this->sendJsonResponse($r); - } - - // Store the options in the session. - $model->storeOptions($return); - - // Setup language - Factory::$language = Language::getInstance($return['language']); - - // Redirect to the page. - $r->view = $this->input->getWord('view', 'setup'); - - $this->sendJsonResponse($r); - } - - /** - * Sets the default language. - * - * @return void - * - * @since 4.0.0 - */ - public function setdefault() - { - $this->checkValidToken(); - - $app = $this->app; - - /** @var \Joomla\CMS\Installation\Model\LanguagesModel $model */ - $model = $this->getModel('Languages'); - - // Check for request forgeries in the administrator language - $admin_lang = $this->input->getString('administratorlang', false); - - // Check that the string is an ISO Language Code avoiding any injection. - if (!preg_match('/^[a-z]{2}(\-[A-Z]{2})?$/', $admin_lang)) - { - $admin_lang = 'en-GB'; - } - - // Attempt to set the default administrator language - if (!$model->setDefault($admin_lang, 'administrator')) - { - // Create an error response message. - $this->app->enqueueMessage(Text::_('INSTL_DEFAULTLANGUAGE_ADMIN_COULDNT_SET_DEFAULT'), 'error'); - } - else - { - // Create a response body. - $app->enqueueMessage(Text::sprintf('INSTL_DEFAULTLANGUAGE_ADMIN_SET_DEFAULT', $admin_lang), 'message'); - } - - // Check for request forgeries in the site language - $frontend_lang = $this->input->getString('frontendlang', false); - - // Check that the string is an ISO Language Code avoiding any injection. - if (!preg_match('/^[a-z]{2}(\-[A-Z]{2})?$/', $frontend_lang)) - { - $frontend_lang = 'en-GB'; - } - - // Attempt to set the default site language - if (!$model->setDefault($frontend_lang, 'site')) - { - // Create an error response message. - $app->enqueueMessage(Text::_('INSTL_DEFAULTLANGUAGE_FRONTEND_COULDNT_SET_DEFAULT'), 'error'); - } - else - { - // Create a response body. - $app->enqueueMessage(Text::sprintf('INSTL_DEFAULTLANGUAGE_FRONTEND_SET_DEFAULT', $frontend_lang), 'message'); - } - - $r = new \stdClass; - - // Redirect to the final page. - $r->view = 'remove'; - $this->sendJsonResponse($r); - } + /** + * Sets the language. + * + * @return void + * + * @since 4.0.0 + */ + public function set() + { + $this->checkValidToken(); + + // Check for potentially unwritable session + $session = $this->app->getSession(); + + if ($session->isNew()) { + $this->sendJsonResponse(new \Exception(Text::_('INSTL_COOKIES_NOT_ENABLED'), 500)); + } + + /** @var SetupModel $model */ + $model = $this->getModel('Setup'); + + // Get the posted values from the request and validate them. + $data = $this->input->post->get('jform', [], 'array'); + $return = $model->validate($data, 'language'); + + $r = new \stdClass(); + + // Check for validation errors. + if ($return === false) { + /* + * The validate method enqueued all messages for us, so we just need to + * redirect back to the site setup screen. + */ + $r->view = $this->input->getWord('view', 'setup'); + $this->sendJsonResponse($r); + } + + // Store the options in the session. + $model->storeOptions($return); + + // Setup language + Factory::$language = Language::getInstance($return['language']); + + // Redirect to the page. + $r->view = $this->input->getWord('view', 'setup'); + + $this->sendJsonResponse($r); + } + + /** + * Sets the default language. + * + * @return void + * + * @since 4.0.0 + */ + public function setdefault() + { + $this->checkValidToken(); + + $app = $this->app; + + /** @var \Joomla\CMS\Installation\Model\LanguagesModel $model */ + $model = $this->getModel('Languages'); + + // Check for request forgeries in the administrator language + $admin_lang = $this->input->getString('administratorlang', false); + + // Check that the string is an ISO Language Code avoiding any injection. + if (!preg_match('/^[a-z]{2}(\-[A-Z]{2})?$/', $admin_lang)) { + $admin_lang = 'en-GB'; + } + + // Attempt to set the default administrator language + if (!$model->setDefault($admin_lang, 'administrator')) { + // Create an error response message. + $this->app->enqueueMessage(Text::_('INSTL_DEFAULTLANGUAGE_ADMIN_COULDNT_SET_DEFAULT'), 'error'); + } else { + // Create a response body. + $app->enqueueMessage(Text::sprintf('INSTL_DEFAULTLANGUAGE_ADMIN_SET_DEFAULT', $admin_lang), 'message'); + } + + // Check for request forgeries in the site language + $frontend_lang = $this->input->getString('frontendlang', false); + + // Check that the string is an ISO Language Code avoiding any injection. + if (!preg_match('/^[a-z]{2}(\-[A-Z]{2})?$/', $frontend_lang)) { + $frontend_lang = 'en-GB'; + } + + // Attempt to set the default site language + if (!$model->setDefault($frontend_lang, 'site')) { + // Create an error response message. + $app->enqueueMessage(Text::_('INSTL_DEFAULTLANGUAGE_FRONTEND_COULDNT_SET_DEFAULT'), 'error'); + } else { + // Create a response body. + $app->enqueueMessage(Text::sprintf('INSTL_DEFAULTLANGUAGE_FRONTEND_SET_DEFAULT', $frontend_lang), 'message'); + } + + $r = new \stdClass(); + + // Redirect to the final page. + $r->view = 'remove'; + $this->sendJsonResponse($r); + } } diff --git a/installation/src/Error/Renderer/JsonRenderer.php b/installation/src/Error/Renderer/JsonRenderer.php index 72c0438eec900..dd3ab0f273e37 100644 --- a/installation/src/Error/Renderer/JsonRenderer.php +++ b/installation/src/Error/Renderer/JsonRenderer.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.2.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $value = $this->getNativeLanguage(); - - return parent::setup($element, $value, $group); - } - - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 1.6 - */ - protected function getOptions() - { - $native = $this->getNativeLanguage(); - - // Get the list of available languages. - $options = LanguageHelper::createLanguageList($native); - - // Fix wrongly set parentheses in RTL languages - if (Factory::getLanguage()->isRtl()) - { - foreach ($options as &$option) - { - $option['text'] .= '‎'; - } - } - - if (!$options || $options instanceof \Exception) - { - $options = array(); - } - // Sort languages by name - else - { - usort($options, array($this, '_sortLanguages')); - } - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); - - return $options; - } - - /** - * Method to sort languages by name. - * - * @param array $a The first value to determine sort - * @param array $b The second value to determine sort - * - * @return integer - * - * @since 3.1 - */ - protected function _sortLanguages($a, $b) - { - return strcmp($a['text'], $b['text']); - } - - /** - * Determinate the native language to select - * - * @return string The native language to use - * - * @since 4.2.0 - */ - protected function getNativeLanguage() - { - static $native; - - if (isset($native)) - { - return $native; - } - - $app = Factory::getApplication(); - - // Detect the native language. - $native = LanguageHelper::detectLanguage(); - - if (empty($native)) - { - $native = 'en-GB'; - } - - // Get a forced language if it exists. - $forced = $app->getLocalise(); - - if (!empty($forced['language'])) - { - $native = $forced['language']; - } - - // If a language is already set in the session, use this instead - $model = new SetupModel; - $options = $model->getOptions(); - - if (isset($options['language'])) - { - $native = $options['language']; - } - - return $native; - } + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'Language'; + + /** + * Method to attach a Form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.2.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $value = $this->getNativeLanguage(); + + return parent::setup($element, $value, $group); + } + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 1.6 + */ + protected function getOptions() + { + $native = $this->getNativeLanguage(); + + // Get the list of available languages. + $options = LanguageHelper::createLanguageList($native); + + // Fix wrongly set parentheses in RTL languages + if (Factory::getLanguage()->isRtl()) { + foreach ($options as &$option) { + $option['text'] .= '‎'; + } + } + + if (!$options || $options instanceof \Exception) { + $options = array(); + } + // Sort languages by name + else { + usort($options, array($this, '_sortLanguages')); + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); + + return $options; + } + + /** + * Method to sort languages by name. + * + * @param array $a The first value to determine sort + * @param array $b The second value to determine sort + * + * @return integer + * + * @since 3.1 + */ + protected function _sortLanguages($a, $b) + { + return strcmp($a['text'], $b['text']); + } + + /** + * Determinate the native language to select + * + * @return string The native language to use + * + * @since 4.2.0 + */ + protected function getNativeLanguage() + { + static $native; + + if (isset($native)) { + return $native; + } + + $app = Factory::getApplication(); + + // Detect the native language. + $native = LanguageHelper::detectLanguage(); + + if (empty($native)) { + $native = 'en-GB'; + } + + // Get a forced language if it exists. + $forced = $app->getLocalise(); + + if (!empty($forced['language'])) { + $native = $forced['language']; + } + + // If a language is already set in the session, use this instead + $model = new SetupModel(); + $options = $model->getOptions(); + + if (isset($options['language'])) { + $native = $options['language']; + } + + return $native; + } } diff --git a/installation/src/Form/Field/Installation/PrefixField.php b/installation/src/Form/Field/Installation/PrefixField.php index 272150ee895db..7e6c1a5343dbc 100644 --- a/installation/src/Form/Field/Installation/PrefixField.php +++ b/installation/src/Form/Field/Installation/PrefixField.php @@ -1,4 +1,5 @@ element['size'] ? abs((int) $this->element['size']) : 5; - $maxLength = $this->element['maxlength'] ? ' maxlength="' . (int) $this->element['maxlength'] . '"' : ''; - $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; - $readonly = (string) $this->element['readonly'] === 'true' ? ' readonly="readonly"' : ''; - $disabled = (string) $this->element['disabled'] === 'true' ? ' disabled="disabled"' : ''; + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + // Initialize some field attributes. + $size = $this->element['size'] ? abs((int) $this->element['size']) : 5; + $maxLength = $this->element['maxlength'] ? ' maxlength="' . (int) $this->element['maxlength'] . '"' : ''; + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + $readonly = (string) $this->element['readonly'] === 'true' ? ' readonly="readonly"' : ''; + $disabled = (string) $this->element['disabled'] === 'true' ? ' disabled="disabled"' : ''; - // Make sure somebody doesn't put in a too large prefix size value. - if ($size > 10) - { - $size = 10; - } + // Make sure somebody doesn't put in a too large prefix size value. + if ($size > 10) { + $size = 10; + } - // If a prefix is already set, use it instead. - $session = Factory::getSession()->get('setup.options', array()); + // If a prefix is already set, use it instead. + $session = Factory::getSession()->get('setup.options', array()); - if (empty($session['db_prefix'])) - { - // Create the random prefix. - $prefix = ''; - $chars = range('a', 'z'); - $numbers = range(0, 9); + if (empty($session['db_prefix'])) { + // Create the random prefix. + $prefix = ''; + $chars = range('a', 'z'); + $numbers = range(0, 9); - // We want the fist character to be a random letter. - shuffle($chars); - $prefix .= $chars[0]; + // We want the fist character to be a random letter. + shuffle($chars); + $prefix .= $chars[0]; - // Next we combine the numbers and characters to get the other characters. - $symbols = array_merge($numbers, $chars); - shuffle($symbols); + // Next we combine the numbers and characters to get the other characters. + $symbols = array_merge($numbers, $chars); + shuffle($symbols); - for ($i = 0, $j = $size - 1; $i < $j; ++$i) - { - $prefix .= $symbols[$i]; - } + for ($i = 0, $j = $size - 1; $i < $j; ++$i) { + $prefix .= $symbols[$i]; + } - // Add in the underscore. - $prefix .= '_'; - } - else - { - $prefix = $session['db_prefix']; - } + // Add in the underscore. + $prefix .= '_'; + } else { + $prefix = $session['db_prefix']; + } - // Initialize JavaScript field attributes. - $onchange = $this->element['onchange'] ? ' onchange="' . (string) $this->element['onchange'] . '"' : ''; + // Initialize JavaScript field attributes. + $onchange = $this->element['onchange'] ? ' onchange="' . (string) $this->element['onchange'] . '"' : ''; - return ''; - } + return ''; + } } diff --git a/installation/src/Form/Rule/PrefixRule.php b/installation/src/Form/Rule/PrefixRule.php index 6bb6e98c4a660..fa9041abb4cf3 100644 --- a/installation/src/Form/Rule/PrefixRule.php +++ b/installation/src/Form/Rule/PrefixRule.php @@ -1,4 +1,5 @@ tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string|null $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry|null $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form|null $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise. - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - $filterInput = InputFilter::getInstance(); + /** + * Method to test a username + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string|null $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry|null $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form|null $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise. + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + $filterInput = InputFilter::getInstance(); - if (preg_match('#[<>"\'%;()&\\\\]|\\.\\./#', $value) || strlen(utf8_decode($value)) < 2 - || $filterInput->clean($value, 'TRIM') !== $value - || strlen(utf8_decode($value)) > $element['size']) - { - return false; - } + if ( + preg_match('#[<>"\'%;()&\\\\]|\\.\\./#', $value) || strlen(utf8_decode($value)) < 2 + || $filterInput->clean($value, 'TRIM') !== $value + || strlen(utf8_decode($value)) > $element['size'] + ) { + return false; + } - return true; - } + return true; + } } diff --git a/installation/src/Helper/DatabaseHelper.php b/installation/src/Helper/DatabaseHelper.php index 5ffe9c658ec54..95afb0b839d21 100644 --- a/installation/src/Helper/DatabaseHelper.php +++ b/installation/src/Helper/DatabaseHelper.php @@ -1,4 +1,5 @@ $driver, - 'host' => $host, - 'user' => $user, - 'password' => $password, - 'database' => $database, - 'prefix' => $prefix, - 'select' => $select, - ]; - - if (!empty($ssl['dbencryption'])) - { - $options['ssl'] = [ - 'enable' => true, - 'verify_server_cert' => (bool) $ssl['dbsslverifyservercert'], - ]; - - foreach (['cipher', 'ca', 'key', 'cert'] as $value) - { - $confVal = trim($ssl['dbssl' . $value]); - - if ($confVal !== '') - { - $options['ssl'][$value] = $confVal; - } - } - } - - // Enable utf8mb4 connections for mysql adapters - if (strtolower($driver) === 'mysqli') - { - $options['utf8mb4'] = true; - } - - if (strtolower($driver) === 'mysql') - { - $options['charset'] = 'utf8mb4'; - } - - // Get a database object. - $db = DatabaseDriver::getInstance($options); - } - - return $db; - } - - /** - * Convert encryption options to array. - * - * @param \stdClass $options The session options - * - * @return array The encryption settings - * - * @since 4.0.0 - */ - public static function getEncryptionSettings($options) - { - return [ - 'dbencryption' => $options->db_encryption, - 'dbsslverifyservercert' => $options->db_sslverifyservercert, - 'dbsslkey' => $options->db_sslkey, - 'dbsslcert' => $options->db_sslcert, - 'dbsslca' => $options->db_sslca, - 'dbsslcipher' => $options->db_sslcipher, - ]; - } - - /** - * Get the minimum required database server version. - * - * @param DatabaseDriver $db Database object - * @param \stdClass $options The session options - * - * @return string The minimum required database server version. - * - * @since 4.0.0 - */ - public static function getMinimumServerVersion($db, $options) - { - // Get minimum database version required by the database driver - $minDbVersionRequired = $db->getMinimum(); - - // Get minimum database version required by the CMS - if (in_array($options->db_type, ['mysql', 'mysqli'])) - { - if ($db->isMariaDb()) - { - $minDbVersionCms = self::$dbMinimumMariaDb; - } - else - { - $minDbVersionCms = self::$dbMinimumMySql; - } - } - else - { - $minDbVersionCms = self::$dbMinimumPostgreSql; - } - - // Use most restrictive, i.e. largest minimum database version requirement - if (version_compare($minDbVersionCms, $minDbVersionRequired) > 0) - { - $minDbVersionRequired = $minDbVersionCms; - } - - return $minDbVersionRequired; - } - - /** - * Validate and clean up database connection parameters. - * - * @param \stdClass $options The session options - * - * @return string|boolean A string with the translated error message if - * validation error, otherwise false. - * - * @since 4.0.0 - */ - public static function validateConnectionParameters($options) - { - // Ensure a database type was selected. - if (empty($options->db_type)) - { - return Text::_('INSTL_DATABASE_INVALID_TYPE'); - } - - // Ensure that a hostname and user name were input. - if (empty($options->db_host) || empty($options->db_user)) - { - return Text::_('INSTL_DATABASE_INVALID_DB_DETAILS'); - } - - // Ensure that a database name is given. - if (empty($options->db_name)) - { - return Text::_('INSTL_DATABASE_EMPTY_NAME'); - } - - // Validate length of database name. - if (strlen($options->db_name) > 64) - { - return Text::_('INSTL_DATABASE_NAME_TOO_LONG'); - } - - // Validate database table prefix. - if (empty($options->db_prefix) || !preg_match('#^[a-zA-Z]+[a-zA-Z0-9_]*$#', $options->db_prefix)) - { - return Text::_('INSTL_DATABASE_PREFIX_MSG'); - } - - // Validate length of database table prefix. - if (strlen($options->db_prefix) > 15) - { - return Text::_('INSTL_DATABASE_FIX_TOO_LONG'); - } - - // Validate database name. - if (in_array($options->db_type, ['pgsql', 'postgresql']) && !preg_match('#^[a-zA-Z_][0-9a-zA-Z_$]*$#', $options->db_name)) - { - return Text::_('INSTL_DATABASE_NAME_MSG_POSTGRES'); - } - - if (in_array($options->db_type, ['mysql', 'mysqli']) && preg_match('#[\\\\\/]#', $options->db_name)) - { - return Text::_('INSTL_DATABASE_NAME_MSG_MYSQL'); - } - - // Workaround for UPPERCASE table prefix for postgresql - if (in_array($options->db_type, ['pgsql', 'postgresql'])) - { - if (strtolower($options->db_prefix) != $options->db_prefix) - { - return Text::_('INSTL_DATABASE_FIX_LOWERCASE'); - } - } - - // Validate and clean up database connection encryption options - $optionsChanged = false; - - if ($options->db_encryption === 0) - { - // Reset unused options - if (!empty($options->db_sslkey)) - { - $options->db_sslkey = ''; - $optionsChanged = true; - } - - if (!empty($options->db_sslcert)) - { - $options->db_sslcert = ''; - $optionsChanged = true; - } - - if ($options->db_sslverifyservercert) - { - $options->db_sslverifyservercert = false; - $optionsChanged = true; - } - - if (!empty($options->db_sslca)) - { - $options->db_sslca = ''; - $optionsChanged = true; - } - - if (!empty($options->db_sslcipher)) - { - $options->db_sslcipher = ''; - $optionsChanged = true; - } - } - else - { - // Check localhost - if (strtolower($options->db_host) === 'localhost') - { - return Text::_('INSTL_DATABASE_ENCRYPTION_MSG_LOCALHOST'); - } - - // Check CA file and folder depending on database type if server certificate verification - if ($options->db_sslverifyservercert) - { - if (empty($options->db_sslca)) - { - return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_EMPTY', Text::_('INSTL_DATABASE_ENCRYPTION_CA_LABEL')); - } - - if (!File::exists(Path::clean($options->db_sslca))) - { - return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_BAD', Text::_('INSTL_DATABASE_ENCRYPTION_CA_LABEL')); - } - } - else - { - // Reset unused option - if (!empty($options->db_sslca)) - { - $options->db_sslca = ''; - $optionsChanged = true; - } - } - - // Check key and certificate if two-way encryption - if ($options->db_encryption === 2) - { - if (empty($options->db_sslkey)) - { - return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_EMPTY', Text::_('INSTL_DATABASE_ENCRYPTION_KEY_LABEL')); - } - - if (!File::exists(Path::clean($options->db_sslkey))) - { - return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_BAD', Text::_('INSTL_DATABASE_ENCRYPTION_KEY_LABEL')); - } - - if (empty($options->db_sslcert)) - { - return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_EMPTY', Text::_('INSTL_DATABASE_ENCRYPTION_CERT_LABEL')); - } - - if (!File::exists(Path::clean($options->db_sslcert))) - { - return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_BAD', Text::_('INSTL_DATABASE_ENCRYPTION_CERT_LABEL')); - } - } - else - { - // Reset unused options - if (!empty($options->db_sslkey)) - { - $options->db_sslkey = ''; - $optionsChanged = true; - } - - if (!empty($options->db_sslcert)) - { - $options->db_sslcert = ''; - $optionsChanged = true; - } - } - } - - // Save options to session data if changed - if ($optionsChanged) - { - $optsArr = ArrayHelper::fromObject($options); - Factory::getSession()->set('setup.options', $optsArr); - } - - return false; - } - - /** - * Security check for remote db hosts - * - * @param \stdClass $options The session options - * - * @return boolean True if passed, otherwise false. - * - * @since 4.0.0 - */ - public static function checkRemoteDbHost($options) - { - // Security check for remote db hosts: Check env var if disabled - $shouldCheckLocalhost = getenv('JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK') !== '1'; - - // Per default allowed DB hosts: localhost / 127.0.0.1 / ::1 (optionally with port) - $localhost = '/^(((localhost|127\.0\.0\.1|\[\:\:1\])(\:[1-9]{1}[0-9]{0,4})?)|(\:\:1))$/'; - - // Check the security file if the db_host is not localhost / 127.0.0.1 / ::1 - if ($shouldCheckLocalhost && preg_match($localhost, $options->db_host) !== 1) - { - $remoteDbFileTestsPassed = Factory::getSession()->get('remoteDbFileTestsPassed', false); - - // When all checks have been passed we don't need to do this here again. - if ($remoteDbFileTestsPassed === false) - { - $generalRemoteDatabaseMessage = Text::sprintf( - 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_GENERAL_MESSAGE', - 'https://docs.joomla.org/Special:MyLanguage/J3.x:Secured_procedure_for_installing_Joomla_with_a_remote_database' - ); - - $remoteDbFile = Factory::getSession()->get('remoteDbFile', false); - - if ($remoteDbFile === false) - { - // Add the general message - Factory::getApplication()->enqueueMessage($generalRemoteDatabaseMessage, 'warning'); - - // This is the file you need to remove if you want to use a remote database - $remoteDbFile = '_Joomla' . UserHelper::genRandomPassword(21) . '.txt'; - Factory::getSession()->set('remoteDbFile', $remoteDbFile); - - // Get the path - $remoteDbPath = JPATH_INSTALLATION . '/' . $remoteDbFile; - - // When the path is not writable the user needs to create the file manually - if (!File::write($remoteDbPath, '')) - { - // Request to create the file manually - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_CREATE_FILE', - $remoteDbFile, - 'installation', - Text::_('INSTL_INSTALL_JOOMLA') - ), - 'notice' - ); - - Factory::getSession()->set('remoteDbFileUnwritable', true); - - return false; - } - - // Save the file name to the session - Factory::getSession()->set('remoteDbFileWrittenByJoomla', true); - - // Request to delete that file - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_DELETE_FILE', - $remoteDbFile, - 'installation', - Text::_('INSTL_INSTALL_JOOMLA') - ), - 'notice' - ); - - return false; - } - - if (Factory::getSession()->get('remoteDbFileWrittenByJoomla', false) === true - && File::exists(JPATH_INSTALLATION . '/' . $remoteDbFile)) - { - // Add the general message - Factory::getApplication()->enqueueMessage($generalRemoteDatabaseMessage, 'warning'); - - // Request to delete the file - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_DELETE_FILE', - $remoteDbFile, - 'installation', - Text::_('INSTL_INSTALL_JOOMLA') - ), - 'notice' - ); - - return false; - } - - if (Factory::getSession()->get('remoteDbFileUnwritable', false) === true && !File::exists(JPATH_INSTALLATION . '/' . $remoteDbFile)) - { - // Add the general message - Factory::getApplication()->enqueueMessage($generalRemoteDatabaseMessage, 'warning'); - - // Request to create the file manually - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_CREATE_FILE', - $remoteDbFile, - 'installation', - Text::_('INSTL_INSTALL_JOOMLA') - ), - 'notice' - ); - - return false; - } - - // All tests for this session passed set it to the session - Factory::getSession()->set('remoteDbFileTestsPassed', true); - } - } - - return true; - } - - /** - * Check database server parameters after connection - * - * @param DatabaseDriver $db Database object - * @param \stdClass $options The session options - * - * @return string|boolean A string with the translated error message if - * some server parameter is not ok, otherwise false. - * - * @since 4.0.0 - */ - public static function checkDbServerParameters($db, $options) - { - $dbVersion = $db->getVersion(); - - // Get required database version - $minDbVersionRequired = self::getMinimumServerVersion($db, $options); - - // Check minimum database version - if (version_compare($dbVersion, $minDbVersionRequired) < 0) - { - if (in_array($options->db_type, ['mysql', 'mysqli']) && $db->isMariaDb()) - { - $errorMessage = Text::sprintf( - 'INSTL_DATABASE_INVALID_MARIADB_VERSION', - $minDbVersionRequired, - $dbVersion - ); - } - else - { - $errorMessage = Text::sprintf( - 'INSTL_DATABASE_INVALID_' . strtoupper($options->db_type) . '_VERSION', - $minDbVersionRequired, - $dbVersion - ); - } - - return $errorMessage; - } - - // Check database connection encryption - if ($options->db_encryption !== 0 && empty($db->getConnectionEncryption())) - { - if ($db->isConnectionEncryptionSupported()) - { - $errorMessage = Text::_('INSTL_DATABASE_ENCRYPTION_MSG_CONN_NOT_ENCRYPT'); - } - else - { - $errorMessage = Text::_('INSTL_DATABASE_ENCRYPTION_MSG_SRV_NOT_SUPPORTS'); - } - - return $errorMessage; - } - - return false; - } + /** + * The minimum database server version for MariaDB databases as required by the CMS. + * This is not necessarily equal to what the database driver requires. + * + * @var string + * @since 4.0.0 + */ + protected static $dbMinimumMariaDb = '10.1'; + + /** + * The minimum database server version for MySQL databases as required by the CMS. + * This is not necessarily equal to what the database driver requires. + * + * @var string + * @since 4.0.0 + */ + protected static $dbMinimumMySql = '5.6'; + + /** + * The minimum database server version for PostgreSQL databases as required by the CMS. + * This is not necessarily equal to what the database driver requires. + * + * @var string + * @since 4.0.0 + */ + protected static $dbMinimumPostgreSql = '11.0'; + + /** + * Method to get a database driver. + * + * @param string $driver The database driver to use. + * @param string $host The hostname to connect on. + * @param string $user The user name to connect with. + * @param string $password The password to use for connection authentication. + * @param string $database The database to use. + * @param string $prefix The table prefix to use. + * @param boolean $select True if the database should be selected. + * @param array $ssl Database TLS connection options. + * + * @return DatabaseInterface + * + * @since 1.6 + */ + public static function getDbo($driver, $host, $user, $password, $database, $prefix, $select = true, array $ssl = []) + { + static $db; + + if (!$db) { + // Build the connection options array. + $options = [ + 'driver' => $driver, + 'host' => $host, + 'user' => $user, + 'password' => $password, + 'database' => $database, + 'prefix' => $prefix, + 'select' => $select, + ]; + + if (!empty($ssl['dbencryption'])) { + $options['ssl'] = [ + 'enable' => true, + 'verify_server_cert' => (bool) $ssl['dbsslverifyservercert'], + ]; + + foreach (['cipher', 'ca', 'key', 'cert'] as $value) { + $confVal = trim($ssl['dbssl' . $value]); + + if ($confVal !== '') { + $options['ssl'][$value] = $confVal; + } + } + } + + // Enable utf8mb4 connections for mysql adapters + if (strtolower($driver) === 'mysqli') { + $options['utf8mb4'] = true; + } + + if (strtolower($driver) === 'mysql') { + $options['charset'] = 'utf8mb4'; + } + + // Get a database object. + $db = DatabaseDriver::getInstance($options); + } + + return $db; + } + + /** + * Convert encryption options to array. + * + * @param \stdClass $options The session options + * + * @return array The encryption settings + * + * @since 4.0.0 + */ + public static function getEncryptionSettings($options) + { + return [ + 'dbencryption' => $options->db_encryption, + 'dbsslverifyservercert' => $options->db_sslverifyservercert, + 'dbsslkey' => $options->db_sslkey, + 'dbsslcert' => $options->db_sslcert, + 'dbsslca' => $options->db_sslca, + 'dbsslcipher' => $options->db_sslcipher, + ]; + } + + /** + * Get the minimum required database server version. + * + * @param DatabaseDriver $db Database object + * @param \stdClass $options The session options + * + * @return string The minimum required database server version. + * + * @since 4.0.0 + */ + public static function getMinimumServerVersion($db, $options) + { + // Get minimum database version required by the database driver + $minDbVersionRequired = $db->getMinimum(); + + // Get minimum database version required by the CMS + if (in_array($options->db_type, ['mysql', 'mysqli'])) { + if ($db->isMariaDb()) { + $minDbVersionCms = self::$dbMinimumMariaDb; + } else { + $minDbVersionCms = self::$dbMinimumMySql; + } + } else { + $minDbVersionCms = self::$dbMinimumPostgreSql; + } + + // Use most restrictive, i.e. largest minimum database version requirement + if (version_compare($minDbVersionCms, $minDbVersionRequired) > 0) { + $minDbVersionRequired = $minDbVersionCms; + } + + return $minDbVersionRequired; + } + + /** + * Validate and clean up database connection parameters. + * + * @param \stdClass $options The session options + * + * @return string|boolean A string with the translated error message if + * validation error, otherwise false. + * + * @since 4.0.0 + */ + public static function validateConnectionParameters($options) + { + // Ensure a database type was selected. + if (empty($options->db_type)) { + return Text::_('INSTL_DATABASE_INVALID_TYPE'); + } + + // Ensure that a hostname and user name were input. + if (empty($options->db_host) || empty($options->db_user)) { + return Text::_('INSTL_DATABASE_INVALID_DB_DETAILS'); + } + + // Ensure that a database name is given. + if (empty($options->db_name)) { + return Text::_('INSTL_DATABASE_EMPTY_NAME'); + } + + // Validate length of database name. + if (strlen($options->db_name) > 64) { + return Text::_('INSTL_DATABASE_NAME_TOO_LONG'); + } + + // Validate database table prefix. + if (empty($options->db_prefix) || !preg_match('#^[a-zA-Z]+[a-zA-Z0-9_]*$#', $options->db_prefix)) { + return Text::_('INSTL_DATABASE_PREFIX_MSG'); + } + + // Validate length of database table prefix. + if (strlen($options->db_prefix) > 15) { + return Text::_('INSTL_DATABASE_FIX_TOO_LONG'); + } + + // Validate database name. + if (in_array($options->db_type, ['pgsql', 'postgresql']) && !preg_match('#^[a-zA-Z_][0-9a-zA-Z_$]*$#', $options->db_name)) { + return Text::_('INSTL_DATABASE_NAME_MSG_POSTGRES'); + } + + if (in_array($options->db_type, ['mysql', 'mysqli']) && preg_match('#[\\\\\/]#', $options->db_name)) { + return Text::_('INSTL_DATABASE_NAME_MSG_MYSQL'); + } + + // Workaround for UPPERCASE table prefix for postgresql + if (in_array($options->db_type, ['pgsql', 'postgresql'])) { + if (strtolower($options->db_prefix) != $options->db_prefix) { + return Text::_('INSTL_DATABASE_FIX_LOWERCASE'); + } + } + + // Validate and clean up database connection encryption options + $optionsChanged = false; + + if ($options->db_encryption === 0) { + // Reset unused options + if (!empty($options->db_sslkey)) { + $options->db_sslkey = ''; + $optionsChanged = true; + } + + if (!empty($options->db_sslcert)) { + $options->db_sslcert = ''; + $optionsChanged = true; + } + + if ($options->db_sslverifyservercert) { + $options->db_sslverifyservercert = false; + $optionsChanged = true; + } + + if (!empty($options->db_sslca)) { + $options->db_sslca = ''; + $optionsChanged = true; + } + + if (!empty($options->db_sslcipher)) { + $options->db_sslcipher = ''; + $optionsChanged = true; + } + } else { + // Check localhost + if (strtolower($options->db_host) === 'localhost') { + return Text::_('INSTL_DATABASE_ENCRYPTION_MSG_LOCALHOST'); + } + + // Check CA file and folder depending on database type if server certificate verification + if ($options->db_sslverifyservercert) { + if (empty($options->db_sslca)) { + return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_EMPTY', Text::_('INSTL_DATABASE_ENCRYPTION_CA_LABEL')); + } + + if (!File::exists(Path::clean($options->db_sslca))) { + return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_BAD', Text::_('INSTL_DATABASE_ENCRYPTION_CA_LABEL')); + } + } else { + // Reset unused option + if (!empty($options->db_sslca)) { + $options->db_sslca = ''; + $optionsChanged = true; + } + } + + // Check key and certificate if two-way encryption + if ($options->db_encryption === 2) { + if (empty($options->db_sslkey)) { + return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_EMPTY', Text::_('INSTL_DATABASE_ENCRYPTION_KEY_LABEL')); + } + + if (!File::exists(Path::clean($options->db_sslkey))) { + return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_BAD', Text::_('INSTL_DATABASE_ENCRYPTION_KEY_LABEL')); + } + + if (empty($options->db_sslcert)) { + return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_EMPTY', Text::_('INSTL_DATABASE_ENCRYPTION_CERT_LABEL')); + } + + if (!File::exists(Path::clean($options->db_sslcert))) { + return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_BAD', Text::_('INSTL_DATABASE_ENCRYPTION_CERT_LABEL')); + } + } else { + // Reset unused options + if (!empty($options->db_sslkey)) { + $options->db_sslkey = ''; + $optionsChanged = true; + } + + if (!empty($options->db_sslcert)) { + $options->db_sslcert = ''; + $optionsChanged = true; + } + } + } + + // Save options to session data if changed + if ($optionsChanged) { + $optsArr = ArrayHelper::fromObject($options); + Factory::getSession()->set('setup.options', $optsArr); + } + + return false; + } + + /** + * Security check for remote db hosts + * + * @param \stdClass $options The session options + * + * @return boolean True if passed, otherwise false. + * + * @since 4.0.0 + */ + public static function checkRemoteDbHost($options) + { + // Security check for remote db hosts: Check env var if disabled + $shouldCheckLocalhost = getenv('JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK') !== '1'; + + // Per default allowed DB hosts: localhost / 127.0.0.1 / ::1 (optionally with port) + $localhost = '/^(((localhost|127\.0\.0\.1|\[\:\:1\])(\:[1-9]{1}[0-9]{0,4})?)|(\:\:1))$/'; + + // Check the security file if the db_host is not localhost / 127.0.0.1 / ::1 + if ($shouldCheckLocalhost && preg_match($localhost, $options->db_host) !== 1) { + $remoteDbFileTestsPassed = Factory::getSession()->get('remoteDbFileTestsPassed', false); + + // When all checks have been passed we don't need to do this here again. + if ($remoteDbFileTestsPassed === false) { + $generalRemoteDatabaseMessage = Text::sprintf( + 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_GENERAL_MESSAGE', + 'https://docs.joomla.org/Special:MyLanguage/J3.x:Secured_procedure_for_installing_Joomla_with_a_remote_database' + ); + + $remoteDbFile = Factory::getSession()->get('remoteDbFile', false); + + if ($remoteDbFile === false) { + // Add the general message + Factory::getApplication()->enqueueMessage($generalRemoteDatabaseMessage, 'warning'); + + // This is the file you need to remove if you want to use a remote database + $remoteDbFile = '_Joomla' . UserHelper::genRandomPassword(21) . '.txt'; + Factory::getSession()->set('remoteDbFile', $remoteDbFile); + + // Get the path + $remoteDbPath = JPATH_INSTALLATION . '/' . $remoteDbFile; + + // When the path is not writable the user needs to create the file manually + if (!File::write($remoteDbPath, '')) { + // Request to create the file manually + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_CREATE_FILE', + $remoteDbFile, + 'installation', + Text::_('INSTL_INSTALL_JOOMLA') + ), + 'notice' + ); + + Factory::getSession()->set('remoteDbFileUnwritable', true); + + return false; + } + + // Save the file name to the session + Factory::getSession()->set('remoteDbFileWrittenByJoomla', true); + + // Request to delete that file + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_DELETE_FILE', + $remoteDbFile, + 'installation', + Text::_('INSTL_INSTALL_JOOMLA') + ), + 'notice' + ); + + return false; + } + + if ( + Factory::getSession()->get('remoteDbFileWrittenByJoomla', false) === true + && File::exists(JPATH_INSTALLATION . '/' . $remoteDbFile) + ) { + // Add the general message + Factory::getApplication()->enqueueMessage($generalRemoteDatabaseMessage, 'warning'); + + // Request to delete the file + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_DELETE_FILE', + $remoteDbFile, + 'installation', + Text::_('INSTL_INSTALL_JOOMLA') + ), + 'notice' + ); + + return false; + } + + if (Factory::getSession()->get('remoteDbFileUnwritable', false) === true && !File::exists(JPATH_INSTALLATION . '/' . $remoteDbFile)) { + // Add the general message + Factory::getApplication()->enqueueMessage($generalRemoteDatabaseMessage, 'warning'); + + // Request to create the file manually + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_CREATE_FILE', + $remoteDbFile, + 'installation', + Text::_('INSTL_INSTALL_JOOMLA') + ), + 'notice' + ); + + return false; + } + + // All tests for this session passed set it to the session + Factory::getSession()->set('remoteDbFileTestsPassed', true); + } + } + + return true; + } + + /** + * Check database server parameters after connection + * + * @param DatabaseDriver $db Database object + * @param \stdClass $options The session options + * + * @return string|boolean A string with the translated error message if + * some server parameter is not ok, otherwise false. + * + * @since 4.0.0 + */ + public static function checkDbServerParameters($db, $options) + { + $dbVersion = $db->getVersion(); + + // Get required database version + $minDbVersionRequired = self::getMinimumServerVersion($db, $options); + + // Check minimum database version + if (version_compare($dbVersion, $minDbVersionRequired) < 0) { + if (in_array($options->db_type, ['mysql', 'mysqli']) && $db->isMariaDb()) { + $errorMessage = Text::sprintf( + 'INSTL_DATABASE_INVALID_MARIADB_VERSION', + $minDbVersionRequired, + $dbVersion + ); + } else { + $errorMessage = Text::sprintf( + 'INSTL_DATABASE_INVALID_' . strtoupper($options->db_type) . '_VERSION', + $minDbVersionRequired, + $dbVersion + ); + } + + return $errorMessage; + } + + // Check database connection encryption + if ($options->db_encryption !== 0 && empty($db->getConnectionEncryption())) { + if ($db->isConnectionEncryptionSupported()) { + $errorMessage = Text::_('INSTL_DATABASE_ENCRYPTION_MSG_CONN_NOT_ENCRYPT'); + } else { + $errorMessage = Text::_('INSTL_DATABASE_ENCRYPTION_MSG_SRV_NOT_SUPPORTS'); + } + + return $errorMessage; + } + + return false; + } } diff --git a/installation/src/Model/BaseInstallationModel.php b/installation/src/Model/BaseInstallationModel.php index 1a1c7a756b372..6ed3628bae09d 100644 --- a/installation/src/Model/BaseInstallationModel.php +++ b/installation/src/Model/BaseInstallationModel.php @@ -1,4 +1,5 @@ label = Text::_('INSTL_ZLIB_COMPRESSION_SUPPORT'); - $option->state = extension_loaded('zlib'); - $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_ZLIB_COMPRESSION_SUPPORT'); - $options[] = $option; - - // Check for XML support. - $option = new \stdClass; - $option->label = Text::_('INSTL_XML_SUPPORT'); - $option->state = extension_loaded('xml'); - $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_XML_SUPPORT'); - $options[] = $option; - - // Check for database support. - // We are satisfied if there is at least one database driver available. - $available = DatabaseDriver::getConnectors(); - $option = new \stdClass; - $option->label = Text::_('INSTL_DATABASE_SUPPORT'); - $option->label .= '
    (' . implode(', ', $available) . ')'; - $option->state = count($available); - $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_DATABASE_SUPPORT'); - $options[] = $option; - - // Check for mbstring options. - if (extension_loaded('mbstring')) - { - // Check for default MB language. - $option = new \stdClass; - $option->label = Text::_('INSTL_MB_LANGUAGE_IS_DEFAULT'); - $option->state = (strtolower(ini_get('mbstring.language')) == 'neutral'); - $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_MBLANG_NOTDEFAULT'); - $options[] = $option; - - // Check for MB function overload. - $option = new \stdClass; - $option->label = Text::_('INSTL_MB_STRING_OVERLOAD_OFF'); - $option->state = (ini_get('mbstring.func_overload') == 0); - $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_MBSTRING_OVERLOAD_OFF'); - $options[] = $option; - } - - // Check for a missing native parse_ini_file implementation. - $option = new \stdClass; - $option->label = Text::_('INSTL_PARSE_INI_FILE_AVAILABLE'); - $option->state = $this->getIniParserAvailability(); - $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_PARSE_INI_FILE_AVAILABLE'); - $options[] = $option; - - // Check for missing native json_encode / json_decode support. - $option = new \stdClass; - $option->label = Text::_('INSTL_JSON_SUPPORT_AVAILABLE'); - $option->state = function_exists('json_encode') && function_exists('json_decode'); - $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_JSON_SUPPORT_AVAILABLE'); - $options[] = $option; - - // Check for configuration file writable. - $writable = (is_writable(JPATH_CONFIGURATION . '/configuration.php') - || (!file_exists(JPATH_CONFIGURATION . '/configuration.php') && is_writable(JPATH_ROOT))); - - $option = new \stdClass; - $option->label = Text::sprintf('INSTL_WRITABLE', 'configuration.php'); - $option->state = $writable; - $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_NEEDSTOBEWRITABLE'); - $options[] = $option; - - return $options; - } - - /** - * Checks if all of the mandatory PHP options are met. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function getPhpOptionsSufficient() - { - $options = $this->getPhpOptions(); - - foreach ($options as $option) - { - if ($option->state === false) - { - $result = $option->state; - } - } - - return isset($result) ? false : true; - } - - /** - * Gets PHP Settings. - * - * @return array - * - * @since 3.1 - */ - public function getPhpSettings() - { - $settings = array(); - - // Check for display errors. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_DISPLAY_ERRORS'); - $setting->state = (bool) ini_get('display_errors'); - $setting->recommended = false; - $settings[] = $setting; - - // Check for file uploads. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_FILE_UPLOADS'); - $setting->state = (bool) ini_get('file_uploads'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for output buffering. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_OUTPUT_BUFFERING'); - $setting->state = (int) ini_get('output_buffering') !== 0; - $setting->recommended = false; - $settings[] = $setting; - - // Check for session auto-start. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_SESSION_AUTO_START'); - $setting->state = (bool) ini_get('session.auto_start'); - $setting->recommended = false; - $settings[] = $setting; - - // Check for native ZIP support. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_ZIP_SUPPORT_AVAILABLE'); - $setting->state = function_exists('zip_open') && function_exists('zip_read'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for GD support - $setting = new \stdClass; - $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'GD'); - $setting->state = extension_loaded('gd'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for iconv support - $setting = new \stdClass; - $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'iconv'); - $setting->state = function_exists('iconv'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for intl support - $setting = new \stdClass; - $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'intl'); - $setting->state = function_exists('transliterator_transliterate'); - $setting->recommended = true; - $settings[] = $setting; - - return $settings; - } - - /** - * Get the current setup options from the session. - * - * @return array An array of options from the session. - * - * @since 3.1 - */ - public function getOptions() - { - if (!empty(Factory::getSession()->get('setup.options', array()))) - { - return Factory::getSession()->get('setup.options', array()); - } - } - - /** - * Method to get the form. - * - * @param string|null $view The view being processed. - * - * @return Form|boolean Form object on success, false on failure. - * - * @since 3.1 - */ - public function getForm($view = null) - { - if (!$view) - { - $view = Factory::getApplication()->input->getWord('view', 'setup'); - } - - // Get the form. - Form::addFormPath(JPATH_COMPONENT . '/forms'); - - try - { - $form = Form::getInstance('jform', $view, array('control' => 'jform')); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // Check the session for previously entered form data. - $data = (array) $this->getOptions(); - - // Bind the form data if present. - if (!empty($data)) - { - $form->bind($data); - } - - return $form; - } + /** + * Checks the availability of the parse_ini_file and parse_ini_string functions. + * + * @return boolean True if the method exists. + * + * @since 3.1 + */ + public function getIniParserAvailability() + { + $disabled_functions = ini_get('disable_functions'); + + if (!empty($disabled_functions)) { + // Attempt to detect them in the PHP INI disable_functions variable. + $disabled_functions = explode(',', trim($disabled_functions)); + $number_of_disabled_functions = count($disabled_functions); + + for ($i = 0, $l = $number_of_disabled_functions; $i < $l; $i++) { + $disabled_functions[$i] = trim($disabled_functions[$i]); + } + + $result = !in_array('parse_ini_string', $disabled_functions); + } else { + // Attempt to detect their existence; even pure PHP implementation of them will trigger a positive response, though. + $result = function_exists('parse_ini_string'); + } + + return $result; + } + + /** + * Gets PHP options. + * + * @return array Array of PHP config options + * + * @since 3.1 + */ + public function getPhpOptions() + { + $options = []; + + // Check for zlib support. + $option = new \stdClass(); + $option->label = Text::_('INSTL_ZLIB_COMPRESSION_SUPPORT'); + $option->state = extension_loaded('zlib'); + $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_ZLIB_COMPRESSION_SUPPORT'); + $options[] = $option; + + // Check for XML support. + $option = new \stdClass(); + $option->label = Text::_('INSTL_XML_SUPPORT'); + $option->state = extension_loaded('xml'); + $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_XML_SUPPORT'); + $options[] = $option; + + // Check for database support. + // We are satisfied if there is at least one database driver available. + $available = DatabaseDriver::getConnectors(); + $option = new \stdClass(); + $option->label = Text::_('INSTL_DATABASE_SUPPORT'); + $option->label .= '
    (' . implode(', ', $available) . ')'; + $option->state = count($available); + $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_DATABASE_SUPPORT'); + $options[] = $option; + + // Check for mbstring options. + if (extension_loaded('mbstring')) { + // Check for default MB language. + $option = new \stdClass(); + $option->label = Text::_('INSTL_MB_LANGUAGE_IS_DEFAULT'); + $option->state = (strtolower(ini_get('mbstring.language')) == 'neutral'); + $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_MBLANG_NOTDEFAULT'); + $options[] = $option; + + // Check for MB function overload. + $option = new \stdClass(); + $option->label = Text::_('INSTL_MB_STRING_OVERLOAD_OFF'); + $option->state = (ini_get('mbstring.func_overload') == 0); + $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_MBSTRING_OVERLOAD_OFF'); + $options[] = $option; + } + + // Check for a missing native parse_ini_file implementation. + $option = new \stdClass(); + $option->label = Text::_('INSTL_PARSE_INI_FILE_AVAILABLE'); + $option->state = $this->getIniParserAvailability(); + $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_PARSE_INI_FILE_AVAILABLE'); + $options[] = $option; + + // Check for missing native json_encode / json_decode support. + $option = new \stdClass(); + $option->label = Text::_('INSTL_JSON_SUPPORT_AVAILABLE'); + $option->state = function_exists('json_encode') && function_exists('json_decode'); + $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_JSON_SUPPORT_AVAILABLE'); + $options[] = $option; + + // Check for configuration file writable. + $writable = (is_writable(JPATH_CONFIGURATION . '/configuration.php') + || (!file_exists(JPATH_CONFIGURATION . '/configuration.php') && is_writable(JPATH_ROOT))); + + $option = new \stdClass(); + $option->label = Text::sprintf('INSTL_WRITABLE', 'configuration.php'); + $option->state = $writable; + $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_NEEDSTOBEWRITABLE'); + $options[] = $option; + + return $options; + } + + /** + * Checks if all of the mandatory PHP options are met. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function getPhpOptionsSufficient() + { + $options = $this->getPhpOptions(); + + foreach ($options as $option) { + if ($option->state === false) { + $result = $option->state; + } + } + + return isset($result) ? false : true; + } + + /** + * Gets PHP Settings. + * + * @return array + * + * @since 3.1 + */ + public function getPhpSettings() + { + $settings = array(); + + // Check for display errors. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_DISPLAY_ERRORS'); + $setting->state = (bool) ini_get('display_errors'); + $setting->recommended = false; + $settings[] = $setting; + + // Check for file uploads. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_FILE_UPLOADS'); + $setting->state = (bool) ini_get('file_uploads'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for output buffering. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_OUTPUT_BUFFERING'); + $setting->state = (int) ini_get('output_buffering') !== 0; + $setting->recommended = false; + $settings[] = $setting; + + // Check for session auto-start. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_SESSION_AUTO_START'); + $setting->state = (bool) ini_get('session.auto_start'); + $setting->recommended = false; + $settings[] = $setting; + + // Check for native ZIP support. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_ZIP_SUPPORT_AVAILABLE'); + $setting->state = function_exists('zip_open') && function_exists('zip_read'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for GD support + $setting = new \stdClass(); + $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'GD'); + $setting->state = extension_loaded('gd'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for iconv support + $setting = new \stdClass(); + $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'iconv'); + $setting->state = function_exists('iconv'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for intl support + $setting = new \stdClass(); + $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'intl'); + $setting->state = function_exists('transliterator_transliterate'); + $setting->recommended = true; + $settings[] = $setting; + + return $settings; + } + + /** + * Get the current setup options from the session. + * + * @return array An array of options from the session. + * + * @since 3.1 + */ + public function getOptions() + { + if (!empty(Factory::getSession()->get('setup.options', array()))) { + return Factory::getSession()->get('setup.options', array()); + } + } + + /** + * Method to get the form. + * + * @param string|null $view The view being processed. + * + * @return Form|boolean Form object on success, false on failure. + * + * @since 3.1 + */ + public function getForm($view = null) + { + if (!$view) { + $view = Factory::getApplication()->input->getWord('view', 'setup'); + } + + // Get the form. + Form::addFormPath(JPATH_COMPONENT . '/forms'); + + try { + $form = Form::getInstance('jform', $view, array('control' => 'jform')); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // Check the session for previously entered form data. + $data = (array) $this->getOptions(); + + // Bind the form data if present. + if (!empty($data)) { + $form->bind($data); + } + + return $form; + } } diff --git a/installation/src/Model/CleanupModel.php b/installation/src/Model/CleanupModel.php index 6065faf799924..bb040d1c948c0 100644 --- a/installation/src/Model/CleanupModel.php +++ b/installation/src/Model/CleanupModel.php @@ -1,4 +1,5 @@ db_type, - $options->db_host, - $options->db_user, - $options->db_pass_plain, - $options->db_name, - $options->db_prefix, - true, - DatabaseHelper::getEncryptionSettings($options) - ); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_ERROR_CONNECT_DB', $e->getMessage()), 'error'); - - return false; - } - - // Attempt to create the configuration. - if (!$this->createConfiguration($options)) - { - return false; - } - - $serverType = $db->getServerType(); - - // Attempt to update the table #__schema. - $pathPart = JPATH_ADMINISTRATOR . '/components/com_admin/sql/updates/' . $serverType . '/'; - - $files = Folder::files($pathPart, '\.sql$'); - - if (empty($files)) - { - Factory::getApplication()->enqueueMessage(Text::_('INSTL_ERROR_INITIALISE_SCHEMA'), 'error'); - - return false; - } - - $version = ''; - - foreach ($files as $file) - { - if (version_compare($version, File::stripExt($file)) < 0) - { - $version = File::stripExt($file); - } - } - - $query = $db->getQuery(true) - ->select('extension_id') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('name') . ' = ' . $db->quote('files_joomla')); - $db->setQuery($query); - $eid = $db->loadResult(); - - $query->clear() - ->insert($db->quoteName('#__schemas')) - ->columns( - array( - $db->quoteName('extension_id'), - $db->quoteName('version_id') - ) - ) - ->values($eid . ', ' . $db->quote($version)); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // Attempt to refresh manifest caches. - $query->clear() - ->select('*') - ->from('#__extensions'); - $db->setQuery($query); - - $return = true; - - try - { - $extensions = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - $return = false; - } - - // This is needed because the installer loads the extension table in constructor, needs to be refactored in 5.0 - Factory::$database = $db; - $installer = Installer::getInstance(); - - foreach ($extensions as $extension) - { - if (!$installer->refreshManifestCache($extension->extension_id)) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf('INSTL_DATABASE_COULD_NOT_REFRESH_MANIFEST_CACHE', $extension->name), - 'error' - ); - - return false; - } - } - - // Handle default backend language setting. This feature is available for localized versions of Joomla. - $languages = Factory::getApplication()->getLocaliseAdmin($db); - - if (in_array($options->language, $languages['admin']) || in_array($options->language, $languages['site'])) - { - // Build the language parameters for the language manager. - $params = array(); - - // Set default administrator/site language to sample data values. - $params['administrator'] = 'en-GB'; - $params['site'] = 'en-GB'; - - if (in_array($options->language, $languages['admin'])) - { - $params['administrator'] = $options->language; - } - - if (in_array($options->language, $languages['site'])) - { - $params['site'] = $options->language; - } - - $params = json_encode($params); - - // Update the language settings in the language manager. - $query->clear() - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('params') . ' = ' . $db->quote($params)) - ->where($db->quoteName('element') . ' = ' . $db->quote('com_languages')); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - $return = false; - } - } - - // Attempt to create the root user. - if (!$this->createRootUser($options, $db)) - { - $this->deleteConfiguration(); - - return false; - } - - // Update the cms data user ids. - $this->updateUserIds($db); - - // Check for testing sampledata plugin. - $this->checkTestingSampledata($db); - - return $return; - } - - /** - * Retrieves the default user ID and sets it if necessary. - * - * @return integer The user ID. - * - * @since 3.1 - */ - public static function getUserId() - { - if (!self::$userId) - { - self::$userId = self::generateRandUserId(); - } - - return self::$userId; - } - - /** - * Generates the user ID. - * - * @return integer The user ID. - * - * @since 3.1 - */ - protected static function generateRandUserId() - { - $session = Factory::getSession(); - $randUserId = $session->get('randUserId'); - - if (empty($randUserId)) - { - // Create the ID for the root user only once and store in session. - $randUserId = mt_rand(1, 1000); - $session->set('randUserId', $randUserId); - } - - return $randUserId; - } - - /** - * Resets the user ID. - * - * @return void - * - * @since 3.1 - */ - public static function resetRandUserId() - { - self::$userId = 0; - - Factory::getSession()->set('randUserId', self::$userId); - } - - /** - * Method to update the user id of sql data content to the new rand user id. - * - * @param DatabaseDriver $db Database connector object $db*. - * - * @return void - * - * @since 3.6.1 - */ - protected function updateUserIds($db) - { - // Create the ID for the root user. - $userId = self::getUserId(); - - // Update all core tables created_by fields of the tables with the random user id. - $updatesArray = array( - '#__banners' => array('created_by', 'modified_by'), - '#__categories' => array('created_user_id', 'modified_user_id'), - '#__contact_details' => array('created_by', 'modified_by'), - '#__content' => array('created_by', 'modified_by'), - '#__fields' => array('created_user_id', 'modified_by'), - '#__finder_filters' => array('created_by', 'modified_by'), - '#__newsfeeds' => array('created_by', 'modified_by'), - '#__tags' => array('created_user_id', 'modified_user_id'), - '#__ucm_content' => array('core_created_user_id', 'core_modified_user_id'), - '#__history' => array('editor_user_id'), - '#__user_notes' => array('created_user_id', 'modified_user_id'), - '#__workflows' => array('created_by', 'modified_by'), - ); - - foreach ($updatesArray as $table => $fields) - { - foreach ($fields as $field) - { - $query = $db->getQuery(true) - ->update($db->quoteName($table)) - ->set($db->quoteName($field) . ' = ' . $db->quote($userId)) - ->where($db->quoteName($field) . ' != 0') - ->where($db->quoteName($field) . ' IS NOT NULL'); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - } - } - - /** - * Method to check for the testing sampledata plugin. - * - * @param DatabaseDriver $db Database connector object $db*. - * - * @return void - * - * @since 4.0.0 - */ - public function checkTestingSampledata($db) - { - $version = new Version; - - if (!$version->isInDevelopmentState() || !is_file(JPATH_PLUGINS . '/sampledata/testing/testing.php')) - { - return; - } - - $testingPlugin = new \stdClass; - $testingPlugin->extension_id = null; - $testingPlugin->name = 'plg_sampledata_testing'; - $testingPlugin->type = 'plugin'; - $testingPlugin->element = 'testing'; - $testingPlugin->folder = 'sampledata'; - $testingPlugin->client_id = 0; - $testingPlugin->enabled = 1; - $testingPlugin->access = 1; - $testingPlugin->manifest_cache = ''; - $testingPlugin->params = '{}'; - $testingPlugin->custom_data = ''; - - $db->insertObject('#__extensions', $testingPlugin, 'extension_id'); - - $installer = new Installer; - $installer->setDatabase($db); - - if (!$installer->refreshManifestCache($testingPlugin->extension_id)) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf('INSTL_DATABASE_COULD_NOT_REFRESH_MANIFEST_CACHE', $testingPlugin->name), - 'error' - ); - } - } - - /** - * Method to create the configuration file - * - * @param \stdClass $options The session options - * - * @return boolean True on success - * - * @since 3.1 - */ - public function createConfiguration($options) - { - // Create a new registry to build the configuration options. - $registry = new Registry; - - // Site settings. - $registry->set('offline', false); - $registry->set('offline_message', Text::_('INSTL_STD_OFFLINE_MSG')); - $registry->set('display_offline_message', 1); - $registry->set('offline_image', ''); - $registry->set('sitename', $options->site_name); - $registry->set('editor', 'tinymce'); - $registry->set('captcha', '0'); - $registry->set('list_limit', 20); - $registry->set('access', 1); - - // Debug settings. - $registry->set('debug', false); - $registry->set('debug_lang', false); - $registry->set('debug_lang_const', true); - - // Database settings. - $registry->set('dbtype', $options->db_type); - $registry->set('host', $options->db_host); - $registry->set('user', $options->db_user); - $registry->set('password', $options->db_pass_plain); - $registry->set('db', $options->db_name); - $registry->set('dbprefix', $options->db_prefix); - $registry->set('dbencryption', $options->db_encryption); - $registry->set('dbsslverifyservercert', $options->db_sslverifyservercert); - $registry->set('dbsslkey', $options->db_sslkey); - $registry->set('dbsslcert', $options->db_sslcert); - $registry->set('dbsslca', $options->db_sslca); - $registry->set('dbsslcipher', $options->db_sslcipher); - - // Server settings. - $registry->set('force_ssl', 0); - $registry->set('live_site', ''); - $registry->set('secret', UserHelper::genRandomPassword(16)); - $registry->set('gzip', false); - $registry->set('error_reporting', 'default'); - $registry->set('helpurl', $options->helpurl); - - // Locale settings. - $registry->set('offset', 'UTC'); - - // Mail settings. - $registry->set('mailonline', true); - $registry->set('mailer', 'mail'); - $registry->set('mailfrom', $options->admin_email); - $registry->set('fromname', $options->site_name); - $registry->set('sendmail', '/usr/sbin/sendmail'); - $registry->set('smtpauth', false); - $registry->set('smtpuser', ''); - $registry->set('smtppass', ''); - $registry->set('smtphost', 'localhost'); - $registry->set('smtpsecure', 'none'); - $registry->set('smtpport', 25); - - // Cache settings. - $registry->set('caching', 0); - $registry->set('cache_handler', 'file'); - $registry->set('cachetime', 15); - $registry->set('cache_platformprefix', false); - - // Meta settings. - $registry->set('MetaDesc', ''); - $registry->set('MetaAuthor', true); - $registry->set('MetaVersion', false); - $registry->set('robots', ''); - - // SEO settings. - $registry->set('sef', true); - $registry->set('sef_rewrite', false); - $registry->set('sef_suffix', false); - $registry->set('unicodeslugs', false); - - // Feed settings. - $registry->set('feed_limit', 10); - $registry->set('feed_email', 'none'); - - $registry->set('log_path', JPATH_ADMINISTRATOR . '/logs'); - $registry->set('tmp_path', JPATH_ROOT . '/tmp'); - - // Session setting. - $registry->set('lifetime', 15); - $registry->set('session_handler', 'database'); - $registry->set('shared_session', false); - $registry->set('session_metadata', true); - - // Generate the configuration class string buffer. - $buffer = $registry->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); - - // Build the configuration file path. - $path = JPATH_CONFIGURATION . '/configuration.php'; - - // Determine if the configuration file path is writable. - if (file_exists($path)) - { - $canWrite = is_writable($path); - } - else - { - $canWrite = is_writable(JPATH_CONFIGURATION . '/'); - } - - /* - * If the file exists but isn't writable OR if the file doesn't exist and the parent directory - * is not writable the user needs to fix this. - */ - if ((file_exists($path) && !is_writable($path)) || (!file_exists($path) && !is_writable(dirname($path) . '/'))) - { - return false; - } - - // Get the session - $session = Factory::getSession(); - - if ($canWrite) - { - file_put_contents($path, $buffer); - $session->set('setup.config', null); - } - else - { - // If we cannot write the configuration.php, setup fails! - return false; - } - - return true; - } - - /** - * Method to create the root user for the site. - * - * @param object $options The session options. - * @param DatabaseDriver $db Database connector object $db*. - * - * @return boolean True on success. - * - * @since 3.1 - */ - private function createRootUser($options, $db) - { - $cryptpass = UserHelper::hashPassword($options->admin_password_plain); - - // Take the admin user id - we'll need to leave this in the session for sample data install later on. - $userId = self::getUserId(); - - // Create the admin user. - date_default_timezone_set('UTC'); - $installdate = date('Y-m-d H:i:s'); - - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('id') . ' = ' . $db->quote($userId)); - - $db->setQuery($query); - - try - { - $result = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - if ($result) - { - $query->clear() - ->update($db->quoteName('#__users')) - ->set($db->quoteName('name') . ' = ' . $db->quote(trim($options->admin_user))) - ->set($db->quoteName('username') . ' = ' . $db->quote(trim($options->admin_username))) - ->set($db->quoteName('email') . ' = ' . $db->quote($options->admin_email)) - ->set($db->quoteName('password') . ' = ' . $db->quote($cryptpass)) - ->set($db->quoteName('block') . ' = 0') - ->set($db->quoteName('sendEmail') . ' = 1') - ->set($db->quoteName('registerDate') . ' = ' . $db->quote($installdate)) - ->set($db->quoteName('lastvisitDate') . ' = NULL') - ->set($db->quoteName('activation') . ' = ' . $db->quote('0')) - ->set($db->quoteName('params') . ' = ' . $db->quote('')) - ->where($db->quoteName('id') . ' = ' . $db->quote($userId)); - } - else - { - $columns = array( - $db->quoteName('id'), - $db->quoteName('name'), - $db->quoteName('username'), - $db->quoteName('email'), - $db->quoteName('password'), - $db->quoteName('block'), - $db->quoteName('sendEmail'), - $db->quoteName('registerDate'), - $db->quoteName('lastvisitDate'), - $db->quoteName('activation'), - $db->quoteName('params') - ); - $query->clear() - ->insert('#__users', true) - ->columns($columns) - ->values( - $db->quote($userId) . ', ' . $db->quote(trim($options->admin_user)) . ', ' . $db->quote(trim($options->admin_username)) . ', ' . - $db->quote($options->admin_email) . ', ' . $db->quote($cryptpass) . ', ' . - $db->quote('0') . ', ' . $db->quote('1') . ', ' . $db->quote($installdate) . ', NULL, ' . - $db->quote('0') . ', ' . $db->quote('') - ); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // Map the super user to the Super Users group - $query->clear() - ->select($db->quoteName('user_id')) - ->from($db->quoteName('#__user_usergroup_map')) - ->where($db->quoteName('user_id') . ' = ' . $db->quote($userId)); - - $db->setQuery($query); - - if ($db->loadResult()) - { - $query->clear() - ->update($db->quoteName('#__user_usergroup_map')) - ->set($db->quoteName('user_id') . ' = ' . $db->quote($userId)) - ->set($db->quoteName('group_id') . ' = 8'); - } - else - { - $query->clear() - ->insert($db->quoteName('#__user_usergroup_map'), false) - ->columns(array($db->quoteName('user_id'), $db->quoteName('group_id'))) - ->values($db->quote($userId) . ', 8'); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - return true; - } - - /** - * Method to erase the configuration file. - * - * @return void - * - * @since 4.0.0 - */ - private function deleteConfiguration() - { - // The configuration file path. - $path = JPATH_CONFIGURATION . '/configuration.php'; - - if (file_exists($path)) - { - File::delete($path); - } - } + /** + * The generated user ID. + * + * @var integer + * @since 4.0.0 + */ + protected static $userId = 0; + + /** + * Method to setup the configuration file + * + * @param array $options The session options + * + * @return boolean True on success + * + * @since 3.1 + */ + public function setup($options) + { + // Get the options as an object for easier handling. + $options = ArrayHelper::toObject($options); + + // Get a database object. + try { + $db = DatabaseHelper::getDbo( + $options->db_type, + $options->db_host, + $options->db_user, + $options->db_pass_plain, + $options->db_name, + $options->db_prefix, + true, + DatabaseHelper::getEncryptionSettings($options) + ); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_ERROR_CONNECT_DB', $e->getMessage()), 'error'); + + return false; + } + + // Attempt to create the configuration. + if (!$this->createConfiguration($options)) { + return false; + } + + $serverType = $db->getServerType(); + + // Attempt to update the table #__schema. + $pathPart = JPATH_ADMINISTRATOR . '/components/com_admin/sql/updates/' . $serverType . '/'; + + $files = Folder::files($pathPart, '\.sql$'); + + if (empty($files)) { + Factory::getApplication()->enqueueMessage(Text::_('INSTL_ERROR_INITIALISE_SCHEMA'), 'error'); + + return false; + } + + $version = ''; + + foreach ($files as $file) { + if (version_compare($version, File::stripExt($file)) < 0) { + $version = File::stripExt($file); + } + } + + $query = $db->getQuery(true) + ->select('extension_id') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('name') . ' = ' . $db->quote('files_joomla')); + $db->setQuery($query); + $eid = $db->loadResult(); + + $query->clear() + ->insert($db->quoteName('#__schemas')) + ->columns( + array( + $db->quoteName('extension_id'), + $db->quoteName('version_id') + ) + ) + ->values($eid . ', ' . $db->quote($version)); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // Attempt to refresh manifest caches. + $query->clear() + ->select('*') + ->from('#__extensions'); + $db->setQuery($query); + + $return = true; + + try { + $extensions = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + $return = false; + } + + // This is needed because the installer loads the extension table in constructor, needs to be refactored in 5.0 + Factory::$database = $db; + $installer = Installer::getInstance(); + + foreach ($extensions as $extension) { + if (!$installer->refreshManifestCache($extension->extension_id)) { + Factory::getApplication()->enqueueMessage( + Text::sprintf('INSTL_DATABASE_COULD_NOT_REFRESH_MANIFEST_CACHE', $extension->name), + 'error' + ); + + return false; + } + } + + // Handle default backend language setting. This feature is available for localized versions of Joomla. + $languages = Factory::getApplication()->getLocaliseAdmin($db); + + if (in_array($options->language, $languages['admin']) || in_array($options->language, $languages['site'])) { + // Build the language parameters for the language manager. + $params = array(); + + // Set default administrator/site language to sample data values. + $params['administrator'] = 'en-GB'; + $params['site'] = 'en-GB'; + + if (in_array($options->language, $languages['admin'])) { + $params['administrator'] = $options->language; + } + + if (in_array($options->language, $languages['site'])) { + $params['site'] = $options->language; + } + + $params = json_encode($params); + + // Update the language settings in the language manager. + $query->clear() + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote($params)) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_languages')); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + $return = false; + } + } + + // Attempt to create the root user. + if (!$this->createRootUser($options, $db)) { + $this->deleteConfiguration(); + + return false; + } + + // Update the cms data user ids. + $this->updateUserIds($db); + + // Check for testing sampledata plugin. + $this->checkTestingSampledata($db); + + return $return; + } + + /** + * Retrieves the default user ID and sets it if necessary. + * + * @return integer The user ID. + * + * @since 3.1 + */ + public static function getUserId() + { + if (!self::$userId) { + self::$userId = self::generateRandUserId(); + } + + return self::$userId; + } + + /** + * Generates the user ID. + * + * @return integer The user ID. + * + * @since 3.1 + */ + protected static function generateRandUserId() + { + $session = Factory::getSession(); + $randUserId = $session->get('randUserId'); + + if (empty($randUserId)) { + // Create the ID for the root user only once and store in session. + $randUserId = mt_rand(1, 1000); + $session->set('randUserId', $randUserId); + } + + return $randUserId; + } + + /** + * Resets the user ID. + * + * @return void + * + * @since 3.1 + */ + public static function resetRandUserId() + { + self::$userId = 0; + + Factory::getSession()->set('randUserId', self::$userId); + } + + /** + * Method to update the user id of sql data content to the new rand user id. + * + * @param DatabaseDriver $db Database connector object $db*. + * + * @return void + * + * @since 3.6.1 + */ + protected function updateUserIds($db) + { + // Create the ID for the root user. + $userId = self::getUserId(); + + // Update all core tables created_by fields of the tables with the random user id. + $updatesArray = array( + '#__banners' => array('created_by', 'modified_by'), + '#__categories' => array('created_user_id', 'modified_user_id'), + '#__contact_details' => array('created_by', 'modified_by'), + '#__content' => array('created_by', 'modified_by'), + '#__fields' => array('created_user_id', 'modified_by'), + '#__finder_filters' => array('created_by', 'modified_by'), + '#__newsfeeds' => array('created_by', 'modified_by'), + '#__tags' => array('created_user_id', 'modified_user_id'), + '#__ucm_content' => array('core_created_user_id', 'core_modified_user_id'), + '#__history' => array('editor_user_id'), + '#__user_notes' => array('created_user_id', 'modified_user_id'), + '#__workflows' => array('created_by', 'modified_by'), + ); + + foreach ($updatesArray as $table => $fields) { + foreach ($fields as $field) { + $query = $db->getQuery(true) + ->update($db->quoteName($table)) + ->set($db->quoteName($field) . ' = ' . $db->quote($userId)) + ->where($db->quoteName($field) . ' != 0') + ->where($db->quoteName($field) . ' IS NOT NULL'); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + } + } + + /** + * Method to check for the testing sampledata plugin. + * + * @param DatabaseDriver $db Database connector object $db*. + * + * @return void + * + * @since 4.0.0 + */ + public function checkTestingSampledata($db) + { + $version = new Version(); + + if (!$version->isInDevelopmentState() || !is_file(JPATH_PLUGINS . '/sampledata/testing/testing.php')) { + return; + } + + $testingPlugin = new \stdClass(); + $testingPlugin->extension_id = null; + $testingPlugin->name = 'plg_sampledata_testing'; + $testingPlugin->type = 'plugin'; + $testingPlugin->element = 'testing'; + $testingPlugin->folder = 'sampledata'; + $testingPlugin->client_id = 0; + $testingPlugin->enabled = 1; + $testingPlugin->access = 1; + $testingPlugin->manifest_cache = ''; + $testingPlugin->params = '{}'; + $testingPlugin->custom_data = ''; + + $db->insertObject('#__extensions', $testingPlugin, 'extension_id'); + + $installer = new Installer(); + $installer->setDatabase($db); + + if (!$installer->refreshManifestCache($testingPlugin->extension_id)) { + Factory::getApplication()->enqueueMessage( + Text::sprintf('INSTL_DATABASE_COULD_NOT_REFRESH_MANIFEST_CACHE', $testingPlugin->name), + 'error' + ); + } + } + + /** + * Method to create the configuration file + * + * @param \stdClass $options The session options + * + * @return boolean True on success + * + * @since 3.1 + */ + public function createConfiguration($options) + { + // Create a new registry to build the configuration options. + $registry = new Registry(); + + // Site settings. + $registry->set('offline', false); + $registry->set('offline_message', Text::_('INSTL_STD_OFFLINE_MSG')); + $registry->set('display_offline_message', 1); + $registry->set('offline_image', ''); + $registry->set('sitename', $options->site_name); + $registry->set('editor', 'tinymce'); + $registry->set('captcha', '0'); + $registry->set('list_limit', 20); + $registry->set('access', 1); + + // Debug settings. + $registry->set('debug', false); + $registry->set('debug_lang', false); + $registry->set('debug_lang_const', true); + + // Database settings. + $registry->set('dbtype', $options->db_type); + $registry->set('host', $options->db_host); + $registry->set('user', $options->db_user); + $registry->set('password', $options->db_pass_plain); + $registry->set('db', $options->db_name); + $registry->set('dbprefix', $options->db_prefix); + $registry->set('dbencryption', $options->db_encryption); + $registry->set('dbsslverifyservercert', $options->db_sslverifyservercert); + $registry->set('dbsslkey', $options->db_sslkey); + $registry->set('dbsslcert', $options->db_sslcert); + $registry->set('dbsslca', $options->db_sslca); + $registry->set('dbsslcipher', $options->db_sslcipher); + + // Server settings. + $registry->set('force_ssl', 0); + $registry->set('live_site', ''); + $registry->set('secret', UserHelper::genRandomPassword(16)); + $registry->set('gzip', false); + $registry->set('error_reporting', 'default'); + $registry->set('helpurl', $options->helpurl); + + // Locale settings. + $registry->set('offset', 'UTC'); + + // Mail settings. + $registry->set('mailonline', true); + $registry->set('mailer', 'mail'); + $registry->set('mailfrom', $options->admin_email); + $registry->set('fromname', $options->site_name); + $registry->set('sendmail', '/usr/sbin/sendmail'); + $registry->set('smtpauth', false); + $registry->set('smtpuser', ''); + $registry->set('smtppass', ''); + $registry->set('smtphost', 'localhost'); + $registry->set('smtpsecure', 'none'); + $registry->set('smtpport', 25); + + // Cache settings. + $registry->set('caching', 0); + $registry->set('cache_handler', 'file'); + $registry->set('cachetime', 15); + $registry->set('cache_platformprefix', false); + + // Meta settings. + $registry->set('MetaDesc', ''); + $registry->set('MetaAuthor', true); + $registry->set('MetaVersion', false); + $registry->set('robots', ''); + + // SEO settings. + $registry->set('sef', true); + $registry->set('sef_rewrite', false); + $registry->set('sef_suffix', false); + $registry->set('unicodeslugs', false); + + // Feed settings. + $registry->set('feed_limit', 10); + $registry->set('feed_email', 'none'); + + $registry->set('log_path', JPATH_ADMINISTRATOR . '/logs'); + $registry->set('tmp_path', JPATH_ROOT . '/tmp'); + + // Session setting. + $registry->set('lifetime', 15); + $registry->set('session_handler', 'database'); + $registry->set('shared_session', false); + $registry->set('session_metadata', true); + + // Generate the configuration class string buffer. + $buffer = $registry->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); + + // Build the configuration file path. + $path = JPATH_CONFIGURATION . '/configuration.php'; + + // Determine if the configuration file path is writable. + if (file_exists($path)) { + $canWrite = is_writable($path); + } else { + $canWrite = is_writable(JPATH_CONFIGURATION . '/'); + } + + /* + * If the file exists but isn't writable OR if the file doesn't exist and the parent directory + * is not writable the user needs to fix this. + */ + if ((file_exists($path) && !is_writable($path)) || (!file_exists($path) && !is_writable(dirname($path) . '/'))) { + return false; + } + + // Get the session + $session = Factory::getSession(); + + if ($canWrite) { + file_put_contents($path, $buffer); + $session->set('setup.config', null); + } else { + // If we cannot write the configuration.php, setup fails! + return false; + } + + return true; + } + + /** + * Method to create the root user for the site. + * + * @param object $options The session options. + * @param DatabaseDriver $db Database connector object $db*. + * + * @return boolean True on success. + * + * @since 3.1 + */ + private function createRootUser($options, $db) + { + $cryptpass = UserHelper::hashPassword($options->admin_password_plain); + + // Take the admin user id - we'll need to leave this in the session for sample data install later on. + $userId = self::getUserId(); + + // Create the admin user. + date_default_timezone_set('UTC'); + $installdate = date('Y-m-d H:i:s'); + + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('id') . ' = ' . $db->quote($userId)); + + $db->setQuery($query); + + try { + $result = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + if ($result) { + $query->clear() + ->update($db->quoteName('#__users')) + ->set($db->quoteName('name') . ' = ' . $db->quote(trim($options->admin_user))) + ->set($db->quoteName('username') . ' = ' . $db->quote(trim($options->admin_username))) + ->set($db->quoteName('email') . ' = ' . $db->quote($options->admin_email)) + ->set($db->quoteName('password') . ' = ' . $db->quote($cryptpass)) + ->set($db->quoteName('block') . ' = 0') + ->set($db->quoteName('sendEmail') . ' = 1') + ->set($db->quoteName('registerDate') . ' = ' . $db->quote($installdate)) + ->set($db->quoteName('lastvisitDate') . ' = NULL') + ->set($db->quoteName('activation') . ' = ' . $db->quote('0')) + ->set($db->quoteName('params') . ' = ' . $db->quote('')) + ->where($db->quoteName('id') . ' = ' . $db->quote($userId)); + } else { + $columns = array( + $db->quoteName('id'), + $db->quoteName('name'), + $db->quoteName('username'), + $db->quoteName('email'), + $db->quoteName('password'), + $db->quoteName('block'), + $db->quoteName('sendEmail'), + $db->quoteName('registerDate'), + $db->quoteName('lastvisitDate'), + $db->quoteName('activation'), + $db->quoteName('params') + ); + $query->clear() + ->insert('#__users', true) + ->columns($columns) + ->values( + $db->quote($userId) . ', ' . $db->quote(trim($options->admin_user)) . ', ' . $db->quote(trim($options->admin_username)) . ', ' . + $db->quote($options->admin_email) . ', ' . $db->quote($cryptpass) . ', ' . + $db->quote('0') . ', ' . $db->quote('1') . ', ' . $db->quote($installdate) . ', NULL, ' . + $db->quote('0') . ', ' . $db->quote('') + ); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // Map the super user to the Super Users group + $query->clear() + ->select($db->quoteName('user_id')) + ->from($db->quoteName('#__user_usergroup_map')) + ->where($db->quoteName('user_id') . ' = ' . $db->quote($userId)); + + $db->setQuery($query); + + if ($db->loadResult()) { + $query->clear() + ->update($db->quoteName('#__user_usergroup_map')) + ->set($db->quoteName('user_id') . ' = ' . $db->quote($userId)) + ->set($db->quoteName('group_id') . ' = 8'); + } else { + $query->clear() + ->insert($db->quoteName('#__user_usergroup_map'), false) + ->columns(array($db->quoteName('user_id'), $db->quoteName('group_id'))) + ->values($db->quote($userId) . ', 8'); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + return true; + } + + /** + * Method to erase the configuration file. + * + * @return void + * + * @since 4.0.0 + */ + private function deleteConfiguration() + { + // The configuration file path. + $path = JPATH_CONFIGURATION . '/configuration.php'; + + if (file_exists($path)) { + File::delete($path); + } + } } diff --git a/installation/src/Model/DatabaseModel.php b/installation/src/Model/DatabaseModel.php index 98e4827386b3d..d785b74e2a3d1 100644 --- a/installation/src/Model/DatabaseModel.php +++ b/installation/src/Model/DatabaseModel.php @@ -1,4 +1,5 @@ get('setup.options', array()); - } - - /** - * Method to initialise the database. - * - * @param boolean $select Select the database when creating the connections. - * - * @return DatabaseInterface|boolean Database object on success, boolean false on failure - * - * @since 3.1 - */ - public function initialise($select = true) - { - $options = $this->getOptions(); - - // Get the options as an object for easier handling. - $options = ArrayHelper::toObject($options); - - // Load the backend language files so that the DB error messages work. - $lang = Factory::getLanguage(); - $currentLang = $lang->getTag(); - - // Load the selected language - if (LanguageHelper::exists($currentLang, JPATH_ADMINISTRATOR)) - { - $lang->load('joomla', JPATH_ADMINISTRATOR, $currentLang, true); - } - // Pre-load en-GB in case the chosen language files do not exist. - else - { - $lang->load('joomla', JPATH_ADMINISTRATOR, 'en-GB', true); - } - - // Validate and clean up connection parameters - $paramsCheck = DatabaseHelper::validateConnectionParameters($options); - - if ($paramsCheck) - { - Factory::getApplication()->enqueueMessage($paramsCheck, 'warning'); - - return false; - } - - // Security check for remote db hosts - if (!DatabaseHelper::checkRemoteDbHost($options)) - { - // Messages have been enqueued in the called function. - return false; - } - - // Get a database object. - try - { - return DatabaseHelper::getDbo( - $options->db_type, - $options->db_host, - $options->db_user, - $options->db_pass_plain, - $options->db_name, - $options->db_prefix, - $select, - DatabaseHelper::getEncryptionSettings($options) - ); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_COULD_NOT_CONNECT', $e->getMessage()), 'error'); - - return false; - } - } - - /** - * Method to create a new database. - * - * @return boolean - * - * @since 3.1 - * @throws \RuntimeException - */ - public function createDatabase() - { - $options = (object) $this->getOptions(); - - $db = $this->initialise(false); - - if ($db === false) - { - // Error messages are enqueued by the initialise function, we just need to tell the controller how to redirect - return false; - } - - // Check database version. - $type = $options->db_type; - - try - { - $db_version = $db->getVersion(); - } - catch (\RuntimeException $e) - { - /* - * We may get here if the database doesn't exist, if so then explain that to users instead of showing the database connector's error - * This only supports PDO PostgreSQL and the PDO MySQL drivers presently - * - * Error Messages: - * PDO MySQL: [1049] Unknown database 'database_name' - * PDO PostgreSQL: database "database_name" does not exist - */ - if ($type === 'mysql' && strpos($e->getMessage(), '[1049] Unknown database') === 42 - || $type === 'pgsql' && strpos($e->getMessage(), 'database "' . $options->db_name . '" does not exist')) - { - /* - * Now we're really getting insane here; we're going to try building a new JDatabaseDriver instance - * in order to trick the connection into creating the database - */ - if ($type === 'mysql') - { - // MySQL (PDO): Don't specify database name - $altDBoptions = array( - 'driver' => $options->db_type, - 'host' => $options->db_host, - 'user' => $options->db_user, - 'password' => $options->db_pass_plain, - 'prefix' => $options->db_prefix, - 'select' => false, - DatabaseHelper::getEncryptionSettings($options), - ); - } - else - { - // PostgreSQL (PDO): Use 'postgres' - $altDBoptions = array( - 'driver' => $options->db_type, - 'host' => $options->db_host, - 'user' => $options->db_user, - 'password' => $options->db_pass_plain, - 'database' => 'postgres', - 'prefix' => $options->db_prefix, - 'select' => false, - DatabaseHelper::getEncryptionSettings($options), - ); - } - - $altDB = DatabaseDriver::getInstance($altDBoptions); - - // Check database server parameters - $dbServerCheck = DatabaseHelper::checkDbServerParameters($altDB, $options); - - if ($dbServerCheck) - { - // Some server parameter is not ok - throw new \RuntimeException($dbServerCheck, 500, $e); - } - - // Try to create the database now using the alternate driver - try - { - $this->createDb($altDB, $options, $altDB->hasUtfSupport()); - } - catch (\RuntimeException $e) - { - // We did everything we could - throw new \RuntimeException(Text::_('INSTL_DATABASE_COULD_NOT_CREATE_DATABASE'), 500, $e); - } - - // If we got here, the database should have been successfully created, now try one more time to get the version - try - { - $db_version = $db->getVersion(); - } - catch (\RuntimeException $e) - { - // We did everything we could - throw new \RuntimeException(Text::sprintf('INSTL_DATABASE_COULD_NOT_CONNECT', $e->getMessage()), 500, $e); - } - } - // Anything getting into this part of the conditional either doesn't support manually creating the database or isn't that type of error - else - { - throw new \RuntimeException(Text::sprintf('INSTL_DATABASE_COULD_NOT_CONNECT', $e->getMessage()), 500, $e); - } - } - - // Check database server parameters - $dbServerCheck = DatabaseHelper::checkDbServerParameters($db, $options); - - if ($dbServerCheck) - { - // Some server parameter is not ok - throw new \RuntimeException($dbServerCheck, 500, $e); - } - - // @internal Check for spaces in beginning or end of name. - if (strlen(trim($options->db_name)) <> strlen($options->db_name)) - { - throw new \RuntimeException(Text::_('INSTL_DATABASE_NAME_INVALID_SPACES')); - } - - // @internal Check for asc(00) Null in name. - if (strpos($options->db_name, chr(00)) !== false) - { - throw new \RuntimeException(Text::_('INSTL_DATABASE_NAME_INVALID_CHAR')); - } - - // Get database's UTF support. - $utfSupport = $db->hasUtfSupport(); - - // Try to select the database. - try - { - $db->select($options->db_name); - } - catch (\RuntimeException $e) - { - // If the database could not be selected, attempt to create it and then select it. - if (!$this->createDb($db, $options, $utfSupport)) - { - throw new \RuntimeException(Text::sprintf('INSTL_DATABASE_ERROR_CREATE', $options->db_name), 500, $e); - } - - $db->select($options->db_name); - } - - // Set the character set to UTF-8 for pre-existing databases. - try - { - $db->alterDbCharacterSet($options->db_name); - } - catch (\RuntimeException $e) - { - // Continue Anyhow - } - - $options = (array) $options; - - // Remove *_errors value. - foreach ($options as $i => $option) - { - if (isset($i['1']) && $i['1'] == '*') - { - unset($options[$i]); - - break; - } - } - - $options = array_merge(['db_created' => 1], $options); - - Factory::getSession()->set('setup.options', $options); - - return true; - } - - /** - * Method to process the old database. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function handleOldDatabase() - { - $options = $this->getOptions(); - - if (!isset($options['db_created']) || !$options['db_created']) - { - return $this->createDatabase($options); - } - - // Get the options as an object for easier handling. - $options = ArrayHelper::toObject($options); - - if (!$db = $this->initialise()) - { - return false; - } - - // Set the character set to UTF-8 for pre-existing databases. - try - { - $db->alterDbCharacterSet($options->db_name); - } - catch (\RuntimeException $e) - { - // Continue Anyhow - } - - // Backup any old database. - if (!$this->backupDatabase($db, $options->db_prefix)) - { - return false; - } - - return true; - } - - /** - * Method to create the database tables. - * - * @param string $schema The SQL schema file to apply. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function createTables($schema) - { - if (!$db = $this->initialise()) - { - return false; - } - - $serverType = $db->getServerType(); - - // Set the appropriate schema script based on UTF-8 support. - $schemaFile = 'sql/' . $serverType . '/' . $schema . '.sql'; - - // Check if the schema is a valid file - if (!is_file($schemaFile)) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_ERROR_DB', Text::_('INSTL_DATABASE_NO_SCHEMA')), 'error'); - - return false; - } - - // Attempt to import the database schema. - if (!$this->populateDatabase($db, $schemaFile)) - { - return false; - } - - return true; - } - - /** - * Method to backup all tables in a database with a given prefix. - * - * @param DatabaseDriver $db JDatabaseDriver object. - * @param string $prefix Database table prefix. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function backupDatabase($db, $prefix) - { - $return = true; - $backup = 'bak_' . $prefix; - - // Get the tables in the database. - $tables = $db->getTableList(); - - if ($tables) - { - foreach ($tables as $table) - { - // If the table uses the given prefix, back it up. - if (strpos($table, $prefix) === 0) - { - // Backup table name. - $backupTable = str_replace($prefix, $backup, $table); - - // Drop the backup table. - try - { - $db->dropTable($backupTable, true); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_ERROR_BACKINGUP', $e->getMessage()), 'error'); - - $return = false; - } - - // Rename the current table to the backup table. - try - { - $db->renameTable($table, $backupTable, $backup, $prefix); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_ERROR_BACKINGUP', $e->getMessage()), 'error'); - - $return = false; - } - } - } - } - - return $return; - } - - /** - * Method to create a new database. - * - * @param DatabaseDriver $db Database object. - * @param CMSObject $options CMSObject coming from "initialise" function to pass user - * and database name to database driver. - * @param boolean $utf True if the database supports the UTF-8 character set. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function createDb($db, $options, $utf) - { - // Build the create database query. - try - { - // Run the create database query. - $db->createDatabase($options, $utf); - } - catch (\RuntimeException $e) - { - // If an error occurred return false. - return false; - } - - return true; - } - - /** - * Method to import a database schema from a file. - * - * @param \Joomla\Database\DatabaseInterface $db JDatabase object. - * @param string $schema Path to the schema file. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function populateDatabase($db, $schema) - { - $return = true; - - // Get the contents of the schema file. - if (!($buffer = file_get_contents($schema))) - { - Factory::getApplication()->enqueueMessage(Text::_('INSTL_SAMPLE_DATA_NOT_FOUND'), 'error'); - - return false; - } - - // Get an array of queries from the schema and process them. - $queries = $this->splitQueries($buffer); - - foreach ($queries as $query) - { - // Trim any whitespace. - $query = trim($query); - - // If the query isn't empty and is not a MySQL or PostgreSQL comment, execute it. - if (!empty($query) && ($query[0] != '#') && ($query[0] != '-')) - { - // Execute the query. - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - $return = false; - } - } - } - - return $return; - } - - /** - * Method to split up queries from a schema file into an array. - * - * @param string $query SQL schema. - * - * @return array Queries to perform. - * - * @since 3.1 - */ - protected function splitQueries($query) - { - $buffer = array(); - $queries = array(); - $in_string = false; - - // Trim any whitespace. - $query = trim($query); - - // Remove comment lines. - $query = preg_replace("/\n\#[^\n]*/", '', "\n" . $query); - - // Remove PostgreSQL comment lines. - $query = preg_replace("/\n\--[^\n]*/", '', "\n" . $query); - - // Find function. - $funct = explode('CREATE OR REPLACE FUNCTION', $query); - - // Save sql before function and parse it. - $query = $funct[0]; - - // Parse the schema file to break up queries. - for ($i = 0; $i < strlen($query) - 1; $i++) - { - if ($query[$i] == ';' && !$in_string) - { - $queries[] = substr($query, 0, $i); - $query = substr($query, $i + 1); - $i = 0; - } - - if ($in_string && ($query[$i] == $in_string) && $buffer[1] != "\\") - { - $in_string = false; - } - elseif (!$in_string && ($query[$i] == '"' || $query[$i] == "'") && (!isset($buffer[0]) || $buffer[0] != "\\")) - { - $in_string = $query[$i]; - } - - if (isset($buffer[1])) - { - $buffer[0] = $buffer[1]; - } - - $buffer[1] = $query[$i]; - } - - // If the is anything left over, add it to the queries. - if (!empty($query)) - { - $queries[] = $query; - } - - // Add function part as is. - for ($f = 1, $fMax = count($funct); $f < $fMax; $f++) - { - $queries[] = 'CREATE OR REPLACE FUNCTION ' . $funct[$f]; - } - - return $queries; - } + /** + * Get the current setup options from the session. + * + * @return array An array of options from the session. + * + * @since 4.0.0 + */ + public function getOptions() + { + return Factory::getSession()->get('setup.options', array()); + } + + /** + * Method to initialise the database. + * + * @param boolean $select Select the database when creating the connections. + * + * @return DatabaseInterface|boolean Database object on success, boolean false on failure + * + * @since 3.1 + */ + public function initialise($select = true) + { + $options = $this->getOptions(); + + // Get the options as an object for easier handling. + $options = ArrayHelper::toObject($options); + + // Load the backend language files so that the DB error messages work. + $lang = Factory::getLanguage(); + $currentLang = $lang->getTag(); + + // Load the selected language + if (LanguageHelper::exists($currentLang, JPATH_ADMINISTRATOR)) { + $lang->load('joomla', JPATH_ADMINISTRATOR, $currentLang, true); + } + // Pre-load en-GB in case the chosen language files do not exist. + else { + $lang->load('joomla', JPATH_ADMINISTRATOR, 'en-GB', true); + } + + // Validate and clean up connection parameters + $paramsCheck = DatabaseHelper::validateConnectionParameters($options); + + if ($paramsCheck) { + Factory::getApplication()->enqueueMessage($paramsCheck, 'warning'); + + return false; + } + + // Security check for remote db hosts + if (!DatabaseHelper::checkRemoteDbHost($options)) { + // Messages have been enqueued in the called function. + return false; + } + + // Get a database object. + try { + return DatabaseHelper::getDbo( + $options->db_type, + $options->db_host, + $options->db_user, + $options->db_pass_plain, + $options->db_name, + $options->db_prefix, + $select, + DatabaseHelper::getEncryptionSettings($options) + ); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_COULD_NOT_CONNECT', $e->getMessage()), 'error'); + + return false; + } + } + + /** + * Method to create a new database. + * + * @return boolean + * + * @since 3.1 + * @throws \RuntimeException + */ + public function createDatabase() + { + $options = (object) $this->getOptions(); + + $db = $this->initialise(false); + + if ($db === false) { + // Error messages are enqueued by the initialise function, we just need to tell the controller how to redirect + return false; + } + + // Check database version. + $type = $options->db_type; + + try { + $db_version = $db->getVersion(); + } catch (\RuntimeException $e) { + /* + * We may get here if the database doesn't exist, if so then explain that to users instead of showing the database connector's error + * This only supports PDO PostgreSQL and the PDO MySQL drivers presently + * + * Error Messages: + * PDO MySQL: [1049] Unknown database 'database_name' + * PDO PostgreSQL: database "database_name" does not exist + */ + if ( + $type === 'mysql' && strpos($e->getMessage(), '[1049] Unknown database') === 42 + || $type === 'pgsql' && strpos($e->getMessage(), 'database "' . $options->db_name . '" does not exist') + ) { + /* + * Now we're really getting insane here; we're going to try building a new JDatabaseDriver instance + * in order to trick the connection into creating the database + */ + if ($type === 'mysql') { + // MySQL (PDO): Don't specify database name + $altDBoptions = array( + 'driver' => $options->db_type, + 'host' => $options->db_host, + 'user' => $options->db_user, + 'password' => $options->db_pass_plain, + 'prefix' => $options->db_prefix, + 'select' => false, + DatabaseHelper::getEncryptionSettings($options), + ); + } else { + // PostgreSQL (PDO): Use 'postgres' + $altDBoptions = array( + 'driver' => $options->db_type, + 'host' => $options->db_host, + 'user' => $options->db_user, + 'password' => $options->db_pass_plain, + 'database' => 'postgres', + 'prefix' => $options->db_prefix, + 'select' => false, + DatabaseHelper::getEncryptionSettings($options), + ); + } + + $altDB = DatabaseDriver::getInstance($altDBoptions); + + // Check database server parameters + $dbServerCheck = DatabaseHelper::checkDbServerParameters($altDB, $options); + + if ($dbServerCheck) { + // Some server parameter is not ok + throw new \RuntimeException($dbServerCheck, 500, $e); + } + + // Try to create the database now using the alternate driver + try { + $this->createDb($altDB, $options, $altDB->hasUtfSupport()); + } catch (\RuntimeException $e) { + // We did everything we could + throw new \RuntimeException(Text::_('INSTL_DATABASE_COULD_NOT_CREATE_DATABASE'), 500, $e); + } + + // If we got here, the database should have been successfully created, now try one more time to get the version + try { + $db_version = $db->getVersion(); + } catch (\RuntimeException $e) { + // We did everything we could + throw new \RuntimeException(Text::sprintf('INSTL_DATABASE_COULD_NOT_CONNECT', $e->getMessage()), 500, $e); + } + } + // Anything getting into this part of the conditional either doesn't support manually creating the database or isn't that type of error + else { + throw new \RuntimeException(Text::sprintf('INSTL_DATABASE_COULD_NOT_CONNECT', $e->getMessage()), 500, $e); + } + } + + // Check database server parameters + $dbServerCheck = DatabaseHelper::checkDbServerParameters($db, $options); + + if ($dbServerCheck) { + // Some server parameter is not ok + throw new \RuntimeException($dbServerCheck, 500, $e); + } + + // @internal Check for spaces in beginning or end of name. + if (strlen(trim($options->db_name)) <> strlen($options->db_name)) { + throw new \RuntimeException(Text::_('INSTL_DATABASE_NAME_INVALID_SPACES')); + } + + // @internal Check for asc(00) Null in name. + if (strpos($options->db_name, chr(00)) !== false) { + throw new \RuntimeException(Text::_('INSTL_DATABASE_NAME_INVALID_CHAR')); + } + + // Get database's UTF support. + $utfSupport = $db->hasUtfSupport(); + + // Try to select the database. + try { + $db->select($options->db_name); + } catch (\RuntimeException $e) { + // If the database could not be selected, attempt to create it and then select it. + if (!$this->createDb($db, $options, $utfSupport)) { + throw new \RuntimeException(Text::sprintf('INSTL_DATABASE_ERROR_CREATE', $options->db_name), 500, $e); + } + + $db->select($options->db_name); + } + + // Set the character set to UTF-8 for pre-existing databases. + try { + $db->alterDbCharacterSet($options->db_name); + } catch (\RuntimeException $e) { + // Continue Anyhow + } + + $options = (array) $options; + + // Remove *_errors value. + foreach ($options as $i => $option) { + if (isset($i['1']) && $i['1'] == '*') { + unset($options[$i]); + + break; + } + } + + $options = array_merge(['db_created' => 1], $options); + + Factory::getSession()->set('setup.options', $options); + + return true; + } + + /** + * Method to process the old database. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function handleOldDatabase() + { + $options = $this->getOptions(); + + if (!isset($options['db_created']) || !$options['db_created']) { + return $this->createDatabase($options); + } + + // Get the options as an object for easier handling. + $options = ArrayHelper::toObject($options); + + if (!$db = $this->initialise()) { + return false; + } + + // Set the character set to UTF-8 for pre-existing databases. + try { + $db->alterDbCharacterSet($options->db_name); + } catch (\RuntimeException $e) { + // Continue Anyhow + } + + // Backup any old database. + if (!$this->backupDatabase($db, $options->db_prefix)) { + return false; + } + + return true; + } + + /** + * Method to create the database tables. + * + * @param string $schema The SQL schema file to apply. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function createTables($schema) + { + if (!$db = $this->initialise()) { + return false; + } + + $serverType = $db->getServerType(); + + // Set the appropriate schema script based on UTF-8 support. + $schemaFile = 'sql/' . $serverType . '/' . $schema . '.sql'; + + // Check if the schema is a valid file + if (!is_file($schemaFile)) { + Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_ERROR_DB', Text::_('INSTL_DATABASE_NO_SCHEMA')), 'error'); + + return false; + } + + // Attempt to import the database schema. + if (!$this->populateDatabase($db, $schemaFile)) { + return false; + } + + return true; + } + + /** + * Method to backup all tables in a database with a given prefix. + * + * @param DatabaseDriver $db JDatabaseDriver object. + * @param string $prefix Database table prefix. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function backupDatabase($db, $prefix) + { + $return = true; + $backup = 'bak_' . $prefix; + + // Get the tables in the database. + $tables = $db->getTableList(); + + if ($tables) { + foreach ($tables as $table) { + // If the table uses the given prefix, back it up. + if (strpos($table, $prefix) === 0) { + // Backup table name. + $backupTable = str_replace($prefix, $backup, $table); + + // Drop the backup table. + try { + $db->dropTable($backupTable, true); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_ERROR_BACKINGUP', $e->getMessage()), 'error'); + + $return = false; + } + + // Rename the current table to the backup table. + try { + $db->renameTable($table, $backupTable, $backup, $prefix); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_ERROR_BACKINGUP', $e->getMessage()), 'error'); + + $return = false; + } + } + } + } + + return $return; + } + + /** + * Method to create a new database. + * + * @param DatabaseDriver $db Database object. + * @param CMSObject $options CMSObject coming from "initialise" function to pass user + * and database name to database driver. + * @param boolean $utf True if the database supports the UTF-8 character set. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function createDb($db, $options, $utf) + { + // Build the create database query. + try { + // Run the create database query. + $db->createDatabase($options, $utf); + } catch (\RuntimeException $e) { + // If an error occurred return false. + return false; + } + + return true; + } + + /** + * Method to import a database schema from a file. + * + * @param \Joomla\Database\DatabaseInterface $db JDatabase object. + * @param string $schema Path to the schema file. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function populateDatabase($db, $schema) + { + $return = true; + + // Get the contents of the schema file. + if (!($buffer = file_get_contents($schema))) { + Factory::getApplication()->enqueueMessage(Text::_('INSTL_SAMPLE_DATA_NOT_FOUND'), 'error'); + + return false; + } + + // Get an array of queries from the schema and process them. + $queries = $this->splitQueries($buffer); + + foreach ($queries as $query) { + // Trim any whitespace. + $query = trim($query); + + // If the query isn't empty and is not a MySQL or PostgreSQL comment, execute it. + if (!empty($query) && ($query[0] != '#') && ($query[0] != '-')) { + // Execute the query. + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + $return = false; + } + } + } + + return $return; + } + + /** + * Method to split up queries from a schema file into an array. + * + * @param string $query SQL schema. + * + * @return array Queries to perform. + * + * @since 3.1 + */ + protected function splitQueries($query) + { + $buffer = array(); + $queries = array(); + $in_string = false; + + // Trim any whitespace. + $query = trim($query); + + // Remove comment lines. + $query = preg_replace("/\n\#[^\n]*/", '', "\n" . $query); + + // Remove PostgreSQL comment lines. + $query = preg_replace("/\n\--[^\n]*/", '', "\n" . $query); + + // Find function. + $funct = explode('CREATE OR REPLACE FUNCTION', $query); + + // Save sql before function and parse it. + $query = $funct[0]; + + // Parse the schema file to break up queries. + for ($i = 0; $i < strlen($query) - 1; $i++) { + if ($query[$i] == ';' && !$in_string) { + $queries[] = substr($query, 0, $i); + $query = substr($query, $i + 1); + $i = 0; + } + + if ($in_string && ($query[$i] == $in_string) && $buffer[1] != "\\") { + $in_string = false; + } elseif (!$in_string && ($query[$i] == '"' || $query[$i] == "'") && (!isset($buffer[0]) || $buffer[0] != "\\")) { + $in_string = $query[$i]; + } + + if (isset($buffer[1])) { + $buffer[0] = $buffer[1]; + } + + $buffer[1] = $query[$i]; + } + + // If the is anything left over, add it to the queries. + if (!empty($query)) { + $queries[] = $query; + } + + // Add function part as is. + for ($f = 1, $fMax = count($funct); $f < $fMax; $f++) { + $queries[] = 'CREATE OR REPLACE FUNCTION ' . $funct[$f]; + } + + return $queries; + } } diff --git a/installation/src/Model/LanguagesModel.php b/installation/src/Model/LanguagesModel.php index 73af151cf7ddf..62605c280cc38 100644 --- a/installation/src/Model/LanguagesModel.php +++ b/installation/src/Model/LanguagesModel.php @@ -1,4 +1,5 @@ setConfiguration(new Registry(new \JConfig)); - } - - parent::__construct(); - } - - /** - * Generate a list of language choices to install in the Joomla CMS. - * - * @return array - * - * @since 3.1 - */ - public function getItems() - { - // Get the extension_id of the en-GB package. - $db = $this->getDatabase(); - $extQuery = $db->getQuery(true); - - $extQuery->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('package')) - ->where($db->quoteName('element') . ' = ' . $db->quote('pkg_en-GB')) - ->where($db->quoteName('client_id') . ' = 0'); - - $db->setQuery($extQuery); - - $extId = (int) $db->loadResult(); - - if ($extId) - { - $updater = Updater::getInstance(); - - /* - * The following function call uses the extension_id of the en-GB package. - * In #__update_sites_extensions you should have this extension_id linked - * to the Accredited Translations Repo. - */ - $updater->findUpdates(array($extId), 0); - - $query = $db->getQuery(true); - - // Select the required fields from the updates table. - $query->select($db->quoteName(array('update_id', 'name', 'element', 'version'))) - ->from($db->quoteName('#__updates')) - ->order($db->quoteName('name')); - - $db->setQuery($query); - $list = $db->loadObjectList(); - - if (!$list || $list instanceof \Exception) - { - $list = array(); - } - } - else - { - $list = array(); - } - - return $list; - } - - /** - * Method that installs in Joomla! the selected languages in the Languages View of the installer. - * - * @param array $lids List of the update_id value of the languages to install. - * - * @return boolean True if successful - */ - public function install($lids) - { - $app = Factory::getApplication(); - $installerBase = new Installer; - $installerBase->setDatabase($this->getDatabase()); - - // Loop through every selected language. - foreach ($lids as $id) - { - $installer = clone $installerBase; - - // Loads the update database object that represents the language. - $language = Table::getInstance('update'); - $language->load($id); - - // Get the URL to the XML manifest file of the selected language. - $remote_manifest = $this->getLanguageManifest($id); - - if (!$remote_manifest) - { - // Could not find the url, the information in the update server may be corrupt. - $message = Text::sprintf('INSTL_DEFAULTLANGUAGE_COULD_NOT_INSTALL_LANGUAGE', $language->name); - $message .= ' ' . Text::_('INSTL_DEFAULTLANGUAGE_TRY_LATER'); - - $app->enqueueMessage($message, 'warning'); - - continue; - } - - // Based on the language XML manifest get the URL of the package to download. - $package_url = $this->getPackageUrl($remote_manifest); - - if (!$package_url) - { - // Could not find the URL, maybe the URL is wrong in the update server, or there is no internet access. - $message = Text::sprintf('INSTL_DEFAULTLANGUAGE_COULD_NOT_INSTALL_LANGUAGE', $language->name); - $message .= ' ' . Text::_('INSTL_DEFAULTLANGUAGE_TRY_LATER'); - - $app->enqueueMessage($message, 'warning'); - - continue; - } - - // Download the package to the tmp folder. - $package = $this->downloadPackage($package_url); - - if (!$package) - { - $app->enqueueMessage(Text::sprintf('INSTL_DEFAULTLANGUAGE_COULD_NOT_DOWNLOAD_PACKAGE', $package_url), 'error'); - - continue; - } - - // Install the package. - if (!$installer->install($package['dir'])) - { - // There was an error installing the package. - $message = Text::sprintf('INSTL_DEFAULTLANGUAGE_COULD_NOT_INSTALL_LANGUAGE', $language->name); - $message .= ' ' . Text::_('INSTL_DEFAULTLANGUAGE_TRY_LATER'); - - $app->enqueueMessage($message, 'warning'); - - continue; - } - - // Cleanup the install files in tmp folder. - if (!is_file($package['packagefile'])) - { - $package['packagefile'] = $app->get('tmp_path') . '/' . $package['packagefile']; - } - - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - - // Delete the installed language from the list. - $language->delete($id); - } - - return true; - } - - /** - * Gets the manifest file of a selected language from a the language list in an update server. - * - * @param integer $uid The id of the language in the #__updates table. - * - * @return string - * - * @since 3.1 - */ - protected function getLanguageManifest($uid) - { - $instance = Table::getInstance('update'); - $instance->load($uid); - - return trim($instance->detailsurl); - } - - /** - * Finds the URL of the package to download. - * - * @param string $remoteManifest URL to the manifest XML file of the remote package. - * - * @return string|boolean - * - * @since 3.1 - */ - protected function getPackageUrl($remoteManifest) - { - $update = new Update; - $update->loadFromXml($remoteManifest); - - // Get the download url from the remote manifest - $downloadUrl = $update->get('downloadurl', false); - - // Check if the download url exist, otherwise return empty value - if ($downloadUrl === false) - { - return ''; - } - - return trim($downloadUrl->_data); - } - - /** - * Download a language package from a URL and unpack it in the tmp folder. - * - * @param string $url URL of the package. - * - * @return array|boolean Package details or false on failure. - * - * @since 3.1 - */ - protected function downloadPackage($url) - { - $app = Factory::getApplication(); - - // Download the package from the given URL. - $p_file = InstallerHelper::downloadPackage($url); - - // Was the package downloaded? - if (!$p_file) - { - $app->enqueueMessage(Text::_('INSTL_ERROR_INVALID_URL'), 'warning'); - - return false; - } - - // Unpack the downloaded package file. - return InstallerHelper::unpack($app->get('tmp_path') . '/' . $p_file); - } - - /** - * Get Languages item data for the Administrator. - * - * @return array - * - * @since 3.1 - */ - public function getInstalledlangsAdministrator() - { - return $this->getInstalledlangs('administrator'); - } - - /** - * Get Languages item data for the Frontend. - * - * @return array List of installed languages in the frontend application. - * - * @since 3.1 - */ - public function getInstalledlangsFrontend() - { - return $this->getInstalledlangs('site'); - } - - /** - * Get Languages item data. - * - * @param string $clientName Name of the cms client. - * - * @return array - * - * @since 3.1 - */ - protected function getInstalledlangs($clientName = 'administrator') - { - // Get information. - $path = $this->getPath(); - $client = $this->getClient($clientName); - $langlist = $this->getLanguageList($client->id); - - // Compute all the languages. - $data = array(); - - foreach ($langlist as $lang) - { - $file = $path . '/' . $lang . '/langmetadata.xml'; - - if (!is_file($file)) - { - $file = $path . '/' . $lang . '/' . $lang . '.xml'; - } - - $info = Installer::parseXMLInstallFile($file); - $row = new \stdClass; - $row->language = $lang; - - if (!is_array($info)) - { - continue; - } - - foreach ($info as $key => $value) - { - $row->$key = $value; - } - - // If current then set published. - $params = ComponentHelper::getParams('com_languages'); - - if ($params->get($client->name, 'en-GB') == $row->language) - { - $row->published = 1; - } - else - { - $row->published = 0; - } - - $row->checked_out = null; - $data[] = $row; - } - - usort($data, array($this, 'compareLanguages')); - - return $data; - } - - /** - * Get installed languages data. - * - * @param integer $clientId The client ID to retrieve data for. - * - * @return object The language data. - * - * @since 3.1 - */ - protected function getLanguageList($clientId = 1) - { - // Create a new db object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select field element from the extensions table. - $query->select($db->quoteName(array('element', 'name'))) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('language')) - ->where($db->quoteName('state') . ' = 0') - ->where($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('client_id') . ' = ' . (int) $clientId); - - $db->setQuery($query); - - $this->langlist = $db->loadColumn(); - - return $this->langlist; - } - - /** - * Compare two languages in order to sort them. - * - * @param object $lang1 The first language. - * @param object $lang2 The second language. - * - * @return integer - * - * @since 3.1 - */ - protected function compareLanguages($lang1, $lang2) - { - return strcmp($lang1->name, $lang2->name); - } - - /** - * Get the languages folder path. - * - * @return string The path to the languages folders. - * - * @since 3.1 - */ - protected function getPath() - { - if ($this->path === null) - { - $client = $this->getClient(); - $this->path = LanguageHelper::getLanguagePath($client->path); - } - - return $this->path; - } - - /** - * Get the client object of Administrator or Frontend. - * - * @param string $client Name of the client object. - * - * @return object - * - * @since 3.1 - */ - protected function getClient($client = 'administrator') - { - $this->client = ApplicationHelper::getClientInfo($client, true); - - return $this->client; - } - - /** - * Set the default language. - * - * @param string $language The language to be set as default. - * @param string $clientName The name of the CMS client. - * - * @return boolean - * - * @since 3.1 - */ - public function setDefault($language, $clientName = 'administrator') - { - $client = $this->getClient($clientName); - - $params = ComponentHelper::getParams('com_languages'); - $params->set($client->name, $language); - - $table = Table::getInstance('extension'); - $id = $table->find(array('element' => 'com_languages')); - - // Load - if (!$table->load($id)) - { - Factory::getApplication()->enqueueMessage($table->getError(), 'warning'); - - return false; - } - - $table->params = (string) $params; - - // Pre-save checks. - if (!$table->check()) - { - Factory::getApplication()->enqueueMessage($table->getError(), 'warning'); - - return false; - } - - // Save the changes. - if (!$table->store()) - { - Factory::getApplication()->enqueueMessage($table->getError(), 'warning'); - - return false; - } - - return true; - } - - /** - * Get the current setup options from the session. - * - * @return array - * - * @since 3.1 - */ - public function getOptions() - { - return Factory::getSession()->get('setup.options', array()); - } - - /** - * Get the model form. - * - * @param string|null $view The view being processed. - * - * @return mixed JForm object on success, false on failure. - * - * @since 3.1 - */ - public function getForm($view = null) - { - if (!$view) - { - $view = Factory::getApplication()->input->getWord('view', 'defaultlanguage'); - } - - // Get the form. - Form::addFormPath(JPATH_COMPONENT . '/forms'); - Form::addFieldPath(JPATH_COMPONENT . '/model/fields'); - Form::addRulePath(JPATH_COMPONENT . '/model/rules'); - - try - { - $form = Form::getInstance('jform', $view, array('control' => 'jform')); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // Check the session for previously entered form data. - $data = (array) $this->getOptions(); - - // Bind the form data if present. - if (!empty($data)) - { - $form->bind($data); - } - - return $form; - } + use DatabaseAwareTrait; + + /** + * @var object Client object. + * @since 3.1 + */ + protected $client; + + /** + * @var array Languages description. + * @since 3.1 + */ + protected $data; + + /** + * @var string Language path. + * @since 3.1 + */ + protected $path; + + /** + * @var integer Total number of languages installed. + * @since 3.1 + */ + protected $langlist; + + /** + * @var integer Admin Id, author of all generated content. + * @since 3.1 + */ + protected $adminId; + + /** + * Constructor: Deletes the default installation config file and recreates it with the good config file. + * + * @since 3.1 + */ + public function __construct() + { + // Overrides application config and set the configuration.php file so tokens and database works. + if (file_exists(JPATH_BASE . '/configuration.php')) { + Factory::getApplication()->setConfiguration(new Registry(new \JConfig())); + } + + parent::__construct(); + } + + /** + * Generate a list of language choices to install in the Joomla CMS. + * + * @return array + * + * @since 3.1 + */ + public function getItems() + { + // Get the extension_id of the en-GB package. + $db = $this->getDatabase(); + $extQuery = $db->getQuery(true); + + $extQuery->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('package')) + ->where($db->quoteName('element') . ' = ' . $db->quote('pkg_en-GB')) + ->where($db->quoteName('client_id') . ' = 0'); + + $db->setQuery($extQuery); + + $extId = (int) $db->loadResult(); + + if ($extId) { + $updater = Updater::getInstance(); + + /* + * The following function call uses the extension_id of the en-GB package. + * In #__update_sites_extensions you should have this extension_id linked + * to the Accredited Translations Repo. + */ + $updater->findUpdates(array($extId), 0); + + $query = $db->getQuery(true); + + // Select the required fields from the updates table. + $query->select($db->quoteName(array('update_id', 'name', 'element', 'version'))) + ->from($db->quoteName('#__updates')) + ->order($db->quoteName('name')); + + $db->setQuery($query); + $list = $db->loadObjectList(); + + if (!$list || $list instanceof \Exception) { + $list = array(); + } + } else { + $list = array(); + } + + return $list; + } + + /** + * Method that installs in Joomla! the selected languages in the Languages View of the installer. + * + * @param array $lids List of the update_id value of the languages to install. + * + * @return boolean True if successful + */ + public function install($lids) + { + $app = Factory::getApplication(); + $installerBase = new Installer(); + $installerBase->setDatabase($this->getDatabase()); + + // Loop through every selected language. + foreach ($lids as $id) { + $installer = clone $installerBase; + + // Loads the update database object that represents the language. + $language = Table::getInstance('update'); + $language->load($id); + + // Get the URL to the XML manifest file of the selected language. + $remote_manifest = $this->getLanguageManifest($id); + + if (!$remote_manifest) { + // Could not find the url, the information in the update server may be corrupt. + $message = Text::sprintf('INSTL_DEFAULTLANGUAGE_COULD_NOT_INSTALL_LANGUAGE', $language->name); + $message .= ' ' . Text::_('INSTL_DEFAULTLANGUAGE_TRY_LATER'); + + $app->enqueueMessage($message, 'warning'); + + continue; + } + + // Based on the language XML manifest get the URL of the package to download. + $package_url = $this->getPackageUrl($remote_manifest); + + if (!$package_url) { + // Could not find the URL, maybe the URL is wrong in the update server, or there is no internet access. + $message = Text::sprintf('INSTL_DEFAULTLANGUAGE_COULD_NOT_INSTALL_LANGUAGE', $language->name); + $message .= ' ' . Text::_('INSTL_DEFAULTLANGUAGE_TRY_LATER'); + + $app->enqueueMessage($message, 'warning'); + + continue; + } + + // Download the package to the tmp folder. + $package = $this->downloadPackage($package_url); + + if (!$package) { + $app->enqueueMessage(Text::sprintf('INSTL_DEFAULTLANGUAGE_COULD_NOT_DOWNLOAD_PACKAGE', $package_url), 'error'); + + continue; + } + + // Install the package. + if (!$installer->install($package['dir'])) { + // There was an error installing the package. + $message = Text::sprintf('INSTL_DEFAULTLANGUAGE_COULD_NOT_INSTALL_LANGUAGE', $language->name); + $message .= ' ' . Text::_('INSTL_DEFAULTLANGUAGE_TRY_LATER'); + + $app->enqueueMessage($message, 'warning'); + + continue; + } + + // Cleanup the install files in tmp folder. + if (!is_file($package['packagefile'])) { + $package['packagefile'] = $app->get('tmp_path') . '/' . $package['packagefile']; + } + + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + + // Delete the installed language from the list. + $language->delete($id); + } + + return true; + } + + /** + * Gets the manifest file of a selected language from a the language list in an update server. + * + * @param integer $uid The id of the language in the #__updates table. + * + * @return string + * + * @since 3.1 + */ + protected function getLanguageManifest($uid) + { + $instance = Table::getInstance('update'); + $instance->load($uid); + + return trim($instance->detailsurl); + } + + /** + * Finds the URL of the package to download. + * + * @param string $remoteManifest URL to the manifest XML file of the remote package. + * + * @return string|boolean + * + * @since 3.1 + */ + protected function getPackageUrl($remoteManifest) + { + $update = new Update(); + $update->loadFromXml($remoteManifest); + + // Get the download url from the remote manifest + $downloadUrl = $update->get('downloadurl', false); + + // Check if the download url exist, otherwise return empty value + if ($downloadUrl === false) { + return ''; + } + + return trim($downloadUrl->_data); + } + + /** + * Download a language package from a URL and unpack it in the tmp folder. + * + * @param string $url URL of the package. + * + * @return array|boolean Package details or false on failure. + * + * @since 3.1 + */ + protected function downloadPackage($url) + { + $app = Factory::getApplication(); + + // Download the package from the given URL. + $p_file = InstallerHelper::downloadPackage($url); + + // Was the package downloaded? + if (!$p_file) { + $app->enqueueMessage(Text::_('INSTL_ERROR_INVALID_URL'), 'warning'); + + return false; + } + + // Unpack the downloaded package file. + return InstallerHelper::unpack($app->get('tmp_path') . '/' . $p_file); + } + + /** + * Get Languages item data for the Administrator. + * + * @return array + * + * @since 3.1 + */ + public function getInstalledlangsAdministrator() + { + return $this->getInstalledlangs('administrator'); + } + + /** + * Get Languages item data for the Frontend. + * + * @return array List of installed languages in the frontend application. + * + * @since 3.1 + */ + public function getInstalledlangsFrontend() + { + return $this->getInstalledlangs('site'); + } + + /** + * Get Languages item data. + * + * @param string $clientName Name of the cms client. + * + * @return array + * + * @since 3.1 + */ + protected function getInstalledlangs($clientName = 'administrator') + { + // Get information. + $path = $this->getPath(); + $client = $this->getClient($clientName); + $langlist = $this->getLanguageList($client->id); + + // Compute all the languages. + $data = array(); + + foreach ($langlist as $lang) { + $file = $path . '/' . $lang . '/langmetadata.xml'; + + if (!is_file($file)) { + $file = $path . '/' . $lang . '/' . $lang . '.xml'; + } + + $info = Installer::parseXMLInstallFile($file); + $row = new \stdClass(); + $row->language = $lang; + + if (!is_array($info)) { + continue; + } + + foreach ($info as $key => $value) { + $row->$key = $value; + } + + // If current then set published. + $params = ComponentHelper::getParams('com_languages'); + + if ($params->get($client->name, 'en-GB') == $row->language) { + $row->published = 1; + } else { + $row->published = 0; + } + + $row->checked_out = null; + $data[] = $row; + } + + usort($data, array($this, 'compareLanguages')); + + return $data; + } + + /** + * Get installed languages data. + * + * @param integer $clientId The client ID to retrieve data for. + * + * @return object The language data. + * + * @since 3.1 + */ + protected function getLanguageList($clientId = 1) + { + // Create a new db object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select field element from the extensions table. + $query->select($db->quoteName(array('element', 'name'))) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('language')) + ->where($db->quoteName('state') . ' = 0') + ->where($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('client_id') . ' = ' . (int) $clientId); + + $db->setQuery($query); + + $this->langlist = $db->loadColumn(); + + return $this->langlist; + } + + /** + * Compare two languages in order to sort them. + * + * @param object $lang1 The first language. + * @param object $lang2 The second language. + * + * @return integer + * + * @since 3.1 + */ + protected function compareLanguages($lang1, $lang2) + { + return strcmp($lang1->name, $lang2->name); + } + + /** + * Get the languages folder path. + * + * @return string The path to the languages folders. + * + * @since 3.1 + */ + protected function getPath() + { + if ($this->path === null) { + $client = $this->getClient(); + $this->path = LanguageHelper::getLanguagePath($client->path); + } + + return $this->path; + } + + /** + * Get the client object of Administrator or Frontend. + * + * @param string $client Name of the client object. + * + * @return object + * + * @since 3.1 + */ + protected function getClient($client = 'administrator') + { + $this->client = ApplicationHelper::getClientInfo($client, true); + + return $this->client; + } + + /** + * Set the default language. + * + * @param string $language The language to be set as default. + * @param string $clientName The name of the CMS client. + * + * @return boolean + * + * @since 3.1 + */ + public function setDefault($language, $clientName = 'administrator') + { + $client = $this->getClient($clientName); + + $params = ComponentHelper::getParams('com_languages'); + $params->set($client->name, $language); + + $table = Table::getInstance('extension'); + $id = $table->find(array('element' => 'com_languages')); + + // Load + if (!$table->load($id)) { + Factory::getApplication()->enqueueMessage($table->getError(), 'warning'); + + return false; + } + + $table->params = (string) $params; + + // Pre-save checks. + if (!$table->check()) { + Factory::getApplication()->enqueueMessage($table->getError(), 'warning'); + + return false; + } + + // Save the changes. + if (!$table->store()) { + Factory::getApplication()->enqueueMessage($table->getError(), 'warning'); + + return false; + } + + return true; + } + + /** + * Get the current setup options from the session. + * + * @return array + * + * @since 3.1 + */ + public function getOptions() + { + return Factory::getSession()->get('setup.options', array()); + } + + /** + * Get the model form. + * + * @param string|null $view The view being processed. + * + * @return mixed JForm object on success, false on failure. + * + * @since 3.1 + */ + public function getForm($view = null) + { + if (!$view) { + $view = Factory::getApplication()->input->getWord('view', 'defaultlanguage'); + } + + // Get the form. + Form::addFormPath(JPATH_COMPONENT . '/forms'); + Form::addFieldPath(JPATH_COMPONENT . '/model/fields'); + Form::addRulePath(JPATH_COMPONENT . '/model/rules'); + + try { + $form = Form::getInstance('jform', $view, array('control' => 'jform')); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // Check the session for previously entered form data. + $data = (array) $this->getOptions(); + + // Bind the form data if present. + if (!empty($data)) { + $form->bind($data); + } + + return $form; + } } diff --git a/installation/src/Model/SetupModel.php b/installation/src/Model/SetupModel.php index 7ea706f79c2f4..f3a02cadda25a 100644 --- a/installation/src/Model/SetupModel.php +++ b/installation/src/Model/SetupModel.php @@ -1,4 +1,5 @@ get('setup.options', array()))) - { - return Factory::getSession()->get('setup.options', array()); - } - } - - /** - * Store the current setup options in the session. - * - * @param array $options The installation options. - * - * @return array An array of options from the session. - * - * @since 3.1 - */ - public function storeOptions($options) - { - // Get the current setup options from the session. - $old = (array) $this->getOptions(); - - // Ensure that we have language - if (!isset($options['language']) || empty($options['language'])) - { - $options['language'] = Factory::getLanguage()->getTag(); - } - - // Store passwords as a separate key that is not used in the forms - foreach (array('admin_password', 'db_pass') as $passwordField) - { - if (isset($options[$passwordField])) - { - $plainTextKey = $passwordField . '_plain'; - - $options[$plainTextKey] = $options[$passwordField]; - - unset($options[$passwordField]); - } - } - - // Get the session - $session = Factory::getSession(); - $options['helpurl'] = $session->get('setup.helpurl', null); - - // Merge the new setup options into the current ones and store in the session. - $options = array_merge($old, (array) $options); - $session->set('setup.options', $options); - - return $options; - } - - /** - * Method to get the form. - * - * @param string|null $view The view being processed. - * - * @return Form|boolean JForm object on success, false on failure. - * - * @since 3.1 - */ - public function getForm($view = null) - { - if (!$view) - { - $view = Factory::getApplication()->input->getWord('view', 'setup'); - } - - // Get the form. - Form::addFormPath(JPATH_COMPONENT . '/forms'); - - try - { - $form = Form::getInstance('jform', $view, array('control' => 'jform')); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // Check the session for previously entered form data. - $data = (array) $this->getOptions(); - - // Bind the form data if present. - if (!empty($data)) - { - $form->bind($data); - } - - return $form; - } - - /** - * Method to check the form data. - * - * @param string $page The view being checked. - * - * @return array|boolean Array with the validated form data or boolean false on a validation failure. - * - * @since 3.1 - */ - public function checkForm($page = 'setup') - { - // Get the posted values from the request and validate them. - $data = Factory::getApplication()->input->post->get('jform', array(), 'array'); - $return = $this->validate($data, $page); - - // Attempt to save the data before validation. - $form = $this->getForm(); - $data = $form->filter($data); - - $this->storeOptions($data); - - // Check for validation errors. - if ($return === false) - { - return false; - } - - // Store the options in the session. - return $this->storeOptions($return); - } - - /** - * Generate a panel of language choices for the user to select their language. - * - * @return array - * - * @since 3.1 - */ - public function getLanguages() - { - // Detect the native language. - $native = LanguageHelper::detectLanguage(); - - if (empty($native)) - { - $native = 'en-GB'; - } - - // Get a forced language if it exists. - $forced = Factory::getApplication()->getLocalise(); - - if (!empty($forced['language'])) - { - $native = $forced['language']; - } - - // Get the list of available languages. - $list = LanguageHelper::createLanguageList($native); - - if (!$list || $list instanceof \Exception) - { - $list = array(); - } - - return $list; - } - - /** - * Method to validate the form data. - * - * @param array $data The form data. - * @param string|null $view The view. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @since 3.1 - */ - public function validate($data, $view = null) - { - // Get the form. - $form = $this->getForm($view); - - // Check for an error. - if ($form === false) - { - return false; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - Factory::getApplication()->enqueueMessage($return->getMessage(), 'warning'); - - return false; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - $messages = array_reverse($form->getErrors()); - - foreach ($messages as $message) - { - if ($message instanceof \Exception) - { - Factory::getApplication()->enqueueMessage($message->getMessage(), 'warning'); - } - else - { - Factory::getApplication()->enqueueMessage($message, 'warning'); - } - } - - return false; - } - - return $data; - } - - /** - * Method to validate the db connection properties. - * - * @return boolean - * - * @since 4.0.0 - * @throws \Exception - */ - public function validateDbConnection() - { - $options = $this->getOptions(); - - // Get the options as an object for easier handling. - $options = ArrayHelper::toObject($options); - - // Load the backend language files so that the DB error messages work. - $lang = Factory::getLanguage(); - $currentLang = $lang->getTag(); - - // Load the selected language - if (LanguageHelper::exists($currentLang, JPATH_ADMINISTRATOR)) - { - $lang->load('joomla', JPATH_ADMINISTRATOR, $currentLang, true); - } - // Pre-load en-GB in case the chosen language files do not exist. - else - { - $lang->load('joomla', JPATH_ADMINISTRATOR, 'en-GB', true); - } - - // Validate and clean up connection parameters - $paramsCheck = DatabaseHelper::validateConnectionParameters($options); - - if ($paramsCheck) - { - // Validation error: Enqueue the error message - Factory::getApplication()->enqueueMessage($paramsCheck, 'error'); - - return false; - } - - // Security check for remote db hosts - if (!DatabaseHelper::checkRemoteDbHost($options)) - { - // Messages have been enqueued in the called function. - return false; - } - - // Get a database object. - try - { - $db = DatabaseHelper::getDbo( - $options->db_type, - $options->db_host, - $options->db_user, - $options->db_pass_plain, - $options->db_name, - $options->db_prefix, - false, - DatabaseHelper::getEncryptionSettings($options) - ); - - $db->connect(); - } - catch (\RuntimeException $e) - { - if ($options->db_type === 'mysql' && strpos($e->getMessage(), '[1049] Unknown database') === 42 - || $options->db_type === 'pgsql' && strpos($e->getMessage(), 'database "' . $options->db_name . '" does not exist')) - { - // Database doesn't exist: Skip the below checks, they will be done later at database creation - return true; - } - - Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_COULD_NOT_CONNECT', $e->getMessage()), 'error'); - - return false; - } - - // Check database server parameters - $dbServerCheck = DatabaseHelper::checkDbServerParameters($db, $options); - - if ($dbServerCheck) - { - // Some server parameter is not ok: Enqueue the error message - Factory::getApplication()->enqueueMessage($dbServerCheck, 'error'); - - return false; - } - - return true; - } + /** + * Get the current setup options from the session. + * + * @return array An array of options from the session. + * + * @since 3.1 + */ + public function getOptions() + { + if (!empty(Factory::getSession()->get('setup.options', array()))) { + return Factory::getSession()->get('setup.options', array()); + } + } + + /** + * Store the current setup options in the session. + * + * @param array $options The installation options. + * + * @return array An array of options from the session. + * + * @since 3.1 + */ + public function storeOptions($options) + { + // Get the current setup options from the session. + $old = (array) $this->getOptions(); + + // Ensure that we have language + if (!isset($options['language']) || empty($options['language'])) { + $options['language'] = Factory::getLanguage()->getTag(); + } + + // Store passwords as a separate key that is not used in the forms + foreach (array('admin_password', 'db_pass') as $passwordField) { + if (isset($options[$passwordField])) { + $plainTextKey = $passwordField . '_plain'; + + $options[$plainTextKey] = $options[$passwordField]; + + unset($options[$passwordField]); + } + } + + // Get the session + $session = Factory::getSession(); + $options['helpurl'] = $session->get('setup.helpurl', null); + + // Merge the new setup options into the current ones and store in the session. + $options = array_merge($old, (array) $options); + $session->set('setup.options', $options); + + return $options; + } + + /** + * Method to get the form. + * + * @param string|null $view The view being processed. + * + * @return Form|boolean JForm object on success, false on failure. + * + * @since 3.1 + */ + public function getForm($view = null) + { + if (!$view) { + $view = Factory::getApplication()->input->getWord('view', 'setup'); + } + + // Get the form. + Form::addFormPath(JPATH_COMPONENT . '/forms'); + + try { + $form = Form::getInstance('jform', $view, array('control' => 'jform')); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // Check the session for previously entered form data. + $data = (array) $this->getOptions(); + + // Bind the form data if present. + if (!empty($data)) { + $form->bind($data); + } + + return $form; + } + + /** + * Method to check the form data. + * + * @param string $page The view being checked. + * + * @return array|boolean Array with the validated form data or boolean false on a validation failure. + * + * @since 3.1 + */ + public function checkForm($page = 'setup') + { + // Get the posted values from the request and validate them. + $data = Factory::getApplication()->input->post->get('jform', array(), 'array'); + $return = $this->validate($data, $page); + + // Attempt to save the data before validation. + $form = $this->getForm(); + $data = $form->filter($data); + + $this->storeOptions($data); + + // Check for validation errors. + if ($return === false) { + return false; + } + + // Store the options in the session. + return $this->storeOptions($return); + } + + /** + * Generate a panel of language choices for the user to select their language. + * + * @return array + * + * @since 3.1 + */ + public function getLanguages() + { + // Detect the native language. + $native = LanguageHelper::detectLanguage(); + + if (empty($native)) { + $native = 'en-GB'; + } + + // Get a forced language if it exists. + $forced = Factory::getApplication()->getLocalise(); + + if (!empty($forced['language'])) { + $native = $forced['language']; + } + + // Get the list of available languages. + $list = LanguageHelper::createLanguageList($native); + + if (!$list || $list instanceof \Exception) { + $list = array(); + } + + return $list; + } + + /** + * Method to validate the form data. + * + * @param array $data The form data. + * @param string|null $view The view. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @since 3.1 + */ + public function validate($data, $view = null) + { + // Get the form. + $form = $this->getForm($view); + + // Check for an error. + if ($form === false) { + return false; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + Factory::getApplication()->enqueueMessage($return->getMessage(), 'warning'); + + return false; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + $messages = array_reverse($form->getErrors()); + + foreach ($messages as $message) { + if ($message instanceof \Exception) { + Factory::getApplication()->enqueueMessage($message->getMessage(), 'warning'); + } else { + Factory::getApplication()->enqueueMessage($message, 'warning'); + } + } + + return false; + } + + return $data; + } + + /** + * Method to validate the db connection properties. + * + * @return boolean + * + * @since 4.0.0 + * @throws \Exception + */ + public function validateDbConnection() + { + $options = $this->getOptions(); + + // Get the options as an object for easier handling. + $options = ArrayHelper::toObject($options); + + // Load the backend language files so that the DB error messages work. + $lang = Factory::getLanguage(); + $currentLang = $lang->getTag(); + + // Load the selected language + if (LanguageHelper::exists($currentLang, JPATH_ADMINISTRATOR)) { + $lang->load('joomla', JPATH_ADMINISTRATOR, $currentLang, true); + } + // Pre-load en-GB in case the chosen language files do not exist. + else { + $lang->load('joomla', JPATH_ADMINISTRATOR, 'en-GB', true); + } + + // Validate and clean up connection parameters + $paramsCheck = DatabaseHelper::validateConnectionParameters($options); + + if ($paramsCheck) { + // Validation error: Enqueue the error message + Factory::getApplication()->enqueueMessage($paramsCheck, 'error'); + + return false; + } + + // Security check for remote db hosts + if (!DatabaseHelper::checkRemoteDbHost($options)) { + // Messages have been enqueued in the called function. + return false; + } + + // Get a database object. + try { + $db = DatabaseHelper::getDbo( + $options->db_type, + $options->db_host, + $options->db_user, + $options->db_pass_plain, + $options->db_name, + $options->db_prefix, + false, + DatabaseHelper::getEncryptionSettings($options) + ); + + $db->connect(); + } catch (\RuntimeException $e) { + if ( + $options->db_type === 'mysql' && strpos($e->getMessage(), '[1049] Unknown database') === 42 + || $options->db_type === 'pgsql' && strpos($e->getMessage(), 'database "' . $options->db_name . '" does not exist') + ) { + // Database doesn't exist: Skip the below checks, they will be done later at database creation + return true; + } + + Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_COULD_NOT_CONNECT', $e->getMessage()), 'error'); + + return false; + } + + // Check database server parameters + $dbServerCheck = DatabaseHelper::checkDbServerParameters($db, $options); + + if ($dbServerCheck) { + // Some server parameter is not ok: Enqueue the error message + Factory::getApplication()->enqueueMessage($dbServerCheck, 'error'); + + return false; + } + + return true; + } } diff --git a/installation/src/Response/JsonResponse.php b/installation/src/Response/JsonResponse.php index 5271d4e153799..d762b90a87707 100644 --- a/installation/src/Response/JsonResponse.php +++ b/installation/src/Response/JsonResponse.php @@ -1,4 +1,5 @@ token = Session::getFormToken(true); + /** + * Constructor for the JSON response + * + * @param mixed $data Exception if there is an error, otherwise, the session data + * + * @since 3.1 + */ + public function __construct($data) + { + // The old token is invalid so send a new one. + $this->token = Session::getFormToken(true); - // Get the language and send its tag along - $this->lang = Factory::getLanguage()->getTag(); + // Get the language and send its tag along + $this->lang = Factory::getLanguage()->getTag(); - // Get the message queue - $messages = Factory::getApplication()->getMessageQueue(); + // Get the message queue + $messages = Factory::getApplication()->getMessageQueue(); - // Build the sorted message list - if (is_array($messages) && count($messages)) - { - foreach ($messages as $msg) - { - if (isset($msg['type'], $msg['message'])) - { - $lists[$msg['type']][] = $msg['message']; - } - } - } + // Build the sorted message list + if (is_array($messages) && count($messages)) { + foreach ($messages as $msg) { + if (isset($msg['type'], $msg['message'])) { + $lists[$msg['type']][] = $msg['message']; + } + } + } - // If messages exist add them to the output - if (isset($lists) && is_array($lists)) - { - $this->messages = $lists; - } + // If messages exist add them to the output + if (isset($lists) && is_array($lists)) { + $this->messages = $lists; + } - // Check if we are dealing with an error. - if ($data instanceof \Throwable) - { - // Prepare the error response. - $this->error = true; - $this->header = Text::_('INSTL_HEADER_ERROR'); - $this->message = $data->getMessage(); - } - else - { - // Prepare the response data. - $this->error = false; - $this->data = $data; - } - } + // Check if we are dealing with an error. + if ($data instanceof \Throwable) { + // Prepare the error response. + $this->error = true; + $this->header = Text::_('INSTL_HEADER_ERROR'); + $this->message = $data->getMessage(); + } else { + // Prepare the response data. + $this->error = false; + $this->data = $data; + } + } } diff --git a/installation/src/Router/InstallationRouter.php b/installation/src/Router/InstallationRouter.php index 662eb898f4eca..e9d6b6851ed2c 100644 --- a/installation/src/Router/InstallationRouter.php +++ b/installation/src/Router/InstallationRouter.php @@ -1,4 +1,5 @@ share( - InstallationApplication::class, - function (Container $container) - { - $app = new InstallationApplication(null, $container->get('config'), null, $container); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->share( + InstallationApplication::class, + function (Container $container) { + $app = new InstallationApplication(null, $container->get('config'), null, $container); - // The session service provider needs Factory::$application, set it if still null - if (Factory::$application === null) - { - Factory::$application = $app; - } + // The session service provider needs Factory::$application, set it if still null + if (Factory::$application === null) { + Factory::$application = $app; + } - $app->setDispatcher($container->get('Joomla\Event\DispatcherInterface')); - $app->setLogger($container->get(LoggerInterface::class)); - $app->setSession($container->get('Joomla\Session\SessionInterface')); + $app->setDispatcher($container->get('Joomla\Event\DispatcherInterface')); + $app->setLogger($container->get(LoggerInterface::class)); + $app->setSession($container->get('Joomla\Session\SessionInterface')); - return $app; - }, - true - ); + return $app; + }, + true + ); - // Inject a custom JSON error renderer - $container->share( - JsonRenderer::class, - function (Container $container) - { - return new \Joomla\CMS\Installation\Error\Renderer\JsonRenderer; - } - ); - } + // Inject a custom JSON error renderer + $container->share( + JsonRenderer::class, + function (Container $container) { + return new \Joomla\CMS\Installation\Error\Renderer\JsonRenderer(); + } + ); + } } diff --git a/installation/src/View/DefaultView.php b/installation/src/View/DefaultView.php index 127ecbe37a9b0..8e3d91aa867dd 100644 --- a/installation/src/View/DefaultView.php +++ b/installation/src/View/DefaultView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); + /** + * Execute and display a template script. + * + * @param string|null $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); - parent::display($tpl); - } + parent::display($tpl); + } } diff --git a/installation/src/View/Error/HtmlView.php b/installation/src/View/Error/HtmlView.php index dbbfe15dfd15c..5a3ff61459c0c 100644 --- a/installation/src/View/Error/HtmlView.php +++ b/installation/src/View/Error/HtmlView.php @@ -1,4 +1,5 @@ options = $this->get('PhpOptions'); - - parent::display($tpl); - } + /** + * Array of PHP config options. + * + * @var array + * @since 3.1 + */ + protected $options; + + /** + * The default model + * + * @var string + * @since 3.0 + */ + protected $_defaultModel = 'checks'; + + /** + * Execute and display a template script. + * + * @param string|null $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->options = $this->get('PhpOptions'); + + parent::display($tpl); + } } diff --git a/installation/src/View/Remove/HtmlView.php b/installation/src/View/Remove/HtmlView.php index 53499f3721b5d..2b86b339fc997 100644 --- a/installation/src/View/Remove/HtmlView.php +++ b/installation/src/View/Remove/HtmlView.php @@ -1,4 +1,5 @@ development = (new Version)->isInDevelopmentState(); + /** + * Execute and display a template script. + * + * @param string|null $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->development = (new Version())->isInDevelopmentState(); - $this->items = $this->get('Items', 'Languages'); + $this->items = $this->get('Items', 'Languages'); - $this->installed_languages = new \stdClass; - $this->installed_languages->administrator = $this->get('InstalledlangsAdministrator', 'Languages'); - $this->installed_languages->frontend = $this->get('InstalledlangsFrontend', 'Languages'); + $this->installed_languages = new \stdClass(); + $this->installed_languages->administrator = $this->get('InstalledlangsAdministrator', 'Languages'); + $this->installed_languages->frontend = $this->get('InstalledlangsFrontend', 'Languages'); - $this->phpoptions = $this->get('PhpOptions', 'Checks'); - $this->phpsettings = $this->get('PhpSettings', 'Checks'); + $this->phpoptions = $this->get('PhpOptions', 'Checks'); + $this->phpsettings = $this->get('PhpSettings', 'Checks'); - parent::display($tpl); - } + parent::display($tpl); + } } diff --git a/installation/src/View/Setup/HtmlView.php b/installation/src/View/Setup/HtmlView.php index fe0e3200aa996..bae482d0679b3 100644 --- a/installation/src/View/Setup/HtmlView.php +++ b/installation/src/View/Setup/HtmlView.php @@ -1,4 +1,5 @@ - * @license GNU General Public License version 2 or later; see LICENSE.txt + * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; @@ -14,13 +15,13 @@ /** @var \Joomla\CMS\Document\ErrorDocument $this */ // Add required assets $this->getWebAssetManager() - ->registerAndUseStyle('template.installation', 'template' . ($this->direction === 'rtl' ? '-rtl' : '') . '.css') - ->useScript('core') - ->registerAndUseScript('template.installation', 'installation/template/js/template.js', [], [], ['core']); + ->registerAndUseStyle('template.installation', 'template' . ($this->direction === 'rtl' ? '-rtl' : '') . '.css') + ->useScript('core') + ->registerAndUseScript('template.installation', 'installation/template/js/template.js', [], [], ['core']); $this->getWebAssetManager() - ->useStyle('webcomponent.joomla-alert') - ->useScript('messages'); + ->useStyle('webcomponent.joomla-alert') + ->useScript('messages'); // Add script options $this->addScriptOptions('system.installation', ['url' => Route::_('index.php')]); @@ -33,77 +34,77 @@ ?> - - - - - -
    - - - -
    - -
    - -
    -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -

    -

    error->getCode(); ?> error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    -
    -
    - debug) : ?> -
    - renderBacktrace(); ?> - - error->getPrevious()) : ?> - - _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> - - setError($this->_error->getPrevious()); ?> - -

    -

    _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    - renderBacktrace(); ?> - setError($this->_error->getPrevious()); ?> - - - setError($this->error); ?> - -
    - -
    -
    -
    -
    - - -
    - + + + + + +
    + + + +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +

    +

    error->getCode(); ?> error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    +
    +
    + debug) : ?> +
    + renderBacktrace(); ?> + + error->getPrevious()) : ?> + + _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> + + setError($this->_error->getPrevious()); ?> + +

    +

    _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    + renderBacktrace(); ?> + setError($this->_error->getPrevious()); ?> + + + setError($this->error); ?> + +
    + +
    +
    +
    +
    + + +
    + diff --git a/installation/template/index.php b/installation/template/index.php index b670c113aa76d..75e34d7659f6c 100644 --- a/installation/template/index.php +++ b/installation/template/index.php @@ -1,9 +1,10 @@ - * @license GNU General Public License version 2 or later; see LICENSE.txt + * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; @@ -16,17 +17,17 @@ /** @var \Joomla\CMS\Document\HtmlDocument $this */ // Add required assets $this->getWebAssetManager() - ->registerAndUseStyle('template.installation', 'installation/template/css/template' . ($this->direction === 'rtl' ? '-rtl' : '') . '.css', ['version' => 'auto'], [], []) - ->useScript('core') - ->useScript('keepalive') - ->useScript('form.validate') - ->registerAndUseScript('template.installation', 'installation/template/js/template.js', ['version' => 'auto'], ['defer' => true], ['core', 'form.validate']); + ->registerAndUseStyle('template.installation', 'installation/template/css/template' . ($this->direction === 'rtl' ? '-rtl' : '') . '.css', ['version' => 'auto'], [], []) + ->useScript('core') + ->useScript('keepalive') + ->useScript('form.validate') + ->registerAndUseScript('template.installation', 'installation/template/js/template.js', ['version' => 'auto'], ['defer' => true], ['core', 'form.validate']); $this->getWebAssetManager() - ->useStyle('webcomponent.joomla-alert') - ->useScript('messages') - ->useScript('webcomponent.core-loader') - ->addInlineStyle(':root { + ->useStyle('webcomponent.joomla-alert') + ->useScript('messages') + ->useScript('webcomponent.core-loader') + ->addInlineStyle(':root { --hue: 214; --template-bg-light: #f0f4fb; --template-text-dark: #495057; @@ -61,65 +62,65 @@ ?> - - - - - - -
    - - - -
    -
    -
    -
    -
    - -
    - -
    - -
    -
    -
    - -
    -
    -
    -
    - -
    - + + + + + + +
    + + + +
    +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    + +
    +
    +
    +
    + +
    + diff --git a/installation/tmpl/error/default.php b/installation/tmpl/error/default.php index 53e4239ea0e97..0bbde2a434c0f 100644 --- a/installation/tmpl/error/default.php +++ b/installation/tmpl/error/default.php @@ -1,4 +1,5 @@
    -
    -
    -
    -
    - -
    -
    - options as $option) : ?> - state === 'JNO' || $option->state === false) : ?> -
    -
    - -
    -
    - label; ?> -

    notice; ?>

    -
    -
    - - -
    -
    -
    -
    +
    +
    +
    +
    + +
    +
    + options as $option) : ?> + state === 'JNO' || $option->state === false) : ?> +
    +
    + +
    +
    + label; ?> +

    notice; ?>

    +
    +
    + + +
    +
    +
    +
    diff --git a/installation/tmpl/remove/default.php b/installation/tmpl/remove/default.php index 804a64d05a7fe..e9ebcdcdd7a14 100644 --- a/installation/tmpl/remove/default.php +++ b/installation/tmpl/remove/default.php @@ -1,4 +1,5 @@
    -
    - - - -
    -

    -
    - -
    -
    +
    + + + +
    +

    +
    + +
    +
    - installed_languages->administrator) > 1) : ?> -
    - -
    - -

    - - - - - - - - - - installed_languages->administrator as $i => $lang) : ?> - - - - - - - -
    - - - - - -
    - published) echo 'checked="checked"'; ?> - /> - - - - language; ?> -
    -

    - - - - - - - - - - installed_languages->frontend as $i => $lang) : ?> - - - - - - - -
    - - - - - -
    - published) echo 'checked="checked"'; ?> - /> - - - - language; ?> -
    - - -
    -
    + installed_languages->administrator) > 1) : ?> +
    + +
    + +

    + + + + + + + + + + installed_languages->administrator as $i => $lang) : ?> + + + + + + + +
    + + + + + +
    + published) { + echo 'checked="checked"'; + } ?> + /> + + + + language; ?> +
    +

    + + + + + + + + + + installed_languages->frontend as $i => $lang) : ?> + + + + + + + +
    + + + + + +
    + published) { + echo 'checked="checked"'; + } ?> + /> + + + + language; ?> +
    + + +
    +
    -
    -
    - - phpsettings as $setting) : ?> - state !== $setting->recommended) : ?> - - - - - - - - - - - - - - - phpsettings as $setting) : ?> - state !== $setting->recommended) : ?> - - - - - - - - -
    - -
    - - - - - -
    - label; ?> - - - recommended ? 'JON' : 'JOFF'); ?> - - - - state ? 'JON' : 'JOFF'); ?> - -
    +
    +
    + + phpsettings as $setting) : ?> + state !== $setting->recommended) : ?> + + + + + + + + + + + + + + + phpsettings as $setting) : ?> + state !== $setting->recommended) : ?> + + + + + + + + +
    + +
    + + + + + +
    + label; ?> + + + recommended ? 'JON' : 'JOFF'); ?> + + + + state ? 'JON' : 'JOFF'); ?> + +
    - - development) : ?> -
    - - -
    - - + + development) : ?> +
    + + +
    + + -
    - - -
    -
    -
    +
    + + +
    +
    +
    -
    - - - -
    - items) : ?> -

    -

    - - - - -

    -

    - -
    - -

    - - - - - - - - - - - - - getShortVersion()); ?> - items as $i => $language) : ?> - - element, $element); ?> - code = $element[1]; ?> - - - - - - - - -
    - - - - - - - -
    - - - - - code; ?> - - - version, 0, 3) != $version::MAJOR_VERSION . '.' . $version::MINOR_VERSION || substr($language->version, 0, 5) != $currentShortVersion) : ?> - version; ?> - - version; ?> - -
    - - -
    - - -
    -
    -
    -
    +
    + + + +
    + items) : ?> +

    +

    + + + + +

    +

    + +
    + +

    + + + + + + + + + + + + + getShortVersion()); ?> + items as $i => $language) : ?> + + element, $element); ?> + code = $element[1]; ?> + + + + + + + + +
    + + + + + + + +
    + + + + + code; ?> + + + version, 0, 3) != $version::MAJOR_VERSION . '.' . $version::MINOR_VERSION || substr($language->version, 0, 5) != $currentShortVersion) : ?> + version; ?> + + version; ?> + +
    + + +
    + + +
    +
    +
    +
    -
    - - - -
    -

    -
    -
    +
    + + + +
    +

    +
    +
    diff --git a/installation/tmpl/setup/default.php b/installation/tmpl/setup/default.php index 628163e1f8b01..2cdf7b4adc4ff 100644 --- a/installation/tmpl/setup/default.php +++ b/installation/tmpl/setup/default.php @@ -1,4 +1,5 @@
    -
    -
    - - - -
    -
    - form->renderField('language'); ?> -
    - - - -
    -
    -
    -
    -
    - - - -
    -
    - form->renderField('site_name'); ?> -
    -
    - -
    -
    -
    -
    - - - -
    -
    - form->renderField('admin_user'); ?> -
    -
    - form->renderField('admin_username'); ?> -
    -
    - form->renderField('admin_password'); ?> -
    -
    - form->renderField('admin_email'); ?> -
    -
    - -
    -
    -
    -
    - - - -
    -
    - form->renderField('db_type'); ?> -
    -
    - form->renderField('db_host'); ?> -
    -
    - form->renderField('db_user'); ?> -
    -
    - form->renderField('db_pass'); ?> -
    -
    - form->renderField('db_name'); ?> -
    -
    - form->renderField('db_prefix'); ?> -
    -
    - form->renderField('db_encryption'); ?> -
    -
    - form->renderField('db_sslkey'); ?> -
    -
    - form->renderField('db_sslcert'); ?> -
    -
    - form->renderField('db_sslverifyservercert'); ?> -
    -
    - form->renderField('db_sslca'); ?> -
    -
    - form->renderField('db_sslcipher'); ?> -
    -
    - form->getLabel('db_old'); ?> - form->getInput('db_old'); ?> -
    -
    - -
    -
    -
    + +
    + + + +
    +
    + form->renderField('language'); ?> +
    + + + +
    +
    +
    +
    +
    + + + +
    +
    + form->renderField('site_name'); ?> +
    +
    + +
    +
    +
    +
    + + + +
    +
    + form->renderField('admin_user'); ?> +
    +
    + form->renderField('admin_username'); ?> +
    +
    + form->renderField('admin_password'); ?> +
    +
    + form->renderField('admin_email'); ?> +
    +
    + +
    +
    +
    +
    + + + +
    +
    + form->renderField('db_type'); ?> +
    +
    + form->renderField('db_host'); ?> +
    +
    + form->renderField('db_user'); ?> +
    +
    + form->renderField('db_pass'); ?> +
    +
    + form->renderField('db_name'); ?> +
    +
    + form->renderField('db_prefix'); ?> +
    +
    + form->renderField('db_encryption'); ?> +
    +
    + form->renderField('db_sslkey'); ?> +
    +
    + form->renderField('db_sslcert'); ?> +
    +
    + form->renderField('db_sslverifyservercert'); ?> +
    +
    + form->renderField('db_sslca'); ?> +
    +
    + form->renderField('db_sslcipher'); ?> +
    +
    + form->getLabel('db_old'); ?> + form->getInput('db_old'); ?> +
    +
    + +
    +
    +
    - - -
    + + +
    diff --git a/language/en-GB/localise.php b/language/en-GB/localise.php index 8b46cfdc49ff1..64f1699e3b22c 100644 --- a/language/en-GB/localise.php +++ b/language/en-GB/localise.php @@ -1,4 +1,5 @@ content === '') -{ - return; +if ((string) $module->content === '') { + return; } $moduleTag = htmlspecialchars($params->get('module_tag', 'div'), ENT_QUOTES, 'UTF-8'); @@ -31,27 +31,25 @@ $headerAttribs = []; // Only output a header class if one is set -if ($headerClass !== '') -{ - $headerAttribs['class'] = $headerClass; +if ($headerClass !== '') { + $headerAttribs['class'] = $headerClass; } // Only add aria if the moduleTag is not a div -if ($moduleTag !== 'div') -{ - if ($module->showtitle) : - $moduleAttribs['aria-labelledby'] = 'mod-' . $module->id; - $headerAttribs['id'] = 'mod-' . $module->id; - else: - $moduleAttribs['aria-label'] = $module->title; - endif; +if ($moduleTag !== 'div') { + if ($module->showtitle) : + $moduleAttribs['aria-labelledby'] = 'mod-' . $module->id; + $headerAttribs['id'] = 'mod-' . $module->id; + else : + $moduleAttribs['aria-label'] = $module->title; + endif; } $header = '<' . $headerTag . ' ' . ArrayHelper::toString($headerAttribs) . '>' . $module->title . ''; ?> < > - showtitle) : ?> - - - content; ?> + showtitle) : ?> + + + content; ?> > diff --git a/layouts/chromes/none.php b/layouts/chromes/none.php index aeab58cab272c..8c8c4077a34f1 100644 --- a/layouts/chromes/none.php +++ b/layouts/chromes/none.php @@ -1,4 +1,5 @@ getDocument() - ->getWebAssetManager() - ->registerAndUseStyle('layouts.chromes.outline', 'layouts/chromes/outline.css'); + ->getWebAssetManager() + ->registerAndUseStyle('layouts.chromes.outline', 'layouts/chromes/outline.css'); $module = $displayData['module']; ?>
    -
    -
    - position); ?> -
    -
    - style); ?> -
    -
    -
    - content; ?> -
    +
    +
    + position); ?> +
    +
    + style); ?> +
    +
    +
    + content; ?> +
    diff --git a/layouts/chromes/table.php b/layouts/chromes/table.php index 7d0fc8fe5b0e3..b19b8c1c2dbfe 100644 --- a/layouts/chromes/table.php +++ b/layouts/chromes/table.php @@ -1,4 +1,5 @@ - showtitle) : ?> - - - - - - - + class="moduletable get('moduleclass_sfx'), ENT_COMPAT, 'UTF-8'); ?>"> + showtitle) : ?> + + + + + + +
    - title; ?> -
    - content; ?> -
    + title; ?> +
    + content; ?> +
    diff --git a/layouts/joomla/button/action-button.php b/layouts/joomla/button/action-button.php index e3c630e7ca28e..6c90c112ff1b0 100644 --- a/layouts/joomla/button/action-button.php +++ b/layouts/joomla/button/action-button.php @@ -1,4 +1,5 @@ diff --git a/layouts/joomla/button/iconclass.php b/layouts/joomla/button/iconclass.php index 0697d8daa571d..ca0e74e7d3a51 100644 --- a/layouts/joomla/button/iconclass.php +++ b/layouts/joomla/button/iconclass.php @@ -1,4 +1,5 @@ -
    - - - - escape($options['title'])), - HTMLHelper::_('select.option', '-1', '--------', ['disable' => true]) - ]; +
    + + + + escape($options['title'])), + HTMLHelper::_('select.option', '-1', '--------', ['disable' => true]) + ]; - $transitions = array_merge($default, $options['transitions']); + $transitions = array_merge($default, $options['transitions']); - $attribs = [ - 'id' => 'transition-select_' . (int) $row ?? '', - 'list.attr' => [ - 'class' => 'form-select form-select-sm w-auto', - 'onchange' => "this.form.transition_id.value=this.value;Joomla.listItemTask('" . $checkboxName . $this->escape($row ?? '') . "', '" . $task . "')"] - ]; + $attribs = [ + 'id' => 'transition-select_' . (int) $row ?? '', + 'list.attr' => [ + 'class' => 'form-select form-select-sm w-auto', + 'onchange' => "this.form.transition_id.value=this.value;Joomla.listItemTask('" . $checkboxName . $this->escape($row ?? '') . "', '" . $task . "')"] + ]; - echo HTMLHelper::_('select.genericlist', $transitions, '', $attribs); - ?> -
    + echo HTMLHelper::_('select.genericlist', $transitions, '', $attribs); + ?> +
    diff --git a/layouts/joomla/content/associations.php b/layouts/joomla/content/associations.php index 57ebb493ea261..9104f74e96210 100644 --- a/layouts/joomla/content/associations.php +++ b/layouts/joomla/content/associations.php @@ -1,4 +1,5 @@ -
      - $item) : ?> - -
    • - -
    • - link)) : ?> -
    • - link; ?> -
    • - - -
    +
      + $item) : ?> + +
    • + +
    • + link)) : ?> +
    • + link; ?> +
    • + + +
    diff --git a/layouts/joomla/content/blog_style_default_item_title.php b/layouts/joomla/content/blog_style_default_item_title.php index 24a594496a065..7a3128e50f058 100644 --- a/layouts/joomla/content/blog_style_default_item_title.php +++ b/layouts/joomla/content/blog_style_default_item_title.php @@ -1,4 +1,5 @@ format('Y-m-d H:i:s'); $link = RouteHelper::getArticleRoute($displayData->slug, $displayData->catid, $displayData->language); ?> -state == 0 || $params->get('show_title') || ($params->get('show_author') && !empty($displayData->author ))) : ?> - +state == 0 || $params->get('show_title') || ($params->get('show_author') && !empty($displayData->author))) : ?> + diff --git a/layouts/joomla/content/categories_default.php b/layouts/joomla/content/categories_default.php index baab179cba025..6d1b30e84f9bd 100644 --- a/layouts/joomla/content/categories_default.php +++ b/layouts/joomla/content/categories_default.php @@ -1,4 +1,5 @@ params->get('show_page_heading')) : ?>

    - escape($displayData->params->get('page_heading')); ?> + escape($displayData->params->get('page_heading')); ?>

    params->get('show_base_description')) : ?> - - params->get('categories_description')) : ?> -
    - params->get('categories_description'), '', $displayData->get('extension') . '.categories'); ?> -
    - - - parent->description) : ?> -
    - parent->description, '', $displayData->parent->extension . '.categories'); ?> -
    - - + + params->get('categories_description')) : ?> +
    + params->get('categories_description'), '', $displayData->get('extension') . '.categories'); ?> +
    + + + parent->description) : ?> +
    + parent->description, '', $displayData->parent->extension . '.categories'); ?> +
    + + diff --git a/layouts/joomla/content/categories_default_items.php b/layouts/joomla/content/categories_default_items.php index 49e820c9b7c43..8cf8681b71a03 100644 --- a/layouts/joomla/content/categories_default_items.php +++ b/layouts/joomla/content/categories_default_items.php @@ -1,4 +1,5 @@ extension; $canEdit = $params->get('access-edit'); $className = substr($extension, 4); -$htag = $params->get('show_page_heading') ? 'h2' : 'h1'; +$htag = $params->get('show_page_heading') ? 'h2' : 'h1'; $app = Factory::getApplication(); @@ -44,59 +45,58 @@ * This will work for the core components but not necessarily for other components * that may have different pluralisation rules. */ -if (substr($className, -1) === 's') -{ - $className = rtrim($className, 's'); +if (substr($className, -1) === 's') { + $className = rtrim($className, 's'); } $tagsData = $category->tags->itemTags; ?>
    - get('show_page_heading')) : ?> -

    - escape($params->get('page_heading')); ?> -

    - + get('show_page_heading')) : ?> +

    + escape($params->get('page_heading')); ?> +

    + - get('show_category_title', 1)) : ?> - <> - title, '', $extension . '.category.title'); ?> - > - - + get('show_category_title', 1)) : ?> + <> + title, '', $extension . '.category.title'); ?> + > + + - get('show_cat_tags', 1)) : ?> - - + get('show_cat_tags', 1)) : ?> + + - get('show_description', 1) || $params->def('show_description_image', 1)) : ?> -
    - get('show_description_image') && $category->getParams()->get('image')) : ?> - $category->getParams()->get('image'), - 'alt' => empty($category->getParams()->get('image_alt')) && empty($category->getParams()->get('image_alt_empty')) ? false : $category->getParams()->get('image_alt'), - ] - ); ?> - - - get('show_description') && $category->description) : ?> - description, '', $extension . '.category.description'); ?> - - -
    - - loadTemplate($displayData->subtemplatename); ?> + get('show_description', 1) || $params->def('show_description_image', 1)) : ?> +
    + get('show_description_image') && $category->getParams()->get('image')) : ?> + $category->getParams()->get('image'), + 'alt' => empty($category->getParams()->get('image_alt')) && empty($category->getParams()->get('image_alt_empty')) ? false : $category->getParams()->get('image_alt'), + ] + ); ?> + + + get('show_description') && $category->description) : ?> + description, '', $extension . '.category.description'); ?> + + +
    + + loadTemplate($displayData->subtemplatename); ?> - maxLevel != 0 && $displayData->get('children')) : ?> -
    - get('show_category_heading_title_text', 1) == 1) : ?> -

    - -

    - - loadTemplate('children'); ?> -
    - + maxLevel != 0 && $displayData->get('children')) : ?> +
    + get('show_category_heading_title_text', 1) == 1) : ?> +

    + +

    + + loadTemplate('children'); ?> +
    +
    diff --git a/layouts/joomla/content/emptystate.php b/layouts/joomla/content/emptystate.php index d938227d0691e..e78d3a2752897 100644 --- a/layouts/joomla/content/emptystate.php +++ b/layouts/joomla/content/emptystate.php @@ -1,4 +1,5 @@ input->get('option')); +if (!$textPrefix) { + $textPrefix = strtoupper(Factory::getApplication()->input->get('option')); } $formURL = $displayData['formURL'] ?? ''; @@ -33,32 +33,32 @@
    -
    - -

    -
    -

    - -

    -
    - input->get('tmpl') !== 'component') : ?> - - - - - -
    -
    -
    +
    + +

    +
    +

    + +

    +
    + input->get('tmpl') !== 'component') : ?> + + + + + +
    +
    +
    - + - - - + + +
    diff --git a/layouts/joomla/content/emptystate_module.php b/layouts/joomla/content/emptystate_module.php index c89d4703d8d21..46be472e68f6d 100644 --- a/layouts/joomla/content/emptystate_module.php +++ b/layouts/joomla/content/emptystate_module.php @@ -1,4 +1,5 @@ getLanguage()->hasKey($moduleLangString) ? $moduleLangString : $componentLangString; +if (!$title) { + // Can we find a *_EMPTYSTATE_MODULE_TITLE translation, Else use the components *_EMPTYSTATE_TITLE string + $title = Factory::getApplication()->getLanguage()->hasKey($moduleLangString) ? $moduleLangString : $componentLangString; } ?>
    -

    - -

    +

    + +

    diff --git a/layouts/joomla/content/full_image.php b/layouts/joomla/content/full_image.php index 44b43401b96c8..83fcc57b4489e 100644 --- a/layouts/joomla/content/full_image.php +++ b/layouts/joomla/content/full_image.php @@ -1,4 +1,5 @@ params; $images = json_decode($displayData->images); -if (empty($images->image_fulltext)) -{ - return; +if (empty($images->image_fulltext)) { + return; } $imgclass = empty($images->float_fulltext) ? $params->get('float_fulltext') : $images->float_fulltext; $layoutAttr = [ - 'src' => $images->image_fulltext, - 'itemprop' => 'image', - 'alt' => empty($images->image_fulltext_alt) && empty($images->image_fulltext_alt_empty) ? false : $images->image_fulltext_alt, + 'src' => $images->image_fulltext, + 'itemprop' => 'image', + 'alt' => empty($images->image_fulltext_alt) && empty($images->image_fulltext_alt_empty) ? false : $images->image_fulltext_alt, ]; ?>
    - - image_fulltext_caption) && $images->image_fulltext_caption !== '') : ?> -
    escape($images->image_fulltext_caption); ?>
    - + + image_fulltext_caption) && $images->image_fulltext_caption !== '') : ?> +
    escape($images->image_fulltext_caption); ?>
    +
    diff --git a/layouts/joomla/content/icons.php b/layouts/joomla/content/icons.php index ba1d27136d28e..3369c65429dc4 100644 --- a/layouts/joomla/content/icons.php +++ b/layouts/joomla/content/icons.php @@ -1,4 +1,5 @@ -
    -
    -
    - -
    -
    -
    +
    +
    +
    + +
    +
    +
    diff --git a/layouts/joomla/content/icons/create.php b/layouts/joomla/content/icons/create.php index 74183d7b571f4..2a78ae4c1b034 100644 --- a/layouts/joomla/content/icons/create.php +++ b/layouts/joomla/content/icons/create.php @@ -1,4 +1,5 @@ get('show_icons')) : ?> - - + + - + diff --git a/layouts/joomla/content/icons/edit.php b/layouts/joomla/content/icons/edit.php index fc6702dbed57b..117b232208cb3 100644 --- a/layouts/joomla/content/icons/edit.php +++ b/layouts/joomla/content/icons/edit.php @@ -1,4 +1,5 @@ state ? 'edit' : 'eye-slash'; $currentDate = Factory::getDate()->format('Y-m-d H:i:s'); $isUnpublished = ($article->publish_up > $currentDate) - || !is_null($article->publish_down) && ($article->publish_down < $currentDate); + || !is_null($article->publish_down) && ($article->publish_down < $currentDate); -if ($isUnpublished) -{ - $icon = 'eye-slash'; +if ($isUnpublished) { + $icon = 'eye-slash'; } $aria_described = 'editarticle-' . (int) $article->id; ?> - + diff --git a/layouts/joomla/content/icons/edit_lock.php b/layouts/joomla/content/icons/edit_lock.php index 3b8def519104c..4ebd721fd36c7 100644 --- a/layouts/joomla/content/icons/edit_lock.php +++ b/layouts/joomla/content/icons/edit_lock.php @@ -1,4 +1,5 @@ id; -} -elseif (isset($displayData['contact'])) -{ - $contact = $displayData['contact']; - $aria_described = 'editcontact-' . (int) $contact->id; +if (isset($displayData['ariaDescribed'])) { + $aria_described = $displayData['ariaDescribed']; +} elseif (isset($displayData['article'])) { + $article = $displayData['article']; + $aria_described = 'editarticle-' . (int) $article->id; +} elseif (isset($displayData['contact'])) { + $contact = $displayData['contact']; + $aria_described = 'editcontact-' . (int) $contact->id; } $tooltip = $displayData['tooltip']; ?> - + diff --git a/layouts/joomla/content/info_block.php b/layouts/joomla/content/info_block.php index 402e1179595f2..5a2929993cf23 100644 --- a/layouts/joomla/content/info_block.php +++ b/layouts/joomla/content/info_block.php @@ -1,4 +1,5 @@ diff --git a/layouts/joomla/content/info_block/associations.php b/layouts/joomla/content/info_block/associations.php index 9732c557c3163..cfba66374ac34 100644 --- a/layouts/joomla/content/info_block/associations.php +++ b/layouts/joomla/content/info_block/associations.php @@ -1,4 +1,5 @@ associations)) : ?> -associations; ?> + associations; ?>
    - - - - params->get('flags', 1) && $association['language']->image) : ?> - image . '.gif', $association['language']->title_native, array('title' => $association['language']->title_native), true); ?> - - - lang_code); ?> - lang_code; ?> - title_native; ?> - - - + + + + params->get('flags', 1) && $association['language']->image) : ?> + image . '.gif', $association['language']->title_native, array('title' => $association['language']->title_native), true); ?> + + + lang_code); ?> + lang_code; ?> + title_native; ?> + + +
    diff --git a/layouts/joomla/content/info_block/author.php b/layouts/joomla/content/info_block/author.php index 5d744172c88ab..dcfaf6f851e6e 100644 --- a/layouts/joomla/content/info_block/author.php +++ b/layouts/joomla/content/info_block/author.php @@ -1,4 +1,5 @@ diff --git a/layouts/joomla/content/info_block/category.php b/layouts/joomla/content/info_block/category.php index 2803fe3c91196..4006b0f345fc1 100644 --- a/layouts/joomla/content/info_block/category.php +++ b/layouts/joomla/content/info_block/category.php @@ -1,4 +1,5 @@
    - 'icon-folder-open icon-fw']); ?> - escape($displayData['item']->category_title); ?> - get('link_category') && !empty($displayData['item']->catid)) : ?> - catid, $displayData['item']->category_language) - ) - . '" itemprop="genre">' . $title . ''; ?> - - - ' . $title . ''); ?> - + 'icon-folder-open icon-fw']); ?> + escape($displayData['item']->category_title); ?> + get('link_category') && !empty($displayData['item']->catid)) : ?> + catid, $displayData['item']->category_language) + ) + . '" itemprop="genre">' . $title . ''; ?> + + + ' . $title . ''); ?> +
    diff --git a/layouts/joomla/content/info_block/create_date.php b/layouts/joomla/content/info_block/create_date.php index 75cdc81ca6674..0fed237346ebc 100644 --- a/layouts/joomla/content/info_block/create_date.php +++ b/layouts/joomla/content/info_block/create_date.php @@ -1,4 +1,5 @@
    - - + +
    diff --git a/layouts/joomla/content/info_block/hits.php b/layouts/joomla/content/info_block/hits.php index 8d454f3460590..1ff76321b969b 100644 --- a/layouts/joomla/content/info_block/hits.php +++ b/layouts/joomla/content/info_block/hits.php @@ -1,4 +1,5 @@
    - - - hits); ?> + + + hits); ?>
    diff --git a/layouts/joomla/content/info_block/modify_date.php b/layouts/joomla/content/info_block/modify_date.php index 568b72b01ae3c..5c2aaf90ea1fc 100644 --- a/layouts/joomla/content/info_block/modify_date.php +++ b/layouts/joomla/content/info_block/modify_date.php @@ -1,4 +1,5 @@
    - - + +
    diff --git a/layouts/joomla/content/info_block/parent_category.php b/layouts/joomla/content/info_block/parent_category.php index 663a94674482d..39a3110625ce8 100644 --- a/layouts/joomla/content/info_block/parent_category.php +++ b/layouts/joomla/content/info_block/parent_category.php @@ -1,4 +1,5 @@
    - 'icon-folder icon-fw']); ?> - escape($displayData['item']->parent_title); ?> - get('link_parent_category') && !empty($displayData['item']->parent_id)) : ?> - parent_id, $displayData['item']->parent_language) - ) - . '" itemprop="genre">' . $title . ''; ?> - - - ' . $title . ''); ?> - + 'icon-folder icon-fw']); ?> + escape($displayData['item']->parent_title); ?> + get('link_parent_category') && !empty($displayData['item']->parent_id)) : ?> + parent_id, $displayData['item']->parent_language) + ) + . '" itemprop="genre">' . $title . ''; ?> + + + ' . $title . ''); ?> +
    diff --git a/layouts/joomla/content/info_block/publish_date.php b/layouts/joomla/content/info_block/publish_date.php index 8a528abb756c3..04fbb4492a13c 100644 --- a/layouts/joomla/content/info_block/publish_date.php +++ b/layouts/joomla/content/info_block/publish_date.php @@ -1,4 +1,5 @@
    - - + +
    diff --git a/layouts/joomla/content/intro_image.php b/layouts/joomla/content/intro_image.php index 9c71886774220..f7db3b42a9474 100644 --- a/layouts/joomla/content/intro_image.php +++ b/layouts/joomla/content/intro_image.php @@ -1,4 +1,5 @@ params; $images = json_decode($displayData->images); -if (empty($images->image_intro)) -{ - return; +if (empty($images->image_intro)) { + return; } $imgclass = empty($images->float_intro) ? $params->get('float_intro') : $images->float_intro; $layoutAttr = [ - 'src' => $images->image_intro, - 'alt' => empty($images->image_intro_alt) && empty($images->image_intro_alt_empty) ? false : $images->image_intro_alt, + 'src' => $images->image_intro, + 'alt' => empty($images->image_intro_alt) && empty($images->image_intro_alt_empty) ? false : $images->image_intro_alt, ]; ?>
    - get('link_intro_image') && ($params->get('access-view') || $params->get('show_noauth', '0') == '1')) : ?> - - - 'thumbnail'])); ?> - - image_intro_caption) && $images->image_intro_caption !== '') : ?> -
    escape($images->image_intro_caption); ?>
    - + get('link_intro_image') && ($params->get('access-view') || $params->get('show_noauth', '0') == '1')) : ?> + + + 'thumbnail'])); ?> + + image_intro_caption) && $images->image_intro_caption !== '') : ?> +
    escape($images->image_intro_caption); ?>
    +
    diff --git a/layouts/joomla/content/language.php b/layouts/joomla/content/language.php index b77ac4fd87b1e..90daa395e50f4 100644 --- a/layouts/joomla/content/language.php +++ b/layouts/joomla/content/language.php @@ -1,4 +1,5 @@ language === '*') -{ - echo Text::alt('JALL', 'language'); -} -elseif ($item->language_image) -{ - echo HTMLHelper::_('image', 'mod_languages/' . $item->language_image . '.gif', '', array('class' => 'me-1'), true) . htmlspecialchars($item->language_title, ENT_COMPAT, 'UTF-8'); -} -elseif ($item->language_title) -{ - echo htmlspecialchars($item->language_title, ENT_COMPAT, 'UTF-8'); -} -else -{ - echo Text::_('JUNDEFINED'); +if ($item->language === '*') { + echo Text::alt('JALL', 'language'); +} elseif ($item->language_image) { + echo HTMLHelper::_('image', 'mod_languages/' . $item->language_image . '.gif', '', array('class' => 'me-1'), true) . htmlspecialchars($item->language_title, ENT_COMPAT, 'UTF-8'); +} elseif ($item->language_title) { + echo htmlspecialchars($item->language_title, ENT_COMPAT, 'UTF-8'); +} else { + echo Text::_('JUNDEFINED'); } diff --git a/layouts/joomla/content/options_default.php b/layouts/joomla/content/options_default.php index 77f8f0a431233..9d0981f495d53 100644 --- a/layouts/joomla/content/options_default.php +++ b/layouts/joomla/content/options_default.php @@ -1,4 +1,5 @@
    - name; ?> - description)) : ?> -

    description; ?>

    - - fieldsname); ?> -
    - - form->getFieldset($fieldname) as $field) : ?> - - type === 'Spacer' ? ' field-spacer' : ''; ?> - showon) : ?> - useScript('showon'); ?> - showon, $field->formControl, $field->group)) . '\''; ?> - + name; ?> + description)) : ?> +

    description; ?>

    + + fieldsname); ?> +
    + + form->getFieldset($fieldname) as $field) : ?> + + type === 'Spacer' ? ' field-spacer' : ''; ?> + showon) : ?> + useScript('showon'); ?> + showon, $field->formControl, $field->group)) . '\''; ?> + - showlabel)) : ?> -
    > -
    input; ?>
    -
    - - renderField(); ?> - - - -
    + showlabel)) : ?> +
    > +
    input; ?>
    +
    + + renderField(); ?> + + + +
    diff --git a/layouts/joomla/content/readmore.php b/layouts/joomla/content/readmore.php index 7b4f89e0f1716..53127a969bdbf 100644 --- a/layouts/joomla/content/readmore.php +++ b/layouts/joomla/content/readmore.php @@ -1,4 +1,5 @@

    - get('access-view')) : ?> - - '; ?> - - - alternative_readmore) : ?> - - '; ?> - - get('show_readmore_title', 0) != 0) : ?> - title, $params->get('readmore_limit')); ?> - - - get('show_readmore_title', 0) == 0) : ?> - - '; ?> - - - - - '; ?> - title, $params->get('readmore_limit'))); ?> - - + get('access-view')) : ?> + + '; ?> + + + alternative_readmore) : ?> + + '; ?> + + get('show_readmore_title', 0) != 0) : ?> + title, $params->get('readmore_limit')); ?> + + + get('show_readmore_title', 0) == 0) : ?> + + '; ?> + + + + + '; ?> + title, $params->get('readmore_limit'))); ?> + +

    diff --git a/layouts/joomla/content/tags.php b/layouts/joomla/content/tags.php index d706485fca053..1bef066ea1a85 100644 --- a/layouts/joomla/content/tags.php +++ b/layouts/joomla/content/tags.php @@ -1,4 +1,5 @@ - + diff --git a/layouts/joomla/content/text_filters.php b/layouts/joomla/content/text_filters.php index 215b081f94771..3fc04eaae84db 100644 --- a/layouts/joomla/content/text_filters.php +++ b/layouts/joomla/content/text_filters.php @@ -1,4 +1,5 @@
    - name; ?> -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    - fieldsname); ?> - - form->getFieldset($fieldname) as $field) : ?> -
    input; ?>
    - - + name; ?> +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    + fieldsname); ?> + + form->getFieldset($fieldname) as $field) : ?> +
    input; ?>
    + +
    diff --git a/layouts/joomla/edit/admin_modules.php b/layouts/joomla/edit/admin_modules.php index f7caf445c3e65..f5d84253ce2de 100644 --- a/layouts/joomla/edit/admin_modules.php +++ b/layouts/joomla/edit/admin_modules.php @@ -1,4 +1,5 @@ input; $fields = $displayData->get('fields') ?: array( - array('parent', 'parent_id'), - array('published', 'state', 'enabled'), - array('category', 'catid'), - 'featured', - 'sticky', - 'access', - 'language', - 'tags', - 'note', - 'version_note', + array('parent', 'parent_id'), + array('published', 'state', 'enabled'), + array('category', 'catid'), + 'featured', + 'sticky', + 'access', + 'language', + 'tags', + 'note', + 'version_note', ); $hiddenFields = $displayData->get('hidden_fields') ?: array(); -if (!ModuleHelper::isAdminMultilang()) -{ - $hiddenFields[] = 'language'; - $form->setFieldAttribute('language', 'default', '*'); +if (!ModuleHelper::isAdminMultilang()) { + $hiddenFields[] = 'language'; + $form->setFieldAttribute('language', 'default', '*'); } $html = array(); $html[] = '
    '; -foreach ($fields as $field) -{ - foreach ((array) $field as $f) - { - if ($form->getField($f)) - { - if (in_array($f, $hiddenFields)) - { - $form->setFieldAttribute($f, 'type', 'hidden'); - } - - $html[] = $form->renderField($f); - break; - } - } +foreach ($fields as $field) { + foreach ((array) $field as $f) { + if ($form->getField($f)) { + if (in_array($f, $hiddenFields)) { + $form->setFieldAttribute($f, 'type', 'hidden'); + } + + $html[] = $form->renderField($f); + break; + } + } } $html[] = '
    '; diff --git a/layouts/joomla/edit/associations.php b/layouts/joomla/edit/associations.php index 96e59f01d2057..423cc21beb674 100644 --- a/layouts/joomla/edit/associations.php +++ b/layouts/joomla/edit/associations.php @@ -1,4 +1,5 @@ getForm(); $options = array( - 'formControl' => $form->getFormControl(), - 'hidden' => (int) ($form->getValue('language', null, '*') === '*'), + 'formControl' => $form->getFormControl(), + 'hidden' => (int) ($form->getValue('language', null, '*') === '*'), ); // Load JavaScript message titles diff --git a/layouts/joomla/edit/fieldset.php b/layouts/joomla/edit/fieldset.php index ab45d78010c97..0a20a3beb8a0a 100644 --- a/layouts/joomla/edit/fieldset.php +++ b/layouts/joomla/edit/fieldset.php @@ -1,4 +1,5 @@ get('fieldset'); $fieldSet = $form->getFieldset($name); -if (empty($fieldSet)) -{ - return; +if (empty($fieldSet)) { + return; } $ignoreFields = $displayData->get('ignore_fields') ? : array(); $extraFields = $displayData->get('extra_fields') ? : array(); -if (!empty($displayData->showOptions) || $displayData->get('show_options', 1)) -{ - if (isset($extraFields[$name])) - { - foreach ($extraFields[$name] as $f) - { - if (in_array($f, $ignoreFields)) - { - continue; - } - if ($form->getField($f)) - { - $fieldSet[] = $form->getField($f); - } - } - } - - $html = array(); - - foreach ($fieldSet as $field) - { - $html[] = $field->renderField(); - } - - echo implode('', $html); -} -else -{ - $html = array(); - $html[] = ''; - - echo implode('', $html); +if (!empty($displayData->showOptions) || $displayData->get('show_options', 1)) { + if (isset($extraFields[$name])) { + foreach ($extraFields[$name] as $f) { + if (in_array($f, $ignoreFields)) { + continue; + } + if ($form->getField($f)) { + $fieldSet[] = $form->getField($f); + } + } + } + + $html = array(); + + foreach ($fieldSet as $field) { + $html[] = $field->renderField(); + } + + echo implode('', $html); +} else { + $html = array(); + $html[] = ''; + + echo implode('', $html); } diff --git a/layouts/joomla/edit/frontediting_modules.php b/layouts/joomla/edit/frontediting_modules.php index 14a95464a037b..373a2cde9ac4a 100644 --- a/layouts/joomla/edit/frontediting_modules.php +++ b/layouts/joomla/edit/frontediting_modules.php @@ -1,4 +1,5 @@ id; // If Module editing site -if ($parameters->get('redirect_edit', 'site') === 'site') -{ - $editUrl = Uri::base() . 'index.php?option=com_config&view=modules&id=' . (int) $mod->id . '&Itemid=' . $itemid . $redirectUri; - $target = '_self'; +if ($parameters->get('redirect_edit', 'site') === 'site') { + $editUrl = Uri::base() . 'index.php?option=com_config&view=modules&id=' . (int) $mod->id . '&Itemid=' . $itemid . $redirectUri; + $target = '_self'; } // Add link for editing the module $count = 0; $moduleHtml = preg_replace( - // Find first tag of module - '/^(\s*<(?:div|span|nav|ul|ol|h\d|section|aside|address|article|form) [^>]*>)/', - // Create and add the edit link and tooltip - '\\1 + // Find first tag of module + '/^(\s*<(?:div|span|nav|ul|ol|h\d|section|aside|address|article|form) [^>]*>)/', + // Create and add the edit link and tooltip + '\\1 ' . Text::_('JGLOBAL_EDIT') . ' ', - $moduleHtml, - 1, - $count + $moduleHtml, + 1, + $count ); // If menu editing is enabled and allowed and it's a menu module add link for editing -if ($menusEditing && $mod->module === 'mod_menu') -{ - // find the menu item id - $regex = '/\bitem-(\d+)\b/'; +if ($menusEditing && $mod->module === 'mod_menu') { + // find the menu item id + $regex = '/\bitem-(\d+)\b/'; - preg_match_all($regex, $moduleHtml, $menuItemids); - if ($menuItemids) - { - foreach ($menuItemids[1] as $menuItemid) - { - $menuitemEditUrl = Uri::base() . 'administrator/index.php?option=com_menus&view=item&client_id=0&layout=edit&id=' . (int) $menuItemid; - $moduleHtml = preg_replace( - // Find the link - '/()/', - // Create and add the edit link - '\\1 + preg_match_all($regex, $moduleHtml, $menuItemids); + if ($menuItemids) { + foreach ($menuItemids[1] as $menuItemid) { + $menuitemEditUrl = Uri::base() . 'administrator/index.php?option=com_menus&view=item&client_id=0&layout=edit&id=' . (int) $menuItemid; + $moduleHtml = preg_replace( + // Find the link + '/()/', + // Create and add the edit link + '\\1 ', - $moduleHtml - ); - } - } + $moduleHtml + ); + } + } } diff --git a/layouts/joomla/edit/global.php b/layouts/joomla/edit/global.php index 4cee42c8a6c24..35f72e68959da 100644 --- a/layouts/joomla/edit/global.php +++ b/layouts/joomla/edit/global.php @@ -1,4 +1,5 @@ input; $component = $input->getCmd('option', 'com_content'); -if ($component === 'com_categories') -{ - $extension = $input->getCmd('extension', 'com_content'); - $parts = explode('.', $extension); - $component = $parts[0]; +if ($component === 'com_categories') { + $extension = $input->getCmd('extension', 'com_content'); + $parts = explode('.', $extension); + $component = $parts[0]; } $saveHistory = ComponentHelper::getParams($component)->get('save_history', 0); $fields = $displayData->get('fields') ?: array( - 'transition', - array('parent', 'parent_id'), - array('published', 'state', 'enabled'), - array('category', 'catid'), - 'featured', - 'sticky', - 'access', - 'language', - 'tags', - 'note', - 'version_note', + 'transition', + array('parent', 'parent_id'), + array('published', 'state', 'enabled'), + array('category', 'catid'), + 'featured', + 'sticky', + 'access', + 'language', + 'tags', + 'note', + 'version_note', ); $hiddenFields = $displayData->get('hidden_fields') ?: array(); -if (!$saveHistory) -{ - $hiddenFields[] = 'version_note'; +if (!$saveHistory) { + $hiddenFields[] = 'version_note'; } -if (!Multilanguage::isEnabled()) -{ - $hiddenFields[] = 'language'; - $form->setFieldAttribute('language', 'default', '*'); +if (!Multilanguage::isEnabled()) { + $hiddenFields[] = 'language'; + $form->setFieldAttribute('language', 'default', '*'); } $html = array(); $html[] = '
    '; $html[] = '' . Text::_('JGLOBAL_FIELDSET_GLOBAL') . ''; -foreach ($fields as $field) -{ - foreach ((array) $field as $f) - { - if ($form->getField($f)) - { - if (in_array($f, $hiddenFields)) - { - $form->setFieldAttribute($f, 'type', 'hidden'); - } +foreach ($fields as $field) { + foreach ((array) $field as $f) { + if ($form->getField($f)) { + if (in_array($f, $hiddenFields)) { + $form->setFieldAttribute($f, 'type', 'hidden'); + } - $html[] = $form->renderField($f); - break; - } - } + $html[] = $form->renderField($f); + break; + } + } } $html[] = '
    '; diff --git a/layouts/joomla/edit/metadata.php b/layouts/joomla/edit/metadata.php index ee5be2a76db13..c786035096202 100644 --- a/layouts/joomla/edit/metadata.php +++ b/layouts/joomla/edit/metadata.php @@ -1,4 +1,5 @@ $fieldSet) : ?> - description) && trim($fieldSet->description)) : ?> -
    - - escape(Text::_($fieldSet->description)); ?> -
    - - - renderField('metadesc'); - echo $form->renderField('metakey'); - } - - foreach ($form->getFieldset($name) as $field) - { - if ($field->name !== 'jform[metadata][tags][]') - { - echo $field->renderField(); - } - } ?> + description) && trim($fieldSet->description)) : ?> +
    + + escape(Text::_($fieldSet->description)); ?> +
    + + + renderField('metadesc'); + echo $form->renderField('metakey'); + } + + foreach ($form->getFieldset($name) as $field) { + if ($field->name !== 'jform[metadata][tags][]') { + echo $field->renderField(); + } + } ?> diff --git a/layouts/joomla/edit/params.php b/layouts/joomla/edit/params.php index ae9cd0630bb37..66ac506be20ea 100644 --- a/layouts/joomla/edit/params.php +++ b/layouts/joomla/edit/params.php @@ -1,4 +1,5 @@ getFieldsets(); $helper = $displayData->get('useCoreUI', false) ? 'uitab' : 'bootstrap'; -if (empty($fieldSets)) -{ - return; +if (empty($fieldSets)) { + return; } $ignoreFieldsets = $displayData->get('ignore_fieldsets') ?: array(); @@ -38,44 +38,38 @@ $configFieldsets = $displayData->get('configFieldsets') ?: array(); // Handle the hidden fieldsets when show_options is set false -if (!$displayData->get('show_options', 1)) -{ - // The HTML buffer - $html = array(); - - // Loop over the fieldsets - foreach ($fieldSets as $name => $fieldSet) - { - // Check if the fieldset should be ignored - if (in_array($name, $ignoreFieldsets, true)) - { - continue; - } - - // If it is a hidden fieldset, render the inputs - if (in_array($name, $hiddenFieldsets)) - { - // Loop over the fields - foreach ($form->getFieldset($name) as $field) - { - // Add only the input on the buffer - $html[] = $field->input; - } - - // Make sure the fieldset is not rendered twice - $ignoreFieldsets[] = $name; - } - - // Check if it is the correct fieldset to ignore - if (strpos($name, 'basic') === 0) - { - // Ignore only the fieldsets which are defined by the options not the custom fields ones - $ignoreFieldsets[] = $name; - } - } - - // Echo the hidden fieldsets - echo implode('', $html); +if (!$displayData->get('show_options', 1)) { + // The HTML buffer + $html = array(); + + // Loop over the fieldsets + foreach ($fieldSets as $name => $fieldSet) { + // Check if the fieldset should be ignored + if (in_array($name, $ignoreFieldsets, true)) { + continue; + } + + // If it is a hidden fieldset, render the inputs + if (in_array($name, $hiddenFieldsets)) { + // Loop over the fields + foreach ($form->getFieldset($name) as $field) { + // Add only the input on the buffer + $html[] = $field->input; + } + + // Make sure the fieldset is not rendered twice + $ignoreFieldsets[] = $name; + } + + // Check if it is the correct fieldset to ignore + if (strpos($name, 'basic') === 0) { + // Ignore only the fieldsets which are defined by the options not the custom fields ones + $ignoreFieldsets[] = $name; + } + } + + // Echo the hidden fieldsets + echo implode('', $html); } $opentab = false; @@ -83,131 +77,114 @@ $xml = $form->getXml(); // Loop again over the fieldsets -foreach ($fieldSets as $name => $fieldSet) -{ - // Ensure any fieldsets we don't want to show are skipped (including repeating formfield fieldsets) - if ((isset($fieldSet->repeat) && $fieldSet->repeat === true) - || in_array($name, $ignoreFieldsets) - || (!empty($configFieldsets) && in_array($name, $configFieldsets, true)) - || (!empty($hiddenFieldsets) && in_array($name, $hiddenFieldsets, true)) - ) - { - continue; - } - - // Determine the label - if (!empty($fieldSet->label)) - { - $label = Text::_($fieldSet->label); - } - else - { - $label = strtoupper('JGLOBAL_FIELDSET_' . $name); - if (Text::_($label) === $label) - { - $label = strtoupper($app->input->get('option') . '_' . $name . '_FIELDSET_LABEL'); - } - $label = Text::_($label); - } - - $hasChildren = $xml->xpath('//fieldset[@name="' . $name . '"]//fieldset[not(ancestor::field/form/*)]'); - $hasParent = $xml->xpath('//fieldset//fieldset[@name="' . $name . '"]'); - $isGrandchild = $xml->xpath('//fieldset//fieldset//fieldset[@name="' . $name . '"]'); - - if (!$isGrandchild && $hasParent) - { - echo '
    '; - echo '' . $label . ''; - - // Include the description when available - if (!empty($fieldSet->description)) - { - echo '
    '; - echo '' . Text::_('INFO') . ' '; - echo Text::_($fieldSet->description); - echo '
    '; - } - - echo '
    '; - } - // Tabs - elseif (!$hasParent) - { - if ($opentab) - { - if ($opentab > 1) - { - echo '
    '; - echo '
    '; - } - - // End previous tab - echo HTMLHelper::_($helper . '.endTab'); - } - - // Start the tab - echo HTMLHelper::_($helper . '.addTab', $tabName, 'attrib-' . $name, $label); - - $opentab = 1; - - // Directly add a fieldset if we have no children - if (!$hasChildren) - { - echo '
    '; - echo '' . $label . ''; - - // Include the description when available - if (!empty($fieldSet->description)) - { - echo '
    '; - echo '' . Text::_('INFO') . ' '; - echo Text::_($fieldSet->description); - echo '
    '; - } - - echo '
    '; - - $opentab = 2; - } - // Include the description when available - elseif (!empty($fieldSet->description)) - { - echo '
    '; - echo '' . Text::_('INFO') . ' '; - echo Text::_($fieldSet->description); - echo '
    '; - } - } - - // We're on the deepest level => output fields - if (!$hasChildren) - { - // The name of the fieldset to render - $displayData->fieldset = $name; - - // Force to show the options - $displayData->showOptions = true; - - // Render the fieldset - echo LayoutHelper::render('joomla.edit.fieldset', $displayData); - } - - // Close open fieldset - if (!$isGrandchild && $hasParent) - { - echo '
    '; - echo '
    '; - } +foreach ($fieldSets as $name => $fieldSet) { + // Ensure any fieldsets we don't want to show are skipped (including repeating formfield fieldsets) + if ( + (isset($fieldSet->repeat) && $fieldSet->repeat === true) + || in_array($name, $ignoreFieldsets) + || (!empty($configFieldsets) && in_array($name, $configFieldsets, true)) + || (!empty($hiddenFieldsets) && in_array($name, $hiddenFieldsets, true)) + ) { + continue; + } + + // Determine the label + if (!empty($fieldSet->label)) { + $label = Text::_($fieldSet->label); + } else { + $label = strtoupper('JGLOBAL_FIELDSET_' . $name); + if (Text::_($label) === $label) { + $label = strtoupper($app->input->get('option') . '_' . $name . '_FIELDSET_LABEL'); + } + $label = Text::_($label); + } + + $hasChildren = $xml->xpath('//fieldset[@name="' . $name . '"]//fieldset[not(ancestor::field/form/*)]'); + $hasParent = $xml->xpath('//fieldset//fieldset[@name="' . $name . '"]'); + $isGrandchild = $xml->xpath('//fieldset//fieldset//fieldset[@name="' . $name . '"]'); + + if (!$isGrandchild && $hasParent) { + echo '
    '; + echo '' . $label . ''; + + // Include the description when available + if (!empty($fieldSet->description)) { + echo '
    '; + echo '' . Text::_('INFO') . ' '; + echo Text::_($fieldSet->description); + echo '
    '; + } + + echo '
    '; + } + // Tabs + elseif (!$hasParent) { + if ($opentab) { + if ($opentab > 1) { + echo '
    '; + echo '
    '; + } + + // End previous tab + echo HTMLHelper::_($helper . '.endTab'); + } + + // Start the tab + echo HTMLHelper::_($helper . '.addTab', $tabName, 'attrib-' . $name, $label); + + $opentab = 1; + + // Directly add a fieldset if we have no children + if (!$hasChildren) { + echo '
    '; + echo '' . $label . ''; + + // Include the description when available + if (!empty($fieldSet->description)) { + echo '
    '; + echo '' . Text::_('INFO') . ' '; + echo Text::_($fieldSet->description); + echo '
    '; + } + + echo '
    '; + + $opentab = 2; + } + // Include the description when available + elseif (!empty($fieldSet->description)) { + echo '
    '; + echo '' . Text::_('INFO') . ' '; + echo Text::_($fieldSet->description); + echo '
    '; + } + } + + // We're on the deepest level => output fields + if (!$hasChildren) { + // The name of the fieldset to render + $displayData->fieldset = $name; + + // Force to show the options + $displayData->showOptions = true; + + // Render the fieldset + echo LayoutHelper::render('joomla.edit.fieldset', $displayData); + } + + // Close open fieldset + if (!$isGrandchild && $hasParent) { + echo '
    '; + echo '
    '; + } } -if ($opentab) -{ - if ($opentab > 1) - { - echo ''; - echo ''; - } +if ($opentab) { + if ($opentab > 1) { + echo ''; + echo ''; + } - // End previous tab - echo HTMLHelper::_($helper . '.endTab'); + // End previous tab + echo HTMLHelper::_($helper . '.endTab'); } diff --git a/layouts/joomla/edit/publishingdata.php b/layouts/joomla/edit/publishingdata.php index 452f393c62b90..9bbbce0b8ecc6 100644 --- a/layouts/joomla/edit/publishingdata.php +++ b/layouts/joomla/edit/publishingdata.php @@ -1,4 +1,5 @@ getForm(); $fields = $displayData->get('fields') ?: array( - 'publish_up', - 'publish_down', - 'featured_up', - 'featured_down', - array('created', 'created_time'), - array('created_by', 'created_user_id'), - 'created_by_alias', - array('modified', 'modified_time'), - array('modified_by', 'modified_user_id'), - 'version', - 'hits', - 'id' + 'publish_up', + 'publish_down', + 'featured_up', + 'featured_down', + array('created', 'created_time'), + array('created_by', 'created_user_id'), + 'created_by_alias', + array('modified', 'modified_time'), + array('modified_by', 'modified_user_id'), + 'version', + 'hits', + 'id' ); $hiddenFields = $displayData->get('hidden_fields') ?: array(); -foreach ($fields as $field) -{ - foreach ((array) $field as $f) - { - if ($form->getField($f)) - { - if (in_array($f, $hiddenFields)) - { - $form->setFieldAttribute($f, 'type', 'hidden'); - } +foreach ($fields as $field) { + foreach ((array) $field as $f) { + if ($form->getField($f)) { + if (in_array($f, $hiddenFields)) { + $form->setFieldAttribute($f, 'type', 'hidden'); + } - echo $form->renderField($f); - break; - } - } + echo $form->renderField($f); + break; + } + } } diff --git a/layouts/joomla/edit/title_alias.php b/layouts/joomla/edit/title_alias.php index a241cc4a8c8ea..1bcb121b11c27 100644 --- a/layouts/joomla/edit/title_alias.php +++ b/layouts/joomla/edit/title_alias.php @@ -1,4 +1,5 @@
    -
    - renderField($title) : ''; ?> -
    -
    - renderField('alias'); ?> -
    +
    + renderField($title) : ''; ?> +
    +
    + renderField('alias'); ?> +
    diff --git a/layouts/joomla/editors/buttons.php b/layouts/joomla/editors/buttons.php index 08db5c0d9a58c..3176e1dc44db2 100644 --- a/layouts/joomla/editors/buttons.php +++ b/layouts/joomla/editors/buttons.php @@ -1,4 +1,5 @@ diff --git a/layouts/joomla/editors/buttons/button.php b/layouts/joomla/editors/buttons/button.php index 49993ee41fb85..c7a0712f6d17d 100644 --- a/layouts/joomla/editors/buttons/button.php +++ b/layouts/joomla/editors/buttons/button.php @@ -1,4 +1,5 @@ get('name')) : - $class = 'btn btn-secondary'; - $class .= ($button->get('class')) ? ' ' . $button->get('class') : null; - $class .= ($button->get('modal')) ? ' modal-button' : null; - $href = '#' . strtolower($button->get('name')) . '_modal'; - $link = ($button->get('link')) ? Uri::base() . $button->get('link') : null; - $onclick = ($button->get('onclick')) ? ' onclick="' . $button->get('onclick') . '"' : ''; - $title = ($button->get('title')) ? $button->get('title') : $button->get('text'); - $icon = ($button->get('icon')) ? $button->get('icon') : $button->get('name'); -?> + $class = 'btn btn-secondary'; + $class .= ($button->get('class')) ? ' ' . $button->get('class') : null; + $class .= ($button->get('modal')) ? ' modal-button' : null; + $href = '#' . strtolower($button->get('name')) . '_modal'; + $link = ($button->get('link')) ? Uri::base() . $button->get('link') : null; + $onclick = ($button->get('onclick')) ? ' onclick="' . $button->get('onclick') . '"' : ''; + $title = ($button->get('title')) ? $button->get('title') : $button->get('text'); + $icon = ($button->get('icon')) ? $button->get('icon') : $button->get('name'); + ?> diff --git a/layouts/joomla/editors/buttons/modal.php b/layouts/joomla/editors/buttons/modal.php index 2ae1074d72af2..771de06474f0f 100644 --- a/layouts/joomla/editors/buttons/modal.php +++ b/layouts/joomla/editors/buttons/modal.php @@ -1,4 +1,5 @@ get('modal')) -{ - return; +if (!$button->get('modal')) { + return; } $class = ($button->get('class')) ? $button->get('class') : null; @@ -30,34 +30,30 @@ $confirm = ''; -if (is_array($button->get('options')) && isset($options['confirmText']) && isset($options['confirmCallback'])) -{ - $confirm = ''; +if (is_array($button->get('options')) && isset($options['confirmText']) && isset($options['confirmCallback'])) { + $confirm = ''; } -if (null !== $button->get('id')) -{ - $id = str_replace(' ', '', $button->get('id')); -} -else -{ - $id = strtolower($button->get('name')) . '_modal'; +if (null !== $button->get('id')) { + $id = str_replace(' ', '', $button->get('id')); +} else { + $id = strtolower($button->get('name')) . '_modal'; } // @todo: J4: Move Make buttons fullscreen on smaller devices per https://github.com/joomla/joomla-cms/pull/23091 // Create the modal echo HTMLHelper::_( - 'bootstrap.renderModal', - $id, - array( - 'url' => $link, - 'title' => $title, - 'height' => array_key_exists('height', $options) ? $options['height'] : '400px', - 'width' => array_key_exists('width', $options) ? $options['width'] : '800px', - 'bodyHeight' => array_key_exists('bodyHeight', $options) ? $options['bodyHeight'] : '70', - 'modalWidth' => array_key_exists('modalWidth', $options) ? $options['modalWidth'] : '80', - 'footer' => $confirm . '' - ) + 'bootstrap.renderModal', + $id, + array( + 'url' => $link, + 'title' => $title, + 'height' => array_key_exists('height', $options) ? $options['height'] : '400px', + 'width' => array_key_exists('width', $options) ? $options['width'] : '800px', + 'bodyHeight' => array_key_exists('bodyHeight', $options) ? $options['bodyHeight'] : '70', + 'modalWidth' => array_key_exists('modalWidth', $options) ? $options['modalWidth'] : '80', + 'footer' => $confirm . '' + ) ); diff --git a/layouts/joomla/error/backtrace.php b/layouts/joomla/error/backtrace.php index 415328df69f86..2742f143d2679 100644 --- a/layouts/joomla/error/backtrace.php +++ b/layouts/joomla/error/backtrace.php @@ -1,4 +1,5 @@ - - - + + + - - - - - + + + + + - $backtrace): ?> - - + $backtrace) : ?> + + - - - - - + + + + + - - - - - - - + + + + + + +
    - Call stack -
    + Call stack +
    - # - - Function - - Location -
    + # + + Function + + Location +
    - -
    + + - - - - + + + + - - -   -
    + + +   +
    diff --git a/layouts/joomla/form/field/calendar.php b/layouts/joomla/form/field/calendar.php index a977ef175d687..f81563ab5a821 100644 --- a/layouts/joomla/form/field/calendar.php +++ b/layouts/joomla/form/field/calendar.php @@ -1,4 +1,5 @@ format('Y-m-d H:i:s'); +if (strtoupper($value) === 'NOW') { + $value = Factory::getDate()->format('Y-m-d H:i:s'); } $readonly = isset($attributes['readonly']) && $attributes['readonly'] === 'readonly'; $disabled = isset($attributes['disabled']) && $attributes['disabled'] === 'disabled'; -if (is_array($attributes)) -{ - $attributes = ArrayHelper::toString($attributes); +if (is_array($attributes)) { + $attributes = ArrayHelper::toString($attributes); } $calendarAttrs = [ - 'data-inputfield' => $id, - 'data-button' => $id . '_btn', - 'data-date-format' => $format, - 'data-firstday' => empty($firstday) ? '' : $firstday, - 'data-weekend' => empty($weekend) ? '' : implode(',', $weekend), - 'data-today-btn' => $todaybutton, - 'data-week-numbers' => $weeknumbers, - 'data-show-time' => $showtime, - 'data-show-others' => $filltable, - 'data-time24' => $timeformat, - 'data-only-months-nav' => $singleheader, - 'data-min-year' => $minYear, - 'data-max-year' => $maxYear, - 'data-date-type' => strtolower($calendar), + 'data-inputfield' => $id, + 'data-button' => $id . '_btn', + 'data-date-format' => $format, + 'data-firstday' => empty($firstday) ? '' : $firstday, + 'data-weekend' => empty($weekend) ? '' : implode(',', $weekend), + 'data-today-btn' => $todaybutton, + 'data-week-numbers' => $weeknumbers, + 'data-show-time' => $showtime, + 'data-show-others' => $filltable, + 'data-time24' => $timeformat, + 'data-only-months-nav' => $singleheader, + 'data-min-year' => $minYear, + 'data-max-year' => $maxYear, + 'data-date-type' => strtolower($calendar), ]; $calendarAttrsStr = ArrayHelper::toString($calendarAttrs); // Add language strings $strings = [ - // Days - 'SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', - // Short days - 'SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', - // Months - 'JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER', - // Short months - 'JANUARY_SHORT', 'FEBRUARY_SHORT', 'MARCH_SHORT', 'APRIL_SHORT', 'MAY_SHORT', 'JUNE_SHORT', - 'JULY_SHORT', 'AUGUST_SHORT', 'SEPTEMBER_SHORT', 'OCTOBER_SHORT', 'NOVEMBER_SHORT', 'DECEMBER_SHORT', - // Buttons - 'JCLOSE', 'JCLEAR', 'JLIB_HTML_BEHAVIOR_TODAY', - // Miscellaneous - 'JLIB_HTML_BEHAVIOR_WK', + // Days + 'SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', + // Short days + 'SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', + // Months + 'JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER', + // Short months + 'JANUARY_SHORT', 'FEBRUARY_SHORT', 'MARCH_SHORT', 'APRIL_SHORT', 'MAY_SHORT', 'JUNE_SHORT', + 'JULY_SHORT', 'AUGUST_SHORT', 'SEPTEMBER_SHORT', 'OCTOBER_SHORT', 'NOVEMBER_SHORT', 'DECEMBER_SHORT', + // Buttons + 'JCLOSE', 'JCLEAR', 'JLIB_HTML_BEHAVIOR_TODAY', + // Miscellaneous + 'JLIB_HTML_BEHAVIOR_WK', ]; -foreach ($strings as $c) -{ - Text::script($c); +foreach ($strings as $c) { + Text::script($c); } // These are new strings. Make sure they exist. Can be generalised at later time: eg in 4.1 version. -if ($lang->hasKey('JLIB_HTML_BEHAVIOR_AM')) -{ - Text::script('JLIB_HTML_BEHAVIOR_AM'); +if ($lang->hasKey('JLIB_HTML_BEHAVIOR_AM')) { + Text::script('JLIB_HTML_BEHAVIOR_AM'); } -if ($lang->hasKey('JLIB_HTML_BEHAVIOR_PM')) -{ - Text::script('JLIB_HTML_BEHAVIOR_PM'); +if ($lang->hasKey('JLIB_HTML_BEHAVIOR_PM')) { + Text::script('JLIB_HTML_BEHAVIOR_PM'); } // Redefine locale/helper assets to use correct path, and load calendar assets $document->getWebAssetManager() - ->registerAndUseScript('field.calendar.helper', $helperPath, [], ['defer' => true]) - ->useStyle('field.calendar' . ($direction === 'rtl' ? '-rtl' : '')) - ->useScript('field.calendar'); + ->registerAndUseScript('field.calendar.helper', $helperPath, [], ['defer' => true]) + ->useStyle('field.calendar' . ($direction === 'rtl' ? '-rtl' : '')) + ->useScript('field.calendar'); ?>
    - -
    - - - - - - data-alt-value="" autocomplete="off"> - - -
    - + +
    + + + + + + data-alt-value="" autocomplete="off"> + + +
    +
    diff --git a/layouts/joomla/form/field/checkbox.php b/layouts/joomla/form/field/checkbox.php index 4134cdb9c5e0f..f4049fe630ed7 100644 --- a/layouts/joomla/form/field/checkbox.php +++ b/layouts/joomla/form/field/checkbox.php @@ -1,4 +1,5 @@
    - - > + + >
    diff --git a/layouts/joomla/form/field/checkboxes.php b/layouts/joomla/form/field/checkboxes.php index 58dd3878240f8..905b5238d73f1 100644 --- a/layouts/joomla/form/field/checkboxes.php +++ b/layouts/joomla/form/field/checkboxes.php @@ -1,4 +1,5 @@
    - - > - + + + > + - $option) : ?> - value, $checkedOptions, true) ? 'checked' : ''; + $option) : ?> + value, $checkedOptions, true) ? 'checked' : ''; - // In case there is no stored value, use the option's default state. - $checked = (!$hasValue && $option->checked) ? 'checked' : $checked; - $optionClass = !empty($option->class) ? 'class="form-check-input ' . $option->class . '"' : ' class="form-check-input"'; - $optionDisabled = !empty($option->disable) || $disabled ? 'disabled' : ''; + // In case there is no stored value, use the option's default state. + $checked = (!$hasValue && $option->checked) ? 'checked' : $checked; + $optionClass = !empty($option->class) ? 'class="form-check-input ' . $option->class . '"' : ' class="form-check-input"'; + $optionDisabled = !empty($option->disable) || $disabled ? 'disabled' : ''; - // Initialize some JavaScript option attributes. - $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; - $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; + // Initialize some JavaScript option attributes. + $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; + $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; - $oid = $id . $i; - $value = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); - $attributes = array_filter(array($checked, $optionClass, $optionDisabled, $onchange, $onclick)); - ?> -
    - - -
    - + $oid = $id . $i; + $value = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); + $attributes = array_filter(array($checked, $optionClass, $optionDisabled, $onchange, $onclick)); + ?> +
    + + +
    +
    diff --git a/layouts/joomla/form/field/color/advanced.php b/layouts/joomla/form/field/color/advanced.php index 468368671522d..db1a642557c7b 100644 --- a/layouts/joomla/form/field/color/advanced.php +++ b/layouts/joomla/form/field/color/advanced.php @@ -1,4 +1,5 @@ getDocument()->getWebAssetManager(); $wa->usePreset('minicolors') - ->useScript('field.color-adv'); + ->useScript('field.color-adv'); ?> /> diff --git a/layouts/joomla/form/field/color/simple.php b/layouts/joomla/form/field/color/simple.php index 0e043aff5e179..cfa1923581fde 100644 --- a/layouts/joomla/form/field/color/simple.php +++ b/layouts/joomla/form/field/color/simple.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useStyle('webcomponent.field-simple-color') - ->useScript('webcomponent.field-simple-color'); + ->useStyle('webcomponent.field-simple-color') + ->useScript('webcomponent.field-simple-color'); ?> - + diff --git a/layouts/joomla/form/field/color/slider.php b/layouts/joomla/form/field/color/slider.php index ec0c089be21cf..20baa206d53ac 100644 --- a/layouts/joomla/form/field/color/slider.php +++ b/layouts/joomla/form/field/color/slider.php @@ -1,4 +1,5 @@
    + > - - - > - - - - > - + + + > + + + + > + - - - - > - - - - - > - - - - - > - - - - - > - + + + + > + + + + + > + + + + + > + + + + + > +
    diff --git a/layouts/joomla/form/field/combo.php b/layouts/joomla/form/field/combo.php index 04dbe8bac182a..86f52cee67e07 100644 --- a/layouts/joomla/form/field/combo.php +++ b/layouts/joomla/form/field/combo.php @@ -1,4 +1,5 @@ text; +foreach ($options as $option) { + $val[] = $option->text; } ?> - data-list="" - + type="text" + name="" + id="" + value="" + + data-list="" + /> diff --git a/layouts/joomla/form/field/contenthistory.php b/layouts/joomla/form/field/contenthistory.php index 89b43ac2085a8..1091c6fb3a538 100644 --- a/layouts/joomla/form/field/contenthistory.php +++ b/layouts/joomla/form/field/contenthistory.php @@ -1,4 +1,5 @@ Route::_($link), - 'title' => $label, - 'height' => '100%', - 'width' => '100%', - 'modalWidth' => '80', - 'bodyHeight' => '60', - 'footer' => '' - ) + 'bootstrap.renderModal', + 'versionsModal', + array( + 'url' => Route::_($link), + 'title' => $label, + 'height' => '100%', + 'width' => '100%', + 'modalWidth' => '80', + 'bodyHeight' => '60', + 'footer' => '' + ) ); ?> diff --git a/layouts/joomla/form/field/email.php b/layouts/joomla/form/field/email.php index 254489393c144..2f9e75e09db4f 100644 --- a/layouts/joomla/form/field/email.php +++ b/layouts/joomla/form/field/email.php @@ -1,4 +1,5 @@ '; diff --git a/layouts/joomla/form/field/file.php b/layouts/joomla/form/field/file.php index d4f93a0110ad9..0120973249bfa 100644 --- a/layouts/joomla/form/field/file.php +++ b/layouts/joomla/form/field/file.php @@ -1,4 +1,5 @@ - - - - - - - - >
    - + name="" + id="" + + + + + + + + + >
    + diff --git a/layouts/joomla/form/field/groupedlist-fancy-select.php b/layouts/joomla/form/field/groupedlist-fancy-select.php index 9ce2f3b94fa8c..0cb7eb4d1b588 100644 --- a/layouts/joomla/form/field/groupedlist-fancy-select.php +++ b/layouts/joomla/form/field/groupedlist-fancy-select.php @@ -1,4 +1,5 @@ escape($hint ?: Text::_('JGLOBAL_TYPE_OR_SELECT_SOME_OPTIONS')) . '" '; -if ($required) -{ - $attr .= ' required class="required"'; - $attr2 .= ' required'; +if ($required) { + $attr .= ' required class="required"'; + $attr2 .= ' required'; } // Create a read-only list (no name) with a hidden input to store the value. -if ($readonly) -{ - $html[] = HTMLHelper::_( - 'select.groupedlist', $groups, null, - array( - 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, - 'option.text.toHtml' => false, - ) - ); - - // E.g. form field type tag sends $this->value as array - if ($multiple && \is_array($value)) - { - if (!\count($value)) - { - $value[] = ''; - } - - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } +if ($readonly) { + $html[] = HTMLHelper::_( + 'select.groupedlist', + $groups, + null, + array( + 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, + 'option.text.toHtml' => false, + ) + ); + + // E.g. form field type tag sends $this->value as array + if ($multiple && \is_array($value)) { + if (!\count($value)) { + $value[] = ''; + } + + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } } // Create a regular list. -else -{ - $html[] = HTMLHelper::_( - 'select.groupedlist', $groups, $name, - array( - 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, - 'option.text.toHtml' => false, - ) - ); +else { + $html[] = HTMLHelper::_( + 'select.groupedlist', + $groups, + $name, + array( + 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, + 'option.text.toHtml' => false, + ) + ); } Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getApplication()->getDocument()->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select'); ?> diff --git a/layouts/joomla/form/field/groupedlist.php b/layouts/joomla/form/field/groupedlist.php index f0f0eda4d909b..6c1851bc4aaad 100644 --- a/layouts/joomla/form/field/groupedlist.php +++ b/layouts/joomla/form/field/groupedlist.php @@ -1,4 +1,5 @@ $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, - 'option.text.toHtml' => false, - ) - ); +if ($readonly) { + $html[] = HTMLHelper::_( + 'select.groupedlist', + $groups, + null, + array( + 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, + 'option.text.toHtml' => false, + ) + ); - // E.g. form field type tag sends $this->value as array - if ($multiple && \is_array($value)) - { - if (!\count($value)) - { - $value[] = ''; - } + // E.g. form field type tag sends $this->value as array + if ($multiple && \is_array($value)) { + if (!\count($value)) { + $value[] = ''; + } - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } } // Create a regular list. -else -{ - $html[] = HTMLHelper::_( - 'select.groupedlist', $groups, $name, - array( - 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, - 'option.text.toHtml' => false, - ) - ); +else { + $html[] = HTMLHelper::_( + 'select.groupedlist', + $groups, + $name, + array( + 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, + 'option.text.toHtml' => false, + ) + ); } echo implode($html); diff --git a/layouts/joomla/form/field/hidden.php b/layouts/joomla/form/field/hidden.php index 539e99ae8660e..88ba0160e8772 100644 --- a/layouts/joomla/form/field/hidden.php +++ b/layouts/joomla/form/field/hidden.php @@ -1,4 +1,5 @@ > + type="hidden" + name="" + id="" + value="" + > diff --git a/layouts/joomla/form/field/list-fancy-select.php b/layouts/joomla/form/field/list-fancy-select.php index 8820fe0fee473..89fba4525e3fa 100644 --- a/layouts/joomla/form/field/list-fancy-select.php +++ b/layouts/joomla/form/field/list-fancy-select.php @@ -1,4 +1,5 @@ escape($hint ?: Text::_('JGLOBAL_TYPE_OR_SELECT_SOME_OPTIONS')) . '" '; -if ($required) -{ - $attr .= ' required class="required"'; - $attr2 .= ' required'; +if ($required) { + $attr .= ' required class="required"'; + $attr2 .= ' required'; } // Create a read-only list (no name) with hidden input(s) to store the value(s). -if ($readonly) -{ - $html[] = HTMLHelper::_('select.genericlist', $options, '', trim($attr), 'value', 'text', $value, $id); - - // E.g. form field type tag sends $this->value as array - if ($multiple && is_array($value)) - { - if (!count($value)) - { - $value[] = ''; - } - - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } -} -else -// Create a regular list. +if ($readonly) { + $html[] = HTMLHelper::_('select.genericlist', $options, '', trim($attr), 'value', 'text', $value, $id); + + // E.g. form field type tag sends $this->value as array + if ($multiple && is_array($value)) { + if (!count($value)) { + $value[] = ''; + } + + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } +} else // Create a regular list. { - $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); + $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); } Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getApplication()->getDocument()->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select'); ?> diff --git a/layouts/joomla/form/field/list.php b/layouts/joomla/form/field/list.php index c827e314a99ac..99e6292257508 100644 --- a/layouts/joomla/form/field/list.php +++ b/layouts/joomla/form/field/list.php @@ -1,4 +1,5 @@ value as array - if ($multiple && is_array($value)) - { - if (!count($value)) - { - $value[] = ''; - } + // E.g. form field type tag sends $this->value as array + if ($multiple && is_array($value)) { + if (!count($value)) { + $value[] = ''; + } - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } -} -else -// Create a regular list passing the arguments in an array. + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } +} else // Create a regular list passing the arguments in an array. { - $listoptions = array(); - $listoptions['option.key'] = 'value'; - $listoptions['option.text'] = 'text'; - $listoptions['list.select'] = $value; - $listoptions['id'] = $id; - $listoptions['list.translate'] = false; - $listoptions['option.attr'] = 'optionattr'; - $listoptions['list.attr'] = trim($attr); - $html[] = HTMLHelper::_('select.genericlist', $options, $name, $listoptions); + $listoptions = array(); + $listoptions['option.key'] = 'value'; + $listoptions['option.text'] = 'text'; + $listoptions['list.select'] = $value; + $listoptions['id'] = $id; + $listoptions['list.translate'] = false; + $listoptions['option.attr'] = 'optionattr'; + $listoptions['list.attr'] = trim($attr); + $html[] = HTMLHelper::_('select.genericlist', $options, $name, $listoptions); } echo implode($html); diff --git a/layouts/joomla/form/field/media.php b/layouts/joomla/form/field/media.php index 57fa34bde02c5..c62233c58b3af 100644 --- a/layouts/joomla/form/field/media.php +++ b/layouts/joomla/form/field/media.php @@ -1,4 +1,5 @@ 0) ? 'max-width:' . $width . 'px;' : ''; - $style .= ($height > 0) ? 'max-height:' . $height . 'px;' : ''; - - $imgattr = array( - 'id' => $id . '_preview', - 'class' => 'media-preview', - 'style' => $style, - ); - - $img = HTMLHelper::_('image', $src, Text::_('JLIB_FORM_MEDIA_PREVIEW_ALT'), $imgattr); - - $previewImg = '
    ' . $img . '
    '; - $previewImgEmpty = ''; - - $showPreview = 'static'; +if ($showPreview) { + $cleanValue = MediaHelper::getCleanMediaFieldValue($value); + + if ($cleanValue && file_exists(JPATH_ROOT . '/' . $cleanValue)) { + $src = Uri::root() . $value; + } else { + $src = ''; + } + + $width = $previewWidth; + $height = $previewHeight; + $style = ''; + $style .= ($width > 0) ? 'max-width:' . $width . 'px;' : ''; + $style .= ($height > 0) ? 'max-height:' . $height . 'px;' : ''; + + $imgattr = array( + 'id' => $id . '_preview', + 'class' => 'media-preview', + 'style' => $style, + ); + + $img = HTMLHelper::_('image', $src, Text::_('JLIB_FORM_MEDIA_PREVIEW_ALT'), $imgattr); + + $previewImg = '
    ' . $img . '
    '; + $previewImgEmpty = ''; + + $showPreview = 'static'; } // The url for the modal $url = ($readonly ? '' - : ($link ?: 'index.php?option=com_media&view=media&tmpl=component&mediatypes=' . $mediaTypes - . '&asset=' . $asset . '&author=' . $authorId) - . '&fieldid={field-media-id}&path=' . $folder); + : ($link ?: 'index.php?option=com_media&view=media&tmpl=component&mediatypes=' . $mediaTypes + . '&asset=' . $asset . '&author=' . $authorId) + . '&fieldid={field-media-id}&path=' . $folder); // Correctly route the url to ensure it's correctly using sef modes and subfolders $url = Route::_($url); @@ -143,63 +140,63 @@ Text::script('JLIB_FORM_MEDIA_PREVIEW_EMPTY', true); $modalHTML = HTMLHelper::_( - 'bootstrap.renderModal', - 'imageModal_' . $id, - [ - 'url' => $url, - 'title' => Text::_('JLIB_FORM_CHANGE_IMAGE'), - 'closeButton' => true, - 'height' => '100%', - 'width' => '100%', - 'modalWidth' => '80', - 'bodyHeight' => '60', - 'footer' => '' - . '', - ] + 'bootstrap.renderModal', + 'imageModal_' . $id, + [ + 'url' => $url, + 'title' => Text::_('JLIB_FORM_CHANGE_IMAGE'), + 'closeButton' => true, + 'height' => '100%', + 'width' => '100%', + 'modalWidth' => '80', + 'bodyHeight' => '60', + 'footer' => '' + . '', + ] ); $wam->useStyle('webcomponent.field-media') - ->useScript('webcomponent.field-media'); + ->useScript('webcomponent.field-media'); if (count($doc->getScriptOptions('media-picker')) === 0) { - $doc->addScriptOptions('media-picker', [ - 'images' => $imagesExt, - 'audios' => $audiosExt, - 'videos' => $videosExt, - 'documents' => $documentsExt, - ]); + $doc->addScriptOptions('media-picker', [ + 'images' => $imagesExt, + 'audios' => $audiosExt, + 'videos' => $videosExt, + 'documents' => $documentsExt, + ]); } ?> - base-path="" - root-folder="get('file_path', 'images'); ?>" - url="" - modal-container=".modal" - modal-width="100%" - modal-height="400px" - input=".field-media-input" - button-select=".button-select" - button-clear=".button-clear" - button-save-selected=".button-save-selected" - preview="static" - preview-container=".field-media-preview" - preview-width="" - preview-height="" - supported-extensions=" $imagesAllowedExt, 'audios' => $audiosAllowedExt, 'videos' => $videosAllowedExt, 'documents' => $documentsAllowedExt])); ?> + base-path="" + root-folder="get('file_path', 'images'); ?>" + url="" + modal-container=".modal" + modal-width="100%" + modal-height="400px" + input=".field-media-input" + button-select=".button-select" + button-clear=".button-clear" + button-save-selected=".button-save-selected" + preview="static" + preview-container=".field-media-preview" + preview-width="" + preview-height="" + supported-extensions=" $imagesAllowedExt, 'audios' => $audiosAllowedExt, 'videos' => $videosAllowedExt, 'documents' => $documentsAllowedExt])); ?> "> - - -
    - - -
    - -
    - > - - - - -
    + + +
    + + +
    + +
    + > + + + + +
    diff --git a/layouts/joomla/form/field/meter.php b/layouts/joomla/form/field/meter.php index 3e2a271b7bcaf..f6c112d8ef506 100644 --- a/layouts/joomla/form/field/meter.php +++ b/layouts/joomla/form/field/meter.php @@ -1,4 +1,5 @@
    -
    - style="width:%;">
    +
    + style="width:%;">
    diff --git a/layouts/joomla/form/field/moduleorder.php b/layouts/joomla/form/field/moduleorder.php index f71c8510d6270..95f23a7ba0720 100644 --- a/layouts/joomla/form/field/moduleorder.php +++ b/layouts/joomla/form/field/moduleorder.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('webcomponent.field-module-order'); + ->useScript('webcomponent.field-module-order'); ?> > diff --git a/layouts/joomla/form/field/number.php b/layouts/joomla/form/field/number.php index d10d61f3d12ba..ccc24f22bd283 100644 --- a/layouts/joomla/form/field/number.php +++ b/layouts/joomla/form/field/number.php @@ -1,4 +1,5 @@ > + type="number" + inputmode="numeric" + name="" + id="" + value="" + > diff --git a/layouts/joomla/form/field/password.php b/layouts/joomla/form/field/password.php index a1bc52cd460b3..d128aacb00d04 100644 --- a/layouts/joomla/form/field/password.php +++ b/layouts/joomla/form/field/password.php @@ -1,4 +1,5 @@ getWebAssetManager(); -if ($meter) -{ - $wa->useScript('field.passwordstrength'); +if ($meter) { + $wa->useScript('field.passwordstrength'); - $class = 'js-password-strength ' . $class; + $class = 'js-password-strength ' . $class; - if ($forcePassword) - { - $class = $class . ' meteredPassword'; - } + if ($forcePassword) { + $class = $class . ' meteredPassword'; + } } $wa->useScript('field.passwordview'); @@ -75,92 +74,85 @@ Text::script('JSHOWPASSWORD'); Text::script('JHIDEPASSWORD'); -if ($lock) -{ - Text::script('JMODIFY'); - Text::script('JCANCEL'); +if ($lock) { + Text::script('JMODIFY'); + Text::script('JCANCEL'); - $disabled = true; - $hint = str_repeat('•', 10); - $value = ''; + $disabled = true; + $hint = str_repeat('•', 10); + $value = ''; } $ariaDescribedBy = $rules ? $name . '-rules ' : ''; $ariaDescribedBy .= !empty($description) ? (($id ?: $name) . '-desc') : ''; $attributes = array( - strlen($hint) ? 'placeholder="' . htmlspecialchars($hint, ENT_COMPAT, 'UTF-8') . '"' : '', - !empty($autocomplete) ? 'autocomplete="' . $autocomplete . '"' : '', - !empty($class) ? 'class="form-control ' . $class . '"' : 'class="form-control"', - !empty($ariaDescribedBy) ? 'aria-describedby="' . trim($ariaDescribedBy) . '"' : '', - $readonly ? 'readonly' : '', - $disabled ? 'disabled' : '', - !empty($size) ? 'size="' . $size . '"' : '', - !empty($maxLength) ? 'maxlength="' . $maxLength . '"' : '', - $required ? 'required' : '', - $autofocus ? 'autofocus' : '', - !empty($minLength) ? 'data-min-length="' . $minLength . '"' : '', - !empty($minIntegers) ? 'data-min-integers="' . $minIntegers . '"' : '', - !empty($minSymbols) ? 'data-min-symbols="' . $minSymbols . '"' : '', - !empty($minUppercase) ? 'data-min-uppercase="' . $minUppercase . '"' : '', - !empty($minLowercase) ? 'data-min-lowercase="' . $minLowercase . '"' : '', - !empty($forcePassword) ? 'data-min-force="' . $forcePassword . '"' : '', - $dataAttribute, + strlen($hint) ? 'placeholder="' . htmlspecialchars($hint, ENT_COMPAT, 'UTF-8') . '"' : '', + !empty($autocomplete) ? 'autocomplete="' . $autocomplete . '"' : '', + !empty($class) ? 'class="form-control ' . $class . '"' : 'class="form-control"', + !empty($ariaDescribedBy) ? 'aria-describedby="' . trim($ariaDescribedBy) . '"' : '', + $readonly ? 'readonly' : '', + $disabled ? 'disabled' : '', + !empty($size) ? 'size="' . $size . '"' : '', + !empty($maxLength) ? 'maxlength="' . $maxLength . '"' : '', + $required ? 'required' : '', + $autofocus ? 'autofocus' : '', + !empty($minLength) ? 'data-min-length="' . $minLength . '"' : '', + !empty($minIntegers) ? 'data-min-integers="' . $minIntegers . '"' : '', + !empty($minSymbols) ? 'data-min-symbols="' . $minSymbols . '"' : '', + !empty($minUppercase) ? 'data-min-uppercase="' . $minUppercase . '"' : '', + !empty($minLowercase) ? 'data-min-lowercase="' . $minLowercase . '"' : '', + !empty($forcePassword) ? 'data-min-force="' . $forcePassword . '"' : '', + $dataAttribute, ); -if ($rules) -{ - $requirements = []; - - if ($minLength) - { - $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_CHARACTERS', $minLength); - } - - if ($minIntegers) - { - $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_DIGITS', $minIntegers); - } - - if ($minSymbols) - { - $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_SYMBOLS', $minSymbols); - } - - if ($minUppercase) - { - $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_UPPERCASE', $minUppercase); - } - - if ($minLowercase) - { - $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_LOWERCASE', $minLowercase); - } +if ($rules) { + $requirements = []; + + if ($minLength) { + $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_CHARACTERS', $minLength); + } + + if ($minIntegers) { + $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_DIGITS', $minIntegers); + } + + if ($minSymbols) { + $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_SYMBOLS', $minSymbols); + } + + if ($minUppercase) { + $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_UPPERCASE', $minUppercase); + } + + if ($minLowercase) { + $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_LOWERCASE', $minLowercase); + } } ?> -
    - -
    +
    + +
    -
    - > - - - - - -
    +
    + > + + + + + +
    diff --git a/layouts/joomla/form/field/radio/buttons.php b/layouts/joomla/form/field/radio/buttons.php index 96f36aa57e78c..faf1d93fdce28 100644 --- a/layouts/joomla/form/field/radio/buttons.php +++ b/layouts/joomla/form/field/radio/buttons.php @@ -1,4 +1,5 @@
    > - - - -
    - $option) : ?> - - disable) ? 'disabled' : ''; - $style = $disabled ? ' style="pointer-events: none"' : ''; + + + +
    + $option) : ?> + + disable) ? 'disabled' : ''; + $style = $disabled ? ' style="pointer-events: none"' : ''; - // Initialize some option attributes. - if ($isBtnYesNo) - { - // Set the button classes for the yes/no group - switch ($option->value) - { - case '0': - $btnClass = 'btn btn-outline-danger'; - break; - case '1': - $btnClass = 'btn btn-outline-success'; - break; - default: - $btnClass = 'btn btn-outline-secondary'; - break; - } - } + // Initialize some option attributes. + if ($isBtnYesNo) { + // Set the button classes for the yes/no group + switch ($option->value) { + case '0': + $btnClass = 'btn btn-outline-danger'; + break; + case '1': + $btnClass = 'btn btn-outline-success'; + break; + default: + $btnClass = 'btn btn-outline-secondary'; + break; + } + } - $optionClass = !empty($option->class) ? $option->class : $btnClass; - $optionClass = trim($optionClass . ' ' . $disabled); - $checked = ((string) $option->value === $value) ? 'checked="checked"' : ''; + $optionClass = !empty($option->class) ? $option->class : $btnClass; + $optionClass = trim($optionClass . ' ' . $disabled); + $checked = ((string) $option->value === $value) ? 'checked="checked"' : ''; - // Initialize some JavaScript option attributes. - $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; - $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; - $oid = $id . $i; - $ovalue = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); - $attributes = array_filter(array($checked, $disabled, ltrim($style), $onchange, $onclick)); - ?> - - - - > - - - -
    + // Initialize some JavaScript option attributes. + $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; + $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; + $oid = $id . $i; + $ovalue = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); + $attributes = array_filter(array($checked, $disabled, ltrim($style), $onchange, $onclick)); + ?> + + + + > + + + +
    diff --git a/layouts/joomla/form/field/radio/switcher.php b/layouts/joomla/form/field/radio/switcher.php index f1070fa6df77b..191a5a07850a1 100644 --- a/layouts/joomla/form/field/radio/switcher.php +++ b/layouts/joomla/form/field/radio/switcher.php @@ -1,4 +1,5 @@
    > - - - -
    - $option) : ?> - value == '0') - { - $value = '0'; - } + + + +
    + $option) : ?> + value == '0') { + $value = '0'; + } - // Initialize some option attributes. - $optionValue = (string) $option->value; - $optionId = $id . $i; - $attributes = $optionValue == $value ? 'checked class="active ' . $class . '"' : ($class ? 'class="' . $class . '"' : ''); - $attributes .= $optionValue != $value && $readonly || $disabled ? ' disabled' : ''; - ?> - escape($optionValue), $attributes); ?> - ' . $option->text . ''; ?> - - -
    + // Initialize some option attributes. + $optionValue = (string) $option->value; + $optionId = $id . $i; + $attributes = $optionValue == $value ? 'checked class="active ' . $class . '"' : ($class ? 'class="' . $class . '"' : ''); + $attributes .= $optionValue != $value && $readonly || $disabled ? ' disabled' : ''; + ?> + escape($optionValue), $attributes); ?> + ' . $option->text . ''; ?> + + +
    diff --git a/layouts/joomla/form/field/radiobasic.php b/layouts/joomla/form/field/radiobasic.php index e28959c7d8be4..45f1aa8abc3c9 100644 --- a/layouts/joomla/form/field/radiobasic.php +++ b/layouts/joomla/form/field/radiobasic.php @@ -1,4 +1,5 @@
    - - - > + + + + > - - $option) : ?> - value === $value) ? 'checked="checked"' : ''; - $optionClass = !empty($option->class) ? 'class="' . $option->class . '"' : ''; - $disabled = !empty($option->disable) || ($disabled && !$checked) ? 'disabled' : ''; + + $option) : ?> + value === $value) ? 'checked="checked"' : ''; + $optionClass = !empty($option->class) ? 'class="' . $option->class . '"' : ''; + $disabled = !empty($option->disable) || ($disabled && !$checked) ? 'disabled' : ''; - // Initialize some JavaScript option attributes. - $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; - $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; - $oid = $id . $i; - $ovalue = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); - $attributes = array_filter(array($checked, $optionClass, $disabled, $onchange, $onclick)); - ?> - - - -
    - -
    - - + // Initialize some JavaScript option attributes. + $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; + $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; + $oid = $id . $i; + $ovalue = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); + $attributes = array_filter(array($checked, $optionClass, $disabled, $onchange, $onclick)); + ?> + + + +
    + +
    + +
    diff --git a/layouts/joomla/form/field/range.php b/layouts/joomla/form/field/range.php index 3e07863d863c3..9e5f0a311ac1b 100644 --- a/layouts/joomla/form/field/range.php +++ b/layouts/joomla/form/field/range.php @@ -1,4 +1,5 @@ > + type="range" + name="" + id="" + value="" + > diff --git a/layouts/joomla/form/field/rules.php b/layouts/joomla/form/field/rules.php index 90df4535ad6e5..ff0cd838d7a29 100644 --- a/layouts/joomla/form/field/rules.php +++ b/layouts/joomla/form/field/rules.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useStyle('webcomponent.field-permissions') - ->useScript('webcomponent.field-permissions') - ->useStyle('webcomponent.joomla-tab') - ->useScript('webcomponent.joomla-tab'); + ->useStyle('webcomponent.field-permissions') + ->useScript('webcomponent.field-permissions') + ->useStyle('webcomponent.joomla-tab') + ->useScript('webcomponent.joomla-tab'); // Load JavaScript message titles Text::script('ERROR'); @@ -79,171 +80,159 @@
    - - - -
    - -
    + + + +
    + +
    > - - - - value === 1 ? ' active' : ''; ?> - name=" $group->level + 1)), ENT_COMPAT, 'utf-8') . $group->text; ?>" id="permission-value; ?>"> - - - - - - - - - - - - - - value, 'core.admin'); ?> - - - - - - - - - -
    - - - - - -
    - - description)) : ?> - - - -
    -   - -
    -
    - - - value, $action->name, $assetId); - $inheritedGroupParentAssetRule = !empty($parentAssetId) ? Access::checkGroup($group->value, $action->name, $parentAssetId) : null; - $inheritedParentGroupRule = !empty($group->parent_id) ? Access::checkGroup($group->parent_id, $action->name, $assetId) : null; - - // Current group is a Super User group, so calculated setting is "Allowed (Super User)". - if ($isSuperUserGroup) - { - $result['class'] = 'badge bg-success'; - $result['text'] = '' . Text::_('JLIB_RULES_ALLOWED_ADMIN'); - } - else - { - // First get the real recursive calculated setting and add (Inherited) to it. - - // If recursive calculated setting is "Denied" or null. Calculated permission is "Not Allowed (Inherited)". - if ($inheritedGroupRule === null || $inheritedGroupRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_INHERITED'); - } - // If recursive calculated setting is "Allowed". Calculated permission is "Allowed (Inherited)". - else - { - $result['class'] = 'badge bg-success'; - $result['text'] = Text::_('JLIB_RULES_ALLOWED_INHERITED'); - } - - // Second part: Overwrite the calculated permissions labels if there is an explicit permission in the current group. - - /** - * @todo: incorrect info - * If a component has a permission that doesn't exists in global config (ex: frontend editing in com_modules) by default - * we get "Not Allowed (Inherited)" when we should get "Not Allowed (Default)". - */ - - // If there is an explicit permission "Not Allowed". Calculated permission is "Not Allowed". - if ($assetRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED'); - } - // If there is an explicit permission is "Allowed". Calculated permission is "Allowed". - elseif ($assetRule === true) - { - $result['class'] = 'badge bg-success'; - $result['text'] = Text::_('JLIB_RULES_ALLOWED'); - } - - // Third part: Overwrite the calculated permissions labels for special cases. - - // Global configuration with "Not Set" permission. Calculated permission is "Not Allowed (Default)". - if (empty($group->parent_id) && $isGlobalConfig === true && $assetRule === null) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_DEFAULT'); - } - - /** - * Component/Item with explicit "Denied" permission at parent Asset (Category, Component or Global config) configuration. - * Or some parent group has an explicit "Denied". - * Calculated permission is "Not Allowed (Locked)". - */ - elseif ($inheritedGroupParentAssetRule === false || $inheritedParentGroupRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = ''. Text::_('JLIB_RULES_NOT_ALLOWED_LOCKED'); - } - } - ?> - -
    -
    - -
    + + + + value === 1 ? ' active' : ''; ?> + name=" $group->level + 1)), ENT_COMPAT, 'utf-8') . $group->text; ?>" id="permission-value; ?>"> + + + + + + + + + + + + + + value, 'core.admin'); ?> + + + + + + + + + +
    + + + + + +
    + + description)) : ?> + + + +
    +   + +
    +
    + + + value, $action->name, $assetId); + $inheritedGroupParentAssetRule = !empty($parentAssetId) ? Access::checkGroup($group->value, $action->name, $parentAssetId) : null; + $inheritedParentGroupRule = !empty($group->parent_id) ? Access::checkGroup($group->parent_id, $action->name, $assetId) : null; + + // Current group is a Super User group, so calculated setting is "Allowed (Super User)". + if ($isSuperUserGroup) { + $result['class'] = 'badge bg-success'; + $result['text'] = '' . Text::_('JLIB_RULES_ALLOWED_ADMIN'); + } else { + // First get the real recursive calculated setting and add (Inherited) to it. + + // If recursive calculated setting is "Denied" or null. Calculated permission is "Not Allowed (Inherited)". + if ($inheritedGroupRule === null || $inheritedGroupRule === false) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_INHERITED'); + } + // If recursive calculated setting is "Allowed". Calculated permission is "Allowed (Inherited)". + else { + $result['class'] = 'badge bg-success'; + $result['text'] = Text::_('JLIB_RULES_ALLOWED_INHERITED'); + } + + // Second part: Overwrite the calculated permissions labels if there is an explicit permission in the current group. + + /** + * @todo: incorrect info + * If a component has a permission that doesn't exists in global config (ex: frontend editing in com_modules) by default + * we get "Not Allowed (Inherited)" when we should get "Not Allowed (Default)". + */ + + // If there is an explicit permission "Not Allowed". Calculated permission is "Not Allowed". + if ($assetRule === false) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED'); + } + // If there is an explicit permission is "Allowed". Calculated permission is "Allowed". + elseif ($assetRule === true) { + $result['class'] = 'badge bg-success'; + $result['text'] = Text::_('JLIB_RULES_ALLOWED'); + } + + // Third part: Overwrite the calculated permissions labels for special cases. + + // Global configuration with "Not Set" permission. Calculated permission is "Not Allowed (Default)". + if (empty($group->parent_id) && $isGlobalConfig === true && $assetRule === null) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_DEFAULT'); + } + + /** + * Component/Item with explicit "Denied" permission at parent Asset (Category, Component or Global config) configuration. + * Or some parent group has an explicit "Denied". + * Calculated permission is "Not Allowed (Locked)". + */ + elseif ($inheritedGroupParentAssetRule === false || $inheritedParentGroupRule === false) { + $result['class'] = 'badge bg-danger'; + $result['text'] = '' . Text::_('JLIB_RULES_NOT_ALLOWED_LOCKED'); + } + } + ?> + +
    +
    + +
    diff --git a/layouts/joomla/form/field/subform/default.php b/layouts/joomla/form/field/subform/default.php index 6afbd05d248cf..b93687284cdc3 100644 --- a/layouts/joomla/form/field/subform/default.php +++ b/layouts/joomla/form/field/subform/default.php @@ -1,4 +1,5 @@ getGroup('') as $field) : ?> - renderField(); ?> + renderField(); ?> diff --git a/layouts/joomla/form/field/subform/repeatable-table.php b/layouts/joomla/form/field/subform/repeatable-table.php index e2aa4a00d4010..7625282a9eebc 100644 --- a/layouts/joomla/form/field/subform/repeatable-table.php +++ b/layouts/joomla/form/field/subform/repeatable-table.php @@ -1,4 +1,5 @@ getDocument() - ->getWebAssetManager() - ->useScript('webcomponent.field-subform'); +if ($multiple) { + // Add script + Factory::getApplication() + ->getDocument() + ->getWebAssetManager() + ->useScript('webcomponent.field-subform'); } $class = $class ? ' ' . $class : ''; @@ -47,82 +47,77 @@ // Build heading $table_head = ''; -if (!empty($groupByFieldset)) -{ - foreach ($tmpl->getFieldsets() as $fieldset) { - $table_head .= '' . Text::_($fieldset->label); +if (!empty($groupByFieldset)) { + foreach ($tmpl->getFieldsets() as $fieldset) { + $table_head .= '' . Text::_($fieldset->label); - if ($fieldset->description) - { - $table_head .= ''; - } + if ($fieldset->description) { + $table_head .= ''; + } - $table_head .= ''; - } + $table_head .= ''; + } - $sublayout = 'section-byfieldsets'; -} -else -{ - foreach ($tmpl->getGroup('') as $field) { - $table_head .= '' . strip_tags($field->label); + $sublayout = 'section-byfieldsets'; +} else { + foreach ($tmpl->getGroup('') as $field) { + $table_head .= '' . strip_tags($field->label); - if ($field->description) - { - $table_head .= ''; - } + if ($field->description) { + $table_head .= ''; + } - $table_head .= ''; - } + $table_head .= ''; + } - $sublayout = 'section'; + $sublayout = 'section'; - // Label will not be shown for sections layout, so reset the margin left - Factory::getApplication() - ->getDocument() - ->addStyleDeclaration('.subform-table-sublayout-section .controls { margin-left: 0px }'); + // Label will not be shown for sections layout, so reset the margin left + Factory::getApplication() + ->getDocument() + ->addStyleDeclaration('.subform-table-sublayout-section .controls { margin-left: 0px }'); } ?>
    - -
    - - - - - - - - - - - - $form) : - echo $this->sublayout($sublayout, array('form' => $form, 'basegroup' => $fieldname, 'group' => $fieldname . $k, 'buttons' => $buttons)); - endforeach; - ?> - -
    - -
    - -
    - -
    - -
    -
    - - - -
    + +
    + + + + + + + + + + + + $form) : + echo $this->sublayout($sublayout, array('form' => $form, 'basegroup' => $fieldname, 'group' => $fieldname . $k, 'buttons' => $buttons)); + endforeach; + ?> + +
    + +
    + +
    + +
    + +
    +
    + + + +
    diff --git a/layouts/joomla/form/field/subform/repeatable-table/section-byfieldsets.php b/layouts/joomla/form/field/subform/repeatable-table/section-byfieldsets.php index ba2730f211656..a3ff486d387d1 100644 --- a/layouts/joomla/form/field/subform/repeatable-table/section-byfieldsets.php +++ b/layouts/joomla/form/field/subform/repeatable-table/section-byfieldsets.php @@ -1,4 +1,5 @@ - getFieldsets() as $fieldset) : ?> - - getFieldset($fieldset->name) as $field) : ?> - renderField(); ?> - - - - - -
    - - - - - - - - - -
    - - + getFieldsets() as $fieldset) : ?> + + getFieldset($fieldset->name) as $field) : ?> + renderField(); ?> + + + + + +
    + + + + + + + + + +
    + + diff --git a/layouts/joomla/form/field/subform/repeatable-table/section.php b/layouts/joomla/form/field/subform/repeatable-table/section.php index 3435f1d47403c..ccef5fc9cd47b 100644 --- a/layouts/joomla/form/field/subform/repeatable-table/section.php +++ b/layouts/joomla/form/field/subform/repeatable-table/section.php @@ -1,4 +1,5 @@ - getGroup('') as $field) : ?> - - renderField(array('hiddenLabel' => true, 'hiddenDescription' => true)); ?> - - - - -
    - - - - - - - - - -
    - - + getGroup('') as $field) : ?> + + renderField(array('hiddenLabel' => true, 'hiddenDescription' => true)); ?> + + + + +
    + + + + + + + + + +
    + + diff --git a/layouts/joomla/form/field/subform/repeatable.php b/layouts/joomla/form/field/subform/repeatable.php index 0d272e721aa41..a2504bbc3af36 100644 --- a/layouts/joomla/form/field/subform/repeatable.php +++ b/layouts/joomla/form/field/subform/repeatable.php @@ -1,4 +1,5 @@ getDocument() - ->getWebAssetManager() - ->useScript('webcomponent.field-subform'); +if ($multiple) { + // Add script + Factory::getApplication() + ->getDocument() + ->getWebAssetManager() + ->useScript('webcomponent.field-subform'); } $class = $class ? ' ' . $class : ''; @@ -48,27 +48,27 @@ ?>
    - - -
    -
    - -
    -
    - - $form) : - echo $this->sublayout($sublayout, array('form' => $form, 'basegroup' => $fieldname, 'group' => $fieldname . $k, 'buttons' => $buttons)); - endforeach; - ?> - - - -
    + + +
    +
    + +
    +
    + + $form) : + echo $this->sublayout($sublayout, array('form' => $form, 'basegroup' => $fieldname, 'group' => $fieldname . $k, 'buttons' => $buttons)); + endforeach; + ?> + + + +
    diff --git a/layouts/joomla/form/field/subform/repeatable/section-byfieldsets.php b/layouts/joomla/form/field/subform/repeatable/section-byfieldsets.php index 0abf0645bb01c..4710dc0b04540 100644 --- a/layouts/joomla/form/field/subform/repeatable/section-byfieldsets.php +++ b/layouts/joomla/form/field/subform/repeatable/section-byfieldsets.php @@ -1,4 +1,5 @@
    - -
    -
    - - - -
    -
    - -
    - getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - getFieldset($fieldset->name) as $field) : ?> - renderField(); ?> - -
    - -
    + +
    +
    + + + +
    +
    + +
    + getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + getFieldset($fieldset->name) as $field) : ?> + renderField(); ?> + +
    + +
    diff --git a/layouts/joomla/form/field/subform/repeatable/section.php b/layouts/joomla/form/field/subform/repeatable/section.php index e3693ca5d1ad2..7eb4826223d2c 100644 --- a/layouts/joomla/form/field/subform/repeatable/section.php +++ b/layouts/joomla/form/field/subform/repeatable/section.php @@ -1,4 +1,5 @@
    - -
    -
    - - - -
    -
    - + +
    +
    + + + +
    +
    + getGroup('') as $field) : ?> - renderField(); ?> + renderField(); ?>
    diff --git a/layouts/joomla/form/field/tag.php b/layouts/joomla/form/field/tag.php index 1c6de774d47be..e7a586c6e9bdf 100644 --- a/layouts/joomla/form/field/tag.php +++ b/layouts/joomla/form/field/tag.php @@ -1,4 +1,5 @@ escape($hint ?: Text::_('JGLOBAL_TYPE_OR_SELECT_SOME_TAGS')) . '" '; $attr2 .= $dataAttribute; -if ($allowCustom) -{ - $attr2 .= $allowCustom ? ' allow-custom' : ''; - $attr2 .= $allowCustom ? ' new-item-prefix="#new#"' : ''; +if ($allowCustom) { + $attr2 .= $allowCustom ? ' allow-custom' : ''; + $attr2 .= $allowCustom ? ' new-item-prefix="#new#"' : ''; } -if ($remoteSearch) -{ - $attr2 .= ' remote-search'; - $attr2 .= ' url="' . Uri::root(true) . '/index.php?option=com_tags&task=tags.searchAjax"'; - $attr2 .= ' term-key="like"'; - $attr2 .= ' min-term-length="' . $minTermLength . '"'; +if ($remoteSearch) { + $attr2 .= ' remote-search'; + $attr2 .= ' url="' . Uri::root(true) . '/index.php?option=com_tags&task=tags.searchAjax"'; + $attr2 .= ' term-key="like"'; + $attr2 .= ' min-term-length="' . $minTermLength . '"'; } -if ($required) -{ - $attr .= ' required class="required"'; - $attr2 .= ' required'; +if ($required) { + $attr .= ' required class="required"'; + $attr2 .= ' required'; } // Create a read-only list (no name) with hidden input(s) to store the value(s). -if ($readonly) -{ - $html[] = HTMLHelper::_('select.genericlist', $options, '', trim($attr), 'value', 'text', $value, $id); - - // E.g. form field type tag sends $this->value as array - if ($multiple && is_array($value)) - { - if (!count($value)) - { - $value[] = ''; - } - - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } -} -else -// Create a regular list. +if ($readonly) { + $html[] = HTMLHelper::_('select.genericlist', $options, '', trim($attr), 'value', 'text', $value, $id); + + // E.g. form field type tag sends $this->value as array + if ($multiple && is_array($value)) { + if (!count($value)) { + $value[] = ''; + } + + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } +} else // Create a regular list. { - $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); + $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); } Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getDocument()->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select'); ?> diff --git a/layouts/joomla/form/field/tel.php b/layouts/joomla/form/field/tel.php index ddf0735ed522b..122b1e6dc83cf 100644 --- a/layouts/joomla/form/field/tel.php +++ b/layouts/joomla/form/field/tel.php @@ -1,4 +1,5 @@ - id="" - value="" - > + type="tel" + inputmode="tel" + name="" + + id="" + value="" + > diff --git a/layouts/joomla/form/field/text.php b/layouts/joomla/form/field/text.php index 300f2789ec8d5..a01f49309a8bf 100644 --- a/layouts/joomla/form/field/text.php +++ b/layouts/joomla/form/field/text.php @@ -1,4 +1,5 @@ ' . Text::_($addonBefore) . ''; @@ -88,33 +88,33 @@
    - - - + + + - - > + + > - - - + + +
    - - - value) : ?> - - - - - + + + value) : ?> + + + + + diff --git a/layouts/joomla/form/field/textarea.php b/layouts/joomla/form/field/textarea.php index dcbd3f994fd70..ceb57100957e8 100644 --- a/layouts/joomla/form/field/textarea.php +++ b/layouts/joomla/form/field/textarea.php @@ -1,4 +1,5 @@ getDocument()->getWebAssetManager(); - $wa->useScript('short-and-sweet'); +if ($charcounter) { + // Load the js file + /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + $wa->useScript('short-and-sweet'); - // Set the css class to be used as the trigger - $charcounter = ' charcount'; - // Set the text - $counterlabel = 'data-counter-label="' . $this->escape(Text::_('JFIELD_META_DESCRIPTION_COUNTER')) . '"'; + // Set the css class to be used as the trigger + $charcounter = ' charcount'; + // Set the text + $counterlabel = 'data-counter-label="' . $this->escape(Text::_('JFIELD_META_DESCRIPTION_COUNTER')) . '"'; } $attributes = array( - $columns ?: '', - $rows ?: '', - !empty($class) ? 'class="form-control ' . $class . $charcounter . '"' : 'class="form-control' . $charcounter . '"', - !empty($description) ? 'aria-describedby="' . ($id ?: $name) . '-desc"' : '', - strlen($hint) ? 'placeholder="' . htmlspecialchars($hint, ENT_COMPAT, 'UTF-8') . '"' : '', - $disabled ? 'disabled' : '', - $readonly ? 'readonly' : '', - $onchange ? 'onchange="' . $onchange . '"' : '', - $onclick ? 'onclick="' . $onclick . '"' : '', - $required ? 'required' : '', - !empty($autocomplete) ? 'autocomplete="' . $autocomplete . '"' : '', - $autofocus ? 'autofocus' : '', - $spellcheck ? '' : 'spellcheck="false"', - $maxlength ?: '', - !empty($counterlabel) ? $counterlabel : '', - $dataAttribute, + $columns ?: '', + $rows ?: '', + !empty($class) ? 'class="form-control ' . $class . $charcounter . '"' : 'class="form-control' . $charcounter . '"', + !empty($description) ? 'aria-describedby="' . ($id ?: $name) . '-desc"' : '', + strlen($hint) ? 'placeholder="' . htmlspecialchars($hint, ENT_COMPAT, 'UTF-8') . '"' : '', + $disabled ? 'disabled' : '', + $readonly ? 'readonly' : '', + $onchange ? 'onchange="' . $onchange . '"' : '', + $onclick ? 'onclick="' . $onclick . '"' : '', + $required ? 'required' : '', + !empty($autocomplete) ? 'autocomplete="' . $autocomplete . '"' : '', + $autofocus ? 'autofocus' : '', + $spellcheck ? '' : 'spellcheck="false"', + $maxlength ?: '', + !empty($counterlabel) ? $counterlabel : '', + $dataAttribute, ); ?> diff --git a/layouts/joomla/tinymce/togglebutton.php b/layouts/joomla/tinymce/togglebutton.php index 6b19fc1efc18a..d963552b96c58 100644 --- a/layouts/joomla/tinymce/togglebutton.php +++ b/layouts/joomla/tinymce/togglebutton.php @@ -1,4 +1,5 @@
    -
    - -
    +
    + +
    diff --git a/layouts/joomla/toolbar/base.php b/layouts/joomla/toolbar/base.php index b27e92096da46..dfba1abbcd6e0 100644 --- a/layouts/joomla/toolbar/base.php +++ b/layouts/joomla/toolbar/base.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('core') - ->useScript('webcomponent.toolbar-button'); + ->useScript('core') + ->useScript('webcomponent.toolbar-button'); extract($displayData, EXTR_OVERWRITE); @@ -47,35 +48,31 @@ $validate = !empty($formValidation) ? ' form-validation' : ''; $msgAttr = !empty($message) ? ' confirm-message="' . $this->escape($message) . '"' : ''; -if ($id === 'toolbar-help') -{ - $title = ' title="' . Text::_('JGLOBAL_OPENS_IN_A_NEW_WINDOW') . '"'; +if ($id === 'toolbar-help') { + $title = ' title="' . Text::_('JGLOBAL_OPENS_IN_A_NEW_WINDOW') . '"'; } -if (!empty($task)) -{ - $taskAttr = ' task="' . $task . '"'; -} -elseif (!empty($onclick)) -{ - $htmlAttributes .= ' onclick="' . $onclick . '"'; +if (!empty($task)) { + $taskAttr = ' task="' . $task . '"'; +} elseif (!empty($onclick)) { + $htmlAttributes .= ' onclick="' . $onclick . '"'; } $direction = Factory::getLanguage()->isRtl() ? 'dropdown-menu-end' : ''; ?> > < - class="" - - - > - - + class="" + + + > + + > - - + + diff --git a/layouts/joomla/toolbar/batch.php b/layouts/joomla/toolbar/batch.php index f9cd16ed06f71..a3d2cbc4604a2 100644 --- a/layouts/joomla/toolbar/batch.php +++ b/layouts/joomla/toolbar/batch.php @@ -1,4 +1,5 @@ type="button" onclick="if (document.adminForm.boxchecked.value==0){}else{document.getElementById('collapseModal').open(); return true;}" class="btn btn-primary"> - - + + diff --git a/layouts/joomla/toolbar/containerclose.php b/layouts/joomla/toolbar/containerclose.php index 8c687c4a992ef..aa9e8e5bb13cd 100644 --- a/layouts/joomla/toolbar/containerclose.php +++ b/layouts/joomla/toolbar/containerclose.php @@ -1,4 +1,5 @@ - - - - - - - - - + + + + + + + + + diff --git a/layouts/joomla/toolbar/iconclass.php b/layouts/joomla/toolbar/iconclass.php index 28a101b0c7b98..4a26e57453bf5 100644 --- a/layouts/joomla/toolbar/iconclass.php +++ b/layouts/joomla/toolbar/iconclass.php @@ -1,4 +1,5 @@ getDocument() - ->getWebAssetManager()->useScript('inlinehelp'); + ->getWebAssetManager()->useScript('inlinehelp'); echo LayoutHelper::render('joomla.toolbar.standard', $displayData); diff --git a/layouts/joomla/toolbar/link.php b/layouts/joomla/toolbar/link.php index ff89f87fba0c5..6ee810135850b 100644 --- a/layouts/joomla/toolbar/link.php +++ b/layouts/joomla/toolbar/link.php @@ -1,4 +1,5 @@ - - > - - - + + > + + + diff --git a/layouts/joomla/toolbar/popup.php b/layouts/joomla/toolbar/popup.php index 25dcd392453b1..3ee4f2198e441 100644 --- a/layouts/joomla/toolbar/popup.php +++ b/layouts/joomla/toolbar/popup.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('core') - ->useScript('webcomponent.toolbar-button'); + ->useScript('core') + ->useScript('webcomponent.toolbar-button'); $tagName = $tagName ?? 'button'; @@ -43,12 +44,12 @@ ?> > < - value="" - class="" - - + value="" + class="" + + > - - + + > diff --git a/layouts/joomla/toolbar/separator.php b/layouts/joomla/toolbar/separator.php index dc5970af51b97..2adc1fc26e5be 100644 --- a/layouts/joomla/toolbar/separator.php +++ b/layouts/joomla/toolbar/separator.php @@ -1,4 +1,5 @@ - - - - - - + + + + + + diff --git a/layouts/joomla/toolbar/standard.php b/layouts/joomla/toolbar/standard.php index f7dbff435d94d..a7d18c83685b6 100644 --- a/layouts/joomla/toolbar/standard.php +++ b/layouts/joomla/toolbar/standard.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('core') - ->useScript('webcomponent.toolbar-button'); + ->useScript('core') + ->useScript('webcomponent.toolbar-button'); $tagName = $tagName ?? 'button'; @@ -43,13 +44,10 @@ $validate = !empty($formValidation) ? ' form-validation' : ''; $msgAttr = !empty($message) ? ' confirm-message="' . $this->escape($message) . '"' : ''; -if (!empty($task)) -{ - $taskAttr = ' task="' . $task . '"'; -} -elseif (!empty($onclick)) -{ - $htmlAttributes .= ' onclick="' . $onclick . '"'; +if (!empty($task)) { + $taskAttr = ' task="' . $task . '"'; +} elseif (!empty($onclick)) { + $htmlAttributes .= ' onclick="' . $onclick . '"'; } ?> @@ -57,16 +55,16 @@ > - - + + < - class="" - - > - - + class="" + + > + + > diff --git a/layouts/joomla/toolbar/title.php b/layouts/joomla/toolbar/title.php index ad2c9a6075344..2f2fb21c0df33 100644 --- a/layouts/joomla/toolbar/title.php +++ b/layouts/joomla/toolbar/title.php @@ -1,4 +1,5 @@

    - $icon]); ?> - + $icon]); ?> +

    diff --git a/layouts/joomla/toolbar/versions.php b/layouts/joomla/toolbar/versions.php index 15d47b41d85b6..68a56dd768c38 100644 --- a/layouts/joomla/toolbar/versions.php +++ b/layouts/joomla/toolbar/versions.php @@ -1,4 +1,5 @@ getRegistry()->addExtensionRegistryFile('com_contenthistory'); $wa->useScript('core') - ->useScript('webcomponent.toolbar-button') - ->useScript('com_contenthistory.admin-history-versions'); + ->useScript('webcomponent.toolbar-button') + ->useScript('com_contenthistory.admin-history-versions'); echo HTMLHelper::_( - 'bootstrap.renderModal', - 'versionsModal', - array( - 'url' => 'index.php?' . http_build_query( - [ - 'option' => 'com_contenthistory', - 'view' => 'history', - 'layout' => 'modal', - 'tmpl' => 'component', - 'item_id' => $itemId, - Session::getFormToken() => 1 - ] - ), - 'title' => $title, - 'height' => '100%', - 'width' => '100%', - 'modalWidth' => '80', - 'bodyHeight' => '60', - 'footer' => '' - ) + 'bootstrap.renderModal', + 'versionsModal', + array( + 'url' => 'index.php?' . http_build_query( + [ + 'option' => 'com_contenthistory', + 'view' => 'history', + 'layout' => 'modal', + 'tmpl' => 'component', + 'item_id' => $itemId, + Session::getFormToken() => 1 + ] + ), + 'title' => $title, + 'height' => '100%', + 'width' => '100%', + 'modalWidth' => '80', + 'bodyHeight' => '60', + 'footer' => '' + ) ); ?> - + diff --git a/layouts/libraries/html/bootstrap/modal/body.php b/layouts/libraries/html/bootstrap/modal/body.php index 26dc032fcc01e..9bac638c53749 100644 --- a/layouts/libraries/html/bootstrap/modal/body.php +++ b/layouts/libraries/html/bootstrap/modal/body.php @@ -1,4 +1,5 @@ = 20 && $bodyHeight < 90) -{ - $bodyClass .= ' jviewport-height' . $bodyHeight; +if ($bodyHeight && $bodyHeight >= 20 && $bodyHeight < 90) { + $bodyClass .= ' jviewport-height' . $bodyHeight; } ?>
    - +
    diff --git a/layouts/libraries/html/bootstrap/modal/footer.php b/layouts/libraries/html/bootstrap/modal/footer.php index 78f11f45d95ec..8dd0f62c21669 100644 --- a/layouts/libraries/html/bootstrap/modal/footer.php +++ b/layouts/libraries/html/bootstrap/modal/footer.php @@ -1,4 +1,5 @@ diff --git a/layouts/libraries/html/bootstrap/modal/header.php b/layouts/libraries/html/bootstrap/modal/header.php index 61d7c7265f5e9..0b03945234a68 100644 --- a/layouts/libraries/html/bootstrap/modal/header.php +++ b/layouts/libraries/html/bootstrap/modal/header.php @@ -1,4 +1,5 @@ diff --git a/layouts/libraries/html/bootstrap/modal/iframe.php b/layouts/libraries/html/bootstrap/modal/iframe.php index 468b04ce65a05..db4911b45b9b9 100644 --- a/layouts/libraries/html/bootstrap/modal/iframe.php +++ b/layouts/libraries/html/bootstrap/modal/iframe.php @@ -1,4 +1,5 @@ 'iframe', - 'src' => $params['url'] + 'class' => 'iframe', + 'src' => $params['url'] ); -if (isset($params['title'])) -{ - $iframeAttributes['name'] = addslashes($params['title']); - $iframeAttributes['title'] = addslashes($params['title']); +if (isset($params['title'])) { + $iframeAttributes['name'] = addslashes($params['title']); + $iframeAttributes['title'] = addslashes($params['title']); } -if (isset($params['height'])) -{ - $iframeAttributes['height'] = $params['height']; +if (isset($params['height'])) { + $iframeAttributes['height'] = $params['height']; } -if (isset($params['width'])) -{ - $iframeAttributes['width'] = $params['width']; +if (isset($params['width'])) { + $iframeAttributes['width'] = $params['width']; } ?> diff --git a/layouts/libraries/html/bootstrap/modal/main.php b/layouts/libraries/html/bootstrap/modal/main.php index 3cdbabb0e9ade..2f0fe32b0ab92 100644 --- a/layouts/libraries/html/bootstrap/modal/main.php +++ b/layouts/libraries/html/bootstrap/modal/main.php @@ -1,4 +1,5 @@ 0 && $modalWidth <= 100) -{ - $modalDialogClass = ' jviewport-width' . $modalWidth; +if ($modalWidth && $modalWidth > 0 && $modalWidth <= 100) { + $modalDialogClass = ' jviewport-width' . $modalWidth; } $modalAttributes = array( - 'tabindex' => '-1', - 'class' => 'joomla-modal ' .implode(' ', $modalClasses) + 'tabindex' => '-1', + 'class' => 'joomla-modal ' . implode(' ', $modalClasses) ); -if (isset($params['backdrop'])) -{ - $modalAttributes['data-bs-backdrop'] = (is_bool($params['backdrop']) ? ($params['backdrop'] ? 'true' : 'false') : $params['backdrop']); +if (isset($params['backdrop'])) { + $modalAttributes['data-bs-backdrop'] = (is_bool($params['backdrop']) ? ($params['backdrop'] ? 'true' : 'false') : $params['backdrop']); } -if (isset($params['keyboard'])) -{ - $modalAttributes['data-bs-keyboard'] = (is_bool($params['keyboard']) ? ($params['keyboard'] ? 'true' : 'false') : 'true'); +if (isset($params['keyboard'])) { + $modalAttributes['data-bs-keyboard'] = (is_bool($params['keyboard']) ? ($params['keyboard'] ? 'true' : 'false') : 'true'); } -if (isset($params['url'])) -{ - $url = 'data-url="' . $params['url'] . '"'; - $iframeHtml = htmlspecialchars(LayoutHelper::render('libraries.html.bootstrap.modal.iframe', $displayData), ENT_COMPAT, 'UTF-8'); +if (isset($params['url'])) { + $url = 'data-url="' . $params['url'] . '"'; + $iframeHtml = htmlspecialchars(LayoutHelper::render('libraries.html.bootstrap.modal.iframe', $displayData), ENT_COMPAT, 'UTF-8'); } ?> - diff --git a/layouts/libraries/html/bootstrap/tab/addtab.php b/layouts/libraries/html/bootstrap/tab/addtab.php index 286b32bcb7214..a882acda2c95b 100644 --- a/layouts/libraries/html/bootstrap/tab/addtab.php +++ b/layouts/libraries/html/bootstrap/tab/addtab.php @@ -1,4 +1,5 @@
    + class="tab-pane" + data-active="" + data-id="" + data-title=""> diff --git a/layouts/libraries/html/bootstrap/tab/endtab.php b/layouts/libraries/html/bootstrap/tab/endtab.php index 84bdf84dac664..4b21cdf6045f5 100644 --- a/layouts/libraries/html/bootstrap/tab/endtab.php +++ b/layouts/libraries/html/bootstrap/tab/endtab.php @@ -1,4 +1,5 @@ getWebAssetManager(); $wa->registerScript('tinymce', 'media/vendor/tinymce/tinymce.min.js', [], ['defer' => true]) - ->registerScript('plg_editors_tinymce', 'plg_editors_tinymce/tinymce.min.js', [], ['defer' => true], ['core', 'tinymce']) - ->registerAndUseStyle('tinymce.skin', 'media/vendor/tinymce/skins/ui/oxide/skin.min.css') - ->registerAndUseStyle('plg_editors_tinymce.builder', 'plg_editors_tinymce/tinymce-builder.css', [], [], ['tinymce.skin', 'dragula']) - ->registerScript('plg_editors_tinymce.builder', 'plg_editors_tinymce/tinymce-builder.js', [], ['type' => 'module'], ['dragula', 'plg_editors_tinymce']) - ->useScript('plg_editors_tinymce.builder') - ->useStyle('webcomponent.joomla-tab') - ->useScript('webcomponent.joomla-tab'); + ->registerScript('plg_editors_tinymce', 'plg_editors_tinymce/tinymce.min.js', [], ['defer' => true], ['core', 'tinymce']) + ->registerAndUseStyle('tinymce.skin', 'media/vendor/tinymce/skins/ui/oxide/skin.min.css') + ->registerAndUseStyle('plg_editors_tinymce.builder', 'plg_editors_tinymce/tinymce-builder.css', [], [], ['tinymce.skin', 'dragula']) + ->registerScript('plg_editors_tinymce.builder', 'plg_editors_tinymce/tinymce-builder.js', [], ['type' => 'module'], ['dragula', 'plg_editors_tinymce']) + ->useScript('plg_editors_tinymce.builder') + ->useStyle('webcomponent.joomla-tab') + ->useScript('webcomponent.joomla-tab'); // Add TinyMCE language file to translate the buttons -if ($languageFile) -{ - $wa->registerAndUseScript('tinymce.language', $languageFile, [], ['defer' => true], []); +if ($languageFile) { + $wa->registerAndUseScript('tinymce.language', $languageFile, [], ['defer' => true], []); } // Add the builder options -$doc->addScriptOptions('plg_editors_tinymce_builder', - [ - 'menus' => $menus, - 'buttons' => $buttons, - 'toolbarPreset' => $toolbarPreset, - 'formControl' => $name . '[toolbars]', - ] +$doc->addScriptOptions( + 'plg_editors_tinymce_builder', + [ + 'menus' => $menus, + 'buttons' => $buttons, + 'toolbarPreset' => $toolbarPreset, + 'formControl' => $name . '[toolbars]', + ] ); ?>
    -

    -

    -

    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - $title) : ?> - - name=""> - - 'btn-success', - 'medium' => 'btn-info', - 'advanced' => 'btn-warning', - ]; - // Check whether the values exists, and if empty then use from preset - if (empty($value['toolbars'][$num]['menu']) - && empty($value['toolbars'][$num]['toolbar1']) - && empty($value['toolbars'][$num]['toolbar2'])) - { - // Take the preset for default value - switch ($num) { - case 0: - $preset = $toolbarPreset['advanced']; - break; - case 1: - $preset = $toolbarPreset['medium']; - break; - default: - $preset = $toolbarPreset['simple']; - } - - $value['toolbars'][$num] = $preset; - } - - // Take existing values - $valMenu = empty($value['toolbars'][$num]['menu']) ? array() : $value['toolbars'][$num]['menu']; - $valBar1 = empty($value['toolbars'][$num]['toolbar1']) ? array() : $value['toolbars'][$num]['toolbar1']; - $valBar2 = empty($value['toolbars'][$num]['toolbar2']) ? array() : $value['toolbars'][$num]['toolbar2']; - - ?> - sublayout('setaccess', array('form' => $setsForms[$num])); ?> -
    -
    - - - - - - -
    -
    - -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - sublayout('setoptions', array('form' => $setsForms[$num])); ?> -
    - -
    +

    +

    +

    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + $title) : ?> + + name=""> + + 'btn-success', + 'medium' => 'btn-info', + 'advanced' => 'btn-warning', + ]; + // Check whether the values exists, and if empty then use from preset + if ( + empty($value['toolbars'][$num]['menu']) + && empty($value['toolbars'][$num]['toolbar1']) + && empty($value['toolbars'][$num]['toolbar2']) + ) { + // Take the preset for default value + switch ($num) { + case 0: + $preset = $toolbarPreset['advanced']; + break; + case 1: + $preset = $toolbarPreset['medium']; + break; + default: + $preset = $toolbarPreset['simple']; + } + + $value['toolbars'][$num] = $preset; + } + + // Take existing values + $valMenu = empty($value['toolbars'][$num]['menu']) ? array() : $value['toolbars'][$num]['menu']; + $valBar1 = empty($value['toolbars'][$num]['toolbar1']) ? array() : $value['toolbars'][$num]['toolbar1']; + $valBar2 = empty($value['toolbars'][$num]['toolbar2']) ? array() : $value['toolbars'][$num]['toolbar2']; + + ?> + sublayout('setaccess', array('form' => $setsForms[$num])); ?> +
    +
    + + + + + + +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + sublayout('setoptions', array('form' => $setsForms[$num])); ?> +
    + +
    diff --git a/layouts/plugins/editors/tinymce/field/tinymcebuilder/setaccess.php b/layouts/plugins/editors/tinymce/field/tinymcebuilder/setaccess.php index 177300678ea48..c4be4b9eccc61 100644 --- a/layouts/plugins/editors/tinymce/field/tinymcebuilder/setaccess.php +++ b/layouts/plugins/editors/tinymce/field/tinymcebuilder/setaccess.php @@ -1,4 +1,5 @@
    - renderField('access'); ?> + renderField('access'); ?>
    diff --git a/layouts/plugins/editors/tinymce/field/tinymcebuilder/setoptions.php b/layouts/plugins/editors/tinymce/field/tinymcebuilder/setoptions.php index d3df455aea199..4b51137dcd171 100644 --- a/layouts/plugins/editors/tinymce/field/tinymcebuilder/setoptions.php +++ b/layouts/plugins/editors/tinymce/field/tinymcebuilder/setoptions.php @@ -1,4 +1,5 @@
    getFieldset('basic') as $field) : ?> - renderField(); ?> + renderField(); ?>
    diff --git a/layouts/plugins/system/privacyconsent/label.php b/layouts/plugins/system/privacyconsent/label.php index 655f11905ca5f..394f10f738b3d 100644 --- a/layouts/plugins/system/privacyconsent/label.php +++ b/layouts/plugins/system/privacyconsent/label.php @@ -1,4 +1,5 @@ 'modal', - 'data-bs-target' => '#consentModal', - 'class' => 'required', - ]; +if ($privacyLink) { + $attribs = [ + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#consentModal', + 'class' => 'required', + ]; - $link = HTMLHelper::_('link', Route::_($privacyLink . '&tmpl=component'), $text, $attribs); + $link = HTMLHelper::_('link', Route::_($privacyLink . '&tmpl=component'), $text, $attribs); - echo HTMLHelper::_( - 'bootstrap.renderModal', - 'consentModal', - [ - 'url' => Route::_($privacyLink . '&tmpl=component'), - 'title' => $text, - 'height' => '100%', - 'width' => '100%', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ] - ); -} -else -{ - $link = '' . $text . ''; + echo HTMLHelper::_( + 'bootstrap.renderModal', + 'consentModal', + [ + 'url' => Route::_($privacyLink . '&tmpl=component'), + 'title' => $text, + 'height' => '100%', + 'width' => '100%', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ] + ); +} else { + $link = '' . $text . ''; } // Add the label text and star. diff --git a/layouts/plugins/system/privacyconsent/message.php b/layouts/plugins/system/privacyconsent/message.php index a9b10704b0dfb..d9c991e472ace 100644 --- a/layouts/plugins/system/privacyconsent/message.php +++ b/layouts/plugins/system/privacyconsent/message.php @@ -1,4 +1,5 @@ ' . $privacynote . '
    '; - diff --git a/layouts/plugins/system/webauthn/manage.php b/layouts/plugins/system/webauthn/manage.php index 49fc727ac5d46..186cf67956998 100644 --- a/layouts/plugins/system/webauthn/manage.php +++ b/layouts/plugins/system/webauthn/manage.php @@ -1,4 +1,5 @@ getIdentity(); +try { + $app = Factory::getApplication(); + $loggedInUser = $app->getIdentity(); - $app->getDocument()->getWebAssetManager() - ->registerAndUseStyle('plg_system_webauthn.backend', 'plg_system_webauthn/backend.css'); -} -catch (Exception $e) -{ - $loggedInUser = new User; + $app->getDocument()->getWebAssetManager() + ->registerAndUseStyle('plg_system_webauthn.backend', 'plg_system_webauthn/backend.css'); +} catch (Exception $e) { + $loggedInUser = new User(); } $defaultDisplayData = [ - 'user' => $loggedInUser, - 'allow_add' => false, - 'credentials' => [], - 'error' => '', - 'knownAuthenticators' => [], - 'attestationSupport' => true, + 'user' => $loggedInUser, + 'allow_add' => false, + 'credentials' => [], + 'error' => '', + 'knownAuthenticators' => [], + 'attestationSupport' => true, ]; extract(array_merge($defaultDisplayData, $displayData)); -if ($displayData['allow_add'] === false) -{ - $error = Text::_('PLG_SYSTEM_WEBAUTHN_CANNOT_ADD_FOR_A_USER'); +if ($displayData['allow_add'] === false) { + $error = Text::_('PLG_SYSTEM_WEBAUTHN_CANNOT_ADD_FOR_A_USER'); //phpcs:ignore $allow_add = false; } @@ -69,7 +66,7 @@ //phpcs:ignore if ($allow_add && function_exists('gmp_intval') === false && function_exists('bccomp') === false) { - $error = Text::_('PLG_SYSTEM_WEBAUTHN_REQUIRES_GMP'); + $error = Text::_('PLG_SYSTEM_WEBAUTHN_REQUIRES_GMP'); //phpcs:ignore $allow_add = false; } @@ -81,74 +78,76 @@
    -
    - -
    - +
    + +
    + - - - - - - - - - +
    - , -
    colspan="2" scope="col"> - -
    + + + + + + + + - - getAaguid() : ''; - $authMetadata = $knownAuthenticators[$aaguid->toString()] ?? $knownAuthenticators['']; - ?> - - - - - - + + getAaguid() : ''; + $authMetadata = $knownAuthenticators[$aaguid->toString()] ?? $knownAuthenticators['']; + ?> + + + + + + - - - - - -
    + , +
    colspan="2" scope="col"> + +
    - <?php echo $authMetadata->description ?> - - - -
    + <?php echo $authMetadata->description ?> + + + +
    - -
    + + + + + + + + -

    - -

    - +

    + +

    +
    diff --git a/layouts/plugins/user/terms/label.php b/layouts/plugins/user/terms/label.php index 1b4d3278a5b58..61223ffe99097 100644 --- a/layouts/plugins/user/terms/label.php +++ b/layouts/plugins/user/terms/label.php @@ -1,4 +1,5 @@ 'modal', - 'data-bs-target' => '#tosModal', - 'class' => 'required', - ]; +if ($article) { + $attribs = [ + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#tosModal', + 'class' => 'required', + ]; - $link = HTMLHelper::_('link', Route::_($article->link . '&tmpl=component'), $text, $attribs); + $link = HTMLHelper::_('link', Route::_($article->link . '&tmpl=component'), $text, $attribs); - echo HTMLHelper::_( - 'bootstrap.renderModal', - 'tosModal', - [ - 'url' => Route::_($article->link . '&tmpl=component'), - 'title' => $text, - 'height' => '100%', - 'width' => '100%', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ] - ); -} -else -{ - $link = '' . $text . ''; + echo HTMLHelper::_( + 'bootstrap.renderModal', + 'tosModal', + [ + 'url' => Route::_($article->link . '&tmpl=component'), + 'title' => $text, + 'height' => '100%', + 'width' => '100%', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ] + ); +} else { + $link = '' . $text . ''; } // Add the label text and star. diff --git a/layouts/plugins/user/terms/message.php b/layouts/plugins/user/terms/message.php index e6284a235f632..f28f1c34491ad 100644 --- a/layouts/plugins/user/terms/message.php +++ b/layouts/plugins/user/terms/message.php @@ -1,4 +1,5 @@ getDocument()->getWebAssetManager() - ->registerAndUseScript('plg_user_token.token', 'plg_user_token/token.js', [], ['defer' => true], ['core']); + ->registerAndUseScript('plg_user_token.token', 'plg_user_token/token.js', [], ['defer' => true], ['core']); ?>
    - - + +
    diff --git a/libraries/bootstrap.php b/libraries/bootstrap.php index 54a59760a2fa1..6f9f749178f01 100644 --- a/libraries/bootstrap.php +++ b/libraries/bootstrap.php @@ -1,4 +1,5 @@ withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor) + $behavior->withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor()) ); -if (in_array('phar', stream_get_wrappers())) -{ - stream_wrapper_unregister('phar'); - stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper'); +if (in_array('phar', stream_get_wrappers())) { + stream_wrapper_unregister('phar'); + stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper'); } // Define the Joomla version if not already defined. -defined('JVERSION') or define('JVERSION', (new \Joomla\CMS\Version)->getShortVersion()); +defined('JVERSION') or define('JVERSION', (new \Joomla\CMS\Version())->getShortVersion()); // Set up the message queue logger for web requests -if (array_key_exists('REQUEST_METHOD', $_SERVER)) -{ - \Joomla\CMS\Log\Log::addLogger(['logger' => 'messagequeue'], \Joomla\CMS\Log\Log::ALL, ['jerror']); +if (array_key_exists('REQUEST_METHOD', $_SERVER)) { + \Joomla\CMS\Log\Log::addLogger(['logger' => 'messagequeue'], \Joomla\CMS\Log\Log::ALL, ['jerror']); } // Register the Crypto lib diff --git a/libraries/classmap.php b/libraries/classmap.php index 1b43fa4334ef0..5b203ff2ed2fc 100644 --- a/libraries/classmap.php +++ b/libraries/classmap.php @@ -1,4 +1,5 @@ withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor) + $behavior->withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor()) ); -if (in_array('phar', stream_get_wrappers())) -{ - stream_wrapper_unregister('phar'); - stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper'); +if (in_array('phar', stream_get_wrappers())) { + stream_wrapper_unregister('phar'); + stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper'); } // Define the Joomla version if not already defined -if (!defined('JVERSION')) -{ - define('JVERSION', (new \Joomla\CMS\Version)->getShortVersion()); +if (!defined('JVERSION')) { + define('JVERSION', (new \Joomla\CMS\Version())->getShortVersion()); } // Register a handler for uncaught exceptions that shows a pretty error page when possible set_exception_handler(array('Joomla\CMS\Exception\ExceptionHandler', 'handleException')); // Set up the message queue logger for web requests -if (array_key_exists('REQUEST_METHOD', $_SERVER)) -{ - \Joomla\CMS\Log\Log::addLogger(array('logger' => 'messagequeue'), \Joomla\CMS\Log\Log::ALL, ['jerror']); +if (array_key_exists('REQUEST_METHOD', $_SERVER)) { + \Joomla\CMS\Log\Log::addLogger(array('logger' => 'messagequeue'), \Joomla\CMS\Log\Log::ALL, ['jerror']); } // Register the Crypto lib diff --git a/libraries/extensions.classmap.php b/libraries/extensions.classmap.php index fd57dc934970b..efbd76a2526d8 100644 --- a/libraries/extensions.classmap.php +++ b/libraries/extensions.classmap.php @@ -1,4 +1,5 @@ path map. - * - * @var array - * @since 3.1.4 - */ - protected static $namespaces = array(); - - /** - * Holds a reference for all deprecated aliases (mainly for use by a logging platform). - * - * @var array - * @since 3.6.3 - */ - protected static $deprecatedAliases = array(); - - /** - * The root folders where extensions can be found. - * - * @var array - * @since 4.0.0 - */ - protected static $extensionRootFolders = array(); - - /** - * Method to discover classes of a given type in a given path. - * - * @param string $classPrefix The class name prefix to use for discovery. - * @param string $parentPath Full path to the parent folder for the classes to discover. - * @param boolean $force True to overwrite the autoload path value for the class if it already exists. - * @param boolean $recurse Recurse through all child directories as well as the parent path. - * - * @return void - * - * @since 1.7.0 - * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for - * your files. - */ - public static function discover($classPrefix, $parentPath, $force = true, $recurse = false) - { - try - { - if ($recurse) - { - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($parentPath), - RecursiveIteratorIterator::SELF_FIRST - ); - } - else - { - $iterator = new DirectoryIterator($parentPath); - } - - /** @type $file DirectoryIterator */ - foreach ($iterator as $file) - { - $fileName = $file->getFilename(); - - // Only load for php files. - if ($file->isFile() && $file->getExtension() === 'php') - { - // Get the class name and full path for each file. - $class = strtolower($classPrefix . preg_replace('#\.php$#', '', $fileName)); - - // Register the class with the autoloader if not already registered or the force flag is set. - if ($force || empty(self::$classes[$class])) - { - self::register($class, $file->getPath() . '/' . $fileName); - } - } - } - } - catch (UnexpectedValueException $e) - { - // Exception will be thrown if the path is not a directory. Ignore it. - } - } - - /** - * Method to get the list of registered classes and their respective file paths for the autoloader. - * - * @return array The array of class => path values for the autoloader. - * - * @since 1.7.0 - */ - public static function getClassList() - { - return self::$classes; - } - - /** - * Method to get the list of deprecated class aliases. - * - * @return array An associative array with deprecated class alias data. - * - * @since 3.6.3 - */ - public static function getDeprecatedAliases() - { - return self::$deprecatedAliases; - } - - /** - * Method to get the list of registered namespaces. - * - * @return array The array of namespace => path values for the autoloader. - * - * @since 3.1.4 - */ - public static function getNamespaces() - { - return self::$namespaces; - } - - /** - * Loads a class from specified directories. - * - * @param string $key The class name to look for (dot notation). - * @param string $base Search this directory for the class. - * - * @return boolean True on success. - * - * @since 1.7.0 - * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for - * your files. - */ - public static function import($key, $base = null) - { - // Only import the library if not already attempted. - if (!isset(self::$imported[$key])) - { - // Setup some variables. - $success = false; - $parts = explode('.', $key); - $class = array_pop($parts); - $base = (!empty($base)) ? $base : __DIR__; - $path = str_replace('.', DIRECTORY_SEPARATOR, $key); - - // Handle special case for helper classes. - if ($class === 'helper') - { - $class = ucfirst(array_pop($parts)) . ucfirst($class); - } - // Standard class. - else - { - $class = ucfirst($class); - } - - // If we are importing a library from the Joomla namespace set the class to autoload. - if (strpos($path, 'joomla') === 0) - { - // Since we are in the Joomla namespace prepend the classname with J. - $class = 'J' . $class; - - // Only register the class for autoloading if the file exists. - if (is_file($base . '/' . $path . '.php')) - { - self::$classes[strtolower($class)] = $base . '/' . $path . '.php'; - $success = true; - } - } - /* - * If we are not importing a library from the Joomla namespace directly include the - * file since we cannot assert the file/folder naming conventions. - */ - else - { - // If the file exists attempt to include it. - if (is_file($base . '/' . $path . '.php')) - { - $success = (bool) include_once $base . '/' . $path . '.php'; - } - } - - // Add the import key to the memory cache container. - self::$imported[$key] = $success; - } - - return self::$imported[$key]; - } - - /** - * Load the file for a class. - * - * @param string $class The class to be loaded. - * - * @return boolean True on success - * - * @since 1.7.0 - */ - public static function load($class) - { - // Sanitize class name. - $key = strtolower($class); - - // If the class already exists do nothing. - if (class_exists($class, false)) - { - return true; - } - - // If the class is registered include the file. - if (isset(self::$classes[$key])) - { - $found = (bool) include_once self::$classes[$key]; - - if ($found) - { - self::loadAliasFor($class); - } - - // If the class doesn't exists, we probably have a class alias available - if (!class_exists($class, false)) - { - // Search the alias class, first none namespaced and then namespaced - $original = array_search($class, self::$classAliases) ? : array_search('\\' . $class, self::$classAliases); - - // When we have an original and the class exists an alias should be created - if ($original && class_exists($original, false)) - { - class_alias($original, $class); - } - } - - return true; - } - - return false; - } - - /** - * Directly register a class to the autoload list. - * - * @param string $class The class name to register. - * @param string $path Full path to the file that holds the class to register. - * @param boolean $force True to overwrite the autoload path value for the class if it already exists. - * - * @return void - * - * @since 1.7.0 - * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for - * your files. - */ - public static function register($class, $path, $force = true) - { - // When an alias exists, register it as well - if (array_key_exists(strtolower($class), self::$classAliases)) - { - self::register(self::stripFirstBackslash(self::$classAliases[strtolower($class)]), $path, $force); - } - - // Sanitize class name. - $class = strtolower($class); - - // Only attempt to register the class if the name and file exist. - if (!empty($class) && is_file($path)) - { - // Register the class with the autoloader if not already registered or the force flag is set. - if ($force || empty(self::$classes[$class])) - { - self::$classes[$class] = $path; - } - } - } - - /** - * Register a class prefix with lookup path. This will allow developers to register library - * packages with different class prefixes to the system autoloader. More than one lookup path - * may be registered for the same class prefix, but if this method is called with the reset flag - * set to true then any registered lookups for the given prefix will be overwritten with the current - * lookup path. When loaded, prefix paths are searched in a "last in, first out" order. - * - * @param string $prefix The class prefix to register. - * @param string $path Absolute file path to the library root where classes with the given prefix can be found. - * @param boolean $reset True to reset the prefix with only the given lookup path. - * @param boolean $prepend If true, push the path to the beginning of the prefix lookup paths array. - * - * @return void - * - * @throws RuntimeException - * - * @since 3.0.0 - */ - public static function registerPrefix($prefix, $path, $reset = false, $prepend = false) - { - // Verify the library path exists. - if (!is_dir($path)) - { - $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path); - - throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); - } - - // If the prefix is not yet registered or we have an explicit reset flag then set set the path. - if ($reset || !isset(self::$prefixes[$prefix])) - { - self::$prefixes[$prefix] = array($path); - } - // Otherwise we want to simply add the path to the prefix. - else - { - if ($prepend) - { - array_unshift(self::$prefixes[$prefix], $path); - } - else - { - self::$prefixes[$prefix][] = $path; - } - } - } - - /** - * Offers the ability for "just in time" usage of `class_alias()`. - * You cannot overwrite an existing alias. - * - * @param string $alias The alias name to register. - * @param string $original The original class to alias. - * @param string|boolean $version The version in which the alias will no longer be present. - * - * @return boolean True if registration was successful. False if the alias already exists. - * - * @since 3.2 - */ - public static function registerAlias($alias, $original, $version = false) - { - // PHP is case insensitive so support all kind of alias combination - $lowercasedAlias = strtolower($alias); - - if (!isset(self::$classAliases[$lowercasedAlias])) - { - self::$classAliases[$lowercasedAlias] = $original; - - $original = self::stripFirstBackslash($original); - - if (!isset(self::$classAliasesInverse[$original])) - { - self::$classAliasesInverse[$original] = array($lowercasedAlias); - } - else - { - self::$classAliasesInverse[$original][] = $lowercasedAlias; - } - - // If given a version, log this alias as deprecated - if ($version) - { - self::$deprecatedAliases[] = array('old' => $alias, 'new' => $original, 'version' => $version); - } - - return true; - } - - return false; - } - - /** - * Register a namespace to the autoloader. When loaded, namespace paths are searched in a "last in, first out" order. - * - * @param string $namespace A case sensitive Namespace to register. - * @param string $path A case sensitive absolute file path to the library root where classes of the given namespace can be found. - * @param boolean $reset True to reset the namespace with only the given lookup path. - * @param boolean $prepend If true, push the path to the beginning of the namespace lookup paths array. - * - * @return void - * - * @throws RuntimeException - * - * @since 3.1.4 - */ - public static function registerNamespace($namespace, $path, $reset = false, $prepend = false) - { - // Verify the library path exists. - if (!is_dir($path)) - { - $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path); - - throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); - } - - // Trim leading and trailing backslashes from namespace, allowing "\Parent\Child", "Parent\Child\" and "\Parent\Child\" to be treated the same way. - $namespace = trim($namespace, '\\'); - - // If the namespace is not yet registered or we have an explicit reset flag then set the path. - if ($reset || !isset(self::$namespaces[$namespace])) - { - self::$namespaces[$namespace] = array($path); - } - - // Otherwise we want to simply add the path to the namespace. - else - { - if ($prepend) - { - array_unshift(self::$namespaces[$namespace], $path); - } - else - { - self::$namespaces[$namespace][] = $path; - } - } - } - - /** - * Method to setup the autoloaders for the Joomla Platform. - * Since the SPL autoloaders are called in a queue we will add our explicit - * class-registration based loader first, then fall back on the autoloader based on conventions. - * This will allow people to register a class in a specific location and override platform libraries - * as was previously possible. - * - * @param boolean $enablePsr True to enable autoloading based on PSR-0. - * @param boolean $enablePrefixes True to enable prefix based class loading (needed to auto load the Joomla core). - * @param boolean $enableClasses True to enable class map based class loading (needed to auto load the Joomla core). - * - * @return void - * - * @since 3.1.4 - */ - public static function setup($enablePsr = true, $enablePrefixes = true, $enableClasses = true) - { - if ($enableClasses) - { - // Register the class map based autoloader. - spl_autoload_register(array('JLoader', 'load')); - } - - if ($enablePrefixes) - { - // Register the prefix autoloader. - spl_autoload_register(array('JLoader', '_autoload')); - } - - if ($enablePsr) - { - // Register the PSR based autoloader. - spl_autoload_register(array('JLoader', 'loadByPsr')); - spl_autoload_register(array('JLoader', 'loadByAlias')); - } - } - - /** - * Method to autoload classes that are namespaced to the PSR-4 standard. - * - * @param string $class The fully qualified class name to autoload. - * - * @return boolean True on success, false otherwise. - * - * @since 3.7.0 - * @deprecated 5.0 Use JLoader::loadByPsr instead - */ - public static function loadByPsr4($class) - { - return self::loadByPsr($class); - } - - /** - * Method to autoload classes that are namespaced to the PSR-4 standard. - * - * @param string $class The fully qualified class name to autoload. - * - * @return boolean True on success, false otherwise. - * - * @since 4.0.0 - */ - public static function loadByPsr($class) - { - $class = self::stripFirstBackslash($class); - - // Find the location of the last NS separator. - $pos = strrpos($class, '\\'); - - // If one is found, we're dealing with a NS'd class. - if ($pos !== false) - { - $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR; - $className = substr($class, $pos + 1); - } - // If not, no need to parse path. - else - { - $classPath = null; - $className = $class; - } - - $classPath .= $className . '.php'; - - // Loop through registered namespaces until we find a match. - foreach (self::$namespaces as $ns => $paths) - { - if (strpos($class, "{$ns}\\") === 0) - { - $nsPath = trim(str_replace('\\', DIRECTORY_SEPARATOR, $ns), DIRECTORY_SEPARATOR); - - // Loop through paths registered to this namespace until we find a match. - foreach ($paths as $path) - { - $classFilePath = realpath($path . DIRECTORY_SEPARATOR . substr_replace($classPath, '', 0, strlen($nsPath) + 1)); - - // We do not allow files outside the namespace root to be loaded - if (strpos($classFilePath, realpath($path)) !== 0) - { - continue; - } - - // We check for class_exists to handle case-sensitive file systems - if (is_file($classFilePath) && !class_exists($class, false)) - { - $found = (bool) include_once $classFilePath; - - if ($found) - { - self::loadAliasFor($class); - } - - return $found; - } - } - } - } - - return false; - } - - /** - * Method to autoload classes that have been aliased using the registerAlias method. - * - * @param string $class The fully qualified class name to autoload. - * - * @return boolean True on success, false otherwise. - * - * @since 3.2 - */ - public static function loadByAlias($class) - { - $class = strtolower(self::stripFirstBackslash($class)); - - if (isset(self::$classAliases[$class])) - { - // Force auto-load of the regular class - class_exists(self::$classAliases[$class], true); - - // Normally this shouldn't execute as the autoloader will execute applyAliasFor when the regular class is - // auto-loaded above. - if (!class_exists($class, false) && !interface_exists($class, false)) - { - class_alias(self::$classAliases[$class], $class); - } - } - } - - /** - * Applies a class alias for an already loaded class, if a class alias was created for it. - * - * @param string $class We'll look for and register aliases for this (real) class name - * - * @return void - * - * @since 3.4 - */ - public static function applyAliasFor($class) - { - $class = self::stripFirstBackslash($class); - - if (isset(self::$classAliasesInverse[$class])) - { - foreach (self::$classAliasesInverse[$class] as $alias) - { - class_alias($class, $alias); - } - } - } - - /** - * Autoload a class based on name. - * - * @param string $class The class to be loaded. - * - * @return boolean True if the class was loaded, false otherwise. - * - * @since 1.7.3 - */ - public static function _autoload($class) - { - foreach (self::$prefixes as $prefix => $lookup) - { - $chr = strlen($prefix) < strlen($class) ? $class[strlen($prefix)] : 0; - - if (strpos($class, $prefix) === 0 && ($chr === strtoupper($chr))) - { - return self::_load(substr($class, strlen($prefix)), $lookup); - } - } - - return false; - } - - /** - * Load a class based on name and lookup array. - * - * @param string $class The class to be loaded (without prefix). - * @param array $lookup The array of base paths to use for finding the class file. - * - * @return boolean True if the class was loaded, false otherwise. - * - * @since 3.0.0 - */ - private static function _load($class, $lookup) - { - // Split the class name into parts separated by camelCase. - $parts = preg_split('/(?<=[a-z0-9])(?=[A-Z])/x', $class); - $partsCount = count($parts); - - foreach ($lookup as $base) - { - // Generate the path based on the class name parts. - $path = realpath($base . '/' . implode('/', array_map('strtolower', $parts)) . '.php'); - - // Load the file if it exists and is in the lookup path. - if (strpos($path, realpath($base)) === 0 && is_file($path)) - { - $found = (bool) include_once $path; - - if ($found) - { - self::loadAliasFor($class); - } - - return $found; - } - - // Backwards compatibility patch - - // If there is only one part we want to duplicate that part for generating the path. - if ($partsCount === 1) - { - // Generate the path based on the class name parts. - $path = realpath($base . '/' . implode('/', array_map('strtolower', array($parts[0], $parts[0]))) . '.php'); - - // Load the file if it exists and is in the lookup path. - if (strpos($path, realpath($base)) === 0 && is_file($path)) - { - $found = (bool) include_once $path; - - if ($found) - { - self::loadAliasFor($class); - } - - return $found; - } - } - } - - return false; - } - - /** - * Loads the aliases for the given class. - * - * @param string $class The class. - * - * @return void - * - * @since 3.8.0 - */ - private static function loadAliasFor($class) - { - if (!array_key_exists($class, self::$classAliasesInverse)) - { - return; - } - - foreach (self::$classAliasesInverse[$class] as $alias) - { - // Force auto-load of the alias class - class_exists($alias, true); - } - } - - /** - * Strips the first backslash from the given class if present. - * - * @param string $class The class to strip the first prefix from. - * - * @return string The striped class name. - * - * @since 3.8.0 - */ - private static function stripFirstBackslash($class) - { - return $class && $class[0] === '\\' ? substr($class, 1) : $class; - } + /** + * Container for already imported library paths. + * + * @var array + * @since 1.7.0 + */ + protected static $classes = array(); + + /** + * Container for already imported library paths. + * + * @var array + * @since 1.7.0 + */ + protected static $imported = array(); + + /** + * Container for registered library class prefixes and path lookups. + * + * @var array + * @since 3.0.0 + */ + protected static $prefixes = array(); + + /** + * Holds proxy classes and the class names the proxy. + * + * @var array + * @since 3.2 + */ + protected static $classAliases = array(); + + /** + * Holds the inverse lookup for proxy classes and the class names the proxy. + * + * @var array + * @since 3.4 + */ + protected static $classAliasesInverse = array(); + + /** + * Container for namespace => path map. + * + * @var array + * @since 3.1.4 + */ + protected static $namespaces = array(); + + /** + * Holds a reference for all deprecated aliases (mainly for use by a logging platform). + * + * @var array + * @since 3.6.3 + */ + protected static $deprecatedAliases = array(); + + /** + * The root folders where extensions can be found. + * + * @var array + * @since 4.0.0 + */ + protected static $extensionRootFolders = array(); + + /** + * Method to discover classes of a given type in a given path. + * + * @param string $classPrefix The class name prefix to use for discovery. + * @param string $parentPath Full path to the parent folder for the classes to discover. + * @param boolean $force True to overwrite the autoload path value for the class if it already exists. + * @param boolean $recurse Recurse through all child directories as well as the parent path. + * + * @return void + * + * @since 1.7.0 + * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for + * your files. + */ + public static function discover($classPrefix, $parentPath, $force = true, $recurse = false) + { + try { + if ($recurse) { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($parentPath), + RecursiveIteratorIterator::SELF_FIRST + ); + } else { + $iterator = new DirectoryIterator($parentPath); + } + + /** @type $file DirectoryIterator */ + foreach ($iterator as $file) { + $fileName = $file->getFilename(); + + // Only load for php files. + if ($file->isFile() && $file->getExtension() === 'php') { + // Get the class name and full path for each file. + $class = strtolower($classPrefix . preg_replace('#\.php$#', '', $fileName)); + + // Register the class with the autoloader if not already registered or the force flag is set. + if ($force || empty(self::$classes[$class])) { + self::register($class, $file->getPath() . '/' . $fileName); + } + } + } + } catch (UnexpectedValueException $e) { + // Exception will be thrown if the path is not a directory. Ignore it. + } + } + + /** + * Method to get the list of registered classes and their respective file paths for the autoloader. + * + * @return array The array of class => path values for the autoloader. + * + * @since 1.7.0 + */ + public static function getClassList() + { + return self::$classes; + } + + /** + * Method to get the list of deprecated class aliases. + * + * @return array An associative array with deprecated class alias data. + * + * @since 3.6.3 + */ + public static function getDeprecatedAliases() + { + return self::$deprecatedAliases; + } + + /** + * Method to get the list of registered namespaces. + * + * @return array The array of namespace => path values for the autoloader. + * + * @since 3.1.4 + */ + public static function getNamespaces() + { + return self::$namespaces; + } + + /** + * Loads a class from specified directories. + * + * @param string $key The class name to look for (dot notation). + * @param string $base Search this directory for the class. + * + * @return boolean True on success. + * + * @since 1.7.0 + * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for + * your files. + */ + public static function import($key, $base = null) + { + // Only import the library if not already attempted. + if (!isset(self::$imported[$key])) { + // Setup some variables. + $success = false; + $parts = explode('.', $key); + $class = array_pop($parts); + $base = (!empty($base)) ? $base : __DIR__; + $path = str_replace('.', DIRECTORY_SEPARATOR, $key); + + // Handle special case for helper classes. + if ($class === 'helper') { + $class = ucfirst(array_pop($parts)) . ucfirst($class); + } + // Standard class. + else { + $class = ucfirst($class); + } + + // If we are importing a library from the Joomla namespace set the class to autoload. + if (strpos($path, 'joomla') === 0) { + // Since we are in the Joomla namespace prepend the classname with J. + $class = 'J' . $class; + + // Only register the class for autoloading if the file exists. + if (is_file($base . '/' . $path . '.php')) { + self::$classes[strtolower($class)] = $base . '/' . $path . '.php'; + $success = true; + } + } + /* + * If we are not importing a library from the Joomla namespace directly include the + * file since we cannot assert the file/folder naming conventions. + */ + else { + // If the file exists attempt to include it. + if (is_file($base . '/' . $path . '.php')) { + $success = (bool) include_once $base . '/' . $path . '.php'; + } + } + + // Add the import key to the memory cache container. + self::$imported[$key] = $success; + } + + return self::$imported[$key]; + } + + /** + * Load the file for a class. + * + * @param string $class The class to be loaded. + * + * @return boolean True on success + * + * @since 1.7.0 + */ + public static function load($class) + { + // Sanitize class name. + $key = strtolower($class); + + // If the class already exists do nothing. + if (class_exists($class, false)) { + return true; + } + + // If the class is registered include the file. + if (isset(self::$classes[$key])) { + $found = (bool) include_once self::$classes[$key]; + + if ($found) { + self::loadAliasFor($class); + } + + // If the class doesn't exists, we probably have a class alias available + if (!class_exists($class, false)) { + // Search the alias class, first none namespaced and then namespaced + $original = array_search($class, self::$classAliases) ? : array_search('\\' . $class, self::$classAliases); + + // When we have an original and the class exists an alias should be created + if ($original && class_exists($original, false)) { + class_alias($original, $class); + } + } + + return true; + } + + return false; + } + + /** + * Directly register a class to the autoload list. + * + * @param string $class The class name to register. + * @param string $path Full path to the file that holds the class to register. + * @param boolean $force True to overwrite the autoload path value for the class if it already exists. + * + * @return void + * + * @since 1.7.0 + * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for + * your files. + */ + public static function register($class, $path, $force = true) + { + // When an alias exists, register it as well + if (array_key_exists(strtolower($class), self::$classAliases)) { + self::register(self::stripFirstBackslash(self::$classAliases[strtolower($class)]), $path, $force); + } + + // Sanitize class name. + $class = strtolower($class); + + // Only attempt to register the class if the name and file exist. + if (!empty($class) && is_file($path)) { + // Register the class with the autoloader if not already registered or the force flag is set. + if ($force || empty(self::$classes[$class])) { + self::$classes[$class] = $path; + } + } + } + + /** + * Register a class prefix with lookup path. This will allow developers to register library + * packages with different class prefixes to the system autoloader. More than one lookup path + * may be registered for the same class prefix, but if this method is called with the reset flag + * set to true then any registered lookups for the given prefix will be overwritten with the current + * lookup path. When loaded, prefix paths are searched in a "last in, first out" order. + * + * @param string $prefix The class prefix to register. + * @param string $path Absolute file path to the library root where classes with the given prefix can be found. + * @param boolean $reset True to reset the prefix with only the given lookup path. + * @param boolean $prepend If true, push the path to the beginning of the prefix lookup paths array. + * + * @return void + * + * @throws RuntimeException + * + * @since 3.0.0 + */ + public static function registerPrefix($prefix, $path, $reset = false, $prepend = false) + { + // Verify the library path exists. + if (!is_dir($path)) { + $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path); + + throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); + } + + // If the prefix is not yet registered or we have an explicit reset flag then set set the path. + if ($reset || !isset(self::$prefixes[$prefix])) { + self::$prefixes[$prefix] = array($path); + } + // Otherwise we want to simply add the path to the prefix. + else { + if ($prepend) { + array_unshift(self::$prefixes[$prefix], $path); + } else { + self::$prefixes[$prefix][] = $path; + } + } + } + + /** + * Offers the ability for "just in time" usage of `class_alias()`. + * You cannot overwrite an existing alias. + * + * @param string $alias The alias name to register. + * @param string $original The original class to alias. + * @param string|boolean $version The version in which the alias will no longer be present. + * + * @return boolean True if registration was successful. False if the alias already exists. + * + * @since 3.2 + */ + public static function registerAlias($alias, $original, $version = false) + { + // PHP is case insensitive so support all kind of alias combination + $lowercasedAlias = strtolower($alias); + + if (!isset(self::$classAliases[$lowercasedAlias])) { + self::$classAliases[$lowercasedAlias] = $original; + + $original = self::stripFirstBackslash($original); + + if (!isset(self::$classAliasesInverse[$original])) { + self::$classAliasesInverse[$original] = array($lowercasedAlias); + } else { + self::$classAliasesInverse[$original][] = $lowercasedAlias; + } + + // If given a version, log this alias as deprecated + if ($version) { + self::$deprecatedAliases[] = array('old' => $alias, 'new' => $original, 'version' => $version); + } + + return true; + } + + return false; + } + + /** + * Register a namespace to the autoloader. When loaded, namespace paths are searched in a "last in, first out" order. + * + * @param string $namespace A case sensitive Namespace to register. + * @param string $path A case sensitive absolute file path to the library root where classes of the given namespace can be found. + * @param boolean $reset True to reset the namespace with only the given lookup path. + * @param boolean $prepend If true, push the path to the beginning of the namespace lookup paths array. + * + * @return void + * + * @throws RuntimeException + * + * @since 3.1.4 + */ + public static function registerNamespace($namespace, $path, $reset = false, $prepend = false) + { + // Verify the library path exists. + if (!is_dir($path)) { + $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path); + + throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); + } + + // Trim leading and trailing backslashes from namespace, allowing "\Parent\Child", "Parent\Child\" and "\Parent\Child\" to be treated the same way. + $namespace = trim($namespace, '\\'); + + // If the namespace is not yet registered or we have an explicit reset flag then set the path. + if ($reset || !isset(self::$namespaces[$namespace])) { + self::$namespaces[$namespace] = array($path); + } + + // Otherwise we want to simply add the path to the namespace. + else { + if ($prepend) { + array_unshift(self::$namespaces[$namespace], $path); + } else { + self::$namespaces[$namespace][] = $path; + } + } + } + + /** + * Method to setup the autoloaders for the Joomla Platform. + * Since the SPL autoloaders are called in a queue we will add our explicit + * class-registration based loader first, then fall back on the autoloader based on conventions. + * This will allow people to register a class in a specific location and override platform libraries + * as was previously possible. + * + * @param boolean $enablePsr True to enable autoloading based on PSR-0. + * @param boolean $enablePrefixes True to enable prefix based class loading (needed to auto load the Joomla core). + * @param boolean $enableClasses True to enable class map based class loading (needed to auto load the Joomla core). + * + * @return void + * + * @since 3.1.4 + */ + public static function setup($enablePsr = true, $enablePrefixes = true, $enableClasses = true) + { + if ($enableClasses) { + // Register the class map based autoloader. + spl_autoload_register(array('JLoader', 'load')); + } + + if ($enablePrefixes) { + // Register the prefix autoloader. + spl_autoload_register(array('JLoader', '_autoload')); + } + + if ($enablePsr) { + // Register the PSR based autoloader. + spl_autoload_register(array('JLoader', 'loadByPsr')); + spl_autoload_register(array('JLoader', 'loadByAlias')); + } + } + + /** + * Method to autoload classes that are namespaced to the PSR-4 standard. + * + * @param string $class The fully qualified class name to autoload. + * + * @return boolean True on success, false otherwise. + * + * @since 3.7.0 + * @deprecated 5.0 Use JLoader::loadByPsr instead + */ + public static function loadByPsr4($class) + { + return self::loadByPsr($class); + } + + /** + * Method to autoload classes that are namespaced to the PSR-4 standard. + * + * @param string $class The fully qualified class name to autoload. + * + * @return boolean True on success, false otherwise. + * + * @since 4.0.0 + */ + public static function loadByPsr($class) + { + $class = self::stripFirstBackslash($class); + + // Find the location of the last NS separator. + $pos = strrpos($class, '\\'); + + // If one is found, we're dealing with a NS'd class. + if ($pos !== false) { + $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR; + $className = substr($class, $pos + 1); + } + // If not, no need to parse path. + else { + $classPath = null; + $className = $class; + } + + $classPath .= $className . '.php'; + + // Loop through registered namespaces until we find a match. + foreach (self::$namespaces as $ns => $paths) { + if (strpos($class, "{$ns}\\") === 0) { + $nsPath = trim(str_replace('\\', DIRECTORY_SEPARATOR, $ns), DIRECTORY_SEPARATOR); + + // Loop through paths registered to this namespace until we find a match. + foreach ($paths as $path) { + $classFilePath = realpath($path . DIRECTORY_SEPARATOR . substr_replace($classPath, '', 0, strlen($nsPath) + 1)); + + // We do not allow files outside the namespace root to be loaded + if (strpos($classFilePath, realpath($path)) !== 0) { + continue; + } + + // We check for class_exists to handle case-sensitive file systems + if (is_file($classFilePath) && !class_exists($class, false)) { + $found = (bool) include_once $classFilePath; + + if ($found) { + self::loadAliasFor($class); + } + + return $found; + } + } + } + } + + return false; + } + + /** + * Method to autoload classes that have been aliased using the registerAlias method. + * + * @param string $class The fully qualified class name to autoload. + * + * @return boolean True on success, false otherwise. + * + * @since 3.2 + */ + public static function loadByAlias($class) + { + $class = strtolower(self::stripFirstBackslash($class)); + + if (isset(self::$classAliases[$class])) { + // Force auto-load of the regular class + class_exists(self::$classAliases[$class], true); + + // Normally this shouldn't execute as the autoloader will execute applyAliasFor when the regular class is + // auto-loaded above. + if (!class_exists($class, false) && !interface_exists($class, false)) { + class_alias(self::$classAliases[$class], $class); + } + } + } + + /** + * Applies a class alias for an already loaded class, if a class alias was created for it. + * + * @param string $class We'll look for and register aliases for this (real) class name + * + * @return void + * + * @since 3.4 + */ + public static function applyAliasFor($class) + { + $class = self::stripFirstBackslash($class); + + if (isset(self::$classAliasesInverse[$class])) { + foreach (self::$classAliasesInverse[$class] as $alias) { + class_alias($class, $alias); + } + } + } + + /** + * Autoload a class based on name. + * + * @param string $class The class to be loaded. + * + * @return boolean True if the class was loaded, false otherwise. + * + * @since 1.7.3 + */ + public static function _autoload($class) + { + foreach (self::$prefixes as $prefix => $lookup) { + $chr = strlen($prefix) < strlen($class) ? $class[strlen($prefix)] : 0; + + if (strpos($class, $prefix) === 0 && ($chr === strtoupper($chr))) { + return self::_load(substr($class, strlen($prefix)), $lookup); + } + } + + return false; + } + + /** + * Load a class based on name and lookup array. + * + * @param string $class The class to be loaded (without prefix). + * @param array $lookup The array of base paths to use for finding the class file. + * + * @return boolean True if the class was loaded, false otherwise. + * + * @since 3.0.0 + */ + private static function _load($class, $lookup) + { + // Split the class name into parts separated by camelCase. + $parts = preg_split('/(?<=[a-z0-9])(?=[A-Z])/x', $class); + $partsCount = count($parts); + + foreach ($lookup as $base) { + // Generate the path based on the class name parts. + $path = realpath($base . '/' . implode('/', array_map('strtolower', $parts)) . '.php'); + + // Load the file if it exists and is in the lookup path. + if (strpos($path, realpath($base)) === 0 && is_file($path)) { + $found = (bool) include_once $path; + + if ($found) { + self::loadAliasFor($class); + } + + return $found; + } + + // Backwards compatibility patch + + // If there is only one part we want to duplicate that part for generating the path. + if ($partsCount === 1) { + // Generate the path based on the class name parts. + $path = realpath($base . '/' . implode('/', array_map('strtolower', array($parts[0], $parts[0]))) . '.php'); + + // Load the file if it exists and is in the lookup path. + if (strpos($path, realpath($base)) === 0 && is_file($path)) { + $found = (bool) include_once $path; + + if ($found) { + self::loadAliasFor($class); + } + + return $found; + } + } + } + + return false; + } + + /** + * Loads the aliases for the given class. + * + * @param string $class The class. + * + * @return void + * + * @since 3.8.0 + */ + private static function loadAliasFor($class) + { + if (!array_key_exists($class, self::$classAliasesInverse)) { + return; + } + + foreach (self::$classAliasesInverse[$class] as $alias) { + // Force auto-load of the alias class + class_exists($alias, true); + } + } + + /** + * Strips the first backslash from the given class if present. + * + * @param string $class The class to strip the first prefix from. + * + * @return string The striped class name. + * + * @since 3.8.0 + */ + private static function stripFirstBackslash($class) + { + return $class && $class[0] === '\\' ? substr($class, 1) : $class; + } } // Check if jexit is defined first (our unit tests mock this) -if (!function_exists('jexit')) -{ - /** - * Global application exit. - * - * This function provides a single exit point for the platform. - * - * @param mixed $message Exit code or string. Defaults to zero. - * - * @return void - * - * @codeCoverageIgnore - * @since 1.7.0 - */ - function jexit($message = 0) - { - exit($message); - } +if (!function_exists('jexit')) { + /** + * Global application exit. + * + * This function provides a single exit point for the platform. + * + * @param mixed $message Exit code or string. Defaults to zero. + * + * @return void + * + * @codeCoverageIgnore + * @since 1.7.0 + */ + function jexit($message = 0) + { + exit($message); + } } /** @@ -786,5 +720,5 @@ function jexit($message = 0) */ function jimport($path, $base = null) { - return JLoader::import($path, $base); + return JLoader::import($path, $base); } diff --git a/libraries/namespacemap.php b/libraries/namespacemap.php index 8c1dba0a101c4..d7f9373bab31a 100644 --- a/libraries/namespacemap.php +++ b/libraries/namespacemap.php @@ -1,4 +1,5 @@ file); - } - - /** - * Check if the namespace mapping file exists, if not create it - * - * @return void - * - * @since 4.0.0 - */ - public function ensureMapFileExists() - { - if (!$this->exists()) - { - $this->create(); - } - } - - /** - * Create the namespace file - * - * @return boolean - * - * @since 4.0.0 - */ - public function create() - { - $extensions = array_merge( - $this->getNamespaces('component'), - $this->getNamespaces('module'), - $this->getNamespaces('plugin'), - $this->getNamespaces('library') - ); - - ksort($extensions); - - $this->writeNamespaceFile($extensions); - - return true; - } - - /** - * Load the PSR4 file - * - * @return boolean - * - * @since 4.0.0 - */ - public function load() - { - if (!$this->exists()) - { - $this->create(); - } - - $map = $this->cachedMap ?: require $this->file; - - $loader = include JPATH_LIBRARIES . '/vendor/autoload.php'; - - foreach ($map as $namespace => $path) - { - $loader->setPsr4($namespace, $path); - } - - return true; - } - - /** - * Write the Namespace mapping file - * - * @param array $elements Array of elements - * - * @return void - * - * @since 4.0.0 - */ - protected function writeNamespaceFile($elements) - { - $content = array(); - $content[] = " $path) - { - $content[] = "\t'" . $namespace . "'" . ' => [' . $path . '],'; - } - - $content[] = '];'; - - /** - * Backup the current error_reporting level and set a new level - * - * We do this because file_put_contents can raise a Warning if it cannot write the autoload_psr4.php file - * and this will output to the response BEFORE the session has started, causing the session start to fail - * and ultimately leading us to a 500 Internal Server Error page just because of the output warning, which - * we can safely ignore as we can use an in-memory autoload_psr4 map temporarily, and display real errors later. - */ - $error_reporting = error_reporting(0); - - try - { - File::write($this->file, implode("\n", $content)); - } - catch (Exception $e) - { - Log::add('Could not save ' . $this->file, Log::WARNING); - - $map = []; - $constants = ['JPATH_ADMINISTRATOR', 'JPATH_API', 'JPATH_SITE', 'JPATH_PLUGINS']; - - foreach ($elements as $namespace => $path) - { - foreach ($constants as $constant) - { - $path = preg_replace(['/^(' . $constant . ")\s\.\s\'/", '/\'$/'], [constant($constant), ''], $path); - } - - $namespace = str_replace('\\\\', '\\', $namespace); - $map[$namespace] = [ $path ]; - } - - $this->cachedMap = $map; - } - - // Restore previous value of error_reporting - error_reporting($error_reporting); - } - - /** - * Get an array of namespaces with their respective path for the given extension type. - * - * @param string $type The extension type - * - * @return array - * - * @since 4.0.0 - */ - private function getNamespaces(string $type): array - { - if (!in_array($type, ['component', 'module', 'plugin', 'library'], true)) - { - return []; - } - - // Select directories containing extension manifest files. - if ($type === 'component') - { - $directories = [JPATH_ADMINISTRATOR . '/components']; - } - elseif ($type === 'module') - { - $directories = [JPATH_SITE . '/modules', JPATH_ADMINISTRATOR . '/modules']; - } - elseif ($type === 'plugin') - { - try - { - $directories = Folder::folders(JPATH_PLUGINS, '.', false, true); - } - catch (Exception $e) - { - $directories = []; - } - } - else - { - $directories = [JPATH_LIBRARIES]; - } - - $extensions = []; - - foreach ($directories as $directory) - { - try - { - $extensionFolders = Folder::folders($directory); - } - catch (Exception $e) - { - continue; - } - - foreach ($extensionFolders as $extension) - { - // Compile the extension path - $extensionPath = $directory . '/' . $extension . '/'; - - // Strip the com_ from the extension name for components - $name = str_replace('com_', '', $extension, $count); - $file = $extensionPath . $name . '.xml'; - - // If there is no manifest file, ignore. If it was a component check if the xml was named with the com_ prefix. - if (!is_file($file)) - { - if (!$count) - { - continue; - } - - $file = $extensionPath . $extension . '.xml'; - - if (!is_file($file)) - { - continue; - } - } - - // Load the manifest file - $xml = simplexml_load_file($file); - - // When invalid, ignore - if (!$xml) - { - continue; - } - - // The namespace node - $namespaceNode = $xml->namespace; - - // The namespace string - $namespace = (string) $namespaceNode; - - // Ignore when the string is empty - if (!$namespace) - { - continue; - } - - // Normalize the namespace string - $namespace = str_replace('\\', '\\\\', $namespace) . '\\\\'; - $namespacePath = rtrim($extensionPath . $namespaceNode->attributes()->path, '/'); - - if ($type === 'plugin' || $type === 'library') - { - $baseDir = $type === 'plugin' ? 'JPATH_PLUGINS . \'' : 'JPATH_LIBRARIES . \''; - $path = str_replace($type === 'plugin' ? JPATH_PLUGINS : JPATH_LIBRARIES, '', $namespacePath); - - // Set the namespace - $extensions[$namespace] = $baseDir . $path . '\''; - - continue; - } - - // Check if we need to use administrator path - $isAdministrator = strpos($namespacePath, JPATH_ADMINISTRATOR) === 0; - $path = str_replace($isAdministrator ? JPATH_ADMINISTRATOR : JPATH_SITE, '', $namespacePath); - - // Add the site path when a component - if ($type === 'component') - { - if (is_dir(JPATH_SITE . $path)) - { - $extensions[$namespace . 'Site\\\\'] = 'JPATH_SITE . \'' . $path . '\''; - } - - if (is_dir(JPATH_API . $path)) - { - $extensions[$namespace . 'Api\\\\'] = 'JPATH_API . \'' . $path . '\''; - } - } - - // Add the application specific segment when a component or module - $baseDir = $isAdministrator ? 'JPATH_ADMINISTRATOR . \'' : 'JPATH_SITE . \''; - $namespace .= $isAdministrator ? 'Administrator\\\\' : 'Site\\\\'; - - // Set the namespace - $extensions[$namespace] = $baseDir . $path . '\''; - } - } - - // Return the namespaces - return $extensions; - } + /** + * Path to the autoloader + * + * @var string + * @since 4.0.0 + */ + protected $file = JPATH_CACHE . '/autoload_psr4.php'; + + /** + * @var array|null + * @since 4.0.0 + */ + private $cachedMap = null; + + /** + * Check if the file exists + * + * @return boolean + * + * @since 4.0.0 + */ + public function exists() + { + return is_file($this->file); + } + + /** + * Check if the namespace mapping file exists, if not create it + * + * @return void + * + * @since 4.0.0 + */ + public function ensureMapFileExists() + { + if (!$this->exists()) { + $this->create(); + } + } + + /** + * Create the namespace file + * + * @return boolean + * + * @since 4.0.0 + */ + public function create() + { + $extensions = array_merge( + $this->getNamespaces('component'), + $this->getNamespaces('module'), + $this->getNamespaces('plugin'), + $this->getNamespaces('library') + ); + + ksort($extensions); + + $this->writeNamespaceFile($extensions); + + return true; + } + + /** + * Load the PSR4 file + * + * @return boolean + * + * @since 4.0.0 + */ + public function load() + { + if (!$this->exists()) { + $this->create(); + } + + $map = $this->cachedMap ?: require $this->file; + + $loader = include JPATH_LIBRARIES . '/vendor/autoload.php'; + + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + return true; + } + + /** + * Write the Namespace mapping file + * + * @param array $elements Array of elements + * + * @return void + * + * @since 4.0.0 + */ + protected function writeNamespaceFile($elements) + { + $content = array(); + $content[] = " $path) { + $content[] = "\t'" . $namespace . "'" . ' => [' . $path . '],'; + } + + $content[] = '];'; + + /** + * Backup the current error_reporting level and set a new level + * + * We do this because file_put_contents can raise a Warning if it cannot write the autoload_psr4.php file + * and this will output to the response BEFORE the session has started, causing the session start to fail + * and ultimately leading us to a 500 Internal Server Error page just because of the output warning, which + * we can safely ignore as we can use an in-memory autoload_psr4 map temporarily, and display real errors later. + */ + $error_reporting = error_reporting(0); + + try { + File::write($this->file, implode("\n", $content)); + } catch (Exception $e) { + Log::add('Could not save ' . $this->file, Log::WARNING); + + $map = []; + $constants = ['JPATH_ADMINISTRATOR', 'JPATH_API', 'JPATH_SITE', 'JPATH_PLUGINS']; + + foreach ($elements as $namespace => $path) { + foreach ($constants as $constant) { + $path = preg_replace(['/^(' . $constant . ")\s\.\s\'/", '/\'$/'], [constant($constant), ''], $path); + } + + $namespace = str_replace('\\\\', '\\', $namespace); + $map[$namespace] = [ $path ]; + } + + $this->cachedMap = $map; + } + + // Restore previous value of error_reporting + error_reporting($error_reporting); + } + + /** + * Get an array of namespaces with their respective path for the given extension type. + * + * @param string $type The extension type + * + * @return array + * + * @since 4.0.0 + */ + private function getNamespaces(string $type): array + { + if (!in_array($type, ['component', 'module', 'plugin', 'library'], true)) { + return []; + } + + // Select directories containing extension manifest files. + if ($type === 'component') { + $directories = [JPATH_ADMINISTRATOR . '/components']; + } elseif ($type === 'module') { + $directories = [JPATH_SITE . '/modules', JPATH_ADMINISTRATOR . '/modules']; + } elseif ($type === 'plugin') { + try { + $directories = Folder::folders(JPATH_PLUGINS, '.', false, true); + } catch (Exception $e) { + $directories = []; + } + } else { + $directories = [JPATH_LIBRARIES]; + } + + $extensions = []; + + foreach ($directories as $directory) { + try { + $extensionFolders = Folder::folders($directory); + } catch (Exception $e) { + continue; + } + + foreach ($extensionFolders as $extension) { + // Compile the extension path + $extensionPath = $directory . '/' . $extension . '/'; + + // Strip the com_ from the extension name for components + $name = str_replace('com_', '', $extension, $count); + $file = $extensionPath . $name . '.xml'; + + // If there is no manifest file, ignore. If it was a component check if the xml was named with the com_ prefix. + if (!is_file($file)) { + if (!$count) { + continue; + } + + $file = $extensionPath . $extension . '.xml'; + + if (!is_file($file)) { + continue; + } + } + + // Load the manifest file + $xml = simplexml_load_file($file); + + // When invalid, ignore + if (!$xml) { + continue; + } + + // The namespace node + $namespaceNode = $xml->namespace; + + // The namespace string + $namespace = (string) $namespaceNode; + + // Ignore when the string is empty + if (!$namespace) { + continue; + } + + // Normalize the namespace string + $namespace = str_replace('\\', '\\\\', $namespace) . '\\\\'; + $namespacePath = rtrim($extensionPath . $namespaceNode->attributes()->path, '/'); + + if ($type === 'plugin' || $type === 'library') { + $baseDir = $type === 'plugin' ? 'JPATH_PLUGINS . \'' : 'JPATH_LIBRARIES . \''; + $path = str_replace($type === 'plugin' ? JPATH_PLUGINS : JPATH_LIBRARIES, '', $namespacePath); + + // Set the namespace + $extensions[$namespace] = $baseDir . $path . '\''; + + continue; + } + + // Check if we need to use administrator path + $isAdministrator = strpos($namespacePath, JPATH_ADMINISTRATOR) === 0; + $path = str_replace($isAdministrator ? JPATH_ADMINISTRATOR : JPATH_SITE, '', $namespacePath); + + // Add the site path when a component + if ($type === 'component') { + if (is_dir(JPATH_SITE . $path)) { + $extensions[$namespace . 'Site\\\\'] = 'JPATH_SITE . \'' . $path . '\''; + } + + if (is_dir(JPATH_API . $path)) { + $extensions[$namespace . 'Api\\\\'] = 'JPATH_API . \'' . $path . '\''; + } + } + + // Add the application specific segment when a component or module + $baseDir = $isAdministrator ? 'JPATH_ADMINISTRATOR . \'' : 'JPATH_SITE . \''; + $namespace .= $isAdministrator ? 'Administrator\\\\' : 'Site\\\\'; + + // Set the namespace + $extensions[$namespace] = $baseDir . $path . '\''; + } + } + + // Return the namespaces + return $extensions; + } } diff --git a/libraries/src/Access/Access.php b/libraries/src/Access/Access.php index 2452c86673710..74327114b6b31 100644 --- a/libraries/src/Access/Access.php +++ b/libraries/src/Access/Access.php @@ -1,4 +1,5 @@ allow($action, self::$identities[$userId]); - } - - /** - * Method to preload the Rules object for the given asset type. - * - * @param integer|string|array $assetTypes The type or name of the asset (e.g. 'com_content.article', 'com_menus.menu.2'). - * Also accepts the asset id. An array of asset type or a special - * 'components' string to load all component assets. - * @param boolean $reload Set to true to reload from database. - * - * @return boolean True on success. - * - * @since 1.6 - * @note This method will return void in 4.0. - */ - public static function preload($assetTypes = 'components', $reload = false) - { - // If sent an asset id, we first get the asset type for that asset id. - if (is_numeric($assetTypes)) - { - $assetTypes = self::getAssetType($assetTypes); - } - - // Check for default case: - $isDefault = \is_string($assetTypes) && \in_array($assetTypes, array('components', 'component')); - - // Preload the rules for all of the components. - if ($isDefault) - { - self::preloadComponents(); - - return true; - } - - // If we get to this point, this is a regular asset type and we'll proceed with the preloading process. - if (!\is_array($assetTypes)) - { - $assetTypes = (array) $assetTypes; - } - - foreach ($assetTypes as $assetType) - { - self::preloadPermissions($assetType, $reload); - } - - return true; - } - - /** - * Method to recursively retrieve the list of parent Asset IDs - * for a particular Asset. - * - * @param string $assetType The asset type, or the asset name, or the extension of the asset - * (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact'). - * @param integer $assetId The numeric asset id. - * - * @return array List of ancestor ids (includes original $assetId). - * - * @since 1.6 - */ - protected static function getAssetAncestors($assetType, $assetId) - { - // Get the extension name from the $assetType provided - $extensionName = self::getExtensionNameFromAsset($assetType); - - // Holds the list of ancestors for the Asset ID: - $ancestors = array(); - - // Add in our starting Asset ID: - $ancestors[] = (int) $assetId; - - // Initialize the variable we'll use in the loop: - $id = (int) $assetId; - - while ($id !== 0) - { - if (isset(self::$assetPermissionsParentIdMapping[$extensionName][$id])) - { - $id = (int) self::$assetPermissionsParentIdMapping[$extensionName][$id]->parent_id; - - if ($id !== 0) - { - $ancestors[] = $id; - } - } - else - { - // Add additional case to break out of the while loop automatically in - // the case that the ID is non-existent in our mapping variable above. - break; - } - } - - return $ancestors; - } - - /** - * Method to retrieve the Asset Rule strings for this particular - * Asset Type and stores them for later usage in getAssetRules(). - * Stores 2 arrays: one where the list has the Asset ID as the key - * and a second one where the Asset Name is the key. - * - * @param string $assetType The asset type, or the asset name, or the extension of the asset - * (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact'). - * @param boolean $reload Reload the preloaded assets. - * - * @return void - * - * @since 1.6 - */ - protected static function preloadPermissions($assetType, $reload = false) - { - // Get the extension name from the $assetType provided - $extensionName = self::getExtensionNameFromAsset($assetType); - - // If asset is a component, make sure that all the component assets are preloaded. - if ((isset(self::$preloadedAssetTypes[$extensionName]) || isset(self::$preloadedAssetTypes[$assetType])) && !$reload) - { - return; - } - - !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::preloadPermissions (' . $extensionName . ')'); - - // Get the database connection object. - $db = Factory::getDbo(); - $assetKey = $extensionName . '.%'; - - // Get a fresh query object. - $query = $db->getQuery(true) - ->select($db->quoteName(array('id', 'name', 'rules', 'parent_id'))) - ->from($db->quoteName('#__assets')) - ->where( - [ - $db->quoteName('name') . ' LIKE :asset', - $db->quoteName('name') . ' = :extension', - $db->quoteName('parent_id') . ' = 0', - ], - 'OR' - ) - ->bind(':extension', $extensionName) - ->bind(':asset', $assetKey); - - // Get the permission map for all assets in the asset extension. - $assets = $db->setQuery($query)->loadObjectList(); - - self::$assetPermissionsParentIdMapping[$extensionName] = array(); - - foreach ($assets as $asset) - { - self::$assetPermissionsParentIdMapping[$extensionName][$asset->id] = $asset; - self::$preloadedAssets[$asset->id] = $asset->name; - } - - // Mark asset type and it's extension name as preloaded. - self::$preloadedAssetTypes[$assetType] = true; - self::$preloadedAssetTypes[$extensionName] = true; - - !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::preloadPermissions (' . $extensionName . ')'); - } - - /** - * Method to preload the Rules objects for all components. - * - * Note: This will only get the base permissions for the component. - * e.g. it will get 'com_content', but not 'com_content.article.1' or - * any more specific asset type rules. - * - * @return array Array of component names that were preloaded. - * - * @since 1.6 - */ - protected static function preloadComponents() - { - // If the components already been preloaded do nothing. - if (isset(self::$preloadedAssetTypes['components'])) - { - return array(); - } - - !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::preloadComponents (all components)'); - - // Add root to asset names list. - $components = array('root.1'); - - // Add enabled components to asset names list. - foreach (ComponentHelper::getComponents() as $component) - { - if ($component->enabled) - { - $components[] = $component->option; - } - } - - // Get the database connection object. - $db = Factory::getDbo(); - - // Get the asset info for all assets in asset names list. - $query = $db->getQuery(true) - ->select($db->quoteName(array('id', 'name', 'rules', 'parent_id'))) - ->from($db->quoteName('#__assets')) - ->whereIn($db->quoteName('name'), $components, ParameterType::STRING); - - // Get the Name Permission Map List - $assets = $db->setQuery($query)->loadObjectList(); - - $rootAsset = null; - - // First add the root asset and save it to preload memory and mark it as preloaded. - foreach ($assets as &$asset) - { - if ((int) $asset->parent_id === 0) - { - $rootAsset = $asset; - self::$rootAssetId = $asset->id; - self::$preloadedAssetTypes[$asset->name] = true; - self::$preloadedAssets[$asset->id] = $asset->name; - self::$assetPermissionsParentIdMapping[$asset->name][$asset->id] = $asset; - - unset($asset); - break; - } - } - - // Now create save the components asset tree to preload memory. - foreach ($assets as $asset) - { - if (!isset(self::$assetPermissionsParentIdMapping[$asset->name])) - { - self::$assetPermissionsParentIdMapping[$asset->name] = array($rootAsset->id => $rootAsset, $asset->id => $asset); - self::$preloadedAssets[$asset->id] = $asset->name; - } - } - - // Mark all components asset type as preloaded. - self::$preloadedAssetTypes['components'] = true; - - !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::preloadComponents (all components)'); - - return $components; - } - - /** - * Method to check if a group is authorised to perform an action, optionally on an asset. - * - * @param integer $groupId The path to the group for which to check authorisation. - * @param string $action The name of the action to authorise. - * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. - * @param boolean $preload Indicates whether preloading should be used. - * - * @return boolean True if authorised. - * - * @since 1.7.0 - */ - public static function checkGroup($groupId, $action, $assetKey = null, $preload = true) - { - // Sanitize input. - $groupId = (int) $groupId; - $action = strtolower(preg_replace('#[\s\-]+#', '.', trim($action))); - - return self::getAssetRules($assetKey, true, true, $preload)->allow($action, self::getGroupPath($groupId)); - } - - /** - * Gets the parent groups that a leaf group belongs to in its branch back to the root of the tree - * (including the leaf group id). - * - * @param mixed $groupId An integer or array of integers representing the identities to check. - * - * @return mixed True if allowed, false for an explicit deny, null for an implicit deny. - * - * @since 1.7.0 - */ - protected static function getGroupPath($groupId) - { - // Load all the groups to improve performance on intensive groups checks - $groups = UserGroupsHelper::getInstance()->getAll(); - - if (!isset($groups[$groupId])) - { - return array(); - } - - return $groups[$groupId]->path; - } - - /** - * Method to return the Rules object for an asset. The returned object can optionally hold - * only the rules explicitly set for the asset or the summation of all inherited rules from - * parent assets and explicit rules. - * - * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. - * @param boolean $recursive True to return the rules object with inherited rules. - * @param boolean $recursiveParentAsset True to calculate the rule also based on inherited component/extension rules. - * @param boolean $preload Indicates whether preloading should be used. - * - * @return Rules Rules object for the asset. - * - * @since 1.7.0 - * @note The non preloading code will be removed in 4.0. All asset rules should use asset preloading. - */ - public static function getAssetRules($assetKey, $recursive = false, $recursiveParentAsset = true, $preload = true) - { - // Auto preloads the components assets and root asset (if chosen). - if ($preload) - { - self::preload('components'); - } - - // When asset key is null fallback to root asset. - $assetKey = self::cleanAssetKey($assetKey); - - // Auto preloads assets for the asset type (if chosen). - if ($preload) - { - self::preload(self::getAssetType($assetKey)); - } - - // Get the asset id and name. - $assetId = self::getAssetId($assetKey); - - // If asset rules already cached em memory return it (only in full recursive mode). - if ($recursive && $recursiveParentAsset && $assetId && isset(self::$assetRules[$assetId])) - { - return self::$assetRules[$assetId]; - } - - // Get the asset name and the extension name. - $assetName = self::getAssetName($assetKey); - $extensionName = self::getExtensionNameFromAsset($assetName); - - // If asset id does not exist fallback to extension asset, then root asset. - if (!$assetId) - { - if ($extensionName && $assetName !== $extensionName) - { - Log::add('No asset found for ' . $assetName . ', falling back to ' . $extensionName, Log::WARNING, 'assets'); - - return self::getAssetRules($extensionName, $recursive, $recursiveParentAsset, $preload); - } - - if (self::$rootAssetId !== null && $assetName !== self::$preloadedAssets[self::$rootAssetId]) - { - Log::add('No asset found for ' . $assetName . ', falling back to ' . self::$preloadedAssets[self::$rootAssetId], Log::WARNING, 'assets'); - - return self::getAssetRules(self::$preloadedAssets[self::$rootAssetId], $recursive, $recursiveParentAsset, $preload); - } - } - - // Almost all calls can take advantage of preloading. - if ($assetId && isset(self::$preloadedAssets[$assetId])) - { - !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules (id:' . $assetId . ' name:' . $assetName . ')'); - - // Collects permissions for each asset - $collected = array(); - - // If not in any recursive mode. We only want the asset rules. - if (!$recursive && !$recursiveParentAsset) - { - $collected = array(self::$assetPermissionsParentIdMapping[$extensionName][$assetId]->rules); - } - // If there is any type of recursive mode. - else - { - $ancestors = array_reverse(self::getAssetAncestors($extensionName, $assetId)); - - foreach ($ancestors as $id) - { - // There are no rules for this ancestor - if (!isset(self::$assetPermissionsParentIdMapping[$extensionName][$id])) - { - continue; - } - - // If full recursive mode, but not recursive parent mode, do not add the extension asset rules. - if ($recursive && !$recursiveParentAsset && self::$assetPermissionsParentIdMapping[$extensionName][$id]->name === $extensionName) - { - continue; - } - - // If not full recursive mode, but recursive parent mode, do not add other recursion rules. - if (!$recursive && $recursiveParentAsset && self::$assetPermissionsParentIdMapping[$extensionName][$id]->name !== $extensionName - && (int) self::$assetPermissionsParentIdMapping[$extensionName][$id]->id !== $assetId) - { - continue; - } - - // If empty asset to not add to rules. - if (self::$assetPermissionsParentIdMapping[$extensionName][$id]->rules === '{}') - { - continue; - } - - $collected[] = self::$assetPermissionsParentIdMapping[$extensionName][$id]->rules; - } - } - - /** - * Hashing the collected rules allows us to store - * only one instance of the Rules object for - * Assets that have the same exact permissions... - * it's a great way to save some memory. - */ - $hash = md5(implode(',', $collected)); - - if (!isset(self::$assetRulesIdentities[$hash])) - { - $rules = new Rules; - $rules->mergeCollection($collected); - - self::$assetRulesIdentities[$hash] = $rules; - } - - // Save asset rules to memory cache(only in full recursive mode). - if ($recursive && $recursiveParentAsset) - { - self::$assetRules[$assetId] = self::$assetRulesIdentities[$hash]; - } - - !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::getAssetRules (id:' . $assetId . ' name:' . $assetName . ')'); - - return self::$assetRulesIdentities[$hash]; - } - - // Non preloading code. Use old slower method, slower. Only used in rare cases (if any) or without preloading chosen. - Log::add('Asset ' . $assetKey . ' permissions fetch without preloading (slower method).', Log::INFO, 'assets'); - - !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules (assetKey:' . $assetKey . ')'); - - // There's no need to process it with the recursive method for the Root Asset ID. - if ((int) $assetKey === 1) - { - $recursive = false; - } - - // Get the database connection object. - $db = Factory::getDbo(); - - // Build the database query to get the rules for the asset. - $query = $db->getQuery(true) - ->select($db->quoteName($recursive ? 'b.rules' : 'a.rules', 'rules')) - ->from($db->quoteName('#__assets', 'a')); - - // If the asset identifier is numeric assume it is a primary key, else lookup by name. - if (is_numeric($assetKey)) - { - $query->where($db->quoteName('a.id') . ' = :asset', 'OR') - ->bind(':asset', $assetKey, ParameterType::INTEGER); - } - else - { - $query->where($db->quoteName('a.name') . ' = :asset', 'OR') - ->bind(':asset', $assetKey); - } - - if ($recursiveParentAsset && ($extensionName !== $assetKey || is_numeric($assetKey))) - { - $query->where($db->quoteName('a.name') . ' = :extension') - ->bind(':extension', $extensionName); - } - - // If we want the rules cascading up to the global asset node we need a self-join. - if ($recursive) - { - $query->where($db->quoteName('a.parent_id') . ' = 0') - ->join( - 'LEFT', - $db->quoteName('#__assets', 'b'), - $db->quoteName('b.lft') . ' <= ' . $db->quoteName('a.lft') . ' AND ' . $db->quoteName('b.rgt') . ' >= ' . $db->quoteName('a.rgt') - ) - ->order($db->quoteName('b.lft')); - } - - // Execute the query and load the rules from the result. - $result = $db->setQuery($query)->loadColumn(); - - // Get the root even if the asset is not found and in recursive mode - if (empty($result)) - { - $rootId = (new Asset($db))->getRootId(); - - $query->clear() - ->select($db->quoteName('rules')) - ->from($db->quoteName('#__assets')) - ->where($db->quoteName('id') . ' = :rootId') - ->bind(':rootId', $rootId, ParameterType::INTEGER); - - $result = $db->setQuery($query)->loadColumn(); - } - - // Instantiate and return the Rules object for the asset rules. - $rules = new Rules; - $rules->mergeCollection($result); - - !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules Slower (assetKey:' . $assetKey . ')'); - - return $rules; - } - - /** - * Method to clean the asset key to make sure we always have something. - * - * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. - * - * @return integer|string Asset id or asset name. - * - * @since 3.7.0 - */ - protected static function cleanAssetKey($assetKey = null) - { - // If it's a valid asset key, clean it and return it. - if ($assetKey) - { - return strtolower(preg_replace('#[\s\-]+#', '.', trim($assetKey))); - } - - // Return root asset id if already preloaded. - if (self::$rootAssetId !== null) - { - return self::$rootAssetId; - } - - // No preload. Return root asset id from Assets. - $assets = new Asset(Factory::getDbo()); - - return $assets->getRootId(); - } - - /** - * Method to get the asset id from the asset key. - * - * @param integer|string $assetKey The asset key (asset id or asset name). - * - * @return integer The asset id. - * - * @since 3.7.0 - */ - protected static function getAssetId($assetKey) - { - static $loaded = array(); - - // If the asset is already an id return it. - if (is_numeric($assetKey)) - { - return (int) $assetKey; - } - - if (!isset($loaded[$assetKey])) - { - // It's the root asset. - if (self::$rootAssetId !== null && $assetKey === self::$preloadedAssets[self::$rootAssetId]) - { - $loaded[$assetKey] = self::$rootAssetId; - } - else - { - $preloadedAssetsByName = array_flip(self::$preloadedAssets); - - // If we already have the asset name stored in preloading, example, a component, no need to fetch it from table. - if (isset($preloadedAssetsByName[$assetKey])) - { - $loaded[$assetKey] = $preloadedAssetsByName[$assetKey]; - } - // Else we have to do an extra db query to fetch it from the table fetch it from table. - else - { - $table = new Asset(Factory::getDbo()); - $table->load(array('name' => $assetKey)); - $loaded[$assetKey] = $table->id; - } - } - } - - return (int) $loaded[$assetKey]; - } - - /** - * Method to get the asset name from the asset key. - * - * @param integer|string $assetKey The asset key (asset id or asset name). - * - * @return string The asset name (ex: com_content.article.8). - * - * @since 3.7.0 - */ - protected static function getAssetName($assetKey) - { - static $loaded = array(); - - // If the asset is already a string return it. - if (!is_numeric($assetKey)) - { - return $assetKey; - } - - if (!isset($loaded[$assetKey])) - { - // It's the root asset. - if (self::$rootAssetId !== null && $assetKey === self::$rootAssetId) - { - $loaded[$assetKey] = self::$preloadedAssets[self::$rootAssetId]; - } - // If we already have the asset name stored in preloading, example, a component, no need to fetch it from table. - elseif (isset(self::$preloadedAssets[$assetKey])) - { - $loaded[$assetKey] = self::$preloadedAssets[$assetKey]; - } - // Else we have to do an extra db query to fetch it from the table fetch it from table. - else - { - $table = new Asset(Factory::getDbo()); - $table->load($assetKey); - $loaded[$assetKey] = $table->name; - } - } - - return $loaded[$assetKey]; - } - - /** - * Method to get the extension name from the asset name. - * - * @param integer|string $assetKey The asset key (asset id or asset name). - * - * @return string The extension name (ex: com_content). - * - * @since 1.6 - */ - public static function getExtensionNameFromAsset($assetKey) - { - static $loaded = array(); - - if (!isset($loaded[$assetKey])) - { - $assetName = self::getAssetName($assetKey); - $firstDot = strpos($assetName, '.'); - - if ($assetName !== 'root.1' && $firstDot !== false) - { - $assetName = substr($assetName, 0, $firstDot); - } - - $loaded[$assetKey] = $assetName; - } - - return $loaded[$assetKey]; - } - - /** - * Method to get the asset type from the asset name. - * - * For top level components this returns "components": - * 'com_content' returns 'components' - * - * For other types: - * 'com_content.article.1' returns 'com_content.article' - * 'com_content.category.1' returns 'com_content.category' - * - * @param integer|string $assetKey The asset key (asset id or asset name). - * - * @return string The asset type (ex: com_content.article). - * - * @since 1.6 - */ - public static function getAssetType($assetKey) - { - // If the asset is already a string return it. - $assetName = self::getAssetName($assetKey); - $lastDot = strrpos($assetName, '.'); - - if ($assetName !== 'root.1' && $lastDot !== false) - { - return substr($assetName, 0, $lastDot); - } - - return 'components'; - } - - /** - * Method to return the title of a user group - * - * @param integer $groupId Id of the group for which to get the title of. - * - * @return string The title of the group - * - * @since 3.5 - */ - public static function getGroupTitle($groupId) - { - // Cast as integer until method is typehinted. - $groupId = (int) $groupId; - - // Fetch the group title from the database - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $query->select($db->quoteName('title')) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('id') . ' = :groupId') - ->bind(':groupId', $groupId, ParameterType::INTEGER); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Method to return a list of user groups mapped to a user. The returned list can optionally hold - * only the groups explicitly mapped to the user or all groups both explicitly mapped and inherited - * by the user. - * - * @param integer $userId Id of the user for which to get the list of groups. - * @param boolean $recursive True to include inherited user groups. - * - * @return array List of user group ids to which the user is mapped. - * - * @since 1.7.0 - */ - public static function getGroupsByUser($userId, $recursive = true) - { - // Cast as integer until method is typehinted. - $userId = (int) $userId; - - // Creates a simple unique string for each parameter combination: - $storeId = $userId . ':' . (int) $recursive; - - if (!isset(self::$groupsByUser[$storeId])) - { - // @todo: Uncouple this from ComponentHelper and allow for a configuration setting or value injection. - $guestUsergroup = (int) ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); - - // Guest user (if only the actually assigned group is requested) - if (empty($userId) && !$recursive) - { - $result = array($guestUsergroup); - } - // Registered user and guest if all groups are requested - else - { - $db = Factory::getDbo(); - - // Build the database query to get the rules for the asset. - $query = $db->getQuery(true) - ->select($db->quoteName($recursive ? 'b.id' : 'a.id')); - - if (empty($userId)) - { - $query->from($db->quoteName('#__usergroups', 'a')) - ->where($db->quoteName('a.id') . ' = :guest') - ->bind(':guest', $guestUsergroup, ParameterType::INTEGER); - } - else - { - $query->from($db->quoteName('#__user_usergroup_map', 'map')) - ->where($db->quoteName('map.user_id') . ' = :userId') - ->join('LEFT', $db->quoteName('#__usergroups', 'a'), $db->quoteName('a.id') . ' = ' . $db->quoteName('map.group_id')) - ->bind(':userId', $userId, ParameterType::INTEGER); - } - - // If we want the rules cascading up to the global asset node we need a self-join. - if ($recursive) - { - $query->join( - 'LEFT', - $db->quoteName('#__usergroups', 'b'), - $db->quoteName('b.lft') . ' <= ' . $db->quoteName('a.lft') . ' AND ' . $db->quoteName('b.rgt') . ' >= ' . $db->quoteName('a.rgt') - ); - } - - // Execute the query and load the rules from the result. - $db->setQuery($query); - $result = $db->loadColumn(); - - // Clean up any NULL or duplicate values, just in case - $result = ArrayHelper::toInteger($result); - - if (empty($result)) - { - $result = array(1); - } - else - { - $result = array_unique($result); - } - } - - self::$groupsByUser[$storeId] = $result; - } - - return self::$groupsByUser[$storeId]; - } - - /** - * Method to return a list of user Ids contained in a Group - * - * @param integer $groupId The group Id - * @param boolean $recursive Recursively include all child groups (optional) - * - * @return array - * - * @since 1.7.0 - * @todo This method should move somewhere else - */ - public static function getUsersByGroup($groupId, $recursive = false) - { - // Cast as integer until method is typehinted. - $groupId = (int) $groupId; - - // Get a database object. - $db = Factory::getDbo(); - - $test = $recursive ? ' >= ' : ' = '; - - // First find the users contained in the group - $query = $db->getQuery(true) - ->select('DISTINCT(' . $db->quoteName('user_id') . ')') - ->from($db->quoteName('#__usergroups', 'ug1')) - ->join( - 'INNER', - $db->quoteName('#__usergroups', 'ug2'), - $db->quoteName('ug2.lft') . $test . $db->quoteName('ug1.lft') . ' AND ' . $db->quoteName('ug1.rgt') . $test . $db->quoteName('ug2.rgt') - ) - ->join('INNER', $db->quoteName('#__user_usergroup_map', 'm'), $db->quoteName('ug2.id') . ' = ' . $db->quoteName('m.group_id')) - ->where($db->quoteName('ug1.id') . ' = :groupId') - ->bind(':groupId', $groupId, ParameterType::INTEGER); - - $db->setQuery($query); - - $result = $db->loadColumn(); - - // Clean up any NULL values, just in case - $result = ArrayHelper::toInteger($result); - - return $result; - } - - /** - * Method to return a list of view levels for which the user is authorised. - * - * @param integer $userId Id of the user for which to get the list of authorised view levels. - * - * @return array List of view levels for which the user is authorised. - * - * @since 1.7.0 - */ - public static function getAuthorisedViewLevels($userId) - { - // Only load the view levels once. - if (empty(self::$viewLevels)) - { - // Get a database object. - $db = Factory::getDbo(); - - // Build the base query. - $query = $db->getQuery(true) - ->select($db->quoteName(['id', 'rules'])) - ->from($db->quoteName('#__viewlevels')); - - // Set the query for execution. - $db->setQuery($query); - - // Build the view levels array. - foreach ($db->loadAssocList() as $level) - { - self::$viewLevels[$level['id']] = (array) json_decode($level['rules']); - } - } - - // Initialise the authorised array. - $authorised = array(1); - - // Check for the recovery mode setting and return early. - $user = User::getInstance($userId); - $root_user = Factory::getApplication()->get('root_user'); - - if (($user->username && $user->username == $root_user) || (is_numeric($root_user) && $user->id > 0 && $user->id == $root_user)) - { - // Find the super user levels. - foreach (self::$viewLevels as $level => $rule) - { - foreach ($rule as $id) - { - if ($id > 0 && self::checkGroup($id, 'core.admin')) - { - $authorised[] = $level; - break; - } - } - } - - return array_values(array_unique($authorised)); - } - - // Get all groups that the user is mapped to recursively. - $groups = self::getGroupsByUser($userId); - - // Find the authorised levels. - foreach (self::$viewLevels as $level => $rule) - { - foreach ($rule as $id) - { - if (($id < 0) && (($id * -1) == $userId)) - { - $authorised[] = $level; - break; - } - // Check to see if the group is mapped to the level. - elseif (($id >= 0) && \in_array($id, $groups)) - { - $authorised[] = $level; - break; - } - } - } - - return array_values(array_unique($authorised)); - } - - /** - * Method to return a list of actions from a file for which permissions can be set. - * - * @param string $file The path to the XML file. - * @param string $xpath An optional xpath to search for the fields. - * - * @return boolean|array False if case of error or the list of actions available. - * - * @since 3.0.0 - */ - public static function getActionsFromFile($file, $xpath = "/access/section[@name='component']/") - { - if (!is_file($file) || !is_readable($file)) - { - // If unable to find the file return false. - return false; - } - else - { - // Else return the actions from the xml. - $xml = simplexml_load_file($file); - - return self::getActionsFromData($xml, $xpath); - } - } - - /** - * Method to return a list of actions from a string or from an xml for which permissions can be set. - * - * @param string|\SimpleXMLElement $data The XML string or an XML element. - * @param string $xpath An optional xpath to search for the fields. - * - * @return boolean|array False if case of error or the list of actions available. - * - * @since 3.0.0 - */ - public static function getActionsFromData($data, $xpath = "/access/section[@name='component']/") - { - // If the data to load isn't already an XML element or string return false. - if ((!($data instanceof \SimpleXMLElement)) && (!\is_string($data))) - { - return false; - } - - // Attempt to load the XML if a string. - if (\is_string($data)) - { - try - { - $data = new \SimpleXMLElement($data); - } - catch (\Exception $e) - { - return false; - } - - // Make sure the XML loaded correctly. - if (!$data) - { - return false; - } - } - - // Initialise the actions array - $actions = array(); - - // Get the elements from the xpath - $elements = $data->xpath($xpath . 'action[@name][@title]'); - - // If there some elements, analyse them - if (!empty($elements)) - { - foreach ($elements as $element) - { - // Add the action to the actions array - $action = array( - 'name' => (string) $element['name'], - 'title' => (string) $element['title'], - ); - - if (isset($element['description'])) - { - $action['description'] = (string) $element['description']; - } - - $actions[] = (object) $action; - } - } - - // Finally return the actions array - return $actions; - } + /** + * Array of view levels + * + * @var array + * @since 1.7.0 + */ + protected static $viewLevels = array(); + + /** + * Array of rules for the asset + * + * @var array + * @since 1.7.0 + */ + protected static $assetRules = array(); + + /** + * Array of identities for asset rules + * + * @var array + * @since 1.7.0 + */ + protected static $assetRulesIdentities = array(); + + /** + * Array of the permission parent ID mappings + * + * @var array + * @since 1.7.0 + */ + protected static $assetPermissionsParentIdMapping = array(); + + /** + * Array of asset types that have been preloaded + * + * @var array + * @since 1.7.0 + */ + protected static $preloadedAssetTypes = array(); + + /** + * Array of loaded user identities + * + * @var array + * @since 1.7.0 + */ + protected static $identities = array(); + + /** + * Array of user groups. + * + * @var array + * @since 1.7.0 + */ + protected static $userGroups = array(); + + /** + * Array of user group paths. + * + * @var array + * @since 1.7.0 + */ + protected static $userGroupPaths = array(); + + /** + * Array of cached groups by user. + * + * @var array + * @since 1.7.0 + */ + protected static $groupsByUser = array(); + + /** + * Array of preloaded asset names and ids (key is the asset id). + * + * @var array + * @since 3.7.0 + */ + protected static $preloadedAssets = array(); + + /** + * The root asset id. + * + * @var integer + * @since 3.7.0 + */ + protected static $rootAssetId = null; + + /** + * Method for clearing static caches. + * + * @return void + * + * @since 1.7.3 + */ + public static function clearStatics() + { + self::$viewLevels = array(); + self::$assetRules = array(); + self::$assetRulesIdentities = array(); + self::$assetPermissionsParentIdMapping = array(); + self::$preloadedAssetTypes = array(); + self::$identities = array(); + self::$userGroups = array(); + self::$userGroupPaths = array(); + self::$groupsByUser = array(); + self::$preloadedAssets = array(); + self::$rootAssetId = null; + } + + /** + * Method to check if a user is authorised to perform an action, optionally on an asset. + * + * @param integer $userId Id of the user for which to check authorisation. + * @param string $action The name of the action to authorise. + * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. + * @param boolean $preload Indicates whether preloading should be used. + * + * @return boolean|null True if allowed, false for an explicit deny, null for an implicit deny. + * + * @since 1.7.0 + */ + public static function check($userId, $action, $assetKey = null, $preload = true) + { + // Sanitise inputs. + $userId = (int) $userId; + $action = strtolower(preg_replace('#[\s\-]+#', '.', trim($action))); + + if (!isset(self::$identities[$userId])) { + // Get all groups against which the user is mapped. + self::$identities[$userId] = self::getGroupsByUser($userId); + array_unshift(self::$identities[$userId], $userId * -1); + } + + return self::getAssetRules($assetKey, true, true, $preload)->allow($action, self::$identities[$userId]); + } + + /** + * Method to preload the Rules object for the given asset type. + * + * @param integer|string|array $assetTypes The type or name of the asset (e.g. 'com_content.article', 'com_menus.menu.2'). + * Also accepts the asset id. An array of asset type or a special + * 'components' string to load all component assets. + * @param boolean $reload Set to true to reload from database. + * + * @return boolean True on success. + * + * @since 1.6 + * @note This method will return void in 4.0. + */ + public static function preload($assetTypes = 'components', $reload = false) + { + // If sent an asset id, we first get the asset type for that asset id. + if (is_numeric($assetTypes)) { + $assetTypes = self::getAssetType($assetTypes); + } + + // Check for default case: + $isDefault = \is_string($assetTypes) && \in_array($assetTypes, array('components', 'component')); + + // Preload the rules for all of the components. + if ($isDefault) { + self::preloadComponents(); + + return true; + } + + // If we get to this point, this is a regular asset type and we'll proceed with the preloading process. + if (!\is_array($assetTypes)) { + $assetTypes = (array) $assetTypes; + } + + foreach ($assetTypes as $assetType) { + self::preloadPermissions($assetType, $reload); + } + + return true; + } + + /** + * Method to recursively retrieve the list of parent Asset IDs + * for a particular Asset. + * + * @param string $assetType The asset type, or the asset name, or the extension of the asset + * (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact'). + * @param integer $assetId The numeric asset id. + * + * @return array List of ancestor ids (includes original $assetId). + * + * @since 1.6 + */ + protected static function getAssetAncestors($assetType, $assetId) + { + // Get the extension name from the $assetType provided + $extensionName = self::getExtensionNameFromAsset($assetType); + + // Holds the list of ancestors for the Asset ID: + $ancestors = array(); + + // Add in our starting Asset ID: + $ancestors[] = (int) $assetId; + + // Initialize the variable we'll use in the loop: + $id = (int) $assetId; + + while ($id !== 0) { + if (isset(self::$assetPermissionsParentIdMapping[$extensionName][$id])) { + $id = (int) self::$assetPermissionsParentIdMapping[$extensionName][$id]->parent_id; + + if ($id !== 0) { + $ancestors[] = $id; + } + } else { + // Add additional case to break out of the while loop automatically in + // the case that the ID is non-existent in our mapping variable above. + break; + } + } + + return $ancestors; + } + + /** + * Method to retrieve the Asset Rule strings for this particular + * Asset Type and stores them for later usage in getAssetRules(). + * Stores 2 arrays: one where the list has the Asset ID as the key + * and a second one where the Asset Name is the key. + * + * @param string $assetType The asset type, or the asset name, or the extension of the asset + * (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact'). + * @param boolean $reload Reload the preloaded assets. + * + * @return void + * + * @since 1.6 + */ + protected static function preloadPermissions($assetType, $reload = false) + { + // Get the extension name from the $assetType provided + $extensionName = self::getExtensionNameFromAsset($assetType); + + // If asset is a component, make sure that all the component assets are preloaded. + if ((isset(self::$preloadedAssetTypes[$extensionName]) || isset(self::$preloadedAssetTypes[$assetType])) && !$reload) { + return; + } + + !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::preloadPermissions (' . $extensionName . ')'); + + // Get the database connection object. + $db = Factory::getDbo(); + $assetKey = $extensionName . '.%'; + + // Get a fresh query object. + $query = $db->getQuery(true) + ->select($db->quoteName(array('id', 'name', 'rules', 'parent_id'))) + ->from($db->quoteName('#__assets')) + ->where( + [ + $db->quoteName('name') . ' LIKE :asset', + $db->quoteName('name') . ' = :extension', + $db->quoteName('parent_id') . ' = 0', + ], + 'OR' + ) + ->bind(':extension', $extensionName) + ->bind(':asset', $assetKey); + + // Get the permission map for all assets in the asset extension. + $assets = $db->setQuery($query)->loadObjectList(); + + self::$assetPermissionsParentIdMapping[$extensionName] = array(); + + foreach ($assets as $asset) { + self::$assetPermissionsParentIdMapping[$extensionName][$asset->id] = $asset; + self::$preloadedAssets[$asset->id] = $asset->name; + } + + // Mark asset type and it's extension name as preloaded. + self::$preloadedAssetTypes[$assetType] = true; + self::$preloadedAssetTypes[$extensionName] = true; + + !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::preloadPermissions (' . $extensionName . ')'); + } + + /** + * Method to preload the Rules objects for all components. + * + * Note: This will only get the base permissions for the component. + * e.g. it will get 'com_content', but not 'com_content.article.1' or + * any more specific asset type rules. + * + * @return array Array of component names that were preloaded. + * + * @since 1.6 + */ + protected static function preloadComponents() + { + // If the components already been preloaded do nothing. + if (isset(self::$preloadedAssetTypes['components'])) { + return array(); + } + + !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::preloadComponents (all components)'); + + // Add root to asset names list. + $components = array('root.1'); + + // Add enabled components to asset names list. + foreach (ComponentHelper::getComponents() as $component) { + if ($component->enabled) { + $components[] = $component->option; + } + } + + // Get the database connection object. + $db = Factory::getDbo(); + + // Get the asset info for all assets in asset names list. + $query = $db->getQuery(true) + ->select($db->quoteName(array('id', 'name', 'rules', 'parent_id'))) + ->from($db->quoteName('#__assets')) + ->whereIn($db->quoteName('name'), $components, ParameterType::STRING); + + // Get the Name Permission Map List + $assets = $db->setQuery($query)->loadObjectList(); + + $rootAsset = null; + + // First add the root asset and save it to preload memory and mark it as preloaded. + foreach ($assets as &$asset) { + if ((int) $asset->parent_id === 0) { + $rootAsset = $asset; + self::$rootAssetId = $asset->id; + self::$preloadedAssetTypes[$asset->name] = true; + self::$preloadedAssets[$asset->id] = $asset->name; + self::$assetPermissionsParentIdMapping[$asset->name][$asset->id] = $asset; + + unset($asset); + break; + } + } + + // Now create save the components asset tree to preload memory. + foreach ($assets as $asset) { + if (!isset(self::$assetPermissionsParentIdMapping[$asset->name])) { + self::$assetPermissionsParentIdMapping[$asset->name] = array($rootAsset->id => $rootAsset, $asset->id => $asset); + self::$preloadedAssets[$asset->id] = $asset->name; + } + } + + // Mark all components asset type as preloaded. + self::$preloadedAssetTypes['components'] = true; + + !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::preloadComponents (all components)'); + + return $components; + } + + /** + * Method to check if a group is authorised to perform an action, optionally on an asset. + * + * @param integer $groupId The path to the group for which to check authorisation. + * @param string $action The name of the action to authorise. + * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. + * @param boolean $preload Indicates whether preloading should be used. + * + * @return boolean True if authorised. + * + * @since 1.7.0 + */ + public static function checkGroup($groupId, $action, $assetKey = null, $preload = true) + { + // Sanitize input. + $groupId = (int) $groupId; + $action = strtolower(preg_replace('#[\s\-]+#', '.', trim($action))); + + return self::getAssetRules($assetKey, true, true, $preload)->allow($action, self::getGroupPath($groupId)); + } + + /** + * Gets the parent groups that a leaf group belongs to in its branch back to the root of the tree + * (including the leaf group id). + * + * @param mixed $groupId An integer or array of integers representing the identities to check. + * + * @return mixed True if allowed, false for an explicit deny, null for an implicit deny. + * + * @since 1.7.0 + */ + protected static function getGroupPath($groupId) + { + // Load all the groups to improve performance on intensive groups checks + $groups = UserGroupsHelper::getInstance()->getAll(); + + if (!isset($groups[$groupId])) { + return array(); + } + + return $groups[$groupId]->path; + } + + /** + * Method to return the Rules object for an asset. The returned object can optionally hold + * only the rules explicitly set for the asset or the summation of all inherited rules from + * parent assets and explicit rules. + * + * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. + * @param boolean $recursive True to return the rules object with inherited rules. + * @param boolean $recursiveParentAsset True to calculate the rule also based on inherited component/extension rules. + * @param boolean $preload Indicates whether preloading should be used. + * + * @return Rules Rules object for the asset. + * + * @since 1.7.0 + * @note The non preloading code will be removed in 4.0. All asset rules should use asset preloading. + */ + public static function getAssetRules($assetKey, $recursive = false, $recursiveParentAsset = true, $preload = true) + { + // Auto preloads the components assets and root asset (if chosen). + if ($preload) { + self::preload('components'); + } + + // When asset key is null fallback to root asset. + $assetKey = self::cleanAssetKey($assetKey); + + // Auto preloads assets for the asset type (if chosen). + if ($preload) { + self::preload(self::getAssetType($assetKey)); + } + + // Get the asset id and name. + $assetId = self::getAssetId($assetKey); + + // If asset rules already cached em memory return it (only in full recursive mode). + if ($recursive && $recursiveParentAsset && $assetId && isset(self::$assetRules[$assetId])) { + return self::$assetRules[$assetId]; + } + + // Get the asset name and the extension name. + $assetName = self::getAssetName($assetKey); + $extensionName = self::getExtensionNameFromAsset($assetName); + + // If asset id does not exist fallback to extension asset, then root asset. + if (!$assetId) { + if ($extensionName && $assetName !== $extensionName) { + Log::add('No asset found for ' . $assetName . ', falling back to ' . $extensionName, Log::WARNING, 'assets'); + + return self::getAssetRules($extensionName, $recursive, $recursiveParentAsset, $preload); + } + + if (self::$rootAssetId !== null && $assetName !== self::$preloadedAssets[self::$rootAssetId]) { + Log::add('No asset found for ' . $assetName . ', falling back to ' . self::$preloadedAssets[self::$rootAssetId], Log::WARNING, 'assets'); + + return self::getAssetRules(self::$preloadedAssets[self::$rootAssetId], $recursive, $recursiveParentAsset, $preload); + } + } + + // Almost all calls can take advantage of preloading. + if ($assetId && isset(self::$preloadedAssets[$assetId])) { + !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules (id:' . $assetId . ' name:' . $assetName . ')'); + + // Collects permissions for each asset + $collected = array(); + + // If not in any recursive mode. We only want the asset rules. + if (!$recursive && !$recursiveParentAsset) { + $collected = array(self::$assetPermissionsParentIdMapping[$extensionName][$assetId]->rules); + } + // If there is any type of recursive mode. + else { + $ancestors = array_reverse(self::getAssetAncestors($extensionName, $assetId)); + + foreach ($ancestors as $id) { + // There are no rules for this ancestor + if (!isset(self::$assetPermissionsParentIdMapping[$extensionName][$id])) { + continue; + } + + // If full recursive mode, but not recursive parent mode, do not add the extension asset rules. + if ($recursive && !$recursiveParentAsset && self::$assetPermissionsParentIdMapping[$extensionName][$id]->name === $extensionName) { + continue; + } + + // If not full recursive mode, but recursive parent mode, do not add other recursion rules. + if ( + !$recursive && $recursiveParentAsset && self::$assetPermissionsParentIdMapping[$extensionName][$id]->name !== $extensionName + && (int) self::$assetPermissionsParentIdMapping[$extensionName][$id]->id !== $assetId + ) { + continue; + } + + // If empty asset to not add to rules. + if (self::$assetPermissionsParentIdMapping[$extensionName][$id]->rules === '{}') { + continue; + } + + $collected[] = self::$assetPermissionsParentIdMapping[$extensionName][$id]->rules; + } + } + + /** + * Hashing the collected rules allows us to store + * only one instance of the Rules object for + * Assets that have the same exact permissions... + * it's a great way to save some memory. + */ + $hash = md5(implode(',', $collected)); + + if (!isset(self::$assetRulesIdentities[$hash])) { + $rules = new Rules(); + $rules->mergeCollection($collected); + + self::$assetRulesIdentities[$hash] = $rules; + } + + // Save asset rules to memory cache(only in full recursive mode). + if ($recursive && $recursiveParentAsset) { + self::$assetRules[$assetId] = self::$assetRulesIdentities[$hash]; + } + + !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::getAssetRules (id:' . $assetId . ' name:' . $assetName . ')'); + + return self::$assetRulesIdentities[$hash]; + } + + // Non preloading code. Use old slower method, slower. Only used in rare cases (if any) or without preloading chosen. + Log::add('Asset ' . $assetKey . ' permissions fetch without preloading (slower method).', Log::INFO, 'assets'); + + !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules (assetKey:' . $assetKey . ')'); + + // There's no need to process it with the recursive method for the Root Asset ID. + if ((int) $assetKey === 1) { + $recursive = false; + } + + // Get the database connection object. + $db = Factory::getDbo(); + + // Build the database query to get the rules for the asset. + $query = $db->getQuery(true) + ->select($db->quoteName($recursive ? 'b.rules' : 'a.rules', 'rules')) + ->from($db->quoteName('#__assets', 'a')); + + // If the asset identifier is numeric assume it is a primary key, else lookup by name. + if (is_numeric($assetKey)) { + $query->where($db->quoteName('a.id') . ' = :asset', 'OR') + ->bind(':asset', $assetKey, ParameterType::INTEGER); + } else { + $query->where($db->quoteName('a.name') . ' = :asset', 'OR') + ->bind(':asset', $assetKey); + } + + if ($recursiveParentAsset && ($extensionName !== $assetKey || is_numeric($assetKey))) { + $query->where($db->quoteName('a.name') . ' = :extension') + ->bind(':extension', $extensionName); + } + + // If we want the rules cascading up to the global asset node we need a self-join. + if ($recursive) { + $query->where($db->quoteName('a.parent_id') . ' = 0') + ->join( + 'LEFT', + $db->quoteName('#__assets', 'b'), + $db->quoteName('b.lft') . ' <= ' . $db->quoteName('a.lft') . ' AND ' . $db->quoteName('b.rgt') . ' >= ' . $db->quoteName('a.rgt') + ) + ->order($db->quoteName('b.lft')); + } + + // Execute the query and load the rules from the result. + $result = $db->setQuery($query)->loadColumn(); + + // Get the root even if the asset is not found and in recursive mode + if (empty($result)) { + $rootId = (new Asset($db))->getRootId(); + + $query->clear() + ->select($db->quoteName('rules')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('id') . ' = :rootId') + ->bind(':rootId', $rootId, ParameterType::INTEGER); + + $result = $db->setQuery($query)->loadColumn(); + } + + // Instantiate and return the Rules object for the asset rules. + $rules = new Rules(); + $rules->mergeCollection($result); + + !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules Slower (assetKey:' . $assetKey . ')'); + + return $rules; + } + + /** + * Method to clean the asset key to make sure we always have something. + * + * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. + * + * @return integer|string Asset id or asset name. + * + * @since 3.7.0 + */ + protected static function cleanAssetKey($assetKey = null) + { + // If it's a valid asset key, clean it and return it. + if ($assetKey) { + return strtolower(preg_replace('#[\s\-]+#', '.', trim($assetKey))); + } + + // Return root asset id if already preloaded. + if (self::$rootAssetId !== null) { + return self::$rootAssetId; + } + + // No preload. Return root asset id from Assets. + $assets = new Asset(Factory::getDbo()); + + return $assets->getRootId(); + } + + /** + * Method to get the asset id from the asset key. + * + * @param integer|string $assetKey The asset key (asset id or asset name). + * + * @return integer The asset id. + * + * @since 3.7.0 + */ + protected static function getAssetId($assetKey) + { + static $loaded = array(); + + // If the asset is already an id return it. + if (is_numeric($assetKey)) { + return (int) $assetKey; + } + + if (!isset($loaded[$assetKey])) { + // It's the root asset. + if (self::$rootAssetId !== null && $assetKey === self::$preloadedAssets[self::$rootAssetId]) { + $loaded[$assetKey] = self::$rootAssetId; + } else { + $preloadedAssetsByName = array_flip(self::$preloadedAssets); + + // If we already have the asset name stored in preloading, example, a component, no need to fetch it from table. + if (isset($preloadedAssetsByName[$assetKey])) { + $loaded[$assetKey] = $preloadedAssetsByName[$assetKey]; + } + // Else we have to do an extra db query to fetch it from the table fetch it from table. + else { + $table = new Asset(Factory::getDbo()); + $table->load(array('name' => $assetKey)); + $loaded[$assetKey] = $table->id; + } + } + } + + return (int) $loaded[$assetKey]; + } + + /** + * Method to get the asset name from the asset key. + * + * @param integer|string $assetKey The asset key (asset id or asset name). + * + * @return string The asset name (ex: com_content.article.8). + * + * @since 3.7.0 + */ + protected static function getAssetName($assetKey) + { + static $loaded = array(); + + // If the asset is already a string return it. + if (!is_numeric($assetKey)) { + return $assetKey; + } + + if (!isset($loaded[$assetKey])) { + // It's the root asset. + if (self::$rootAssetId !== null && $assetKey === self::$rootAssetId) { + $loaded[$assetKey] = self::$preloadedAssets[self::$rootAssetId]; + } + // If we already have the asset name stored in preloading, example, a component, no need to fetch it from table. + elseif (isset(self::$preloadedAssets[$assetKey])) { + $loaded[$assetKey] = self::$preloadedAssets[$assetKey]; + } + // Else we have to do an extra db query to fetch it from the table fetch it from table. + else { + $table = new Asset(Factory::getDbo()); + $table->load($assetKey); + $loaded[$assetKey] = $table->name; + } + } + + return $loaded[$assetKey]; + } + + /** + * Method to get the extension name from the asset name. + * + * @param integer|string $assetKey The asset key (asset id or asset name). + * + * @return string The extension name (ex: com_content). + * + * @since 1.6 + */ + public static function getExtensionNameFromAsset($assetKey) + { + static $loaded = array(); + + if (!isset($loaded[$assetKey])) { + $assetName = self::getAssetName($assetKey); + $firstDot = strpos($assetName, '.'); + + if ($assetName !== 'root.1' && $firstDot !== false) { + $assetName = substr($assetName, 0, $firstDot); + } + + $loaded[$assetKey] = $assetName; + } + + return $loaded[$assetKey]; + } + + /** + * Method to get the asset type from the asset name. + * + * For top level components this returns "components": + * 'com_content' returns 'components' + * + * For other types: + * 'com_content.article.1' returns 'com_content.article' + * 'com_content.category.1' returns 'com_content.category' + * + * @param integer|string $assetKey The asset key (asset id or asset name). + * + * @return string The asset type (ex: com_content.article). + * + * @since 1.6 + */ + public static function getAssetType($assetKey) + { + // If the asset is already a string return it. + $assetName = self::getAssetName($assetKey); + $lastDot = strrpos($assetName, '.'); + + if ($assetName !== 'root.1' && $lastDot !== false) { + return substr($assetName, 0, $lastDot); + } + + return 'components'; + } + + /** + * Method to return the title of a user group + * + * @param integer $groupId Id of the group for which to get the title of. + * + * @return string The title of the group + * + * @since 3.5 + */ + public static function getGroupTitle($groupId) + { + // Cast as integer until method is typehinted. + $groupId = (int) $groupId; + + // Fetch the group title from the database + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select($db->quoteName('title')) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('id') . ' = :groupId') + ->bind(':groupId', $groupId, ParameterType::INTEGER); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Method to return a list of user groups mapped to a user. The returned list can optionally hold + * only the groups explicitly mapped to the user or all groups both explicitly mapped and inherited + * by the user. + * + * @param integer $userId Id of the user for which to get the list of groups. + * @param boolean $recursive True to include inherited user groups. + * + * @return array List of user group ids to which the user is mapped. + * + * @since 1.7.0 + */ + public static function getGroupsByUser($userId, $recursive = true) + { + // Cast as integer until method is typehinted. + $userId = (int) $userId; + + // Creates a simple unique string for each parameter combination: + $storeId = $userId . ':' . (int) $recursive; + + if (!isset(self::$groupsByUser[$storeId])) { + // @todo: Uncouple this from ComponentHelper and allow for a configuration setting or value injection. + $guestUsergroup = (int) ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); + + // Guest user (if only the actually assigned group is requested) + if (empty($userId) && !$recursive) { + $result = array($guestUsergroup); + } + // Registered user and guest if all groups are requested + else { + $db = Factory::getDbo(); + + // Build the database query to get the rules for the asset. + $query = $db->getQuery(true) + ->select($db->quoteName($recursive ? 'b.id' : 'a.id')); + + if (empty($userId)) { + $query->from($db->quoteName('#__usergroups', 'a')) + ->where($db->quoteName('a.id') . ' = :guest') + ->bind(':guest', $guestUsergroup, ParameterType::INTEGER); + } else { + $query->from($db->quoteName('#__user_usergroup_map', 'map')) + ->where($db->quoteName('map.user_id') . ' = :userId') + ->join('LEFT', $db->quoteName('#__usergroups', 'a'), $db->quoteName('a.id') . ' = ' . $db->quoteName('map.group_id')) + ->bind(':userId', $userId, ParameterType::INTEGER); + } + + // If we want the rules cascading up to the global asset node we need a self-join. + if ($recursive) { + $query->join( + 'LEFT', + $db->quoteName('#__usergroups', 'b'), + $db->quoteName('b.lft') . ' <= ' . $db->quoteName('a.lft') . ' AND ' . $db->quoteName('b.rgt') . ' >= ' . $db->quoteName('a.rgt') + ); + } + + // Execute the query and load the rules from the result. + $db->setQuery($query); + $result = $db->loadColumn(); + + // Clean up any NULL or duplicate values, just in case + $result = ArrayHelper::toInteger($result); + + if (empty($result)) { + $result = array(1); + } else { + $result = array_unique($result); + } + } + + self::$groupsByUser[$storeId] = $result; + } + + return self::$groupsByUser[$storeId]; + } + + /** + * Method to return a list of user Ids contained in a Group + * + * @param integer $groupId The group Id + * @param boolean $recursive Recursively include all child groups (optional) + * + * @return array + * + * @since 1.7.0 + * @todo This method should move somewhere else + */ + public static function getUsersByGroup($groupId, $recursive = false) + { + // Cast as integer until method is typehinted. + $groupId = (int) $groupId; + + // Get a database object. + $db = Factory::getDbo(); + + $test = $recursive ? ' >= ' : ' = '; + + // First find the users contained in the group + $query = $db->getQuery(true) + ->select('DISTINCT(' . $db->quoteName('user_id') . ')') + ->from($db->quoteName('#__usergroups', 'ug1')) + ->join( + 'INNER', + $db->quoteName('#__usergroups', 'ug2'), + $db->quoteName('ug2.lft') . $test . $db->quoteName('ug1.lft') . ' AND ' . $db->quoteName('ug1.rgt') . $test . $db->quoteName('ug2.rgt') + ) + ->join('INNER', $db->quoteName('#__user_usergroup_map', 'm'), $db->quoteName('ug2.id') . ' = ' . $db->quoteName('m.group_id')) + ->where($db->quoteName('ug1.id') . ' = :groupId') + ->bind(':groupId', $groupId, ParameterType::INTEGER); + + $db->setQuery($query); + + $result = $db->loadColumn(); + + // Clean up any NULL values, just in case + $result = ArrayHelper::toInteger($result); + + return $result; + } + + /** + * Method to return a list of view levels for which the user is authorised. + * + * @param integer $userId Id of the user for which to get the list of authorised view levels. + * + * @return array List of view levels for which the user is authorised. + * + * @since 1.7.0 + */ + public static function getAuthorisedViewLevels($userId) + { + // Only load the view levels once. + if (empty(self::$viewLevels)) { + // Get a database object. + $db = Factory::getDbo(); + + // Build the base query. + $query = $db->getQuery(true) + ->select($db->quoteName(['id', 'rules'])) + ->from($db->quoteName('#__viewlevels')); + + // Set the query for execution. + $db->setQuery($query); + + // Build the view levels array. + foreach ($db->loadAssocList() as $level) { + self::$viewLevels[$level['id']] = (array) json_decode($level['rules']); + } + } + + // Initialise the authorised array. + $authorised = array(1); + + // Check for the recovery mode setting and return early. + $user = User::getInstance($userId); + $root_user = Factory::getApplication()->get('root_user'); + + if (($user->username && $user->username == $root_user) || (is_numeric($root_user) && $user->id > 0 && $user->id == $root_user)) { + // Find the super user levels. + foreach (self::$viewLevels as $level => $rule) { + foreach ($rule as $id) { + if ($id > 0 && self::checkGroup($id, 'core.admin')) { + $authorised[] = $level; + break; + } + } + } + + return array_values(array_unique($authorised)); + } + + // Get all groups that the user is mapped to recursively. + $groups = self::getGroupsByUser($userId); + + // Find the authorised levels. + foreach (self::$viewLevels as $level => $rule) { + foreach ($rule as $id) { + if (($id < 0) && (($id * -1) == $userId)) { + $authorised[] = $level; + break; + } + // Check to see if the group is mapped to the level. + elseif (($id >= 0) && \in_array($id, $groups)) { + $authorised[] = $level; + break; + } + } + } + + return array_values(array_unique($authorised)); + } + + /** + * Method to return a list of actions from a file for which permissions can be set. + * + * @param string $file The path to the XML file. + * @param string $xpath An optional xpath to search for the fields. + * + * @return boolean|array False if case of error or the list of actions available. + * + * @since 3.0.0 + */ + public static function getActionsFromFile($file, $xpath = "/access/section[@name='component']/") + { + if (!is_file($file) || !is_readable($file)) { + // If unable to find the file return false. + return false; + } else { + // Else return the actions from the xml. + $xml = simplexml_load_file($file); + + return self::getActionsFromData($xml, $xpath); + } + } + + /** + * Method to return a list of actions from a string or from an xml for which permissions can be set. + * + * @param string|\SimpleXMLElement $data The XML string or an XML element. + * @param string $xpath An optional xpath to search for the fields. + * + * @return boolean|array False if case of error or the list of actions available. + * + * @since 3.0.0 + */ + public static function getActionsFromData($data, $xpath = "/access/section[@name='component']/") + { + // If the data to load isn't already an XML element or string return false. + if ((!($data instanceof \SimpleXMLElement)) && (!\is_string($data))) { + return false; + } + + // Attempt to load the XML if a string. + if (\is_string($data)) { + try { + $data = new \SimpleXMLElement($data); + } catch (\Exception $e) { + return false; + } + + // Make sure the XML loaded correctly. + if (!$data) { + return false; + } + } + + // Initialise the actions array + $actions = array(); + + // Get the elements from the xpath + $elements = $data->xpath($xpath . 'action[@name][@title]'); + + // If there some elements, analyse them + if (!empty($elements)) { + foreach ($elements as $element) { + // Add the action to the actions array + $action = array( + 'name' => (string) $element['name'], + 'title' => (string) $element['title'], + ); + + if (isset($element['description'])) { + $action['description'] = (string) $element['description']; + } + + $actions[] = (object) $action; + } + } + + // Finally return the actions array + return $actions; + } } diff --git a/libraries/src/Access/Exception/AuthenticationFailed.php b/libraries/src/Access/Exception/AuthenticationFailed.php index 5703a129c5ae1..1b3df0aeeb175 100644 --- a/libraries/src/Access/Exception/AuthenticationFailed.php +++ b/libraries/src/Access/Exception/AuthenticationFailed.php @@ -1,4 +1,5 @@ true, 3 => true, 4 => false) - * or an equivalent JSON encoded string. - * - * @param mixed $identities A JSON format string (probably from the database) or a named array. - * - * @since 1.7.0 - */ - public function __construct($identities) - { - // Convert string input to an array. - if (\is_string($identities)) - { - $identities = json_decode($identities, true); - } - - $this->mergeIdentities($identities); - } - - /** - * Get the data for the action. - * - * @return array A named array - * - * @since 1.7.0 - */ - public function getData() - { - return $this->data; - } - - /** - * Merges the identities - * - * @param mixed $identities An integer or array of integers representing the identities to check. - * - * @return void - * - * @since 1.7.0 - */ - public function mergeIdentities($identities) - { - if ($identities instanceof Rule) - { - $identities = $identities->getData(); - } - - if (\is_array($identities)) - { - foreach ($identities as $identity => $allow) - { - $this->mergeIdentity($identity, $allow); - } - } - } - - /** - * Merges the values for an identity. - * - * @param integer $identity The identity. - * @param boolean $allow The value for the identity (true == allow, false == deny). - * - * @return void - * - * @since 1.7.0 - */ - public function mergeIdentity($identity, $allow) - { - $identity = (int) $identity; - $allow = (int) ((boolean) $allow); - - // Check that the identity exists. - if (isset($this->data[$identity])) - { - // Explicit deny always wins a merge. - if ($this->data[$identity] !== 0) - { - $this->data[$identity] = $allow; - } - } - else - { - $this->data[$identity] = $allow; - } - } - - /** - * Checks that this action can be performed by an identity. - * - * The identity is an integer where +ve represents a user group, - * and -ve represents a user. - * - * @param mixed $identities An integer or array of integers representing the identities to check. - * - * @return mixed True if allowed, false for an explicit deny, null for an implicit deny. - * - * @since 1.7.0 - */ - public function allow($identities) - { - // Implicit deny by default. - $result = null; - - // Check that the inputs are valid. - if (!empty($identities)) - { - if (!\is_array($identities)) - { - $identities = array($identities); - } - - foreach ($identities as $identity) - { - // Technically the identity just needs to be unique. - $identity = (int) $identity; - - // Check if the identity is known. - if (isset($this->data[$identity])) - { - $result = (boolean) $this->data[$identity]; - - // An explicit deny wins. - if ($result === false) - { - break; - } - } - } - } - - return $result; - } - - /** - * Convert this object into a JSON encoded string. - * - * @return string JSON encoded string - * - * @since 1.7.0 - */ - public function __toString() - { - return json_encode($this->data); - } + /** + * A named array + * + * @var array + * @since 1.7.0 + */ + protected $data = array(); + + /** + * Constructor. + * + * The input array must be in the form: array(-42 => true, 3 => true, 4 => false) + * or an equivalent JSON encoded string. + * + * @param mixed $identities A JSON format string (probably from the database) or a named array. + * + * @since 1.7.0 + */ + public function __construct($identities) + { + // Convert string input to an array. + if (\is_string($identities)) { + $identities = json_decode($identities, true); + } + + $this->mergeIdentities($identities); + } + + /** + * Get the data for the action. + * + * @return array A named array + * + * @since 1.7.0 + */ + public function getData() + { + return $this->data; + } + + /** + * Merges the identities + * + * @param mixed $identities An integer or array of integers representing the identities to check. + * + * @return void + * + * @since 1.7.0 + */ + public function mergeIdentities($identities) + { + if ($identities instanceof Rule) { + $identities = $identities->getData(); + } + + if (\is_array($identities)) { + foreach ($identities as $identity => $allow) { + $this->mergeIdentity($identity, $allow); + } + } + } + + /** + * Merges the values for an identity. + * + * @param integer $identity The identity. + * @param boolean $allow The value for the identity (true == allow, false == deny). + * + * @return void + * + * @since 1.7.0 + */ + public function mergeIdentity($identity, $allow) + { + $identity = (int) $identity; + $allow = (int) ((bool) $allow); + + // Check that the identity exists. + if (isset($this->data[$identity])) { + // Explicit deny always wins a merge. + if ($this->data[$identity] !== 0) { + $this->data[$identity] = $allow; + } + } else { + $this->data[$identity] = $allow; + } + } + + /** + * Checks that this action can be performed by an identity. + * + * The identity is an integer where +ve represents a user group, + * and -ve represents a user. + * + * @param mixed $identities An integer or array of integers representing the identities to check. + * + * @return mixed True if allowed, false for an explicit deny, null for an implicit deny. + * + * @since 1.7.0 + */ + public function allow($identities) + { + // Implicit deny by default. + $result = null; + + // Check that the inputs are valid. + if (!empty($identities)) { + if (!\is_array($identities)) { + $identities = array($identities); + } + + foreach ($identities as $identity) { + // Technically the identity just needs to be unique. + $identity = (int) $identity; + + // Check if the identity is known. + if (isset($this->data[$identity])) { + $result = (bool) $this->data[$identity]; + + // An explicit deny wins. + if ($result === false) { + break; + } + } + } + } + + return $result; + } + + /** + * Convert this object into a JSON encoded string. + * + * @return string JSON encoded string + * + * @since 1.7.0 + */ + public function __toString() + { + return json_encode($this->data); + } } diff --git a/libraries/src/Access/Rules.php b/libraries/src/Access/Rules.php index 3f605ca6e1531..4a91a4cd2f775 100644 --- a/libraries/src/Access/Rules.php +++ b/libraries/src/Access/Rules.php @@ -1,4 +1,5 @@ array(-42 => true, 3 => true, 4 => false)) - * or an equivalent JSON encoded string, or an object where properties are arrays. - * - * @param mixed $input A JSON format string (probably from the database) or a nested array. - * - * @since 1.7.0 - */ - public function __construct($input = '') - { - // Convert in input to an array. - if (\is_string($input)) - { - $input = json_decode($input, true); - } - elseif (\is_object($input)) - { - $input = (array) $input; - } - - if (\is_array($input)) - { - // Top level keys represent the actions. - foreach ($input as $action => $identities) - { - $this->mergeAction($action, $identities); - } - } - } - - /** - * Get the data for the action. - * - * @return array A named array of Rule objects. - * - * @since 1.7.0 - */ - public function getData() - { - return $this->data; - } - - /** - * Method to merge a collection of Rules. - * - * @param mixed $input Rule or array of Rules - * - * @return void - * - * @since 1.7.0 - */ - public function mergeCollection($input) - { - // Check if the input is an array. - if (\is_array($input)) - { - foreach ($input as $actions) - { - $this->merge($actions); - } - } - } - - /** - * Method to merge actions with this object. - * - * @param mixed $actions Rule object, an array of actions or a JSON string array of actions. - * - * @return void - * - * @since 1.7.0 - */ - public function merge($actions) - { - if (\is_string($actions)) - { - $actions = json_decode($actions, true); - } - - if (\is_array($actions)) - { - foreach ($actions as $action => $identities) - { - $this->mergeAction($action, $identities); - } - } - elseif ($actions instanceof Rules) - { - $data = $actions->getData(); - - foreach ($data as $name => $identities) - { - $this->mergeAction($name, $identities); - } - } - } - - /** - * Merges an array of identities for an action. - * - * @param string $action The name of the action. - * @param array $identities An array of identities - * - * @return void - * - * @since 1.7.0 - */ - public function mergeAction($action, $identities) - { - if (isset($this->data[$action])) - { - // If exists, merge the action. - $this->data[$action]->mergeIdentities($identities); - } - else - { - // If new, add the action. - $this->data[$action] = new Rule($identities); - } - } - - /** - * Checks that an action can be performed by an identity. - * - * The identity is an integer where +ve represents a user group, - * and -ve represents a user. - * - * @param string $action The name of the action. - * @param mixed $identity An integer representing the identity, or an array of identities - * - * @return mixed Object or null if there is no information about the action. - * - * @since 1.7.0 - */ - public function allow($action, $identity) - { - // Check we have information about this action. - if (isset($this->data[$action])) - { - return $this->data[$action]->allow($identity); - } - } - - /** - * Get the allowed actions for an identity. - * - * @param mixed $identity An integer representing the identity or an array of identities - * - * @return CMSObject Allowed actions for the identity or identities - * - * @since 1.7.0 - */ - public function getAllowed($identity) - { - // Sweep for the allowed actions. - $allowed = new CMSObject; - - foreach ($this->data as $name => &$action) - { - if ($action->allow($identity)) - { - $allowed->set($name, true); - } - } - - return $allowed; - } - - /** - * Magic method to convert the object to JSON string representation. - * - * @return string JSON representation of the actions array - * - * @since 1.7.0 - */ - public function __toString() - { - $temp = array(); - - foreach ($this->data as $name => $rule) - { - if ($data = $rule->getData()) - { - $temp[$name] = $data; - } - } - - return json_encode($temp, JSON_FORCE_OBJECT); - } + /** + * A named array. + * + * @var array + * @since 1.7.0 + */ + protected $data = array(); + + /** + * Constructor. + * + * The input array must be in the form: array('action' => array(-42 => true, 3 => true, 4 => false)) + * or an equivalent JSON encoded string, or an object where properties are arrays. + * + * @param mixed $input A JSON format string (probably from the database) or a nested array. + * + * @since 1.7.0 + */ + public function __construct($input = '') + { + // Convert in input to an array. + if (\is_string($input)) { + $input = json_decode($input, true); + } elseif (\is_object($input)) { + $input = (array) $input; + } + + if (\is_array($input)) { + // Top level keys represent the actions. + foreach ($input as $action => $identities) { + $this->mergeAction($action, $identities); + } + } + } + + /** + * Get the data for the action. + * + * @return array A named array of Rule objects. + * + * @since 1.7.0 + */ + public function getData() + { + return $this->data; + } + + /** + * Method to merge a collection of Rules. + * + * @param mixed $input Rule or array of Rules + * + * @return void + * + * @since 1.7.0 + */ + public function mergeCollection($input) + { + // Check if the input is an array. + if (\is_array($input)) { + foreach ($input as $actions) { + $this->merge($actions); + } + } + } + + /** + * Method to merge actions with this object. + * + * @param mixed $actions Rule object, an array of actions or a JSON string array of actions. + * + * @return void + * + * @since 1.7.0 + */ + public function merge($actions) + { + if (\is_string($actions)) { + $actions = json_decode($actions, true); + } + + if (\is_array($actions)) { + foreach ($actions as $action => $identities) { + $this->mergeAction($action, $identities); + } + } elseif ($actions instanceof Rules) { + $data = $actions->getData(); + + foreach ($data as $name => $identities) { + $this->mergeAction($name, $identities); + } + } + } + + /** + * Merges an array of identities for an action. + * + * @param string $action The name of the action. + * @param array $identities An array of identities + * + * @return void + * + * @since 1.7.0 + */ + public function mergeAction($action, $identities) + { + if (isset($this->data[$action])) { + // If exists, merge the action. + $this->data[$action]->mergeIdentities($identities); + } else { + // If new, add the action. + $this->data[$action] = new Rule($identities); + } + } + + /** + * Checks that an action can be performed by an identity. + * + * The identity is an integer where +ve represents a user group, + * and -ve represents a user. + * + * @param string $action The name of the action. + * @param mixed $identity An integer representing the identity, or an array of identities + * + * @return mixed Object or null if there is no information about the action. + * + * @since 1.7.0 + */ + public function allow($action, $identity) + { + // Check we have information about this action. + if (isset($this->data[$action])) { + return $this->data[$action]->allow($identity); + } + } + + /** + * Get the allowed actions for an identity. + * + * @param mixed $identity An integer representing the identity or an array of identities + * + * @return CMSObject Allowed actions for the identity or identities + * + * @since 1.7.0 + */ + public function getAllowed($identity) + { + // Sweep for the allowed actions. + $allowed = new CMSObject(); + + foreach ($this->data as $name => &$action) { + if ($action->allow($identity)) { + $allowed->set($name, true); + } + } + + return $allowed; + } + + /** + * Magic method to convert the object to JSON string representation. + * + * @return string JSON representation of the actions array + * + * @since 1.7.0 + */ + public function __toString() + { + $temp = array(); + + foreach ($this->data as $name => $rule) { + if ($data = $rule->getData()) { + $temp[$name] = $data; + } + } + + return json_encode($temp, JSON_FORCE_OBJECT); + } } diff --git a/libraries/src/Adapter/Adapter.php b/libraries/src/Adapter/Adapter.php index e2161c130f401..3e4b2d022c15d 100644 --- a/libraries/src/Adapter/Adapter.php +++ b/libraries/src/Adapter/Adapter.php @@ -1,4 +1,5 @@ _basepath = $basepath; - $this->_classprefix = $classprefix ?: 'J'; - $this->_adapterfolder = $adapterfolder ?: 'adapters'; - - $this->_db = Factory::getDbo(); - - // Ensure BC, when removed in 5, then the db must be set with setDatabase explicitly - if ($this instanceof DatabaseAwareInterface) - { - $this->setDatabase($this->_db); - } - } - - /** - * Get the database connector object - * - * @return \Joomla\Database\DatabaseDriver Database connector object - * - * @since 1.6 - */ - public function getDbo() - { - return $this->_db; - } - - /** - * Return an adapter. - * - * @param string $name Name of adapter to return - * @param array $options Adapter options - * - * @return static|boolean Adapter of type 'name' or false - * - * @since 1.6 - */ - public function getAdapter($name, $options = array()) - { - if (array_key_exists($name, $this->_adapters)) - { - return $this->_adapters[$name]; - } - - if ($this->setAdapter($name, $options)) - { - return $this->_adapters[$name]; - } - - return false; - } - - /** - * Set an adapter by name - * - * @param string $name Adapter name - * @param object $adapter Adapter object - * @param array $options Adapter options - * - * @return boolean True if successful - * - * @since 1.6 - */ - public function setAdapter($name, &$adapter = null, $options = array()) - { - if (is_object($adapter)) - { - $this->_adapters[$name] = &$adapter; - - return true; - } - - $class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name); - - if (class_exists($class)) - { - $this->_adapters[$name] = new $class($this, $this->_db, $options); - - return true; - } - - $class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name) . 'Adapter'; - - if (class_exists($class)) - { - $this->_adapters[$name] = new $class($this, $this->_db, $options); - - return true; - } - - $fullpath = $this->_basepath . '/' . $this->_adapterfolder . '/' . strtolower($name) . '.php'; - - if (!is_file($fullpath)) - { - return false; - } - - // Try to load the adapter object - $class = $this->_classprefix . ucfirst($name); - - \JLoader::register($class, $fullpath); - - if (!class_exists($class)) - { - return false; - } - - $this->_adapters[$name] = new $class($this, $this->_db, $options); - - return true; - } - - /** - * Loads all adapters. - * - * @param array $options Adapter options - * - * @return void - * - * @since 1.6 - */ - public function loadAllAdapters($options = array()) - { - $files = new \DirectoryIterator($this->_basepath . '/' . $this->_adapterfolder); - - /** @type $file \DirectoryIterator */ - foreach ($files as $file) - { - $fileName = $file->getFilename(); - - // Only load for php files. - if (!$file->isFile() || $file->getExtension() != 'php') - { - continue; - } - - // Try to load the adapter object - require_once $this->_basepath . '/' . $this->_adapterfolder . '/' . $fileName; - - // Derive the class name from the filename. - $name = str_ireplace('.php', '', ucfirst(trim($fileName))); - $class = $this->_classprefix . ucfirst($name); - - if (!class_exists($class)) - { - // Skip to next one - continue; - } - - $adapter = new $class($this, $this->_db, $options); - $this->_adapters[$name] = clone $adapter; - } - } + /** + * Associative array of adapters + * + * @var static[] + * @since 1.6 + */ + protected $_adapters = array(); + + /** + * Adapter Folder + * + * @var string + * @since 1.6 + */ + protected $_adapterfolder = 'adapters'; + + /** + * Adapter Class Prefix + * + * @var string + * @since 1.6 + */ + protected $_classprefix = 'J'; + + /** + * Base Path for the adapter instance + * + * @var string + * @since 1.6 + */ + protected $_basepath = null; + + /** + * Database Connector Object + * + * @var \Joomla\Database\DatabaseDriver + * @since 1.6 + */ + protected $_db; + + /** + * Constructor + * + * @param string $basepath Base Path of the adapters + * @param string $classprefix Class prefix of adapters + * @param string $adapterfolder Name of folder to append to base path + * + * @since 1.6 + */ + public function __construct($basepath, $classprefix = null, $adapterfolder = null) + { + $this->_basepath = $basepath; + $this->_classprefix = $classprefix ?: 'J'; + $this->_adapterfolder = $adapterfolder ?: 'adapters'; + + $this->_db = Factory::getDbo(); + + // Ensure BC, when removed in 5, then the db must be set with setDatabase explicitly + if ($this instanceof DatabaseAwareInterface) { + $this->setDatabase($this->_db); + } + } + + /** + * Get the database connector object + * + * @return \Joomla\Database\DatabaseDriver Database connector object + * + * @since 1.6 + */ + public function getDbo() + { + return $this->_db; + } + + /** + * Return an adapter. + * + * @param string $name Name of adapter to return + * @param array $options Adapter options + * + * @return static|boolean Adapter of type 'name' or false + * + * @since 1.6 + */ + public function getAdapter($name, $options = array()) + { + if (array_key_exists($name, $this->_adapters)) { + return $this->_adapters[$name]; + } + + if ($this->setAdapter($name, $options)) { + return $this->_adapters[$name]; + } + + return false; + } + + /** + * Set an adapter by name + * + * @param string $name Adapter name + * @param object $adapter Adapter object + * @param array $options Adapter options + * + * @return boolean True if successful + * + * @since 1.6 + */ + public function setAdapter($name, &$adapter = null, $options = array()) + { + if (is_object($adapter)) { + $this->_adapters[$name] = &$adapter; + + return true; + } + + $class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name); + + if (class_exists($class)) { + $this->_adapters[$name] = new $class($this, $this->_db, $options); + + return true; + } + + $class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name) . 'Adapter'; + + if (class_exists($class)) { + $this->_adapters[$name] = new $class($this, $this->_db, $options); + + return true; + } + + $fullpath = $this->_basepath . '/' . $this->_adapterfolder . '/' . strtolower($name) . '.php'; + + if (!is_file($fullpath)) { + return false; + } + + // Try to load the adapter object + $class = $this->_classprefix . ucfirst($name); + + \JLoader::register($class, $fullpath); + + if (!class_exists($class)) { + return false; + } + + $this->_adapters[$name] = new $class($this, $this->_db, $options); + + return true; + } + + /** + * Loads all adapters. + * + * @param array $options Adapter options + * + * @return void + * + * @since 1.6 + */ + public function loadAllAdapters($options = array()) + { + $files = new \DirectoryIterator($this->_basepath . '/' . $this->_adapterfolder); + + /** @type $file \DirectoryIterator */ + foreach ($files as $file) { + $fileName = $file->getFilename(); + + // Only load for php files. + if (!$file->isFile() || $file->getExtension() != 'php') { + continue; + } + + // Try to load the adapter object + require_once $this->_basepath . '/' . $this->_adapterfolder . '/' . $fileName; + + // Derive the class name from the filename. + $name = str_ireplace('.php', '', ucfirst(trim($fileName))); + $class = $this->_classprefix . ucfirst($name); + + if (!class_exists($class)) { + // Skip to next one + continue; + } + + $adapter = new $class($this, $this->_db, $options); + $this->_adapters[$name] = clone $adapter; + } + } } diff --git a/libraries/src/Adapter/AdapterInstance.php b/libraries/src/Adapter/AdapterInstance.php index 297ba584a09b5..56470cd3f3760 100644 --- a/libraries/src/Adapter/AdapterInstance.php +++ b/libraries/src/Adapter/AdapterInstance.php @@ -1,4 +1,5 @@ setProperties($options); + /** + * Constructor + * + * @param Adapter $parent Parent object + * @param DatabaseDriver $db Database object + * @param array $options Configuration Options + * + * @since 1.6 + */ + public function __construct(Adapter $parent, DatabaseDriver $db, array $options = array()) + { + // Set the properties from the options array that is passed in + $this->setProperties($options); - // Set the parent and db in case $options for some reason overrides it. - $this->parent = $parent; + // Set the parent and db in case $options for some reason overrides it. + $this->parent = $parent; - // Pull in the global dbo in case something happened to it. - $this->db = $db ?: Factory::getDbo(); - } + // Pull in the global dbo in case something happened to it. + $this->db = $db ?: Factory::getDbo(); + } - /** - * Retrieves the parent object - * - * @return Adapter - * - * @since 1.6 - */ - public function getParent() - { - return $this->parent; - } + /** + * Retrieves the parent object + * + * @return Adapter + * + * @since 1.6 + */ + public function getParent() + { + return $this->parent; + } } diff --git a/libraries/src/Application/AdministratorApplication.php b/libraries/src/Application/AdministratorApplication.php index bd6287f27703e..85811ecf8c9b8 100644 --- a/libraries/src/Application/AdministratorApplication.php +++ b/libraries/src/Application/AdministratorApplication.php @@ -1,4 +1,5 @@ name = 'administrator'; - - // Register the client ID - $this->clientId = 1; - - // Execute the parent constructor - parent::__construct($input, $config, $client, $container); - - // Set the root in the URI based on the application name - Uri::root(null, rtrim(\dirname(Uri::base(true)), '/\\')); - } - - /** - * Dispatch the application - * - * @param string $component The component which is being rendered. - * - * @return void - * - * @since 3.2 - */ - public function dispatch($component = null) - { - if ($component === null) - { - $component = $this->findOption(); - } - - // Load the document to the API - $this->loadDocument(); - - // Set up the params - $document = Factory::getDocument(); - - // Register the document object with Factory - Factory::$document = $document; - - switch ($document->getType()) - { - case 'html': - // Get the template - $template = $this->getTemplate(true); - $clientId = $this->getClientId(); - - // Store the template and its params to the config - $this->set('theme', $template->template); - $this->set('themeParams', $template->params); - - // Add Asset registry files - $wr = $document->getWebAssetManager()->getRegistry(); - - if ($component) - { - $wr->addExtensionRegistryFile($component); - } - - if (!empty($template->parent)) - { - $wr->addTemplateRegistryFile($template->parent, $clientId); - } - - $wr->addTemplateRegistryFile($template->template, $clientId); - - break; - - default: - break; - } - - $document->setTitle($this->get('sitename') . ' - ' . Text::_('JADMINISTRATION')); - $document->setDescription($this->get('MetaDesc')); - $document->setGenerator('Joomla! - Open Source Content Management'); - - $contents = ComponentHelper::renderComponent($component); - $document->setBuffer($contents, 'component'); - - // Trigger the onAfterDispatch event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterDispatch'); - } - - /** - * Method to run the Web application routines. - * - * @return void - * - * @since 3.2 - */ - protected function doExecute() - { - // Get the language from the (login) form or user state - $login_lang = ($this->input->get('option') === 'com_login') ? $this->input->get('lang') : ''; - $options = array('language' => $login_lang ?: $this->getUserState('application.lang')); - - // Initialise the application - $this->initialiseApp($options); - - // Mark afterInitialise in the profiler. - JDEBUG ? $this->profiler->mark('afterInitialise') : null; - - // Route the application - $this->route(); - - // Mark afterRoute in the profiler. - JDEBUG ? $this->profiler->mark('afterRoute') : null; - - /* - * Check if the user is required to reset their password - * - * Before $this->route(); "option" and "view" can't be safely read using: - * $this->input->getCmd('option'); or $this->input->getCmd('view'); - * ex: due of the sef urls - */ - $this->checkUserRequireReset('com_users', 'user', 'edit', 'com_users/user.edit,com_users/user.save,com_users/user.apply,com_login/logout'); - - // Dispatch the application - $this->dispatch(); - - // Mark afterDispatch in the profiler. - JDEBUG ? $this->profiler->mark('afterDispatch') : null; - } - - /** - * Return a reference to the Router object. - * - * @param string $name The name of the application. - * @param array $options An optional associative array of configuration settings. - * - * @return Router - * - * @since 3.2 - * @deprecated 5.0 Inject the router or load it from the dependency injection container - */ - public static function getRouter($name = 'administrator', array $options = array()) - { - return parent::getRouter($name, $options); - } - - /** - * Gets the name of the current template. - * - * @param boolean $params True to return the template parameters - * - * @return string The name of the template. - * - * @since 3.2 - * @throws \InvalidArgumentException - */ - public function getTemplate($params = false) - { - if (\is_object($this->template)) - { - if ($params) - { - return $this->template; - } - - return $this->template->template; - } - - $adminStyle = $this->getIdentity() ? (int) $this->getIdentity()->getParam('admin_style') : 0; - $template = $this->bootComponent('templates')->getMVCFactory() - ->createModel('Style', 'Administrator')->getAdminTemplate($adminStyle); - - $template->template = InputFilter::getInstance()->clean($template->template, 'cmd'); - $template->params = new Registry($template->params); - - // Fallback template - if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php') - && !is_file(JPATH_THEMES . '/' . $template->parent . '/index.php')) - { - $this->getLogger()->error(Text::_('JERROR_ALERTNOTEMPLATE'), ['category' => 'system']); - $template->params = new Registry; - $template->template = 'atum'; - - // Check, the data were found and if template really exists - if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) - { - throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $template->template)); - } - } - - // Cache the result - $this->template = $template; - - // Pass the parent template to the state - $this->set('themeInherits', $template->parent); - - if ($params) - { - return $template; - } - - return $template->template; - } - - /** - * Initialise the application. - * - * @param array $options An optional associative array of configuration settings. - * - * @return void - * - * @since 3.2 - */ - protected function initialiseApp($options = array()) - { - $user = Factory::getUser(); - - // If the user is a guest we populate it with the guest user group. - if ($user->guest) - { - $guestUsergroup = ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); - $user->groups = array($guestUsergroup); - } - - // If a language was specified it has priority, otherwise use user or default language settings - if (empty($options['language'])) - { - $lang = $user->getParam('admin_language'); - - // Make sure that the user's language exists - if ($lang && LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - else - { - $params = ComponentHelper::getParams('com_languages'); - $options['language'] = $params->get('administrator', $this->get('language', 'en-GB')); - } - } - - // One last check to make sure we have something - if (!LanguageHelper::exists($options['language'])) - { - $lang = $this->get('language', 'en-GB'); - - if (LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - else - { - // As a last ditch fail to english - $options['language'] = 'en-GB'; - } - } - - // Finish initialisation - parent::initialiseApp($options); - } - - /** - * Login authentication function - * - * @param array $credentials Array('username' => string, 'password' => string) - * @param array $options Array('remember' => boolean) - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function login($credentials, $options = array()) - { - // The minimum group - $options['group'] = 'Public Backend'; - - // Make sure users are not auto-registered - $options['autoregister'] = false; - - // Set the application login entry point - if (!\array_key_exists('entry_url', $options)) - { - $options['entry_url'] = Uri::base() . 'index.php?option=com_users&task=login'; - } - - // Set the access control action to check. - $options['action'] = 'core.login.admin'; - - $result = parent::login($credentials, $options); - - if (!($result instanceof \Exception)) - { - $lang = $this->input->getCmd('lang', ''); - $lang = preg_replace('/[^A-Z-]/i', '', $lang); - - if ($lang) - { - $this->setUserState('application.lang', $lang); - } - - $this->bootComponent('messages')->getMVCFactory() - ->createModel('Messages', 'Administrator')->purge($this->getIdentity() ? $this->getIdentity()->id : 0); - } - - return $result; - } - - /** - * Purge the jos_messages table of old messages - * - * @return void - * - * @since 3.2 - * - * @deprecated 5.0 Purge the messages through the model - */ - public static function purgeMessages() - { - Factory::getApplication()->bootComponent('messages')->getMVCFactory() - ->createModel('Messages', 'Administrator')->purge(Factory::getUser()->id); - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 3.2 - */ - protected function render() - { - // Get the \JInput object - $input = $this->input; - - $component = $input->getCmd('option', 'com_login'); - $file = $input->getCmd('tmpl', 'index'); - - if ($component === 'com_login') - { - $file = 'login'; - } - - $this->set('themeFile', $file . '.php'); - - // Safety check for when configuration.php root_user is in use. - $rootUser = $this->get('root_user'); - - if (property_exists('\JConfig', 'root_user')) - { - if (Factory::getUser()->get('username') === $rootUser || Factory::getUser()->id === (string) $rootUser) - { - $this->enqueueMessage( - Text::sprintf( - 'JWARNING_REMOVE_ROOT_USER', - 'index.php?option=com_config&task=application.removeroot&' . Session::getFormToken() . '=1' - ), - 'warning' - ); - } - // Show this message to superusers too - elseif (Factory::getUser()->authorise('core.admin')) - { - $this->enqueueMessage( - Text::sprintf( - 'JWARNING_REMOVE_ROOT_USER_ADMIN', - $rootUser, - 'index.php?option=com_config&task=application.removeroot&' . Session::getFormToken() . '=1' - ), - 'warning' - ); - } - } - - parent::render(); - } - - /** - * Route the application. - * - * Routing is the process of examining the request environment to determine which - * component should receive the request. The component optional parameters - * are then set in the request object to be processed when the application is being - * dispatched. - * - * @return void - * - * @since 3.2 - */ - protected function route() - { - $uri = Uri::getInstance(); - - if ($this->get('force_ssl') >= 1 && strtolower($uri->getScheme()) !== 'https') - { - // Forward to https - $uri->setScheme('https'); - $this->redirect((string) $uri, 301); - } - - $this->isHandlingMultiFactorAuthentication(); - - // Trigger the onAfterRoute event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterRoute'); - } - - /** - * Return the application option string [main component]. - * - * @return string The component to access. - * - * @since 4.0.0 - */ - public function findOption(): string - { - /** @var self $app */ - $app = Factory::getApplication(); - $option = strtolower($app->input->get('option', '')); - $user = $app->getIdentity(); - - /** - * Special handling for guest users and authenticated users without the Backend Login privilege. - * - * If the component they are trying to access is in the $this->allowedUnprivilegedOptions array we allow the - * request to go through. Otherwise we force com_login to be loaded, letting the user (re)try authenticating - * with a user account that has the Backend Login privilege. - */ - if ($user->get('guest') || !$user->authorise('core.login.admin')) - { - $option = in_array($option, $this->allowedUnprivilegedOptions) ? $option : 'com_login'; - } - - /** - * If no component is defined in the request we will try to load com_cpanel, the administrator Control Panel - * component. This allows the /administrator URL to display something meaningful after logging in instead of an - * error. - */ - if (empty($option)) - { - $option = 'com_cpanel'; - } - - /** - * Force the option to the input object. This is necessary because we might have force-changed the component in - * the two if-blocks above. - */ - $app->input->set('option', $option); - - return $option; - } + use MultiFactorAuthenticationHandler; + + /** + * List of allowed components for guests and users which do not have the core.login.admin privilege. + * + * By default we allow two core components: + * + * - com_login Absolutely necessary to let users log into the backend of the site. Do NOT remove! + * - com_ajax Handle AJAX requests or other administrative callbacks without logging in. Required for + * passwordless authentication using WebAuthn. + * + * @var array + */ + protected $allowedUnprivilegedOptions = [ + 'com_login', + 'com_ajax', + ]; + + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's input + * object. If the argument is a JInput object that object will become the + * application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's config + * object. If the argument is a Registry object that object will become the + * application's config object, otherwise a default config object is created. + * @param WebClient $client An optional argument to provide dependency injection for the application's + * client object. If the argument is a WebClient object that object will become the + * application's client object, otherwise a default client object is created. + * @param Container $container Dependency injection container. + * + * @since 3.2 + */ + public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, Container $container = null) + { + // Register the application name + $this->name = 'administrator'; + + // Register the client ID + $this->clientId = 1; + + // Execute the parent constructor + parent::__construct($input, $config, $client, $container); + + // Set the root in the URI based on the application name + Uri::root(null, rtrim(\dirname(Uri::base(true)), '/\\')); + } + + /** + * Dispatch the application + * + * @param string $component The component which is being rendered. + * + * @return void + * + * @since 3.2 + */ + public function dispatch($component = null) + { + if ($component === null) { + $component = $this->findOption(); + } + + // Load the document to the API + $this->loadDocument(); + + // Set up the params + $document = Factory::getDocument(); + + // Register the document object with Factory + Factory::$document = $document; + + switch ($document->getType()) { + case 'html': + // Get the template + $template = $this->getTemplate(true); + $clientId = $this->getClientId(); + + // Store the template and its params to the config + $this->set('theme', $template->template); + $this->set('themeParams', $template->params); + + // Add Asset registry files + $wr = $document->getWebAssetManager()->getRegistry(); + + if ($component) { + $wr->addExtensionRegistryFile($component); + } + + if (!empty($template->parent)) { + $wr->addTemplateRegistryFile($template->parent, $clientId); + } + + $wr->addTemplateRegistryFile($template->template, $clientId); + + break; + + default: + break; + } + + $document->setTitle($this->get('sitename') . ' - ' . Text::_('JADMINISTRATION')); + $document->setDescription($this->get('MetaDesc')); + $document->setGenerator('Joomla! - Open Source Content Management'); + + $contents = ComponentHelper::renderComponent($component); + $document->setBuffer($contents, 'component'); + + // Trigger the onAfterDispatch event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterDispatch'); + } + + /** + * Method to run the Web application routines. + * + * @return void + * + * @since 3.2 + */ + protected function doExecute() + { + // Get the language from the (login) form or user state + $login_lang = ($this->input->get('option') === 'com_login') ? $this->input->get('lang') : ''; + $options = array('language' => $login_lang ?: $this->getUserState('application.lang')); + + // Initialise the application + $this->initialiseApp($options); + + // Mark afterInitialise in the profiler. + JDEBUG ? $this->profiler->mark('afterInitialise') : null; + + // Route the application + $this->route(); + + // Mark afterRoute in the profiler. + JDEBUG ? $this->profiler->mark('afterRoute') : null; + + /* + * Check if the user is required to reset their password + * + * Before $this->route(); "option" and "view" can't be safely read using: + * $this->input->getCmd('option'); or $this->input->getCmd('view'); + * ex: due of the sef urls + */ + $this->checkUserRequireReset('com_users', 'user', 'edit', 'com_users/user.edit,com_users/user.save,com_users/user.apply,com_login/logout'); + + // Dispatch the application + $this->dispatch(); + + // Mark afterDispatch in the profiler. + JDEBUG ? $this->profiler->mark('afterDispatch') : null; + } + + /** + * Return a reference to the Router object. + * + * @param string $name The name of the application. + * @param array $options An optional associative array of configuration settings. + * + * @return Router + * + * @since 3.2 + * @deprecated 5.0 Inject the router or load it from the dependency injection container + */ + public static function getRouter($name = 'administrator', array $options = array()) + { + return parent::getRouter($name, $options); + } + + /** + * Gets the name of the current template. + * + * @param boolean $params True to return the template parameters + * + * @return string The name of the template. + * + * @since 3.2 + * @throws \InvalidArgumentException + */ + public function getTemplate($params = false) + { + if (\is_object($this->template)) { + if ($params) { + return $this->template; + } + + return $this->template->template; + } + + $adminStyle = $this->getIdentity() ? (int) $this->getIdentity()->getParam('admin_style') : 0; + $template = $this->bootComponent('templates')->getMVCFactory() + ->createModel('Style', 'Administrator')->getAdminTemplate($adminStyle); + + $template->template = InputFilter::getInstance()->clean($template->template, 'cmd'); + $template->params = new Registry($template->params); + + // Fallback template + if ( + !is_file(JPATH_THEMES . '/' . $template->template . '/index.php') + && !is_file(JPATH_THEMES . '/' . $template->parent . '/index.php') + ) { + $this->getLogger()->error(Text::_('JERROR_ALERTNOTEMPLATE'), ['category' => 'system']); + $template->params = new Registry(); + $template->template = 'atum'; + + // Check, the data were found and if template really exists + if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) { + throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $template->template)); + } + } + + // Cache the result + $this->template = $template; + + // Pass the parent template to the state + $this->set('themeInherits', $template->parent); + + if ($params) { + return $template; + } + + return $template->template; + } + + /** + * Initialise the application. + * + * @param array $options An optional associative array of configuration settings. + * + * @return void + * + * @since 3.2 + */ + protected function initialiseApp($options = array()) + { + $user = Factory::getUser(); + + // If the user is a guest we populate it with the guest user group. + if ($user->guest) { + $guestUsergroup = ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); + $user->groups = array($guestUsergroup); + } + + // If a language was specified it has priority, otherwise use user or default language settings + if (empty($options['language'])) { + $lang = $user->getParam('admin_language'); + + // Make sure that the user's language exists + if ($lang && LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } else { + $params = ComponentHelper::getParams('com_languages'); + $options['language'] = $params->get('administrator', $this->get('language', 'en-GB')); + } + } + + // One last check to make sure we have something + if (!LanguageHelper::exists($options['language'])) { + $lang = $this->get('language', 'en-GB'); + + if (LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } else { + // As a last ditch fail to english + $options['language'] = 'en-GB'; + } + } + + // Finish initialisation + parent::initialiseApp($options); + } + + /** + * Login authentication function + * + * @param array $credentials Array('username' => string, 'password' => string) + * @param array $options Array('remember' => boolean) + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function login($credentials, $options = array()) + { + // The minimum group + $options['group'] = 'Public Backend'; + + // Make sure users are not auto-registered + $options['autoregister'] = false; + + // Set the application login entry point + if (!\array_key_exists('entry_url', $options)) { + $options['entry_url'] = Uri::base() . 'index.php?option=com_users&task=login'; + } + + // Set the access control action to check. + $options['action'] = 'core.login.admin'; + + $result = parent::login($credentials, $options); + + if (!($result instanceof \Exception)) { + $lang = $this->input->getCmd('lang', ''); + $lang = preg_replace('/[^A-Z-]/i', '', $lang); + + if ($lang) { + $this->setUserState('application.lang', $lang); + } + + $this->bootComponent('messages')->getMVCFactory() + ->createModel('Messages', 'Administrator')->purge($this->getIdentity() ? $this->getIdentity()->id : 0); + } + + return $result; + } + + /** + * Purge the jos_messages table of old messages + * + * @return void + * + * @since 3.2 + * + * @deprecated 5.0 Purge the messages through the model + */ + public static function purgeMessages() + { + Factory::getApplication()->bootComponent('messages')->getMVCFactory() + ->createModel('Messages', 'Administrator')->purge(Factory::getUser()->id); + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 3.2 + */ + protected function render() + { + // Get the \JInput object + $input = $this->input; + + $component = $input->getCmd('option', 'com_login'); + $file = $input->getCmd('tmpl', 'index'); + + if ($component === 'com_login') { + $file = 'login'; + } + + $this->set('themeFile', $file . '.php'); + + // Safety check for when configuration.php root_user is in use. + $rootUser = $this->get('root_user'); + + if (property_exists('\JConfig', 'root_user')) { + if (Factory::getUser()->get('username') === $rootUser || Factory::getUser()->id === (string) $rootUser) { + $this->enqueueMessage( + Text::sprintf( + 'JWARNING_REMOVE_ROOT_USER', + 'index.php?option=com_config&task=application.removeroot&' . Session::getFormToken() . '=1' + ), + 'warning' + ); + } + // Show this message to superusers too + elseif (Factory::getUser()->authorise('core.admin')) { + $this->enqueueMessage( + Text::sprintf( + 'JWARNING_REMOVE_ROOT_USER_ADMIN', + $rootUser, + 'index.php?option=com_config&task=application.removeroot&' . Session::getFormToken() . '=1' + ), + 'warning' + ); + } + } + + parent::render(); + } + + /** + * Route the application. + * + * Routing is the process of examining the request environment to determine which + * component should receive the request. The component optional parameters + * are then set in the request object to be processed when the application is being + * dispatched. + * + * @return void + * + * @since 3.2 + */ + protected function route() + { + $uri = Uri::getInstance(); + + if ($this->get('force_ssl') >= 1 && strtolower($uri->getScheme()) !== 'https') { + // Forward to https + $uri->setScheme('https'); + $this->redirect((string) $uri, 301); + } + + $this->isHandlingMultiFactorAuthentication(); + + // Trigger the onAfterRoute event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterRoute'); + } + + /** + * Return the application option string [main component]. + * + * @return string The component to access. + * + * @since 4.0.0 + */ + public function findOption(): string + { + /** @var self $app */ + $app = Factory::getApplication(); + $option = strtolower($app->input->get('option', '')); + $user = $app->getIdentity(); + + /** + * Special handling for guest users and authenticated users without the Backend Login privilege. + * + * If the component they are trying to access is in the $this->allowedUnprivilegedOptions array we allow the + * request to go through. Otherwise we force com_login to be loaded, letting the user (re)try authenticating + * with a user account that has the Backend Login privilege. + */ + if ($user->get('guest') || !$user->authorise('core.login.admin')) { + $option = in_array($option, $this->allowedUnprivilegedOptions) ? $option : 'com_login'; + } + + /** + * If no component is defined in the request we will try to load com_cpanel, the administrator Control Panel + * component. This allows the /administrator URL to display something meaningful after logging in instead of an + * error. + */ + if (empty($option)) { + $option = 'com_cpanel'; + } + + /** + * Force the option to the input object. This is necessary because we might have force-changed the component in + * the two if-blocks above. + */ + $app->input->set('option', $option); + + return $option; + } } diff --git a/libraries/src/Application/ApiApplication.php b/libraries/src/Application/ApiApplication.php index 50947d831f3e4..50c1c06d7ddd0 100644 --- a/libraries/src/Application/ApiApplication.php +++ b/libraries/src/Application/ApiApplication.php @@ -1,4 +1,5 @@ name = 'api'; - - // Register the client ID - $this->clientId = 3; - - // Execute the parent constructor - parent::__construct($input, $config, $client, $container); - - $this->addFormatMap('application/json', 'json'); - $this->addFormatMap('application/vnd.api+json', 'jsonapi'); - - // Set the root in the URI based on the application name - Uri::root(null, str_ireplace('/' . $this->getName(), '', Uri::base(true))); - } - - - /** - * Method to run the application routines. - * - * Most likely you will want to instantiate a controller and execute it, or perform some sort of task directly. - * - * @return void - * - * @since 4.0.0 - */ - protected function doExecute() - { - // Initialise the application - $this->initialiseApp(); - - // Mark afterInitialise in the profiler. - JDEBUG ? $this->profiler->mark('afterInitialise') : null; - - // Route the application - $this->route(); - - // Mark afterApiRoute in the profiler. - JDEBUG ? $this->profiler->mark('afterApiRoute') : null; - - // Dispatch the application - $this->dispatch(); - - // Mark afterDispatch in the profiler. - JDEBUG ? $this->profiler->mark('afterDispatch') : null; - } - - /** - * Adds a mapping from a content type to the format stored. Note the format type cannot be overwritten. - * - * @param string $contentHeader The content header - * @param string $format The content type format - * - * @return void - * - * @since 4.0.0 - */ - public function addFormatMap($contentHeader, $format) - { - if (!\array_key_exists($contentHeader, $this->formatMapper)) - { - $this->formatMapper[$contentHeader] = $format; - } - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 4.0.0 - * - * @note Rendering should be overridden to get rid of the theme files. - */ - protected function render() - { - // Render the document - $this->setBody($this->document->render($this->allowCache())); - } - - /** - * Method to send the application response to the client. All headers will be sent prior to the main application output data. - * - * @param array $options An optional argument to enable CORS. (Temporary) - * - * @return void - * - * @since 4.0.0 - */ - protected function respond($options = array()) - { - // Set the Joomla! API signature - $this->setHeader('X-Powered-By', 'JoomlaAPI/1.0', true); - - $forceCORS = (int) $this->get('cors'); - - if ($forceCORS) - { - /** - * Enable CORS (Cross-origin resource sharing) - * Obtain allowed CORS origin from Global Settings. - * Set to * (=all) if not set. - */ - $allowedOrigin = $this->get('cors_allow_origin', '*'); - $this->setHeader('Access-Control-Allow-Origin', $allowedOrigin, true); - $this->setHeader('Access-Control-Allow-Headers', 'Authorization'); - - if ($this->input->server->getString('HTTP_ORIGIN', null) !== null) - { - $this->setHeader('Access-Control-Allow-Origin', $this->input->server->getString('HTTP_ORIGIN'), true); - $this->setHeader('Access-Control-Allow-Credentials', 'true', true); - } - } - - // Parent function can be overridden later on for debugging. - parent::respond(); - } - - /** - * Gets the name of the current template. - * - * @param boolean $params True to return the template parameters - * - * @return string|\stdClass - * - * @since 4.0.0 - */ - public function getTemplate($params = false) - { - // The API application should not need to use a template - if ($params) - { - $template = new \stdClass; - $template->template = 'system'; - $template->params = new Registry; - $template->inheritable = 0; - $template->parent = ''; - - return $template; - } - - return 'system'; - } - - /** - * Route the application. - * - * Routing is the process of examining the request environment to determine which - * component should receive the request. The component optional parameters - * are then set in the request object to be processed when the application is being - * dispatched. - * - * @return void - * - * @since 4.0.0 - */ - protected function route() - { - $router = $this->getContainer()->get(ApiRouter::class); - - // Trigger the onBeforeApiRoute event. - PluginHelper::importPlugin('webservices'); - $this->triggerEvent('onBeforeApiRoute', array(&$router, $this)); - $caught404 = false; - $method = $this->input->getMethod(); - - try - { - $this->handlePreflight($method, $router); - - $route = $router->parseApiRoute($method); - } - catch (RouteNotFoundException $e) - { - $caught404 = true; - } - - /** - * Now we have an API perform content negotiation to ensure we have a valid header. Assume if the route doesn't - * tell us otherwise it uses the plain JSON API - */ - $priorities = array('application/vnd.api+json'); - - if (!$caught404 && \array_key_exists('format', $route['vars'])) - { - $priorities = $route['vars']['format']; - } - - $negotiator = new Negotiator; - - try - { - $mediaType = $negotiator->getBest($this->input->server->getString('HTTP_ACCEPT'), $priorities); - } - catch (InvalidArgument $e) - { - $mediaType = null; - } - - // If we can't find a match bail with a 406 - Not Acceptable - if ($mediaType === null) - { - throw new Exception\NotAcceptable('Could not match accept header', 406); - } - - /** @var $mediaType Accept */ - $format = $mediaType->getValue(); - - if (\array_key_exists($mediaType->getValue(), $this->formatMapper)) - { - $format = $this->formatMapper[$mediaType->getValue()]; - } - - $this->input->set('format', $format); - - if ($caught404) - { - throw $e; - } - - $this->input->set('option', $route['vars']['component']); - $this->input->set('controller', $route['controller']); - $this->input->set('task', $route['task']); - - foreach ($route['vars'] as $key => $value) - { - if ($key !== 'component') - { - if ($this->input->getMethod() === 'POST') - { - $this->input->post->set($key, $value); - } - else - { - $this->input->set($key, $value); - } - } - } - - $this->triggerEvent('onAfterApiRoute', array($this)); - - if (!isset($route['vars']['public']) || $route['vars']['public'] === false) - { - if (!$this->login(array('username' => ''), array('silent' => true, 'action' => 'core.login.api'))) - { - throw new AuthenticationFailed; - } - } - } - - /** - * Handles preflight requests. - * - * @param String $method The REST verb - * - * @param ApiRouter $router The API Routing object - * - * @return void - * - * @since 4.0.0 - */ - protected function handlePreflight($method, $router) - { - /** - * If not an OPTIONS request or CORS is not enabled, - * there's nothing useful to do here. - */ - if ($method !== 'OPTIONS' || !(int) $this->get('cors')) - { - return; - } - - // Extract routes matching current route from all known routes. - $matchingRoutes = $router->getMatchingRoutes(); - - // Extract exposed methods from matching routes. - $matchingRoutesMethods = array_unique( - array_reduce($matchingRoutes, - function ($carry, $route) { - return array_merge($carry, $route->getMethods()); - }, - [] - ) - ); - - /** - * Obtain allowed CORS origin from Global Settings. - * Set to * (=all) if not set. - */ - $allowedOrigin = $this->get('cors_allow_origin', '*'); - - /** - * Obtain allowed CORS headers from Global Settings. - * Set to sensible default if not set. - */ - $allowedHeaders = $this->get('cors_allow_headers', 'Content-Type,X-Joomla-Token'); - - /** - * Obtain allowed CORS methods from Global Settings. - * Set to methods exposed by current route if not set. - */ - $allowedMethods = $this->get('cors_allow_methods', implode(',', $matchingRoutesMethods)); - - // No use to go through the regular route handling hassle, - // so let's simply output the headers and exit. - $this->setHeader('status', '204'); - $this->setHeader('Access-Control-Allow-Origin', $allowedOrigin); - $this->setHeader('Access-Control-Allow-Headers', $allowedHeaders); - $this->setHeader('Access-Control-Allow-Methods', $allowedMethods); - $this->sendHeaders(); - - $this->close(); - } - - /** - * Returns the application Router object. - * - * @return ApiRouter - * - * @since 4.0.0 - * @deprecated 5.0 Inject the router or load it from the dependency injection container - */ - public function getApiRouter() - { - return $this->getContainer()->get(ApiRouter::class); - } - - /** - * Dispatch the application - * - * @param string $component The component which is being rendered. - * - * @return void - * - * @since 4.0.0 - */ - public function dispatch($component = null) - { - // Get the component if not set. - if (!$component) - { - $component = $this->input->get('option', null); - } - - // Load the document to the API - $this->loadDocument(); - - // Set up the params - $document = Factory::getDocument(); - - // Register the document object with Factory - Factory::$document = $document; - - $contents = ComponentHelper::renderComponent($component); - $document->setBuffer($contents, 'component'); - - // Trigger the onAfterDispatch event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterDispatch'); - } + /** + * Maps extension types to their + * + * @var array + * @since 4.0.0 + */ + protected $formatMapper = array(); + + /** + * The authentication plugin type + * + * @var string + * @since 4.0.0 + */ + protected $authenticationPluginType = 'api-authentication'; + + /** + * Class constructor. + * + * @param JInputJson $input An optional argument to provide dependency injection for the application's input + * object. If the argument is a JInput object that object will become the + * application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's config + * object. If the argument is a Registry object that object will become the + * application's config object, otherwise a default config object is created. + * @param WebClient $client An optional argument to provide dependency injection for the application's client + * object. If the argument is a WebClient object that object will become the + * application's client object, otherwise a default client object is created. + * @param Container $container Dependency injection container. + * + * @since 4.0.0 + */ + public function __construct(JInputJson $input = null, Registry $config = null, WebClient $client = null, Container $container = null) + { + // Register the application name + $this->name = 'api'; + + // Register the client ID + $this->clientId = 3; + + // Execute the parent constructor + parent::__construct($input, $config, $client, $container); + + $this->addFormatMap('application/json', 'json'); + $this->addFormatMap('application/vnd.api+json', 'jsonapi'); + + // Set the root in the URI based on the application name + Uri::root(null, str_ireplace('/' . $this->getName(), '', Uri::base(true))); + } + + + /** + * Method to run the application routines. + * + * Most likely you will want to instantiate a controller and execute it, or perform some sort of task directly. + * + * @return void + * + * @since 4.0.0 + */ + protected function doExecute() + { + // Initialise the application + $this->initialiseApp(); + + // Mark afterInitialise in the profiler. + JDEBUG ? $this->profiler->mark('afterInitialise') : null; + + // Route the application + $this->route(); + + // Mark afterApiRoute in the profiler. + JDEBUG ? $this->profiler->mark('afterApiRoute') : null; + + // Dispatch the application + $this->dispatch(); + + // Mark afterDispatch in the profiler. + JDEBUG ? $this->profiler->mark('afterDispatch') : null; + } + + /** + * Adds a mapping from a content type to the format stored. Note the format type cannot be overwritten. + * + * @param string $contentHeader The content header + * @param string $format The content type format + * + * @return void + * + * @since 4.0.0 + */ + public function addFormatMap($contentHeader, $format) + { + if (!\array_key_exists($contentHeader, $this->formatMapper)) { + $this->formatMapper[$contentHeader] = $format; + } + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 4.0.0 + * + * @note Rendering should be overridden to get rid of the theme files. + */ + protected function render() + { + // Render the document + $this->setBody($this->document->render($this->allowCache())); + } + + /** + * Method to send the application response to the client. All headers will be sent prior to the main application output data. + * + * @param array $options An optional argument to enable CORS. (Temporary) + * + * @return void + * + * @since 4.0.0 + */ + protected function respond($options = array()) + { + // Set the Joomla! API signature + $this->setHeader('X-Powered-By', 'JoomlaAPI/1.0', true); + + $forceCORS = (int) $this->get('cors'); + + if ($forceCORS) { + /** + * Enable CORS (Cross-origin resource sharing) + * Obtain allowed CORS origin from Global Settings. + * Set to * (=all) if not set. + */ + $allowedOrigin = $this->get('cors_allow_origin', '*'); + $this->setHeader('Access-Control-Allow-Origin', $allowedOrigin, true); + $this->setHeader('Access-Control-Allow-Headers', 'Authorization'); + + if ($this->input->server->getString('HTTP_ORIGIN', null) !== null) { + $this->setHeader('Access-Control-Allow-Origin', $this->input->server->getString('HTTP_ORIGIN'), true); + $this->setHeader('Access-Control-Allow-Credentials', 'true', true); + } + } + + // Parent function can be overridden later on for debugging. + parent::respond(); + } + + /** + * Gets the name of the current template. + * + * @param boolean $params True to return the template parameters + * + * @return string|\stdClass + * + * @since 4.0.0 + */ + public function getTemplate($params = false) + { + // The API application should not need to use a template + if ($params) { + $template = new \stdClass(); + $template->template = 'system'; + $template->params = new Registry(); + $template->inheritable = 0; + $template->parent = ''; + + return $template; + } + + return 'system'; + } + + /** + * Route the application. + * + * Routing is the process of examining the request environment to determine which + * component should receive the request. The component optional parameters + * are then set in the request object to be processed when the application is being + * dispatched. + * + * @return void + * + * @since 4.0.0 + */ + protected function route() + { + $router = $this->getContainer()->get(ApiRouter::class); + + // Trigger the onBeforeApiRoute event. + PluginHelper::importPlugin('webservices'); + $this->triggerEvent('onBeforeApiRoute', array(&$router, $this)); + $caught404 = false; + $method = $this->input->getMethod(); + + try { + $this->handlePreflight($method, $router); + + $route = $router->parseApiRoute($method); + } catch (RouteNotFoundException $e) { + $caught404 = true; + } + + /** + * Now we have an API perform content negotiation to ensure we have a valid header. Assume if the route doesn't + * tell us otherwise it uses the plain JSON API + */ + $priorities = array('application/vnd.api+json'); + + if (!$caught404 && \array_key_exists('format', $route['vars'])) { + $priorities = $route['vars']['format']; + } + + $negotiator = new Negotiator(); + + try { + $mediaType = $negotiator->getBest($this->input->server->getString('HTTP_ACCEPT'), $priorities); + } catch (InvalidArgument $e) { + $mediaType = null; + } + + // If we can't find a match bail with a 406 - Not Acceptable + if ($mediaType === null) { + throw new Exception\NotAcceptable('Could not match accept header', 406); + } + + /** @var $mediaType Accept */ + $format = $mediaType->getValue(); + + if (\array_key_exists($mediaType->getValue(), $this->formatMapper)) { + $format = $this->formatMapper[$mediaType->getValue()]; + } + + $this->input->set('format', $format); + + if ($caught404) { + throw $e; + } + + $this->input->set('option', $route['vars']['component']); + $this->input->set('controller', $route['controller']); + $this->input->set('task', $route['task']); + + foreach ($route['vars'] as $key => $value) { + if ($key !== 'component') { + if ($this->input->getMethod() === 'POST') { + $this->input->post->set($key, $value); + } else { + $this->input->set($key, $value); + } + } + } + + $this->triggerEvent('onAfterApiRoute', array($this)); + + if (!isset($route['vars']['public']) || $route['vars']['public'] === false) { + if (!$this->login(array('username' => ''), array('silent' => true, 'action' => 'core.login.api'))) { + throw new AuthenticationFailed(); + } + } + } + + /** + * Handles preflight requests. + * + * @param String $method The REST verb + * + * @param ApiRouter $router The API Routing object + * + * @return void + * + * @since 4.0.0 + */ + protected function handlePreflight($method, $router) + { + /** + * If not an OPTIONS request or CORS is not enabled, + * there's nothing useful to do here. + */ + if ($method !== 'OPTIONS' || !(int) $this->get('cors')) { + return; + } + + // Extract routes matching current route from all known routes. + $matchingRoutes = $router->getMatchingRoutes(); + + // Extract exposed methods from matching routes. + $matchingRoutesMethods = array_unique( + array_reduce( + $matchingRoutes, + function ($carry, $route) { + return array_merge($carry, $route->getMethods()); + }, + [] + ) + ); + + /** + * Obtain allowed CORS origin from Global Settings. + * Set to * (=all) if not set. + */ + $allowedOrigin = $this->get('cors_allow_origin', '*'); + + /** + * Obtain allowed CORS headers from Global Settings. + * Set to sensible default if not set. + */ + $allowedHeaders = $this->get('cors_allow_headers', 'Content-Type,X-Joomla-Token'); + + /** + * Obtain allowed CORS methods from Global Settings. + * Set to methods exposed by current route if not set. + */ + $allowedMethods = $this->get('cors_allow_methods', implode(',', $matchingRoutesMethods)); + + // No use to go through the regular route handling hassle, + // so let's simply output the headers and exit. + $this->setHeader('status', '204'); + $this->setHeader('Access-Control-Allow-Origin', $allowedOrigin); + $this->setHeader('Access-Control-Allow-Headers', $allowedHeaders); + $this->setHeader('Access-Control-Allow-Methods', $allowedMethods); + $this->sendHeaders(); + + $this->close(); + } + + /** + * Returns the application Router object. + * + * @return ApiRouter + * + * @since 4.0.0 + * @deprecated 5.0 Inject the router or load it from the dependency injection container + */ + public function getApiRouter() + { + return $this->getContainer()->get(ApiRouter::class); + } + + /** + * Dispatch the application + * + * @param string $component The component which is being rendered. + * + * @return void + * + * @since 4.0.0 + */ + public function dispatch($component = null) + { + // Get the component if not set. + if (!$component) { + $component = $this->input->get('option', null); + } + + // Load the document to the API + $this->loadDocument(); + + // Set up the params + $document = Factory::getDocument(); + + // Register the document object with Factory + Factory::$document = $document; + + $contents = ComponentHelper::renderComponent($component); + $document->setBuffer($contents, 'component'); + + // Trigger the onAfterDispatch event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterDispatch'); + } } diff --git a/libraries/src/Application/ApplicationHelper.php b/libraries/src/Application/ApplicationHelper.php index 454e5978659d7..cd96ac9fb18d6 100644 --- a/libraries/src/Application/ApplicationHelper.php +++ b/libraries/src/Application/ApplicationHelper.php @@ -1,4 +1,5 @@ input; - $option = strtolower($input->get('option', '')); - - if (empty($option)) - { - $option = $default; - } - - $input->set('option', $option); - - return $option; - } - - /** - * Provides a secure hash based on a seed - * - * @param string $seed Seed string. - * - * @return string A secure hash - * - * @since 3.2 - */ - public static function getHash($seed) - { - return md5(Factory::getApplication()->get('secret') . $seed); - } - - /** - * This method transliterates a string into a URL - * safe string or returns a URL safe UTF-8 string - * based on the global configuration - * - * @param string $string String to process - * @param string $language Language to transliterate to if unicode slugs are disabled - * - * @return string Processed string - * - * @since 3.2 - */ - public static function stringURLSafe($string, $language = '') - { - if (Factory::getApplication()->get('unicodeslugs') == 1) - { - $output = OutputFilter::stringUrlUnicodeSlug($string); - } - else - { - if ($language === '*' || $language === '') - { - $languageParams = ComponentHelper::getParams('com_languages'); - $language = $languageParams->get('site'); - } - - $output = OutputFilter::stringURLSafe($string, $language); - } - - return $output; - } - - /** - * Gets information on a specific client id. This method will be useful in - * future versions when we start mapping applications in the database. - * - * This method will return a client information array if called - * with no arguments which can be used to add custom application information. - * - * @param integer|string|null $id A client identifier - * @param boolean $byName If true, find the client by its name - * - * @return \stdClass|array|void Object describing the client, array containing all the clients or void if $id not known - * - * @since 1.5 - */ - public static function getClientInfo($id = null, $byName = false) - { - // Only create the array if it is empty - if (empty(self::$_clients)) - { - $obj = new \stdClass; - - // Site Client - $obj->id = 0; - $obj->name = 'site'; - $obj->path = JPATH_SITE; - self::$_clients[0] = clone $obj; - - // Administrator Client - $obj->id = 1; - $obj->name = 'administrator'; - $obj->path = JPATH_ADMINISTRATOR; - self::$_clients[1] = clone $obj; - - // Installation Client - $obj->id = 2; - $obj->name = 'installation'; - $obj->path = JPATH_INSTALLATION; - self::$_clients[2] = clone $obj; - - // API Client - $obj->id = 3; - $obj->name = 'api'; - $obj->path = JPATH_API; - self::$_clients[3] = clone $obj; - - // CLI Client - $obj->id = 4; - $obj->name = 'cli'; - $obj->path = JPATH_CLI; - self::$_clients[4] = clone $obj; - } - - // If no client id has been passed return the whole array - if ($id === null) - { - return self::$_clients; - } - - // Are we looking for client information by id or by name? - if (!$byName) - { - if (isset(self::$_clients[$id])) - { - return self::$_clients[$id]; - } - } - else - { - foreach (self::$_clients as $client) - { - if ($client->name == strtolower($id)) - { - return $client; - } - } - } - } - - /** - * Adds information for a client. - * - * @param mixed $client A client identifier either an array or object - * - * @return boolean True if the information is added. False on error - * - * @since 1.6 - */ - public static function addClientInfo($client) - { - if (\is_array($client)) - { - $client = (object) $client; - } - - if (!\is_object($client)) - { - return false; - } - - $info = self::getClientInfo(); - - if (!isset($client->id)) - { - $client->id = \count($info); - } - - self::$_clients[$client->id] = clone $client; - - return true; - } + /** + * Client information array + * + * @var array + * @since 1.6 + */ + protected static $_clients = array(); + + /** + * Return the name of the request component [main component] + * + * @param string $default The default option + * + * @return string Option (e.g. com_something) + * + * @since 1.6 + */ + public static function getComponentName($default = null) + { + static $option; + + if ($option) { + return $option; + } + + $input = Factory::getApplication()->input; + $option = strtolower($input->get('option', '')); + + if (empty($option)) { + $option = $default; + } + + $input->set('option', $option); + + return $option; + } + + /** + * Provides a secure hash based on a seed + * + * @param string $seed Seed string. + * + * @return string A secure hash + * + * @since 3.2 + */ + public static function getHash($seed) + { + return md5(Factory::getApplication()->get('secret') . $seed); + } + + /** + * This method transliterates a string into a URL + * safe string or returns a URL safe UTF-8 string + * based on the global configuration + * + * @param string $string String to process + * @param string $language Language to transliterate to if unicode slugs are disabled + * + * @return string Processed string + * + * @since 3.2 + */ + public static function stringURLSafe($string, $language = '') + { + if (Factory::getApplication()->get('unicodeslugs') == 1) { + $output = OutputFilter::stringUrlUnicodeSlug($string); + } else { + if ($language === '*' || $language === '') { + $languageParams = ComponentHelper::getParams('com_languages'); + $language = $languageParams->get('site'); + } + + $output = OutputFilter::stringURLSafe($string, $language); + } + + return $output; + } + + /** + * Gets information on a specific client id. This method will be useful in + * future versions when we start mapping applications in the database. + * + * This method will return a client information array if called + * with no arguments which can be used to add custom application information. + * + * @param integer|string|null $id A client identifier + * @param boolean $byName If true, find the client by its name + * + * @return \stdClass|array|void Object describing the client, array containing all the clients or void if $id not known + * + * @since 1.5 + */ + public static function getClientInfo($id = null, $byName = false) + { + // Only create the array if it is empty + if (empty(self::$_clients)) { + $obj = new \stdClass(); + + // Site Client + $obj->id = 0; + $obj->name = 'site'; + $obj->path = JPATH_SITE; + self::$_clients[0] = clone $obj; + + // Administrator Client + $obj->id = 1; + $obj->name = 'administrator'; + $obj->path = JPATH_ADMINISTRATOR; + self::$_clients[1] = clone $obj; + + // Installation Client + $obj->id = 2; + $obj->name = 'installation'; + $obj->path = JPATH_INSTALLATION; + self::$_clients[2] = clone $obj; + + // API Client + $obj->id = 3; + $obj->name = 'api'; + $obj->path = JPATH_API; + self::$_clients[3] = clone $obj; + + // CLI Client + $obj->id = 4; + $obj->name = 'cli'; + $obj->path = JPATH_CLI; + self::$_clients[4] = clone $obj; + } + + // If no client id has been passed return the whole array + if ($id === null) { + return self::$_clients; + } + + // Are we looking for client information by id or by name? + if (!$byName) { + if (isset(self::$_clients[$id])) { + return self::$_clients[$id]; + } + } else { + foreach (self::$_clients as $client) { + if ($client->name == strtolower($id)) { + return $client; + } + } + } + } + + /** + * Adds information for a client. + * + * @param mixed $client A client identifier either an array or object + * + * @return boolean True if the information is added. False on error + * + * @since 1.6 + */ + public static function addClientInfo($client) + { + if (\is_array($client)) { + $client = (object) $client; + } + + if (!\is_object($client)) { + return false; + } + + $info = self::getClientInfo(); + + if (!isset($client->id)) { + $client->id = \count($info); + } + + self::$_clients[$client->id] = clone $client; + + return true; + } } diff --git a/libraries/src/Application/BaseApplication.php b/libraries/src/Application/BaseApplication.php index f4e63fa739e6f..2bc19b7b9b637 100644 --- a/libraries/src/Application/BaseApplication.php +++ b/libraries/src/Application/BaseApplication.php @@ -1,4 +1,5 @@ input = $input instanceof Input ? $input : new Input; - $this->config = $config instanceof Registry ? $config : new Registry; + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's + * input object. If the argument is a \JInput object that object will become + * the application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's + * config object. If the argument is a Registry object that object will become + * the application's config object, otherwise a default config object is created. + * + * @since 3.0.0 + */ + public function __construct(Input $input = null, Registry $config = null) + { + $this->input = $input instanceof Input ? $input : new Input(); + $this->config = $config instanceof Registry ? $config : new Registry(); - $this->initialise(); - } + $this->initialise(); + } } diff --git a/libraries/src/Application/CLI/CliInput.php b/libraries/src/Application/CLI/CliInput.php index 0f214306f7b6d..bb0b343d0889e 100644 --- a/libraries/src/Application/CLI/CliInput.php +++ b/libraries/src/Application/CLI/CliInput.php @@ -1,4 +1,5 @@ setProcessor($processor ?: new Output\Processor\ColorProcessor); - } + /** + * Constructor + * + * @param ProcessorInterface $processor The output processor. + * + * @since 4.0.0 + */ + public function __construct(ProcessorInterface $processor = null) + { + $this->setProcessor($processor ?: new Output\Processor\ColorProcessor()); + } - /** - * Set a processor - * - * @param ProcessorInterface $processor The output processor. - * - * @return $this - * - * @since 4.0.0 - */ - public function setProcessor(ProcessorInterface $processor) - { - $this->processor = $processor; + /** + * Set a processor + * + * @param ProcessorInterface $processor The output processor. + * + * @return $this + * + * @since 4.0.0 + */ + public function setProcessor(ProcessorInterface $processor) + { + $this->processor = $processor; - return $this; - } + return $this; + } - /** - * Get a processor - * - * @return ProcessorInterface - * - * @since 4.0.0 - * @throws \RuntimeException - */ - public function getProcessor() - { - if ($this->processor) - { - return $this->processor; - } + /** + * Get a processor + * + * @return ProcessorInterface + * + * @since 4.0.0 + * @throws \RuntimeException + */ + public function getProcessor() + { + if ($this->processor) { + return $this->processor; + } - throw new \RuntimeException('A ProcessorInterface object has not been set.'); - } + throw new \RuntimeException('A ProcessorInterface object has not been set.'); + } - /** - * Write a string to an output handler. - * - * @param string $text The text to display. - * @param boolean $nl True (default) to append a new line at the end of the output string. - * - * @return $this - * - * @since 4.0.0 - * @codeCoverageIgnore - */ - abstract public function out($text = '', $nl = true); + /** + * Write a string to an output handler. + * + * @param string $text The text to display. + * @param boolean $nl True (default) to append a new line at the end of the output string. + * + * @return $this + * + * @since 4.0.0 + * @codeCoverageIgnore + */ + abstract public function out($text = '', $nl = true); } diff --git a/libraries/src/Application/CLI/ColorStyle.php b/libraries/src/Application/CLI/ColorStyle.php index 0b1050af826ef..35e76137009d3 100644 --- a/libraries/src/Application/CLI/ColorStyle.php +++ b/libraries/src/Application/CLI/ColorStyle.php @@ -1,4 +1,5 @@ 0, - 'red' => 1, - 'green' => 2, - 'yellow' => 3, - 'blue' => 4, - 'magenta' => 5, - 'cyan' => 6, - 'white' => 7, - ]; - - /** - * Known styles - * - * @var array - * @since 4.0.0 - */ - private static $knownOptions = [ - 'bold' => 1, - 'underscore' => 4, - 'blink' => 5, - 'reverse' => 7, - ]; - - /** - * Foreground base value - * - * @var integer - * @since 4.0.0 - */ - private static $fgBase = 30; - - /** - * Background base value - * - * @var integer - * @since 4.0.0 - */ - private static $bgBase = 40; - - /** - * Foreground color - * - * @var integer - * @since 4.0.0 - */ - private $fgColor = 0; - - /** - * Background color - * - * @var integer - * @since 4.0.0 - */ - private $bgColor = 0; - - /** - * Array of style options - * - * @var array - * @since 4.0.0 - */ - private $options = []; - - /** - * Constructor - * - * @param string $fg Foreground color. - * @param string $bg Background color. - * @param array $options Style options. - * - * @since 4.0.0 - * @throws \InvalidArgumentException - */ - public function __construct(string $fg = '', string $bg = '', array $options = []) - { - if ($fg) - { - if (\array_key_exists($fg, static::$knownColors) == false) - { - throw new \InvalidArgumentException( - sprintf( - 'Invalid foreground color "%1$s" [%2$s]', - $fg, - implode(', ', $this->getKnownColors()) - ) - ); - } - - $this->fgColor = static::$fgBase + static::$knownColors[$fg]; - } - - if ($bg) - { - if (\array_key_exists($bg, static::$knownColors) == false) - { - throw new \InvalidArgumentException( - sprintf( - 'Invalid background color "%1$s" [%2$s]', - $bg, - implode(', ', $this->getKnownColors()) - ) - ); - } - - $this->bgColor = static::$bgBase + static::$knownColors[$bg]; - } - - foreach ($options as $option) - { - if (\array_key_exists($option, static::$knownOptions) == false) - { - throw new \InvalidArgumentException( - sprintf( - 'Invalid option "%1$s" [%2$s]', - $option, - implode(', ', $this->getKnownOptions()) - ) - ); - } - - $this->options[] = $option; - } - } - - /** - * Convert to a string. - * - * @return string - * - * @since 4.0.0 - */ - public function __toString() - { - return $this->getStyle(); - } - - /** - * Create a color style from a parameter string. - * - * Example: fg=red;bg=blue;options=bold,blink - * - * @param string $string The parameter string. - * - * @return $this - * - * @since 4.0.0 - * @throws \RuntimeException - */ - public static function fromString(string $string): self - { - $fg = ''; - $bg = ''; - $options = []; - - $parts = explode(';', $string); - - foreach ($parts as $part) - { - $subParts = explode('=', $part); - - if (\count($subParts) < 2) - { - continue; - } - - switch ($subParts[0]) - { - case 'fg': - $fg = $subParts[1]; - - break; - - case 'bg': - $bg = $subParts[1]; - - break; - - case 'options': - $options = explode(',', $subParts[1]); - - break; - - default: - throw new \RuntimeException('Invalid option: ' . $subParts[0]); - } - } - - return new self($fg, $bg, $options); - } - - /** - * Get the translated color code. - * - * @return string - * - * @since 4.0.0 - */ - public function getStyle(): string - { - $values = []; - - if ($this->fgColor) - { - $values[] = $this->fgColor; - } - - if ($this->bgColor) - { - $values[] = $this->bgColor; - } - - foreach ($this->options as $option) - { - $values[] = static::$knownOptions[$option]; - } - - return implode(';', $values); - } - - /** - * Get the known colors. - * - * @return string[] - * - * @since 4.0.0 - */ - public function getKnownColors(): array - { - return array_keys(static::$knownColors); - } - - /** - * Get the known options. - * - * @return string[] - * - * @since 4.0.0 - */ - public function getKnownOptions(): array - { - return array_keys(static::$knownOptions); - } + /** + * Known colors + * + * @var array + * @since 4.0.0 + */ + private static $knownColors = [ + 'black' => 0, + 'red' => 1, + 'green' => 2, + 'yellow' => 3, + 'blue' => 4, + 'magenta' => 5, + 'cyan' => 6, + 'white' => 7, + ]; + + /** + * Known styles + * + * @var array + * @since 4.0.0 + */ + private static $knownOptions = [ + 'bold' => 1, + 'underscore' => 4, + 'blink' => 5, + 'reverse' => 7, + ]; + + /** + * Foreground base value + * + * @var integer + * @since 4.0.0 + */ + private static $fgBase = 30; + + /** + * Background base value + * + * @var integer + * @since 4.0.0 + */ + private static $bgBase = 40; + + /** + * Foreground color + * + * @var integer + * @since 4.0.0 + */ + private $fgColor = 0; + + /** + * Background color + * + * @var integer + * @since 4.0.0 + */ + private $bgColor = 0; + + /** + * Array of style options + * + * @var array + * @since 4.0.0 + */ + private $options = []; + + /** + * Constructor + * + * @param string $fg Foreground color. + * @param string $bg Background color. + * @param array $options Style options. + * + * @since 4.0.0 + * @throws \InvalidArgumentException + */ + public function __construct(string $fg = '', string $bg = '', array $options = []) + { + if ($fg) { + if (\array_key_exists($fg, static::$knownColors) == false) { + throw new \InvalidArgumentException( + sprintf( + 'Invalid foreground color "%1$s" [%2$s]', + $fg, + implode(', ', $this->getKnownColors()) + ) + ); + } + + $this->fgColor = static::$fgBase + static::$knownColors[$fg]; + } + + if ($bg) { + if (\array_key_exists($bg, static::$knownColors) == false) { + throw new \InvalidArgumentException( + sprintf( + 'Invalid background color "%1$s" [%2$s]', + $bg, + implode(', ', $this->getKnownColors()) + ) + ); + } + + $this->bgColor = static::$bgBase + static::$knownColors[$bg]; + } + + foreach ($options as $option) { + if (\array_key_exists($option, static::$knownOptions) == false) { + throw new \InvalidArgumentException( + sprintf( + 'Invalid option "%1$s" [%2$s]', + $option, + implode(', ', $this->getKnownOptions()) + ) + ); + } + + $this->options[] = $option; + } + } + + /** + * Convert to a string. + * + * @return string + * + * @since 4.0.0 + */ + public function __toString() + { + return $this->getStyle(); + } + + /** + * Create a color style from a parameter string. + * + * Example: fg=red;bg=blue;options=bold,blink + * + * @param string $string The parameter string. + * + * @return $this + * + * @since 4.0.0 + * @throws \RuntimeException + */ + public static function fromString(string $string): self + { + $fg = ''; + $bg = ''; + $options = []; + + $parts = explode(';', $string); + + foreach ($parts as $part) { + $subParts = explode('=', $part); + + if (\count($subParts) < 2) { + continue; + } + + switch ($subParts[0]) { + case 'fg': + $fg = $subParts[1]; + + break; + + case 'bg': + $bg = $subParts[1]; + + break; + + case 'options': + $options = explode(',', $subParts[1]); + + break; + + default: + throw new \RuntimeException('Invalid option: ' . $subParts[0]); + } + } + + return new self($fg, $bg, $options); + } + + /** + * Get the translated color code. + * + * @return string + * + * @since 4.0.0 + */ + public function getStyle(): string + { + $values = []; + + if ($this->fgColor) { + $values[] = $this->fgColor; + } + + if ($this->bgColor) { + $values[] = $this->bgColor; + } + + foreach ($this->options as $option) { + $values[] = static::$knownOptions[$option]; + } + + return implode(';', $values); + } + + /** + * Get the known colors. + * + * @return string[] + * + * @since 4.0.0 + */ + public function getKnownColors(): array + { + return array_keys(static::$knownColors); + } + + /** + * Get the known options. + * + * @return string[] + * + * @since 4.0.0 + */ + public function getKnownOptions(): array + { + return array_keys(static::$knownOptions); + } } diff --git a/libraries/src/Application/CLI/Output/Processor/ColorProcessor.php b/libraries/src/Application/CLI/Output/Processor/ColorProcessor.php index 894d96f6ec987..9a4da582f9814 100644 --- a/libraries/src/Application/CLI/Output/Processor/ColorProcessor.php +++ b/libraries/src/Application/CLI/Output/Processor/ColorProcessor.php @@ -1,4 +1,5 @@ (.*?)<\/\\1>/s'; - - /** - * Regex used for removing color codes - * - * @var string - * @since 4.0.0 - */ - protected static $stripFilter = '/<[\/]?[a-z=;]+>/'; - - /** - * Array of ColorStyle objects - * - * @var ColorStyle[] - * @since 4.0.0 - */ - protected $styles = []; - - /** - * Class constructor - * - * @param boolean $noColors Defines non-colored mode on construct - * - * @since 4.0.0 - */ - public function __construct($noColors = null) - { - if ($noColors === null) - { - /* - * By default windows cmd.exe and PowerShell does not support ANSI-colored output - * if the variable is not set explicitly colors should be disabled on Windows - */ - $noColors = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'); - } - - $this->noColors = $noColors; - - $this->addPredefinedStyles(); - } - - /** - * Add a style. - * - * @param string $name The style name. - * @param ColorStyle $style The color style. - * - * @return $this - * - * @since 4.0.0 - */ - public function addStyle($name, ColorStyle $style) - { - $this->styles[$name] = $style; - - return $this; - } - - /** - * Strip color tags from a string. - * - * @param string $string The string. - * - * @return string - * - * @since 4.0.0 - */ - public static function stripColors($string) - { - return preg_replace(static::$stripFilter, '', $string); - } - - /** - * Process a string. - * - * @param string $string The string to process. - * - * @return string - * - * @since 4.0.0 - */ - public function process($string) - { - preg_match_all($this->tagFilter, $string, $matches); - - if (!$matches) - { - return $string; - } - - foreach ($matches[0] as $i => $m) - { - if (\array_key_exists($matches[1][$i], $this->styles)) - { - $string = $this->replaceColors($string, $matches[1][$i], $matches[2][$i], $this->styles[$matches[1][$i]]); - } - // Custom format - elseif (strpos($matches[1][$i], '=')) - { - $string = $this->replaceColors($string, $matches[1][$i], $matches[2][$i], ColorStyle::fromString($matches[1][$i])); - } - } - - return $string; - } - - /** - * Replace color tags in a string. - * - * @param string $text The original text. - * @param string $tag The matched tag. - * @param string $match The match. - * @param ColorStyle $style The color style to apply. - * - * @return mixed - * - * @since 4.0.0 - */ - private function replaceColors($text, $tag, $match, ColorStyle $style) - { - $replace = $this->noColors - ? $match - : "\033[" . $style . 'm' . $match . "\033[0m"; - - return str_replace('<' . $tag . '>' . $match . '', $replace, $text); - } - - /** - * Adds predefined color styles to the ColorProcessor object - * - * @return $this - * - * @since 4.0.0 - */ - private function addPredefinedStyles() - { - $this->addStyle( - 'info', - new ColorStyle('green', '', ['bold']) - ); - - $this->addStyle( - 'comment', - new ColorStyle('yellow', '', ['bold']) - ); - - $this->addStyle( - 'question', - new ColorStyle('black', 'cyan') - ); - - $this->addStyle( - 'error', - new ColorStyle('white', 'red') - ); - - return $this; - } + /** + * Flag to remove color codes from the output + * + * @var boolean + * @since 4.0.0 + */ + public $noColors = false; + + /** + * Regex to match tags + * + * @var string + * @since 4.0.0 + */ + protected $tagFilter = '/<([a-z=;]+)>(.*?)<\/\\1>/s'; + + /** + * Regex used for removing color codes + * + * @var string + * @since 4.0.0 + */ + protected static $stripFilter = '/<[\/]?[a-z=;]+>/'; + + /** + * Array of ColorStyle objects + * + * @var ColorStyle[] + * @since 4.0.0 + */ + protected $styles = []; + + /** + * Class constructor + * + * @param boolean $noColors Defines non-colored mode on construct + * + * @since 4.0.0 + */ + public function __construct($noColors = null) + { + if ($noColors === null) { + /* + * By default windows cmd.exe and PowerShell does not support ANSI-colored output + * if the variable is not set explicitly colors should be disabled on Windows + */ + $noColors = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'); + } + + $this->noColors = $noColors; + + $this->addPredefinedStyles(); + } + + /** + * Add a style. + * + * @param string $name The style name. + * @param ColorStyle $style The color style. + * + * @return $this + * + * @since 4.0.0 + */ + public function addStyle($name, ColorStyle $style) + { + $this->styles[$name] = $style; + + return $this; + } + + /** + * Strip color tags from a string. + * + * @param string $string The string. + * + * @return string + * + * @since 4.0.0 + */ + public static function stripColors($string) + { + return preg_replace(static::$stripFilter, '', $string); + } + + /** + * Process a string. + * + * @param string $string The string to process. + * + * @return string + * + * @since 4.0.0 + */ + public function process($string) + { + preg_match_all($this->tagFilter, $string, $matches); + + if (!$matches) { + return $string; + } + + foreach ($matches[0] as $i => $m) { + if (\array_key_exists($matches[1][$i], $this->styles)) { + $string = $this->replaceColors($string, $matches[1][$i], $matches[2][$i], $this->styles[$matches[1][$i]]); + } + // Custom format + elseif (strpos($matches[1][$i], '=')) { + $string = $this->replaceColors($string, $matches[1][$i], $matches[2][$i], ColorStyle::fromString($matches[1][$i])); + } + } + + return $string; + } + + /** + * Replace color tags in a string. + * + * @param string $text The original text. + * @param string $tag The matched tag. + * @param string $match The match. + * @param ColorStyle $style The color style to apply. + * + * @return mixed + * + * @since 4.0.0 + */ + private function replaceColors($text, $tag, $match, ColorStyle $style) + { + $replace = $this->noColors + ? $match + : "\033[" . $style . 'm' . $match . "\033[0m"; + + return str_replace('<' . $tag . '>' . $match . '', $replace, $text); + } + + /** + * Adds predefined color styles to the ColorProcessor object + * + * @return $this + * + * @since 4.0.0 + */ + private function addPredefinedStyles() + { + $this->addStyle( + 'info', + new ColorStyle('green', '', ['bold']) + ); + + $this->addStyle( + 'comment', + new ColorStyle('yellow', '', ['bold']) + ); + + $this->addStyle( + 'question', + new ColorStyle('black', 'cyan') + ); + + $this->addStyle( + 'error', + new ColorStyle('white', 'red') + ); + + return $this; + } } diff --git a/libraries/src/Application/CLI/Output/Processor/ProcessorInterface.php b/libraries/src/Application/CLI/Output/Processor/ProcessorInterface.php index 0e54f3e277226..38bcfcace6884 100644 --- a/libraries/src/Application/CLI/Output/Processor/ProcessorInterface.php +++ b/libraries/src/Application/CLI/Output/Processor/ProcessorInterface.php @@ -1,4 +1,5 @@ getProcessor()->process($text) . ($nl ? "\n" : null)); + /** + * Write a string to standard output + * + * @param string $text The text to display. + * @param boolean $nl True (default) to append a new line at the end of the output string. + * + * @return $this + * + * @codeCoverageIgnore + * @since 4.0.0 + */ + public function out($text = '', $nl = true) + { + fwrite(STDOUT, $this->getProcessor()->process($text) . ($nl ? "\n" : null)); - return $this; - } + return $this; + } } diff --git a/libraries/src/Application/CLI/Output/Xml.php b/libraries/src/Application/CLI/Output/Xml.php index a5d09c1916ffb..7f4c6d2ec6c7b 100644 --- a/libraries/src/Application/CLI/Output/Xml.php +++ b/libraries/src/Application/CLI/Output/Xml.php @@ -1,4 +1,5 @@ setContainer($container); - - parent::__construct($input, $config, $client); - - // If JDEBUG is defined, load the profiler instance - if (\defined('JDEBUG') && JDEBUG) - { - $this->profiler = Profiler::getInstance('Application'); - } - - // Enable sessions by default. - if ($this->config->get('session') === null) - { - $this->config->set('session', true); - } - - // Set the session default name. - if ($this->config->get('session_name') === null) - { - $this->config->set('session_name', $this->getName()); - } - } - - /** - * Checks the user session. - * - * If the session record doesn't exist, initialise it. - * If session is new, create session variables - * - * @return void - * - * @since 3.2 - * @throws \RuntimeException - */ - public function checkSession() - { - $this->getContainer()->get(MetadataManager::class)->createOrUpdateRecord($this->getSession(), $this->getIdentity()); - } - - /** - * Enqueue a system message. - * - * @param string $msg The message to enqueue. - * @param string $type The message type. Default is message. - * - * @return void - * - * @since 3.2 - */ - public function enqueueMessage($msg, $type = self::MSG_INFO) - { - // Don't add empty messages. - if ($msg === null || trim($msg) === '') - { - return; - } - - $inputFilter = InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ); - - // Build the message array and apply the HTML InputFilter with the default blacklist to the message - $message = array( - 'message' => $inputFilter->clean($msg, 'html'), - 'type' => $inputFilter->clean(strtolower($type), 'cmd'), - ); - - // For empty queue, if messages exists in the session, enqueue them first. - $messages = $this->getMessageQueue(); - - if (!\in_array($message, $this->messageQueue)) - { - // Enqueue the message. - $this->messageQueue[] = $message; - } - } - - /** - * Ensure several core system input variables are not arrays. - * - * @return void - * - * @since 3.9 - */ - private function sanityCheckSystemVariables() - { - $input = $this->input; - - // Get invalid input variables - $invalidInputVariables = array_filter( - array('option', 'view', 'format', 'lang', 'Itemid', 'template', 'templateStyle', 'task'), - function ($systemVariable) use ($input) { - return $input->exists($systemVariable) && is_array($input->getRaw($systemVariable)); - } - ); - - // Unset invalid system variables - foreach ($invalidInputVariables as $systemVariable) - { - $input->set($systemVariable, null); - } - - // Abort when there are invalid variables - if ($invalidInputVariables) - { - throw new \RuntimeException('Invalid input, aborting application.'); - } - } - - /** - * Execute the application. - * - * @return void - * - * @since 3.2 - */ - public function execute() - { - try - { - $this->sanityCheckSystemVariables(); - $this->setupLogging(); - $this->createExtensionNamespaceMap(); - - // Perform application routines. - $this->doExecute(); - - // If we have an application document object, render it. - if ($this->document instanceof \Joomla\CMS\Document\Document) - { - // Render the application output. - $this->render(); - } - - // If gzip compression is enabled in configuration and the server is compliant, compress the output. - if ($this->get('gzip') && !ini_get('zlib.output_compression') && ini_get('output_handler') !== 'ob_gzhandler') - { - $this->compress(); - - // Trigger the onAfterCompress event. - $this->triggerEvent('onAfterCompress'); - } - } - catch (\Throwable $throwable) - { - /** @var ErrorEvent $event */ - $event = AbstractEvent::create( - 'onError', - [ - 'subject' => $throwable, - 'eventClass' => ErrorEvent::class, - 'application' => $this, - ] - ); - - // Trigger the onError event. - $this->triggerEvent('onError', $event); - - ExceptionHandler::handleException($event->getError()); - } - - // Trigger the onBeforeRespond event. - $this->getDispatcher()->dispatch('onBeforeRespond'); - - // Send the application response. - $this->respond(); - - // Trigger the onAfterRespond event. - $this->getDispatcher()->dispatch('onAfterRespond'); - } - - /** - * Check if the user is required to reset their password. - * - * If the user is required to reset their password will be redirected to the page that manage the password reset. - * - * @param string $option The option that manage the password reset - * @param string $view The view that manage the password reset - * @param string $layout The layout of the view that manage the password reset - * @param string $tasks Permitted tasks - * - * @return void - * - * @throws \Exception - */ - protected function checkUserRequireReset($option, $view, $layout, $tasks) - { - if (Factory::getUser()->get('requireReset', 0)) - { - $redirect = false; - - /* - * By default user profile edit page is used. - * That page allows you to change more than just the password and might not be the desired behavior. - * This allows a developer to override the page that manage the password reset. - * (can be configured using the file: configuration.php, or if extended, through the global configuration form) - */ - $name = $this->getName(); - - if ($this->get($name . '_reset_password_override', 0)) - { - $option = $this->get($name . '_reset_password_option', ''); - $view = $this->get($name . '_reset_password_view', ''); - $layout = $this->get($name . '_reset_password_layout', ''); - $tasks = $this->get($name . '_reset_password_tasks', ''); - } - - $task = $this->input->getCmd('task', ''); - - // Check task or option/view/layout - if (!empty($task)) - { - $tasks = explode(',', $tasks); - - // Check full task version "option/task" - if (array_search($this->input->getCmd('option', '') . '/' . $task, $tasks) === false) - { - // Check short task version, must be on the same option of the view - if ($this->input->getCmd('option', '') !== $option || array_search($task, $tasks) === false) - { - // Not permitted task - $redirect = true; - } - } - } - else - { - if ($this->input->getCmd('option', '') !== $option || $this->input->getCmd('view', '') !== $view - || $this->input->getCmd('layout', '') !== $layout) - { - // Requested a different option/view/layout - $redirect = true; - } - } - - if ($redirect) - { - // Redirect to the profile edit page - $this->enqueueMessage(Text::_('JGLOBAL_PASSWORD_RESET_REQUIRED'), 'notice'); - - $url = Route::_('index.php?option=' . $option . '&view=' . $view . '&layout=' . $layout, false); - - // In the administrator we need a different URL - if (strtolower($name) === 'administrator') - { - $user = Factory::getApplication()->getIdentity(); - $url = Route::_('index.php?option=' . $option . '&task=' . $view . '.' . $layout . '&id=' . $user->id, false); - } - - $this->redirect($url); - } - } - } - - /** - * Gets a configuration value. - * - * @param string $varname The name of the value to get. - * @param string $default Default value to return - * - * @return mixed The user state. - * - * @since 3.2 - * @deprecated 5.0 Use get() instead - */ - public function getCfg($varname, $default = null) - { - try - { - Log::add( - sprintf('%s() is deprecated and will be removed in 5.0. Use JFactory->getApplication()->get() instead.', __METHOD__), - Log::WARNING, - 'deprecated' - ); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - return $this->get($varname, $default); - } - - /** - * Gets the client id of the current running application. - * - * @return integer A client identifier. - * - * @since 3.2 - */ - public function getClientId() - { - return $this->clientId; - } - - /** - * Returns a reference to the global CmsApplication object, only creating it if it doesn't already exist. - * - * This method must be invoked as: $web = CmsApplication::getInstance(); - * - * @param string $name The name (optional) of the CmsApplication class to instantiate. - * @param string $prefix The class name prefix of the object. - * @param Container $container An optional dependency injection container to inject into the application. - * - * @return CmsApplication - * - * @since 3.2 - * @throws \RuntimeException - * @deprecated 5.0 Use \Joomla\CMS\Factory::getContainer()->get($name) instead - */ - public static function getInstance($name = null, $prefix = '\JApplication', Container $container = null) - { - if (empty(static::$instances[$name])) - { - // Create a CmsApplication object. - $classname = $prefix . ucfirst($name); - - if (!$container) - { - $container = Factory::getContainer(); - } - - if ($container->has($classname)) - { - static::$instances[$name] = $container->get($classname); - } - elseif (class_exists($classname)) - { - // @todo This creates an implicit hard requirement on the ApplicationCms constructor - static::$instances[$name] = new $classname(null, null, null, $container); - } - else - { - throw new \RuntimeException(Text::sprintf('JLIB_APPLICATION_ERROR_APPLICATION_LOAD', $name), 500); - } - - static::$instances[$name]->loadIdentity(Factory::getUser()); - } - - return static::$instances[$name]; - } - - /** - * Returns the application \JMenu object. - * - * @param string $name The name of the application/client. - * @param array $options An optional associative array of configuration settings. - * - * @return AbstractMenu - * - * @since 3.2 - */ - public function getMenu($name = null, $options = array()) - { - if (!isset($name)) - { - $name = $this->getName(); - } - - // Inject this application object into the \JMenu tree if one isn't already specified - if (!isset($options['app'])) - { - $options['app'] = $this; - } - - if (array_key_exists($name, $this->menus)) - { - return $this->menus[$name]; - } - - if ($this->menuFactory === null) - { - @trigger_error('Menu factory must be set in 5.0', E_USER_DEPRECATED); - $this->menuFactory = $this->getContainer()->get(MenuFactoryInterface::class); - } - - $this->menus[$name] = $this->menuFactory->createMenu($name, $options); - - // Make sure the abstract menu has the instance too, is needed for BC and will be removed with version 5 - AbstractMenu::$instances[$name] = $this->menus[$name]; - - return $this->menus[$name]; - } - - /** - * Get the system message queue. - * - * @param boolean $clear Clear the messages currently attached to the application object - * - * @return array The system message queue. - * - * @since 3.2 - */ - public function getMessageQueue($clear = false) - { - // For empty queue, if messages exists in the session, enqueue them. - if (!\count($this->messageQueue)) - { - $sessionQueue = $this->getSession()->get('application.queue', []); - - if ($sessionQueue) - { - $this->messageQueue = $sessionQueue; - $this->getSession()->set('application.queue', []); - } - } - - $messageQueue = $this->messageQueue; - - if ($clear) - { - $this->messageQueue = array(); - } - - return $messageQueue; - } - - /** - * Gets the name of the current running application. - * - * @return string The name of the application. - * - * @since 3.2 - */ - public function getName() - { - return $this->name; - } - - /** - * Returns the application Pathway object. - * - * @return Pathway - * - * @since 3.2 - */ - public function getPathway() - { - if (!$this->pathway) - { - $resourceName = ucfirst($this->getName()) . 'Pathway'; - - if (!$this->getContainer()->has($resourceName)) - { - throw new \RuntimeException( - Text::sprintf('JLIB_APPLICATION_ERROR_PATHWAY_LOAD', $this->getName()), - 500 - ); - } - - $this->pathway = $this->getContainer()->get($resourceName); - } - - return $this->pathway; - } - - /** - * Returns the application Router object. - * - * @param string $name The name of the application. - * @param array $options An optional associative array of configuration settings. - * - * @return Router - * - * @since 3.2 - * - * @deprecated 5.0 Inject the router or load it from the dependency injection container - */ - public static function getRouter($name = null, array $options = array()) - { - $app = Factory::getApplication(); - - if (!isset($name)) - { - $name = $app->getName(); - } - - $options['mode'] = $app->get('sef'); - - return Router::getInstance($name, $options); - } - - /** - * Gets the name of the current template. - * - * @param boolean $params An optional associative array of configuration settings - * - * @return mixed System is the fallback. - * - * @since 3.2 - */ - public function getTemplate($params = false) - { - if ($params) - { - $template = new \stdClass; - - $template->template = 'system'; - $template->params = new Registry; - $template->inheritable = 0; - $template->parent = ''; - - return $template; - } - - return 'system'; - } - - /** - * Gets a user state. - * - * @param string $key The path of the state. - * @param mixed $default Optional default value, returned if the internal value is null. - * - * @return mixed The user state or null. - * - * @since 3.2 - */ - public function getUserState($key, $default = null) - { - $registry = $this->getSession()->get('registry'); - - if ($registry !== null) - { - return $registry->get($key, $default); - } - - return $default; - } - - /** - * Gets the value of a user state variable. - * - * @param string $key The key of the user state variable. - * @param string $request The name of the variable passed in a request. - * @param string $default The default value for the variable if not found. Optional. - * @param string $type Filter for the variable, for valid values see {@link InputFilter::clean()}. Optional. - * - * @return mixed The request user state. - * - * @since 3.2 - */ - public function getUserStateFromRequest($key, $request, $default = null, $type = 'none') - { - $cur_state = $this->getUserState($key, $default); - $new_state = $this->input->get($request, null, $type); - - if ($new_state === null) - { - return $cur_state; - } - - // Save the new value only if it was set in this request. - $this->setUserState($key, $new_state); - - return $new_state; - } - - /** - * Initialise the application. - * - * @param array $options An optional associative array of configuration settings. - * - * @return void - * - * @since 3.2 - */ - protected function initialiseApp($options = array()) - { - // Check that we were given a language in the array (since by default may be blank). - if (isset($options['language'])) - { - $this->set('language', $options['language']); - } - - // Build our language object - $lang = Language::getInstance($this->get('language'), $this->get('debug_lang')); - - // Load the language to the API - $this->loadLanguage($lang); - - // Register the language object with Factory - Factory::$language = $this->getLanguage(); - - // Load the library language files - $this->loadLibraryLanguage(); - - // Set user specific editor. - $user = Factory::getUser(); - $editor = $user->getParam('editor', $this->get('editor')); - - if (!PluginHelper::isEnabled('editors', $editor)) - { - $editor = $this->get('editor'); - - if (!PluginHelper::isEnabled('editors', $editor)) - { - $editor = 'none'; - } - } - - $this->set('editor', $editor); - - // Load the behaviour plugins - PluginHelper::importPlugin('behaviour'); - - // Trigger the onAfterInitialise event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterInitialise'); - } - - /** - * Checks if HTTPS is forced in the client configuration. - * - * @param integer $clientId An optional client id (defaults to current application client). - * - * @return boolean True if is forced for the client, false otherwise. - * - * @since 3.7.3 - */ - public function isHttpsForced($clientId = null) - { - $clientId = (int) ($clientId !== null ? $clientId : $this->getClientId()); - $forceSsl = (int) $this->get('force_ssl'); - - if ($clientId === 0 && $forceSsl === 2) - { - return true; - } - - if ($clientId === 1 && $forceSsl >= 1) - { - return true; - } - - return false; - } - - /** - * Check the client interface by name. - * - * @param string $identifier String identifier for the application interface - * - * @return boolean True if this application is of the given type client interface. - * - * @since 3.7.0 - */ - public function isClient($identifier) - { - return $this->getName() === $identifier; - } - - /** - * Load the library language files for the application - * - * @return void - * - * @since 3.6.3 - */ - protected function loadLibraryLanguage() - { - $this->getLanguage()->load('lib_joomla', JPATH_ADMINISTRATOR); - } - - /** - * Login authentication function. - * - * Username and encoded password are passed the onUserLogin event which - * is responsible for the user validation. A successful validation updates - * the current session record with the user's details. - * - * Username and encoded password are sent as credentials (along with other - * possibilities) to each observer (authentication plugin) for user - * validation. Successful validation will update the current session with - * the user details. - * - * @param array $credentials Array('username' => string, 'password' => string) - * @param array $options Array('remember' => boolean) - * - * @return boolean|\Exception True on success, false if failed or silent handling is configured, or a \Exception object on authentication error. - * - * @since 3.2 - */ - public function login($credentials, $options = array()) - { - // Get the global Authentication object. - $authenticate = Authentication::getInstance($this->authenticationPluginType); - $response = $authenticate->authenticate($credentials, $options); - - // Import the user plugin group. - PluginHelper::importPlugin('user'); - - if ($response->status === Authentication::STATUS_SUCCESS) - { - /* - * Validate that the user should be able to login (different to being authenticated). - * This permits authentication plugins blocking the user. - */ - $authorisations = $authenticate->authorise($response, $options); - $denied_states = Authentication::STATUS_EXPIRED | Authentication::STATUS_DENIED; - - foreach ($authorisations as $authorisation) - { - if ((int) $authorisation->status & $denied_states) - { - // Trigger onUserAuthorisationFailure Event. - $this->triggerEvent('onUserAuthorisationFailure', array((array) $authorisation)); - - // If silent is set, just return false. - if (isset($options['silent']) && $options['silent']) - { - return false; - } - - // Return the error. - switch ($authorisation->status) - { - case Authentication::STATUS_EXPIRED: - Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_EXPIRED'), 'error'); - - return false; - - case Authentication::STATUS_DENIED: - Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_DENIED'), 'error'); - - return false; - - default: - Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_AUTHORISATION'), 'error'); - - return false; - } - } - } - - // OK, the credentials are authenticated and user is authorised. Let's fire the onLogin event. - $results = $this->triggerEvent('onUserLogin', array((array) $response, $options)); - - /* - * If any of the user plugins did not successfully complete the login routine - * then the whole method fails. - * - * Any errors raised should be done in the plugin as this provides the ability - * to provide much more information about why the routine may have failed. - */ - $user = Factory::getUser(); - - if ($response->type === 'Cookie') - { - $user->set('cookieLogin', true); - } - - if (\in_array(false, $results, true) == false) - { - $options['user'] = $user; - $options['responseType'] = $response->type; - - // The user is successfully logged in. Run the after login events - $this->triggerEvent('onUserAfterLogin', array($options)); - - return true; - } - } - - // Trigger onUserLoginFailure Event. - $this->triggerEvent('onUserLoginFailure', array((array) $response)); - - // If silent is set, just return false. - if (isset($options['silent']) && $options['silent']) - { - return false; - } - - // If status is success, any error will have been raised by the user plugin - if ($response->status !== Authentication::STATUS_SUCCESS) - { - $this->getLogger()->warning($response->error_message, array('category' => 'jerror')); - } - - return false; - } - - /** - * Logout authentication function. - * - * Passed the current user information to the onUserLogout event and reverts the current - * session record back to 'anonymous' parameters. - * If any of the authentication plugins did not successfully complete - * the logout routine then the whole method fails. Any errors raised - * should be done in the plugin as this provides the ability to give - * much more information about why the routine may have failed. - * - * @param integer $userid The user to load - Can be an integer or string - If string, it is converted to ID automatically - * @param array $options Array('clientid' => array of client id's) - * - * @return boolean True on success - * - * @since 3.2 - */ - public function logout($userid = null, $options = array()) - { - // Get a user object from the Application. - $user = Factory::getUser($userid); - - // Build the credentials array. - $parameters['username'] = $user->get('username'); - $parameters['id'] = $user->get('id'); - - // Set clientid in the options array if it hasn't been set already and shared sessions are not enabled. - if (!$this->get('shared_session', '0') && !isset($options['clientid'])) - { - $options['clientid'] = $this->getClientId(); - } - - // Import the user plugin group. - PluginHelper::importPlugin('user'); - - // OK, the credentials are built. Lets fire the onLogout event. - $results = $this->triggerEvent('onUserLogout', array($parameters, $options)); - - // Check if any of the plugins failed. If none did, success. - if (!\in_array(false, $results, true)) - { - $options['username'] = $user->get('username'); - $this->triggerEvent('onUserAfterLogout', array($options)); - - return true; - } - - // Trigger onUserLogoutFailure Event. - $this->triggerEvent('onUserLogoutFailure', array($parameters)); - - return false; - } - - /** - * Redirect to another URL. - * - * If the headers have not been sent the redirect will be accomplished using a "301 Moved Permanently" - * or "303 See Other" code in the header pointing to the new location. If the headers have already been - * sent this will be accomplished using a JavaScript statement. - * - * @param string $url The URL to redirect to. Can only be http/https URL - * @param integer $status The HTTP 1.1 status code to be provided. 303 is assumed by default. - * - * @return void - * - * @since 3.2 - */ - public function redirect($url, $status = 303) - { - // Persist messages if they exist. - if (\count($this->messageQueue)) - { - $this->getSession()->set('application.queue', $this->messageQueue); - } - - // Hand over processing to the parent now - parent::redirect($url, $status); - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 3.2 - */ - protected function render() - { - // Setup the document options. - $this->docOptions['template'] = $this->get('theme'); - $this->docOptions['file'] = $this->get('themeFile', 'index.php'); - $this->docOptions['params'] = $this->get('themeParams'); - $this->docOptions['csp_nonce'] = $this->get('csp_nonce'); - $this->docOptions['templateInherits'] = $this->get('themeInherits'); - - if ($this->get('themes.base')) - { - $this->docOptions['directory'] = $this->get('themes.base'); - } - // Fall back to constants. - else - { - $this->docOptions['directory'] = \defined('JPATH_THEMES') ? JPATH_THEMES : (\defined('JPATH_BASE') ? JPATH_BASE : __DIR__) . '/themes'; - } - - // Parse the document. - $this->document->parse($this->docOptions); - - // Trigger the onBeforeRender event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onBeforeRender'); - - $caching = false; - - if ($this->isClient('site') && $this->get('caching') && $this->get('caching', 2) == 2 && !Factory::getUser()->get('id')) - { - $caching = true; - } - - // Render the document. - $data = $this->document->render($caching, $this->docOptions); - - // Set the application output data. - $this->setBody($data); - - // Trigger the onAfterRender event. - $this->triggerEvent('onAfterRender'); - - // Mark afterRender in the profiler. - JDEBUG ? $this->profiler->mark('afterRender') : null; - } - - /** - * Route the application. - * - * Routing is the process of examining the request environment to determine which - * component should receive the request. The component optional parameters - * are then set in the request object to be processed when the application is being - * dispatched. - * - * @return void - * - * @since 3.2 - * - * @deprecated 5.0 Implement the route functionality in the extending class, this here will be removed without replacement - */ - protected function route() - { - // Get the full request URI. - $uri = clone Uri::getInstance(); - - $router = static::getRouter(); - $result = $router->parse($uri, true); - - $active = $this->getMenu()->getActive(); - - if ($active !== null - && $active->type === 'alias' - && $active->getParams()->get('alias_redirect') - && \in_array($this->input->getMethod(), array('GET', 'HEAD'), true)) - { - $item = $this->getMenu()->getItem($active->getParams()->get('aliasoptions')); - - if ($item !== null) - { - $oldUri = clone Uri::getInstance(); - - if ($oldUri->getVar('Itemid') == $active->id) - { - $oldUri->setVar('Itemid', $item->id); - } - - $base = Uri::base(true); - $oldPath = StringHelper::strtolower(substr($oldUri->getPath(), \strlen($base) + 1)); - $activePathPrefix = StringHelper::strtolower($active->route); - - $position = strpos($oldPath, $activePathPrefix); - - if ($position !== false) - { - $oldUri->setPath($base . '/' . substr_replace($oldPath, $item->route, $position, \strlen($activePathPrefix))); - - $this->setHeader('Expires', 'Wed, 17 Aug 2005 00:00:00 GMT', true); - $this->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true); - $this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate', false); - $this->sendHeaders(); - - $this->redirect((string) $oldUri, 301); - } - } - } - - foreach ($result as $key => $value) - { - $this->input->def($key, $value); - } - - // Trigger the onAfterRoute event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterRoute'); - } - - /** - * Sets the value of a user state variable. - * - * @param string $key The path of the state. - * @param mixed $value The value of the variable. - * - * @return mixed|void The previous state, if one existed. - * - * @since 3.2 - */ - public function setUserState($key, $value) - { - $session = $this->getSession(); - $registry = $session->get('registry'); - - if ($registry !== null) - { - return $registry->set($key, $value); - } - } - - /** - * Sends all headers prior to returning the string - * - * @param boolean $compress If true, compress the data - * - * @return string - * - * @since 3.2 - */ - public function toString($compress = false) - { - // Don't compress something if the server is going to do it anyway. Waste of time. - if ($compress && !ini_get('zlib.output_compression') && ini_get('output_handler') !== 'ob_gzhandler') - { - $this->compress(); - } - - if ($this->allowCache() === false) - { - $this->setHeader('Cache-Control', 'no-cache', false); - } - - $this->sendHeaders(); - - return $this->getBody(); - } - - /** - * Method to determine a hash for anti-spoofing variable names - * - * @param boolean $forceNew If true, force a new token to be created - * - * @return string Hashed var name - * - * @since 4.0.0 - */ - public function getFormToken($forceNew = false) - { - /** @var Session $session */ - $session = $this->getSession(); - - return $session->getFormToken($forceNew); - } - - /** - * Checks for a form token in the request. - * - * Use in conjunction with getFormToken. - * - * @param string $method The request method in which to look for the token key. - * - * @return boolean True if found and valid, false otherwise. - * - * @since 4.0.0 - */ - public function checkToken($method = 'post') - { - /** @var Session $session */ - $session = $this->getSession(); - - return $session->checkToken($method); - } - - /** - * Flag if the application instance is a CLI or web based application. - * - * Helper function, you should use the native PHP functions to detect if it is a CLI application. - * - * @return boolean - * - * @since 4.0.0 - * @deprecated 5.0 Will be removed without replacements - */ - public function isCli() - { - return false; - } - - /** - * No longer used - * - * @return boolean - * - * @since 4.0.0 - * - * @throws \Exception - * @deprecated 4.2.0 Will be removed in 5.0 without replacement. - */ - protected function isTwoFactorAuthenticationRequired(): bool - { - return false; - } - - /** - * No longer used - * - * @return boolean - * - * @since 4.0.0 - * - * @throws \Exception - * @deprecated 4.2.0 Will be removed in 5.0 without replacement. - */ - private function hasUserConfiguredTwoFactorAuthentication(): bool - { - return false; - } - - /** - * Setup logging functionality. - * - * @return void - * - * @since 4.0.0 - */ - private function setupLogging(): void - { - // Add InMemory logger that will collect all log entries to allow to display them later by extensions - if ($this->get('debug')) - { - Log::addLogger(['logger' => 'inmemory']); - } - - // Log the deprecated API. - if ($this->get('log_deprecated')) - { - Log::addLogger(['text_file' => 'deprecated.php'], Log::ALL, ['deprecated']); - } - - // We only log errors unless Site Debug is enabled - $logLevels = Log::ERROR | Log::CRITICAL | Log::ALERT | Log::EMERGENCY; - - if ($this->get('debug')) - { - $logLevels = Log::ALL; - } - - Log::addLogger(['text_file' => 'joomla_core_errors.php'], $logLevels, ['system']); - - // Log everything (except deprecated APIs, these are logged separately with the option above). - if ($this->get('log_everything')) - { - Log::addLogger(['text_file' => 'everything.php'], Log::ALL, ['deprecated', 'deprecation-notes', 'databasequery'], true); - } - - if ($this->get('log_categories')) - { - $priority = 0; - - foreach ($this->get('log_priorities', ['all']) as $p) - { - $const = '\\Joomla\\CMS\\Log\\Log::' . strtoupper($p); - - if (defined($const)) - { - $priority |= constant($const); - } - } - - // Split into an array at any character other than alphabet, numbers, _, ., or - - $categories = preg_split('/[^\w.-]+/', $this->get('log_categories', ''), -1, PREG_SPLIT_NO_EMPTY); - $mode = (bool) $this->get('log_category_mode', false); - - if (!$categories) - { - return; - } - - Log::addLogger(['text_file' => 'custom-logging.php'], $priority, $categories, $mode); - } - } - - /** - * Sets the internal menu factory. - * - * @param MenuFactoryInterface $menuFactory The menu factory - * - * @return void - * - * @since 4.2.0 - */ - public function setMenuFactory(MenuFactoryInterface $menuFactory): void - { - $this->menuFactory = $menuFactory; - } + use ContainerAwareTrait; + use ExtensionManagerTrait; + use ExtensionNamespaceMapper; + use SessionAwareWebApplicationTrait; + + /** + * Array of options for the \JDocument object + * + * @var array + * @since 3.2 + */ + protected $docOptions = array(); + + /** + * Application instances container. + * + * @var CmsApplication[] + * @since 3.2 + */ + protected static $instances = array(); + + /** + * The scope of the application. + * + * @var string + * @since 3.2 + */ + public $scope = null; + + /** + * The client identifier. + * + * @var integer + * @since 4.0.0 + */ + protected $clientId = null; + + /** + * The application message queue. + * + * @var array + * @since 4.0.0 + */ + protected $messageQueue = array(); + + /** + * The name of the application. + * + * @var string + * @since 4.0.0 + */ + protected $name = null; + + /** + * The profiler instance + * + * @var Profiler + * @since 3.2 + */ + protected $profiler = null; + + /** + * Currently active template + * + * @var object + * @since 3.2 + */ + protected $template = null; + + /** + * The pathway object + * + * @var Pathway + * @since 4.0.0 + */ + protected $pathway = null; + + /** + * The authentication plugin type + * + * @var string + * @since 4.0.0 + */ + protected $authenticationPluginType = 'authentication'; + + /** + * Menu instances container. + * + * @var AbstractMenu[] + * @since 4.2.0 + */ + protected $menus = []; + + /** + * The menu factory + * + * @var MenuFactoryInterface + * + * @since 4.2.0 + */ + private $menuFactory; + + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's input + * object. If the argument is a JInput object that object will become the + * application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's config + * object. If the argument is a Registry object that object will become the + * application's config object, otherwise a default config object is created. + * @param WebClient $client An optional argument to provide dependency injection for the application's client + * object. If the argument is a WebClient object that object will become the + * application's client object, otherwise a default client object is created. + * @param Container $container Dependency injection container. + * + * @since 3.2 + */ + public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, Container $container = null) + { + $container = $container ?: new Container(); + $this->setContainer($container); + + parent::__construct($input, $config, $client); + + // If JDEBUG is defined, load the profiler instance + if (\defined('JDEBUG') && JDEBUG) { + $this->profiler = Profiler::getInstance('Application'); + } + + // Enable sessions by default. + if ($this->config->get('session') === null) { + $this->config->set('session', true); + } + + // Set the session default name. + if ($this->config->get('session_name') === null) { + $this->config->set('session_name', $this->getName()); + } + } + + /** + * Checks the user session. + * + * If the session record doesn't exist, initialise it. + * If session is new, create session variables + * + * @return void + * + * @since 3.2 + * @throws \RuntimeException + */ + public function checkSession() + { + $this->getContainer()->get(MetadataManager::class)->createOrUpdateRecord($this->getSession(), $this->getIdentity()); + } + + /** + * Enqueue a system message. + * + * @param string $msg The message to enqueue. + * @param string $type The message type. Default is message. + * + * @return void + * + * @since 3.2 + */ + public function enqueueMessage($msg, $type = self::MSG_INFO) + { + // Don't add empty messages. + if ($msg === null || trim($msg) === '') { + return; + } + + $inputFilter = InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ); + + // Build the message array and apply the HTML InputFilter with the default blacklist to the message + $message = array( + 'message' => $inputFilter->clean($msg, 'html'), + 'type' => $inputFilter->clean(strtolower($type), 'cmd'), + ); + + // For empty queue, if messages exists in the session, enqueue them first. + $messages = $this->getMessageQueue(); + + if (!\in_array($message, $this->messageQueue)) { + // Enqueue the message. + $this->messageQueue[] = $message; + } + } + + /** + * Ensure several core system input variables are not arrays. + * + * @return void + * + * @since 3.9 + */ + private function sanityCheckSystemVariables() + { + $input = $this->input; + + // Get invalid input variables + $invalidInputVariables = array_filter( + array('option', 'view', 'format', 'lang', 'Itemid', 'template', 'templateStyle', 'task'), + function ($systemVariable) use ($input) { + return $input->exists($systemVariable) && is_array($input->getRaw($systemVariable)); + } + ); + + // Unset invalid system variables + foreach ($invalidInputVariables as $systemVariable) { + $input->set($systemVariable, null); + } + + // Abort when there are invalid variables + if ($invalidInputVariables) { + throw new \RuntimeException('Invalid input, aborting application.'); + } + } + + /** + * Execute the application. + * + * @return void + * + * @since 3.2 + */ + public function execute() + { + try { + $this->sanityCheckSystemVariables(); + $this->setupLogging(); + $this->createExtensionNamespaceMap(); + + // Perform application routines. + $this->doExecute(); + + // If we have an application document object, render it. + if ($this->document instanceof \Joomla\CMS\Document\Document) { + // Render the application output. + $this->render(); + } + + // If gzip compression is enabled in configuration and the server is compliant, compress the output. + if ($this->get('gzip') && !ini_get('zlib.output_compression') && ini_get('output_handler') !== 'ob_gzhandler') { + $this->compress(); + + // Trigger the onAfterCompress event. + $this->triggerEvent('onAfterCompress'); + } + } catch (\Throwable $throwable) { + /** @var ErrorEvent $event */ + $event = AbstractEvent::create( + 'onError', + [ + 'subject' => $throwable, + 'eventClass' => ErrorEvent::class, + 'application' => $this, + ] + ); + + // Trigger the onError event. + $this->triggerEvent('onError', $event); + + ExceptionHandler::handleException($event->getError()); + } + + // Trigger the onBeforeRespond event. + $this->getDispatcher()->dispatch('onBeforeRespond'); + + // Send the application response. + $this->respond(); + + // Trigger the onAfterRespond event. + $this->getDispatcher()->dispatch('onAfterRespond'); + } + + /** + * Check if the user is required to reset their password. + * + * If the user is required to reset their password will be redirected to the page that manage the password reset. + * + * @param string $option The option that manage the password reset + * @param string $view The view that manage the password reset + * @param string $layout The layout of the view that manage the password reset + * @param string $tasks Permitted tasks + * + * @return void + * + * @throws \Exception + */ + protected function checkUserRequireReset($option, $view, $layout, $tasks) + { + if (Factory::getUser()->get('requireReset', 0)) { + $redirect = false; + + /* + * By default user profile edit page is used. + * That page allows you to change more than just the password and might not be the desired behavior. + * This allows a developer to override the page that manage the password reset. + * (can be configured using the file: configuration.php, or if extended, through the global configuration form) + */ + $name = $this->getName(); + + if ($this->get($name . '_reset_password_override', 0)) { + $option = $this->get($name . '_reset_password_option', ''); + $view = $this->get($name . '_reset_password_view', ''); + $layout = $this->get($name . '_reset_password_layout', ''); + $tasks = $this->get($name . '_reset_password_tasks', ''); + } + + $task = $this->input->getCmd('task', ''); + + // Check task or option/view/layout + if (!empty($task)) { + $tasks = explode(',', $tasks); + + // Check full task version "option/task" + if (array_search($this->input->getCmd('option', '') . '/' . $task, $tasks) === false) { + // Check short task version, must be on the same option of the view + if ($this->input->getCmd('option', '') !== $option || array_search($task, $tasks) === false) { + // Not permitted task + $redirect = true; + } + } + } else { + if ( + $this->input->getCmd('option', '') !== $option || $this->input->getCmd('view', '') !== $view + || $this->input->getCmd('layout', '') !== $layout + ) { + // Requested a different option/view/layout + $redirect = true; + } + } + + if ($redirect) { + // Redirect to the profile edit page + $this->enqueueMessage(Text::_('JGLOBAL_PASSWORD_RESET_REQUIRED'), 'notice'); + + $url = Route::_('index.php?option=' . $option . '&view=' . $view . '&layout=' . $layout, false); + + // In the administrator we need a different URL + if (strtolower($name) === 'administrator') { + $user = Factory::getApplication()->getIdentity(); + $url = Route::_('index.php?option=' . $option . '&task=' . $view . '.' . $layout . '&id=' . $user->id, false); + } + + $this->redirect($url); + } + } + } + + /** + * Gets a configuration value. + * + * @param string $varname The name of the value to get. + * @param string $default Default value to return + * + * @return mixed The user state. + * + * @since 3.2 + * @deprecated 5.0 Use get() instead + */ + public function getCfg($varname, $default = null) + { + try { + Log::add( + sprintf('%s() is deprecated and will be removed in 5.0. Use JFactory->getApplication()->get() instead.', __METHOD__), + Log::WARNING, + 'deprecated' + ); + } catch (\RuntimeException $exception) { + // Informational log only + } + + return $this->get($varname, $default); + } + + /** + * Gets the client id of the current running application. + * + * @return integer A client identifier. + * + * @since 3.2 + */ + public function getClientId() + { + return $this->clientId; + } + + /** + * Returns a reference to the global CmsApplication object, only creating it if it doesn't already exist. + * + * This method must be invoked as: $web = CmsApplication::getInstance(); + * + * @param string $name The name (optional) of the CmsApplication class to instantiate. + * @param string $prefix The class name prefix of the object. + * @param Container $container An optional dependency injection container to inject into the application. + * + * @return CmsApplication + * + * @since 3.2 + * @throws \RuntimeException + * @deprecated 5.0 Use \Joomla\CMS\Factory::getContainer()->get($name) instead + */ + public static function getInstance($name = null, $prefix = '\JApplication', Container $container = null) + { + if (empty(static::$instances[$name])) { + // Create a CmsApplication object. + $classname = $prefix . ucfirst($name); + + if (!$container) { + $container = Factory::getContainer(); + } + + if ($container->has($classname)) { + static::$instances[$name] = $container->get($classname); + } elseif (class_exists($classname)) { + // @todo This creates an implicit hard requirement on the ApplicationCms constructor + static::$instances[$name] = new $classname(null, null, null, $container); + } else { + throw new \RuntimeException(Text::sprintf('JLIB_APPLICATION_ERROR_APPLICATION_LOAD', $name), 500); + } + + static::$instances[$name]->loadIdentity(Factory::getUser()); + } + + return static::$instances[$name]; + } + + /** + * Returns the application \JMenu object. + * + * @param string $name The name of the application/client. + * @param array $options An optional associative array of configuration settings. + * + * @return AbstractMenu + * + * @since 3.2 + */ + public function getMenu($name = null, $options = array()) + { + if (!isset($name)) { + $name = $this->getName(); + } + + // Inject this application object into the \JMenu tree if one isn't already specified + if (!isset($options['app'])) { + $options['app'] = $this; + } + + if (array_key_exists($name, $this->menus)) { + return $this->menus[$name]; + } + + if ($this->menuFactory === null) { + @trigger_error('Menu factory must be set in 5.0', E_USER_DEPRECATED); + $this->menuFactory = $this->getContainer()->get(MenuFactoryInterface::class); + } + + $this->menus[$name] = $this->menuFactory->createMenu($name, $options); + + // Make sure the abstract menu has the instance too, is needed for BC and will be removed with version 5 + AbstractMenu::$instances[$name] = $this->menus[$name]; + + return $this->menus[$name]; + } + + /** + * Get the system message queue. + * + * @param boolean $clear Clear the messages currently attached to the application object + * + * @return array The system message queue. + * + * @since 3.2 + */ + public function getMessageQueue($clear = false) + { + // For empty queue, if messages exists in the session, enqueue them. + if (!\count($this->messageQueue)) { + $sessionQueue = $this->getSession()->get('application.queue', []); + + if ($sessionQueue) { + $this->messageQueue = $sessionQueue; + $this->getSession()->set('application.queue', []); + } + } + + $messageQueue = $this->messageQueue; + + if ($clear) { + $this->messageQueue = array(); + } + + return $messageQueue; + } + + /** + * Gets the name of the current running application. + * + * @return string The name of the application. + * + * @since 3.2 + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the application Pathway object. + * + * @return Pathway + * + * @since 3.2 + */ + public function getPathway() + { + if (!$this->pathway) { + $resourceName = ucfirst($this->getName()) . 'Pathway'; + + if (!$this->getContainer()->has($resourceName)) { + throw new \RuntimeException( + Text::sprintf('JLIB_APPLICATION_ERROR_PATHWAY_LOAD', $this->getName()), + 500 + ); + } + + $this->pathway = $this->getContainer()->get($resourceName); + } + + return $this->pathway; + } + + /** + * Returns the application Router object. + * + * @param string $name The name of the application. + * @param array $options An optional associative array of configuration settings. + * + * @return Router + * + * @since 3.2 + * + * @deprecated 5.0 Inject the router or load it from the dependency injection container + */ + public static function getRouter($name = null, array $options = array()) + { + $app = Factory::getApplication(); + + if (!isset($name)) { + $name = $app->getName(); + } + + $options['mode'] = $app->get('sef'); + + return Router::getInstance($name, $options); + } + + /** + * Gets the name of the current template. + * + * @param boolean $params An optional associative array of configuration settings + * + * @return mixed System is the fallback. + * + * @since 3.2 + */ + public function getTemplate($params = false) + { + if ($params) { + $template = new \stdClass(); + + $template->template = 'system'; + $template->params = new Registry(); + $template->inheritable = 0; + $template->parent = ''; + + return $template; + } + + return 'system'; + } + + /** + * Gets a user state. + * + * @param string $key The path of the state. + * @param mixed $default Optional default value, returned if the internal value is null. + * + * @return mixed The user state or null. + * + * @since 3.2 + */ + public function getUserState($key, $default = null) + { + $registry = $this->getSession()->get('registry'); + + if ($registry !== null) { + return $registry->get($key, $default); + } + + return $default; + } + + /** + * Gets the value of a user state variable. + * + * @param string $key The key of the user state variable. + * @param string $request The name of the variable passed in a request. + * @param string $default The default value for the variable if not found. Optional. + * @param string $type Filter for the variable, for valid values see {@link InputFilter::clean()}. Optional. + * + * @return mixed The request user state. + * + * @since 3.2 + */ + public function getUserStateFromRequest($key, $request, $default = null, $type = 'none') + { + $cur_state = $this->getUserState($key, $default); + $new_state = $this->input->get($request, null, $type); + + if ($new_state === null) { + return $cur_state; + } + + // Save the new value only if it was set in this request. + $this->setUserState($key, $new_state); + + return $new_state; + } + + /** + * Initialise the application. + * + * @param array $options An optional associative array of configuration settings. + * + * @return void + * + * @since 3.2 + */ + protected function initialiseApp($options = array()) + { + // Check that we were given a language in the array (since by default may be blank). + if (isset($options['language'])) { + $this->set('language', $options['language']); + } + + // Build our language object + $lang = Language::getInstance($this->get('language'), $this->get('debug_lang')); + + // Load the language to the API + $this->loadLanguage($lang); + + // Register the language object with Factory + Factory::$language = $this->getLanguage(); + + // Load the library language files + $this->loadLibraryLanguage(); + + // Set user specific editor. + $user = Factory::getUser(); + $editor = $user->getParam('editor', $this->get('editor')); + + if (!PluginHelper::isEnabled('editors', $editor)) { + $editor = $this->get('editor'); + + if (!PluginHelper::isEnabled('editors', $editor)) { + $editor = 'none'; + } + } + + $this->set('editor', $editor); + + // Load the behaviour plugins + PluginHelper::importPlugin('behaviour'); + + // Trigger the onAfterInitialise event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterInitialise'); + } + + /** + * Checks if HTTPS is forced in the client configuration. + * + * @param integer $clientId An optional client id (defaults to current application client). + * + * @return boolean True if is forced for the client, false otherwise. + * + * @since 3.7.3 + */ + public function isHttpsForced($clientId = null) + { + $clientId = (int) ($clientId !== null ? $clientId : $this->getClientId()); + $forceSsl = (int) $this->get('force_ssl'); + + if ($clientId === 0 && $forceSsl === 2) { + return true; + } + + if ($clientId === 1 && $forceSsl >= 1) { + return true; + } + + return false; + } + + /** + * Check the client interface by name. + * + * @param string $identifier String identifier for the application interface + * + * @return boolean True if this application is of the given type client interface. + * + * @since 3.7.0 + */ + public function isClient($identifier) + { + return $this->getName() === $identifier; + } + + /** + * Load the library language files for the application + * + * @return void + * + * @since 3.6.3 + */ + protected function loadLibraryLanguage() + { + $this->getLanguage()->load('lib_joomla', JPATH_ADMINISTRATOR); + } + + /** + * Login authentication function. + * + * Username and encoded password are passed the onUserLogin event which + * is responsible for the user validation. A successful validation updates + * the current session record with the user's details. + * + * Username and encoded password are sent as credentials (along with other + * possibilities) to each observer (authentication plugin) for user + * validation. Successful validation will update the current session with + * the user details. + * + * @param array $credentials Array('username' => string, 'password' => string) + * @param array $options Array('remember' => boolean) + * + * @return boolean|\Exception True on success, false if failed or silent handling is configured, or a \Exception object on authentication error. + * + * @since 3.2 + */ + public function login($credentials, $options = array()) + { + // Get the global Authentication object. + $authenticate = Authentication::getInstance($this->authenticationPluginType); + $response = $authenticate->authenticate($credentials, $options); + + // Import the user plugin group. + PluginHelper::importPlugin('user'); + + if ($response->status === Authentication::STATUS_SUCCESS) { + /* + * Validate that the user should be able to login (different to being authenticated). + * This permits authentication plugins blocking the user. + */ + $authorisations = $authenticate->authorise($response, $options); + $denied_states = Authentication::STATUS_EXPIRED | Authentication::STATUS_DENIED; + + foreach ($authorisations as $authorisation) { + if ((int) $authorisation->status & $denied_states) { + // Trigger onUserAuthorisationFailure Event. + $this->triggerEvent('onUserAuthorisationFailure', array((array) $authorisation)); + + // If silent is set, just return false. + if (isset($options['silent']) && $options['silent']) { + return false; + } + + // Return the error. + switch ($authorisation->status) { + case Authentication::STATUS_EXPIRED: + Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_EXPIRED'), 'error'); + + return false; + + case Authentication::STATUS_DENIED: + Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_DENIED'), 'error'); + + return false; + + default: + Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_AUTHORISATION'), 'error'); + + return false; + } + } + } + + // OK, the credentials are authenticated and user is authorised. Let's fire the onLogin event. + $results = $this->triggerEvent('onUserLogin', array((array) $response, $options)); + + /* + * If any of the user plugins did not successfully complete the login routine + * then the whole method fails. + * + * Any errors raised should be done in the plugin as this provides the ability + * to provide much more information about why the routine may have failed. + */ + $user = Factory::getUser(); + + if ($response->type === 'Cookie') { + $user->set('cookieLogin', true); + } + + if (\in_array(false, $results, true) == false) { + $options['user'] = $user; + $options['responseType'] = $response->type; + + // The user is successfully logged in. Run the after login events + $this->triggerEvent('onUserAfterLogin', array($options)); + + return true; + } + } + + // Trigger onUserLoginFailure Event. + $this->triggerEvent('onUserLoginFailure', array((array) $response)); + + // If silent is set, just return false. + if (isset($options['silent']) && $options['silent']) { + return false; + } + + // If status is success, any error will have been raised by the user plugin + if ($response->status !== Authentication::STATUS_SUCCESS) { + $this->getLogger()->warning($response->error_message, array('category' => 'jerror')); + } + + return false; + } + + /** + * Logout authentication function. + * + * Passed the current user information to the onUserLogout event and reverts the current + * session record back to 'anonymous' parameters. + * If any of the authentication plugins did not successfully complete + * the logout routine then the whole method fails. Any errors raised + * should be done in the plugin as this provides the ability to give + * much more information about why the routine may have failed. + * + * @param integer $userid The user to load - Can be an integer or string - If string, it is converted to ID automatically + * @param array $options Array('clientid' => array of client id's) + * + * @return boolean True on success + * + * @since 3.2 + */ + public function logout($userid = null, $options = array()) + { + // Get a user object from the Application. + $user = Factory::getUser($userid); + + // Build the credentials array. + $parameters['username'] = $user->get('username'); + $parameters['id'] = $user->get('id'); + + // Set clientid in the options array if it hasn't been set already and shared sessions are not enabled. + if (!$this->get('shared_session', '0') && !isset($options['clientid'])) { + $options['clientid'] = $this->getClientId(); + } + + // Import the user plugin group. + PluginHelper::importPlugin('user'); + + // OK, the credentials are built. Lets fire the onLogout event. + $results = $this->triggerEvent('onUserLogout', array($parameters, $options)); + + // Check if any of the plugins failed. If none did, success. + if (!\in_array(false, $results, true)) { + $options['username'] = $user->get('username'); + $this->triggerEvent('onUserAfterLogout', array($options)); + + return true; + } + + // Trigger onUserLogoutFailure Event. + $this->triggerEvent('onUserLogoutFailure', array($parameters)); + + return false; + } + + /** + * Redirect to another URL. + * + * If the headers have not been sent the redirect will be accomplished using a "301 Moved Permanently" + * or "303 See Other" code in the header pointing to the new location. If the headers have already been + * sent this will be accomplished using a JavaScript statement. + * + * @param string $url The URL to redirect to. Can only be http/https URL + * @param integer $status The HTTP 1.1 status code to be provided. 303 is assumed by default. + * + * @return void + * + * @since 3.2 + */ + public function redirect($url, $status = 303) + { + // Persist messages if they exist. + if (\count($this->messageQueue)) { + $this->getSession()->set('application.queue', $this->messageQueue); + } + + // Hand over processing to the parent now + parent::redirect($url, $status); + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 3.2 + */ + protected function render() + { + // Setup the document options. + $this->docOptions['template'] = $this->get('theme'); + $this->docOptions['file'] = $this->get('themeFile', 'index.php'); + $this->docOptions['params'] = $this->get('themeParams'); + $this->docOptions['csp_nonce'] = $this->get('csp_nonce'); + $this->docOptions['templateInherits'] = $this->get('themeInherits'); + + if ($this->get('themes.base')) { + $this->docOptions['directory'] = $this->get('themes.base'); + } + // Fall back to constants. + else { + $this->docOptions['directory'] = \defined('JPATH_THEMES') ? JPATH_THEMES : (\defined('JPATH_BASE') ? JPATH_BASE : __DIR__) . '/themes'; + } + + // Parse the document. + $this->document->parse($this->docOptions); + + // Trigger the onBeforeRender event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onBeforeRender'); + + $caching = false; + + if ($this->isClient('site') && $this->get('caching') && $this->get('caching', 2) == 2 && !Factory::getUser()->get('id')) { + $caching = true; + } + + // Render the document. + $data = $this->document->render($caching, $this->docOptions); + + // Set the application output data. + $this->setBody($data); + + // Trigger the onAfterRender event. + $this->triggerEvent('onAfterRender'); + + // Mark afterRender in the profiler. + JDEBUG ? $this->profiler->mark('afterRender') : null; + } + + /** + * Route the application. + * + * Routing is the process of examining the request environment to determine which + * component should receive the request. The component optional parameters + * are then set in the request object to be processed when the application is being + * dispatched. + * + * @return void + * + * @since 3.2 + * + * @deprecated 5.0 Implement the route functionality in the extending class, this here will be removed without replacement + */ + protected function route() + { + // Get the full request URI. + $uri = clone Uri::getInstance(); + + $router = static::getRouter(); + $result = $router->parse($uri, true); + + $active = $this->getMenu()->getActive(); + + if ( + $active !== null + && $active->type === 'alias' + && $active->getParams()->get('alias_redirect') + && \in_array($this->input->getMethod(), array('GET', 'HEAD'), true) + ) { + $item = $this->getMenu()->getItem($active->getParams()->get('aliasoptions')); + + if ($item !== null) { + $oldUri = clone Uri::getInstance(); + + if ($oldUri->getVar('Itemid') == $active->id) { + $oldUri->setVar('Itemid', $item->id); + } + + $base = Uri::base(true); + $oldPath = StringHelper::strtolower(substr($oldUri->getPath(), \strlen($base) + 1)); + $activePathPrefix = StringHelper::strtolower($active->route); + + $position = strpos($oldPath, $activePathPrefix); + + if ($position !== false) { + $oldUri->setPath($base . '/' . substr_replace($oldPath, $item->route, $position, \strlen($activePathPrefix))); + + $this->setHeader('Expires', 'Wed, 17 Aug 2005 00:00:00 GMT', true); + $this->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true); + $this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate', false); + $this->sendHeaders(); + + $this->redirect((string) $oldUri, 301); + } + } + } + + foreach ($result as $key => $value) { + $this->input->def($key, $value); + } + + // Trigger the onAfterRoute event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterRoute'); + } + + /** + * Sets the value of a user state variable. + * + * @param string $key The path of the state. + * @param mixed $value The value of the variable. + * + * @return mixed|void The previous state, if one existed. + * + * @since 3.2 + */ + public function setUserState($key, $value) + { + $session = $this->getSession(); + $registry = $session->get('registry'); + + if ($registry !== null) { + return $registry->set($key, $value); + } + } + + /** + * Sends all headers prior to returning the string + * + * @param boolean $compress If true, compress the data + * + * @return string + * + * @since 3.2 + */ + public function toString($compress = false) + { + // Don't compress something if the server is going to do it anyway. Waste of time. + if ($compress && !ini_get('zlib.output_compression') && ini_get('output_handler') !== 'ob_gzhandler') { + $this->compress(); + } + + if ($this->allowCache() === false) { + $this->setHeader('Cache-Control', 'no-cache', false); + } + + $this->sendHeaders(); + + return $this->getBody(); + } + + /** + * Method to determine a hash for anti-spoofing variable names + * + * @param boolean $forceNew If true, force a new token to be created + * + * @return string Hashed var name + * + * @since 4.0.0 + */ + public function getFormToken($forceNew = false) + { + /** @var Session $session */ + $session = $this->getSession(); + + return $session->getFormToken($forceNew); + } + + /** + * Checks for a form token in the request. + * + * Use in conjunction with getFormToken. + * + * @param string $method The request method in which to look for the token key. + * + * @return boolean True if found and valid, false otherwise. + * + * @since 4.0.0 + */ + public function checkToken($method = 'post') + { + /** @var Session $session */ + $session = $this->getSession(); + + return $session->checkToken($method); + } + + /** + * Flag if the application instance is a CLI or web based application. + * + * Helper function, you should use the native PHP functions to detect if it is a CLI application. + * + * @return boolean + * + * @since 4.0.0 + * @deprecated 5.0 Will be removed without replacements + */ + public function isCli() + { + return false; + } + + /** + * No longer used + * + * @return boolean + * + * @since 4.0.0 + * + * @throws \Exception + * @deprecated 4.2.0 Will be removed in 5.0 without replacement. + */ + protected function isTwoFactorAuthenticationRequired(): bool + { + return false; + } + + /** + * No longer used + * + * @return boolean + * + * @since 4.0.0 + * + * @throws \Exception + * @deprecated 4.2.0 Will be removed in 5.0 without replacement. + */ + private function hasUserConfiguredTwoFactorAuthentication(): bool + { + return false; + } + + /** + * Setup logging functionality. + * + * @return void + * + * @since 4.0.0 + */ + private function setupLogging(): void + { + // Add InMemory logger that will collect all log entries to allow to display them later by extensions + if ($this->get('debug')) { + Log::addLogger(['logger' => 'inmemory']); + } + + // Log the deprecated API. + if ($this->get('log_deprecated')) { + Log::addLogger(['text_file' => 'deprecated.php'], Log::ALL, ['deprecated']); + } + + // We only log errors unless Site Debug is enabled + $logLevels = Log::ERROR | Log::CRITICAL | Log::ALERT | Log::EMERGENCY; + + if ($this->get('debug')) { + $logLevels = Log::ALL; + } + + Log::addLogger(['text_file' => 'joomla_core_errors.php'], $logLevels, ['system']); + + // Log everything (except deprecated APIs, these are logged separately with the option above). + if ($this->get('log_everything')) { + Log::addLogger(['text_file' => 'everything.php'], Log::ALL, ['deprecated', 'deprecation-notes', 'databasequery'], true); + } + + if ($this->get('log_categories')) { + $priority = 0; + + foreach ($this->get('log_priorities', ['all']) as $p) { + $const = '\\Joomla\\CMS\\Log\\Log::' . strtoupper($p); + + if (defined($const)) { + $priority |= constant($const); + } + } + + // Split into an array at any character other than alphabet, numbers, _, ., or - + $categories = preg_split('/[^\w.-]+/', $this->get('log_categories', ''), -1, PREG_SPLIT_NO_EMPTY); + $mode = (bool) $this->get('log_category_mode', false); + + if (!$categories) { + return; + } + + Log::addLogger(['text_file' => 'custom-logging.php'], $priority, $categories, $mode); + } + } + + /** + * Sets the internal menu factory. + * + * @param MenuFactoryInterface $menuFactory The menu factory + * + * @return void + * + * @since 4.2.0 + */ + public function setMenuFactory(MenuFactoryInterface $menuFactory): void + { + $this->menuFactory = $menuFactory; + } } diff --git a/libraries/src/Application/CMSApplicationInterface.php b/libraries/src/Application/CMSApplicationInterface.php index ebab368d8596e..5a68ebdf4e07b 100644 --- a/libraries/src/Application/CMSApplicationInterface.php +++ b/libraries/src/Application/CMSApplicationInterface.php @@ -1,4 +1,5 @@ close(); - } - - $container = $container ?: Factory::getContainer(); - $this->setContainer($container); - $this->setDispatcher($dispatcher ?: $container->get(\Joomla\Event\DispatcherInterface::class)); - - if (!$container->has('session')) - { - $container->alias('session', 'session.cli') - ->alias('JSession', 'session.cli') - ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); - } - - $this->input = new \Joomla\CMS\Input\Cli; - $this->language = Factory::getLanguage(); - $this->output = $output ?: new Stdout; - $this->cliInput = $cliInput ?: new CliInput; - - parent::__construct($config); - - // Set the current directory. - $this->set('cwd', getcwd()); - - // Set up the environment - $this->input->set('format', 'cli'); - } - - /** - * Magic method to access properties of the application. - * - * @param string $name The name of the property. - * - * @return mixed A value if the property name is valid, null otherwise. - * - * @since 4.0.0 - * @deprecated 5.0 This is a B/C proxy for deprecated read accesses - */ - public function __get($name) - { - switch ($name) - { - case 'input': - @trigger_error( - 'Accessing the input property of the application is deprecated, use the getInput() method instead.', - E_USER_DEPRECATED - ); - - return $this->getInput(); - - default: - $trace = debug_backtrace(); - trigger_error( - sprintf( - 'Undefined property via __get(): %1$s in %2$s on line %3$s', - $name, - $trace[0]['file'], - $trace[0]['line'] - ), - E_USER_NOTICE - ); - } - } - - /** - * Method to get the application input object. - * - * @return Input - * - * @since 4.0.0 - */ - public function getInput(): Input - { - return $this->input; - } - - /** - * Method to get the application language object. - * - * @return Language The language object - * - * @since 4.0.0 - */ - public function getLanguage() - { - return $this->language; - } - - /** - * Returns a reference to the global CliApplication object, only creating it if it doesn't already exist. - * - * This method must be invoked as: $cli = CliApplication::getInstance(); - * - * @param string $name The name (optional) of the Application Cli class to instantiate. - * - * @return CliApplication - * - * @since 1.7.0 - * @deprecated 5.0 Load the app through the container - * @throws \RuntimeException - */ - public static function getInstance($name = null) - { - // Only create the object if it doesn't exist. - if (empty(static::$instance)) - { - if (!class_exists($name)) - { - throw new \RuntimeException(sprintf('Unable to load application: %s', $name), 500); - } - - static::$instance = new $name; - } - - return static::$instance; - } - - /** - * Execute the application. - * - * @return void - * - * @since 1.7.0 - */ - public function execute() - { - $this->createExtensionNamespaceMap(); - - // Trigger the onBeforeExecute event - $this->triggerEvent('onBeforeExecute'); - - // Perform application routines. - $this->doExecute(); - - // Trigger the onAfterExecute event. - $this->triggerEvent('onAfterExecute'); - } - - /** - * Get an output object. - * - * @return CliOutput - * - * @since 4.0.0 - */ - public function getOutput() - { - return $this->output; - } - - /** - * Get a CLI input object. - * - * @return CliInput - * - * @since 4.0.0 - */ - public function getCliInput() - { - return $this->cliInput; - } - - /** - * Write a string to standard output. - * - * @param string $text The text to display. - * @param boolean $nl True (default) to append a new line at the end of the output string. - * - * @return $this - * - * @since 4.0.0 - */ - public function out($text = '', $nl = true) - { - $this->getOutput()->out($text, $nl); - - return $this; - } - - /** - * Get a value from standard input. - * - * @return string The input string from standard input. - * - * @codeCoverageIgnore - * @since 4.0.0 - */ - public function in() - { - return $this->getCliInput()->in(); - } - - /** - * Set an output object. - * - * @param CliOutput $output CliOutput object - * - * @return $this - * - * @since 3.3 - */ - public function setOutput(CliOutput $output) - { - $this->output = $output; - - return $this; - } - - /** - * Enqueue a system message. - * - * @param string $msg The message to enqueue. - * @param string $type The message type. - * - * @return void - * - * @since 4.0.0 - */ - public function enqueueMessage($msg, $type = self::MSG_INFO) - { - if (!\array_key_exists($type, $this->messages)) - { - $this->messages[$type] = []; - } - - $this->messages[$type][] = $msg; - } - - /** - * Get the system message queue. - * - * @return array The system message queue. - * - * @since 4.0.0 - */ - public function getMessageQueue() - { - return $this->messages; - } - - /** - * Check the client interface by name. - * - * @param string $identifier String identifier for the application interface - * - * @return boolean True if this application is of the given type client interface. - * - * @since 4.0.0 - */ - public function isClient($identifier) - { - return $identifier === 'cli'; - } - - /** - * Method to get the application session object. - * - * @return SessionInterface The session object - * - * @since 4.0.0 - */ - public function getSession() - { - return $this->container->get(SessionInterface::class); - } - - /** - * Retrieve the application configuration object. - * - * @return Registry - * - * @since 4.0.0 - */ - public function getConfig() - { - return $this->config; - } - - /** - * Flag if the application instance is a CLI or web based application. - * - * Helper function, you should use the native PHP functions to detect if it is a CLI application. - * - * @return boolean - * - * @since 4.0.0 - * @deprecated 5.0 Will be removed without replacements - */ - public function isCli() - { - return true; - } + use DispatcherAwareTrait; + use EventAware; + use IdentityAware; + use ContainerAwareTrait; + use ExtensionManagerTrait; + use ExtensionNamespaceMapper; + + /** + * Output object + * + * @var CliOutput + * @since 4.0.0 + */ + protected $output; + + /** + * The input. + * + * @var \Joomla\Input\Input + * @since 4.0.0 + */ + protected $input = null; + + /** + * CLI Input object + * + * @var CliInput + * @since 4.0.0 + */ + protected $cliInput; + + /** + * The application language object. + * + * @var Language + * @since 4.0.0 + */ + protected $language; + + /** + * The application message queue. + * + * @var array + * @since 4.0.0 + */ + protected $messages = []; + + /** + * The application instance. + * + * @var CliApplication + * @since 1.7.0 + */ + protected static $instance; + + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's + * input object. If the argument is a JInputCli object that object will become + * the application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's + * config object. If the argument is a Registry object that object will become + * the application's config object, otherwise a default config object is created. + * @param CliOutput $output The output handler. + * @param CliInput $cliInput The CLI input handler. + * @param DispatcherInterface $dispatcher An optional argument to provide dependency injection for the application's + * event dispatcher. If the argument is a DispatcherInterface object that object will become + * the application's event dispatcher, if it is null then the default event dispatcher + * will be created based on the application's loadDispatcher() method. + * @param Container $container Dependency injection container. + * + * @since 1.7.0 + */ + public function __construct( + Input $input = null, + Registry $config = null, + CliOutput $output = null, + CliInput $cliInput = null, + DispatcherInterface $dispatcher = null, + Container $container = null + ) { + // Close the application if we are not executed from the command line. + if (!\defined('STDOUT') || !\defined('STDIN') || !isset($_SERVER['argv'])) { + $this->close(); + } + + $container = $container ?: Factory::getContainer(); + $this->setContainer($container); + $this->setDispatcher($dispatcher ?: $container->get(\Joomla\Event\DispatcherInterface::class)); + + if (!$container->has('session')) { + $container->alias('session', 'session.cli') + ->alias('JSession', 'session.cli') + ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); + } + + $this->input = new \Joomla\CMS\Input\Cli(); + $this->language = Factory::getLanguage(); + $this->output = $output ?: new Stdout(); + $this->cliInput = $cliInput ?: new CliInput(); + + parent::__construct($config); + + // Set the current directory. + $this->set('cwd', getcwd()); + + // Set up the environment + $this->input->set('format', 'cli'); + } + + /** + * Magic method to access properties of the application. + * + * @param string $name The name of the property. + * + * @return mixed A value if the property name is valid, null otherwise. + * + * @since 4.0.0 + * @deprecated 5.0 This is a B/C proxy for deprecated read accesses + */ + public function __get($name) + { + switch ($name) { + case 'input': + @trigger_error( + 'Accessing the input property of the application is deprecated, use the getInput() method instead.', + E_USER_DEPRECATED + ); + + return $this->getInput(); + + default: + $trace = debug_backtrace(); + trigger_error( + sprintf( + 'Undefined property via __get(): %1$s in %2$s on line %3$s', + $name, + $trace[0]['file'], + $trace[0]['line'] + ), + E_USER_NOTICE + ); + } + } + + /** + * Method to get the application input object. + * + * @return Input + * + * @since 4.0.0 + */ + public function getInput(): Input + { + return $this->input; + } + + /** + * Method to get the application language object. + * + * @return Language The language object + * + * @since 4.0.0 + */ + public function getLanguage() + { + return $this->language; + } + + /** + * Returns a reference to the global CliApplication object, only creating it if it doesn't already exist. + * + * This method must be invoked as: $cli = CliApplication::getInstance(); + * + * @param string $name The name (optional) of the Application Cli class to instantiate. + * + * @return CliApplication + * + * @since 1.7.0 + * @deprecated 5.0 Load the app through the container + * @throws \RuntimeException + */ + public static function getInstance($name = null) + { + // Only create the object if it doesn't exist. + if (empty(static::$instance)) { + if (!class_exists($name)) { + throw new \RuntimeException(sprintf('Unable to load application: %s', $name), 500); + } + + static::$instance = new $name(); + } + + return static::$instance; + } + + /** + * Execute the application. + * + * @return void + * + * @since 1.7.0 + */ + public function execute() + { + $this->createExtensionNamespaceMap(); + + // Trigger the onBeforeExecute event + $this->triggerEvent('onBeforeExecute'); + + // Perform application routines. + $this->doExecute(); + + // Trigger the onAfterExecute event. + $this->triggerEvent('onAfterExecute'); + } + + /** + * Get an output object. + * + * @return CliOutput + * + * @since 4.0.0 + */ + public function getOutput() + { + return $this->output; + } + + /** + * Get a CLI input object. + * + * @return CliInput + * + * @since 4.0.0 + */ + public function getCliInput() + { + return $this->cliInput; + } + + /** + * Write a string to standard output. + * + * @param string $text The text to display. + * @param boolean $nl True (default) to append a new line at the end of the output string. + * + * @return $this + * + * @since 4.0.0 + */ + public function out($text = '', $nl = true) + { + $this->getOutput()->out($text, $nl); + + return $this; + } + + /** + * Get a value from standard input. + * + * @return string The input string from standard input. + * + * @codeCoverageIgnore + * @since 4.0.0 + */ + public function in() + { + return $this->getCliInput()->in(); + } + + /** + * Set an output object. + * + * @param CliOutput $output CliOutput object + * + * @return $this + * + * @since 3.3 + */ + public function setOutput(CliOutput $output) + { + $this->output = $output; + + return $this; + } + + /** + * Enqueue a system message. + * + * @param string $msg The message to enqueue. + * @param string $type The message type. + * + * @return void + * + * @since 4.0.0 + */ + public function enqueueMessage($msg, $type = self::MSG_INFO) + { + if (!\array_key_exists($type, $this->messages)) { + $this->messages[$type] = []; + } + + $this->messages[$type][] = $msg; + } + + /** + * Get the system message queue. + * + * @return array The system message queue. + * + * @since 4.0.0 + */ + public function getMessageQueue() + { + return $this->messages; + } + + /** + * Check the client interface by name. + * + * @param string $identifier String identifier for the application interface + * + * @return boolean True if this application is of the given type client interface. + * + * @since 4.0.0 + */ + public function isClient($identifier) + { + return $identifier === 'cli'; + } + + /** + * Method to get the application session object. + * + * @return SessionInterface The session object + * + * @since 4.0.0 + */ + public function getSession() + { + return $this->container->get(SessionInterface::class); + } + + /** + * Retrieve the application configuration object. + * + * @return Registry + * + * @since 4.0.0 + */ + public function getConfig() + { + return $this->config; + } + + /** + * Flag if the application instance is a CLI or web based application. + * + * Helper function, you should use the native PHP functions to detect if it is a CLI application. + * + * @return boolean + * + * @since 4.0.0 + * @deprecated 5.0 Will be removed without replacements + */ + public function isCli() + { + return true; + } } diff --git a/libraries/src/Application/ConsoleApplication.php b/libraries/src/Application/ConsoleApplication.php index 9961710d3a275..2dda91cce3221 100644 --- a/libraries/src/Application/ConsoleApplication.php +++ b/libraries/src/Application/ConsoleApplication.php @@ -1,4 +1,5 @@ close(); - } - - // Set up a Input object for Controllers etc to use - $this->input = new \Joomla\CMS\Input\Cli; - $this->language = $language; - - parent::__construct($input, $output, $config); - - $this->setVersion(JVERSION); - - // Register the client name as cli - $this->name = 'cli'; - - $this->setContainer($container); - $this->setDispatcher($dispatcher); - - // Set the execution datetime and timestamp; - $this->set('execution.datetime', gmdate('Y-m-d H:i:s')); - $this->set('execution.timestamp', time()); - $this->set('execution.microtimestamp', microtime(true)); - - // Set the current directory. - $this->set('cwd', getcwd()); - - // Set up the environment - $this->input->set('format', 'cli'); - } - - /** - * Magic method to access properties of the application. - * - * @param string $name The name of the property. - * - * @return mixed A value if the property name is valid, null otherwise. - * - * @since 4.0.0 - * @deprecated 5.0 This is a B/C proxy for deprecated read accesses - */ - public function __get($name) - { - switch ($name) - { - case 'input': - @trigger_error( - 'Accessing the input property of the application is deprecated, use the getInput() method instead.', - E_USER_DEPRECATED - ); - - return $this->getInput(); - - default: - $trace = debug_backtrace(); - trigger_error( - sprintf( - 'Undefined property via __get(): %1$s in %2$s on line %3$s', - $name, - $trace[0]['file'], - $trace[0]['line'] - ), - E_USER_NOTICE - ); - } - } - - /** - * Method to run the application routines. - * - * @return integer The exit code for the application - * - * @since 4.0.0 - * @throws \Throwable - */ - protected function doExecute(): int - { - $exitCode = parent::doExecute(); - - $style = new SymfonyStyle($this->getConsoleInput(), $this->getConsoleOutput()); - - $methodMap = [ - self::MSG_ALERT => 'error', - self::MSG_CRITICAL => 'caution', - self::MSG_DEBUG => 'comment', - self::MSG_EMERGENCY => 'caution', - self::MSG_ERROR => 'error', - self::MSG_INFO => 'note', - self::MSG_NOTICE => 'note', - self::MSG_WARNING => 'warning', - ]; - - // Output any enqueued messages before the app exits - foreach ($this->getMessageQueue() as $type => $messages) - { - $method = $methodMap[$type] ?? 'comment'; - - $style->$method($messages); - } - - return $exitCode; - } - - /** - * Execute the application. - * - * @return void - * - * @since 4.0.0 - * @throws \Throwable - */ - public function execute() - { - // Load extension namespaces - $this->createExtensionNamespaceMap(); - - // Import CMS plugin groups to be able to subscribe to events - PluginHelper::importPlugin('system'); - PluginHelper::importPlugin('console'); - - parent::execute(); - } - - /** - * Enqueue a system message. - * - * @param string $msg The message to enqueue. - * @param string $type The message type. - * - * @return void - * - * @since 4.0.0 - */ - public function enqueueMessage($msg, $type = self::MSG_INFO) - { - if (!array_key_exists($type, $this->messages)) - { - $this->messages[$type] = []; - } - - $this->messages[$type][] = $msg; - } - - /** - * Gets the name of the current running application. - * - * @return string The name of the application. - * - * @since 4.0.0 - */ - public function getName(): string - { - return $this->name; - } - - /** - * Get the commands which should be registered by default to the application. - * - * @return \Joomla\Console\Command\AbstractCommand[] - * - * @since 4.0.0 - */ - protected function getDefaultCommands(): array - { - return array_merge( - parent::getDefaultCommands(), - [ - new Console\CleanCacheCommand, - new Console\CheckUpdatesCommand, - new Console\RemoveOldFilesCommand, - new Console\AddUserCommand($this->getDatabase()), - new Console\AddUserToGroupCommand($this->getDatabase()), - new Console\RemoveUserFromGroupCommand($this->getDatabase()), - new Console\DeleteUserCommand($this->getDatabase()), - new Console\ChangeUserPasswordCommand, - new Console\ListUserCommand($this->getDatabase()), - ] - ); - } - - /** - * Retrieve the application configuration object. - * - * @return Registry - * - * @since 4.0.0 - */ - public function getConfig() - { - return $this->config; - } - - /** - * Method to get the application input object. - * - * @return Input - * - * @since 4.0.0 - */ - public function getInput(): Input - { - return $this->input; - } - - /** - * Method to get the application language object. - * - * @return Language The language object - * - * @since 4.0.0 - */ - public function getLanguage() - { - return $this->language; - } - - /** - * Get the system message queue. - * - * @return array The system message queue. - * - * @since 4.0.0 - */ - public function getMessageQueue() - { - return $this->messages; - } - - /** - * Method to get the application session object. - * - * @return SessionInterface The session object - * - * @since 4.0.0 - */ - public function getSession() - { - return $this->session; - } - - /** - * Check the client interface by name. - * - * @param string $identifier String identifier for the application interface - * - * @return boolean True if this application is of the given type client interface. - * - * @since 4.0.0 - */ - public function isClient($identifier) - { - return $this->getName() === $identifier; - } - - /** - * Flag if the application instance is a CLI or web based application. - * - * Helper function, you should use the native PHP functions to detect if it is a CLI application. - * - * @return boolean - * - * @since 4.0.0 - * @deprecated 5.0 Will be removed without replacements - */ - public function isCli() - { - return true; - } - - /** - * Sets the session for the application to use, if required. - * - * @param SessionInterface $session A session object. - * - * @return $this - * - * @since 4.0.0 - */ - public function setSession(SessionInterface $session): self - { - $this->session = $session; - - return $this; - } - - /** - * Flush the media version to refresh versionable assets - * - * @return void - * - * @since 4.0.0 - */ - public function flushAssets() - { - (new Version)->refreshMediaVersion(); - } - - /** - * Get the long version string for the application. - * - * Overrides the parent method due to conflicting use of the getName method between the console application and - * the CMS application interface. - * - * @return string - * - * @since 4.0.0 - */ - public function getLongVersion(): string - { - return sprintf('Joomla! %s (debug: %s)', (new Version)->getShortVersion(), (\defined('JDEBUG') && JDEBUG ? 'Yes' : 'No')); - } - - /** - * Set the name of the application. - * - * @param string $name The new application name. - * - * @return void - * - * @since 4.0.0 - * @throws \RuntimeException because the application name cannot be changed - */ - public function setName(string $name): void - { - throw new \RuntimeException('The console application name cannot be changed'); - } - - /** - * Returns the application Router object. - * - * @param string $name The name of the application. - * @param array $options An optional associative array of configuration settings. - * - * @return Router - * - * @since 4.0.6 - * - * @throws \InvalidArgumentException - * - * @deprecated 5.0 Inject the router or load it from the dependency injection container - */ - public static function getRouter($name = null, array $options = array()) - { - if (empty($name)) - { - throw new InvalidArgumentException('A router name must be set in console application.'); - } - - $options['mode'] = Factory::getApplication()->get('sef'); - - return Router::getInstance($name, $options); - } + use DispatcherAwareTrait; + use EventAware; + use IdentityAware; + use ContainerAwareTrait; + use ExtensionManagerTrait; + use ExtensionNamespaceMapper; + use DatabaseAwareTrait; + + /** + * The input. + * + * @var Input + * @since 4.0.0 + */ + protected $input = null; + + /** + * The name of the application. + * + * @var string + * @since 4.0.0 + */ + protected $name = null; + + /** + * The application language object. + * + * @var Language + * @since 4.0.0 + */ + protected $language; + + /** + * The application message queue. + * + * @var array + * @since 4.0.0 + */ + private $messages = []; + + /** + * The application session object. + * + * @var SessionInterface + * @since 4.0.0 + */ + private $session; + + /** + * Class constructor. + * + * @param Registry $config An optional argument to provide dependency injection for the application's config object. If the + * argument is a Registry object that object will become the application's config object, + * otherwise a default config object is created. + * @param DispatcherInterface $dispatcher An optional argument to provide dependency injection for the application's event dispatcher. If the + * argument is a DispatcherInterface object that object will become the application's event dispatcher, + * if it is null then the default event dispatcher will be created based on the application's + * loadDispatcher() method. + * @param Container $container Dependency injection container. + * @param Language $language The language object provisioned for the application. + * @param InputInterface|null $input An optional argument to provide dependency injection for the application's input object. If the + * argument is an InputInterface object that object will become the application's input object, + * otherwise a default input object is created. + * @param OutputInterface|null $output An optional argument to provide dependency injection for the application's output object. If the + * argument is an OutputInterface object that object will become the application's output object, + * otherwise a default output object is created. + * + * @since 4.0.0 + */ + public function __construct( + Registry $config, + DispatcherInterface $dispatcher, + Container $container, + Language $language, + ?InputInterface $input = null, + ?OutputInterface $output = null + ) { + // Close the application if it is not executed from the command line. + if (!\defined('STDOUT') || !\defined('STDIN') || !isset($_SERVER['argv'])) { + $this->close(); + } + + // Set up a Input object for Controllers etc to use + $this->input = new \Joomla\CMS\Input\Cli(); + $this->language = $language; + + parent::__construct($input, $output, $config); + + $this->setVersion(JVERSION); + + // Register the client name as cli + $this->name = 'cli'; + + $this->setContainer($container); + $this->setDispatcher($dispatcher); + + // Set the execution datetime and timestamp; + $this->set('execution.datetime', gmdate('Y-m-d H:i:s')); + $this->set('execution.timestamp', time()); + $this->set('execution.microtimestamp', microtime(true)); + + // Set the current directory. + $this->set('cwd', getcwd()); + + // Set up the environment + $this->input->set('format', 'cli'); + } + + /** + * Magic method to access properties of the application. + * + * @param string $name The name of the property. + * + * @return mixed A value if the property name is valid, null otherwise. + * + * @since 4.0.0 + * @deprecated 5.0 This is a B/C proxy for deprecated read accesses + */ + public function __get($name) + { + switch ($name) { + case 'input': + @trigger_error( + 'Accessing the input property of the application is deprecated, use the getInput() method instead.', + E_USER_DEPRECATED + ); + + return $this->getInput(); + + default: + $trace = debug_backtrace(); + trigger_error( + sprintf( + 'Undefined property via __get(): %1$s in %2$s on line %3$s', + $name, + $trace[0]['file'], + $trace[0]['line'] + ), + E_USER_NOTICE + ); + } + } + + /** + * Method to run the application routines. + * + * @return integer The exit code for the application + * + * @since 4.0.0 + * @throws \Throwable + */ + protected function doExecute(): int + { + $exitCode = parent::doExecute(); + + $style = new SymfonyStyle($this->getConsoleInput(), $this->getConsoleOutput()); + + $methodMap = [ + self::MSG_ALERT => 'error', + self::MSG_CRITICAL => 'caution', + self::MSG_DEBUG => 'comment', + self::MSG_EMERGENCY => 'caution', + self::MSG_ERROR => 'error', + self::MSG_INFO => 'note', + self::MSG_NOTICE => 'note', + self::MSG_WARNING => 'warning', + ]; + + // Output any enqueued messages before the app exits + foreach ($this->getMessageQueue() as $type => $messages) { + $method = $methodMap[$type] ?? 'comment'; + + $style->$method($messages); + } + + return $exitCode; + } + + /** + * Execute the application. + * + * @return void + * + * @since 4.0.0 + * @throws \Throwable + */ + public function execute() + { + // Load extension namespaces + $this->createExtensionNamespaceMap(); + + // Import CMS plugin groups to be able to subscribe to events + PluginHelper::importPlugin('system'); + PluginHelper::importPlugin('console'); + + parent::execute(); + } + + /** + * Enqueue a system message. + * + * @param string $msg The message to enqueue. + * @param string $type The message type. + * + * @return void + * + * @since 4.0.0 + */ + public function enqueueMessage($msg, $type = self::MSG_INFO) + { + if (!array_key_exists($type, $this->messages)) { + $this->messages[$type] = []; + } + + $this->messages[$type][] = $msg; + } + + /** + * Gets the name of the current running application. + * + * @return string The name of the application. + * + * @since 4.0.0 + */ + public function getName(): string + { + return $this->name; + } + + /** + * Get the commands which should be registered by default to the application. + * + * @return \Joomla\Console\Command\AbstractCommand[] + * + * @since 4.0.0 + */ + protected function getDefaultCommands(): array + { + return array_merge( + parent::getDefaultCommands(), + [ + new Console\CleanCacheCommand(), + new Console\CheckUpdatesCommand(), + new Console\RemoveOldFilesCommand(), + new Console\AddUserCommand($this->getDatabase()), + new Console\AddUserToGroupCommand($this->getDatabase()), + new Console\RemoveUserFromGroupCommand($this->getDatabase()), + new Console\DeleteUserCommand($this->getDatabase()), + new Console\ChangeUserPasswordCommand(), + new Console\ListUserCommand($this->getDatabase()), + ] + ); + } + + /** + * Retrieve the application configuration object. + * + * @return Registry + * + * @since 4.0.0 + */ + public function getConfig() + { + return $this->config; + } + + /** + * Method to get the application input object. + * + * @return Input + * + * @since 4.0.0 + */ + public function getInput(): Input + { + return $this->input; + } + + /** + * Method to get the application language object. + * + * @return Language The language object + * + * @since 4.0.0 + */ + public function getLanguage() + { + return $this->language; + } + + /** + * Get the system message queue. + * + * @return array The system message queue. + * + * @since 4.0.0 + */ + public function getMessageQueue() + { + return $this->messages; + } + + /** + * Method to get the application session object. + * + * @return SessionInterface The session object + * + * @since 4.0.0 + */ + public function getSession() + { + return $this->session; + } + + /** + * Check the client interface by name. + * + * @param string $identifier String identifier for the application interface + * + * @return boolean True if this application is of the given type client interface. + * + * @since 4.0.0 + */ + public function isClient($identifier) + { + return $this->getName() === $identifier; + } + + /** + * Flag if the application instance is a CLI or web based application. + * + * Helper function, you should use the native PHP functions to detect if it is a CLI application. + * + * @return boolean + * + * @since 4.0.0 + * @deprecated 5.0 Will be removed without replacements + */ + public function isCli() + { + return true; + } + + /** + * Sets the session for the application to use, if required. + * + * @param SessionInterface $session A session object. + * + * @return $this + * + * @since 4.0.0 + */ + public function setSession(SessionInterface $session): self + { + $this->session = $session; + + return $this; + } + + /** + * Flush the media version to refresh versionable assets + * + * @return void + * + * @since 4.0.0 + */ + public function flushAssets() + { + (new Version())->refreshMediaVersion(); + } + + /** + * Get the long version string for the application. + * + * Overrides the parent method due to conflicting use of the getName method between the console application and + * the CMS application interface. + * + * @return string + * + * @since 4.0.0 + */ + public function getLongVersion(): string + { + return sprintf('Joomla! %s (debug: %s)', (new Version())->getShortVersion(), (\defined('JDEBUG') && JDEBUG ? 'Yes' : 'No')); + } + + /** + * Set the name of the application. + * + * @param string $name The new application name. + * + * @return void + * + * @since 4.0.0 + * @throws \RuntimeException because the application name cannot be changed + */ + public function setName(string $name): void + { + throw new \RuntimeException('The console application name cannot be changed'); + } + + /** + * Returns the application Router object. + * + * @param string $name The name of the application. + * @param array $options An optional associative array of configuration settings. + * + * @return Router + * + * @since 4.0.6 + * + * @throws \InvalidArgumentException + * + * @deprecated 5.0 Inject the router or load it from the dependency injection container + */ + public static function getRouter($name = null, array $options = array()) + { + if (empty($name)) { + throw new InvalidArgumentException('A router name must be set in console application.'); + } + + $options['mode'] = Factory::getApplication()->get('sef'); + + return Router::getInstance($name, $options); + } } diff --git a/libraries/src/Application/DaemonApplication.php b/libraries/src/Application/DaemonApplication.php index f56ff88a07504..f66c44f289ebc 100644 --- a/libraries/src/Application/DaemonApplication.php +++ b/libraries/src/Application/DaemonApplication.php @@ -1,4 +1,5 @@ config->get('max_execution_time', 0)); - - if ($this->config->get('max_memory_limit') !== null) - { - ini_set('memory_limit', $this->config->get('max_memory_limit', '256M')); - } - - // Flush content immediately. - ob_implicit_flush(); - } - - /** - * Method to handle POSIX signals. - * - * @param integer $signal The received POSIX signal. - * - * @return void - * - * @since 1.7.0 - * @see pcntl_signal() - * @throws \RuntimeException - */ - public static function signal($signal) - { - // Log all signals sent to the daemon. - Log::add('Received signal: ' . $signal, Log::DEBUG); - - // Let's make sure we have an application instance. - if (!is_subclass_of(static::$instance, CliApplication::class)) - { - Log::add('Cannot find the application instance.', Log::EMERGENCY); - throw new \RuntimeException('Cannot find the application instance.'); - } - - // Fire the onReceiveSignal event. - static::$instance->triggerEvent('onReceiveSignal', array($signal)); - - switch ($signal) - { - case SIGINT: - case SIGTERM: - // Handle shutdown tasks - if (static::$instance->running && static::$instance->isActive()) - { - static::$instance->shutdown(); - } - else - { - static::$instance->close(); - } - break; - case SIGHUP: - // Handle restart tasks - if (static::$instance->running && static::$instance->isActive()) - { - static::$instance->shutdown(true); - } - else - { - static::$instance->close(); - } - break; - case SIGCHLD: - // A child process has died - while (static::$instance->pcntlWait($signal, WNOHANG || WUNTRACED) > 0) - { - usleep(1000); - } - break; - case SIGCLD: - while (static::$instance->pcntlWait($signal, WNOHANG) > 0) - { - $signal = static::$instance->pcntlChildExitStatus($signal); - } - break; - default: - break; - } - } - - /** - * Check to see if the daemon is active. This does not assume that $this daemon is active, but - * only if an instance of the application is active as a daemon. - * - * @return boolean True if daemon is active. - * - * @since 1.7.0 - */ - public function isActive() - { - // Get the process id file location for the application. - $pidFile = $this->config->get('application_pid_file'); - - // If the process id file doesn't exist then the daemon is obviously not running. - if (!is_file($pidFile)) - { - return false; - } - - // Read the contents of the process id file as an integer. - $fp = fopen($pidFile, 'r'); - $pid = fread($fp, filesize($pidFile)); - $pid = (int) $pid; - fclose($fp); - - // Check to make sure that the process id exists as a positive integer. - if (!$pid) - { - return false; - } - - // Check to make sure the process is active by pinging it and ensure it responds. - if (!posix_kill($pid, 0)) - { - // No response so remove the process id file and log the situation. - @ unlink($pidFile); - Log::add('The process found based on PID file was unresponsive.', Log::WARNING); - - return false; - } - - return true; - } - - /** - * Load an object or array into the application configuration object. - * - * @param mixed $data Either an array or object to be loaded into the configuration object. - * - * @return DaemonApplication Instance of $this to allow chaining. - * - * @since 1.7.0 - */ - public function loadConfiguration($data) - { - /* - * Setup some application metadata options. This is useful if we ever want to write out startup scripts - * or just have some sort of information available to share about things. - */ - - // The application author name. This string is used in generating startup scripts and has - // a maximum of 50 characters. - $tmp = (string) $this->config->get('author_name', 'Joomla Platform'); - $this->config->set('author_name', (\strlen($tmp) > 50) ? substr($tmp, 0, 50) : $tmp); - - // The application author email. This string is used in generating startup scripts. - $tmp = (string) $this->config->get('author_email', 'admin@joomla.org'); - $this->config->set('author_email', filter_var($tmp, FILTER_VALIDATE_EMAIL)); - - // The application name. This string is used in generating startup scripts. - $tmp = (string) $this->config->get('application_name', 'DaemonApplication'); - $this->config->set('application_name', (string) preg_replace('/[^A-Z0-9_-]/i', '', $tmp)); - - // The application description. This string is used in generating startup scripts. - $tmp = (string) $this->config->get('application_description', 'A generic Joomla Platform application.'); - $this->config->set('application_description', filter_var($tmp, FILTER_SANITIZE_STRING)); - - /* - * Setup the application path options. This defines the default executable name, executable directory, - * and also the path to the daemon process id file. - */ - - // The application executable daemon. This string is used in generating startup scripts. - $tmp = (string) $this->config->get('application_executable', basename($this->input->executable)); - $this->config->set('application_executable', $tmp); - - // The home directory of the daemon. - $tmp = (string) $this->config->get('application_directory', \dirname($this->input->executable)); - $this->config->set('application_directory', $tmp); - - // The pid file location. This defaults to a path inside the /tmp directory. - $name = $this->config->get('application_name'); - $tmp = (string) $this->config->get('application_pid_file', strtolower('/tmp/' . $name . '/' . $name . '.pid')); - $this->config->set('application_pid_file', $tmp); - - /* - * Setup the application identity options. It is important to remember if the default of 0 is set for - * either UID or GID then changing that setting will not be attempted as there is no real way to "change" - * the identity of a process from some user to root. - */ - - // The user id under which to run the daemon. - $tmp = (int) $this->config->get('application_uid', 0); - $options = array('options' => array('min_range' => 0, 'max_range' => 65000)); - $this->config->set('application_uid', filter_var($tmp, FILTER_VALIDATE_INT, $options)); - - // The group id under which to run the daemon. - $tmp = (int) $this->config->get('application_gid', 0); - $options = array('options' => array('min_range' => 0, 'max_range' => 65000)); - $this->config->set('application_gid', filter_var($tmp, FILTER_VALIDATE_INT, $options)); - - // Option to kill the daemon if it cannot switch to the chosen identity. - $tmp = (bool) $this->config->get('application_require_identity', 1); - $this->config->set('application_require_identity', $tmp); - - /* - * Setup the application runtime options. By default our execution time limit is infinite obviously - * because a daemon should be constantly running unless told otherwise. The default limit for memory - * usage is 256M, which admittedly is a little high, but remember it is a "limit" and PHP's memory - * management leaves a bit to be desired :-) - */ - - // The maximum execution time of the application in seconds. Zero is infinite. - $tmp = $this->config->get('max_execution_time'); - - if ($tmp !== null) - { - $this->config->set('max_execution_time', (int) $tmp); - } - - // The maximum amount of memory the application can use. - $tmp = $this->config->get('max_memory_limit', '256M'); - - if ($tmp !== null) - { - $this->config->set('max_memory_limit', (string) $tmp); - } - - return $this; - } - - /** - * Execute the daemon. - * - * @return void - * - * @since 1.7.0 - */ - public function execute() - { - // Trigger the onBeforeExecute event - $this->triggerEvent('onBeforeExecute'); - - // Enable basic garbage collection. - gc_enable(); - - Log::add('Starting ' . $this->name, Log::INFO); - - // Set off the process for becoming a daemon. - if ($this->daemonize()) - { - // Declare ticks to start signal monitoring. When you declare ticks, PCNTL will monitor - // incoming signals after each tick and call the relevant signal handler automatically. - declare (ticks = 1); - - // Start the main execution loop. - while (true) - { - // Perform basic garbage collection. - $this->gc(); - - // Don't completely overload the CPU. - usleep(1000); - - // Execute the main application logic. - $this->doExecute(); - } - } - // We were not able to daemonize the application so log the failure and die gracefully. - else - { - Log::add('Starting ' . $this->name . ' failed', Log::INFO); - } - - // Trigger the onAfterExecute event. - $this->triggerEvent('onAfterExecute'); - } - - /** - * Restart daemon process. - * - * @return void - * - * @since 1.7.0 - */ - public function restart() - { - Log::add('Stopping ' . $this->name, Log::INFO); - $this->shutdown(true); - } - - /** - * Stop daemon process. - * - * @return void - * - * @since 1.7.0 - */ - public function stop() - { - Log::add('Stopping ' . $this->name, Log::INFO); - $this->shutdown(); - } - - /** - * Method to change the identity of the daemon process and resources. - * - * @return boolean True if identity successfully changed - * - * @since 1.7.0 - * @see posix_setuid() - */ - protected function changeIdentity() - { - // Get the group and user ids to set for the daemon. - $uid = (int) $this->config->get('application_uid', 0); - $gid = (int) $this->config->get('application_gid', 0); - - // Get the application process id file path. - $file = $this->config->get('application_pid_file'); - - // Change the user id for the process id file if necessary. - if ($uid && (fileowner($file) != $uid) && (!@ chown($file, $uid))) - { - Log::add('Unable to change user ownership of the process id file.', Log::ERROR); - - return false; - } - - // Change the group id for the process id file if necessary. - if ($gid && (filegroup($file) != $gid) && (!@ chgrp($file, $gid))) - { - Log::add('Unable to change group ownership of the process id file.', Log::ERROR); - - return false; - } - - // Set the correct home directory for the process. - if ($uid && ($info = posix_getpwuid($uid)) && is_dir($info['dir'])) - { - system('export HOME="' . $info['dir'] . '"'); - } - - // Change the user id for the process necessary. - if ($uid && (posix_getuid() != $uid) && (!@ posix_setuid($uid))) - { - Log::add('Unable to change user ownership of the process.', Log::ERROR); - - return false; - } - - // Change the group id for the process necessary. - if ($gid && (posix_getgid() != $gid) && (!@ posix_setgid($gid))) - { - Log::add('Unable to change group ownership of the process.', Log::ERROR); - - return false; - } - - // Get the user and group information based on uid and gid. - $user = posix_getpwuid($uid); - $group = posix_getgrgid($gid); - - Log::add('Changed daemon identity to ' . $user['name'] . ':' . $group['name'], Log::INFO); - - return true; - } - - /** - * Method to put the application into the background. - * - * @return boolean - * - * @since 1.7.0 - * @throws \RuntimeException - */ - protected function daemonize() - { - // Is there already an active daemon running? - if ($this->isActive()) - { - Log::add($this->name . ' daemon is still running. Exiting the application.', Log::EMERGENCY); - - return false; - } - - // Reset Process Information - $this->safeMode = !!@ ini_get('safe_mode'); - $this->processId = 0; - $this->running = false; - - // Detach process! - try - { - // Check if we should run in the foreground. - if (!$this->input->get('f')) - { - // Detach from the terminal. - $this->detach(); - } - else - { - // Setup running values. - $this->exiting = false; - $this->running = true; - - // Set the process id. - $this->processId = (int) posix_getpid(); - $this->parentId = $this->processId; - } - } - catch (\RuntimeException $e) - { - Log::add('Unable to fork.', Log::EMERGENCY); - - return false; - } - - // Verify the process id is valid. - if ($this->processId < 1) - { - Log::add('The process id is invalid; the fork failed.', Log::EMERGENCY); - - return false; - } - - // Clear the umask. - @ umask(0); - - // Write out the process id file for concurrency management. - if (!$this->writeProcessIdFile()) - { - Log::add('Unable to write the pid file at: ' . $this->config->get('application_pid_file'), Log::EMERGENCY); - - return false; - } - - // Attempt to change the identity of user running the process. - if (!$this->changeIdentity()) - { - // If the identity change was required then we need to return false. - if ($this->config->get('application_require_identity')) - { - Log::add('Unable to change process owner.', Log::CRITICAL); - - return false; - } - else - { - Log::add('Unable to change process owner.', Log::WARNING); - } - } - - // Setup the signal handlers for the daemon. - if (!$this->setupSignalHandlers()) - { - return false; - } - - // Change the current working directory to the application working directory. - @ chdir($this->config->get('application_directory')); - - return true; - } - - /** - * This is truly where the magic happens. This is where we fork the process and kill the parent - * process, which is essentially what turns the application into a daemon. - * - * @return void - * - * @since 3.0.0 - * @throws \RuntimeException - */ - protected function detach() - { - Log::add('Detaching the ' . $this->name . ' daemon.', Log::DEBUG); - - // Attempt to fork the process. - $pid = $this->fork(); - - // If the pid is positive then we successfully forked, and can close this application. - if ($pid) - { - // Add the log entry for debugging purposes and exit gracefully. - Log::add('Ending ' . $this->name . ' parent process', Log::DEBUG); - $this->close(); - } - // We are in the forked child process. - else - { - // Setup some protected values. - $this->exiting = false; - $this->running = true; - - // Set the parent to self. - $this->parentId = $this->processId; - } - } - - /** - * Method to fork the process. - * - * @return integer The child process id to the parent process, zero to the child process. - * - * @since 1.7.0 - * @throws \RuntimeException - */ - protected function fork() - { - // Attempt to fork the process. - $pid = $this->pcntlFork(); - - // If the fork failed, throw an exception. - if ($pid === -1) - { - throw new \RuntimeException('The process could not be forked.'); - } - // Update the process id for the child. - elseif ($pid === 0) - { - $this->processId = (int) posix_getpid(); - } - // Log the fork in the parent. - else - { - // Log the fork. - Log::add('Process forked ' . $pid, Log::DEBUG); - } - - // Trigger the onFork event. - $this->postFork(); - - return $pid; - } - - /** - * Method to perform basic garbage collection and memory management in the sense of clearing the - * stat cache. We will probably call this method pretty regularly in our main loop. - * - * @return void - * - * @since 1.7.0 - */ - protected function gc() - { - // Perform generic garbage collection. - gc_collect_cycles(); - - // Clear the stat cache so it doesn't blow up memory. - clearstatcache(); - } - - /** - * Method to attach the DaemonApplication signal handler to the known signals. Applications - * can override these handlers by using the pcntl_signal() function and attaching a different - * callback method. - * - * @return boolean - * - * @since 1.7.0 - * @see pcntl_signal() - */ - protected function setupSignalHandlers() - { - // We add the error suppression for the loop because on some platforms some constants are not defined. - foreach (self::$signals as $signal) - { - // Ignore signals that are not defined. - if (!\defined($signal) || !\is_int(\constant($signal)) || (\constant($signal) === 0)) - { - // Define the signal to avoid notices. - Log::add('Signal "' . $signal . '" not defined. Defining it as null.', Log::DEBUG); - \define($signal, null); - - // Don't listen for signal. - continue; - } - - // Attach the signal handler for the signal. - if (!$this->pcntlSignal(\constant($signal), array('DaemonApplication', 'signal'))) - { - Log::add(sprintf('Unable to reroute signal handler: %s', $signal), Log::EMERGENCY); - - return false; - } - } - - return true; - } - - /** - * Method to shut down the daemon and optionally restart it. - * - * @param boolean $restart True to restart the daemon on exit. - * - * @return void - * - * @since 1.7.0 - */ - protected function shutdown($restart = false) - { - // If we are already exiting, chill. - if ($this->exiting) - { - return; - } - // If not, now we are. - else - { - $this->exiting = true; - } - - // If we aren't already daemonized then just kill the application. - if (!$this->running && !$this->isActive()) - { - Log::add('Process was not daemonized yet, just halting current process', Log::INFO); - $this->close(); - } - - // Only read the pid for the parent file. - if ($this->parentId == $this->processId) - { - // Read the contents of the process id file as an integer. - $fp = fopen($this->config->get('application_pid_file'), 'r'); - $pid = fread($fp, filesize($this->config->get('application_pid_file'))); - $pid = (int) $pid; - fclose($fp); - - // Remove the process id file. - @ unlink($this->config->get('application_pid_file')); - - // If we are supposed to restart the daemon we need to execute the same command. - if ($restart) - { - $this->close(exec(implode(' ', $GLOBALS['argv']) . ' > /dev/null &')); - } - // If we are not supposed to restart the daemon let's just kill -9. - else - { - passthru('kill -9 ' . $pid); - $this->close(); - } - } - } - - /** - * Method to write the process id file out to disk. - * - * @return boolean - * - * @since 1.7.0 - */ - protected function writeProcessIdFile() - { - // Verify the process id is valid. - if ($this->processId < 1) - { - Log::add('The process id is invalid.', Log::EMERGENCY); - - return false; - } - - // Get the application process id file path. - $file = $this->config->get('application_pid_file'); - - if (empty($file)) - { - Log::add('The process id file path is empty.', Log::ERROR); - - return false; - } - - // Make sure that the folder where we are writing the process id file exists. - $folder = \dirname($file); - - if (!is_dir($folder) && !Folder::create($folder)) - { - Log::add('Unable to create directory: ' . $folder, Log::ERROR); - - return false; - } - - // Write the process id file out to disk. - if (!file_put_contents($file, $this->processId)) - { - Log::add('Unable to write process id file: ' . $file, Log::ERROR); - - return false; - } - - // Make sure the permissions for the process id file are accurate. - if (!chmod($file, 0644)) - { - Log::add('Unable to adjust permissions for the process id file: ' . $file, Log::ERROR); - - return false; - } - - return true; - } - - /** - * Method to handle post-fork triggering of the onFork event. - * - * @return void - * - * @since 3.0.0 - */ - protected function postFork() - { - // Trigger the onFork event. - $this->triggerEvent('onFork'); - } - - /** - * Method to return the exit code of a terminated child process. - * - * @param integer $status The status parameter is the status parameter supplied to a successful call to pcntl_waitpid(). - * - * @return integer The child process exit code. - * - * @see pcntl_wexitstatus() - * @since 1.7.3 - */ - protected function pcntlChildExitStatus($status) - { - return pcntl_wexitstatus($status); - } - - /** - * Method to return the exit code of a terminated child process. - * - * @return integer On success, the PID of the child process is returned in the parent's thread - * of execution, and a 0 is returned in the child's thread of execution. On - * failure, a -1 will be returned in the parent's context, no child process - * will be created, and a PHP error is raised. - * - * @see pcntl_fork() - * @since 1.7.3 - */ - protected function pcntlFork() - { - return pcntl_fork(); - } - - /** - * Method to install a signal handler. - * - * @param integer $signal The signal number. - * @param callable $handler The signal handler which may be the name of a user created function, - * or method, or either of the two global constants SIG_IGN or SIG_DFL. - * @param boolean $restart Specifies whether system call restarting should be used when this - * signal arrives. - * - * @return boolean True on success. - * - * @see pcntl_signal() - * @since 1.7.3 - */ - protected function pcntlSignal($signal, $handler, $restart = true) - { - return pcntl_signal($signal, $handler, $restart); - } - - /** - * Method to wait on or return the status of a forked child. - * - * @param integer &$status Status information. - * @param integer $options If wait3 is available on your system (mostly BSD-style systems), - * you can provide the optional options parameter. - * - * @return integer The process ID of the child which exited, -1 on error or zero if WNOHANG - * was provided as an option (on wait3-available systems) and no child was available. - * - * @see pcntl_wait() - * @since 1.7.3 - */ - protected function pcntlWait(&$status, $options = 0) - { - return pcntl_wait($status, $options); - } + /** + * @var array The available POSIX signals to be caught by default. + * @link https://www.php.net/manual/pcntl.constants.php + * @since 1.7.0 + */ + protected static $signals = array( + 'SIGHUP', + 'SIGINT', + 'SIGQUIT', + 'SIGILL', + 'SIGTRAP', + 'SIGABRT', + 'SIGIOT', + 'SIGBUS', + 'SIGFPE', + 'SIGUSR1', + 'SIGSEGV', + 'SIGUSR2', + 'SIGPIPE', + 'SIGALRM', + 'SIGTERM', + 'SIGSTKFLT', + 'SIGCLD', + 'SIGCHLD', + 'SIGCONT', + 'SIGTSTP', + 'SIGTTIN', + 'SIGTTOU', + 'SIGURG', + 'SIGXCPU', + 'SIGXFSZ', + 'SIGVTALRM', + 'SIGPROF', + 'SIGWINCH', + 'SIGPOLL', + 'SIGIO', + 'SIGPWR', + 'SIGSYS', + 'SIGBABY', + 'SIG_BLOCK', + 'SIG_UNBLOCK', + 'SIG_SETMASK', + ); + + /** + * @var boolean True if the daemon is in the process of exiting. + * @since 1.7.0 + */ + protected $exiting = false; + + /** + * @var integer The parent process id. + * @since 3.0.0 + */ + protected $parentId = 0; + + /** + * @var integer The process id of the daemon. + * @since 1.7.0 + */ + protected $processId = 0; + + /** + * @var boolean True if the daemon is currently running. + * @since 1.7.0 + */ + protected $running = false; + + /** + * Class constructor. + * + * @param Cli $input An optional argument to provide dependency injection for the application's + * input object. If the argument is a JInputCli object that object will become + * the application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's + * config object. If the argument is a Registry object that object will become + * the application's config object, otherwise a default config object is created. + * @param DispatcherInterface $dispatcher An optional argument to provide dependency injection for the application's + * event dispatcher. If the argument is a DispatcherInterface object that object will become + * the application's event dispatcher, if it is null then the default event dispatcher + * will be created based on the application's loadDispatcher() method. + * + * @since 1.7.0 + */ + public function __construct(Cli $input = null, Registry $config = null, DispatcherInterface $dispatcher = null) + { + // Verify that the process control extension for PHP is available. + if (!\defined('SIGHUP')) { + Log::add('The PCNTL extension for PHP is not available.', Log::ERROR); + throw new \RuntimeException('The PCNTL extension for PHP is not available.'); + } + + // Verify that POSIX support for PHP is available. + if (!\function_exists('posix_getpid')) { + Log::add('The POSIX extension for PHP is not available.', Log::ERROR); + throw new \RuntimeException('The POSIX extension for PHP is not available.'); + } + + // Call the parent constructor. + parent::__construct($input, $config, null, null, $dispatcher); + + // Set some system limits. + @set_time_limit($this->config->get('max_execution_time', 0)); + + if ($this->config->get('max_memory_limit') !== null) { + ini_set('memory_limit', $this->config->get('max_memory_limit', '256M')); + } + + // Flush content immediately. + ob_implicit_flush(); + } + + /** + * Method to handle POSIX signals. + * + * @param integer $signal The received POSIX signal. + * + * @return void + * + * @since 1.7.0 + * @see pcntl_signal() + * @throws \RuntimeException + */ + public static function signal($signal) + { + // Log all signals sent to the daemon. + Log::add('Received signal: ' . $signal, Log::DEBUG); + + // Let's make sure we have an application instance. + if (!is_subclass_of(static::$instance, CliApplication::class)) { + Log::add('Cannot find the application instance.', Log::EMERGENCY); + throw new \RuntimeException('Cannot find the application instance.'); + } + + // Fire the onReceiveSignal event. + static::$instance->triggerEvent('onReceiveSignal', array($signal)); + + switch ($signal) { + case SIGINT: + case SIGTERM: + // Handle shutdown tasks + if (static::$instance->running && static::$instance->isActive()) { + static::$instance->shutdown(); + } else { + static::$instance->close(); + } + break; + case SIGHUP: + // Handle restart tasks + if (static::$instance->running && static::$instance->isActive()) { + static::$instance->shutdown(true); + } else { + static::$instance->close(); + } + break; + case SIGCHLD: + // A child process has died + while (static::$instance->pcntlWait($signal, WNOHANG || WUNTRACED) > 0) { + usleep(1000); + } + break; + case SIGCLD: + while (static::$instance->pcntlWait($signal, WNOHANG) > 0) { + $signal = static::$instance->pcntlChildExitStatus($signal); + } + break; + default: + break; + } + } + + /** + * Check to see if the daemon is active. This does not assume that $this daemon is active, but + * only if an instance of the application is active as a daemon. + * + * @return boolean True if daemon is active. + * + * @since 1.7.0 + */ + public function isActive() + { + // Get the process id file location for the application. + $pidFile = $this->config->get('application_pid_file'); + + // If the process id file doesn't exist then the daemon is obviously not running. + if (!is_file($pidFile)) { + return false; + } + + // Read the contents of the process id file as an integer. + $fp = fopen($pidFile, 'r'); + $pid = fread($fp, filesize($pidFile)); + $pid = (int) $pid; + fclose($fp); + + // Check to make sure that the process id exists as a positive integer. + if (!$pid) { + return false; + } + + // Check to make sure the process is active by pinging it and ensure it responds. + if (!posix_kill($pid, 0)) { + // No response so remove the process id file and log the situation. + @ unlink($pidFile); + Log::add('The process found based on PID file was unresponsive.', Log::WARNING); + + return false; + } + + return true; + } + + /** + * Load an object or array into the application configuration object. + * + * @param mixed $data Either an array or object to be loaded into the configuration object. + * + * @return DaemonApplication Instance of $this to allow chaining. + * + * @since 1.7.0 + */ + public function loadConfiguration($data) + { + /* + * Setup some application metadata options. This is useful if we ever want to write out startup scripts + * or just have some sort of information available to share about things. + */ + + // The application author name. This string is used in generating startup scripts and has + // a maximum of 50 characters. + $tmp = (string) $this->config->get('author_name', 'Joomla Platform'); + $this->config->set('author_name', (\strlen($tmp) > 50) ? substr($tmp, 0, 50) : $tmp); + + // The application author email. This string is used in generating startup scripts. + $tmp = (string) $this->config->get('author_email', 'admin@joomla.org'); + $this->config->set('author_email', filter_var($tmp, FILTER_VALIDATE_EMAIL)); + + // The application name. This string is used in generating startup scripts. + $tmp = (string) $this->config->get('application_name', 'DaemonApplication'); + $this->config->set('application_name', (string) preg_replace('/[^A-Z0-9_-]/i', '', $tmp)); + + // The application description. This string is used in generating startup scripts. + $tmp = (string) $this->config->get('application_description', 'A generic Joomla Platform application.'); + $this->config->set('application_description', filter_var($tmp, FILTER_SANITIZE_STRING)); + + /* + * Setup the application path options. This defines the default executable name, executable directory, + * and also the path to the daemon process id file. + */ + + // The application executable daemon. This string is used in generating startup scripts. + $tmp = (string) $this->config->get('application_executable', basename($this->input->executable)); + $this->config->set('application_executable', $tmp); + + // The home directory of the daemon. + $tmp = (string) $this->config->get('application_directory', \dirname($this->input->executable)); + $this->config->set('application_directory', $tmp); + + // The pid file location. This defaults to a path inside the /tmp directory. + $name = $this->config->get('application_name'); + $tmp = (string) $this->config->get('application_pid_file', strtolower('/tmp/' . $name . '/' . $name . '.pid')); + $this->config->set('application_pid_file', $tmp); + + /* + * Setup the application identity options. It is important to remember if the default of 0 is set for + * either UID or GID then changing that setting will not be attempted as there is no real way to "change" + * the identity of a process from some user to root. + */ + + // The user id under which to run the daemon. + $tmp = (int) $this->config->get('application_uid', 0); + $options = array('options' => array('min_range' => 0, 'max_range' => 65000)); + $this->config->set('application_uid', filter_var($tmp, FILTER_VALIDATE_INT, $options)); + + // The group id under which to run the daemon. + $tmp = (int) $this->config->get('application_gid', 0); + $options = array('options' => array('min_range' => 0, 'max_range' => 65000)); + $this->config->set('application_gid', filter_var($tmp, FILTER_VALIDATE_INT, $options)); + + // Option to kill the daemon if it cannot switch to the chosen identity. + $tmp = (bool) $this->config->get('application_require_identity', 1); + $this->config->set('application_require_identity', $tmp); + + /* + * Setup the application runtime options. By default our execution time limit is infinite obviously + * because a daemon should be constantly running unless told otherwise. The default limit for memory + * usage is 256M, which admittedly is a little high, but remember it is a "limit" and PHP's memory + * management leaves a bit to be desired :-) + */ + + // The maximum execution time of the application in seconds. Zero is infinite. + $tmp = $this->config->get('max_execution_time'); + + if ($tmp !== null) { + $this->config->set('max_execution_time', (int) $tmp); + } + + // The maximum amount of memory the application can use. + $tmp = $this->config->get('max_memory_limit', '256M'); + + if ($tmp !== null) { + $this->config->set('max_memory_limit', (string) $tmp); + } + + return $this; + } + + /** + * Execute the daemon. + * + * @return void + * + * @since 1.7.0 + */ + public function execute() + { + // Trigger the onBeforeExecute event + $this->triggerEvent('onBeforeExecute'); + + // Enable basic garbage collection. + gc_enable(); + + Log::add('Starting ' . $this->name, Log::INFO); + + // Set off the process for becoming a daemon. + if ($this->daemonize()) { + // Declare ticks to start signal monitoring. When you declare ticks, PCNTL will monitor + // incoming signals after each tick and call the relevant signal handler automatically. + declare(ticks=1); + + // Start the main execution loop. + while (true) { + // Perform basic garbage collection. + $this->gc(); + + // Don't completely overload the CPU. + usleep(1000); + + // Execute the main application logic. + $this->doExecute(); + } + } + // We were not able to daemonize the application so log the failure and die gracefully. + else { + Log::add('Starting ' . $this->name . ' failed', Log::INFO); + } + + // Trigger the onAfterExecute event. + $this->triggerEvent('onAfterExecute'); + } + + /** + * Restart daemon process. + * + * @return void + * + * @since 1.7.0 + */ + public function restart() + { + Log::add('Stopping ' . $this->name, Log::INFO); + $this->shutdown(true); + } + + /** + * Stop daemon process. + * + * @return void + * + * @since 1.7.0 + */ + public function stop() + { + Log::add('Stopping ' . $this->name, Log::INFO); + $this->shutdown(); + } + + /** + * Method to change the identity of the daemon process and resources. + * + * @return boolean True if identity successfully changed + * + * @since 1.7.0 + * @see posix_setuid() + */ + protected function changeIdentity() + { + // Get the group and user ids to set for the daemon. + $uid = (int) $this->config->get('application_uid', 0); + $gid = (int) $this->config->get('application_gid', 0); + + // Get the application process id file path. + $file = $this->config->get('application_pid_file'); + + // Change the user id for the process id file if necessary. + if ($uid && (fileowner($file) != $uid) && (!@ chown($file, $uid))) { + Log::add('Unable to change user ownership of the process id file.', Log::ERROR); + + return false; + } + + // Change the group id for the process id file if necessary. + if ($gid && (filegroup($file) != $gid) && (!@ chgrp($file, $gid))) { + Log::add('Unable to change group ownership of the process id file.', Log::ERROR); + + return false; + } + + // Set the correct home directory for the process. + if ($uid && ($info = posix_getpwuid($uid)) && is_dir($info['dir'])) { + system('export HOME="' . $info['dir'] . '"'); + } + + // Change the user id for the process necessary. + if ($uid && (posix_getuid() != $uid) && (!@ posix_setuid($uid))) { + Log::add('Unable to change user ownership of the process.', Log::ERROR); + + return false; + } + + // Change the group id for the process necessary. + if ($gid && (posix_getgid() != $gid) && (!@ posix_setgid($gid))) { + Log::add('Unable to change group ownership of the process.', Log::ERROR); + + return false; + } + + // Get the user and group information based on uid and gid. + $user = posix_getpwuid($uid); + $group = posix_getgrgid($gid); + + Log::add('Changed daemon identity to ' . $user['name'] . ':' . $group['name'], Log::INFO); + + return true; + } + + /** + * Method to put the application into the background. + * + * @return boolean + * + * @since 1.7.0 + * @throws \RuntimeException + */ + protected function daemonize() + { + // Is there already an active daemon running? + if ($this->isActive()) { + Log::add($this->name . ' daemon is still running. Exiting the application.', Log::EMERGENCY); + + return false; + } + + // Reset Process Information + $this->safeMode = !!@ ini_get('safe_mode'); + $this->processId = 0; + $this->running = false; + + // Detach process! + try { + // Check if we should run in the foreground. + if (!$this->input->get('f')) { + // Detach from the terminal. + $this->detach(); + } else { + // Setup running values. + $this->exiting = false; + $this->running = true; + + // Set the process id. + $this->processId = (int) posix_getpid(); + $this->parentId = $this->processId; + } + } catch (\RuntimeException $e) { + Log::add('Unable to fork.', Log::EMERGENCY); + + return false; + } + + // Verify the process id is valid. + if ($this->processId < 1) { + Log::add('The process id is invalid; the fork failed.', Log::EMERGENCY); + + return false; + } + + // Clear the umask. + @ umask(0); + + // Write out the process id file for concurrency management. + if (!$this->writeProcessIdFile()) { + Log::add('Unable to write the pid file at: ' . $this->config->get('application_pid_file'), Log::EMERGENCY); + + return false; + } + + // Attempt to change the identity of user running the process. + if (!$this->changeIdentity()) { + // If the identity change was required then we need to return false. + if ($this->config->get('application_require_identity')) { + Log::add('Unable to change process owner.', Log::CRITICAL); + + return false; + } else { + Log::add('Unable to change process owner.', Log::WARNING); + } + } + + // Setup the signal handlers for the daemon. + if (!$this->setupSignalHandlers()) { + return false; + } + + // Change the current working directory to the application working directory. + @ chdir($this->config->get('application_directory')); + + return true; + } + + /** + * This is truly where the magic happens. This is where we fork the process and kill the parent + * process, which is essentially what turns the application into a daemon. + * + * @return void + * + * @since 3.0.0 + * @throws \RuntimeException + */ + protected function detach() + { + Log::add('Detaching the ' . $this->name . ' daemon.', Log::DEBUG); + + // Attempt to fork the process. + $pid = $this->fork(); + + // If the pid is positive then we successfully forked, and can close this application. + if ($pid) { + // Add the log entry for debugging purposes and exit gracefully. + Log::add('Ending ' . $this->name . ' parent process', Log::DEBUG); + $this->close(); + } + // We are in the forked child process. + else { + // Setup some protected values. + $this->exiting = false; + $this->running = true; + + // Set the parent to self. + $this->parentId = $this->processId; + } + } + + /** + * Method to fork the process. + * + * @return integer The child process id to the parent process, zero to the child process. + * + * @since 1.7.0 + * @throws \RuntimeException + */ + protected function fork() + { + // Attempt to fork the process. + $pid = $this->pcntlFork(); + + // If the fork failed, throw an exception. + if ($pid === -1) { + throw new \RuntimeException('The process could not be forked.'); + } + // Update the process id for the child. + elseif ($pid === 0) { + $this->processId = (int) posix_getpid(); + } + // Log the fork in the parent. + else { + // Log the fork. + Log::add('Process forked ' . $pid, Log::DEBUG); + } + + // Trigger the onFork event. + $this->postFork(); + + return $pid; + } + + /** + * Method to perform basic garbage collection and memory management in the sense of clearing the + * stat cache. We will probably call this method pretty regularly in our main loop. + * + * @return void + * + * @since 1.7.0 + */ + protected function gc() + { + // Perform generic garbage collection. + gc_collect_cycles(); + + // Clear the stat cache so it doesn't blow up memory. + clearstatcache(); + } + + /** + * Method to attach the DaemonApplication signal handler to the known signals. Applications + * can override these handlers by using the pcntl_signal() function and attaching a different + * callback method. + * + * @return boolean + * + * @since 1.7.0 + * @see pcntl_signal() + */ + protected function setupSignalHandlers() + { + // We add the error suppression for the loop because on some platforms some constants are not defined. + foreach (self::$signals as $signal) { + // Ignore signals that are not defined. + if (!\defined($signal) || !\is_int(\constant($signal)) || (\constant($signal) === 0)) { + // Define the signal to avoid notices. + Log::add('Signal "' . $signal . '" not defined. Defining it as null.', Log::DEBUG); + \define($signal, null); + + // Don't listen for signal. + continue; + } + + // Attach the signal handler for the signal. + if (!$this->pcntlSignal(\constant($signal), array('DaemonApplication', 'signal'))) { + Log::add(sprintf('Unable to reroute signal handler: %s', $signal), Log::EMERGENCY); + + return false; + } + } + + return true; + } + + /** + * Method to shut down the daemon and optionally restart it. + * + * @param boolean $restart True to restart the daemon on exit. + * + * @return void + * + * @since 1.7.0 + */ + protected function shutdown($restart = false) + { + // If we are already exiting, chill. + if ($this->exiting) { + return; + } + // If not, now we are. + else { + $this->exiting = true; + } + + // If we aren't already daemonized then just kill the application. + if (!$this->running && !$this->isActive()) { + Log::add('Process was not daemonized yet, just halting current process', Log::INFO); + $this->close(); + } + + // Only read the pid for the parent file. + if ($this->parentId == $this->processId) { + // Read the contents of the process id file as an integer. + $fp = fopen($this->config->get('application_pid_file'), 'r'); + $pid = fread($fp, filesize($this->config->get('application_pid_file'))); + $pid = (int) $pid; + fclose($fp); + + // Remove the process id file. + @ unlink($this->config->get('application_pid_file')); + + // If we are supposed to restart the daemon we need to execute the same command. + if ($restart) { + $this->close(exec(implode(' ', $GLOBALS['argv']) . ' > /dev/null &')); + } + // If we are not supposed to restart the daemon let's just kill -9. + else { + passthru('kill -9 ' . $pid); + $this->close(); + } + } + } + + /** + * Method to write the process id file out to disk. + * + * @return boolean + * + * @since 1.7.0 + */ + protected function writeProcessIdFile() + { + // Verify the process id is valid. + if ($this->processId < 1) { + Log::add('The process id is invalid.', Log::EMERGENCY); + + return false; + } + + // Get the application process id file path. + $file = $this->config->get('application_pid_file'); + + if (empty($file)) { + Log::add('The process id file path is empty.', Log::ERROR); + + return false; + } + + // Make sure that the folder where we are writing the process id file exists. + $folder = \dirname($file); + + if (!is_dir($folder) && !Folder::create($folder)) { + Log::add('Unable to create directory: ' . $folder, Log::ERROR); + + return false; + } + + // Write the process id file out to disk. + if (!file_put_contents($file, $this->processId)) { + Log::add('Unable to write process id file: ' . $file, Log::ERROR); + + return false; + } + + // Make sure the permissions for the process id file are accurate. + if (!chmod($file, 0644)) { + Log::add('Unable to adjust permissions for the process id file: ' . $file, Log::ERROR); + + return false; + } + + return true; + } + + /** + * Method to handle post-fork triggering of the onFork event. + * + * @return void + * + * @since 3.0.0 + */ + protected function postFork() + { + // Trigger the onFork event. + $this->triggerEvent('onFork'); + } + + /** + * Method to return the exit code of a terminated child process. + * + * @param integer $status The status parameter is the status parameter supplied to a successful call to pcntl_waitpid(). + * + * @return integer The child process exit code. + * + * @see pcntl_wexitstatus() + * @since 1.7.3 + */ + protected function pcntlChildExitStatus($status) + { + return pcntl_wexitstatus($status); + } + + /** + * Method to return the exit code of a terminated child process. + * + * @return integer On success, the PID of the child process is returned in the parent's thread + * of execution, and a 0 is returned in the child's thread of execution. On + * failure, a -1 will be returned in the parent's context, no child process + * will be created, and a PHP error is raised. + * + * @see pcntl_fork() + * @since 1.7.3 + */ + protected function pcntlFork() + { + return pcntl_fork(); + } + + /** + * Method to install a signal handler. + * + * @param integer $signal The signal number. + * @param callable $handler The signal handler which may be the name of a user created function, + * or method, or either of the two global constants SIG_IGN or SIG_DFL. + * @param boolean $restart Specifies whether system call restarting should be used when this + * signal arrives. + * + * @return boolean True on success. + * + * @see pcntl_signal() + * @since 1.7.3 + */ + protected function pcntlSignal($signal, $handler, $restart = true) + { + return pcntl_signal($signal, $handler, $restart); + } + + /** + * Method to wait on or return the status of a forked child. + * + * @param integer &$status Status information. + * @param integer $options If wait3 is available on your system (mostly BSD-style systems), + * you can provide the optional options parameter. + * + * @return integer The process ID of the child which exited, -1 on error or zero if WNOHANG + * was provided as an option (on wait3-available systems) and no child was available. + * + * @see pcntl_wait() + * @since 1.7.3 + */ + protected function pcntlWait(&$status, $options = 0) + { + return pcntl_wait($status, $options); + } } diff --git a/libraries/src/Application/EventAware.php b/libraries/src/Application/EventAware.php index d1020858129a5..9404184a0ccc0 100644 --- a/libraries/src/Application/EventAware.php +++ b/libraries/src/Application/EventAware.php @@ -1,4 +1,5 @@ getDispatcher()->addListener($event, $handler); - } - catch (\UnexpectedValueException $e) - { - // No dispatcher is registered, don't throw an error (mimics old behavior) - } + /** + * Registers a handler to a particular event group. + * + * @param string $event The event name. + * @param callable $handler The handler, a function or an instance of an event object. + * + * @return $this + * + * @since 4.0.0 + */ + public function registerEvent($event, callable $handler) + { + try { + $this->getDispatcher()->addListener($event, $handler); + } catch (\UnexpectedValueException $e) { + // No dispatcher is registered, don't throw an error (mimics old behavior) + } - return $this; - } + return $this; + } - /** - * Calls all handlers associated with an event group. - * - * This is a legacy method, implementing old-style (Joomla! 3.x) plugin calls. It's best to go directly through the - * Dispatcher and handle the returned EventInterface object instead of going through this method. This method is - * deprecated and will be removed in Joomla! 5.x. - * - * This method will only return the 'result' argument of the event - * - * @param string $eventName The event name. - * @param array|Event $args An array of arguments or an Event object (optional). - * - * @return array An array of results from each function call. Note this will be an empty array if no dispatcher is set. - * - * @since 4.0.0 - * @throws \InvalidArgumentException - * @deprecated 5.0 - */ - public function triggerEvent($eventName, $args = []) - { - try - { - $dispatcher = $this->getDispatcher(); - } - catch (\UnexpectedValueException $exception) - { - $this->getLogger()->error(sprintf('Dispatcher not set in %s, cannot trigger events.', \get_class($this))); + /** + * Calls all handlers associated with an event group. + * + * This is a legacy method, implementing old-style (Joomla! 3.x) plugin calls. It's best to go directly through the + * Dispatcher and handle the returned EventInterface object instead of going through this method. This method is + * deprecated and will be removed in Joomla! 5.x. + * + * This method will only return the 'result' argument of the event + * + * @param string $eventName The event name. + * @param array|Event $args An array of arguments or an Event object (optional). + * + * @return array An array of results from each function call. Note this will be an empty array if no dispatcher is set. + * + * @since 4.0.0 + * @throws \InvalidArgumentException + * @deprecated 5.0 + */ + public function triggerEvent($eventName, $args = []) + { + try { + $dispatcher = $this->getDispatcher(); + } catch (\UnexpectedValueException $exception) { + $this->getLogger()->error(sprintf('Dispatcher not set in %s, cannot trigger events.', \get_class($this))); - return []; - } + return []; + } - if ($args instanceof Event) - { - $event = $args; - } - elseif (\is_array($args)) - { - $className = self::getEventClassByEventName($eventName); - $event = new $className($eventName, $args); - } - else - { - throw new \InvalidArgumentException('The arguments must either be an event or an array'); - } + if ($args instanceof Event) { + $event = $args; + } elseif (\is_array($args)) { + $className = self::getEventClassByEventName($eventName); + $event = new $className($eventName, $args); + } else { + throw new \InvalidArgumentException('The arguments must either be an event or an array'); + } - $result = $dispatcher->dispatch($eventName, $event); + $result = $dispatcher->dispatch($eventName, $event); - // @todo - There are still test cases where the result isn't defined, temporarily leave the isset check in place - return !isset($result['result']) || \is_null($result['result']) ? [] : $result['result']; - } + // @todo - There are still test cases where the result isn't defined, temporarily leave the isset check in place + return !isset($result['result']) || \is_null($result['result']) ? [] : $result['result']; + } } diff --git a/libraries/src/Application/EventAwareInterface.php b/libraries/src/Application/EventAwareInterface.php index f8bb168176af4..e650467bdc7b2 100644 --- a/libraries/src/Application/EventAwareInterface.php +++ b/libraries/src/Application/EventAwareInterface.php @@ -1,4 +1,5 @@ load(); - } + /** + * Allows the application to load a custom or default identity. + * + * @return void + * + * @since 4.0.0 + */ + public function createExtensionNamespaceMap() + { + JLoader::register('JNamespacePsr4Map', JPATH_LIBRARIES . '/namespacemap.php'); + $extensionPsr4Loader = new \JNamespacePsr4Map(); + $extensionPsr4Loader->load(); + } } diff --git a/libraries/src/Application/IdentityAware.php b/libraries/src/Application/IdentityAware.php index 03152ac51f31b..4005f33fa856f 100644 --- a/libraries/src/Application/IdentityAware.php +++ b/libraries/src/Application/IdentityAware.php @@ -1,4 +1,5 @@ identity; - } + /** + * Get the application identity. + * + * @return User + * + * @since 4.0.0 + */ + public function getIdentity() + { + return $this->identity; + } - /** - * Allows the application to load a custom or default identity. - * - * @param User $identity An optional identity object. If omitted, a null user object is created. - * - * @return $this - * - * @since 4.0.0 - */ - public function loadIdentity(User $identity = null) - { - $this->identity = $identity ?: $this->userFactory->loadUserById(0); + /** + * Allows the application to load a custom or default identity. + * + * @param User $identity An optional identity object. If omitted, a null user object is created. + * + * @return $this + * + * @since 4.0.0 + */ + public function loadIdentity(User $identity = null) + { + $this->identity = $identity ?: $this->userFactory->loadUserById(0); - return $this; - } + return $this; + } - /** - * Set the user factory to use. - * - * @param UserFactoryInterface $userFactory The user factory to use - * - * @return void - * - * @since 4.0.0 - */ - public function setUserFactory(UserFactoryInterface $userFactory) - { - $this->userFactory = $userFactory; - } + /** + * Set the user factory to use. + * + * @param UserFactoryInterface $userFactory The user factory to use + * + * @return void + * + * @since 4.0.0 + */ + public function setUserFactory(UserFactoryInterface $userFactory) + { + $this->userFactory = $userFactory; + } } diff --git a/libraries/src/Application/MultiFactorAuthenticationHandler.php b/libraries/src/Application/MultiFactorAuthenticationHandler.php index c991cb960a86d..1e3173442f0bb 100644 --- a/libraries/src/Application/MultiFactorAuthenticationHandler.php +++ b/libraries/src/Application/MultiFactorAuthenticationHandler.php @@ -1,4 +1,5 @@ getIdentity() ?? null; - } - catch (Exception $e) - { - return false; - } - - if (!($user instanceof User) || $user->guest) - { - return false; - } - - // If there is no need for a redirection I must not proceed - if (!$this->needsMultiFactorAuthenticationRedirection()) - { - return false; - } - - /** - * Automatically migrate from legacy MFA, if needed. - * - * We prefer to do a user-by-user migration instead of migrating everybody on Joomla update - * for practical reasons. On a site with hundreds or thousands of users the migration could - * take several minutes, causing Joomla Update to time out. - * - * Instead, every time we are in a captive Multi-factor Authentication page (captive MFA login - * or captive forced MFA setup) we spend a few milliseconds to check if a migration is - * necessary. If it's necessary, we perform it. - * - * The captive pages don't load any content or modules, therefore the few extra milliseconds - * we spend here are not a big deal. A failed all-users migration which would stop Joomla - * Update dead in its tracks would, however, be a big deal (broken sites). Moreover, a - * migration that has to be initiated by the site owner would also be a big deal — if they - * did not know they need to do it none of their users who had previously enabled MFA would - * now have it enabled! - * - * To paraphrase Otto von Bismarck: programming, like politics, is the art of the possible, - * the attainable -- the art of the next best. - */ - $this->migrateFromLegacyMFA(); - - // We only kick in when the user has actually set up MFA or must definitely enable MFA. - $userOptions = ComponentHelper::getParams('com_users'); - $neverMFAUserGroups = $userOptions->get('neverMFAUserGroups', []); - $forceMFAUserGroups = $userOptions->get('forceMFAUserGroups', []); - $isMFADisallowed = count( - array_intersect( - is_array($neverMFAUserGroups) ? $neverMFAUserGroups : [], - $user->getAuthorisedGroups() - ) - ) >= 1; - $isMFAMandatory = count( - array_intersect( - is_array($forceMFAUserGroups) ? $forceMFAUserGroups : [], - $user->getAuthorisedGroups() - ) - ) >= 1; - $isMFADisallowed = $isMFADisallowed && !$isMFAMandatory; - $isMFAPending = $this->isMultiFactorAuthenticationPending(); - $session = $this->getSession(); - $isNonHtml = $this->input->getCmd('format', 'html') !== 'html'; - - // Prevent non-interactive (non-HTML) content from being loaded until MFA is validated. - if ($isMFAPending && $isNonHtml) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - if ($isMFAPending && !$isMFADisallowed) - { - /** - * Saves the current URL as the return URL if all of the following conditions apply - * - It is not a URL to com_users' MFA feature itself - * - A return URL does not already exist, is imperfect or external to the site - * - * If no return URL has been set up and the current URL is com_users' MFA feature - * we will save the home page as the redirect target. - */ - $returnUrl = $session->get('com_users.return_url', ''); - - if (empty($returnUrl) || !Uri::isInternal($returnUrl)) - { - $returnUrl = $this->isMultiFactorAuthenticationPage() - ? Uri::base() - : Uri::getInstance()->toString(['scheme', 'user', 'pass', 'host', 'port', 'path', 'query', 'fragment']); - $session->set('com_users.return_url', $returnUrl); - } - - // Redirect - $this->redirect(Route::_('index.php?option=com_users&view=captive', false), 307); - } - - // If we're here someone just logged in but does not have MFA set up. Just flag him as logged in and continue. - $session->set('com_users.mfa_checked', 1); - - // If the user is in a group that requires MFA we will redirect them to the setup page. - if (!$isMFAPending && $isMFAMandatory) - { - // First unset the flag to make sure the redirection will apply until they conform to the mandatory MFA - $session->set('com_users.mfa_checked', 0); - - // Now set a flag which forces rechecking MFA for this user - $session->set('com_users.mandatory_mfa_setup', 1); - - // Then redirect them to the setup page - if (!$this->isMultiFactorAuthenticationPage()) - { - $url = Route::_('index.php?option=com_users&view=methods', false); - $this->redirect($url, 307); - } - } - - // Do I need to redirect the user to the MFA setup page after they have fully logged in? - $hasRejectedMultiFactorAuthenticationSetup = $this->hasRejectedMultiFactorAuthenticationSetup() && !$isMFAMandatory; - - if (!$isMFAPending && !$isMFADisallowed && ($userOptions->get('mfaredirectonlogin', 0) == 1) - && !$user->guest && !$hasRejectedMultiFactorAuthenticationSetup && !empty(MfaHelper::getMfaMethods())) - { - $this->redirect( - $userOptions->get('mfaredirecturl', '') ?: - Route::_('index.php?option=com_users&view=methods&layout=firsttime', false) - ); - } - - return true; - } - - /** - * Does the current user need to complete MFA authentication before being allowed to access the site? - * - * @return boolean - * @throws Exception - * @since 4.2.0 - */ - private function isMultiFactorAuthenticationPending(): bool - { - $user = $this->getIdentity(); - - if (empty($user) || $user->guest) - { - return false; - } - - // Get the user's MFA records - $records = MfaHelper::getUserMfaRecords($user->id); - - // No MFA Methods? Then we obviously don't need to display a Captive login page. - if (count($records) < 1) - { - return false; - } - - // Let's get a list of all currently active MFA Methods - $mfaMethods = MfaHelper::getMfaMethods(); - - // If no MFA Method is active we can't really display a Captive login page. - if (empty($mfaMethods)) - { - return false; - } - - // Get a list of just the Method names - $methodNames = []; - - foreach ($mfaMethods as $mfaMethod) - { - $methodNames[] = $mfaMethod['name']; - } - - // Filter the records based on currently active MFA Methods - foreach ($records as $record) - { - if (in_array($record->method, $methodNames)) - { - // We found an active Method. Show the Captive page. - return true; - } - } - - // No viable MFA Method found. We won't show the Captive page. - return false; - } - - /** - * Check whether we'll need to do a redirection to the Multi-factor Authentication captive page. - * - * @return boolean - * @since 4.2.0 - */ - private function needsMultiFactorAuthenticationRedirection(): bool - { - $isAdmin = $this->isClient('administrator'); - - /** - * We only kick in if the session flag is not set AND the user is not flagged for monitoring of their MFA status - * - * In case a user belongs to a group which requires MFA to be always enabled and they logged in without having - * MFA enabled we have the recheck flag. This prevents the user from enabling and immediately disabling MFA, - * circumventing the requirement for MFA. - */ - $session = $this->getSession(); - $isMFAComplete = $session->get('com_users.mfa_checked', 0) != 0; - $isMFASetupMandatory = $session->get('com_users.mandatory_mfa_setup', 0) != 0; - - if ($isMFAComplete && !$isMFASetupMandatory) - { - return false; - } - - // Make sure we are logged in - try - { - $user = $this->getIdentity(); - } - catch (Exception $e) - { - // This would happen if we are in CLI or under an old Joomla! version. Either case is not supported. - return false; - } - - // The plugin only needs to kick in when you have logged in - if (empty($user) || $user->guest) - { - return false; - } - - // If we are in the administrator section we only kick in when the user has backend access privileges - if ($isAdmin && !$user->authorise('core.login.admin')) - { - // @todo How exactly did you end up here if you didn't have the core.login.admin privilege to begin with?! - return false; - } - - // Do not redirect if we are already in a MFA management or captive page - if ($this->isMultiFactorAuthenticationPage()) - { - return false; - } - - $option = strtolower($this->input->getCmd('option', '')); - $task = strtolower($this->input->getCmd('task', '')); - - // Allow the frontend user to log out (in case they forgot their MFA code or something) - if (!$isAdmin && ($option == 'com_users') && in_array($task, ['user.logout', 'user.menulogout'])) - { - return false; - } - - // Allow the backend user to log out (in case they forgot their MFA code or something) - if ($isAdmin && ($option == 'com_login') && ($task == 'logout')) - { - return false; - } - - // Allow the Joomla update finalisation to run - if ($isAdmin && $option === 'com_joomlaupdate' && in_array($task, ['update.finalise', 'update.cleanup', 'update.finaliseconfirm'])) - { - return false; - } - - return true; - } - - /** - * Is this a page concerning the Multi-factor Authentication feature? - * - * @return boolean - * @since 4.2.0 - */ - private function isMultiFactorAuthenticationPage(): bool - { - $option = $this->input->get('option'); - $task = $this->input->get('task'); - $view = $this->input->get('view'); - - if ($option !== 'com_users') - { - return false; - } - - $allowedViews = ['captive', 'method', 'methods', 'callback']; - $allowedTasks = [ - 'captive.display', 'captive.captive', 'captive.validate', - 'method.display', 'method.add', 'method.edit', 'method.regenerateBackupCodes', 'method.delete', 'method.save', - 'methods.display', 'methods.disable', 'methods.doNotShowThisAgain', - ]; - - return in_array($view, $allowedViews) || in_array($task, $allowedTasks); - } - - /** - * Does the user have a "don't show this again" flag? - * - * @return boolean - * @since 4.2.0 - */ - private function hasRejectedMultiFactorAuthenticationSetup(): bool - { - $user = $this->getIdentity(); - $profileKey = 'mfa.dontshow'; - /** @var DatabaseDriver $db */ - $db = Factory::getContainer()->get('DatabaseDriver'); - $query = $db->getQuery(true) - ->select($db->quoteName('profile_value')) - ->from($db->quoteName('#__user_profiles')) - ->where($db->quoteName('user_id') . ' = :userId') - ->where($db->quoteName('profile_key') . ' = :profileKey') - ->bind(':userId', $user->id, ParameterType::INTEGER) - ->bind(':profileKey', $profileKey); - - try - { - $result = $db->setQuery($query)->loadResult(); - } - catch (Exception $e) - { - $result = 1; - } - - return $result == 1; - } - - /** - * Automatically migrates a user's legacy MFA records into the new Captive MFA format. - * - * @return void - * @since 4.2.0 - */ - private function migrateFromLegacyMFA(): void - { - $user = $this->getIdentity(); - - if (!($user instanceof User) || $user->guest || $user->id <= 0) - { - return; - } - - /** @var DatabaseDriver $db */ - $db = Factory::getContainer()->get('DatabaseDriver'); - - $userTable = new UserTable($db); - - if (!$userTable->load($user->id) || empty($userTable->otpKey)) - { - return; - } - - [$otpMethod, $otpKey] = explode(':', $userTable->otpKey, 2); - $secret = $this->get('secret'); - $otpKey = $this->decryptLegacyTFAString($secret, $otpKey); - $otep = $this->decryptLegacyTFAString($secret, $userTable->otep); - $config = @json_decode($otpKey, true); - $hasConverted = true; - - if (!empty($config)) - { - switch ($otpMethod) - { - case 'totp': - $this->getLanguage()->load('plg_multifactorauth_totp', JPATH_ADMINISTRATOR); - - (new MfaTable($db))->save( - [ - 'user_id' => $user->id, - 'title' => Text::_('PLG_MULTIFACTORAUTH_TOTP_METHOD_TITLE'), - 'method' => 'totp', - 'default' => 0, - 'created_on' => Date::getInstance()->toSql(), - 'last_used' => null, - 'options' => ['key' => $config['code']], - ] - ); - break; - - case 'yubikey': - $this->getLanguage()->load('plg_multifactorauth_yubikey', JPATH_ADMINISTRATOR); - - (new MfaTable($db))->save( - [ - 'user_id' => $user->id, - 'title' => sprintf("%s %s", Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_METHOD_TITLE'), $config['yubikey']), - 'method' => 'yubikey', - 'default' => 0, - 'created_on' => Date::getInstance()->toSql(), - 'last_used' => null, - 'options' => ['id' => $config['yubikey']], - ] - ); - break; - - default: - $hasConverted = false; - break; - } - } - - // Convert the emergency codes - if ($hasConverted && !empty(@json_decode($otep, true))) - { - // Delete any other record with the same user_id and Method. - $method = 'emergencycodes'; - $userId = $user->id; - $query = $db->getQuery(true) - ->delete($db->qn('#__user_mfa')) - ->where($db->qn('user_id') . ' = :user_id') - ->where($db->qn('method') . ' = :method') - ->bind(':user_id', $userId, ParameterType::INTEGER) - ->bind(':method', $method); - $db->setQuery($query)->execute(); - - // Migrate data - (new MfaTable($db))->save( - [ - 'user_id' => $user->id, - 'title' => Text::_('COM_USERS_USER_BACKUPCODES'), - 'method' => 'backupcodes', - 'default' => 0, - 'created_on' => Date::getInstance()->toSql(), - 'last_used' => null, - 'options' => @json_decode($otep, true), - ] - ); - } - - // Remove the legacy MFA - $update = (object) [ - 'id' => $user->id, - 'otpKey' => '', - 'otep' => '', - ]; - $db->updateObject('#__users', $update, ['id']); - } - - /** - * Tries to decrypt the legacy MFA configuration. - * - * @param string $secret Site's secret key - * @param string $stringToDecrypt Base64-encoded and encrypted, JSON-encoded information - * - * @return string Decrypted, but JSON-encoded, information - * - * @see https://github.com/joomla/joomla-cms/pull/12497 - * @since 4.2.0 - */ - private function decryptLegacyTFAString(string $secret, string $stringToDecrypt): string - { - // Is this already decrypted? - try - { - $decrypted = @json_decode($stringToDecrypt, true); - } - catch (Exception $e) - { - $decrypted = null; - } - - if (!empty($decrypted)) - { - return $stringToDecrypt; - } - - // No, we need to decrypt the string - $aes = new Aes($secret, 256); - $decrypted = $aes->decryptString($stringToDecrypt); - - if (!is_string($decrypted) || empty($decrypted)) - { - $aes->setPassword($secret, true); - - $decrypted = $aes->decryptString($stringToDecrypt); - } - - if (!is_string($decrypted) || empty($decrypted)) - { - return ''; - } - - // Remove the null padding added during encryption - return rtrim($decrypted, "\0"); - } + /** + * Handle the redirection to the Multi-factor Authentication captive login or setup page. + * + * @return boolean True if we are currently handling a Multi-factor Authentication captive page. + * @throws Exception + * @since 4.2.0 + */ + protected function isHandlingMultiFactorAuthentication(): bool + { + // Multi-factor Authentication checks take place only for logged in users. + try { + $user = $this->getIdentity() ?? null; + } catch (Exception $e) { + return false; + } + + if (!($user instanceof User) || $user->guest) { + return false; + } + + // If there is no need for a redirection I must not proceed + if (!$this->needsMultiFactorAuthenticationRedirection()) { + return false; + } + + /** + * Automatically migrate from legacy MFA, if needed. + * + * We prefer to do a user-by-user migration instead of migrating everybody on Joomla update + * for practical reasons. On a site with hundreds or thousands of users the migration could + * take several minutes, causing Joomla Update to time out. + * + * Instead, every time we are in a captive Multi-factor Authentication page (captive MFA login + * or captive forced MFA setup) we spend a few milliseconds to check if a migration is + * necessary. If it's necessary, we perform it. + * + * The captive pages don't load any content or modules, therefore the few extra milliseconds + * we spend here are not a big deal. A failed all-users migration which would stop Joomla + * Update dead in its tracks would, however, be a big deal (broken sites). Moreover, a + * migration that has to be initiated by the site owner would also be a big deal — if they + * did not know they need to do it none of their users who had previously enabled MFA would + * now have it enabled! + * + * To paraphrase Otto von Bismarck: programming, like politics, is the art of the possible, + * the attainable -- the art of the next best. + */ + $this->migrateFromLegacyMFA(); + + // We only kick in when the user has actually set up MFA or must definitely enable MFA. + $userOptions = ComponentHelper::getParams('com_users'); + $neverMFAUserGroups = $userOptions->get('neverMFAUserGroups', []); + $forceMFAUserGroups = $userOptions->get('forceMFAUserGroups', []); + $isMFADisallowed = count( + array_intersect( + is_array($neverMFAUserGroups) ? $neverMFAUserGroups : [], + $user->getAuthorisedGroups() + ) + ) >= 1; + $isMFAMandatory = count( + array_intersect( + is_array($forceMFAUserGroups) ? $forceMFAUserGroups : [], + $user->getAuthorisedGroups() + ) + ) >= 1; + $isMFADisallowed = $isMFADisallowed && !$isMFAMandatory; + $isMFAPending = $this->isMultiFactorAuthenticationPending(); + $session = $this->getSession(); + $isNonHtml = $this->input->getCmd('format', 'html') !== 'html'; + + // Prevent non-interactive (non-HTML) content from being loaded until MFA is validated. + if ($isMFAPending && $isNonHtml) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + if ($isMFAPending && !$isMFADisallowed) { + /** + * Saves the current URL as the return URL if all of the following conditions apply + * - It is not a URL to com_users' MFA feature itself + * - A return URL does not already exist, is imperfect or external to the site + * + * If no return URL has been set up and the current URL is com_users' MFA feature + * we will save the home page as the redirect target. + */ + $returnUrl = $session->get('com_users.return_url', ''); + + if (empty($returnUrl) || !Uri::isInternal($returnUrl)) { + $returnUrl = $this->isMultiFactorAuthenticationPage() + ? Uri::base() + : Uri::getInstance()->toString(['scheme', 'user', 'pass', 'host', 'port', 'path', 'query', 'fragment']); + $session->set('com_users.return_url', $returnUrl); + } + + // Redirect + $this->redirect(Route::_('index.php?option=com_users&view=captive', false), 307); + } + + // If we're here someone just logged in but does not have MFA set up. Just flag him as logged in and continue. + $session->set('com_users.mfa_checked', 1); + + // If the user is in a group that requires MFA we will redirect them to the setup page. + if (!$isMFAPending && $isMFAMandatory) { + // First unset the flag to make sure the redirection will apply until they conform to the mandatory MFA + $session->set('com_users.mfa_checked', 0); + + // Now set a flag which forces rechecking MFA for this user + $session->set('com_users.mandatory_mfa_setup', 1); + + // Then redirect them to the setup page + if (!$this->isMultiFactorAuthenticationPage()) { + $url = Route::_('index.php?option=com_users&view=methods', false); + $this->redirect($url, 307); + } + } + + // Do I need to redirect the user to the MFA setup page after they have fully logged in? + $hasRejectedMultiFactorAuthenticationSetup = $this->hasRejectedMultiFactorAuthenticationSetup() && !$isMFAMandatory; + + if ( + !$isMFAPending && !$isMFADisallowed && ($userOptions->get('mfaredirectonlogin', 0) == 1) + && !$user->guest && !$hasRejectedMultiFactorAuthenticationSetup && !empty(MfaHelper::getMfaMethods()) + ) { + $this->redirect( + $userOptions->get('mfaredirecturl', '') ?: + Route::_('index.php?option=com_users&view=methods&layout=firsttime', false) + ); + } + + return true; + } + + /** + * Does the current user need to complete MFA authentication before being allowed to access the site? + * + * @return boolean + * @throws Exception + * @since 4.2.0 + */ + private function isMultiFactorAuthenticationPending(): bool + { + $user = $this->getIdentity(); + + if (empty($user) || $user->guest) { + return false; + } + + // Get the user's MFA records + $records = MfaHelper::getUserMfaRecords($user->id); + + // No MFA Methods? Then we obviously don't need to display a Captive login page. + if (count($records) < 1) { + return false; + } + + // Let's get a list of all currently active MFA Methods + $mfaMethods = MfaHelper::getMfaMethods(); + + // If no MFA Method is active we can't really display a Captive login page. + if (empty($mfaMethods)) { + return false; + } + + // Get a list of just the Method names + $methodNames = []; + + foreach ($mfaMethods as $mfaMethod) { + $methodNames[] = $mfaMethod['name']; + } + + // Filter the records based on currently active MFA Methods + foreach ($records as $record) { + if (in_array($record->method, $methodNames)) { + // We found an active Method. Show the Captive page. + return true; + } + } + + // No viable MFA Method found. We won't show the Captive page. + return false; + } + + /** + * Check whether we'll need to do a redirection to the Multi-factor Authentication captive page. + * + * @return boolean + * @since 4.2.0 + */ + private function needsMultiFactorAuthenticationRedirection(): bool + { + $isAdmin = $this->isClient('administrator'); + + /** + * We only kick in if the session flag is not set AND the user is not flagged for monitoring of their MFA status + * + * In case a user belongs to a group which requires MFA to be always enabled and they logged in without having + * MFA enabled we have the recheck flag. This prevents the user from enabling and immediately disabling MFA, + * circumventing the requirement for MFA. + */ + $session = $this->getSession(); + $isMFAComplete = $session->get('com_users.mfa_checked', 0) != 0; + $isMFASetupMandatory = $session->get('com_users.mandatory_mfa_setup', 0) != 0; + + if ($isMFAComplete && !$isMFASetupMandatory) { + return false; + } + + // Make sure we are logged in + try { + $user = $this->getIdentity(); + } catch (Exception $e) { + // This would happen if we are in CLI or under an old Joomla! version. Either case is not supported. + return false; + } + + // The plugin only needs to kick in when you have logged in + if (empty($user) || $user->guest) { + return false; + } + + // If we are in the administrator section we only kick in when the user has backend access privileges + if ($isAdmin && !$user->authorise('core.login.admin')) { + // @todo How exactly did you end up here if you didn't have the core.login.admin privilege to begin with?! + return false; + } + + // Do not redirect if we are already in a MFA management or captive page + if ($this->isMultiFactorAuthenticationPage()) { + return false; + } + + $option = strtolower($this->input->getCmd('option', '')); + $task = strtolower($this->input->getCmd('task', '')); + + // Allow the frontend user to log out (in case they forgot their MFA code or something) + if (!$isAdmin && ($option == 'com_users') && in_array($task, ['user.logout', 'user.menulogout'])) { + return false; + } + + // Allow the backend user to log out (in case they forgot their MFA code or something) + if ($isAdmin && ($option == 'com_login') && ($task == 'logout')) { + return false; + } + + // Allow the Joomla update finalisation to run + if ($isAdmin && $option === 'com_joomlaupdate' && in_array($task, ['update.finalise', 'update.cleanup', 'update.finaliseconfirm'])) { + return false; + } + + return true; + } + + /** + * Is this a page concerning the Multi-factor Authentication feature? + * + * @return boolean + * @since 4.2.0 + */ + private function isMultiFactorAuthenticationPage(): bool + { + $option = $this->input->get('option'); + $task = $this->input->get('task'); + $view = $this->input->get('view'); + + if ($option !== 'com_users') { + return false; + } + + $allowedViews = ['captive', 'method', 'methods', 'callback']; + $allowedTasks = [ + 'captive.display', 'captive.captive', 'captive.validate', + 'method.display', 'method.add', 'method.edit', 'method.regenerateBackupCodes', 'method.delete', 'method.save', + 'methods.display', 'methods.disable', 'methods.doNotShowThisAgain', + ]; + + return in_array($view, $allowedViews) || in_array($task, $allowedTasks); + } + + /** + * Does the user have a "don't show this again" flag? + * + * @return boolean + * @since 4.2.0 + */ + private function hasRejectedMultiFactorAuthenticationSetup(): bool + { + $user = $this->getIdentity(); + $profileKey = 'mfa.dontshow'; + /** @var DatabaseDriver $db */ + $db = Factory::getContainer()->get('DatabaseDriver'); + $query = $db->getQuery(true) + ->select($db->quoteName('profile_value')) + ->from($db->quoteName('#__user_profiles')) + ->where($db->quoteName('user_id') . ' = :userId') + ->where($db->quoteName('profile_key') . ' = :profileKey') + ->bind(':userId', $user->id, ParameterType::INTEGER) + ->bind(':profileKey', $profileKey); + + try { + $result = $db->setQuery($query)->loadResult(); + } catch (Exception $e) { + $result = 1; + } + + return $result == 1; + } + + /** + * Automatically migrates a user's legacy MFA records into the new Captive MFA format. + * + * @return void + * @since 4.2.0 + */ + private function migrateFromLegacyMFA(): void + { + $user = $this->getIdentity(); + + if (!($user instanceof User) || $user->guest || $user->id <= 0) { + return; + } + + /** @var DatabaseDriver $db */ + $db = Factory::getContainer()->get('DatabaseDriver'); + + $userTable = new UserTable($db); + + if (!$userTable->load($user->id) || empty($userTable->otpKey)) { + return; + } + + [$otpMethod, $otpKey] = explode(':', $userTable->otpKey, 2); + $secret = $this->get('secret'); + $otpKey = $this->decryptLegacyTFAString($secret, $otpKey); + $otep = $this->decryptLegacyTFAString($secret, $userTable->otep); + $config = @json_decode($otpKey, true); + $hasConverted = true; + + if (!empty($config)) { + switch ($otpMethod) { + case 'totp': + $this->getLanguage()->load('plg_multifactorauth_totp', JPATH_ADMINISTRATOR); + + (new MfaTable($db))->save( + [ + 'user_id' => $user->id, + 'title' => Text::_('PLG_MULTIFACTORAUTH_TOTP_METHOD_TITLE'), + 'method' => 'totp', + 'default' => 0, + 'created_on' => Date::getInstance()->toSql(), + 'last_used' => null, + 'options' => ['key' => $config['code']], + ] + ); + break; + + case 'yubikey': + $this->getLanguage()->load('plg_multifactorauth_yubikey', JPATH_ADMINISTRATOR); + + (new MfaTable($db))->save( + [ + 'user_id' => $user->id, + 'title' => sprintf("%s %s", Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_METHOD_TITLE'), $config['yubikey']), + 'method' => 'yubikey', + 'default' => 0, + 'created_on' => Date::getInstance()->toSql(), + 'last_used' => null, + 'options' => ['id' => $config['yubikey']], + ] + ); + break; + + default: + $hasConverted = false; + break; + } + } + + // Convert the emergency codes + if ($hasConverted && !empty(@json_decode($otep, true))) { + // Delete any other record with the same user_id and Method. + $method = 'emergencycodes'; + $userId = $user->id; + $query = $db->getQuery(true) + ->delete($db->qn('#__user_mfa')) + ->where($db->qn('user_id') . ' = :user_id') + ->where($db->qn('method') . ' = :method') + ->bind(':user_id', $userId, ParameterType::INTEGER) + ->bind(':method', $method); + $db->setQuery($query)->execute(); + + // Migrate data + (new MfaTable($db))->save( + [ + 'user_id' => $user->id, + 'title' => Text::_('COM_USERS_USER_BACKUPCODES'), + 'method' => 'backupcodes', + 'default' => 0, + 'created_on' => Date::getInstance()->toSql(), + 'last_used' => null, + 'options' => @json_decode($otep, true), + ] + ); + } + + // Remove the legacy MFA + $update = (object) [ + 'id' => $user->id, + 'otpKey' => '', + 'otep' => '', + ]; + $db->updateObject('#__users', $update, ['id']); + } + + /** + * Tries to decrypt the legacy MFA configuration. + * + * @param string $secret Site's secret key + * @param string $stringToDecrypt Base64-encoded and encrypted, JSON-encoded information + * + * @return string Decrypted, but JSON-encoded, information + * + * @see https://github.com/joomla/joomla-cms/pull/12497 + * @since 4.2.0 + */ + private function decryptLegacyTFAString(string $secret, string $stringToDecrypt): string + { + // Is this already decrypted? + try { + $decrypted = @json_decode($stringToDecrypt, true); + } catch (Exception $e) { + $decrypted = null; + } + + if (!empty($decrypted)) { + return $stringToDecrypt; + } + + // No, we need to decrypt the string + $aes = new Aes($secret, 256); + $decrypted = $aes->decryptString($stringToDecrypt); + + if (!is_string($decrypted) || empty($decrypted)) { + $aes->setPassword($secret, true); + + $decrypted = $aes->decryptString($stringToDecrypt); + } + + if (!is_string($decrypted) || empty($decrypted)) { + return ''; + } + + // Remove the null padding added during encryption + return rtrim($decrypted, "\0"); + } } diff --git a/libraries/src/Application/SiteApplication.php b/libraries/src/Application/SiteApplication.php index c4b9f8c3b4b87..4967598a761df 100644 --- a/libraries/src/Application/SiteApplication.php +++ b/libraries/src/Application/SiteApplication.php @@ -1,4 +1,5 @@ name = 'site'; - - // Register the client ID - $this->clientId = 0; - - // Execute the parent constructor - parent::__construct($input, $config, $client, $container); - } - - /** - * Check if the user can access the application - * - * @param integer $itemid The item ID to check authorisation for - * - * @return void - * - * @since 3.2 - * - * @throws \Exception When you are not authorised to view the home page menu item - */ - protected function authorise($itemid) - { - $menus = $this->getMenu(); - $user = Factory::getUser(); - - if (!$menus->authorise($itemid)) - { - if ($user->get('id') == 0) - { - // Set the data - $this->setUserState('users.login.form.data', array('return' => Uri::getInstance()->toString())); - - $url = Route::_('index.php?option=com_users&view=login', false); - - $this->enqueueMessage(Text::_('JGLOBAL_YOU_MUST_LOGIN_FIRST'), 'error'); - $this->redirect($url); - } - else - { - // Get the home page menu item - $home_item = $menus->getDefault($this->getLanguage()->getTag()); - - // If we are already in the homepage raise an exception - if ($menus->getActive()->id == $home_item->id) - { - throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - // Otherwise redirect to the homepage and show an error - $this->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $this->redirect(Route::_('index.php?Itemid=' . $home_item->id, false)); - } - } - } - - /** - * Dispatch the application - * - * @param string $component The component which is being rendered. - * - * @return void - * - * @since 3.2 - */ - public function dispatch($component = null) - { - // Get the component if not set. - if (!$component) - { - $component = $this->input->getCmd('option', null); - } - - // Load the document to the API - $this->loadDocument(); - - // Set up the params - $document = $this->getDocument(); - $params = $this->getParams(); - - // Register the document object with Factory - Factory::$document = $document; - - switch ($document->getType()) - { - case 'html': - // Set up the language - LanguageHelper::getLanguages('lang_code'); - - // Set metadata - $document->setMetaData('rights', $this->get('MetaRights')); - - // Get the template - $template = $this->getTemplate(true); - - // Store the template and its params to the config - $this->set('theme', $template->template); - $this->set('themeParams', $template->params); - - // Add Asset registry files - $wr = $document->getWebAssetManager()->getRegistry(); - - if ($component) - { - $wr->addExtensionRegistryFile($component); - } - - if ($template->parent) - { - $wr->addTemplateRegistryFile($template->parent, $this->getClientId()); - } - - $wr->addTemplateRegistryFile($template->template, $this->getClientId()); - - break; - - case 'feed': - $document->setBase(htmlspecialchars(Uri::current())); - break; - } - - $document->setTitle($params->get('page_title')); - $document->setDescription($params->get('page_description')); - - // Add version number or not based on global configuration - if ($this->get('MetaVersion', 0)) - { - $document->setGenerator('Joomla! - Open Source Content Management - Version ' . JVERSION); - } - else - { - $document->setGenerator('Joomla! - Open Source Content Management'); - } - - $contents = ComponentHelper::renderComponent($component); - $document->setBuffer($contents, 'component'); - - // Trigger the onAfterDispatch event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterDispatch'); - } - - /** - * Method to run the Web application routines. - * - * @return void - * - * @since 3.2 - */ - protected function doExecute() - { - // Initialise the application - $this->initialiseApp(); - - // Mark afterInitialise in the profiler. - JDEBUG ? $this->profiler->mark('afterInitialise') : null; - - // Route the application - $this->route(); - - // Mark afterRoute in the profiler. - JDEBUG ? $this->profiler->mark('afterRoute') : null; - - if (!$this->isHandlingMultiFactorAuthentication()) - { - /* - * Check if the user is required to reset their password - * - * Before $this->route(); "option" and "view" can't be safely read using: - * $this->input->getCmd('option'); or $this->input->getCmd('view'); - * ex: due of the sef urls - */ - $this->checkUserRequireReset('com_users', 'profile', 'edit', 'com_users/profile.save,com_users/profile.apply,com_users/user.logout'); - } - - // Dispatch the application - $this->dispatch(); - - // Mark afterDispatch in the profiler. - JDEBUG ? $this->profiler->mark('afterDispatch') : null; - } - - /** - * Return the current state of the detect browser option. - * - * @return boolean - * - * @since 3.2 - */ - public function getDetectBrowser() - { - return $this->detect_browser; - } - - /** - * Return the current state of the language filter. - * - * @return boolean - * - * @since 3.2 - */ - public function getLanguageFilter() - { - return $this->language_filter; - } - - /** - * Get the application parameters - * - * @param string $option The component option - * - * @return Registry The parameters object - * - * @since 3.2 - */ - public function getParams($option = null) - { - static $params = array(); - - $hash = '__default'; - - if (!empty($option)) - { - $hash = $option; - } - - if (!isset($params[$hash])) - { - // Get component parameters - if (!$option) - { - $option = $this->input->getCmd('option', null); - } - - // Get new instance of component global parameters - $params[$hash] = clone ComponentHelper::getParams($option); - - // Get menu parameters - $menus = $this->getMenu(); - $menu = $menus->getActive(); - - // Get language - $lang_code = $this->getLanguage()->getTag(); - $languages = LanguageHelper::getLanguages('lang_code'); - - $title = $this->get('sitename'); - - if (isset($languages[$lang_code]) && $languages[$lang_code]->metadesc) - { - $description = $languages[$lang_code]->metadesc; - } - else - { - $description = $this->get('MetaDesc'); - } - - $rights = $this->get('MetaRights'); - $robots = $this->get('robots'); - - // Retrieve com_menu global settings - $temp = clone ComponentHelper::getParams('com_menus'); - - // Lets cascade the parameters if we have menu item parameters - if (\is_object($menu)) - { - // Get show_page_heading from com_menu global settings - $params[$hash]->def('show_page_heading', $temp->get('show_page_heading')); - - $params[$hash]->merge($menu->getParams()); - $title = $menu->title; - } - else - { - // Merge com_menu global settings - $params[$hash]->merge($temp); - - // If supplied, use page title - $title = $temp->get('page_title', $title); - } - - $params[$hash]->def('page_title', $title); - $params[$hash]->def('page_description', $description); - $params[$hash]->def('page_rights', $rights); - $params[$hash]->def('robots', $robots); - } - - return $params[$hash]; - } - - /** - * Return a reference to the Pathway object. - * - * @param string $name The name of the application. - * @param array $options An optional associative array of configuration settings. - * - * @return Pathway A Pathway object - * - * @since 3.2 - */ - public function getPathway($name = 'site', $options = array()) - { - return parent::getPathway($name, $options); - } - - /** - * Return a reference to the Router object. - * - * @param string $name The name of the application. - * @param array $options An optional associative array of configuration settings. - * - * @return \Joomla\CMS\Router\Router - * - * @since 3.2 - * - * @deprecated 5.0 Inject the router or load it from the dependency injection container - */ - public static function getRouter($name = 'site', array $options = array()) - { - return parent::getRouter($name, $options); - } - - /** - * Gets the name of the current template. - * - * @param boolean $params True to return the template parameters - * - * @return string The name of the template. - * - * @since 3.2 - * @throws \InvalidArgumentException - */ - public function getTemplate($params = false) - { - if (\is_object($this->template)) - { - if ($this->template->parent) - { - if (!is_file(JPATH_THEMES . '/' . $this->template->template . '/index.php')) - { - if (!is_file(JPATH_THEMES . '/' . $this->template->parent . '/index.php')) - { - throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $this->template->template)); - } - } - } - elseif (!is_file(JPATH_THEMES . '/' . $this->template->template . '/index.php')) - { - throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $this->template->template)); - } - - if ($params) - { - return $this->template; - } - - return $this->template->template; - } - - // Get the id of the active menu item - $menu = $this->getMenu(); - $item = $menu->getActive(); - - if (!$item) - { - $item = $menu->getItem($this->input->getInt('Itemid', null)); - } - - $id = 0; - - if (\is_object($item)) - { - // Valid item retrieved - $id = $item->template_style_id; - } - - $tid = $this->input->getUint('templateStyle', 0); - - if (is_numeric($tid) && (int) $tid > 0) - { - $id = (int) $tid; - } - - /** @var OutputController $cache */ - $cache = $this->getCacheControllerFactory()->createCacheController('output', ['defaultgroup' => 'com_templates']); - - if ($this->getLanguageFilter()) - { - $tag = $this->getLanguage()->getTag(); - } - else - { - $tag = ''; - } - - $cacheId = 'templates0' . $tag; - - if ($cache->contains($cacheId)) - { - $templates = $cache->get($cacheId); - } - else - { - $templates = $this->bootComponent('templates')->getMVCFactory() - ->createModel('Style', 'Administrator')->getSiteTemplates(); - - foreach ($templates as &$template) - { - // Create home element - if ($template->home == 1 && !isset($template_home) || $this->getLanguageFilter() && $template->home == $tag) - { - $template_home = clone $template; - } - - $template->params = new Registry($template->params); - } - - // Unset the $template reference to the last $templates[n] item cycled in the foreach above to avoid editing it later - unset($template); - - // Add home element, after loop to avoid double execution - if (isset($template_home)) - { - $template_home->params = new Registry($template_home->params); - $templates[0] = $template_home; - } - - $cache->store($templates, $cacheId); - } - - if (isset($templates[$id])) - { - $template = $templates[$id]; - } - else - { - $template = $templates[0]; - } - - // Allows for overriding the active template from the request - $template_override = $this->input->getCmd('template', ''); - - // Only set template override if it is a valid template (= it exists and is enabled) - if (!empty($template_override)) - { - if (is_file(JPATH_THEMES . '/' . $template_override . '/index.php')) - { - foreach ($templates as $tmpl) - { - if ($tmpl->template === $template_override) - { - $template = $tmpl; - break; - } - } - } - } - - // Need to filter the default value as well - $template->template = InputFilter::getInstance()->clean($template->template, 'cmd'); - - // Fallback template - if (!empty($template->parent)) - { - if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) - { - if (!is_file(JPATH_THEMES . '/' . $template->parent . '/index.php')) - { - $this->enqueueMessage(Text::_('JERROR_ALERTNOTEMPLATE'), 'error'); - - // Try to find data for 'cassiopeia' template - $original_tmpl = $template->template; - - foreach ($templates as $tmpl) - { - if ($tmpl->template === 'cassiopeia') - { - $template = $tmpl; - break; - } - } - - // Check, the data were found and if template really exists - if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) - { - throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $original_tmpl)); - } - } - } - } - elseif (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) - { - $this->enqueueMessage(Text::_('JERROR_ALERTNOTEMPLATE'), 'error'); - - // Try to find data for 'cassiopeia' template - $original_tmpl = $template->template; - - foreach ($templates as $tmpl) - { - if ($tmpl->template === 'cassiopeia') - { - $template = $tmpl; - break; - } - } - - // Check, the data were found and if template really exists - if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) - { - throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $original_tmpl)); - } - } - - // Cache the result - $this->template = $template; - - if ($params) - { - return $template; - } - - return $template->template; - } - - /** - * Initialise the application. - * - * @param array $options An optional associative array of configuration settings. - * - * @return void - * - * @since 3.2 - */ - protected function initialiseApp($options = array()) - { - $user = Factory::getUser(); - - // If the user is a guest we populate it with the guest user group. - if ($user->guest) - { - $guestUsergroup = ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); - $user->groups = array($guestUsergroup); - } - - if ($plugin = PluginHelper::getPlugin('system', 'languagefilter')) - { - $pluginParams = new Registry($plugin->params); - $this->setLanguageFilter(true); - $this->setDetectBrowser($pluginParams->get('detect_browser', 1) == 1); - } - - if (empty($options['language'])) - { - // Detect the specified language - $lang = $this->input->getString('language', null); - - // Make sure that the user's language exists - if ($lang && LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - } - - if (empty($options['language']) && $this->getLanguageFilter()) - { - // Detect cookie language - $lang = $this->input->cookie->get(md5($this->get('secret') . 'language'), null, 'string'); - - // Make sure that the user's language exists - if ($lang && LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - } - - if (empty($options['language'])) - { - // Detect user language - $lang = $user->getParam('language'); - - // Make sure that the user's language exists - if ($lang && LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - } - - if (empty($options['language']) && $this->getDetectBrowser()) - { - // Detect browser language - $lang = LanguageHelper::detectLanguage(); - - // Make sure that the user's language exists - if ($lang && LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - } - - if (empty($options['language'])) - { - // Detect default language - $params = ComponentHelper::getParams('com_languages'); - $options['language'] = $params->get('site', $this->get('language', 'en-GB')); - } - - // One last check to make sure we have something - if (!LanguageHelper::exists($options['language'])) - { - $lang = $this->config->get('language', 'en-GB'); - - if (LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - else - { - // As a last ditch fail to english - $options['language'] = 'en-GB'; - } - } - - // Finish initialisation - parent::initialiseApp($options); - } - - /** - * Load the library language files for the application - * - * @return void - * - * @since 3.6.3 - */ - protected function loadLibraryLanguage() - { - /* - * Try the lib_joomla file in the current language (without allowing the loading of the file in the default language) - * Fallback to the default language if necessary - */ - $this->getLanguage()->load('lib_joomla', JPATH_SITE) - || $this->getLanguage()->load('lib_joomla', JPATH_ADMINISTRATOR); - } - - /** - * Login authentication function - * - * @param array $credentials Array('username' => string, 'password' => string) - * @param array $options Array('remember' => boolean) - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function login($credentials, $options = array()) - { - // Set the application login entry point - if (!\array_key_exists('entry_url', $options)) - { - $options['entry_url'] = Uri::base() . 'index.php?option=com_users&task=user.login'; - } - - // Set the access control action to check. - $options['action'] = 'core.login.site'; - - return parent::login($credentials, $options); - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 3.2 - */ - protected function render() - { - switch ($this->document->getType()) - { - case 'feed': - // No special processing for feeds - break; - - case 'html': - default: - $template = $this->getTemplate(true); - $file = $this->input->get('tmpl', 'index'); - - if ($file === 'offline' && !$this->get('offline')) - { - $this->set('themeFile', 'index.php'); - } - - if ($this->get('offline') && !Factory::getUser()->authorise('core.login.offline')) - { - $this->setUserState('users.login.form.data', array('return' => Uri::getInstance()->toString())); - $this->set('themeFile', 'offline.php'); - $this->setHeader('Status', '503 Service Temporarily Unavailable', 'true'); - } - - if (!is_dir(JPATH_THEMES . '/' . $template->template) && !$this->get('offline')) - { - $this->set('themeFile', 'component.php'); - } - - // Ensure themeFile is set by now - if ($this->get('themeFile') == '') - { - $this->set('themeFile', $file . '.php'); - } - - // Pass the parent template to the state - $this->set('themeInherits', $template->parent); - - break; - } - - parent::render(); - } - - /** - * Route the application. - * - * Routing is the process of examining the request environment to determine which - * component should receive the request. The component optional parameters - * are then set in the request object to be processed when the application is being - * dispatched. - * - * @return void - * - * @since 3.2 - */ - protected function route() - { - // Get the full request URI. - $uri = clone Uri::getInstance(); - - // It is not possible to inject the SiteRouter as it requires a SiteApplication - // and we would end in an infinite loop - $result = $this->getContainer()->get(SiteRouter::class)->parse($uri, true); - - $active = $this->getMenu()->getActive(); - - if ($active !== null - && $active->type === 'alias' - && $active->getParams()->get('alias_redirect') - && \in_array($this->input->getMethod(), ['GET', 'HEAD'], true)) - { - $item = $this->getMenu()->getItem($active->getParams()->get('aliasoptions')); - - if ($item !== null) - { - $oldUri = clone Uri::getInstance(); - - if ($oldUri->getVar('Itemid') == $active->id) - { - $oldUri->setVar('Itemid', $item->id); - } - - $base = Uri::base(true); - $oldPath = StringHelper::strtolower(substr($oldUri->getPath(), \strlen($base) + 1)); - $activePathPrefix = StringHelper::strtolower($active->route); - - $position = strpos($oldPath, $activePathPrefix); - - if ($position !== false) - { - $oldUri->setPath($base . '/' . substr_replace($oldPath, $item->route, $position, \strlen($activePathPrefix))); - - $this->setHeader('Expires', 'Wed, 17 Aug 2005 00:00:00 GMT', true); - $this->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true); - $this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate', false); - $this->sendHeaders(); - - $this->redirect((string) $oldUri, 301); - } - } - } - - foreach ($result as $key => $value) - { - $this->input->def($key, $value); - } - - // Trigger the onAfterRoute event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterRoute'); - - $Itemid = $this->input->getInt('Itemid', null); - $this->authorise($Itemid); - } - - /** - * Set the current state of the detect browser option. - * - * @param boolean $state The new state of the detect browser option - * - * @return boolean The previous state - * - * @since 3.2 - */ - public function setDetectBrowser($state = false) - { - $old = $this->getDetectBrowser(); - $this->detect_browser = $state; - - return $old; - } - - /** - * Set the current state of the language filter. - * - * @param boolean $state The new state of the language filter - * - * @return boolean The previous state - * - * @since 3.2 - */ - public function setLanguageFilter($state = false) - { - $old = $this->getLanguageFilter(); - $this->language_filter = $state; - - return $old; - } - - /** - * Overrides the default template that would be used - * - * @param \stdClass|string $template The template name or definition - * @param mixed $styleParams The template style parameters - * - * @return void - * - * @since 3.2 - */ - public function setTemplate($template, $styleParams = null) - { - if (is_object($template)) - { - $templateName = empty($template->template) - ? '' - : $template->template; - $templateInheritable = empty($template->inheritable) - ? 0 - : $template->inheritable; - $templateParent = empty($template->parent) - ? '' - : $template->parent; - $templateParams = empty($template->params) - ? $styleParams - : $template->params; - } - else - { - $templateName = $template; - $templateInheritable = 0; - $templateParent = ''; - $templateParams = $styleParams; - } - - if (is_dir(JPATH_THEMES . '/' . $templateName)) - { - $this->template = new \stdClass; - $this->template->template = $templateName; - - if ($templateParams instanceof Registry) - { - $this->template->params = $templateParams; - } - else - { - $this->template->params = new Registry($templateParams); - } - - $this->template->inheritable = $templateInheritable; - $this->template->parent = $templateParent; - - // Store the template and its params to the config - $this->set('theme', $this->template->template); - $this->set('themeParams', $this->template->params); - } - } + use CacheControllerFactoryAwareTrait; + use MultiFactorAuthenticationHandler; + + /** + * Option to filter by language + * + * @var boolean + * @since 4.0.0 + */ + protected $language_filter = false; + + /** + * Option to detect language by the browser + * + * @var boolean + * @since 4.0.0 + */ + protected $detect_browser = false; + + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's input + * object. If the argument is a JInput object that object will become the + * application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's config + * object. If the argument is a Registry object that object will become the + * application's config object, otherwise a default config object is created. + * @param WebClient $client An optional argument to provide dependency injection for the application's client + * object. If the argument is a WebClient object that object will become the + * application's client object, otherwise a default client object is created. + * @param Container $container Dependency injection container. + * + * @since 3.2 + */ + public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, Container $container = null) + { + // Register the application name + $this->name = 'site'; + + // Register the client ID + $this->clientId = 0; + + // Execute the parent constructor + parent::__construct($input, $config, $client, $container); + } + + /** + * Check if the user can access the application + * + * @param integer $itemid The item ID to check authorisation for + * + * @return void + * + * @since 3.2 + * + * @throws \Exception When you are not authorised to view the home page menu item + */ + protected function authorise($itemid) + { + $menus = $this->getMenu(); + $user = Factory::getUser(); + + if (!$menus->authorise($itemid)) { + if ($user->get('id') == 0) { + // Set the data + $this->setUserState('users.login.form.data', array('return' => Uri::getInstance()->toString())); + + $url = Route::_('index.php?option=com_users&view=login', false); + + $this->enqueueMessage(Text::_('JGLOBAL_YOU_MUST_LOGIN_FIRST'), 'error'); + $this->redirect($url); + } else { + // Get the home page menu item + $home_item = $menus->getDefault($this->getLanguage()->getTag()); + + // If we are already in the homepage raise an exception + if ($menus->getActive()->id == $home_item->id) { + throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Otherwise redirect to the homepage and show an error + $this->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $this->redirect(Route::_('index.php?Itemid=' . $home_item->id, false)); + } + } + } + + /** + * Dispatch the application + * + * @param string $component The component which is being rendered. + * + * @return void + * + * @since 3.2 + */ + public function dispatch($component = null) + { + // Get the component if not set. + if (!$component) { + $component = $this->input->getCmd('option', null); + } + + // Load the document to the API + $this->loadDocument(); + + // Set up the params + $document = $this->getDocument(); + $params = $this->getParams(); + + // Register the document object with Factory + Factory::$document = $document; + + switch ($document->getType()) { + case 'html': + // Set up the language + LanguageHelper::getLanguages('lang_code'); + + // Set metadata + $document->setMetaData('rights', $this->get('MetaRights')); + + // Get the template + $template = $this->getTemplate(true); + + // Store the template and its params to the config + $this->set('theme', $template->template); + $this->set('themeParams', $template->params); + + // Add Asset registry files + $wr = $document->getWebAssetManager()->getRegistry(); + + if ($component) { + $wr->addExtensionRegistryFile($component); + } + + if ($template->parent) { + $wr->addTemplateRegistryFile($template->parent, $this->getClientId()); + } + + $wr->addTemplateRegistryFile($template->template, $this->getClientId()); + + break; + + case 'feed': + $document->setBase(htmlspecialchars(Uri::current())); + break; + } + + $document->setTitle($params->get('page_title')); + $document->setDescription($params->get('page_description')); + + // Add version number or not based on global configuration + if ($this->get('MetaVersion', 0)) { + $document->setGenerator('Joomla! - Open Source Content Management - Version ' . JVERSION); + } else { + $document->setGenerator('Joomla! - Open Source Content Management'); + } + + $contents = ComponentHelper::renderComponent($component); + $document->setBuffer($contents, 'component'); + + // Trigger the onAfterDispatch event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterDispatch'); + } + + /** + * Method to run the Web application routines. + * + * @return void + * + * @since 3.2 + */ + protected function doExecute() + { + // Initialise the application + $this->initialiseApp(); + + // Mark afterInitialise in the profiler. + JDEBUG ? $this->profiler->mark('afterInitialise') : null; + + // Route the application + $this->route(); + + // Mark afterRoute in the profiler. + JDEBUG ? $this->profiler->mark('afterRoute') : null; + + if (!$this->isHandlingMultiFactorAuthentication()) { + /* + * Check if the user is required to reset their password + * + * Before $this->route(); "option" and "view" can't be safely read using: + * $this->input->getCmd('option'); or $this->input->getCmd('view'); + * ex: due of the sef urls + */ + $this->checkUserRequireReset('com_users', 'profile', 'edit', 'com_users/profile.save,com_users/profile.apply,com_users/user.logout'); + } + + // Dispatch the application + $this->dispatch(); + + // Mark afterDispatch in the profiler. + JDEBUG ? $this->profiler->mark('afterDispatch') : null; + } + + /** + * Return the current state of the detect browser option. + * + * @return boolean + * + * @since 3.2 + */ + public function getDetectBrowser() + { + return $this->detect_browser; + } + + /** + * Return the current state of the language filter. + * + * @return boolean + * + * @since 3.2 + */ + public function getLanguageFilter() + { + return $this->language_filter; + } + + /** + * Get the application parameters + * + * @param string $option The component option + * + * @return Registry The parameters object + * + * @since 3.2 + */ + public function getParams($option = null) + { + static $params = array(); + + $hash = '__default'; + + if (!empty($option)) { + $hash = $option; + } + + if (!isset($params[$hash])) { + // Get component parameters + if (!$option) { + $option = $this->input->getCmd('option', null); + } + + // Get new instance of component global parameters + $params[$hash] = clone ComponentHelper::getParams($option); + + // Get menu parameters + $menus = $this->getMenu(); + $menu = $menus->getActive(); + + // Get language + $lang_code = $this->getLanguage()->getTag(); + $languages = LanguageHelper::getLanguages('lang_code'); + + $title = $this->get('sitename'); + + if (isset($languages[$lang_code]) && $languages[$lang_code]->metadesc) { + $description = $languages[$lang_code]->metadesc; + } else { + $description = $this->get('MetaDesc'); + } + + $rights = $this->get('MetaRights'); + $robots = $this->get('robots'); + + // Retrieve com_menu global settings + $temp = clone ComponentHelper::getParams('com_menus'); + + // Lets cascade the parameters if we have menu item parameters + if (\is_object($menu)) { + // Get show_page_heading from com_menu global settings + $params[$hash]->def('show_page_heading', $temp->get('show_page_heading')); + + $params[$hash]->merge($menu->getParams()); + $title = $menu->title; + } else { + // Merge com_menu global settings + $params[$hash]->merge($temp); + + // If supplied, use page title + $title = $temp->get('page_title', $title); + } + + $params[$hash]->def('page_title', $title); + $params[$hash]->def('page_description', $description); + $params[$hash]->def('page_rights', $rights); + $params[$hash]->def('robots', $robots); + } + + return $params[$hash]; + } + + /** + * Return a reference to the Pathway object. + * + * @param string $name The name of the application. + * @param array $options An optional associative array of configuration settings. + * + * @return Pathway A Pathway object + * + * @since 3.2 + */ + public function getPathway($name = 'site', $options = array()) + { + return parent::getPathway($name, $options); + } + + /** + * Return a reference to the Router object. + * + * @param string $name The name of the application. + * @param array $options An optional associative array of configuration settings. + * + * @return \Joomla\CMS\Router\Router + * + * @since 3.2 + * + * @deprecated 5.0 Inject the router or load it from the dependency injection container + */ + public static function getRouter($name = 'site', array $options = array()) + { + return parent::getRouter($name, $options); + } + + /** + * Gets the name of the current template. + * + * @param boolean $params True to return the template parameters + * + * @return string The name of the template. + * + * @since 3.2 + * @throws \InvalidArgumentException + */ + public function getTemplate($params = false) + { + if (\is_object($this->template)) { + if ($this->template->parent) { + if (!is_file(JPATH_THEMES . '/' . $this->template->template . '/index.php')) { + if (!is_file(JPATH_THEMES . '/' . $this->template->parent . '/index.php')) { + throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $this->template->template)); + } + } + } elseif (!is_file(JPATH_THEMES . '/' . $this->template->template . '/index.php')) { + throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $this->template->template)); + } + + if ($params) { + return $this->template; + } + + return $this->template->template; + } + + // Get the id of the active menu item + $menu = $this->getMenu(); + $item = $menu->getActive(); + + if (!$item) { + $item = $menu->getItem($this->input->getInt('Itemid', null)); + } + + $id = 0; + + if (\is_object($item)) { + // Valid item retrieved + $id = $item->template_style_id; + } + + $tid = $this->input->getUint('templateStyle', 0); + + if (is_numeric($tid) && (int) $tid > 0) { + $id = (int) $tid; + } + + /** @var OutputController $cache */ + $cache = $this->getCacheControllerFactory()->createCacheController('output', ['defaultgroup' => 'com_templates']); + + if ($this->getLanguageFilter()) { + $tag = $this->getLanguage()->getTag(); + } else { + $tag = ''; + } + + $cacheId = 'templates0' . $tag; + + if ($cache->contains($cacheId)) { + $templates = $cache->get($cacheId); + } else { + $templates = $this->bootComponent('templates')->getMVCFactory() + ->createModel('Style', 'Administrator')->getSiteTemplates(); + + foreach ($templates as &$template) { + // Create home element + if ($template->home == 1 && !isset($template_home) || $this->getLanguageFilter() && $template->home == $tag) { + $template_home = clone $template; + } + + $template->params = new Registry($template->params); + } + + // Unset the $template reference to the last $templates[n] item cycled in the foreach above to avoid editing it later + unset($template); + + // Add home element, after loop to avoid double execution + if (isset($template_home)) { + $template_home->params = new Registry($template_home->params); + $templates[0] = $template_home; + } + + $cache->store($templates, $cacheId); + } + + if (isset($templates[$id])) { + $template = $templates[$id]; + } else { + $template = $templates[0]; + } + + // Allows for overriding the active template from the request + $template_override = $this->input->getCmd('template', ''); + + // Only set template override if it is a valid template (= it exists and is enabled) + if (!empty($template_override)) { + if (is_file(JPATH_THEMES . '/' . $template_override . '/index.php')) { + foreach ($templates as $tmpl) { + if ($tmpl->template === $template_override) { + $template = $tmpl; + break; + } + } + } + } + + // Need to filter the default value as well + $template->template = InputFilter::getInstance()->clean($template->template, 'cmd'); + + // Fallback template + if (!empty($template->parent)) { + if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) { + if (!is_file(JPATH_THEMES . '/' . $template->parent . '/index.php')) { + $this->enqueueMessage(Text::_('JERROR_ALERTNOTEMPLATE'), 'error'); + + // Try to find data for 'cassiopeia' template + $original_tmpl = $template->template; + + foreach ($templates as $tmpl) { + if ($tmpl->template === 'cassiopeia') { + $template = $tmpl; + break; + } + } + + // Check, the data were found and if template really exists + if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) { + throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $original_tmpl)); + } + } + } + } elseif (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) { + $this->enqueueMessage(Text::_('JERROR_ALERTNOTEMPLATE'), 'error'); + + // Try to find data for 'cassiopeia' template + $original_tmpl = $template->template; + + foreach ($templates as $tmpl) { + if ($tmpl->template === 'cassiopeia') { + $template = $tmpl; + break; + } + } + + // Check, the data were found and if template really exists + if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) { + throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $original_tmpl)); + } + } + + // Cache the result + $this->template = $template; + + if ($params) { + return $template; + } + + return $template->template; + } + + /** + * Initialise the application. + * + * @param array $options An optional associative array of configuration settings. + * + * @return void + * + * @since 3.2 + */ + protected function initialiseApp($options = array()) + { + $user = Factory::getUser(); + + // If the user is a guest we populate it with the guest user group. + if ($user->guest) { + $guestUsergroup = ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); + $user->groups = array($guestUsergroup); + } + + if ($plugin = PluginHelper::getPlugin('system', 'languagefilter')) { + $pluginParams = new Registry($plugin->params); + $this->setLanguageFilter(true); + $this->setDetectBrowser($pluginParams->get('detect_browser', 1) == 1); + } + + if (empty($options['language'])) { + // Detect the specified language + $lang = $this->input->getString('language', null); + + // Make sure that the user's language exists + if ($lang && LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } + } + + if (empty($options['language']) && $this->getLanguageFilter()) { + // Detect cookie language + $lang = $this->input->cookie->get(md5($this->get('secret') . 'language'), null, 'string'); + + // Make sure that the user's language exists + if ($lang && LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } + } + + if (empty($options['language'])) { + // Detect user language + $lang = $user->getParam('language'); + + // Make sure that the user's language exists + if ($lang && LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } + } + + if (empty($options['language']) && $this->getDetectBrowser()) { + // Detect browser language + $lang = LanguageHelper::detectLanguage(); + + // Make sure that the user's language exists + if ($lang && LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } + } + + if (empty($options['language'])) { + // Detect default language + $params = ComponentHelper::getParams('com_languages'); + $options['language'] = $params->get('site', $this->get('language', 'en-GB')); + } + + // One last check to make sure we have something + if (!LanguageHelper::exists($options['language'])) { + $lang = $this->config->get('language', 'en-GB'); + + if (LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } else { + // As a last ditch fail to english + $options['language'] = 'en-GB'; + } + } + + // Finish initialisation + parent::initialiseApp($options); + } + + /** + * Load the library language files for the application + * + * @return void + * + * @since 3.6.3 + */ + protected function loadLibraryLanguage() + { + /* + * Try the lib_joomla file in the current language (without allowing the loading of the file in the default language) + * Fallback to the default language if necessary + */ + $this->getLanguage()->load('lib_joomla', JPATH_SITE) + || $this->getLanguage()->load('lib_joomla', JPATH_ADMINISTRATOR); + } + + /** + * Login authentication function + * + * @param array $credentials Array('username' => string, 'password' => string) + * @param array $options Array('remember' => boolean) + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function login($credentials, $options = array()) + { + // Set the application login entry point + if (!\array_key_exists('entry_url', $options)) { + $options['entry_url'] = Uri::base() . 'index.php?option=com_users&task=user.login'; + } + + // Set the access control action to check. + $options['action'] = 'core.login.site'; + + return parent::login($credentials, $options); + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 3.2 + */ + protected function render() + { + switch ($this->document->getType()) { + case 'feed': + // No special processing for feeds + break; + + case 'html': + default: + $template = $this->getTemplate(true); + $file = $this->input->get('tmpl', 'index'); + + if ($file === 'offline' && !$this->get('offline')) { + $this->set('themeFile', 'index.php'); + } + + if ($this->get('offline') && !Factory::getUser()->authorise('core.login.offline')) { + $this->setUserState('users.login.form.data', array('return' => Uri::getInstance()->toString())); + $this->set('themeFile', 'offline.php'); + $this->setHeader('Status', '503 Service Temporarily Unavailable', 'true'); + } + + if (!is_dir(JPATH_THEMES . '/' . $template->template) && !$this->get('offline')) { + $this->set('themeFile', 'component.php'); + } + + // Ensure themeFile is set by now + if ($this->get('themeFile') == '') { + $this->set('themeFile', $file . '.php'); + } + + // Pass the parent template to the state + $this->set('themeInherits', $template->parent); + + break; + } + + parent::render(); + } + + /** + * Route the application. + * + * Routing is the process of examining the request environment to determine which + * component should receive the request. The component optional parameters + * are then set in the request object to be processed when the application is being + * dispatched. + * + * @return void + * + * @since 3.2 + */ + protected function route() + { + // Get the full request URI. + $uri = clone Uri::getInstance(); + + // It is not possible to inject the SiteRouter as it requires a SiteApplication + // and we would end in an infinite loop + $result = $this->getContainer()->get(SiteRouter::class)->parse($uri, true); + + $active = $this->getMenu()->getActive(); + + if ( + $active !== null + && $active->type === 'alias' + && $active->getParams()->get('alias_redirect') + && \in_array($this->input->getMethod(), ['GET', 'HEAD'], true) + ) { + $item = $this->getMenu()->getItem($active->getParams()->get('aliasoptions')); + + if ($item !== null) { + $oldUri = clone Uri::getInstance(); + + if ($oldUri->getVar('Itemid') == $active->id) { + $oldUri->setVar('Itemid', $item->id); + } + + $base = Uri::base(true); + $oldPath = StringHelper::strtolower(substr($oldUri->getPath(), \strlen($base) + 1)); + $activePathPrefix = StringHelper::strtolower($active->route); + + $position = strpos($oldPath, $activePathPrefix); + + if ($position !== false) { + $oldUri->setPath($base . '/' . substr_replace($oldPath, $item->route, $position, \strlen($activePathPrefix))); + + $this->setHeader('Expires', 'Wed, 17 Aug 2005 00:00:00 GMT', true); + $this->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true); + $this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate', false); + $this->sendHeaders(); + + $this->redirect((string) $oldUri, 301); + } + } + } + + foreach ($result as $key => $value) { + $this->input->def($key, $value); + } + + // Trigger the onAfterRoute event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterRoute'); + + $Itemid = $this->input->getInt('Itemid', null); + $this->authorise($Itemid); + } + + /** + * Set the current state of the detect browser option. + * + * @param boolean $state The new state of the detect browser option + * + * @return boolean The previous state + * + * @since 3.2 + */ + public function setDetectBrowser($state = false) + { + $old = $this->getDetectBrowser(); + $this->detect_browser = $state; + + return $old; + } + + /** + * Set the current state of the language filter. + * + * @param boolean $state The new state of the language filter + * + * @return boolean The previous state + * + * @since 3.2 + */ + public function setLanguageFilter($state = false) + { + $old = $this->getLanguageFilter(); + $this->language_filter = $state; + + return $old; + } + + /** + * Overrides the default template that would be used + * + * @param \stdClass|string $template The template name or definition + * @param mixed $styleParams The template style parameters + * + * @return void + * + * @since 3.2 + */ + public function setTemplate($template, $styleParams = null) + { + if (is_object($template)) { + $templateName = empty($template->template) + ? '' + : $template->template; + $templateInheritable = empty($template->inheritable) + ? 0 + : $template->inheritable; + $templateParent = empty($template->parent) + ? '' + : $template->parent; + $templateParams = empty($template->params) + ? $styleParams + : $template->params; + } else { + $templateName = $template; + $templateInheritable = 0; + $templateParent = ''; + $templateParams = $styleParams; + } + + if (is_dir(JPATH_THEMES . '/' . $templateName)) { + $this->template = new \stdClass(); + $this->template->template = $templateName; + + if ($templateParams instanceof Registry) { + $this->template->params = $templateParams; + } else { + $this->template->params = new Registry($templateParams); + } + + $this->template->inheritable = $templateInheritable; + $this->template->parent = $templateParent; + + // Store the template and its params to the config + $this->set('theme', $this->template->template); + $this->set('themeParams', $this->template->params); + } + } } diff --git a/libraries/src/Application/WebApplication.php b/libraries/src/Application/WebApplication.php index b2b9e5881bd35..68a028a9d35d2 100644 --- a/libraries/src/Application/WebApplication.php +++ b/libraries/src/Application/WebApplication.php @@ -1,4 +1,5 @@ set('execution.datetime', gmdate('Y-m-d H:i:s')); - $this->set('execution.timestamp', time()); - - // Set the system URIs. - $this->loadSystemUris(); - } - - /** - * Returns a reference to the global WebApplication object, only creating it if it doesn't already exist. - * - * This method must be invoked as: $web = WebApplication::getInstance(); - * - * @param string $name The name (optional) of the WebApplication class to instantiate. - * - * @return WebApplication - * - * @since 1.7.3 - * @throws \RuntimeException - * @deprecated 5.0 Use \Joomla\CMS\Factory::getContainer()->get($name) instead - */ - public static function getInstance($name = null) - { - // Only create the object if it doesn't exist. - if (empty(static::$instance)) - { - if (!is_subclass_of($name, '\\Joomla\\CMS\\Application\\WebApplication')) - { - throw new \RuntimeException(sprintf('Unable to load application: %s', $name), 500); - } - - static::$instance = new $name; - } - - return static::$instance; - } - - /** - * Execute the application. - * - * @return void - * - * @since 1.7.3 - */ - public function execute() - { - // Trigger the onBeforeExecute event. - $this->triggerEvent('onBeforeExecute'); - - // Perform application routines. - $this->doExecute(); - - // Trigger the onAfterExecute event. - $this->triggerEvent('onAfterExecute'); - - // If we have an application document object, render it. - if ($this->document instanceof Document) - { - // Trigger the onBeforeRender event. - $this->triggerEvent('onBeforeRender'); - - // Render the application output. - $this->render(); - - // Trigger the onAfterRender event. - $this->triggerEvent('onAfterRender'); - } - - // If gzip compression is enabled in configuration and the server is compliant, compress the output. - if ($this->get('gzip') && !ini_get('zlib.output_compression') && (ini_get('output_handler') !== 'ob_gzhandler')) - { - $this->compress(); - } - - // Trigger the onBeforeRespond event. - $this->triggerEvent('onBeforeRespond'); - - // Send the application response. - $this->respond(); - - // Trigger the onAfterRespond event. - $this->triggerEvent('onAfterRespond'); - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 1.7.3 - */ - protected function render() - { - // Setup the document options. - $options = array( - 'template' => $this->get('theme'), - 'file' => $this->get('themeFile', 'index.php'), - 'params' => $this->get('themeParams'), - 'templateInherits' => $this->get('themeInherits'), - ); - - if ($this->get('themes.base')) - { - $options['directory'] = $this->get('themes.base'); - } - // Fall back to constants. - else - { - $options['directory'] = \defined('JPATH_THEMES') ? JPATH_THEMES : (\defined('JPATH_BASE') ? JPATH_BASE : __DIR__) . '/themes'; - } - - // Parse the document. - $this->document->parse($options); - - // Render the document. - $data = $this->document->render($this->get('cache_enabled'), $options); - - // Set the application output data. - $this->setBody($data); - } - - /** - * Method to get the application document object. - * - * @return Document The document object - * - * @since 1.7.3 - */ - public function getDocument() - { - return $this->document; - } - - /** - * Method to get the application language object. - * - * @return Language The language object - * - * @since 1.7.3 - */ - public function getLanguage() - { - return $this->language; - } - - /** - * Flush the media version to refresh versionable assets - * - * @return void - * - * @since 3.2 - */ - public function flushAssets() - { - (new Version)->refreshMediaVersion(); - } - - /** - * Allows the application to load a custom or default document. - * - * The logic and options for creating this object are adequately generic for default cases - * but for many applications it will make sense to override this method and create a document, - * if required, based on more specific needs. - * - * @param Document $document An optional document object. If omitted, the factory document is created. - * - * @return WebApplication This method is chainable. - * - * @since 1.7.3 - */ - public function loadDocument(Document $document = null) - { - $this->document = $document ?? Factory::getDocument(); - - return $this; - } - - /** - * Allows the application to load a custom or default language. - * - * The logic and options for creating this object are adequately generic for default cases - * but for many applications it will make sense to override this method and create a language, - * if required, based on more specific needs. - * - * @param Language $language An optional language object. If omitted, the factory language is created. - * - * @return WebApplication This method is chainable. - * - * @since 1.7.3 - */ - public function loadLanguage(Language $language = null) - { - $this->language = $language ?? Factory::getLanguage(); - - return $this; - } - - /** - * Allows the application to load a custom or default session. - * - * The logic and options for creating this object are adequately generic for default cases - * but for many applications it will make sense to override this method and create a session, - * if required, based on more specific needs. - * - * @param Session $session An optional session object. If omitted, the session is created. - * - * @return WebApplication This method is chainable. - * - * @since 1.7.3 - * @deprecated 5.0 The session should be injected as a service. - */ - public function loadSession(Session $session = null) - { - $this->getLogger()->warning(__METHOD__ . '() is deprecated. Inject the session as a service instead.', array('category' => 'deprecated')); - - return $this; - } - - /** - * After the session has been started we need to populate it with some default values. - * - * @param SessionEvent $event Session event being triggered - * - * @return void - * - * @since 3.0.1 - */ - public function afterSessionStart(SessionEvent $event) - { - $session = $event->getSession(); - - if ($session->isNew()) - { - $session->set('registry', new Registry); - $session->set('user', new User); - } - - // Ensure the identity is loaded - if (!$this->getIdentity()) - { - $this->loadIdentity($session->get('user')); - } - } - - /** - * Method to load the system URI strings for the application. - * - * @param string $requestUri An optional request URI to use instead of detecting one from the - * server environment variables. - * - * @return void - * - * @since 1.7.3 - */ - protected function loadSystemUris($requestUri = null) - { - // Set the request URI. - if (!empty($requestUri)) - { - $this->set('uri.request', $requestUri); - } - else - { - $this->set('uri.request', $this->detectRequestUri()); - } - - // Check to see if an explicit base URI has been set. - $siteUri = trim($this->get('site_uri', '')); - - if ($siteUri !== '') - { - $uri = Uri::getInstance($siteUri); - $path = $uri->toString(array('path')); - } - // No explicit base URI was set so we need to detect it. - else - { - // Start with the requested URI. - $uri = Uri::getInstance($this->get('uri.request')); - - // If we are working from a CGI SAPI with the 'cgi.fix_pathinfo' directive disabled we use PHP_SELF. - if (strpos(PHP_SAPI, 'cgi') !== false && !ini_get('cgi.fix_pathinfo') && !empty($_SERVER['REQUEST_URI'])) - { - // We aren't expecting PATH_INFO within PHP_SELF so this should work. - $path = \dirname($_SERVER['PHP_SELF']); - } - // Pretty much everything else should be handled with SCRIPT_NAME. - else - { - $path = \dirname($_SERVER['SCRIPT_NAME']); - } - } - - $host = $uri->toString(array('scheme', 'user', 'pass', 'host', 'port')); - - // Check if the path includes "index.php". - if (strpos($path, 'index.php') !== false) - { - // Remove the index.php portion of the path. - $path = substr_replace($path, '', strpos($path, 'index.php'), 9); - } - - $path = rtrim($path, '/\\'); - - // Set the base URI both as just a path and as the full URI. - $this->set('uri.base.full', $host . $path . '/'); - $this->set('uri.base.host', $host); - $this->set('uri.base.path', $path . '/'); - - // Set the extended (non-base) part of the request URI as the route. - if (stripos($this->get('uri.request'), $this->get('uri.base.full')) === 0) - { - $this->set('uri.route', substr_replace($this->get('uri.request'), '', 0, \strlen($this->get('uri.base.full')))); - } - - // Get an explicitly set media URI is present. - $mediaURI = trim($this->get('media_uri', '')); - - if ($mediaURI) - { - if (strpos($mediaURI, '://') !== false) - { - $this->set('uri.media.full', $mediaURI); - $this->set('uri.media.path', $mediaURI); - } - else - { - // Normalise slashes. - $mediaURI = trim($mediaURI, '/\\'); - $mediaURI = !empty($mediaURI) ? '/' . $mediaURI . '/' : '/'; - $this->set('uri.media.full', $this->get('uri.base.host') . $mediaURI); - $this->set('uri.media.path', $mediaURI); - } - } - // No explicit media URI was set, build it dynamically from the base uri. - else - { - $this->set('uri.media.full', $this->get('uri.base.full') . 'media/'); - $this->set('uri.media.path', $this->get('uri.base.path') . 'media/'); - } - } - - /** - * Retrieve the application configuration object. - * - * @return Registry - * - * @since 4.0.0 - */ - public function getConfig() - { - return $this->config; - } + use EventAware; + use IdentityAware; + + /** + * The application document object. + * + * @var Document + * @since 1.7.3 + */ + protected $document; + + /** + * The application language object. + * + * @var Language + * @since 1.7.3 + */ + protected $language; + + /** + * The application instance. + * + * @var static + * @since 1.7.3 + */ + protected static $instance; + + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's + * input object. If the argument is a JInput object that object will become + * the application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's + * config object. If the argument is a Registry object that object will become + * the application's config object, otherwise a default config object is created. + * @param WebClient $client An optional argument to provide dependency injection for the application's + * client object. If the argument is a WebClient object that object will become + * the application's client object, otherwise a default client object is created. + * @param ResponseInterface $response An optional argument to provide dependency injection for the application's + * response object. If the argument is a ResponseInterface object that object + * will become the application's response object, otherwise a default response + * object is created. + * + * @since 1.7.3 + */ + public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, ResponseInterface $response = null) + { + // Ensure we have a CMS Input object otherwise the DI for \Joomla\CMS\Session\Storage\JoomlaStorage fails + $input = $input ?: new Input(); + + parent::__construct($input, $config, $client, $response); + + // Set the execution datetime and timestamp; + $this->set('execution.datetime', gmdate('Y-m-d H:i:s')); + $this->set('execution.timestamp', time()); + + // Set the system URIs. + $this->loadSystemUris(); + } + + /** + * Returns a reference to the global WebApplication object, only creating it if it doesn't already exist. + * + * This method must be invoked as: $web = WebApplication::getInstance(); + * + * @param string $name The name (optional) of the WebApplication class to instantiate. + * + * @return WebApplication + * + * @since 1.7.3 + * @throws \RuntimeException + * @deprecated 5.0 Use \Joomla\CMS\Factory::getContainer()->get($name) instead + */ + public static function getInstance($name = null) + { + // Only create the object if it doesn't exist. + if (empty(static::$instance)) { + if (!is_subclass_of($name, '\\Joomla\\CMS\\Application\\WebApplication')) { + throw new \RuntimeException(sprintf('Unable to load application: %s', $name), 500); + } + + static::$instance = new $name(); + } + + return static::$instance; + } + + /** + * Execute the application. + * + * @return void + * + * @since 1.7.3 + */ + public function execute() + { + // Trigger the onBeforeExecute event. + $this->triggerEvent('onBeforeExecute'); + + // Perform application routines. + $this->doExecute(); + + // Trigger the onAfterExecute event. + $this->triggerEvent('onAfterExecute'); + + // If we have an application document object, render it. + if ($this->document instanceof Document) { + // Trigger the onBeforeRender event. + $this->triggerEvent('onBeforeRender'); + + // Render the application output. + $this->render(); + + // Trigger the onAfterRender event. + $this->triggerEvent('onAfterRender'); + } + + // If gzip compression is enabled in configuration and the server is compliant, compress the output. + if ($this->get('gzip') && !ini_get('zlib.output_compression') && (ini_get('output_handler') !== 'ob_gzhandler')) { + $this->compress(); + } + + // Trigger the onBeforeRespond event. + $this->triggerEvent('onBeforeRespond'); + + // Send the application response. + $this->respond(); + + // Trigger the onAfterRespond event. + $this->triggerEvent('onAfterRespond'); + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 1.7.3 + */ + protected function render() + { + // Setup the document options. + $options = array( + 'template' => $this->get('theme'), + 'file' => $this->get('themeFile', 'index.php'), + 'params' => $this->get('themeParams'), + 'templateInherits' => $this->get('themeInherits'), + ); + + if ($this->get('themes.base')) { + $options['directory'] = $this->get('themes.base'); + } + // Fall back to constants. + else { + $options['directory'] = \defined('JPATH_THEMES') ? JPATH_THEMES : (\defined('JPATH_BASE') ? JPATH_BASE : __DIR__) . '/themes'; + } + + // Parse the document. + $this->document->parse($options); + + // Render the document. + $data = $this->document->render($this->get('cache_enabled'), $options); + + // Set the application output data. + $this->setBody($data); + } + + /** + * Method to get the application document object. + * + * @return Document The document object + * + * @since 1.7.3 + */ + public function getDocument() + { + return $this->document; + } + + /** + * Method to get the application language object. + * + * @return Language The language object + * + * @since 1.7.3 + */ + public function getLanguage() + { + return $this->language; + } + + /** + * Flush the media version to refresh versionable assets + * + * @return void + * + * @since 3.2 + */ + public function flushAssets() + { + (new Version())->refreshMediaVersion(); + } + + /** + * Allows the application to load a custom or default document. + * + * The logic and options for creating this object are adequately generic for default cases + * but for many applications it will make sense to override this method and create a document, + * if required, based on more specific needs. + * + * @param Document $document An optional document object. If omitted, the factory document is created. + * + * @return WebApplication This method is chainable. + * + * @since 1.7.3 + */ + public function loadDocument(Document $document = null) + { + $this->document = $document ?? Factory::getDocument(); + + return $this; + } + + /** + * Allows the application to load a custom or default language. + * + * The logic and options for creating this object are adequately generic for default cases + * but for many applications it will make sense to override this method and create a language, + * if required, based on more specific needs. + * + * @param Language $language An optional language object. If omitted, the factory language is created. + * + * @return WebApplication This method is chainable. + * + * @since 1.7.3 + */ + public function loadLanguage(Language $language = null) + { + $this->language = $language ?? Factory::getLanguage(); + + return $this; + } + + /** + * Allows the application to load a custom or default session. + * + * The logic and options for creating this object are adequately generic for default cases + * but for many applications it will make sense to override this method and create a session, + * if required, based on more specific needs. + * + * @param Session $session An optional session object. If omitted, the session is created. + * + * @return WebApplication This method is chainable. + * + * @since 1.7.3 + * @deprecated 5.0 The session should be injected as a service. + */ + public function loadSession(Session $session = null) + { + $this->getLogger()->warning(__METHOD__ . '() is deprecated. Inject the session as a service instead.', array('category' => 'deprecated')); + + return $this; + } + + /** + * After the session has been started we need to populate it with some default values. + * + * @param SessionEvent $event Session event being triggered + * + * @return void + * + * @since 3.0.1 + */ + public function afterSessionStart(SessionEvent $event) + { + $session = $event->getSession(); + + if ($session->isNew()) { + $session->set('registry', new Registry()); + $session->set('user', new User()); + } + + // Ensure the identity is loaded + if (!$this->getIdentity()) { + $this->loadIdentity($session->get('user')); + } + } + + /** + * Method to load the system URI strings for the application. + * + * @param string $requestUri An optional request URI to use instead of detecting one from the + * server environment variables. + * + * @return void + * + * @since 1.7.3 + */ + protected function loadSystemUris($requestUri = null) + { + // Set the request URI. + if (!empty($requestUri)) { + $this->set('uri.request', $requestUri); + } else { + $this->set('uri.request', $this->detectRequestUri()); + } + + // Check to see if an explicit base URI has been set. + $siteUri = trim($this->get('site_uri', '')); + + if ($siteUri !== '') { + $uri = Uri::getInstance($siteUri); + $path = $uri->toString(array('path')); + } + // No explicit base URI was set so we need to detect it. + else { + // Start with the requested URI. + $uri = Uri::getInstance($this->get('uri.request')); + + // If we are working from a CGI SAPI with the 'cgi.fix_pathinfo' directive disabled we use PHP_SELF. + if (strpos(PHP_SAPI, 'cgi') !== false && !ini_get('cgi.fix_pathinfo') && !empty($_SERVER['REQUEST_URI'])) { + // We aren't expecting PATH_INFO within PHP_SELF so this should work. + $path = \dirname($_SERVER['PHP_SELF']); + } + // Pretty much everything else should be handled with SCRIPT_NAME. + else { + $path = \dirname($_SERVER['SCRIPT_NAME']); + } + } + + $host = $uri->toString(array('scheme', 'user', 'pass', 'host', 'port')); + + // Check if the path includes "index.php". + if (strpos($path, 'index.php') !== false) { + // Remove the index.php portion of the path. + $path = substr_replace($path, '', strpos($path, 'index.php'), 9); + } + + $path = rtrim($path, '/\\'); + + // Set the base URI both as just a path and as the full URI. + $this->set('uri.base.full', $host . $path . '/'); + $this->set('uri.base.host', $host); + $this->set('uri.base.path', $path . '/'); + + // Set the extended (non-base) part of the request URI as the route. + if (stripos($this->get('uri.request'), $this->get('uri.base.full')) === 0) { + $this->set('uri.route', substr_replace($this->get('uri.request'), '', 0, \strlen($this->get('uri.base.full')))); + } + + // Get an explicitly set media URI is present. + $mediaURI = trim($this->get('media_uri', '')); + + if ($mediaURI) { + if (strpos($mediaURI, '://') !== false) { + $this->set('uri.media.full', $mediaURI); + $this->set('uri.media.path', $mediaURI); + } else { + // Normalise slashes. + $mediaURI = trim($mediaURI, '/\\'); + $mediaURI = !empty($mediaURI) ? '/' . $mediaURI . '/' : '/'; + $this->set('uri.media.full', $this->get('uri.base.host') . $mediaURI); + $this->set('uri.media.path', $mediaURI); + } + } + // No explicit media URI was set, build it dynamically from the base uri. + else { + $this->set('uri.media.full', $this->get('uri.base.full') . 'media/'); + $this->set('uri.media.path', $this->get('uri.base.path') . 'media/'); + } + } + + /** + * Retrieve the application configuration object. + * + * @return Registry + * + * @since 4.0.0 + */ + public function getConfig() + { + return $this->config; + } } diff --git a/libraries/src/Association/AssociationExtensionHelper.php b/libraries/src/Association/AssociationExtensionHelper.php index 0815984d30bb4..792ab75e4d1d9 100644 --- a/libraries/src/Association/AssociationExtensionHelper.php +++ b/libraries/src/Association/AssociationExtensionHelper.php @@ -1,4 +1,5 @@ associationsSupport; - } - - /** - * Get the item types - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getItemTypes() - { - return $this->itemTypes; - } - - /** - * Get the associated items for an item - * - * @param string $typeName The item type - * @param int $itemId The id of item for which we need the associated items - * - * @return array - * - * @since 3.7.0 - */ - public function getAssociationList($typeName, $itemId) - { - $items = array(); - - $associations = $this->getAssociations($typeName, $itemId); - - foreach ($associations as $key => $association) - { - $items[$key] = ArrayHelper::fromObject($this->getItem($typeName, (int) $association->id), false); - } - - return $items; - } - - /** - * Get information about the type - * - * @param string $typeName The item type - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getType($typeName = '') - { - $fields = $this->getFieldsTemplate(); - $tables = array(); - $joins = array(); - $support = $this->getSupportTemplate(); - $title = ''; - - return array( - 'fields' => $fields, - 'support' => $support, - 'tables' => $tables, - 'joins' => $joins, - 'title' => $title - ); - } - - /** - * Get information about the fields the type provides - * - * @param string $typeName The item type - * - * @return array Array of support information - * - * @since 3.7.0 - */ - public function getTypeFields($typeName) - { - return $this->getTypeInformation($typeName, 'fields'); - } - - /** - * Get information about the fields the type provides - * - * @param string $typeName The item type - * - * @return array Array of support information - * - * @since 3.7.0 - */ - public function getTypeSupport($typeName) - { - return $this->getTypeInformation($typeName, 'support'); - } - - /** - * Get information about the tables the type use - * - * @param string $typeName The item type - * - * @return array Array of support information - * - * @since 3.7.0 - */ - public function getTypeTables($typeName) - { - return $this->getTypeInformation($typeName, 'tables'); - } - - /** - * Get information about the table joins for the type - * - * @param string $typeName The item type - * - * @return array Array of support information - * - * @since 3.7.0 - */ - public function getTypeJoins($typeName) - { - return $this->getTypeInformation($typeName, 'joins'); - } - - /** - * Get the type title - * - * @param string $typeName The item type - * - * @return string The type title - * - * @since 3.7.0 - */ - public function getTypeTitle($typeName) - { - $type = $this->getType($typeName); - - if (!\array_key_exists('title', $type)) - { - return ''; - } - - return $type['title']; - } - - /** - * Get information about the type - * - * @param string $typeName The item type - * @param string $part part of the information - * - * @return array Array of support information - * - * @since 3.7.0 - */ - private function getTypeInformation($typeName, $part = 'support') - { - $type = $this->getType($typeName); - - if (!\array_key_exists($part, $type)) - { - return array(); - } - - return $type[$part]; - } - - /** - * Get a table field name for a type - * - * @param string $typeName The item type - * @param string $fieldName The item type - * - * @return string - * - * @since 3.7.0 - */ - public function getTypeFieldName($typeName, $fieldName) - { - $fields = $this->getTypeFields($typeName); - - if (!\array_key_exists($fieldName, $fields)) - { - return ''; - } - - $tmp = $fields[$fieldName]; - $pos = strpos($tmp, '.'); - - if ($pos === false) - { - return $tmp; - } - - return substr($tmp, $pos + 1); - } - - /** - * Get default values for support array - * - * @return array - * - * @since 3.7.0 - */ - protected function getSupportTemplate() - { - return array( - 'state' => false, - 'acl' => false, - 'checkout' => false - ); - } - - /** - * Get default values for fields array - * - * @return array - * - * @since 3.7.0 - */ - protected function getFieldsTemplate() - { - return array( - 'id' => 'a.id', - 'title' => 'a.title', - 'alias' => 'a.alias', - 'ordering' => 'a.ordering', - 'menutype' => '', - 'level' => '', - 'catid' => 'a.catid', - 'language' => 'a.language', - 'access' => 'a.access', - 'state' => 'a.state', - 'created_user_id' => 'a.created_by', - 'checked_out' => 'a.checked_out', - 'checked_out_time' => 'a.checked_out_time' - ); - } + /** + * The extension name + * + * @var array $extension + * + * @since 3.7.0 + */ + protected $extension = 'com_??'; + + /** + * Array of item types + * + * @var array $itemTypes + * + * @since 3.7.0 + */ + protected $itemTypes = array(); + + /** + * Has the extension association support + * + * @var boolean $associationsSupport + * + * @since 3.7.0 + */ + protected $associationsSupport = false; + + /** + * Checks if the extension supports associations + * + * @return boolean Supports the extension associations + * + * @since 3.7.0 + */ + public function hasAssociationsSupport() + { + return $this->associationsSupport; + } + + /** + * Get the item types + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getItemTypes() + { + return $this->itemTypes; + } + + /** + * Get the associated items for an item + * + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public function getAssociationList($typeName, $itemId) + { + $items = array(); + + $associations = $this->getAssociations($typeName, $itemId); + + foreach ($associations as $key => $association) { + $items[$key] = ArrayHelper::fromObject($this->getItem($typeName, (int) $association->id), false); + } + + return $items; + } + + /** + * Get information about the type + * + * @param string $typeName The item type + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getType($typeName = '') + { + $fields = $this->getFieldsTemplate(); + $tables = array(); + $joins = array(); + $support = $this->getSupportTemplate(); + $title = ''; + + return array( + 'fields' => $fields, + 'support' => $support, + 'tables' => $tables, + 'joins' => $joins, + 'title' => $title + ); + } + + /** + * Get information about the fields the type provides + * + * @param string $typeName The item type + * + * @return array Array of support information + * + * @since 3.7.0 + */ + public function getTypeFields($typeName) + { + return $this->getTypeInformation($typeName, 'fields'); + } + + /** + * Get information about the fields the type provides + * + * @param string $typeName The item type + * + * @return array Array of support information + * + * @since 3.7.0 + */ + public function getTypeSupport($typeName) + { + return $this->getTypeInformation($typeName, 'support'); + } + + /** + * Get information about the tables the type use + * + * @param string $typeName The item type + * + * @return array Array of support information + * + * @since 3.7.0 + */ + public function getTypeTables($typeName) + { + return $this->getTypeInformation($typeName, 'tables'); + } + + /** + * Get information about the table joins for the type + * + * @param string $typeName The item type + * + * @return array Array of support information + * + * @since 3.7.0 + */ + public function getTypeJoins($typeName) + { + return $this->getTypeInformation($typeName, 'joins'); + } + + /** + * Get the type title + * + * @param string $typeName The item type + * + * @return string The type title + * + * @since 3.7.0 + */ + public function getTypeTitle($typeName) + { + $type = $this->getType($typeName); + + if (!\array_key_exists('title', $type)) { + return ''; + } + + return $type['title']; + } + + /** + * Get information about the type + * + * @param string $typeName The item type + * @param string $part part of the information + * + * @return array Array of support information + * + * @since 3.7.0 + */ + private function getTypeInformation($typeName, $part = 'support') + { + $type = $this->getType($typeName); + + if (!\array_key_exists($part, $type)) { + return array(); + } + + return $type[$part]; + } + + /** + * Get a table field name for a type + * + * @param string $typeName The item type + * @param string $fieldName The item type + * + * @return string + * + * @since 3.7.0 + */ + public function getTypeFieldName($typeName, $fieldName) + { + $fields = $this->getTypeFields($typeName); + + if (!\array_key_exists($fieldName, $fields)) { + return ''; + } + + $tmp = $fields[$fieldName]; + $pos = strpos($tmp, '.'); + + if ($pos === false) { + return $tmp; + } + + return substr($tmp, $pos + 1); + } + + /** + * Get default values for support array + * + * @return array + * + * @since 3.7.0 + */ + protected function getSupportTemplate() + { + return array( + 'state' => false, + 'acl' => false, + 'checkout' => false + ); + } + + /** + * Get default values for fields array + * + * @return array + * + * @since 3.7.0 + */ + protected function getFieldsTemplate() + { + return array( + 'id' => 'a.id', + 'title' => 'a.title', + 'alias' => 'a.alias', + 'ordering' => 'a.ordering', + 'menutype' => '', + 'level' => '', + 'catid' => 'a.catid', + 'language' => 'a.language', + 'access' => 'a.access', + 'state' => 'a.state', + 'created_user_id' => 'a.created_by', + 'checked_out' => 'a.checked_out', + 'checked_out_time' => 'a.checked_out_time' + ); + } } diff --git a/libraries/src/Association/AssociationExtensionInterface.php b/libraries/src/Association/AssociationExtensionInterface.php index 8d9d612126462..26b82b82f7367 100644 --- a/libraries/src/Association/AssociationExtensionInterface.php +++ b/libraries/src/Association/AssociationExtensionInterface.php @@ -1,4 +1,5 @@ associationExtension; - } + /** + * Returns the associations extension helper class. + * + * @return AssociationExtensionInterface + * + * @since 4.0.0 + */ + public function getAssociationsExtension(): AssociationExtensionInterface + { + return $this->associationExtension; + } - /** - * The association extension. - * - * @param AssociationExtensionInterface $associationExtension The extension - * - * @return void - * - * @since 4.0.0 - */ - public function setAssociationExtension(AssociationExtensionInterface $associationExtension) - { - $this->associationExtension = $associationExtension; - } + /** + * The association extension. + * + * @param AssociationExtensionInterface $associationExtension The extension + * + * @return void + * + * @since 4.0.0 + */ + public function setAssociationExtension(AssociationExtensionInterface $associationExtension) + { + $this->associationExtension = $associationExtension; + } } diff --git a/libraries/src/Authentication/Authentication.php b/libraries/src/Authentication/Authentication.php index 05f2b629372c2..61453d01e06d2 100644 --- a/libraries/src/Authentication/Authentication.php +++ b/libraries/src/Authentication/Authentication.php @@ -1,4 +1,5 @@ get('dispatcher'); - } - - $this->setDispatcher($dispatcher); - $this->pluginType = $pluginType; - - $isLoaded = PluginHelper::importPlugin($this->pluginType); - - if (!$isLoaded) - { - Log::add(Text::_('JLIB_USER_ERROR_AUTHENTICATION_LIBRARIES'), Log::WARNING, 'jerror'); - } - } - - /** - * Returns the global authentication object, only creating it - * if it doesn't already exist. - * - * @param string $pluginType The plugin type to run authorisation and authentication on - * - * @return Authentication The global Authentication object - * - * @since 1.7.0 - */ - public static function getInstance(string $pluginType = 'authentication') - { - if (empty(self::$instance[$pluginType])) - { - self::$instance[$pluginType] = new static($pluginType); - } - - return self::$instance[$pluginType]; - } - - /** - * Finds out if a set of login credentials are valid by asking all observing - * objects to run their respective authentication routines. - * - * @param array $credentials Array holding the user credentials. - * @param array $options Array holding user options. - * - * @return AuthenticationResponse Response object with status variable filled in for last plugin or first successful plugin. - * - * @see AuthenticationResponse - * @since 1.7.0 - */ - public function authenticate($credentials, $options = array()) - { - // Get plugins - $plugins = PluginHelper::getPlugin($this->pluginType); - - // Create authentication response - $response = new AuthenticationResponse; - - /* - * Loop through the plugins and check if the credentials can be used to authenticate - * the user - * - * Any errors raised in the plugin should be returned via the AuthenticationResponse - * and handled appropriately. - */ - foreach ($plugins as $plugin) - { - $plugin = Factory::getApplication()->bootPlugin($plugin->name, $plugin->type); - - if (!method_exists($plugin, 'onUserAuthenticate')) - { - // Bail here if the plugin can't be created - Log::add(Text::sprintf('JLIB_USER_ERROR_AUTHENTICATION_FAILED_LOAD_PLUGIN', $plugin->name), Log::WARNING, 'jerror'); - continue; - } - - // Try to authenticate - $plugin->onUserAuthenticate($credentials, $options, $response); - - // If authentication is successful break out of the loop - if ($response->status === self::STATUS_SUCCESS) - { - if (empty($response->type)) - { - $response->type = $plugin->_name ?? $plugin->name; - } - - break; - } - } - - if (empty($response->username)) - { - $response->username = $credentials['username']; - } - - if (empty($response->fullname)) - { - $response->fullname = $credentials['username']; - } - - if (empty($response->password) && isset($credentials['password'])) - { - $response->password = $credentials['password']; - } - - return $response; - } - - /** - * Authorises that a particular user should be able to login - * - * @param AuthenticationResponse $response response including username of the user to authorise - * @param array $options list of options - * - * @return AuthenticationResponse[] Array of authentication response objects - * - * @since 1.7.0 - * @throws \Exception - */ - public function authorise($response, $options = array()) - { - // Get plugins in case they haven't been imported already - PluginHelper::importPlugin('user'); - $results = Factory::getApplication()->triggerEvent('onUserAuthorisation', array($response, $options)); - - return $results; - } + use DispatcherAwareTrait; + + /** + * This is the status code returned when the authentication is success (permit login) + * + * @var integer + * @since 1.7.0 + */ + const STATUS_SUCCESS = 1; + + /** + * Status to indicate cancellation of authentication (unused) + * + * @var integer + * @since 1.7.0 + */ + const STATUS_CANCEL = 2; + + /** + * This is the status code returned when the authentication failed (prevent login if no success) + * + * @var integer + * @since 1.7.0 + */ + const STATUS_FAILURE = 4; + + /** + * This is the status code returned when the account has expired (prevent login) + * + * @var integer + * @since 1.7.0 + */ + const STATUS_EXPIRED = 8; + + /** + * This is the status code returned when the account has been denied (prevent login) + * + * @var integer + * @since 1.7.0 + */ + const STATUS_DENIED = 16; + + /** + * This is the status code returned when the account doesn't exist (not an error) + * + * @var integer + * @since 1.7.0 + */ + const STATUS_UNKNOWN = 32; + + /** + * @var Authentication[] JAuthentication instances container. + * @since 1.7.3 + */ + protected static $instance = []; + + /** + * Plugin Type to run + * + * @var string + * @since 4.0.0 + */ + protected $pluginType; + + /** + * Constructor + * + * @param string $pluginType The plugin type to run authorisation and authentication on + * @param DispatcherInterface $dispatcher The event dispatcher we're going to use + * + * @since 1.7.0 + */ + public function __construct(string $pluginType = 'authentication', DispatcherInterface $dispatcher = null) + { + // Set the dispatcher + if (!\is_object($dispatcher)) { + $dispatcher = Factory::getContainer()->get('dispatcher'); + } + + $this->setDispatcher($dispatcher); + $this->pluginType = $pluginType; + + $isLoaded = PluginHelper::importPlugin($this->pluginType); + + if (!$isLoaded) { + Log::add(Text::_('JLIB_USER_ERROR_AUTHENTICATION_LIBRARIES'), Log::WARNING, 'jerror'); + } + } + + /** + * Returns the global authentication object, only creating it + * if it doesn't already exist. + * + * @param string $pluginType The plugin type to run authorisation and authentication on + * + * @return Authentication The global Authentication object + * + * @since 1.7.0 + */ + public static function getInstance(string $pluginType = 'authentication') + { + if (empty(self::$instance[$pluginType])) { + self::$instance[$pluginType] = new static($pluginType); + } + + return self::$instance[$pluginType]; + } + + /** + * Finds out if a set of login credentials are valid by asking all observing + * objects to run their respective authentication routines. + * + * @param array $credentials Array holding the user credentials. + * @param array $options Array holding user options. + * + * @return AuthenticationResponse Response object with status variable filled in for last plugin or first successful plugin. + * + * @see AuthenticationResponse + * @since 1.7.0 + */ + public function authenticate($credentials, $options = array()) + { + // Get plugins + $plugins = PluginHelper::getPlugin($this->pluginType); + + // Create authentication response + $response = new AuthenticationResponse(); + + /* + * Loop through the plugins and check if the credentials can be used to authenticate + * the user + * + * Any errors raised in the plugin should be returned via the AuthenticationResponse + * and handled appropriately. + */ + foreach ($plugins as $plugin) { + $plugin = Factory::getApplication()->bootPlugin($plugin->name, $plugin->type); + + if (!method_exists($plugin, 'onUserAuthenticate')) { + // Bail here if the plugin can't be created + Log::add(Text::sprintf('JLIB_USER_ERROR_AUTHENTICATION_FAILED_LOAD_PLUGIN', $plugin->name), Log::WARNING, 'jerror'); + continue; + } + + // Try to authenticate + $plugin->onUserAuthenticate($credentials, $options, $response); + + // If authentication is successful break out of the loop + if ($response->status === self::STATUS_SUCCESS) { + if (empty($response->type)) { + $response->type = $plugin->_name ?? $plugin->name; + } + + break; + } + } + + if (empty($response->username)) { + $response->username = $credentials['username']; + } + + if (empty($response->fullname)) { + $response->fullname = $credentials['username']; + } + + if (empty($response->password) && isset($credentials['password'])) { + $response->password = $credentials['password']; + } + + return $response; + } + + /** + * Authorises that a particular user should be able to login + * + * @param AuthenticationResponse $response response including username of the user to authorise + * @param array $options list of options + * + * @return AuthenticationResponse[] Array of authentication response objects + * + * @since 1.7.0 + * @throws \Exception + */ + public function authorise($response, $options = array()) + { + // Get plugins in case they haven't been imported already + PluginHelper::importPlugin('user'); + $results = Factory::getApplication()->triggerEvent('onUserAuthorisation', array($response, $options)); + + return $results; + } } diff --git a/libraries/src/Authentication/AuthenticationResponse.php b/libraries/src/Authentication/AuthenticationResponse.php index 8d4b7dba63fc7..41a0dfc1b33af 100644 --- a/libraries/src/Authentication/AuthenticationResponse.php +++ b/libraries/src/Authentication/AuthenticationResponse.php @@ -1,4 +1,5 @@ handlers[] = $handler; - } + /** + * Add a handler to the chain + * + * @param HandlerInterface $handler The password handler to add + * + * @return void + * + * @since 4.0.0 + */ + public function addHandler(HandlerInterface $handler) + { + $this->handlers[] = $handler; + } - /** - * Check if the password requires rehashing - * - * @param string $hash The password hash to check - * - * @return boolean - * - * @since 4.0.0 - */ - public function checkIfRehashNeeded(string $hash): bool - { - foreach ($this->handlers as $handler) - { - if ($handler instanceof CheckIfRehashNeededHandlerInterface && $handler->isSupported() && $handler->checkIfRehashNeeded($hash)) - { - return true; - } - } + /** + * Check if the password requires rehashing + * + * @param string $hash The password hash to check + * + * @return boolean + * + * @since 4.0.0 + */ + public function checkIfRehashNeeded(string $hash): bool + { + foreach ($this->handlers as $handler) { + if ($handler instanceof CheckIfRehashNeededHandlerInterface && $handler->isSupported() && $handler->checkIfRehashNeeded($hash)) { + return true; + } + } - return false; - } + return false; + } - /** - * Generate a hash for a plaintext password - * - * @param string $plaintext The plaintext password to validate - * @param array $options Options for the hashing operation - * - * @return void - * - * @since 4.0.0 - * @throws \RuntimeException - */ - public function hashPassword($plaintext, array $options = []) - { - throw new \RuntimeException('The chained password handler cannot be used to hash a password'); - } + /** + * Generate a hash for a plaintext password + * + * @param string $plaintext The plaintext password to validate + * @param array $options Options for the hashing operation + * + * @return void + * + * @since 4.0.0 + * @throws \RuntimeException + */ + public function hashPassword($plaintext, array $options = []) + { + throw new \RuntimeException('The chained password handler cannot be used to hash a password'); + } - /** - * Check that the password handler is supported in this environment - * - * @return boolean - * - * @since 4.0.0 - */ - public static function isSupported() - { - return true; - } + /** + * Check that the password handler is supported in this environment + * + * @return boolean + * + * @since 4.0.0 + */ + public static function isSupported() + { + return true; + } - /** - * Validate a password - * - * @param string $plaintext The plain text password to validate - * @param string $hashed The password hash to validate against - * - * @return boolean - * - * @since 4.0.0 - */ - public function validatePassword($plaintext, $hashed) - { - foreach ($this->handlers as $handler) - { - if ($handler->isSupported() && $handler->validatePassword($plaintext, $hashed)) - { - return true; - } - } + /** + * Validate a password + * + * @param string $plaintext The plain text password to validate + * @param string $hashed The password hash to validate against + * + * @return boolean + * + * @since 4.0.0 + */ + public function validatePassword($plaintext, $hashed) + { + foreach ($this->handlers as $handler) { + if ($handler->isSupported() && $handler->validatePassword($plaintext, $hashed)) { + return true; + } + } - return false; - } + return false; + } } diff --git a/libraries/src/Authentication/Password/CheckIfRehashNeededHandlerInterface.php b/libraries/src/Authentication/Password/CheckIfRehashNeededHandlerInterface.php index 9974a0b07ecd3..90dbe3c1e53be 100644 --- a/libraries/src/Authentication/Password/CheckIfRehashNeededHandlerInterface.php +++ b/libraries/src/Authentication/Password/CheckIfRehashNeededHandlerInterface.php @@ -1,4 +1,5 @@ getPasswordHash()->HashPassword($plaintext); - } + /** + * Generate a hash for a plaintext password + * + * @param string $plaintext The plaintext password to validate + * @param array $options Options for the hashing operation + * + * @return string + * + * @since 4.0.0 + */ + public function hashPassword($plaintext, array $options = []) + { + return $this->getPasswordHash()->HashPassword($plaintext); + } - /** - * Check that the password handler is supported in this environment - * - * @return boolean - * - * @since 4.0.0 - */ - public static function isSupported() - { - return class_exists(\PasswordHash::class); - } + /** + * Check that the password handler is supported in this environment + * + * @return boolean + * + * @since 4.0.0 + */ + public static function isSupported() + { + return class_exists(\PasswordHash::class); + } - /** - * Validate a password - * - * @param string $plaintext The plain text password to validate - * @param string $hashed The password hash to validate against - * - * @return boolean - * - * @since 4.0.0 - */ - public function validatePassword($plaintext, $hashed) - { - return $this->getPasswordHash()->CheckPassword($plaintext, $hashed); - } + /** + * Validate a password + * + * @param string $plaintext The plain text password to validate + * @param string $hashed The password hash to validate against + * + * @return boolean + * + * @since 4.0.0 + */ + public function validatePassword($plaintext, $hashed) + { + return $this->getPasswordHash()->CheckPassword($plaintext, $hashed); + } - /** - * Get an instance of the PasswordHash class - * - * @return \PasswordHash - * - * @since 4.0.0 - */ - private function getPasswordHash(): \PasswordHash - { - return new \PasswordHash(10, true); - } + /** + * Get an instance of the PasswordHash class + * + * @return \PasswordHash + * + * @since 4.0.0 + */ + private function getPasswordHash(): \PasswordHash + { + return new \PasswordHash(10, true); + } } diff --git a/libraries/src/Authentication/ProviderAwareAuthenticationPluginInterface.php b/libraries/src/Authentication/ProviderAwareAuthenticationPluginInterface.php index 759eaa46a5bb3..6f1768f202321 100644 --- a/libraries/src/Authentication/ProviderAwareAuthenticationPluginInterface.php +++ b/libraries/src/Authentication/ProviderAwareAuthenticationPluginInterface.php @@ -1,4 +1,5 @@ loader = $loader; - } - - /** - * Loads the given class or interface. - * - * @param string $class The name of the class - * - * @return boolean|null True if loaded, null otherwise - * - * @since 3.4 - */ - public function loadClass($class) - { - if ($result = $this->loader->loadClass($class)) - { - \JLoader::applyAliasFor($class); - } - - return $result; - } + /** + * The Composer class loader + * + * @var ComposerClassLoader + * @since 3.4 + */ + private $loader; + + /** + * Constructor + * + * @param ComposerClassLoader $loader Composer autoloader + * + * @since 3.4 + */ + public function __construct(ComposerClassLoader $loader) + { + $this->loader = $loader; + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * + * @return boolean|null True if loaded, null otherwise + * + * @since 3.4 + */ + public function loadClass($class) + { + if ($result = $this->loader->loadClass($class)) { + \JLoader::applyAliasFor($class); + } + + return $result; + } } diff --git a/libraries/src/Button/ActionButton.php b/libraries/src/Button/ActionButton.php index d4da31ef276e7..9253815bddcdb 100644 --- a/libraries/src/Button/ActionButton.php +++ b/libraries/src/Button/ActionButton.php @@ -1,4 +1,5 @@ null, - 'task' => '', - 'icon' => 'question', - 'title' => 'Unknown state', - 'options' => [ - 'disabled' => false, - 'only_icon' => false, - 'tip' => true, - 'tip_title' => '', - 'task_prefix' => '', - 'checkbox_name' => 'cb', - ], - ]; - - /** - * Options of this button set. - * - * @var Registry - * - * @since 4.0.0 - */ - protected $options; - - /** - * The layout path to render. - * - * @var string - * - * @since 4.0.0 - */ - protected $layout = 'joomla.button.action-button'; - - /** - * ActionButton constructor. - * - * @param array $options The options for all buttons in this group. - * - * @since 4.0.0 - */ - public function __construct(array $options = []) - { - $this->options = new Registry($options); - - // Replace some dynamic values - $this->unknownState['title'] = Text::_('JLIB_HTML_UNKNOWN_STATE'); - - $this->preprocess(); - } - - /** - * Configure this object. - * - * @return void - * - * @since 4.0.0 - */ - protected function preprocess() - { - // Implement this method. - } - - /** - * Add a state profile. - * - * @param integer $value The value of this state. - * @param string $task The task you want to execute after click this button. - * @param string $icon The icon to display for user. - * @param string $title Title text will show if we enable tooltips. - * @param array $options The button options, will override group options. - * - * @return static Return self to support chaining. - * - * @since 4.0.0 - */ - public function addState(int $value, string $task, string $icon = 'ok', string $title = '', array $options = []): self - { - // Force type to prevent null data - $this->states[$value] = [ - 'value' => $value, - 'task' => $task, - 'icon' => $icon, - 'title' => $title, - 'options' => $options - ]; - - return $this; - } - - /** - * Get state profile by value name. - * - * @param integer $value The value name we want to get. - * - * @return array|null Return state profile or NULL. - * - * @since 4.0.0 - */ - public function getState(int $value): ?array - { - return $this->states[$value] ?? null; - } - - /** - * Remove a state by value name. - * - * @param integer $value Remove state by this value. - * - * @return static Return to support chaining. - * - * @since 4.0.0 - */ - public function removeState(int $value): self - { - if (isset($this->states[$value])) - { - unset($this->states[$value]); - } - - return $this; - } - - /** - * Render action button by item value. - * - * @param integer|null $value Current value of this item. - * @param integer|null $row The row number of this item. - * @param array $options The options to override group options. - * - * @return string Rendered HTML. - * - * @since 4.0.0 - * - * @throws \InvalidArgumentException - */ - public function render(?int $value = null, ?int $row = null, array $options = []): string - { - $data = $this->getState($value) ?? $this->unknownState; - - $data = ArrayHelper::mergeRecursive( - $this->unknownState, - $data, - [ - 'options' => $this->options->toArray() - ], - [ - 'options' => $options - ] - ); - - $data['row'] = $row; - $data['icon'] = $this->fetchIconClass($data['icon']); - - return LayoutHelper::render($this->layout, $data); - } - - /** - * Render to string. - * - * @return string - * - * @since 4.0.0 - */ - public function __toString(): string - { - try - { - return $this->render(); - } - catch (\Throwable $e) - { - return (string) $e; - } - } - - /** - * Method to get property layout. - * - * @return string - * - * @since 4.0.0 - */ - public function getLayout(): string - { - return $this->layout; - } - - /** - * Method to set property template. - * - * @param string $layout The layout path. - * - * @return static Return self to support chaining. - * - * @since 4.0.0 - */ - public function setLayout(string $layout): self - { - $this->layout = $layout; - - return $this; - } - - /** - * Method to get property options. - * - * @return array - * - * @since 4.0.0 - */ - public function getOptions(): array - { - return (array) $this->options->toArray(); - } - - /** - * Method to set property options. - * - * @param array $options The options of this button group. - * - * @return static Return self to support chaining. - * - * @since 4.0.0 - */ - public function setOptions(array $options): self - { - $this->options = new Registry($options); - - return $this; - } - - /** - * Get an option value. - * - * @param string $name The option name. - * @param mixed $default Default value if not exists. - * - * @return mixed Return option value or default value. - * - * @since 4.0.0 - */ - public function getOption(string $name, $default = null) - { - return $this->options->get($name, $default); - } - - /** - * Set option value. - * - * @param string $name The option name. - * @param mixed $value The option value. - * - * @return static Return self to support chaining. - * - * @since 4.0.0 - */ - public function setOption(string $name, $value): self - { - $this->options->set($name, $value); - - return $this; - } - - /** - * Method to get the CSS class name for an icon identifier. - * - * Can be redefined in the final class. - * - * @param string $identifier Icon identification string. - * - * @return string CSS class name. - * - * @since 4.0.0 - */ - public function fetchIconClass(string $identifier): string - { - // It's an ugly hack, but this allows templates to define the icon classes for the toolbar - $layout = new FileLayout('joomla.button.iconclass'); - - return $layout->render(array('icon' => $identifier)); - } + /** + * The button states profiles. + * + * @var array + * + * @since 4.0.0 + */ + protected $states = []; + + /** + * Default options for unknown state. + * + * @var array + * + * @since 4.0.0 + */ + protected $unknownState = [ + 'value' => null, + 'task' => '', + 'icon' => 'question', + 'title' => 'Unknown state', + 'options' => [ + 'disabled' => false, + 'only_icon' => false, + 'tip' => true, + 'tip_title' => '', + 'task_prefix' => '', + 'checkbox_name' => 'cb', + ], + ]; + + /** + * Options of this button set. + * + * @var Registry + * + * @since 4.0.0 + */ + protected $options; + + /** + * The layout path to render. + * + * @var string + * + * @since 4.0.0 + */ + protected $layout = 'joomla.button.action-button'; + + /** + * ActionButton constructor. + * + * @param array $options The options for all buttons in this group. + * + * @since 4.0.0 + */ + public function __construct(array $options = []) + { + $this->options = new Registry($options); + + // Replace some dynamic values + $this->unknownState['title'] = Text::_('JLIB_HTML_UNKNOWN_STATE'); + + $this->preprocess(); + } + + /** + * Configure this object. + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocess() + { + // Implement this method. + } + + /** + * Add a state profile. + * + * @param integer $value The value of this state. + * @param string $task The task you want to execute after click this button. + * @param string $icon The icon to display for user. + * @param string $title Title text will show if we enable tooltips. + * @param array $options The button options, will override group options. + * + * @return static Return self to support chaining. + * + * @since 4.0.0 + */ + public function addState(int $value, string $task, string $icon = 'ok', string $title = '', array $options = []): self + { + // Force type to prevent null data + $this->states[$value] = [ + 'value' => $value, + 'task' => $task, + 'icon' => $icon, + 'title' => $title, + 'options' => $options + ]; + + return $this; + } + + /** + * Get state profile by value name. + * + * @param integer $value The value name we want to get. + * + * @return array|null Return state profile or NULL. + * + * @since 4.0.0 + */ + public function getState(int $value): ?array + { + return $this->states[$value] ?? null; + } + + /** + * Remove a state by value name. + * + * @param integer $value Remove state by this value. + * + * @return static Return to support chaining. + * + * @since 4.0.0 + */ + public function removeState(int $value): self + { + if (isset($this->states[$value])) { + unset($this->states[$value]); + } + + return $this; + } + + /** + * Render action button by item value. + * + * @param integer|null $value Current value of this item. + * @param integer|null $row The row number of this item. + * @param array $options The options to override group options. + * + * @return string Rendered HTML. + * + * @since 4.0.0 + * + * @throws \InvalidArgumentException + */ + public function render(?int $value = null, ?int $row = null, array $options = []): string + { + $data = $this->getState($value) ?? $this->unknownState; + + $data = ArrayHelper::mergeRecursive( + $this->unknownState, + $data, + [ + 'options' => $this->options->toArray() + ], + [ + 'options' => $options + ] + ); + + $data['row'] = $row; + $data['icon'] = $this->fetchIconClass($data['icon']); + + return LayoutHelper::render($this->layout, $data); + } + + /** + * Render to string. + * + * @return string + * + * @since 4.0.0 + */ + public function __toString(): string + { + try { + return $this->render(); + } catch (\Throwable $e) { + return (string) $e; + } + } + + /** + * Method to get property layout. + * + * @return string + * + * @since 4.0.0 + */ + public function getLayout(): string + { + return $this->layout; + } + + /** + * Method to set property template. + * + * @param string $layout The layout path. + * + * @return static Return self to support chaining. + * + * @since 4.0.0 + */ + public function setLayout(string $layout): self + { + $this->layout = $layout; + + return $this; + } + + /** + * Method to get property options. + * + * @return array + * + * @since 4.0.0 + */ + public function getOptions(): array + { + return (array) $this->options->toArray(); + } + + /** + * Method to set property options. + * + * @param array $options The options of this button group. + * + * @return static Return self to support chaining. + * + * @since 4.0.0 + */ + public function setOptions(array $options): self + { + $this->options = new Registry($options); + + return $this; + } + + /** + * Get an option value. + * + * @param string $name The option name. + * @param mixed $default Default value if not exists. + * + * @return mixed Return option value or default value. + * + * @since 4.0.0 + */ + public function getOption(string $name, $default = null) + { + return $this->options->get($name, $default); + } + + /** + * Set option value. + * + * @param string $name The option name. + * @param mixed $value The option value. + * + * @return static Return self to support chaining. + * + * @since 4.0.0 + */ + public function setOption(string $name, $value): self + { + $this->options->set($name, $value); + + return $this; + } + + /** + * Method to get the CSS class name for an icon identifier. + * + * Can be redefined in the final class. + * + * @param string $identifier Icon identification string. + * + * @return string CSS class name. + * + * @since 4.0.0 + */ + public function fetchIconClass(string $identifier): string + { + // It's an ugly hack, but this allows templates to define the icon classes for the toolbar + $layout = new FileLayout('joomla.button.iconclass'); + + return $layout->render(array('icon' => $identifier)); + } } diff --git a/libraries/src/Button/FeaturedButton.php b/libraries/src/Button/FeaturedButton.php index 818f2b05f3b63..fac1b09cd9b0d 100644 --- a/libraries/src/Button/FeaturedButton.php +++ b/libraries/src/Button/FeaturedButton.php @@ -1,4 +1,5 @@ addState(0, 'featured', 'icon-unfeatured', - Text::_('JGLOBAL_TOGGLE_FEATURED'), ['tip_title' => Text::_('JUNFEATURED')] - ); - $this->addState(1, 'unfeatured', 'icon-color-featured icon-star', - Text::_('JGLOBAL_TOGGLE_FEATURED'), ['tip_title' => Text::_('JFEATURED')] - ); - } - - /** - * Render action button by item value. - * - * @param integer|null $value Current value of this item. - * @param integer|null $row The row number of this item. - * @param array $options The options to override group options. - * @param string|Date $featuredUp The date which item featured up. - * @param string|Date $featuredDown The date which item featured down. - * - * @return string Rendered HTML. - * - * @since 4.0.0 - */ - public function render(?int $value = null, ?int $row = null, array $options = [], $featuredUp = null, $featuredDown = null): string - { - if ($featuredUp || $featuredDown) - { - $bakState = $this->getState($value); - $default = $this->getState($value) ?? $this->unknownState; - - $nowDate = Factory::getDate()->toUnix(); - - $tz = Factory::getUser()->getTimezone(); - - if (!is_null($featuredUp)) - { - $featuredUp = Factory::getDate($featuredUp, 'UTC')->setTimezone($tz); - } - - if (!is_null($featuredDown)) - { - $featuredDown = Factory::getDate($featuredDown, 'UTC')->setTimezone($tz); - } - - // Add tips and special titles - // Create special titles for featured items - if ($value === 1) - { - // Create tip text, only we have featured up or down settings - $tips = []; - - if ($featuredUp) - { - $tips[] = Text::sprintf('JLIB_HTML_FEATURED_STARTED', HTMLHelper::_('date', $featuredUp, Text::_('DATE_FORMAT_LC5'), 'UTC')); - } - - if ($featuredDown) - { - $tips[] = Text::sprintf('JLIB_HTML_FEATURED_FINISHED', HTMLHelper::_('date', $featuredDown, Text::_('DATE_FORMAT_LC5'), 'UTC')); - } - - $tip = empty($tips) ? false : implode('
    ', $tips); - - $default['title'] = $tip; - - $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_ITEM'); - - if ($featuredUp && $nowDate < $featuredUp->toUnix()) - { - $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_PENDING_ITEM'); - $default['icon'] = 'pending'; - } - - if ($featuredDown && $nowDate > $featuredDown->toUnix()) - { - $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_EXPIRED_ITEM'); - $default['icon'] = 'expired'; - } - } - - $this->states[$value] = $default; - - $html = parent::render($value, $row, $options); - - $this->states[$value] = $bakState; - - return $html; - } - - return parent::render($value, $row, $options); - } + /** + * Configure this object. + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocess() + { + $this->addState( + 0, + 'featured', + 'icon-unfeatured', + Text::_('JGLOBAL_TOGGLE_FEATURED'), + ['tip_title' => Text::_('JUNFEATURED')] + ); + $this->addState( + 1, + 'unfeatured', + 'icon-color-featured icon-star', + Text::_('JGLOBAL_TOGGLE_FEATURED'), + ['tip_title' => Text::_('JFEATURED')] + ); + } + + /** + * Render action button by item value. + * + * @param integer|null $value Current value of this item. + * @param integer|null $row The row number of this item. + * @param array $options The options to override group options. + * @param string|Date $featuredUp The date which item featured up. + * @param string|Date $featuredDown The date which item featured down. + * + * @return string Rendered HTML. + * + * @since 4.0.0 + */ + public function render(?int $value = null, ?int $row = null, array $options = [], $featuredUp = null, $featuredDown = null): string + { + if ($featuredUp || $featuredDown) { + $bakState = $this->getState($value); + $default = $this->getState($value) ?? $this->unknownState; + + $nowDate = Factory::getDate()->toUnix(); + + $tz = Factory::getUser()->getTimezone(); + + if (!is_null($featuredUp)) { + $featuredUp = Factory::getDate($featuredUp, 'UTC')->setTimezone($tz); + } + + if (!is_null($featuredDown)) { + $featuredDown = Factory::getDate($featuredDown, 'UTC')->setTimezone($tz); + } + + // Add tips and special titles + // Create special titles for featured items + if ($value === 1) { + // Create tip text, only we have featured up or down settings + $tips = []; + + if ($featuredUp) { + $tips[] = Text::sprintf('JLIB_HTML_FEATURED_STARTED', HTMLHelper::_('date', $featuredUp, Text::_('DATE_FORMAT_LC5'), 'UTC')); + } + + if ($featuredDown) { + $tips[] = Text::sprintf('JLIB_HTML_FEATURED_FINISHED', HTMLHelper::_('date', $featuredDown, Text::_('DATE_FORMAT_LC5'), 'UTC')); + } + + $tip = empty($tips) ? false : implode('
    ', $tips); + + $default['title'] = $tip; + + $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_ITEM'); + + if ($featuredUp && $nowDate < $featuredUp->toUnix()) { + $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_PENDING_ITEM'); + $default['icon'] = 'pending'; + } + + if ($featuredDown && $nowDate > $featuredDown->toUnix()) { + $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_EXPIRED_ITEM'); + $default['icon'] = 'expired'; + } + } + + $this->states[$value] = $default; + + $html = parent::render($value, $row, $options); + + $this->states[$value] = $bakState; + + return $html; + } + + return parent::render($value, $row, $options); + } } diff --git a/libraries/src/Button/PublishedButton.php b/libraries/src/Button/PublishedButton.php index 064e34b0154e4..7fff3a9d70ab9 100644 --- a/libraries/src/Button/PublishedButton.php +++ b/libraries/src/Button/PublishedButton.php @@ -1,4 +1,5 @@ addState(1, 'unpublish', 'publish', Text::_('JLIB_HTML_UNPUBLISH_ITEM'), ['tip_title' => Text::_('JPUBLISHED')]); - $this->addState(0, 'publish', 'unpublish', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JUNPUBLISHED')]); - $this->addState(2, 'unpublish', 'archive', Text::_('JLIB_HTML_UNPUBLISH_ITEM'), ['tip_title' => Text::_('JARCHIVED')]); - $this->addState(-2, 'publish', 'trash', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JTRASHED')]); - } - - /** - * Render action button by item value. - * - * @param integer|null $value Current value of this item. - * @param integer|null $row The row number of this item. - * @param array $options The options to override group options. - * @param string|Date $publishUp The date which item publish up. - * @param string|Date $publishDown The date which item publish down. - * - * @return string Rendered HTML. - * - * @since 4.0.0 - */ - public function render(?int $value = null, ?int $row = null, array $options = [], $publishUp = null, $publishDown = null): string - { - if ($publishUp || $publishDown) - { - $bakState = $this->getState($value); - $default = $this->getState($value) ?? $this->unknownState; - - $nullDate = Factory::getDbo()->getNullDate(); - $nowDate = Factory::getDate()->toUnix(); - - $tz = Factory::getUser()->getTimezone(); - - $publishUp = ($publishUp !== null && $publishUp !== $nullDate) ? Factory::getDate($publishUp, 'UTC')->setTimezone($tz) : false; - $publishDown = ($publishDown !== null && $publishDown !== $nullDate) ? Factory::getDate($publishDown, 'UTC')->setTimezone($tz) : false; - - // Add tips and special titles - // Create special titles for published items - if ($value === 1) - { - // Create tip text, only we have publish up or down settings - $tips = array(); - - if ($publishUp) - { - $tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_START', HTMLHelper::_('date', $publishUp, Text::_('DATE_FORMAT_LC5'), 'UTC')); - $tips[] = Text::_('JLIB_HTML_PUBLISHED_UNPUBLISH'); - } - - if ($publishDown) - { - $tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_FINISHED', HTMLHelper::_('date', $publishDown, Text::_('DATE_FORMAT_LC5'), 'UTC')); - } - - $tip = empty($tips) ? false : implode('
    ', $tips); - - $default['title'] = $tip; - - $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_ITEM'); - - if ($publishUp && $nowDate < $publishUp->toUnix()) - { - $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_PENDING_ITEM'); - $default['icon'] = 'pending'; - } - - if ($publishDown && $nowDate > $publishDown->toUnix()) - { - $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_EXPIRED_ITEM'); - $default['icon'] = 'expired'; - } - - if (array_key_exists('category_published', $options)) - { - $categoryPublished = $options['category_published']; - - if ($categoryPublished === 0) - { - $options['tip_title'] = Text::_('JLIB_HTML_ITEM_PUBLISHED_BUT_CATEGORY_UNPUBLISHED'); - $default['icon'] = 'expired'; - } - - if ($categoryPublished === -2) - { - $options['tip_title'] = Text::_('JLIB_HTML_ITEM_PUBLISHED_BUT_CATEGORY_TRASHED'); - $default['icon'] = 'expired'; - } - } - } - - $this->states[$value] = $default; - - $html = parent::render($value, $row, $options); - - $this->states[$value] = $bakState; - - return $html; - } - - return parent::render($value, $row, $options); - } + /** + * Configure this object. + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocess() + { + $this->addState(1, 'unpublish', 'publish', Text::_('JLIB_HTML_UNPUBLISH_ITEM'), ['tip_title' => Text::_('JPUBLISHED')]); + $this->addState(0, 'publish', 'unpublish', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JUNPUBLISHED')]); + $this->addState(2, 'unpublish', 'archive', Text::_('JLIB_HTML_UNPUBLISH_ITEM'), ['tip_title' => Text::_('JARCHIVED')]); + $this->addState(-2, 'publish', 'trash', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JTRASHED')]); + } + + /** + * Render action button by item value. + * + * @param integer|null $value Current value of this item. + * @param integer|null $row The row number of this item. + * @param array $options The options to override group options. + * @param string|Date $publishUp The date which item publish up. + * @param string|Date $publishDown The date which item publish down. + * + * @return string Rendered HTML. + * + * @since 4.0.0 + */ + public function render(?int $value = null, ?int $row = null, array $options = [], $publishUp = null, $publishDown = null): string + { + if ($publishUp || $publishDown) { + $bakState = $this->getState($value); + $default = $this->getState($value) ?? $this->unknownState; + + $nullDate = Factory::getDbo()->getNullDate(); + $nowDate = Factory::getDate()->toUnix(); + + $tz = Factory::getUser()->getTimezone(); + + $publishUp = ($publishUp !== null && $publishUp !== $nullDate) ? Factory::getDate($publishUp, 'UTC')->setTimezone($tz) : false; + $publishDown = ($publishDown !== null && $publishDown !== $nullDate) ? Factory::getDate($publishDown, 'UTC')->setTimezone($tz) : false; + + // Add tips and special titles + // Create special titles for published items + if ($value === 1) { + // Create tip text, only we have publish up or down settings + $tips = array(); + + if ($publishUp) { + $tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_START', HTMLHelper::_('date', $publishUp, Text::_('DATE_FORMAT_LC5'), 'UTC')); + $tips[] = Text::_('JLIB_HTML_PUBLISHED_UNPUBLISH'); + } + + if ($publishDown) { + $tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_FINISHED', HTMLHelper::_('date', $publishDown, Text::_('DATE_FORMAT_LC5'), 'UTC')); + } + + $tip = empty($tips) ? false : implode('
    ', $tips); + + $default['title'] = $tip; + + $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_ITEM'); + + if ($publishUp && $nowDate < $publishUp->toUnix()) { + $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_PENDING_ITEM'); + $default['icon'] = 'pending'; + } + + if ($publishDown && $nowDate > $publishDown->toUnix()) { + $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_EXPIRED_ITEM'); + $default['icon'] = 'expired'; + } + + if (array_key_exists('category_published', $options)) { + $categoryPublished = $options['category_published']; + + if ($categoryPublished === 0) { + $options['tip_title'] = Text::_('JLIB_HTML_ITEM_PUBLISHED_BUT_CATEGORY_UNPUBLISHED'); + $default['icon'] = 'expired'; + } + + if ($categoryPublished === -2) { + $options['tip_title'] = Text::_('JLIB_HTML_ITEM_PUBLISHED_BUT_CATEGORY_TRASHED'); + $default['icon'] = 'expired'; + } + } + } + + $this->states[$value] = $default; + + $html = parent::render($value, $row, $options); + + $this->states[$value] = $bakState; + + return $html; + } + + return parent::render($value, $row, $options); + } } diff --git a/libraries/src/Button/TransitionButton.php b/libraries/src/Button/TransitionButton.php index 01dbbc3f648ff..3ef1087814522 100644 --- a/libraries/src/Button/TransitionButton.php +++ b/libraries/src/Button/TransitionButton.php @@ -1,4 +1,5 @@ unknownState['icon'] = 'shuffle'; - $this->unknownState['title'] = $options['title'] ?? Text::_('JLIB_HTML_UNKNOWN_STATE'); - $this->unknownState['tip_content'] = $options['tip_content'] ?? $this->unknownState['title']; - } + $this->unknownState['icon'] = 'shuffle'; + $this->unknownState['title'] = $options['title'] ?? Text::_('JLIB_HTML_UNKNOWN_STATE'); + $this->unknownState['tip_content'] = $options['tip_content'] ?? $this->unknownState['title']; + } - /** - * Render action button by item value. - * - * @param integer|null $value Current value of this item. - * @param integer|null $row The row number of this item. - * @param array $options The options to override group options. - * - * @return string Rendered HTML. - * - * @since 4.0.0 - */ - public function render(?int $value = null, ?int $row = null, array $options = []): string - { - $default = $this->unknownState; + /** + * Render action button by item value. + * + * @param integer|null $value Current value of this item. + * @param integer|null $row The row number of this item. + * @param array $options The options to override group options. + * + * @return string Rendered HTML. + * + * @since 4.0.0 + */ + public function render(?int $value = null, ?int $row = null, array $options = []): string + { + $default = $this->unknownState; - $options['tip_title'] = $options['tip_title'] ?? ($options['title'] ?? $default['title']); + $options['tip_title'] = $options['tip_title'] ?? ($options['title'] ?? $default['title']); - return parent::render($value, $row, $options); - } + return parent::render($value, $row, $options); + } } diff --git a/libraries/src/Captcha/Captcha.php b/libraries/src/Captcha/Captcha.php index 651afcebd15ab..01d6c6d7fe27f 100644 --- a/libraries/src/Captcha/Captcha.php +++ b/libraries/src/Captcha/Captcha.php @@ -1,4 +1,5 @@ name = $captcha; - - if (!empty($options['dispatcher']) && $options['dispatcher'] instanceof DispatcherInterface) - { - $this->setDispatcher($options['dispatcher']); - } - else - { - $this->setDispatcher(Factory::getApplication()->getDispatcher()); - } - - $this->_load($options); - } - - /** - * Returns the global Captcha object, only creating it - * if it doesn't already exist. - * - * @param string $captcha The plugin to use. - * @param array $options Associative array of options. - * - * @return Captcha|null Instance of this class. - * - * @since 2.5 - * @throws \RuntimeException - */ - public static function getInstance($captcha, array $options = array()) - { - $signature = md5(serialize(array($captcha, $options))); - - if (empty(self::$instances[$signature])) - { - self::$instances[$signature] = new Captcha($captcha, $options); - } - - return self::$instances[$signature]; - } - - /** - * Fire the onInit event to initialise the captcha plugin. - * - * @param string $id The id of the field. - * - * @return boolean True on success - * - * @since 2.5 - * @throws \RuntimeException - */ - public function initialise($id) - { - $arg = ['id' => $id]; - - $this->update('onInit', $arg); - - return true; - } - - /** - * Get the HTML for the captcha. - * - * @param string $name The control name. - * @param string $id The id for the control. - * @param string $class Value for the HTML class attribute - * - * @return string The return value of the function "onDisplay" of the selected Plugin. - * - * @since 2.5 - * @throws \RuntimeException - */ - public function display($name, $id, $class = '') - { - // Check if captcha is already loaded. - if ($this->captcha === null) - { - return ''; - } - - // Initialise the Captcha. - if (!$this->initialise($id)) - { - return ''; - } - - $arg = [ - 'name' => $name, - 'id' => $id ?: $name, - 'class' => $class, - ]; - - $result = $this->update('onDisplay', $arg); - - return $result; - } - - /** - * Checks if the answer is correct. - * - * @param string $code The answer. - * - * @return bool Whether the provided answer was correct - * - * @since 2.5 - * @throws \RuntimeException - */ - public function checkAnswer($code) - { - // Check if captcha is already loaded - if ($this->captcha === null) - { - return false; - } - - $arg = ['code' => $code]; - - $result = $this->update('onCheckAnswer', $arg); - - return $result; - } - - /** - * Method to react on the setup of a captcha field. Gives the possibility - * to change the field and/or the XML element for the field. - * - * @param \Joomla\CMS\Form\Field\CaptchaField $field Captcha field instance - * @param \SimpleXMLElement $element XML form definition - * - * @return void - */ - public function setupField(\Joomla\CMS\Form\Field\CaptchaField $field, \SimpleXMLElement $element) - { - if ($this->captcha === null) - { - return; - } - - $arg = [ - 'field' => $field, - 'element' => $element, - ]; - - $result = $this->update('onSetupField', $arg); - - return $result; - } - - /** - * Method to call the captcha callback if it exist. - * - * @param string $name Callback name - * @param array &$args Arguments - * - * @return mixed - * - * @since 4.0.0 - */ - private function update($name, &$args) - { - if (method_exists($this->captcha, $name)) - { - return call_user_func_array(array($this->captcha, $name), array_values($args)); - } - - return null; - } - - /** - * Load the Captcha plugin. - * - * @param array $options Associative array of options. - * - * @return void - * - * @since 2.5 - * @throws \RuntimeException - */ - private function _load(array $options = array()) - { - // Build the path to the needed captcha plugin - $name = InputFilter::getInstance()->clean($this->name, 'cmd'); - $path = JPATH_PLUGINS . '/captcha/' . $name . '/' . $name . '.php'; - - if (!is_file($path)) - { - throw new \RuntimeException(Text::sprintf('JLIB_CAPTCHA_ERROR_PLUGIN_NOT_FOUND', $name)); - } - - // Require plugin file - require_once $path; - - // Get the plugin - $plugin = PluginHelper::getPlugin('captcha', $this->name); - - if (!$plugin) - { - throw new \RuntimeException(Text::sprintf('JLIB_CAPTCHA_ERROR_PLUGIN_NOT_FOUND', $name)); - } - - // Check for already loaded params - if (!($plugin->params instanceof Registry)) - { - $params = new Registry($plugin->params); - $plugin->params = $params; - } - - // Build captcha plugin classname - $name = 'PlgCaptcha' . $this->name; - $dispatcher = $this->getDispatcher(); - $this->captcha = new $name($dispatcher, (array) $plugin, $options); - } + use DispatcherAwareTrait; + + /** + * Captcha Plugin object + * + * @var CMSPlugin + * @since 2.5 + */ + private $captcha; + + /** + * Editor Plugin name + * + * @var string + * @since 2.5 + */ + private $name; + + /** + * Array of instances of this class. + * + * @var Captcha[] + * @since 2.5 + */ + private static $instances = array(); + + /** + * Class constructor. + * + * @param string $captcha The plugin to use. + * @param array $options Associative array of options. + * + * @since 2.5 + * @throws \RuntimeException + */ + public function __construct($captcha, $options) + { + $this->name = $captcha; + + if (!empty($options['dispatcher']) && $options['dispatcher'] instanceof DispatcherInterface) { + $this->setDispatcher($options['dispatcher']); + } else { + $this->setDispatcher(Factory::getApplication()->getDispatcher()); + } + + $this->_load($options); + } + + /** + * Returns the global Captcha object, only creating it + * if it doesn't already exist. + * + * @param string $captcha The plugin to use. + * @param array $options Associative array of options. + * + * @return Captcha|null Instance of this class. + * + * @since 2.5 + * @throws \RuntimeException + */ + public static function getInstance($captcha, array $options = array()) + { + $signature = md5(serialize(array($captcha, $options))); + + if (empty(self::$instances[$signature])) { + self::$instances[$signature] = new Captcha($captcha, $options); + } + + return self::$instances[$signature]; + } + + /** + * Fire the onInit event to initialise the captcha plugin. + * + * @param string $id The id of the field. + * + * @return boolean True on success + * + * @since 2.5 + * @throws \RuntimeException + */ + public function initialise($id) + { + $arg = ['id' => $id]; + + $this->update('onInit', $arg); + + return true; + } + + /** + * Get the HTML for the captcha. + * + * @param string $name The control name. + * @param string $id The id for the control. + * @param string $class Value for the HTML class attribute + * + * @return string The return value of the function "onDisplay" of the selected Plugin. + * + * @since 2.5 + * @throws \RuntimeException + */ + public function display($name, $id, $class = '') + { + // Check if captcha is already loaded. + if ($this->captcha === null) { + return ''; + } + + // Initialise the Captcha. + if (!$this->initialise($id)) { + return ''; + } + + $arg = [ + 'name' => $name, + 'id' => $id ?: $name, + 'class' => $class, + ]; + + $result = $this->update('onDisplay', $arg); + + return $result; + } + + /** + * Checks if the answer is correct. + * + * @param string $code The answer. + * + * @return bool Whether the provided answer was correct + * + * @since 2.5 + * @throws \RuntimeException + */ + public function checkAnswer($code) + { + // Check if captcha is already loaded + if ($this->captcha === null) { + return false; + } + + $arg = ['code' => $code]; + + $result = $this->update('onCheckAnswer', $arg); + + return $result; + } + + /** + * Method to react on the setup of a captcha field. Gives the possibility + * to change the field and/or the XML element for the field. + * + * @param \Joomla\CMS\Form\Field\CaptchaField $field Captcha field instance + * @param \SimpleXMLElement $element XML form definition + * + * @return void + */ + public function setupField(\Joomla\CMS\Form\Field\CaptchaField $field, \SimpleXMLElement $element) + { + if ($this->captcha === null) { + return; + } + + $arg = [ + 'field' => $field, + 'element' => $element, + ]; + + $result = $this->update('onSetupField', $arg); + + return $result; + } + + /** + * Method to call the captcha callback if it exist. + * + * @param string $name Callback name + * @param array &$args Arguments + * + * @return mixed + * + * @since 4.0.0 + */ + private function update($name, &$args) + { + if (method_exists($this->captcha, $name)) { + return call_user_func_array(array($this->captcha, $name), array_values($args)); + } + + return null; + } + + /** + * Load the Captcha plugin. + * + * @param array $options Associative array of options. + * + * @return void + * + * @since 2.5 + * @throws \RuntimeException + */ + private function _load(array $options = array()) + { + // Build the path to the needed captcha plugin + $name = InputFilter::getInstance()->clean($this->name, 'cmd'); + $path = JPATH_PLUGINS . '/captcha/' . $name . '/' . $name . '.php'; + + if (!is_file($path)) { + throw new \RuntimeException(Text::sprintf('JLIB_CAPTCHA_ERROR_PLUGIN_NOT_FOUND', $name)); + } + + // Require plugin file + require_once $path; + + // Get the plugin + $plugin = PluginHelper::getPlugin('captcha', $this->name); + + if (!$plugin) { + throw new \RuntimeException(Text::sprintf('JLIB_CAPTCHA_ERROR_PLUGIN_NOT_FOUND', $name)); + } + + // Check for already loaded params + if (!($plugin->params instanceof Registry)) { + $params = new Registry($plugin->params); + $plugin->params = $params; + } + + // Build captcha plugin classname + $name = 'PlgCaptcha' . $this->name; + $dispatcher = $this->getDispatcher(); + $this->captcha = new $name($dispatcher, (array) $plugin, $options); + } } diff --git a/libraries/src/Captcha/Google/HttpBridgePostRequestMethod.php b/libraries/src/Captcha/Google/HttpBridgePostRequestMethod.php index ff4a8f9c91440..4b4703cf066c9 100644 --- a/libraries/src/Captcha/Google/HttpBridgePostRequestMethod.php +++ b/libraries/src/Captcha/Google/HttpBridgePostRequestMethod.php @@ -1,4 +1,5 @@ http = $http ?: HttpFactory::getHttp(); - } + /** + * Class constructor. + * + * @param Http|null $http The HTTP adapter + * + * @since 3.9.0 + */ + public function __construct(Http $http = null) + { + $this->http = $http ?: HttpFactory::getHttp(); + } - /** - * Submit the request with the specified parameters. - * - * @param RequestParameters $params Request parameters - * - * @return string Body of the reCAPTCHA response - * - * @since 3.9.0 - */ - public function submit(RequestParameters $params) - { - try - { - $response = $this->http->post(self::SITE_VERIFY_URL, $params->toArray()); + /** + * Submit the request with the specified parameters. + * + * @param RequestParameters $params Request parameters + * + * @return string Body of the reCAPTCHA response + * + * @since 3.9.0 + */ + public function submit(RequestParameters $params) + { + try { + $response = $this->http->post(self::SITE_VERIFY_URL, $params->toArray()); - return (string) $response->getBody(); - } - catch (InvalidResponseCodeException $exception) - { - return ''; - } - } + return (string) $response->getBody(); + } catch (InvalidResponseCodeException $exception) { + return ''; + } + } } diff --git a/libraries/src/Categories/Categories.php b/libraries/src/Categories/Categories.php index 5194a33d017f1..790c96e891b3d 100644 --- a/libraries/src/Categories/Categories.php +++ b/libraries/src/Categories/Categories.php @@ -1,4 +1,5 @@ _extension = $options['extension']; - $this->_table = $options['table']; - $this->_field = isset($options['field']) && $options['field'] ? $options['field'] : 'catid'; - $this->_key = isset($options['key']) && $options['key'] ? $options['key'] : 'id'; - $this->_statefield = isset($options['statefield']) ? $options['statefield'] : 'state'; - - $options['access'] = isset($options['access']) ? $options['access'] : 'true'; - $options['published'] = isset($options['published']) ? $options['published'] : 1; - $options['countItems'] = isset($options['countItems']) ? $options['countItems'] : 0; - $options['currentlang'] = Multilanguage::isEnabled() ? Factory::getLanguage()->getTag() : 0; - - $this->_options = $options; - } - - /** - * Returns a reference to a Categories object - * - * @param string $extension Name of the categories extension - * @param array $options An array of options - * - * @return Categories|boolean Categories object on success, boolean false if an object does not exist - * - * @since 1.6 - * @deprecated 5.0 Use the ComponentInterface to get the categories - */ - public static function getInstance($extension, $options = array()) - { - $hash = md5(strtolower($extension) . serialize($options)); - - if (isset(self::$instances[$hash])) - { - return self::$instances[$hash]; - } - - $categories = null; - - try - { - $parts = explode('.', $extension, 2); - - $component = Factory::getApplication()->bootComponent($parts[0]); - - if ($component instanceof CategoryServiceInterface) - { - $categories = $component->getCategory($options, \count($parts) > 1 ? $parts[1] : ''); - } - } - catch (SectionNotFoundException $e) - { - $categories = null; - } - - self::$instances[$hash] = $categories; - - return self::$instances[$hash]; - } - - /** - * Loads a specific category and all its children in a CategoryNode object. - * - * @param mixed $id an optional id integer or equal to 'root' - * @param boolean $forceload True to force the _load method to execute - * - * @return CategoryNode|null CategoryNode object or null if $id is not valid - * - * @since 1.6 - */ - public function get($id = 'root', $forceload = false) - { - if ($id !== 'root') - { - $id = (int) $id; - - if ($id == 0) - { - $id = 'root'; - } - } - - // If this $id has not been processed yet, execute the _load method - if ((!isset($this->_nodes[$id]) && !isset($this->_checkedCategories[$id])) || $forceload) - { - $this->_load($id); - } - - // If we already have a value in _nodes for this $id, then use it. - if (isset($this->_nodes[$id])) - { - return $this->_nodes[$id]; - } - - return null; - } - - /** - * Returns the extension of the category. - * - * @return string The extension - * - * @since 3.9.0 - */ - public function getExtension() - { - return $this->_extension; - } - - /** - * Load method - * - * @param integer $id Id of category to load - * - * @return void - * - * @since 1.6 - */ - protected function _load($id) - { - try - { - $db = $this->getDatabase(); - } - catch (DatabaseNotFoundException $e) - { - @trigger_error(sprintf('Database must be set, this will not be caught anymore in 5.0.'), E_USER_DEPRECATED); - $db = Factory::getContainer()->get(DatabaseInterface::class); - } - - $app = Factory::getApplication(); - $user = Factory::getUser(); - $extension = $this->_extension; - - if ($id !== 'root') - { - $id = (int) $id; - - if ($id === 0) - { - $id = 'root'; - } - } - - // Record that has this $id has been checked - $this->_checkedCategories[$id] = true; - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('c.id'), - $db->quoteName('c.asset_id'), - $db->quoteName('c.access'), - $db->quoteName('c.alias'), - $db->quoteName('c.checked_out'), - $db->quoteName('c.checked_out_time'), - $db->quoteName('c.created_time'), - $db->quoteName('c.created_user_id'), - $db->quoteName('c.description'), - $db->quoteName('c.extension'), - $db->quoteName('c.hits'), - $db->quoteName('c.language'), - $db->quoteName('c.level'), - $db->quoteName('c.lft'), - $db->quoteName('c.metadata'), - $db->quoteName('c.metadesc'), - $db->quoteName('c.metakey'), - $db->quoteName('c.modified_time'), - $db->quoteName('c.note'), - $db->quoteName('c.params'), - $db->quoteName('c.parent_id'), - $db->quoteName('c.path'), - $db->quoteName('c.published'), - $db->quoteName('c.rgt'), - $db->quoteName('c.title'), - $db->quoteName('c.modified_user_id'), - $db->quoteName('c.version'), - ] - ); - - $case_when = ' CASE WHEN '; - $case_when .= $query->charLength($db->quoteName('c.alias'), '!=', '0'); - $case_when .= ' THEN '; - $c_id = $query->castAsChar($db->quoteName('c.id')); - $case_when .= $query->concatenate(array($c_id, $db->quoteName('c.alias')), ':'); - $case_when .= ' ELSE '; - $case_when .= $c_id . ' END as ' . $db->quoteName('slug'); - - $query->select($case_when) - ->where('(' . $db->quoteName('c.extension') . ' = :extension OR ' . $db->quoteName('c.extension') . ' = ' . $db->quote('system') . ')') - ->bind(':extension', $extension); - - if ($this->_options['access']) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('c.access'), $groups); - } - - if ($this->_options['published'] == 1) - { - $query->where($db->quoteName('c.published') . ' = 1'); - } - - $query->order($db->quoteName('c.lft')); - - // Note: s for selected id - if ($id !== 'root') - { - // Get the selected category - $query->from($db->quoteName('#__categories', 's')) - ->where($db->quoteName('s.id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - if ($app->isClient('site') && Multilanguage::isEnabled()) - { - // For the most part, we use c.lft column, which index is properly used instead of c.rgt - $query->join( - 'INNER', - $db->quoteName('#__categories', 'c'), - '(' . $db->quoteName('s.lft') . ' < ' . $db->quoteName('c.lft') - . ' AND ' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.rgt') - . ' AND ' . $db->quoteName('c.language') - . ' IN (' . implode(',', $query->bindArray([Factory::getLanguage()->getTag(), '*'], ParameterType::STRING)) . '))' - . ' OR (' . $db->quoteName('c.lft') . ' <= ' . $db->quoteName('s.lft') - . ' AND ' . $db->quoteName('s.rgt') . ' <= ' . $db->quoteName('c.rgt') . ')' - ); - } - else - { - $query->join( - 'INNER', - $db->quoteName('#__categories', 'c'), - '(' . $db->quoteName('s.lft') . ' <= ' . $db->quoteName('c.lft') - . ' AND ' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.rgt') . ')' - . ' OR (' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.lft') - . ' AND ' . $db->quoteName('s.rgt') . ' < ' . $db->quoteName('c.rgt') . ')' - ); - } - } - else - { - $query->from($db->quoteName('#__categories', 'c')); - - if ($app->isClient('site') && Multilanguage::isEnabled()) - { - $query->whereIn($db->quoteName('c.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); - } - } - - // Note: i for item - if ($this->_options['countItems'] == 1) - { - $subQuery = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName($db->escape('i.' . $this->_key)) . ')') - ->from($db->quoteName($db->escape($this->_table), 'i')) - ->where($db->quoteName($db->escape('i.' . $this->_field)) . ' = ' . $db->quoteName('c.id')); - - if ($this->_options['published'] == 1) - { - $subQuery->where($db->quoteName($db->escape('i.' . $this->_statefield)) . ' = 1'); - } - - if ($this->_options['currentlang'] !== 0) - { - $subQuery->where( - $db->quoteName('i.language') - . ' IN (' . implode(',', $query->bindArray([$this->_options['currentlang'], '*'], ParameterType::STRING)) . ')' - ); - } - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('numitems')); - } - - // Get the results - $db->setQuery($query); - $results = $db->loadObjectList('id'); - $childrenLoaded = false; - - if (\count($results)) - { - // Foreach categories - foreach ($results as $result) - { - // Deal with root category - if ($result->id == 1) - { - $result->id = 'root'; - } - - // Deal with parent_id - if ($result->parent_id == 1) - { - $result->parent_id = 'root'; - } - - // Create the node - if (!isset($this->_nodes[$result->id])) - { - // Create the CategoryNode and add to _nodes - $this->_nodes[$result->id] = new CategoryNode($result, $this); - - // If this is not root and if the current node's parent is in the list or the current node parent is 0 - if ($result->id !== 'root' && (isset($this->_nodes[$result->parent_id]) || $result->parent_id == 1)) - { - // Compute relationship between node and its parent - set the parent in the _nodes field - $this->_nodes[$result->id]->setParent($this->_nodes[$result->parent_id]); - } - - // If the node's parent id is not in the _nodes list and the node is not root (doesn't have parent_id == 0), - // then remove the node from the list - if (!(isset($this->_nodes[$result->parent_id]) || $result->parent_id == 0)) - { - unset($this->_nodes[$result->id]); - continue; - } - - if ($result->id == $id || $childrenLoaded) - { - $this->_nodes[$result->id]->setAllLoaded(); - $childrenLoaded = true; - } - } - elseif ($result->id == $id || $childrenLoaded) - { - // Create the CategoryNode - $this->_nodes[$result->id] = new CategoryNode($result, $this); - - if ($result->id !== 'root' && (isset($this->_nodes[$result->parent_id]) || $result->parent_id)) - { - // Compute relationship between node and its parent - $this->_nodes[$result->id]->setParent($this->_nodes[$result->parent_id]); - } - - // If the node's parent id is not in the _nodes list and the node is not root (doesn't have parent_id == 0), - // then remove the node from the list - if (!(isset($this->_nodes[$result->parent_id]) || $result->parent_id == 0)) - { - unset($this->_nodes[$result->id]); - continue; - } - - if ($result->id == $id || $childrenLoaded) - { - $this->_nodes[$result->id]->setAllLoaded(); - $childrenLoaded = true; - } - } - } - } - else - { - $this->_nodes[$id] = null; - } - } + use DatabaseAwareTrait; + + /** + * Array to hold the object instances + * + * @var Categories[] + * @since 1.6 + */ + public static $instances = array(); + + /** + * Array of category nodes + * + * @var CategoryNode[] + * @since 1.6 + */ + protected $_nodes; + + /** + * Array of checked categories -- used to save values when _nodes are null + * + * @var boolean[] + * @since 1.6 + */ + protected $_checkedCategories; + + /** + * Name of the extension the categories belong to + * + * @var string + * @since 1.6 + */ + protected $_extension = null; + + /** + * Name of the linked content table to get category content count + * + * @var string + * @since 1.6 + */ + protected $_table = null; + + /** + * Name of the category field + * + * @var string + * @since 1.6 + */ + protected $_field = null; + + /** + * Name of the key field + * + * @var string + * @since 1.6 + */ + protected $_key = null; + + /** + * Name of the items state field + * + * @var string + * @since 1.6 + */ + protected $_statefield = null; + + /** + * Array of options + * + * @var array + * @since 1.6 + */ + protected $_options = []; + + /** + * Class constructor + * + * @param array $options Array of options + * + * @since 1.6 + */ + public function __construct($options) + { + $this->_extension = $options['extension']; + $this->_table = $options['table']; + $this->_field = isset($options['field']) && $options['field'] ? $options['field'] : 'catid'; + $this->_key = isset($options['key']) && $options['key'] ? $options['key'] : 'id'; + $this->_statefield = isset($options['statefield']) ? $options['statefield'] : 'state'; + + $options['access'] = isset($options['access']) ? $options['access'] : 'true'; + $options['published'] = isset($options['published']) ? $options['published'] : 1; + $options['countItems'] = isset($options['countItems']) ? $options['countItems'] : 0; + $options['currentlang'] = Multilanguage::isEnabled() ? Factory::getLanguage()->getTag() : 0; + + $this->_options = $options; + } + + /** + * Returns a reference to a Categories object + * + * @param string $extension Name of the categories extension + * @param array $options An array of options + * + * @return Categories|boolean Categories object on success, boolean false if an object does not exist + * + * @since 1.6 + * @deprecated 5.0 Use the ComponentInterface to get the categories + */ + public static function getInstance($extension, $options = array()) + { + $hash = md5(strtolower($extension) . serialize($options)); + + if (isset(self::$instances[$hash])) { + return self::$instances[$hash]; + } + + $categories = null; + + try { + $parts = explode('.', $extension, 2); + + $component = Factory::getApplication()->bootComponent($parts[0]); + + if ($component instanceof CategoryServiceInterface) { + $categories = $component->getCategory($options, \count($parts) > 1 ? $parts[1] : ''); + } + } catch (SectionNotFoundException $e) { + $categories = null; + } + + self::$instances[$hash] = $categories; + + return self::$instances[$hash]; + } + + /** + * Loads a specific category and all its children in a CategoryNode object. + * + * @param mixed $id an optional id integer or equal to 'root' + * @param boolean $forceload True to force the _load method to execute + * + * @return CategoryNode|null CategoryNode object or null if $id is not valid + * + * @since 1.6 + */ + public function get($id = 'root', $forceload = false) + { + if ($id !== 'root') { + $id = (int) $id; + + if ($id == 0) { + $id = 'root'; + } + } + + // If this $id has not been processed yet, execute the _load method + if ((!isset($this->_nodes[$id]) && !isset($this->_checkedCategories[$id])) || $forceload) { + $this->_load($id); + } + + // If we already have a value in _nodes for this $id, then use it. + if (isset($this->_nodes[$id])) { + return $this->_nodes[$id]; + } + + return null; + } + + /** + * Returns the extension of the category. + * + * @return string The extension + * + * @since 3.9.0 + */ + public function getExtension() + { + return $this->_extension; + } + + /** + * Load method + * + * @param integer $id Id of category to load + * + * @return void + * + * @since 1.6 + */ + protected function _load($id) + { + try { + $db = $this->getDatabase(); + } catch (DatabaseNotFoundException $e) { + @trigger_error(sprintf('Database must be set, this will not be caught anymore in 5.0.'), E_USER_DEPRECATED); + $db = Factory::getContainer()->get(DatabaseInterface::class); + } + + $app = Factory::getApplication(); + $user = Factory::getUser(); + $extension = $this->_extension; + + if ($id !== 'root') { + $id = (int) $id; + + if ($id === 0) { + $id = 'root'; + } + } + + // Record that has this $id has been checked + $this->_checkedCategories[$id] = true; + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('c.id'), + $db->quoteName('c.asset_id'), + $db->quoteName('c.access'), + $db->quoteName('c.alias'), + $db->quoteName('c.checked_out'), + $db->quoteName('c.checked_out_time'), + $db->quoteName('c.created_time'), + $db->quoteName('c.created_user_id'), + $db->quoteName('c.description'), + $db->quoteName('c.extension'), + $db->quoteName('c.hits'), + $db->quoteName('c.language'), + $db->quoteName('c.level'), + $db->quoteName('c.lft'), + $db->quoteName('c.metadata'), + $db->quoteName('c.metadesc'), + $db->quoteName('c.metakey'), + $db->quoteName('c.modified_time'), + $db->quoteName('c.note'), + $db->quoteName('c.params'), + $db->quoteName('c.parent_id'), + $db->quoteName('c.path'), + $db->quoteName('c.published'), + $db->quoteName('c.rgt'), + $db->quoteName('c.title'), + $db->quoteName('c.modified_user_id'), + $db->quoteName('c.version'), + ] + ); + + $case_when = ' CASE WHEN '; + $case_when .= $query->charLength($db->quoteName('c.alias'), '!=', '0'); + $case_when .= ' THEN '; + $c_id = $query->castAsChar($db->quoteName('c.id')); + $case_when .= $query->concatenate(array($c_id, $db->quoteName('c.alias')), ':'); + $case_when .= ' ELSE '; + $case_when .= $c_id . ' END as ' . $db->quoteName('slug'); + + $query->select($case_when) + ->where('(' . $db->quoteName('c.extension') . ' = :extension OR ' . $db->quoteName('c.extension') . ' = ' . $db->quote('system') . ')') + ->bind(':extension', $extension); + + if ($this->_options['access']) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('c.access'), $groups); + } + + if ($this->_options['published'] == 1) { + $query->where($db->quoteName('c.published') . ' = 1'); + } + + $query->order($db->quoteName('c.lft')); + + // Note: s for selected id + if ($id !== 'root') { + // Get the selected category + $query->from($db->quoteName('#__categories', 's')) + ->where($db->quoteName('s.id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + if ($app->isClient('site') && Multilanguage::isEnabled()) { + // For the most part, we use c.lft column, which index is properly used instead of c.rgt + $query->join( + 'INNER', + $db->quoteName('#__categories', 'c'), + '(' . $db->quoteName('s.lft') . ' < ' . $db->quoteName('c.lft') + . ' AND ' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.rgt') + . ' AND ' . $db->quoteName('c.language') + . ' IN (' . implode(',', $query->bindArray([Factory::getLanguage()->getTag(), '*'], ParameterType::STRING)) . '))' + . ' OR (' . $db->quoteName('c.lft') . ' <= ' . $db->quoteName('s.lft') + . ' AND ' . $db->quoteName('s.rgt') . ' <= ' . $db->quoteName('c.rgt') . ')' + ); + } else { + $query->join( + 'INNER', + $db->quoteName('#__categories', 'c'), + '(' . $db->quoteName('s.lft') . ' <= ' . $db->quoteName('c.lft') + . ' AND ' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.rgt') . ')' + . ' OR (' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.lft') + . ' AND ' . $db->quoteName('s.rgt') . ' < ' . $db->quoteName('c.rgt') . ')' + ); + } + } else { + $query->from($db->quoteName('#__categories', 'c')); + + if ($app->isClient('site') && Multilanguage::isEnabled()) { + $query->whereIn($db->quoteName('c.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); + } + } + + // Note: i for item + if ($this->_options['countItems'] == 1) { + $subQuery = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName($db->escape('i.' . $this->_key)) . ')') + ->from($db->quoteName($db->escape($this->_table), 'i')) + ->where($db->quoteName($db->escape('i.' . $this->_field)) . ' = ' . $db->quoteName('c.id')); + + if ($this->_options['published'] == 1) { + $subQuery->where($db->quoteName($db->escape('i.' . $this->_statefield)) . ' = 1'); + } + + if ($this->_options['currentlang'] !== 0) { + $subQuery->where( + $db->quoteName('i.language') + . ' IN (' . implode(',', $query->bindArray([$this->_options['currentlang'], '*'], ParameterType::STRING)) . ')' + ); + } + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('numitems')); + } + + // Get the results + $db->setQuery($query); + $results = $db->loadObjectList('id'); + $childrenLoaded = false; + + if (\count($results)) { + // Foreach categories + foreach ($results as $result) { + // Deal with root category + if ($result->id == 1) { + $result->id = 'root'; + } + + // Deal with parent_id + if ($result->parent_id == 1) { + $result->parent_id = 'root'; + } + + // Create the node + if (!isset($this->_nodes[$result->id])) { + // Create the CategoryNode and add to _nodes + $this->_nodes[$result->id] = new CategoryNode($result, $this); + + // If this is not root and if the current node's parent is in the list or the current node parent is 0 + if ($result->id !== 'root' && (isset($this->_nodes[$result->parent_id]) || $result->parent_id == 1)) { + // Compute relationship between node and its parent - set the parent in the _nodes field + $this->_nodes[$result->id]->setParent($this->_nodes[$result->parent_id]); + } + + // If the node's parent id is not in the _nodes list and the node is not root (doesn't have parent_id == 0), + // then remove the node from the list + if (!(isset($this->_nodes[$result->parent_id]) || $result->parent_id == 0)) { + unset($this->_nodes[$result->id]); + continue; + } + + if ($result->id == $id || $childrenLoaded) { + $this->_nodes[$result->id]->setAllLoaded(); + $childrenLoaded = true; + } + } elseif ($result->id == $id || $childrenLoaded) { + // Create the CategoryNode + $this->_nodes[$result->id] = new CategoryNode($result, $this); + + if ($result->id !== 'root' && (isset($this->_nodes[$result->parent_id]) || $result->parent_id)) { + // Compute relationship between node and its parent + $this->_nodes[$result->id]->setParent($this->_nodes[$result->parent_id]); + } + + // If the node's parent id is not in the _nodes list and the node is not root (doesn't have parent_id == 0), + // then remove the node from the list + if (!(isset($this->_nodes[$result->parent_id]) || $result->parent_id == 0)) { + unset($this->_nodes[$result->id]); + continue; + } + + if ($result->id == $id || $childrenLoaded) { + $this->_nodes[$result->id]->setAllLoaded(); + $childrenLoaded = true; + } + } + } + } else { + $this->_nodes[$id] = null; + } + } } diff --git a/libraries/src/Categories/CategoryFactory.php b/libraries/src/Categories/CategoryFactory.php index eb6228184acc1..6edc08668b3d4 100644 --- a/libraries/src/Categories/CategoryFactory.php +++ b/libraries/src/Categories/CategoryFactory.php @@ -1,4 +1,5 @@ namespace = $namespace; - } + /** + * The namespace must be like: + * Joomla\Component\Content + * + * @param string $namespace The namespace + * + * @since 4.0.0 + */ + public function __construct($namespace) + { + $this->namespace = $namespace; + } - /** - * Creates a category. - * - * @param array $options The options - * @param string $section The section - * - * @return CategoryInterface - * - * @since 3.10.0 - * - * @throws SectionNotFoundException - */ - public function createCategory(array $options = [], string $section = ''): CategoryInterface - { - $className = trim($this->namespace, '\\') . '\\Site\\Service\\' . ucfirst($section) . 'Category'; + /** + * Creates a category. + * + * @param array $options The options + * @param string $section The section + * + * @return CategoryInterface + * + * @since 3.10.0 + * + * @throws SectionNotFoundException + */ + public function createCategory(array $options = [], string $section = ''): CategoryInterface + { + $className = trim($this->namespace, '\\') . '\\Site\\Service\\' . ucfirst($section) . 'Category'; - if (!class_exists($className)) - { - throw new SectionNotFoundException; - } + if (!class_exists($className)) { + throw new SectionNotFoundException(); + } - $category = new $className($options); + $category = new $className($options); - if ($category instanceof DatabaseAwareInterface) - { - $category->setDatabase($this->getDatabase()); - } + if ($category instanceof DatabaseAwareInterface) { + $category->setDatabase($this->getDatabase()); + } - return $category; - } + return $category; + } } diff --git a/libraries/src/Categories/CategoryFactoryInterface.php b/libraries/src/Categories/CategoryFactoryInterface.php index c3b0dca0bf466..e3150760db1d2 100644 --- a/libraries/src/Categories/CategoryFactoryInterface.php +++ b/libraries/src/Categories/CategoryFactoryInterface.php @@ -1,4 +1,5 @@ setProperties($category); - - if ($constructor) - { - $this->_constructor = $constructor; - } - - return true; - } - - return false; - } - - /** - * Set the parent of this category - * - * If the category already has a parent, the link is unset - * - * @param CategoryNode|null $parent CategoryNode for the parent to be set or null - * - * @return void - * - * @since 1.6 - */ - public function setParent(NodeInterface $parent) - { - if (!\is_null($this->_parent)) - { - $key = array_search($this, $this->_parent->_children); - unset($this->_parent->_children[$key]); - } - - $this->_parent = $parent; - - $this->_parent->_children[] = & $this; - - if (\count($this->_parent->_children) > 1) - { - end($this->_parent->_children); - $this->_leftSibling = prev($this->_parent->_children); - $this->_leftSibling->_rightsibling = & $this; - } - - if ($this->parent_id != 1) - { - $this->_path = $parent->getPath(); - } - - $this->_path[$this->id] = $this->id . ':' . $this->alias; - } - - /** - * Get the children of this node - * - * @param boolean $recursive False by default - * - * @return CategoryNode[] The children - * - * @since 1.6 - */ - public function &getChildren($recursive = false) - { - if (!$this->_allChildrenloaded) - { - $temp = $this->_constructor->get($this->id, true); - - if ($temp) - { - $this->_children = $temp->getChildren(); - $this->_leftSibling = $temp->getSibling(false); - $this->_rightSibling = $temp->getSibling(true); - $this->setAllLoaded(); - } - } - - if ($recursive) - { - $items = array(); - - foreach ($this->_children as $child) - { - $items[] = $child; - $items = array_merge($items, $child->getChildren(true)); - } - - return $items; - } - - return $this->_children; - } - - /** - * Returns the right or left sibling of a category - * - * @param boolean $right If set to false, returns the left sibling - * - * @return CategoryNode|null CategoryNode object with the sibling information or null if there is no sibling on that side. - * - * @since 1.6 - */ - public function getSibling($right = true) - { - if (!$this->_allChildrenloaded) - { - $temp = $this->_constructor->get($this->id, true); - $this->_children = $temp->getChildren(); - $this->_leftSibling = $temp->getSibling(false); - $this->_rightSibling = $temp->getSibling(true); - $this->setAllLoaded(); - } - - if ($right) - { - return $this->_rightSibling; - } - else - { - return $this->_leftSibling; - } - } - - /** - * Returns the category parameters - * - * @return Registry - * - * @since 1.6 - */ - public function getParams() - { - if (!($this->params instanceof Registry)) - { - $this->params = new Registry($this->params); - } - - return $this->params; - } - - /** - * Returns the category metadata - * - * @return Registry A Registry object containing the metadata - * - * @since 1.6 - */ - public function getMetadata() - { - if (!($this->metadata instanceof Registry)) - { - $this->metadata = new Registry($this->metadata); - } - - return $this->metadata; - } - - /** - * Returns the category path to the root category - * - * @return array - * - * @since 1.6 - */ - public function getPath() - { - return $this->_path; - } - - /** - * Returns the user that created the category - * - * @param boolean $modifiedUser Returns the modified_user when set to true - * - * @return \Joomla\CMS\User\User A User object containing a userid - * - * @since 1.6 - */ - public function getAuthor($modifiedUser = false) - { - if ($modifiedUser) - { - return Factory::getUser($this->modified_user_id); - } - - return Factory::getUser($this->created_user_id); - } - - /** - * Set to load all children - * - * @return void - * - * @since 1.6 - */ - public function setAllLoaded() - { - $this->_allChildrenloaded = true; - - foreach ($this->_children as $child) - { - $child->setAllLoaded(); - } - } - - /** - * Returns the number of items. - * - * @param boolean $recursive If false number of children, if true number of descendants - * - * @return integer Number of children or descendants - * - * @since 1.6 - */ - public function getNumItems($recursive = false) - { - if ($recursive) - { - $count = $this->numitems; - - foreach ($this->getChildren() as $child) - { - $count = $count + $child->getNumItems(true); - } - - return $count; - } - - return $this->numitems; - } + use NodeTrait; + + /** + * Primary key + * + * @var integer + * @since 1.6 + */ + public $id = null; + + /** + * The id of the category in the asset table + * + * @var integer + * @since 1.6 + */ + public $asset_id = null; + + /** + * The id of the parent of category in the asset table, 0 for category root + * + * @var integer + * @since 1.6 + */ + public $parent_id = null; + + /** + * The lft value for this category in the category tree + * + * @var integer + * @since 1.6 + */ + public $lft = null; + + /** + * The rgt value for this category in the category tree + * + * @var integer + * @since 1.6 + */ + public $rgt = null; + + /** + * The depth of this category's position in the category tree + * + * @var integer + * @since 1.6 + */ + public $level = null; + + /** + * The extension this category is associated with + * + * @var integer + * @since 1.6 + */ + public $extension = null; + + /** + * The menu title for the category (a short name) + * + * @var string + * @since 1.6 + */ + public $title = null; + + /** + * The the alias for the category + * + * @var string + * @since 1.6 + */ + public $alias = null; + + /** + * Description of the category. + * + * @var string + * @since 1.6 + */ + public $description = null; + + /** + * The publication status of the category + * + * @var boolean + * @since 1.6 + */ + public $published = null; + + /** + * Whether the category is or is not checked out + * + * @var boolean + * @since 1.6 + */ + public $checked_out = null; + + /** + * The time at which the category was checked out + * + * @var string + * @since 1.6 + */ + public $checked_out_time = null; + + /** + * Access level for the category + * + * @var integer + * @since 1.6 + */ + public $access = null; + + /** + * JSON string of parameters + * + * @var string + * @since 1.6 + */ + public $params = null; + + /** + * Metadata description + * + * @var string + * @since 1.6 + */ + public $metadesc = null; + + /** + * Key words for metadata + * + * @var string + * @since 1.6 + */ + public $metakey = null; + + /** + * JSON string of other metadata + * + * @var string + * @since 1.6 + */ + public $metadata = null; + + /** + * The ID of the user who created the category + * + * @var integer + * @since 1.6 + */ + public $created_user_id = null; + + /** + * The time at which the category was created + * + * @var string + * @since 1.6 + */ + public $created_time = null; + + /** + * The ID of the user who last modified the category + * + * @var integer + * @since 1.6 + */ + public $modified_user_id = null; + + /** + * The time at which the category was modified + * + * @var string + * @since 1.6 + */ + public $modified_time = null; + + /** + * Number of times the category has been viewed + * + * @var integer + * @since 1.6 + */ + public $hits = null; + + /** + * The language for the category in xx-XX format + * + * @var string + * @since 1.6 + */ + public $language = null; + + /** + * Number of items in this category or descendants of this category + * + * @var integer + * @since 1.6 + */ + public $numitems = null; + + /** + * Number of children items + * + * @var integer + * @since 1.6 + */ + public $childrennumitems = null; + + /** + * Slug for the category (used in URL) + * + * @var string + * @since 1.6 + */ + public $slug = null; + + /** + * Array of assets + * + * @var array + * @since 1.6 + */ + public $assets = null; + + /** + * Path from root to this category + * + * @var array + * @since 1.6 + */ + protected $_path = array(); + + /** + * Flag if all children have been loaded + * + * @var boolean + * @since 1.6 + */ + protected $_allChildrenloaded = false; + + /** + * Constructor of this tree + * + * @var Categories + * @since 1.6 + */ + protected $_constructor = null; + + /** + * Class constructor + * + * @param array $category The category data. + * @param Categories $constructor The tree constructor. + * + * @since 1.6 + */ + public function __construct($category = null, $constructor = null) + { + if ($category) { + $this->setProperties($category); + + if ($constructor) { + $this->_constructor = $constructor; + } + + return true; + } + + return false; + } + + /** + * Set the parent of this category + * + * If the category already has a parent, the link is unset + * + * @param CategoryNode|null $parent CategoryNode for the parent to be set or null + * + * @return void + * + * @since 1.6 + */ + public function setParent(NodeInterface $parent) + { + if (!\is_null($this->_parent)) { + $key = array_search($this, $this->_parent->_children); + unset($this->_parent->_children[$key]); + } + + $this->_parent = $parent; + + $this->_parent->_children[] = & $this; + + if (\count($this->_parent->_children) > 1) { + end($this->_parent->_children); + $this->_leftSibling = prev($this->_parent->_children); + $this->_leftSibling->_rightsibling = & $this; + } + + if ($this->parent_id != 1) { + $this->_path = $parent->getPath(); + } + + $this->_path[$this->id] = $this->id . ':' . $this->alias; + } + + /** + * Get the children of this node + * + * @param boolean $recursive False by default + * + * @return CategoryNode[] The children + * + * @since 1.6 + */ + public function &getChildren($recursive = false) + { + if (!$this->_allChildrenloaded) { + $temp = $this->_constructor->get($this->id, true); + + if ($temp) { + $this->_children = $temp->getChildren(); + $this->_leftSibling = $temp->getSibling(false); + $this->_rightSibling = $temp->getSibling(true); + $this->setAllLoaded(); + } + } + + if ($recursive) { + $items = array(); + + foreach ($this->_children as $child) { + $items[] = $child; + $items = array_merge($items, $child->getChildren(true)); + } + + return $items; + } + + return $this->_children; + } + + /** + * Returns the right or left sibling of a category + * + * @param boolean $right If set to false, returns the left sibling + * + * @return CategoryNode|null CategoryNode object with the sibling information or null if there is no sibling on that side. + * + * @since 1.6 + */ + public function getSibling($right = true) + { + if (!$this->_allChildrenloaded) { + $temp = $this->_constructor->get($this->id, true); + $this->_children = $temp->getChildren(); + $this->_leftSibling = $temp->getSibling(false); + $this->_rightSibling = $temp->getSibling(true); + $this->setAllLoaded(); + } + + if ($right) { + return $this->_rightSibling; + } else { + return $this->_leftSibling; + } + } + + /** + * Returns the category parameters + * + * @return Registry + * + * @since 1.6 + */ + public function getParams() + { + if (!($this->params instanceof Registry)) { + $this->params = new Registry($this->params); + } + + return $this->params; + } + + /** + * Returns the category metadata + * + * @return Registry A Registry object containing the metadata + * + * @since 1.6 + */ + public function getMetadata() + { + if (!($this->metadata instanceof Registry)) { + $this->metadata = new Registry($this->metadata); + } + + return $this->metadata; + } + + /** + * Returns the category path to the root category + * + * @return array + * + * @since 1.6 + */ + public function getPath() + { + return $this->_path; + } + + /** + * Returns the user that created the category + * + * @param boolean $modifiedUser Returns the modified_user when set to true + * + * @return \Joomla\CMS\User\User A User object containing a userid + * + * @since 1.6 + */ + public function getAuthor($modifiedUser = false) + { + if ($modifiedUser) { + return Factory::getUser($this->modified_user_id); + } + + return Factory::getUser($this->created_user_id); + } + + /** + * Set to load all children + * + * @return void + * + * @since 1.6 + */ + public function setAllLoaded() + { + $this->_allChildrenloaded = true; + + foreach ($this->_children as $child) { + $child->setAllLoaded(); + } + } + + /** + * Returns the number of items. + * + * @param boolean $recursive If false number of children, if true number of descendants + * + * @return integer Number of children or descendants + * + * @since 1.6 + */ + public function getNumItems($recursive = false) + { + if ($recursive) { + $count = $this->numitems; + + foreach ($this->getChildren() as $child) { + $count = $count + $child->getNumItems(true); + } + + return $count; + } + + return $this->numitems; + } } diff --git a/libraries/src/Categories/CategoryServiceInterface.php b/libraries/src/Categories/CategoryServiceInterface.php index 29b313d19b4f6..380e93704747f 100644 --- a/libraries/src/Categories/CategoryServiceInterface.php +++ b/libraries/src/Categories/CategoryServiceInterface.php @@ -1,4 +1,5 @@ categoryFactory->createCategory($options, $section); - } + /** + * Returns the category service. + * + * @param array $options The options + * @param string $section The section + * + * @return CategoryInterface + * + * @since 4.0.0 + * @throws SectionNotFoundException + */ + public function getCategory(array $options = [], $section = ''): CategoryInterface + { + return $this->categoryFactory->createCategory($options, $section); + } - /** - * Sets the internal category factory. - * - * @param CategoryFactoryInterface $categoryFactory The categories factory - * - * @return void - * - * @since 4.0.0 - */ - public function setCategoryFactory(CategoryFactoryInterface $categoryFactory) - { - $this->categoryFactory = $categoryFactory; - } + /** + * Sets the internal category factory. + * + * @param CategoryFactoryInterface $categoryFactory The categories factory + * + * @return void + * + * @since 4.0.0 + */ + public function setCategoryFactory(CategoryFactoryInterface $categoryFactory) + { + $this->categoryFactory = $categoryFactory; + } - /** - * Adds Count Items for Category Manager. - * - * @param \stdClass[] $items The category objects - * @param string $section The section - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function countItems(array $items, string $section) - { - $config = (object) array( - 'related_tbl' => $this->getTableNameForSection($section), - 'state_col' => $this->getStateColumnForSection($section), - 'group_col' => 'catid', - 'relation_type' => 'category_or_group', - ); + /** + * Adds Count Items for Category Manager. + * + * @param \stdClass[] $items The category objects + * @param string $section The section + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function countItems(array $items, string $section) + { + $config = (object) array( + 'related_tbl' => $this->getTableNameForSection($section), + 'state_col' => $this->getStateColumnForSection($section), + 'group_col' => 'catid', + 'relation_type' => 'category_or_group', + ); - ContentHelper::countRelations($items, $config); - } + ContentHelper::countRelations($items, $config); + } - /** - * Prepares the category form - * - * @param Form $form The form to change - * @param array|object $data The form data - * - * @return void - */ - public function prepareForm(Form $form, $data) - { - } + /** + * Prepares the category form + * + * @param Form $form The form to change + * @param array|object $data The form data + * + * @return void + */ + public function prepareForm(Form $form, $data) + { + } - /** - * Returns the table for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getTableNameForSection(string $section = null) - { - return null; - } + /** + * Returns the table for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getTableNameForSection(string $section = null) + { + return null; + } - /** - * Returns the state column for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getStateColumnForSection(string $section = null) - { - return 'state'; - } + /** + * Returns the state column for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getStateColumnForSection(string $section = null) + { + return 'state'; + } } diff --git a/libraries/src/Categories/SectionNotFoundException.php b/libraries/src/Categories/SectionNotFoundException.php index 439101771d9b6..3dd2635ce8768 100644 --- a/libraries/src/Categories/SectionNotFoundException.php +++ b/libraries/src/Categories/SectionNotFoundException.php @@ -1,4 +1,5 @@ ` element - * - * @var string - * @since 4.0.0 - */ - protected $element; - - /** - * Update manifest `` element - * - * @var string - * @since 4.0.0 - */ - protected $type; - - /** - * Update manifest `` element - * - * @var string - * @since 4.0.0 - */ - protected $version; - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $security = array(); - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $fix = array(); - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $language = array(); - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $addition = array(); - - /** - * Update manifest `` elements - * - * @var array - * @since 4.0.0 - */ - protected $change = array(); - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $remove = array(); - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $note = array(); - - /** - * List of node items - * - * @var array - * @since 4.0.0 - */ - private $items = array(); - - /** - * Resource handle for the XML Parser - * - * @var resource - * @since 4.0.0 - */ - protected $xmlParser; - - /** - * Element call stack - * - * @var array - * @since 4.0.0 - */ - protected $stack = array('base'); - - /** - * Object containing the current update data - * - * @var \stdClass - * @since 4.0.0 - */ - protected $currentChangelog; - - /** - * The version to match the changelog - * - * @var string - * @since 4.0.0 - */ - private $matchVersion = ''; - - /** - * Object containing the latest changelog data - * - * @var \stdClass - * @since 4.0.0 - */ - protected $latest; - - /** - * Gets the reference to the current direct parent - * - * @return string - * - * @since 4.0.0 - */ - protected function getStackLocation() - { - return implode('->', $this->stack); - } - - /** - * Get the last position in stack count - * - * @return string - * - * @since 4.0.0 - */ - protected function getLastTag() - { - return $this->stack[\count($this->stack) - 1]; - } - - /** - * Set the version to match. - * - * @param string $version The version to match - * - * @return void - * - * @since 4.0.0 - */ - public function setVersion(string $version) - { - $this->matchVersion = $version; - } - - /** - * XML Start Element callback - * - * @param object $parser Parser object - * @param string $name Name of the tag found - * @param array $attrs Attributes of the tag - * - * @return void - * - * @note This is public because it is called externally - * @since 1.7.0 - */ - public function startElement($parser, $name, $attrs = array()) - { - $this->stack[] = $name; - $tag = $this->getStackLocation(); - - // Reset the data - if (isset($this->$tag)) - { - $this->$tag->data = ''; - } - - $name = strtolower($name); - - if (!isset($this->currentChangelog->$name)) - { - $this->currentChangelog->$name = new \stdClass; - } - - $this->currentChangelog->$name->data = ''; - - foreach ($attrs as $key => $data) - { - $key = strtolower($key); - $this->currentChangelog->$name->$key = $data; - } - } - - /** - * Callback for closing the element - * - * @param object $parser Parser object - * @param string $name Name of element that was closed - * - * @return void - * - * @note This is public because it is called externally - * @since 1.7.0 - */ - public function endElement($parser, $name) - { - array_pop($this->stack); - - switch ($name) - { - case 'SECURITY': - case 'FIX': - case 'LANGUAGE': - case 'ADDITION': - case 'CHANGE': - case 'REMOVE': - case 'NOTE': - $name = strtolower($name); - $this->currentChangelog->$name->data = $this->items; - $this->items = array(); - break; - case 'CHANGELOG': - if (version_compare($this->currentChangelog->version->data, $this->matchVersion, '==') === true) - { - $this->latest = $this->currentChangelog; - } - - // No version match, empty it - $this->currentChangelog = new \stdClass; - break; - case 'CHANGELOGS': - // If the latest item is set then we transfer it to where we want to - if (isset($this->latest)) - { - foreach (get_object_vars($this->latest) as $key => $val) - { - $this->$key = $val; - } - - unset($this->latest); - unset($this->currentChangelog); - } - elseif (isset($this->currentChangelog)) - { - // The update might be for an older version of j! - unset($this->currentChangelog); - } - break; - } - } - - /** - * Character Parser Function - * - * @param object $parser Parser object. - * @param object $data The data. - * - * @return void - * - * @note This is public because its called externally. - * @since 1.7.0 - */ - public function characterData($parser, $data) - { - $tag = $this->getLastTag(); - - switch ($tag) - { - case 'ITEM': - $this->items[] = $data; - break; - case 'SECURITY': - case 'FIX': - case 'LANGUAGE': - case 'ADDITION': - case 'CHANGE': - case 'REMOVE': - case 'NOTE': - break; - default: - // Throw the data for this item together - $tag = strtolower($tag); - - if (isset($this->currentChangelog->$tag)) - { - $this->currentChangelog->$tag->data .= $data; - } - break; - } - } - - /** - * Loads an XML file from a URL. - * - * @param string $url The URL. - * - * @return boolean True on success - * - * @since 4.0.0 - */ - public function loadFromXml($url) - { - $version = new Version; - $httpOption = new Registry; - $httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); - - try - { - $http = HttpFactory::getHttp($httpOption); - $response = $http->get($url); - } - catch (RuntimeException $e) - { - $response = null; - } - - if ($response === null || $response->code !== 200) - { - // @todo: Add a 'mark bad' setting here somehow - Log::add(Text::sprintf('JLIB_UPDATER_ERROR_EXTENSION_OPEN_URL', $url), Log::WARNING, 'jerror'); - - return false; - } - - $this->currentChangelog = new \stdClass; - - $this->xmlParser = xml_parser_create(''); - xml_set_object($this->xmlParser, $this); - xml_set_element_handler($this->xmlParser, 'startElement', 'endElement'); - xml_set_character_data_handler($this->xmlParser, 'characterData'); - - if (!xml_parse($this->xmlParser, $response->body)) - { - Log::add( - sprintf( - 'XML error: %s at line %d', xml_error_string(xml_get_error_code($this->xmlParser)), - xml_get_current_line_number($this->xmlParser) - ), - Log::WARNING, 'updater' - ); - - return false; - } - - xml_parser_free($this->xmlParser); - - return true; - } + /** + * Update manifest `` element + * + * @var string + * @since 4.0.0 + */ + protected $element; + + /** + * Update manifest `` element + * + * @var string + * @since 4.0.0 + */ + protected $type; + + /** + * Update manifest `` element + * + * @var string + * @since 4.0.0 + */ + protected $version; + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $security = array(); + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $fix = array(); + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $language = array(); + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $addition = array(); + + /** + * Update manifest `` elements + * + * @var array + * @since 4.0.0 + */ + protected $change = array(); + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $remove = array(); + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $note = array(); + + /** + * List of node items + * + * @var array + * @since 4.0.0 + */ + private $items = array(); + + /** + * Resource handle for the XML Parser + * + * @var resource + * @since 4.0.0 + */ + protected $xmlParser; + + /** + * Element call stack + * + * @var array + * @since 4.0.0 + */ + protected $stack = array('base'); + + /** + * Object containing the current update data + * + * @var \stdClass + * @since 4.0.0 + */ + protected $currentChangelog; + + /** + * The version to match the changelog + * + * @var string + * @since 4.0.0 + */ + private $matchVersion = ''; + + /** + * Object containing the latest changelog data + * + * @var \stdClass + * @since 4.0.0 + */ + protected $latest; + + /** + * Gets the reference to the current direct parent + * + * @return string + * + * @since 4.0.0 + */ + protected function getStackLocation() + { + return implode('->', $this->stack); + } + + /** + * Get the last position in stack count + * + * @return string + * + * @since 4.0.0 + */ + protected function getLastTag() + { + return $this->stack[\count($this->stack) - 1]; + } + + /** + * Set the version to match. + * + * @param string $version The version to match + * + * @return void + * + * @since 4.0.0 + */ + public function setVersion(string $version) + { + $this->matchVersion = $version; + } + + /** + * XML Start Element callback + * + * @param object $parser Parser object + * @param string $name Name of the tag found + * @param array $attrs Attributes of the tag + * + * @return void + * + * @note This is public because it is called externally + * @since 1.7.0 + */ + public function startElement($parser, $name, $attrs = array()) + { + $this->stack[] = $name; + $tag = $this->getStackLocation(); + + // Reset the data + if (isset($this->$tag)) { + $this->$tag->data = ''; + } + + $name = strtolower($name); + + if (!isset($this->currentChangelog->$name)) { + $this->currentChangelog->$name = new \stdClass(); + } + + $this->currentChangelog->$name->data = ''; + + foreach ($attrs as $key => $data) { + $key = strtolower($key); + $this->currentChangelog->$name->$key = $data; + } + } + + /** + * Callback for closing the element + * + * @param object $parser Parser object + * @param string $name Name of element that was closed + * + * @return void + * + * @note This is public because it is called externally + * @since 1.7.0 + */ + public function endElement($parser, $name) + { + array_pop($this->stack); + + switch ($name) { + case 'SECURITY': + case 'FIX': + case 'LANGUAGE': + case 'ADDITION': + case 'CHANGE': + case 'REMOVE': + case 'NOTE': + $name = strtolower($name); + $this->currentChangelog->$name->data = $this->items; + $this->items = array(); + break; + case 'CHANGELOG': + if (version_compare($this->currentChangelog->version->data, $this->matchVersion, '==') === true) { + $this->latest = $this->currentChangelog; + } + + // No version match, empty it + $this->currentChangelog = new \stdClass(); + break; + case 'CHANGELOGS': + // If the latest item is set then we transfer it to where we want to + if (isset($this->latest)) { + foreach (get_object_vars($this->latest) as $key => $val) { + $this->$key = $val; + } + + unset($this->latest); + unset($this->currentChangelog); + } elseif (isset($this->currentChangelog)) { + // The update might be for an older version of j! + unset($this->currentChangelog); + } + break; + } + } + + /** + * Character Parser Function + * + * @param object $parser Parser object. + * @param object $data The data. + * + * @return void + * + * @note This is public because its called externally. + * @since 1.7.0 + */ + public function characterData($parser, $data) + { + $tag = $this->getLastTag(); + + switch ($tag) { + case 'ITEM': + $this->items[] = $data; + break; + case 'SECURITY': + case 'FIX': + case 'LANGUAGE': + case 'ADDITION': + case 'CHANGE': + case 'REMOVE': + case 'NOTE': + break; + default: + // Throw the data for this item together + $tag = strtolower($tag); + + if (isset($this->currentChangelog->$tag)) { + $this->currentChangelog->$tag->data .= $data; + } + break; + } + } + + /** + * Loads an XML file from a URL. + * + * @param string $url The URL. + * + * @return boolean True on success + * + * @since 4.0.0 + */ + public function loadFromXml($url) + { + $version = new Version(); + $httpOption = new Registry(); + $httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); + + try { + $http = HttpFactory::getHttp($httpOption); + $response = $http->get($url); + } catch (RuntimeException $e) { + $response = null; + } + + if ($response === null || $response->code !== 200) { + // @todo: Add a 'mark bad' setting here somehow + Log::add(Text::sprintf('JLIB_UPDATER_ERROR_EXTENSION_OPEN_URL', $url), Log::WARNING, 'jerror'); + + return false; + } + + $this->currentChangelog = new \stdClass(); + + $this->xmlParser = xml_parser_create(''); + xml_set_object($this->xmlParser, $this); + xml_set_element_handler($this->xmlParser, 'startElement', 'endElement'); + xml_set_character_data_handler($this->xmlParser, 'characterData'); + + if (!xml_parse($this->xmlParser, $response->body)) { + Log::add( + sprintf( + 'XML error: %s at line %d', + xml_error_string(xml_get_error_code($this->xmlParser)), + xml_get_current_line_number($this->xmlParser) + ), + Log::WARNING, + 'updater' + ); + + return false; + } + + xml_parser_free($this->xmlParser); + + return true; + } } diff --git a/libraries/src/Client/ClientHelper.php b/libraries/src/Client/ClientHelper.php index 26ba9afe1b0c9..472765a65a23d 100644 --- a/libraries/src/Client/ClientHelper.php +++ b/libraries/src/Client/ClientHelper.php @@ -1,4 +1,5 @@ $app->get('ftp_enable'), - 'host' => $app->get('ftp_host'), - 'port' => $app->get('ftp_port'), - 'user' => $app->get('ftp_user'), - 'pass' => $app->get('ftp_pass'), - 'root' => $app->get('ftp_root'), - ); - break; - - default: - $options = array('enabled' => false, 'host' => '', 'port' => '', 'user' => '', 'pass' => '', 'root' => ''); - break; - } - - // If user and pass are not set in global config lets see if they are in the session - if ($options['enabled'] == true && ($options['user'] == '' || $options['pass'] == '')) - { - $session = Factory::getSession(); - $options['user'] = $session->get($client . '.user', null, 'JClientHelper'); - $options['pass'] = $session->get($client . '.pass', null, 'JClientHelper'); - } - - // If user or pass are missing, disable this client - if ($options['user'] == '' || $options['pass'] == '') - { - $options['enabled'] = false; - } - - // Save the credentials for later use - $credentials[$client] = $options; - } - - return $credentials[$client]; - } - - /** - * Method to set client login credentials - * - * @param string $client Client name, currently only 'ftp' is supported - * @param string $user Username - * @param string $pass Password - * - * @return boolean True if the given login credentials have been set and are valid - * - * @since 1.7.0 - */ - public static function setCredentials($client, $user, $pass) - { - $return = false; - $client = strtolower($client); - - // Test if the given credentials are valid - switch ($client) - { - case 'ftp': - $app = Factory::getApplication(); - $options = array('enabled' => $app->get('ftp_enable'), 'host' => $app->get('ftp_host'), 'port' => $app->get('ftp_port')); - - if ($options['enabled']) - { - $ftp = FtpClient::getInstance($options['host'], $options['port']); - - // Test the connection and try to log in - if ($ftp->isConnected()) - { - if ($ftp->login($user, $pass)) - { - $return = true; - } - - $ftp->quit(); - } - } - break; - - default: - break; - } - - if ($return) - { - // Save valid credentials to the session - $session = Factory::getSession(); - $session->set($client . '.user', $user, 'JClientHelper'); - $session->set($client . '.pass', $pass, 'JClientHelper'); - - // Force re-creation of the data saved within JClientHelper::getCredentials() - self::getCredentials($client, true); - } - - return $return; - } - - /** - * Method to determine if client login credentials are present - * - * @param string $client Client name, currently only 'ftp' is supported - * - * @return boolean True if login credentials are available - * - * @since 1.7.0 - */ - public static function hasCredentials($client) - { - $return = false; - $client = strtolower($client); - - // Get (unmodified) credentials for this client - switch ($client) - { - case 'ftp': - $app = Factory::getApplication(); - $options = array('enabled' => $app->get('ftp_enable'), 'user' => $app->get('ftp_user'), 'pass' => $app->get('ftp_pass')); - break; - - default: - $options = array('enabled' => false, 'user' => '', 'pass' => ''); - break; - } - - if ($options['enabled'] == false) - { - // The client is disabled in global config, so let's pretend we are OK - $return = true; - } - elseif ($options['user'] != '' && $options['pass'] != '') - { - // Login credentials are available in global config - $return = true; - } - else - { - // Check if login credentials are available in the session - $session = Factory::getSession(); - $user = $session->get($client . '.user', null, 'JClientHelper'); - $pass = $session->get($client . '.pass', null, 'JClientHelper'); - - if ($user != '' && $pass != '') - { - $return = true; - } - } - - return $return; - } - - /** - * Determine whether input fields for client settings need to be shown - * - * If valid credentials were passed along with the request, they are saved to the session. - * This functions returns an exception if invalid credentials have been given or if the - * connection to the server failed for some other reason. - * - * @param string $client The name of the client. - * - * @return boolean True if credentials are present - * - * @since 1.7.0 - * @throws \InvalidArgumentException if credentials invalid - */ - public static function setCredentialsFromRequest($client) - { - // Determine whether FTP credentials have been passed along with the current request - $input = Factory::getApplication()->input; - $user = $input->post->getString('username', null); - $pass = $input->post->getString('password', null); - - if ($user != '' && $pass != '') - { - // Add credentials to the session - if (!self::setCredentials($client, $user, $pass)) - { - throw new \InvalidArgumentException('Invalid user credentials'); - } - - $return = false; - } - else - { - // Just determine if the FTP input fields need to be shown - $return = !self::hasCredentials('ftp'); - } - - return $return; - } + /** + * Method to return the array of client layer configuration options + * + * @param string $client Client name, currently only 'ftp' is supported + * @param boolean $force Forces re-creation of the login credentials. Set this to + * true if login credentials in the session storage have changed + * + * @return array Client layer configuration options, consisting of at least + * these fields: enabled, host, port, user, pass, root + * + * @since 1.7.0 + */ + public static function getCredentials($client, $force = false) + { + static $credentials = array(); + + $client = strtolower($client); + + if (!isset($credentials[$client]) || $force) { + $app = Factory::getApplication(); + + // Fetch the client layer configuration options for the specific client + switch ($client) { + case 'ftp': + $options = array( + 'enabled' => $app->get('ftp_enable'), + 'host' => $app->get('ftp_host'), + 'port' => $app->get('ftp_port'), + 'user' => $app->get('ftp_user'), + 'pass' => $app->get('ftp_pass'), + 'root' => $app->get('ftp_root'), + ); + break; + + default: + $options = array('enabled' => false, 'host' => '', 'port' => '', 'user' => '', 'pass' => '', 'root' => ''); + break; + } + + // If user and pass are not set in global config lets see if they are in the session + if ($options['enabled'] == true && ($options['user'] == '' || $options['pass'] == '')) { + $session = Factory::getSession(); + $options['user'] = $session->get($client . '.user', null, 'JClientHelper'); + $options['pass'] = $session->get($client . '.pass', null, 'JClientHelper'); + } + + // If user or pass are missing, disable this client + if ($options['user'] == '' || $options['pass'] == '') { + $options['enabled'] = false; + } + + // Save the credentials for later use + $credentials[$client] = $options; + } + + return $credentials[$client]; + } + + /** + * Method to set client login credentials + * + * @param string $client Client name, currently only 'ftp' is supported + * @param string $user Username + * @param string $pass Password + * + * @return boolean True if the given login credentials have been set and are valid + * + * @since 1.7.0 + */ + public static function setCredentials($client, $user, $pass) + { + $return = false; + $client = strtolower($client); + + // Test if the given credentials are valid + switch ($client) { + case 'ftp': + $app = Factory::getApplication(); + $options = array('enabled' => $app->get('ftp_enable'), 'host' => $app->get('ftp_host'), 'port' => $app->get('ftp_port')); + + if ($options['enabled']) { + $ftp = FtpClient::getInstance($options['host'], $options['port']); + + // Test the connection and try to log in + if ($ftp->isConnected()) { + if ($ftp->login($user, $pass)) { + $return = true; + } + + $ftp->quit(); + } + } + break; + + default: + break; + } + + if ($return) { + // Save valid credentials to the session + $session = Factory::getSession(); + $session->set($client . '.user', $user, 'JClientHelper'); + $session->set($client . '.pass', $pass, 'JClientHelper'); + + // Force re-creation of the data saved within JClientHelper::getCredentials() + self::getCredentials($client, true); + } + + return $return; + } + + /** + * Method to determine if client login credentials are present + * + * @param string $client Client name, currently only 'ftp' is supported + * + * @return boolean True if login credentials are available + * + * @since 1.7.0 + */ + public static function hasCredentials($client) + { + $return = false; + $client = strtolower($client); + + // Get (unmodified) credentials for this client + switch ($client) { + case 'ftp': + $app = Factory::getApplication(); + $options = array('enabled' => $app->get('ftp_enable'), 'user' => $app->get('ftp_user'), 'pass' => $app->get('ftp_pass')); + break; + + default: + $options = array('enabled' => false, 'user' => '', 'pass' => ''); + break; + } + + if ($options['enabled'] == false) { + // The client is disabled in global config, so let's pretend we are OK + $return = true; + } elseif ($options['user'] != '' && $options['pass'] != '') { + // Login credentials are available in global config + $return = true; + } else { + // Check if login credentials are available in the session + $session = Factory::getSession(); + $user = $session->get($client . '.user', null, 'JClientHelper'); + $pass = $session->get($client . '.pass', null, 'JClientHelper'); + + if ($user != '' && $pass != '') { + $return = true; + } + } + + return $return; + } + + /** + * Determine whether input fields for client settings need to be shown + * + * If valid credentials were passed along with the request, they are saved to the session. + * This functions returns an exception if invalid credentials have been given or if the + * connection to the server failed for some other reason. + * + * @param string $client The name of the client. + * + * @return boolean True if credentials are present + * + * @since 1.7.0 + * @throws \InvalidArgumentException if credentials invalid + */ + public static function setCredentialsFromRequest($client) + { + // Determine whether FTP credentials have been passed along with the current request + $input = Factory::getApplication()->input; + $user = $input->post->getString('username', null); + $pass = $input->post->getString('password', null); + + if ($user != '' && $pass != '') { + // Add credentials to the session + if (!self::setCredentials($client, $user, $pass)) { + throw new \InvalidArgumentException('Invalid user credentials'); + } + + $return = false; + } else { + // Just determine if the FTP input fields need to be shown + $return = !self::hasCredentials('ftp'); + } + + return $return; + } } diff --git a/libraries/src/Client/FtpClient.php b/libraries/src/Client/FtpClient.php index f7d86fb5326e1..4d65ead463980 100644 --- a/libraries/src/Client/FtpClient.php +++ b/libraries/src/Client/FtpClient.php @@ -1,4 +1,5 @@ "\n", 'WIN' => "\r\n"); - - /** - * @var array FtpClient instances container. - * @since 2.5 - */ - protected static $instances = array(); - - /** - * FtpClient object constructor - * - * @param array $options Associative array of options to set - * - * @since 1.5 - */ - public function __construct(array $options = array()) - { - // If default transfer type is not set, set it to autoascii detect - if (!isset($options['type'])) - { - $options['type'] = FTP_BINARY; - } - - $this->setOptions($options); - - if (FTP_NATIVE) - { - BufferStreamHandler::stream_register(); - } - } - - /** - * FtpClient object destructor - * - * Closes an existing connection, if we have one - * - * @since 1.5 - */ - public function __destruct() - { - if (\is_resource($this->_conn)) - { - $this->quit(); - } - } - - /** - * Returns the global FTP connector object, only creating it - * if it doesn't already exist. - * - * You may optionally specify a username and password in the parameters. If you do so, - * you may not login() again with different credentials using the same object. - * If you do not use this option, you must quit() the current connection when you - * are done, to free it for use by others. - * - * @param string $host Host to connect to - * @param string $port Port to connect to - * @param array $options Array with any of these options: type=>[FTP_AUTOASCII|FTP_ASCII|FTP_BINARY], timeout=>(int) - * @param string $user Username to use for a connection - * @param string $pass Password to use for a connection - * - * @return FtpClient The FTP Client object. - * - * @since 1.5 - */ - public static function getInstance($host = '127.0.0.1', $port = '21', array $options = array(), $user = null, $pass = null) - { - $signature = $user . ':' . $pass . '@' . $host . ':' . $port; - - // Create a new instance, or set the options of an existing one - if (!isset(static::$instances[$signature]) || !\is_object(static::$instances[$signature])) - { - static::$instances[$signature] = new static($options); - } - else - { - static::$instances[$signature]->setOptions($options); - } - - // Connect to the server, and login, if requested - if (!static::$instances[$signature]->isConnected()) - { - $return = static::$instances[$signature]->connect($host, $port); - - if ($return && $user !== null && $pass !== null) - { - static::$instances[$signature]->login($user, $pass); - } - } - - return static::$instances[$signature]; - } - - /** - * Set client options - * - * @param array $options Associative array of options to set - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function setOptions(array $options) - { - if (isset($options['type'])) - { - $this->_type = $options['type']; - } - - if (isset($options['timeout'])) - { - $this->_timeout = $options['timeout']; - } - - return true; - } - - /** - * Method to connect to a FTP server - * - * @param string $host Host to connect to [Default: 127.0.0.1] - * @param int $port Port to connect on [Default: port 21] - * - * @return boolean True if successful - * - * @since 3.0.0 - */ - public function connect($host = '127.0.0.1', $port = 21) - { - $errno = null; - $err = null; - - // If already connected, return - if (\is_resource($this->_conn)) - { - return true; - } - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - $this->_conn = @ftp_connect($host, $port, $this->_timeout); - - if ($this->_conn === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__, $host, $port), Log::WARNING, 'jerror'); - - return false; - } - - // Set the timeout for this connection - ftp_set_option($this->_conn, FTP_TIMEOUT_SEC, $this->_timeout); - - return true; - } - - // Connect to the FTP server. - $this->_conn = @ fsockopen($host, $port, $errno, $err, $this->_timeout); - - if (!$this->_conn) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT_SOCKET', __METHOD__, $host, $port, $errno, $err), Log::WARNING, 'jerror'); - - return false; - } - - // Set the timeout for this connection - socket_set_timeout($this->_conn, $this->_timeout, 0); - - // Check for welcome response code - if (!$this->_verifyResponse(220)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 220), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to determine if the object is connected to an FTP server - * - * @return boolean True if connected - * - * @since 1.5 - */ - public function isConnected() - { - return \is_resource($this->_conn); - } - - /** - * Method to login to a server once connected - * - * @param string $user Username to login to the server - * @param string $pass Password to login to the server - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function login($user = 'anonymous', $pass = 'jftp@joomla.org') - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_login($this->_conn, $user, $pass) === false) - { - Log::add('JFtp::login: Unable to login', Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send the username - if (!$this->_putCmd('USER ' . $user, array(331, 503))) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_USERNAME', __METHOD__, $this->_response, $user), Log::WARNING, 'jerror'); - - return false; - } - - // If we are already logged in, continue :) - if ($this->_responseCode == 503) - { - return true; - } - - // Send the password - if (!$this->_putCmd('PASS ' . $pass, 230)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_PASSWORD', __METHOD__, $this->_response, str_repeat('*', \strlen($pass))), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to quit and close the connection - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function quit() - { - // If native FTP support is enabled lets use it... - if (FTP_NATIVE) - { - @ftp_close($this->_conn); - - return true; - } - - // Logout and close connection - @fwrite($this->_conn, "QUIT\r\n"); - @fclose($this->_conn); - - return true; - } - - /** - * Method to retrieve the current working directory on the FTP server - * - * @return string Current working directory - * - * @since 1.5 - */ - public function pwd() - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (($ret = @ftp_pwd($this->_conn)) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return $ret; - } - - $match = array(null); - - // Send print working directory command and verify success - if (!$this->_putCmd('PWD', 257)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 257), Log::WARNING, 'jerror'); - - return false; - } - - // Match just the path - preg_match('/"[^"\r\n]*"/', $this->_response, $match); - - // Return the cleaned path - return preg_replace("/\"/", '', $match[0]); - } - - /** - * Method to system string from the FTP server - * - * @return string System identifier string - * - * @since 1.5 - */ - public function syst() - { - // If native FTP support is enabled lets use it... - if (FTP_NATIVE) - { - if (($ret = @ftp_systype($this->_conn)) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - } - else - { - // Send print working directory command and verify success - if (!$this->_putCmd('SYST', 215)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 215), Log::WARNING, 'jerror'); - - return false; - } - - $ret = $this->_response; - } - - // Match the system string to an OS - if (strpos(strtoupper($ret), 'MAC') !== false) - { - $ret = 'MAC'; - } - elseif (strpos(strtoupper($ret), 'WIN') !== false) - { - $ret = 'WIN'; - } - else - { - $ret = 'UNIX'; - } - - // Return the os type - return $ret; - } - - /** - * Method to change the current working directory on the FTP server - * - * @param string $path Path to change into on the server - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function chdir($path) - { - // If native FTP support is enabled lets use it... - if (FTP_NATIVE) - { - if (@ftp_chdir($this->_conn, $path) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send change directory command and verify success - if (!$this->_putCmd('CWD ' . $path, 250)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 250, $path), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to reinitialise the server, ie. need to login again - * - * NOTE: This command not available on all servers - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function reinit() - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_site($this->_conn, 'REIN') === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send reinitialise command to the server - if (!$this->_putCmd('REIN', 220)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 220), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to rename a file/folder on the FTP server - * - * @param string $from Path to change file/folder from - * @param string $to Path to change file/folder to - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function rename($from, $to) - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_rename($this->_conn, $from, $to) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send rename from command to the server - if (!$this->_putCmd('RNFR ' . $from, 350)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RENAME_BAD_RESPONSE_FROM', __METHOD__, $this->_response, $from), Log::WARNING, 'jerror'); - - return false; - } - - // Send rename to command to the server - if (!$this->_putCmd('RNTO ' . $to, 250)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RENAME_BAD_RESPONSE_TO', __METHOD__, $this->_response, $to), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to change mode for a path on the FTP server - * - * @param string $path Path to change mode on - * @param mixed $mode Octal value to change mode to, e.g. '0777', 0777 or 511 (string or integer) - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function chmod($path, $mode) - { - // If no filename is given, we assume the current directory is the target - if ($path == '') - { - $path = '.'; - } - - // Convert the mode to a string - if (\is_int($mode)) - { - $mode = decoct($mode); - } - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_site($this->_conn, 'CHMOD ' . $mode . ' ' . $path) === false) - { - if (!IS_WIN) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - } - - return false; - } - - return true; - } - - // Send change mode command and verify success [must convert mode from octal] - if (!$this->_putCmd('SITE CHMOD ' . $mode . ' ' . $path, array(200, 250))) - { - if (!IS_WIN) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_CHMOD_BAD_RESPONSE', __METHOD__, $this->_response, $path, $mode), Log::WARNING, 'jerror'); - } - - return false; - } - - return true; - } - - /** - * Method to delete a path [file/folder] on the FTP server - * - * @param string $path Path to delete - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function delete($path) - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_delete($this->_conn, $path) === false) - { - if (@ftp_rmdir($this->_conn, $path) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - } - - return true; - } - - // Send delete file command and if that doesn't work, try to remove a directory - if (!$this->_putCmd('DELE ' . $path, 250)) - { - if (!$this->_putCmd('RMD ' . $path, 250)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 250, $path), Log::WARNING, 'jerror'); - - return false; - } - } - - return true; - } - - /** - * Method to create a directory on the FTP server - * - * @param string $path Directory to create - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function mkdir($path) - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_mkdir($this->_conn, $path) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send change directory command and verify success - if (!$this->_putCmd('MKD ' . $path, 257)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 257, $path), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to restart data transfer at a given byte - * - * @param integer $point Byte to restart transfer at - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function restart($point) - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_site($this->_conn, 'REST ' . $point) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send restart command and verify success - if (!$this->_putCmd('REST ' . $point, 350)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RESTART_BAD_RESPONSE', __METHOD__, $this->_response, $point), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to create an empty file on the FTP server - * - * @param string $path Path local file to store on the FTP server - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function create($path) - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $buffer = fopen('buffer://tmp', 'r'); - - if (@ftp_fput($this->_conn, $path, $buffer, FTP_ASCII) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - fclose($buffer); - - return false; - } - - fclose($buffer); - - return true; - } - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (!$this->_putCmd('STOR ' . $path, array(150, 125))) - { - @ fclose($this->_dataconn); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - - return false; - } - - // To create a zero byte upload close the data port connection - fclose($this->_dataconn); - - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to read a file from the FTP server's contents into a buffer - * - * @param string $remote Path to remote file to read on the FTP server - * @param string &$buffer Buffer variable to read file contents into - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function read($remote, &$buffer) - { - // Determine file type - $mode = $this->_findMode($remote); - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $tmp = fopen('buffer://tmp', 'br+'); - - if (@ftp_fget($this->_conn, $tmp, $remote, $mode) === false) - { - fclose($tmp); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Read tmp buffer contents - rewind($tmp); - $buffer = ''; - - while (!feof($tmp)) - { - $buffer .= fread($tmp, 8192); - } - - fclose($tmp); - - return true; - } - - $this->_mode($mode); - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (!$this->_putCmd('RETR ' . $remote, array(150, 125))) - { - @ fclose($this->_dataconn); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - // Read data from data port connection and add to the buffer - $buffer = ''; - - while (!feof($this->_dataconn)) - { - $buffer .= fread($this->_dataconn, 4096); - } - - // Close the data port connection - fclose($this->_dataconn); - - // Let's try to cleanup some line endings if it is ascii - if ($mode == FTP_ASCII) - { - $os = 'UNIX'; - - if (IS_WIN) - { - $os = 'WIN'; - } - - $buffer = preg_replace('/' . CRLF . '/', $this->_lineEndings[$os], $buffer); - } - - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to get a file from the FTP server and save it to a local file - * - * @param string $local Local path to save remote file to - * @param string $remote Path to remote file to get on the FTP server - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function get($local, $remote) - { - // Determine file type - $mode = $this->_findMode($remote); - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (@ftp_get($this->_conn, $local, $remote, $mode) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - $this->_mode($mode); - - // Check to see if the local file can be opened for writing - $fp = fopen($local, 'wb'); - - if (!$fp) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_OPEN_WRITING', __METHOD__, $local), Log::WARNING, 'jerror'); - - return false; - } - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (!$this->_putCmd('RETR ' . $remote, array(150, 125))) - { - @ fclose($this->_dataconn); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - // Read data from data port connection and add to the buffer - while (!feof($this->_dataconn)) - { - $buffer = fread($this->_dataconn, 4096); - fwrite($fp, $buffer, 4096); - } - - // Close the data port connection and file pointer - fclose($this->_dataconn); - fclose($fp); - - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to store a file to the FTP server - * - * @param string $local Path to local file to store on the FTP server - * @param string $remote FTP path to file to create - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function store($local, $remote = null) - { - // If remote file is not given, use the filename of the local file in the current - // working directory. - if ($remote == null) - { - $remote = basename($local); - } - - // Determine file type - $mode = $this->_findMode($remote); - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (@ftp_put($this->_conn, $remote, $local, $mode) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - $this->_mode($mode); - - // Check to see if the local file exists and if so open it for reading - if (@ file_exists($local)) - { - $fp = fopen($local, 'rb'); - - if (!$fp) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_OPEN_READING', __METHOD__, $local), Log::WARNING, 'jerror'); - - return false; - } - } - else - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_FIND', __METHOD__, $local), Log::WARNING, 'jerror'); - - return false; - } - - // Start passive mode - if (!$this->_passive()) - { - @ fclose($fp); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Send store command to the FTP server - if (!$this->_putCmd('STOR ' . $remote, array(150, 125))) - { - @ fclose($fp); - @ fclose($this->_dataconn); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - // Do actual file transfer, read local file and write to data port connection - while (!feof($fp)) - { - $line = fread($fp, 4096); - - do - { - if (($result = @ fwrite($this->_dataconn, $line)) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $line = substr($line, $result); - } - while ($line != ''); - } - - fclose($fp); - fclose($this->_dataconn); - - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to write a string to the FTP server - * - * @param string $remote FTP path to file to write to - * @param string $buffer Contents to write to the FTP server - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function write($remote, $buffer) - { - // Determine file type - $mode = $this->_findMode($remote); - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $tmp = fopen('buffer://tmp', 'br+'); - fwrite($tmp, $buffer); - rewind($tmp); - - if (@ftp_fput($this->_conn, $remote, $tmp, $mode) === false) - { - fclose($tmp); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - fclose($tmp); - - return true; - } - - // First we need to set the transfer mode - $this->_mode($mode); - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Send store command to the FTP server - if (!$this->_putCmd('STOR ' . $remote, array(150, 125))) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - @ fclose($this->_dataconn); - - return false; - } - - // Write buffer to the data connection port - do - { - if (($result = @ fwrite($this->_dataconn, $buffer)) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $buffer = substr($buffer, $result); - } - while ($buffer != ''); - - // Close the data connection port [Data transfer complete] - fclose($this->_dataconn); - - // Verify that the server received the transfer - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to append a string to the FTP server - * - * @param string $remote FTP path to file to append to - * @param string $buffer Contents to append to the FTP server - * - * @return boolean True if successful - * - * @since 3.6.0 - */ - public function append($remote, $buffer) - { - // Determine file type - $mode = $this->_findMode($remote); - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); - } - - $tmp = fopen('buffer://tmp', 'bw+'); - fwrite($tmp, $buffer); - rewind($tmp); - - $size = $this->size($remote); - - if ($size === false) - { - } - - if (@ftp_fput($this->_conn, $remote, $tmp, $mode, $size) === false) - { - fclose($tmp); - - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), 35); - } - - fclose($tmp); - - return true; - } - - // First we need to set the transfer mode - $this->_mode($mode); - - // Start passive mode - if (!$this->_passive()) - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); - } - - // Send store command to the FTP server - if (!$this->_putCmd('APPE ' . $remote, array(150, 125))) - { - @fclose($this->_dataconn); - - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), 35); - } - - // Write buffer to the data connection port - do - { - if (($result = @ fwrite($this->_dataconn, $buffer)) === false) - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), 37); - } - - $buffer = substr($buffer, $result); - } - while ($buffer != ''); - - // Close the data connection port [Data transfer complete] - fclose($this->_dataconn); - - // Verify that the server received the transfer - if (!$this->_verifyResponse(226)) - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), 37); - } - - return true; - } - - /** - * Get the size of the remote file. - * - * @param string $remote FTP path to file whose size to get - * - * @return mixed number of bytes or false on error - * - * @since 3.6.0 - */ - public function size($remote) - { - if (FTP_NATIVE) - { - $size = ftp_size($this->_conn, $remote); - - // In case ftp_size fails, try the SIZE command directly. - if ($size === -1) - { - $response = ftp_raw($this->_conn, 'SIZE ' . $remote); - $responseCode = substr($response[0], 0, 3); - $responseMessage = substr($response[0], 4); - - if ($responseCode != '213') - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), 35); - } - - $size = (int) $responseMessage; - } - - return $size; - } - - // Start passive mode - if (!$this->_passive()) - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); - } - - // Send size command to the FTP server - if (!$this->_putCmd('SIZE ' . $remote, array(213))) - { - @fclose($this->_dataconn); - - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 213, $remote), 35); - } - - return (int) substr($this->_responseMsg, 4); - } - - /** - * Method to list the filenames of the contents of a directory on the FTP server - * - * Note: Some servers also return folder names. However, to be sure to list folders on all - * servers, you should use listDetails() instead if you also need to deal with folders - * - * @param string $path Path local file to store on the FTP server - * - * @return string Directory listing - * - * @since 1.5 - */ - public function listNames($path = null) - { - $data = null; - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (($list = @ftp_nlist($this->_conn, $path)) === false) - { - // Workaround for empty directories on some servers - if ($this->listDetails($path, 'files') === array()) - { - return array(); - } - - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $list = preg_replace('#^' . preg_quote($path, '#') . '[/\\\\]?#', '', $list); - - if ($keys = array_merge(array_keys($list, '.'), array_keys($list, '..'))) - { - foreach ($keys as $key) - { - unset($list[$key]); - } - } - - return $list; - } - - // If a path exists, prepend a space - if ($path != null) - { - $path = ' ' . $path; - } - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (!$this->_putCmd('NLST' . $path, array(150, 125))) - { - @ fclose($this->_dataconn); - - // Workaround for empty directories on some servers - if ($this->listDetails($path, 'files') === array()) - { - return array(); - } - - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - - return false; - } - - // Read in the file listing. - while (!feof($this->_dataconn)) - { - $data .= fread($this->_dataconn, 4096); - } - - fclose($this->_dataconn); - - // Everything go okay? - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - - return false; - } - - $data = preg_split('/[' . CRLF . ']+/', $data, -1, PREG_SPLIT_NO_EMPTY); - $data = preg_replace('#^' . preg_quote(substr($path, 1), '#') . '[/\\\\]?#', '', $data); - - if ($keys = array_merge(array_keys($data, '.'), array_keys($data, '..'))) - { - foreach ($keys as $key) - { - unset($data[$key]); - } - } - - return $data; - } - - /** - * Method to list the contents of a directory on the FTP server - * - * @param string $path Path to the local file to be stored on the FTP server - * @param string $type Return type [raw|all|folders|files] - * - * @return mixed If $type is raw: string Directory listing, otherwise array of string with file-names - * - * @since 1.5 - */ - public function listDetails($path = null, $type = 'all') - { - $dir_list = array(); - $data = null; - $regs = null; - - // @todo: Deal with recurse -- nightmare - // For now we will just set it to false - $recurse = false; - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (($contents = @ftp_rawlist($this->_conn, $path)) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - } - else - { - // Non Native mode - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // If a path exists, prepend a space - if ($path != null) - { - $path = ' ' . $path; - } - - // Request the file listing - if (!$this->_putCmd(($recurse == true) ? 'LIST -R' : 'LIST' . $path, array(150, 125))) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - @ fclose($this->_dataconn); - - return false; - } - - // Read in the file listing. - while (!feof($this->_dataconn)) - { - $data .= fread($this->_dataconn, 4096); - } - - fclose($this->_dataconn); - - // Everything go okay? - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - - return false; - } - - $contents = explode(CRLF, $data); - } - - // If only raw output is requested we are done - if ($type === 'raw') - { - return $data; - } - - // If we received the listing of an empty directory, we are done as well - if (empty($contents[0])) - { - return $dir_list; - } - - // If the server returned the number of results in the first response, let's dump it - if (strtolower(substr($contents[0], 0, 6)) === 'total ') - { - array_shift($contents); - - if (!isset($contents[0]) || empty($contents[0])) - { - return $dir_list; - } - } - - // Regular expressions for the directory listing parsing. - $regexps = array( - 'UNIX' => '#([-dl][rwxstST-]+).* ([0-9]*) ([a-zA-Z0-9]+).* ([a-zA-Z0-9]+).* ([0-9]*)' - . ' ([a-zA-Z]+[0-9: ]*[0-9])[ ]+(([0-9]{1,2}:[0-9]{2})|[0-9]{4}) (.+)#', - 'MAC' => '#([-dl][rwxstST-]+).* ?([0-9 ]*)?([a-zA-Z0-9]+).* ([a-zA-Z0-9]+).* ([0-9]*)' - . ' ([a-zA-Z]+[0-9: ]*[0-9])[ ]+(([0-9]{2}:[0-9]{2})|[0-9]{4}) (.+)#', - 'WIN' => '#([0-9]{2})-([0-9]{2})-([0-9]{2}) +([0-9]{2}):([0-9]{2})(AM|PM) +([0-9]+|) +(.+)#', - ); - - // Find out the format of the directory listing by matching one of the regexps - $osType = null; - - foreach ($regexps as $k => $v) - { - if (@preg_match($v, $contents[0])) - { - $osType = $k; - $regexp = $v; - break; - } - } - - if (!$osType) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_UNRECOGNISED_FOLDER_LISTING_FORMATJLIB_CLIENT_ERROR_JFTP_LISTDETAILS_UNRECOGNISED', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Here is where it is going to get dirty.... - if ($osType === 'UNIX' || $osType === 'MAC') - { - foreach ($contents as $file) - { - $tmp_array = null; - - if (@preg_match($regexp, $file, $regs)) - { - $fType = (int) strpos('-dl', $regs[1][0]); - - // $tmp_array['line'] = $regs[0]; - $tmp_array['type'] = $fType; - $tmp_array['rights'] = $regs[1]; - - // $tmp_array['number'] = $regs[2]; - $tmp_array['user'] = $regs[3]; - $tmp_array['group'] = $regs[4]; - $tmp_array['size'] = $regs[5]; - $tmp_array['date'] = @date('m-d', strtotime($regs[6])); - $tmp_array['time'] = $regs[7]; - $tmp_array['name'] = $regs[9]; - } - - // If we just want files, do not add a folder - if ($type === 'files' && $tmp_array['type'] == 1) - { - continue; - } - - // If we just want folders, do not add a file - if ($type === 'folders' && $tmp_array['type'] == 0) - { - continue; - } - - if (\is_array($tmp_array) && $tmp_array['name'] != '.' && $tmp_array['name'] != '..') - { - $dir_list[] = $tmp_array; - } - } - } - else - { - foreach ($contents as $file) - { - $tmp_array = null; - - if (@preg_match($regexp, $file, $regs)) - { - $fType = (int) ($regs[7] === ''); - $timestamp = strtotime("$regs[3]-$regs[1]-$regs[2] $regs[4]:$regs[5]$regs[6]"); - - // $tmp_array['line'] = $regs[0]; - $tmp_array['type'] = $fType; - $tmp_array['rights'] = ''; - - // $tmp_array['number'] = 0; - $tmp_array['user'] = ''; - $tmp_array['group'] = ''; - $tmp_array['size'] = (int) $regs[7]; - $tmp_array['date'] = date('m-d', $timestamp); - $tmp_array['time'] = date('H:i', $timestamp); - $tmp_array['name'] = $regs[8]; - } - - // If we just want files, do not add a folder - if ($type === 'files' && $tmp_array['type'] == 1) - { - continue; - } - - // If we just want folders, do not add a file - if ($type === 'folders' && $tmp_array['type'] == 0) - { - continue; - } - - if (\is_array($tmp_array) && $tmp_array['name'] != '.' && $tmp_array['name'] != '..') - { - $dir_list[] = $tmp_array; - } - } - } - - return $dir_list; - } - - /** - * Send command to the FTP server and validate an expected response code - * - * @param string $cmd Command to send to the FTP server - * @param mixed $expectedResponse Integer response code or array of integer response codes - * - * @return boolean True if command executed successfully - * - * @since 1.5 - */ - protected function _putCmd($cmd, $expectedResponse) - { - // Make sure we have a connection to the server - if (!\is_resource($this->_conn)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PUTCMD_UNCONNECTED', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Send the command to the server - if (!fwrite($this->_conn, $cmd . "\r\n")) - { - Log::add(Text::sprintf('DDD', Text::sprintf('JLIB_CLIENT_ERROR_FTP_PUTCMD_SEND', __METHOD__, $cmd)), Log::WARNING, 'jerror'); - } - - return $this->_verifyResponse($expectedResponse); - } - - /** - * Verify the response code from the server and log response if flag is set - * - * @param mixed $expected Integer response code or array of integer response codes - * - * @return boolean True if response code from the server is expected - * - * @since 1.5 - */ - protected function _verifyResponse($expected) - { - $parts = null; - - // Wait for a response from the server, but timeout after the set time limit - $endTime = time() + $this->_timeout; - $this->_response = ''; - - do - { - $this->_response .= fgets($this->_conn, 4096); - } - while (!preg_match('/^([0-9]{3})(-(.*' . CRLF . ')+\1)? [^' . CRLF . ']+' . CRLF . "$/", $this->_response, $parts) && time() < $endTime); - - // Catch a timeout or bad response - if (!isset($parts[1])) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TIMEOUT', __METHOD__, $this->_response), Log::WARNING, 'jerror'); - - return false; - } - - // Separate the code from the message - $this->_responseCode = $parts[1]; - $this->_responseMsg = $parts[0]; - - // Did the server respond with the code we wanted? - if (\is_array($expected)) - { - if (\in_array($this->_responseCode, $expected)) - { - $retval = true; - } - else - { - $retval = false; - } - } - else - { - if ($this->_responseCode == $expected) - { - $retval = true; - } - else - { - $retval = false; - } - } - - return $retval; - } - - /** - * Set server to passive mode and open a data port connection - * - * @return boolean True if successful - * - * @since 1.5 - */ - protected function _passive() - { - $match = array(); - $parts = array(); - $errno = null; - $err = null; - - // Make sure we have a connection to the server - if (!\is_resource($this->_conn)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Request a passive connection - this means, we'll talk to you, you don't talk to us. - @ fwrite($this->_conn, "PASV\r\n"); - - // Wait for a response from the server, but timeout after the set time limit - $endTime = time() + $this->_timeout; - $this->_response = ''; - - do - { - $this->_response .= fgets($this->_conn, 4096); - } - while (!preg_match('/^([0-9]{3})(-(.*' . CRLF . ')+\1)? [^' . CRLF . ']+' . CRLF . "$/", $this->_response, $parts) && time() < $endTime); - - // Catch a timeout or bad response - if (!isset($parts[1])) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TIMEOUT', __METHOD__, $this->_response), Log::WARNING, 'jerror'); - - return false; - } - - // Separate the code from the message - $this->_responseCode = $parts[1]; - $this->_responseMsg = $parts[0]; - - // If it's not 227, we weren't given an IP and port, which means it failed. - if ($this->_responseCode != '227') - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE_IP_OBTAIN', __METHOD__, $this->_responseMsg), Log::WARNING, 'jerror'); - - return false; - } - - // Snatch the IP and port information, or die horribly trying... - if (preg_match('~\((\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))\)~', $this->_responseMsg, $match) == 0) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE_IP_VALID', __METHOD__, $this->_responseMsg), Log::WARNING, 'jerror'); - - return false; - } - - // This is pretty simple - store it for later use ;). - $this->_pasv = array('ip' => $match[1] . '.' . $match[2] . '.' . $match[3] . '.' . $match[4], 'port' => $match[5] * 256 + $match[6]); - - // Connect, assuming we've got a connection. - $this->_dataconn = @fsockopen($this->_pasv['ip'], $this->_pasv['port'], $errno, $err, $this->_timeout); - - if (!$this->_dataconn) - { - Log::add( - Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__, $this->_pasv['ip'], $this->_pasv['port'], $errno, $err), - Log::WARNING, - 'jerror' - ); - - return false; - } - - // Set the timeout for this connection - socket_set_timeout($this->_conn, $this->_timeout, 0); - - return true; - } - - /** - * Method to find out the correct transfer mode for a specific file - * - * @param string $fileName Name of the file - * - * @return integer Transfer-mode for this filetype [FTP_ASCII|FTP_BINARY] - * - * @since 1.5 - */ - protected function _findMode($fileName) - { - if ($this->_type == FTP_AUTOASCII) - { - $dot = strrpos($fileName, '.') + 1; - $ext = substr($fileName, $dot); - - if (\in_array($ext, $this->_autoAscii)) - { - $mode = FTP_ASCII; - } - else - { - $mode = FTP_BINARY; - } - } - elseif ($this->_type == FTP_ASCII) - { - $mode = FTP_ASCII; - } - else - { - $mode = FTP_BINARY; - } - - return $mode; - } - - /** - * Set transfer mode - * - * @param integer $mode Integer representation of data transfer mode [1:Binary|0:Ascii] - * Defined constants can also be used [FTP_BINARY|FTP_ASCII] - * - * @return boolean True if successful - * - * @since 1.5 - */ - protected function _mode($mode) - { - if ($mode == FTP_BINARY) - { - if (!$this->_putCmd('TYPE I', 200)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_MODE_BINARY', __METHOD__, $this->_response), Log::WARNING, 'jerror'); - - return false; - } - } - else - { - if (!$this->_putCmd('TYPE A', 200)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_MODE_ASCII', __METHOD__, $this->_response), Log::WARNING, 'jerror'); - - return false; - } - } - - return true; - } + /** + * @var resource Socket resource + * @since 1.5 + */ + protected $_conn = null; + + /** + * @var resource Data port connection resource + * @since 1.5 + */ + protected $_dataconn = null; + + /** + * @var array Passive connection information + * @since 1.5 + */ + protected $_pasv = null; + + /** + * @var string Response Message + * @since 1.5 + */ + protected $_response = null; + + /** + * @var integer Timeout limit + * @since 1.5 + */ + protected $_timeout = 15; + + /** + * @var integer Transfer Type + * @since 1.5 + */ + protected $_type = null; + + /** + * @var array Array to hold ascii format file extensions + * @since 1.5 + */ + protected $_autoAscii = array( + 'asp', + 'bat', + 'c', + 'cpp', + 'csv', + 'h', + 'htm', + 'html', + 'shtml', + 'ini', + 'inc', + 'log', + 'php', + 'php3', + 'pl', + 'perl', + 'sh', + 'sql', + 'txt', + 'xhtml', + 'xml', + ); + + /** + * Array to hold native line ending characters + * + * @var array + * @since 1.5 + */ + protected $_lineEndings = array('UNIX' => "\n", 'WIN' => "\r\n"); + + /** + * @var array FtpClient instances container. + * @since 2.5 + */ + protected static $instances = array(); + + /** + * FtpClient object constructor + * + * @param array $options Associative array of options to set + * + * @since 1.5 + */ + public function __construct(array $options = array()) + { + // If default transfer type is not set, set it to autoascii detect + if (!isset($options['type'])) { + $options['type'] = FTP_BINARY; + } + + $this->setOptions($options); + + if (FTP_NATIVE) { + BufferStreamHandler::stream_register(); + } + } + + /** + * FtpClient object destructor + * + * Closes an existing connection, if we have one + * + * @since 1.5 + */ + public function __destruct() + { + if (\is_resource($this->_conn)) { + $this->quit(); + } + } + + /** + * Returns the global FTP connector object, only creating it + * if it doesn't already exist. + * + * You may optionally specify a username and password in the parameters. If you do so, + * you may not login() again with different credentials using the same object. + * If you do not use this option, you must quit() the current connection when you + * are done, to free it for use by others. + * + * @param string $host Host to connect to + * @param string $port Port to connect to + * @param array $options Array with any of these options: type=>[FTP_AUTOASCII|FTP_ASCII|FTP_BINARY], timeout=>(int) + * @param string $user Username to use for a connection + * @param string $pass Password to use for a connection + * + * @return FtpClient The FTP Client object. + * + * @since 1.5 + */ + public static function getInstance($host = '127.0.0.1', $port = '21', array $options = array(), $user = null, $pass = null) + { + $signature = $user . ':' . $pass . '@' . $host . ':' . $port; + + // Create a new instance, or set the options of an existing one + if (!isset(static::$instances[$signature]) || !\is_object(static::$instances[$signature])) { + static::$instances[$signature] = new static($options); + } else { + static::$instances[$signature]->setOptions($options); + } + + // Connect to the server, and login, if requested + if (!static::$instances[$signature]->isConnected()) { + $return = static::$instances[$signature]->connect($host, $port); + + if ($return && $user !== null && $pass !== null) { + static::$instances[$signature]->login($user, $pass); + } + } + + return static::$instances[$signature]; + } + + /** + * Set client options + * + * @param array $options Associative array of options to set + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function setOptions(array $options) + { + if (isset($options['type'])) { + $this->_type = $options['type']; + } + + if (isset($options['timeout'])) { + $this->_timeout = $options['timeout']; + } + + return true; + } + + /** + * Method to connect to a FTP server + * + * @param string $host Host to connect to [Default: 127.0.0.1] + * @param int $port Port to connect on [Default: port 21] + * + * @return boolean True if successful + * + * @since 3.0.0 + */ + public function connect($host = '127.0.0.1', $port = 21) + { + $errno = null; + $err = null; + + // If already connected, return + if (\is_resource($this->_conn)) { + return true; + } + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + $this->_conn = @ftp_connect($host, $port, $this->_timeout); + + if ($this->_conn === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__, $host, $port), Log::WARNING, 'jerror'); + + return false; + } + + // Set the timeout for this connection + ftp_set_option($this->_conn, FTP_TIMEOUT_SEC, $this->_timeout); + + return true; + } + + // Connect to the FTP server. + $this->_conn = @ fsockopen($host, $port, $errno, $err, $this->_timeout); + + if (!$this->_conn) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT_SOCKET', __METHOD__, $host, $port, $errno, $err), Log::WARNING, 'jerror'); + + return false; + } + + // Set the timeout for this connection + socket_set_timeout($this->_conn, $this->_timeout, 0); + + // Check for welcome response code + if (!$this->_verifyResponse(220)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 220), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to determine if the object is connected to an FTP server + * + * @return boolean True if connected + * + * @since 1.5 + */ + public function isConnected() + { + return \is_resource($this->_conn); + } + + /** + * Method to login to a server once connected + * + * @param string $user Username to login to the server + * @param string $pass Password to login to the server + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function login($user = 'anonymous', $pass = 'jftp@joomla.org') + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_login($this->_conn, $user, $pass) === false) { + Log::add('JFtp::login: Unable to login', Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send the username + if (!$this->_putCmd('USER ' . $user, array(331, 503))) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_USERNAME', __METHOD__, $this->_response, $user), Log::WARNING, 'jerror'); + + return false; + } + + // If we are already logged in, continue :) + if ($this->_responseCode == 503) { + return true; + } + + // Send the password + if (!$this->_putCmd('PASS ' . $pass, 230)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_PASSWORD', __METHOD__, $this->_response, str_repeat('*', \strlen($pass))), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to quit and close the connection + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function quit() + { + // If native FTP support is enabled lets use it... + if (FTP_NATIVE) { + @ftp_close($this->_conn); + + return true; + } + + // Logout and close connection + @fwrite($this->_conn, "QUIT\r\n"); + @fclose($this->_conn); + + return true; + } + + /** + * Method to retrieve the current working directory on the FTP server + * + * @return string Current working directory + * + * @since 1.5 + */ + public function pwd() + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (($ret = @ftp_pwd($this->_conn)) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return $ret; + } + + $match = array(null); + + // Send print working directory command and verify success + if (!$this->_putCmd('PWD', 257)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 257), Log::WARNING, 'jerror'); + + return false; + } + + // Match just the path + preg_match('/"[^"\r\n]*"/', $this->_response, $match); + + // Return the cleaned path + return preg_replace("/\"/", '', $match[0]); + } + + /** + * Method to system string from the FTP server + * + * @return string System identifier string + * + * @since 1.5 + */ + public function syst() + { + // If native FTP support is enabled lets use it... + if (FTP_NATIVE) { + if (($ret = @ftp_systype($this->_conn)) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + } else { + // Send print working directory command and verify success + if (!$this->_putCmd('SYST', 215)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 215), Log::WARNING, 'jerror'); + + return false; + } + + $ret = $this->_response; + } + + // Match the system string to an OS + if (strpos(strtoupper($ret), 'MAC') !== false) { + $ret = 'MAC'; + } elseif (strpos(strtoupper($ret), 'WIN') !== false) { + $ret = 'WIN'; + } else { + $ret = 'UNIX'; + } + + // Return the os type + return $ret; + } + + /** + * Method to change the current working directory on the FTP server + * + * @param string $path Path to change into on the server + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function chdir($path) + { + // If native FTP support is enabled lets use it... + if (FTP_NATIVE) { + if (@ftp_chdir($this->_conn, $path) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send change directory command and verify success + if (!$this->_putCmd('CWD ' . $path, 250)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 250, $path), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to reinitialise the server, ie. need to login again + * + * NOTE: This command not available on all servers + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function reinit() + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_site($this->_conn, 'REIN') === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send reinitialise command to the server + if (!$this->_putCmd('REIN', 220)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 220), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to rename a file/folder on the FTP server + * + * @param string $from Path to change file/folder from + * @param string $to Path to change file/folder to + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function rename($from, $to) + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_rename($this->_conn, $from, $to) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send rename from command to the server + if (!$this->_putCmd('RNFR ' . $from, 350)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RENAME_BAD_RESPONSE_FROM', __METHOD__, $this->_response, $from), Log::WARNING, 'jerror'); + + return false; + } + + // Send rename to command to the server + if (!$this->_putCmd('RNTO ' . $to, 250)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RENAME_BAD_RESPONSE_TO', __METHOD__, $this->_response, $to), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to change mode for a path on the FTP server + * + * @param string $path Path to change mode on + * @param mixed $mode Octal value to change mode to, e.g. '0777', 0777 or 511 (string or integer) + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function chmod($path, $mode) + { + // If no filename is given, we assume the current directory is the target + if ($path == '') { + $path = '.'; + } + + // Convert the mode to a string + if (\is_int($mode)) { + $mode = decoct($mode); + } + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_site($this->_conn, 'CHMOD ' . $mode . ' ' . $path) === false) { + if (!IS_WIN) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + } + + return false; + } + + return true; + } + + // Send change mode command and verify success [must convert mode from octal] + if (!$this->_putCmd('SITE CHMOD ' . $mode . ' ' . $path, array(200, 250))) { + if (!IS_WIN) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_CHMOD_BAD_RESPONSE', __METHOD__, $this->_response, $path, $mode), Log::WARNING, 'jerror'); + } + + return false; + } + + return true; + } + + /** + * Method to delete a path [file/folder] on the FTP server + * + * @param string $path Path to delete + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function delete($path) + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_delete($this->_conn, $path) === false) { + if (@ftp_rmdir($this->_conn, $path) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + } + + return true; + } + + // Send delete file command and if that doesn't work, try to remove a directory + if (!$this->_putCmd('DELE ' . $path, 250)) { + if (!$this->_putCmd('RMD ' . $path, 250)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 250, $path), Log::WARNING, 'jerror'); + + return false; + } + } + + return true; + } + + /** + * Method to create a directory on the FTP server + * + * @param string $path Directory to create + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function mkdir($path) + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_mkdir($this->_conn, $path) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send change directory command and verify success + if (!$this->_putCmd('MKD ' . $path, 257)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 257, $path), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to restart data transfer at a given byte + * + * @param integer $point Byte to restart transfer at + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function restart($point) + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_site($this->_conn, 'REST ' . $point) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send restart command and verify success + if (!$this->_putCmd('REST ' . $point, 350)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RESTART_BAD_RESPONSE', __METHOD__, $this->_response, $point), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to create an empty file on the FTP server + * + * @param string $path Path local file to store on the FTP server + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function create($path) + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $buffer = fopen('buffer://tmp', 'r'); + + if (@ftp_fput($this->_conn, $path, $buffer, FTP_ASCII) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + fclose($buffer); + + return false; + } + + fclose($buffer); + + return true; + } + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (!$this->_putCmd('STOR ' . $path, array(150, 125))) { + @ fclose($this->_dataconn); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + + return false; + } + + // To create a zero byte upload close the data port connection + fclose($this->_dataconn); + + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to read a file from the FTP server's contents into a buffer + * + * @param string $remote Path to remote file to read on the FTP server + * @param string &$buffer Buffer variable to read file contents into + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function read($remote, &$buffer) + { + // Determine file type + $mode = $this->_findMode($remote); + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $tmp = fopen('buffer://tmp', 'br+'); + + if (@ftp_fget($this->_conn, $tmp, $remote, $mode) === false) { + fclose($tmp); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Read tmp buffer contents + rewind($tmp); + $buffer = ''; + + while (!feof($tmp)) { + $buffer .= fread($tmp, 8192); + } + + fclose($tmp); + + return true; + } + + $this->_mode($mode); + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (!$this->_putCmd('RETR ' . $remote, array(150, 125))) { + @ fclose($this->_dataconn); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + // Read data from data port connection and add to the buffer + $buffer = ''; + + while (!feof($this->_dataconn)) { + $buffer .= fread($this->_dataconn, 4096); + } + + // Close the data port connection + fclose($this->_dataconn); + + // Let's try to cleanup some line endings if it is ascii + if ($mode == FTP_ASCII) { + $os = 'UNIX'; + + if (IS_WIN) { + $os = 'WIN'; + } + + $buffer = preg_replace('/' . CRLF . '/', $this->_lineEndings[$os], $buffer); + } + + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to get a file from the FTP server and save it to a local file + * + * @param string $local Local path to save remote file to + * @param string $remote Path to remote file to get on the FTP server + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function get($local, $remote) + { + // Determine file type + $mode = $this->_findMode($remote); + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (@ftp_get($this->_conn, $local, $remote, $mode) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + $this->_mode($mode); + + // Check to see if the local file can be opened for writing + $fp = fopen($local, 'wb'); + + if (!$fp) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_OPEN_WRITING', __METHOD__, $local), Log::WARNING, 'jerror'); + + return false; + } + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (!$this->_putCmd('RETR ' . $remote, array(150, 125))) { + @ fclose($this->_dataconn); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + // Read data from data port connection and add to the buffer + while (!feof($this->_dataconn)) { + $buffer = fread($this->_dataconn, 4096); + fwrite($fp, $buffer, 4096); + } + + // Close the data port connection and file pointer + fclose($this->_dataconn); + fclose($fp); + + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to store a file to the FTP server + * + * @param string $local Path to local file to store on the FTP server + * @param string $remote FTP path to file to create + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function store($local, $remote = null) + { + // If remote file is not given, use the filename of the local file in the current + // working directory. + if ($remote == null) { + $remote = basename($local); + } + + // Determine file type + $mode = $this->_findMode($remote); + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (@ftp_put($this->_conn, $remote, $local, $mode) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + $this->_mode($mode); + + // Check to see if the local file exists and if so open it for reading + if (@ file_exists($local)) { + $fp = fopen($local, 'rb'); + + if (!$fp) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_OPEN_READING', __METHOD__, $local), Log::WARNING, 'jerror'); + + return false; + } + } else { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_FIND', __METHOD__, $local), Log::WARNING, 'jerror'); + + return false; + } + + // Start passive mode + if (!$this->_passive()) { + @ fclose($fp); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Send store command to the FTP server + if (!$this->_putCmd('STOR ' . $remote, array(150, 125))) { + @ fclose($fp); + @ fclose($this->_dataconn); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + // Do actual file transfer, read local file and write to data port connection + while (!feof($fp)) { + $line = fread($fp, 4096); + + do { + if (($result = @ fwrite($this->_dataconn, $line)) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $line = substr($line, $result); + } while ($line != ''); + } + + fclose($fp); + fclose($this->_dataconn); + + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to write a string to the FTP server + * + * @param string $remote FTP path to file to write to + * @param string $buffer Contents to write to the FTP server + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function write($remote, $buffer) + { + // Determine file type + $mode = $this->_findMode($remote); + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $tmp = fopen('buffer://tmp', 'br+'); + fwrite($tmp, $buffer); + rewind($tmp); + + if (@ftp_fput($this->_conn, $remote, $tmp, $mode) === false) { + fclose($tmp); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + fclose($tmp); + + return true; + } + + // First we need to set the transfer mode + $this->_mode($mode); + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Send store command to the FTP server + if (!$this->_putCmd('STOR ' . $remote, array(150, 125))) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + @ fclose($this->_dataconn); + + return false; + } + + // Write buffer to the data connection port + do { + if (($result = @ fwrite($this->_dataconn, $buffer)) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $buffer = substr($buffer, $result); + } while ($buffer != ''); + + // Close the data connection port [Data transfer complete] + fclose($this->_dataconn); + + // Verify that the server received the transfer + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to append a string to the FTP server + * + * @param string $remote FTP path to file to append to + * @param string $buffer Contents to append to the FTP server + * + * @return boolean True if successful + * + * @since 3.6.0 + */ + public function append($remote, $buffer) + { + // Determine file type + $mode = $this->_findMode($remote); + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); + } + + $tmp = fopen('buffer://tmp', 'bw+'); + fwrite($tmp, $buffer); + rewind($tmp); + + $size = $this->size($remote); + + if ($size === false) { + } + + if (@ftp_fput($this->_conn, $remote, $tmp, $mode, $size) === false) { + fclose($tmp); + + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), 35); + } + + fclose($tmp); + + return true; + } + + // First we need to set the transfer mode + $this->_mode($mode); + + // Start passive mode + if (!$this->_passive()) { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); + } + + // Send store command to the FTP server + if (!$this->_putCmd('APPE ' . $remote, array(150, 125))) { + @fclose($this->_dataconn); + + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), 35); + } + + // Write buffer to the data connection port + do { + if (($result = @ fwrite($this->_dataconn, $buffer)) === false) { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), 37); + } + + $buffer = substr($buffer, $result); + } while ($buffer != ''); + + // Close the data connection port [Data transfer complete] + fclose($this->_dataconn); + + // Verify that the server received the transfer + if (!$this->_verifyResponse(226)) { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), 37); + } + + return true; + } + + /** + * Get the size of the remote file. + * + * @param string $remote FTP path to file whose size to get + * + * @return mixed number of bytes or false on error + * + * @since 3.6.0 + */ + public function size($remote) + { + if (FTP_NATIVE) { + $size = ftp_size($this->_conn, $remote); + + // In case ftp_size fails, try the SIZE command directly. + if ($size === -1) { + $response = ftp_raw($this->_conn, 'SIZE ' . $remote); + $responseCode = substr($response[0], 0, 3); + $responseMessage = substr($response[0], 4); + + if ($responseCode != '213') { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), 35); + } + + $size = (int) $responseMessage; + } + + return $size; + } + + // Start passive mode + if (!$this->_passive()) { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); + } + + // Send size command to the FTP server + if (!$this->_putCmd('SIZE ' . $remote, array(213))) { + @fclose($this->_dataconn); + + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 213, $remote), 35); + } + + return (int) substr($this->_responseMsg, 4); + } + + /** + * Method to list the filenames of the contents of a directory on the FTP server + * + * Note: Some servers also return folder names. However, to be sure to list folders on all + * servers, you should use listDetails() instead if you also need to deal with folders + * + * @param string $path Path local file to store on the FTP server + * + * @return string Directory listing + * + * @since 1.5 + */ + public function listNames($path = null) + { + $data = null; + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (($list = @ftp_nlist($this->_conn, $path)) === false) { + // Workaround for empty directories on some servers + if ($this->listDetails($path, 'files') === array()) { + return array(); + } + + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $list = preg_replace('#^' . preg_quote($path, '#') . '[/\\\\]?#', '', $list); + + if ($keys = array_merge(array_keys($list, '.'), array_keys($list, '..'))) { + foreach ($keys as $key) { + unset($list[$key]); + } + } + + return $list; + } + + // If a path exists, prepend a space + if ($path != null) { + $path = ' ' . $path; + } + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (!$this->_putCmd('NLST' . $path, array(150, 125))) { + @ fclose($this->_dataconn); + + // Workaround for empty directories on some servers + if ($this->listDetails($path, 'files') === array()) { + return array(); + } + + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + + return false; + } + + // Read in the file listing. + while (!feof($this->_dataconn)) { + $data .= fread($this->_dataconn, 4096); + } + + fclose($this->_dataconn); + + // Everything go okay? + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + + return false; + } + + $data = preg_split('/[' . CRLF . ']+/', $data, -1, PREG_SPLIT_NO_EMPTY); + $data = preg_replace('#^' . preg_quote(substr($path, 1), '#') . '[/\\\\]?#', '', $data); + + if ($keys = array_merge(array_keys($data, '.'), array_keys($data, '..'))) { + foreach ($keys as $key) { + unset($data[$key]); + } + } + + return $data; + } + + /** + * Method to list the contents of a directory on the FTP server + * + * @param string $path Path to the local file to be stored on the FTP server + * @param string $type Return type [raw|all|folders|files] + * + * @return mixed If $type is raw: string Directory listing, otherwise array of string with file-names + * + * @since 1.5 + */ + public function listDetails($path = null, $type = 'all') + { + $dir_list = array(); + $data = null; + $regs = null; + + // @todo: Deal with recurse -- nightmare + // For now we will just set it to false + $recurse = false; + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (($contents = @ftp_rawlist($this->_conn, $path)) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + } else { + // Non Native mode + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // If a path exists, prepend a space + if ($path != null) { + $path = ' ' . $path; + } + + // Request the file listing + if (!$this->_putCmd(($recurse == true) ? 'LIST -R' : 'LIST' . $path, array(150, 125))) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + @ fclose($this->_dataconn); + + return false; + } + + // Read in the file listing. + while (!feof($this->_dataconn)) { + $data .= fread($this->_dataconn, 4096); + } + + fclose($this->_dataconn); + + // Everything go okay? + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + + return false; + } + + $contents = explode(CRLF, $data); + } + + // If only raw output is requested we are done + if ($type === 'raw') { + return $data; + } + + // If we received the listing of an empty directory, we are done as well + if (empty($contents[0])) { + return $dir_list; + } + + // If the server returned the number of results in the first response, let's dump it + if (strtolower(substr($contents[0], 0, 6)) === 'total ') { + array_shift($contents); + + if (!isset($contents[0]) || empty($contents[0])) { + return $dir_list; + } + } + + // Regular expressions for the directory listing parsing. + $regexps = array( + 'UNIX' => '#([-dl][rwxstST-]+).* ([0-9]*) ([a-zA-Z0-9]+).* ([a-zA-Z0-9]+).* ([0-9]*)' + . ' ([a-zA-Z]+[0-9: ]*[0-9])[ ]+(([0-9]{1,2}:[0-9]{2})|[0-9]{4}) (.+)#', + 'MAC' => '#([-dl][rwxstST-]+).* ?([0-9 ]*)?([a-zA-Z0-9]+).* ([a-zA-Z0-9]+).* ([0-9]*)' + . ' ([a-zA-Z]+[0-9: ]*[0-9])[ ]+(([0-9]{2}:[0-9]{2})|[0-9]{4}) (.+)#', + 'WIN' => '#([0-9]{2})-([0-9]{2})-([0-9]{2}) +([0-9]{2}):([0-9]{2})(AM|PM) +([0-9]+|) +(.+)#', + ); + + // Find out the format of the directory listing by matching one of the regexps + $osType = null; + + foreach ($regexps as $k => $v) { + if (@preg_match($v, $contents[0])) { + $osType = $k; + $regexp = $v; + break; + } + } + + if (!$osType) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_UNRECOGNISED_FOLDER_LISTING_FORMATJLIB_CLIENT_ERROR_JFTP_LISTDETAILS_UNRECOGNISED', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Here is where it is going to get dirty.... + if ($osType === 'UNIX' || $osType === 'MAC') { + foreach ($contents as $file) { + $tmp_array = null; + + if (@preg_match($regexp, $file, $regs)) { + $fType = (int) strpos('-dl', $regs[1][0]); + + // $tmp_array['line'] = $regs[0]; + $tmp_array['type'] = $fType; + $tmp_array['rights'] = $regs[1]; + + // $tmp_array['number'] = $regs[2]; + $tmp_array['user'] = $regs[3]; + $tmp_array['group'] = $regs[4]; + $tmp_array['size'] = $regs[5]; + $tmp_array['date'] = @date('m-d', strtotime($regs[6])); + $tmp_array['time'] = $regs[7]; + $tmp_array['name'] = $regs[9]; + } + + // If we just want files, do not add a folder + if ($type === 'files' && $tmp_array['type'] == 1) { + continue; + } + + // If we just want folders, do not add a file + if ($type === 'folders' && $tmp_array['type'] == 0) { + continue; + } + + if (\is_array($tmp_array) && $tmp_array['name'] != '.' && $tmp_array['name'] != '..') { + $dir_list[] = $tmp_array; + } + } + } else { + foreach ($contents as $file) { + $tmp_array = null; + + if (@preg_match($regexp, $file, $regs)) { + $fType = (int) ($regs[7] === ''); + $timestamp = strtotime("$regs[3]-$regs[1]-$regs[2] $regs[4]:$regs[5]$regs[6]"); + + // $tmp_array['line'] = $regs[0]; + $tmp_array['type'] = $fType; + $tmp_array['rights'] = ''; + + // $tmp_array['number'] = 0; + $tmp_array['user'] = ''; + $tmp_array['group'] = ''; + $tmp_array['size'] = (int) $regs[7]; + $tmp_array['date'] = date('m-d', $timestamp); + $tmp_array['time'] = date('H:i', $timestamp); + $tmp_array['name'] = $regs[8]; + } + + // If we just want files, do not add a folder + if ($type === 'files' && $tmp_array['type'] == 1) { + continue; + } + + // If we just want folders, do not add a file + if ($type === 'folders' && $tmp_array['type'] == 0) { + continue; + } + + if (\is_array($tmp_array) && $tmp_array['name'] != '.' && $tmp_array['name'] != '..') { + $dir_list[] = $tmp_array; + } + } + } + + return $dir_list; + } + + /** + * Send command to the FTP server and validate an expected response code + * + * @param string $cmd Command to send to the FTP server + * @param mixed $expectedResponse Integer response code or array of integer response codes + * + * @return boolean True if command executed successfully + * + * @since 1.5 + */ + protected function _putCmd($cmd, $expectedResponse) + { + // Make sure we have a connection to the server + if (!\is_resource($this->_conn)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PUTCMD_UNCONNECTED', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Send the command to the server + if (!fwrite($this->_conn, $cmd . "\r\n")) { + Log::add(Text::sprintf('DDD', Text::sprintf('JLIB_CLIENT_ERROR_FTP_PUTCMD_SEND', __METHOD__, $cmd)), Log::WARNING, 'jerror'); + } + + return $this->_verifyResponse($expectedResponse); + } + + /** + * Verify the response code from the server and log response if flag is set + * + * @param mixed $expected Integer response code or array of integer response codes + * + * @return boolean True if response code from the server is expected + * + * @since 1.5 + */ + protected function _verifyResponse($expected) + { + $parts = null; + + // Wait for a response from the server, but timeout after the set time limit + $endTime = time() + $this->_timeout; + $this->_response = ''; + + do { + $this->_response .= fgets($this->_conn, 4096); + } while (!preg_match('/^([0-9]{3})(-(.*' . CRLF . ')+\1)? [^' . CRLF . ']+' . CRLF . "$/", $this->_response, $parts) && time() < $endTime); + + // Catch a timeout or bad response + if (!isset($parts[1])) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TIMEOUT', __METHOD__, $this->_response), Log::WARNING, 'jerror'); + + return false; + } + + // Separate the code from the message + $this->_responseCode = $parts[1]; + $this->_responseMsg = $parts[0]; + + // Did the server respond with the code we wanted? + if (\is_array($expected)) { + if (\in_array($this->_responseCode, $expected)) { + $retval = true; + } else { + $retval = false; + } + } else { + if ($this->_responseCode == $expected) { + $retval = true; + } else { + $retval = false; + } + } + + return $retval; + } + + /** + * Set server to passive mode and open a data port connection + * + * @return boolean True if successful + * + * @since 1.5 + */ + protected function _passive() + { + $match = array(); + $parts = array(); + $errno = null; + $err = null; + + // Make sure we have a connection to the server + if (!\is_resource($this->_conn)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Request a passive connection - this means, we'll talk to you, you don't talk to us. + @ fwrite($this->_conn, "PASV\r\n"); + + // Wait for a response from the server, but timeout after the set time limit + $endTime = time() + $this->_timeout; + $this->_response = ''; + + do { + $this->_response .= fgets($this->_conn, 4096); + } while (!preg_match('/^([0-9]{3})(-(.*' . CRLF . ')+\1)? [^' . CRLF . ']+' . CRLF . "$/", $this->_response, $parts) && time() < $endTime); + + // Catch a timeout or bad response + if (!isset($parts[1])) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TIMEOUT', __METHOD__, $this->_response), Log::WARNING, 'jerror'); + + return false; + } + + // Separate the code from the message + $this->_responseCode = $parts[1]; + $this->_responseMsg = $parts[0]; + + // If it's not 227, we weren't given an IP and port, which means it failed. + if ($this->_responseCode != '227') { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE_IP_OBTAIN', __METHOD__, $this->_responseMsg), Log::WARNING, 'jerror'); + + return false; + } + + // Snatch the IP and port information, or die horribly trying... + if (preg_match('~\((\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))\)~', $this->_responseMsg, $match) == 0) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE_IP_VALID', __METHOD__, $this->_responseMsg), Log::WARNING, 'jerror'); + + return false; + } + + // This is pretty simple - store it for later use ;). + $this->_pasv = array('ip' => $match[1] . '.' . $match[2] . '.' . $match[3] . '.' . $match[4], 'port' => $match[5] * 256 + $match[6]); + + // Connect, assuming we've got a connection. + $this->_dataconn = @fsockopen($this->_pasv['ip'], $this->_pasv['port'], $errno, $err, $this->_timeout); + + if (!$this->_dataconn) { + Log::add( + Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__, $this->_pasv['ip'], $this->_pasv['port'], $errno, $err), + Log::WARNING, + 'jerror' + ); + + return false; + } + + // Set the timeout for this connection + socket_set_timeout($this->_conn, $this->_timeout, 0); + + return true; + } + + /** + * Method to find out the correct transfer mode for a specific file + * + * @param string $fileName Name of the file + * + * @return integer Transfer-mode for this filetype [FTP_ASCII|FTP_BINARY] + * + * @since 1.5 + */ + protected function _findMode($fileName) + { + if ($this->_type == FTP_AUTOASCII) { + $dot = strrpos($fileName, '.') + 1; + $ext = substr($fileName, $dot); + + if (\in_array($ext, $this->_autoAscii)) { + $mode = FTP_ASCII; + } else { + $mode = FTP_BINARY; + } + } elseif ($this->_type == FTP_ASCII) { + $mode = FTP_ASCII; + } else { + $mode = FTP_BINARY; + } + + return $mode; + } + + /** + * Set transfer mode + * + * @param integer $mode Integer representation of data transfer mode [1:Binary|0:Ascii] + * Defined constants can also be used [FTP_BINARY|FTP_ASCII] + * + * @return boolean True if successful + * + * @since 1.5 + */ + protected function _mode($mode) + { + if ($mode == FTP_BINARY) { + if (!$this->_putCmd('TYPE I', 200)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_MODE_BINARY', __METHOD__, $this->_response), Log::WARNING, 'jerror'); + + return false; + } + } else { + if (!$this->_putCmd('TYPE A', 200)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_MODE_ASCII', __METHOD__, $this->_response), Log::WARNING, 'jerror'); + + return false; + } + } + + return true; + } } diff --git a/libraries/src/Component/ComponentHelper.php b/libraries/src/Component/ComponentHelper.php index 9d8fcec6301b3..41f200864389a 100644 --- a/libraries/src/Component/ComponentHelper.php +++ b/libraries/src/Component/ComponentHelper.php @@ -1,4 +1,5 @@ enabled = $strict ? false : true; - $result->setParams(new Registry); - - return $result; - } - - /** - * Checks if the component is enabled - * - * @param string $option The component option. - * - * @return boolean - * - * @since 1.5 - */ - public static function isEnabled($option) - { - $components = static::getComponents(); - - return isset($components[$option]) && $components[$option]->enabled; - } - - /** - * Checks if a component is installed - * - * @param string $option The component option. - * - * @return integer - * - * @since 3.4 - */ - public static function isInstalled($option) - { - $components = static::getComponents(); - - return isset($components[$option]) ? 1 : 0; - } - - /** - * Gets the parameter object for the component - * - * @param string $option The option for the component. - * @param boolean $strict If set and the component does not exist, false will be returned - * - * @return Registry A Registry object. - * - * @see Registry - * @since 1.5 - */ - public static function getParams($option, $strict = false) - { - return static::getComponent($option, $strict)->getParams(); - } - - /** - * Applies the global text filters to arbitrary text as per settings for current user groups - * - * @param string $text The string to filter - * - * @return string The filtered string - * - * @since 2.5 - */ - public static function filterText($text) - { - // Punyencoding utf8 email addresses - $text = InputFilter::getInstance()->emailToPunycode($text); - - // Filter settings - $config = static::getParams('com_config'); - $user = Factory::getUser(); - $userGroups = Access::getGroupsByUser($user->get('id')); - - $filters = $config->get('filters'); - - $forbiddenListTags = array(); - $forbiddenListAttributes = array(); - - $customListTags = array(); - $customListAttributes = array(); - - $allowedListTags = array(); - $allowedListAttributes = array(); - - $allowedList = false; - $forbiddenList = false; - $customList = false; - $unfiltered = false; - - // Cycle through each of the user groups the user is in. - // Remember they are included in the Public group as well. - foreach ($userGroups as $groupId) - { - // May have added a group by not saved the filters. - if (!isset($filters->$groupId)) - { - continue; - } - - // Each group the user is in could have different filtering properties. - $filterData = $filters->$groupId; - $filterType = strtoupper($filterData->filter_type); - - if ($filterType === 'NH') - { - // Maximum HTML filtering. - } - elseif ($filterType === 'NONE') - { - // No HTML filtering. - $unfiltered = true; - } - else - { - // Forbidden list or allowed list. - // Preprocess the tags and attributes. - $tags = explode(',', $filterData->filter_tags); - $attributes = explode(',', $filterData->filter_attributes); - $tempTags = array(); - $tempAttributes = array(); - - foreach ($tags as $tag) - { - $tag = trim($tag); - - if ($tag) - { - $tempTags[] = $tag; - } - } - - foreach ($attributes as $attribute) - { - $attribute = trim($attribute); - - if ($attribute) - { - $tempAttributes[] = $attribute; - } - } - - // Collect the forbidden list or allowed list tags and attributes. - // Each list is cumulative. - if ($filterType === 'BL') - { - $forbiddenList = true; - $forbiddenListTags = array_merge($forbiddenListTags, $tempTags); - $forbiddenListAttributes = array_merge($forbiddenListAttributes, $tempAttributes); - } - elseif ($filterType === 'CBL') - { - // Only set to true if Tags or Attributes were added - if ($tempTags || $tempAttributes) - { - $customList = true; - $customListTags = array_merge($customListTags, $tempTags); - $customListAttributes = array_merge($customListAttributes, $tempAttributes); - } - } - elseif ($filterType === 'WL') - { - $allowedList = true; - $allowedListTags = array_merge($allowedListTags, $tempTags); - $allowedListAttributes = array_merge($allowedListAttributes, $tempAttributes); - } - } - } - - // Remove duplicates before processing (because the forbidden list uses both sets of arrays). - $forbiddenListTags = array_unique($forbiddenListTags); - $forbiddenListAttributes = array_unique($forbiddenListAttributes); - $customListTags = array_unique($customListTags); - $customListAttributes = array_unique($customListAttributes); - $allowedListTags = array_unique($allowedListTags); - $allowedListAttributes = array_unique($allowedListAttributes); - - if (!$unfiltered) - { - // Custom Forbidden list precedes Default forbidden list. - if ($customList) - { - $filter = InputFilter::getInstance(array(), array(), 1, 1); - - // Override filter's default forbidden tags and attributes - if ($customListTags) - { - $filter->blockedTags = $customListTags; - } - - if ($customListAttributes) - { - $filter->blockedAttributes = $customListAttributes; - } - } - // Forbidden list takes second precedence. - elseif ($forbiddenList) - { - // Remove the allowed tags and attributes from the forbidden list. - $forbiddenListTags = array_diff($forbiddenListTags, $allowedListTags); - $forbiddenListAttributes = array_diff($forbiddenListAttributes, $allowedListAttributes); - - $filter = InputFilter::getInstance( - $forbiddenListTags, - $forbiddenListAttributes, - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ); - - // Remove the allowed tags from filter's default forbidden list. - if ($allowedListTags) - { - $filter->blockedTags = array_diff($filter->blockedTags, $allowedListTags); - } - - // Remove the allowed attributes from filter's default forbidden list. - if ($allowedListAttributes) - { - $filter->blockedAttributes = array_diff($filter->blockedAttributes, $allowedListAttributes); - } - } - // Allowed lists take third precedence. - elseif ($allowedList) - { - // Turn off XSS auto clean - $filter = InputFilter::getInstance($allowedListTags, $allowedListAttributes, 0, 0, 0); - } - // No HTML takes last place. - else - { - $filter = InputFilter::getInstance(); - } - - $text = $filter->clean($text, 'html'); - } - - return $text; - } - - /** - * Render the component. - * - * @param string $option The component option. - * @param array $params The component parameters - * - * @return string - * - * @since 1.5 - * @throws MissingComponentException - */ - public static function renderComponent($option, $params = array()) - { - $app = Factory::getApplication(); - $lang = Factory::getLanguage(); - - if (!$app->isClient('api')) - { - // Load template language files. - $template = $app->getTemplate(true)->template; - $lang->load('tpl_' . $template, JPATH_BASE) - || $lang->load('tpl_' . $template, JPATH_THEMES . "/$template"); - } - - if (empty($option)) - { - throw new MissingComponentException(Text::_('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND'), 404); - } - - if (JDEBUG) - { - Profiler::getInstance('Application')->mark('beforeRenderComponent ' . $option); - } - - // Record the scope - $scope = $app->scope; - - // Set scope to component name - $app->scope = $option; - - // Build the component path. - $option = preg_replace('/[^A-Z0-9_\.-]/i', '', $option); - - // Define component path. - - if (!\defined('JPATH_COMPONENT')) - { - /** - * Defines the path to the active component for the request - * - * Note this constant is application aware and is different for each application (site/admin). - * - * @var string - * @since 1.5 - * @deprecated 5.0 without replacement - */ - \define('JPATH_COMPONENT', JPATH_BASE . '/components/' . $option); - } - - if (!\defined('JPATH_COMPONENT_SITE')) - { - /** - * Defines the path to the site element of the active component for the request - * - * @var string - * @since 1.5 - * @deprecated 5.0 without replacement - */ - \define('JPATH_COMPONENT_SITE', JPATH_SITE . '/components/' . $option); - } - - if (!\defined('JPATH_COMPONENT_ADMINISTRATOR')) - { - /** - * Defines the path to the admin element of the active component for the request - * - * @var string - * @since 1.5 - * @deprecated 5.0 without replacement - */ - \define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR . '/components/' . $option); - } - - // If component is disabled throw error - if (!static::isEnabled($option)) - { - throw new MissingComponentException(Text::_('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND'), 404); - } - - ob_start(); - $app->bootComponent($option)->getDispatcher($app)->dispatch(); - $contents = ob_get_clean(); - - // Revert the scope - $app->scope = $scope; - - if (JDEBUG) - { - Profiler::getInstance('Application')->mark('afterRenderComponent ' . $option); - } - - return $contents; - } - - /** - * Load the installed components into the components property. - * - * @return boolean True on success - * - * @since 3.2 - */ - protected static function load() - { - $loader = function () - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName(['extension_id', 'element', 'params', 'enabled'], ['id', 'option', null, null])) - ->from($db->quoteName('#__extensions')) - ->where( - [ - $db->quoteName('type') . ' = ' . $db->quote('component'), - $db->quoteName('state') . ' = 0', - $db->quoteName('enabled') . ' = 1', - ] - ); - - $components = []; - $db->setQuery($query); - - foreach ($db->getIterator() as $component) - { - $components[$component->option] = new ComponentRecord((array) $component); - } - - return $components; - }; - - /** @var CallbackController $cache */ - $cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController('callback', ['defaultgroup' => '_system']); - - try - { - static::$components = $cache->get($loader, array(), __METHOD__); - } - catch (CacheExceptionInterface $e) - { - static::$components = $loader(); - } - - return true; - } - - /** - * Get installed components - * - * @return ComponentRecord[] The components property - * - * @since 3.6.3 - */ - public static function getComponents() - { - if (empty(static::$components)) - { - static::load(); - } - - return static::$components; - } - - /** - * Returns the component name (eg. com_content) for the given object based on the class name. - * If the object is not namespaced, then the alternative name is used. - * - * @param object $object The object controller or model - * @param string $alternativeName Mostly the value of getName() from the object - * - * @return string The name - * - * @since 4.0.0 - */ - public static function getComponentName($object, string $alternativeName): string - { - $reflect = new \ReflectionClass($object); - - if (!$reflect->getNamespaceName() || \get_class($object) === ComponentDispatcher::class || \get_class($object) === ApiDispatcher::class) - { - return 'com_' . strtolower($alternativeName); - } - - $from = strpos($reflect->getNamespaceName(), '\\Component'); - $to = strpos(substr($reflect->getNamespaceName(), $from + 11), '\\'); - - return 'com_' . strtolower(substr($reflect->getNamespaceName(), $from + 11, $to)); - } + /** + * The component list cache + * + * @var ComponentRecord[] + * @since 1.6 + */ + protected static $components = array(); + + /** + * Get the component information. + * + * @param string $option The component option. + * @param boolean $strict If set and the component does not exist, the enabled attribute will be set to false. + * + * @return ComponentRecord An object with the information for the component. + * + * @since 1.5 + */ + public static function getComponent($option, $strict = false) + { + $components = static::getComponents(); + + if (isset($components[$option])) { + return $components[$option]; + } + + $result = new ComponentRecord(); + $result->enabled = $strict ? false : true; + $result->setParams(new Registry()); + + return $result; + } + + /** + * Checks if the component is enabled + * + * @param string $option The component option. + * + * @return boolean + * + * @since 1.5 + */ + public static function isEnabled($option) + { + $components = static::getComponents(); + + return isset($components[$option]) && $components[$option]->enabled; + } + + /** + * Checks if a component is installed + * + * @param string $option The component option. + * + * @return integer + * + * @since 3.4 + */ + public static function isInstalled($option) + { + $components = static::getComponents(); + + return isset($components[$option]) ? 1 : 0; + } + + /** + * Gets the parameter object for the component + * + * @param string $option The option for the component. + * @param boolean $strict If set and the component does not exist, false will be returned + * + * @return Registry A Registry object. + * + * @see Registry + * @since 1.5 + */ + public static function getParams($option, $strict = false) + { + return static::getComponent($option, $strict)->getParams(); + } + + /** + * Applies the global text filters to arbitrary text as per settings for current user groups + * + * @param string $text The string to filter + * + * @return string The filtered string + * + * @since 2.5 + */ + public static function filterText($text) + { + // Punyencoding utf8 email addresses + $text = InputFilter::getInstance()->emailToPunycode($text); + + // Filter settings + $config = static::getParams('com_config'); + $user = Factory::getUser(); + $userGroups = Access::getGroupsByUser($user->get('id')); + + $filters = $config->get('filters'); + + $forbiddenListTags = array(); + $forbiddenListAttributes = array(); + + $customListTags = array(); + $customListAttributes = array(); + + $allowedListTags = array(); + $allowedListAttributes = array(); + + $allowedList = false; + $forbiddenList = false; + $customList = false; + $unfiltered = false; + + // Cycle through each of the user groups the user is in. + // Remember they are included in the Public group as well. + foreach ($userGroups as $groupId) { + // May have added a group by not saved the filters. + if (!isset($filters->$groupId)) { + continue; + } + + // Each group the user is in could have different filtering properties. + $filterData = $filters->$groupId; + $filterType = strtoupper($filterData->filter_type); + + if ($filterType === 'NH') { + // Maximum HTML filtering. + } elseif ($filterType === 'NONE') { + // No HTML filtering. + $unfiltered = true; + } else { + // Forbidden list or allowed list. + // Preprocess the tags and attributes. + $tags = explode(',', $filterData->filter_tags); + $attributes = explode(',', $filterData->filter_attributes); + $tempTags = array(); + $tempAttributes = array(); + + foreach ($tags as $tag) { + $tag = trim($tag); + + if ($tag) { + $tempTags[] = $tag; + } + } + + foreach ($attributes as $attribute) { + $attribute = trim($attribute); + + if ($attribute) { + $tempAttributes[] = $attribute; + } + } + + // Collect the forbidden list or allowed list tags and attributes. + // Each list is cumulative. + if ($filterType === 'BL') { + $forbiddenList = true; + $forbiddenListTags = array_merge($forbiddenListTags, $tempTags); + $forbiddenListAttributes = array_merge($forbiddenListAttributes, $tempAttributes); + } elseif ($filterType === 'CBL') { + // Only set to true if Tags or Attributes were added + if ($tempTags || $tempAttributes) { + $customList = true; + $customListTags = array_merge($customListTags, $tempTags); + $customListAttributes = array_merge($customListAttributes, $tempAttributes); + } + } elseif ($filterType === 'WL') { + $allowedList = true; + $allowedListTags = array_merge($allowedListTags, $tempTags); + $allowedListAttributes = array_merge($allowedListAttributes, $tempAttributes); + } + } + } + + // Remove duplicates before processing (because the forbidden list uses both sets of arrays). + $forbiddenListTags = array_unique($forbiddenListTags); + $forbiddenListAttributes = array_unique($forbiddenListAttributes); + $customListTags = array_unique($customListTags); + $customListAttributes = array_unique($customListAttributes); + $allowedListTags = array_unique($allowedListTags); + $allowedListAttributes = array_unique($allowedListAttributes); + + if (!$unfiltered) { + // Custom Forbidden list precedes Default forbidden list. + if ($customList) { + $filter = InputFilter::getInstance(array(), array(), 1, 1); + + // Override filter's default forbidden tags and attributes + if ($customListTags) { + $filter->blockedTags = $customListTags; + } + + if ($customListAttributes) { + $filter->blockedAttributes = $customListAttributes; + } + } + // Forbidden list takes second precedence. + elseif ($forbiddenList) { + // Remove the allowed tags and attributes from the forbidden list. + $forbiddenListTags = array_diff($forbiddenListTags, $allowedListTags); + $forbiddenListAttributes = array_diff($forbiddenListAttributes, $allowedListAttributes); + + $filter = InputFilter::getInstance( + $forbiddenListTags, + $forbiddenListAttributes, + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ); + + // Remove the allowed tags from filter's default forbidden list. + if ($allowedListTags) { + $filter->blockedTags = array_diff($filter->blockedTags, $allowedListTags); + } + + // Remove the allowed attributes from filter's default forbidden list. + if ($allowedListAttributes) { + $filter->blockedAttributes = array_diff($filter->blockedAttributes, $allowedListAttributes); + } + } + // Allowed lists take third precedence. + elseif ($allowedList) { + // Turn off XSS auto clean + $filter = InputFilter::getInstance($allowedListTags, $allowedListAttributes, 0, 0, 0); + } + // No HTML takes last place. + else { + $filter = InputFilter::getInstance(); + } + + $text = $filter->clean($text, 'html'); + } + + return $text; + } + + /** + * Render the component. + * + * @param string $option The component option. + * @param array $params The component parameters + * + * @return string + * + * @since 1.5 + * @throws MissingComponentException + */ + public static function renderComponent($option, $params = array()) + { + $app = Factory::getApplication(); + $lang = Factory::getLanguage(); + + if (!$app->isClient('api')) { + // Load template language files. + $template = $app->getTemplate(true)->template; + $lang->load('tpl_' . $template, JPATH_BASE) + || $lang->load('tpl_' . $template, JPATH_THEMES . "/$template"); + } + + if (empty($option)) { + throw new MissingComponentException(Text::_('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND'), 404); + } + + if (JDEBUG) { + Profiler::getInstance('Application')->mark('beforeRenderComponent ' . $option); + } + + // Record the scope + $scope = $app->scope; + + // Set scope to component name + $app->scope = $option; + + // Build the component path. + $option = preg_replace('/[^A-Z0-9_\.-]/i', '', $option); + + // Define component path. + + if (!\defined('JPATH_COMPONENT')) { + /** + * Defines the path to the active component for the request + * + * Note this constant is application aware and is different for each application (site/admin). + * + * @var string + * @since 1.5 + * @deprecated 5.0 without replacement + */ + \define('JPATH_COMPONENT', JPATH_BASE . '/components/' . $option); + } + + if (!\defined('JPATH_COMPONENT_SITE')) { + /** + * Defines the path to the site element of the active component for the request + * + * @var string + * @since 1.5 + * @deprecated 5.0 without replacement + */ + \define('JPATH_COMPONENT_SITE', JPATH_SITE . '/components/' . $option); + } + + if (!\defined('JPATH_COMPONENT_ADMINISTRATOR')) { + /** + * Defines the path to the admin element of the active component for the request + * + * @var string + * @since 1.5 + * @deprecated 5.0 without replacement + */ + \define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR . '/components/' . $option); + } + + // If component is disabled throw error + if (!static::isEnabled($option)) { + throw new MissingComponentException(Text::_('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND'), 404); + } + + ob_start(); + $app->bootComponent($option)->getDispatcher($app)->dispatch(); + $contents = ob_get_clean(); + + // Revert the scope + $app->scope = $scope; + + if (JDEBUG) { + Profiler::getInstance('Application')->mark('afterRenderComponent ' . $option); + } + + return $contents; + } + + /** + * Load the installed components into the components property. + * + * @return boolean True on success + * + * @since 3.2 + */ + protected static function load() + { + $loader = function () { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName(['extension_id', 'element', 'params', 'enabled'], ['id', 'option', null, null])) + ->from($db->quoteName('#__extensions')) + ->where( + [ + $db->quoteName('type') . ' = ' . $db->quote('component'), + $db->quoteName('state') . ' = 0', + $db->quoteName('enabled') . ' = 1', + ] + ); + + $components = []; + $db->setQuery($query); + + foreach ($db->getIterator() as $component) { + $components[$component->option] = new ComponentRecord((array) $component); + } + + return $components; + }; + + /** @var CallbackController $cache */ + $cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController('callback', ['defaultgroup' => '_system']); + + try { + static::$components = $cache->get($loader, array(), __METHOD__); + } catch (CacheExceptionInterface $e) { + static::$components = $loader(); + } + + return true; + } + + /** + * Get installed components + * + * @return ComponentRecord[] The components property + * + * @since 3.6.3 + */ + public static function getComponents() + { + if (empty(static::$components)) { + static::load(); + } + + return static::$components; + } + + /** + * Returns the component name (eg. com_content) for the given object based on the class name. + * If the object is not namespaced, then the alternative name is used. + * + * @param object $object The object controller or model + * @param string $alternativeName Mostly the value of getName() from the object + * + * @return string The name + * + * @since 4.0.0 + */ + public static function getComponentName($object, string $alternativeName): string + { + $reflect = new \ReflectionClass($object); + + if (!$reflect->getNamespaceName() || \get_class($object) === ComponentDispatcher::class || \get_class($object) === ApiDispatcher::class) { + return 'com_' . strtolower($alternativeName); + } + + $from = strpos($reflect->getNamespaceName(), '\\Component'); + $to = strpos(substr($reflect->getNamespaceName(), $from + 11), '\\'); + + return 'com_' . strtolower(substr($reflect->getNamespaceName(), $from + 11, $to)); + } } diff --git a/libraries/src/Component/ComponentRecord.php b/libraries/src/Component/ComponentRecord.php index b4389e00a015f..6b132cf66f31f 100644 --- a/libraries/src/Component/ComponentRecord.php +++ b/libraries/src/Component/ComponentRecord.php @@ -1,4 +1,5 @@ $value) - { - $this->$key = $value; - } - } - - /** - * Method to get certain otherwise inaccessible properties from the form field object. - * - * @param string $name The property name for which to get the value. - * - * @return mixed The property value or null. - * - * @since 3.7.0 - * @deprecated 5.0 Access the item parameters through the `getParams()` method - */ - public function __get($name) - { - if ($name === 'params') - { - return $this->getParams(); - } - - return $this->$name; - } - - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 3.7.0 - * @deprecated 5.0 Set the item parameters through the `setParams()` method - */ - public function __set($name, $value) - { - if ($name === 'params') - { - $this->setParams($value); - - return; - } - - $this->$name = $value; - } - - /** - * Returns the menu item parameters - * - * @return Registry - * - * @since 3.7.0 - */ - public function getParams() - { - if (!($this->params instanceof Registry)) - { - $this->params = new Registry($this->params); - } - - return $this->params; - } - - /** - * Sets the menu item parameters - * - * @param Registry|string $params The data to be stored as the parameters - * - * @return void - * - * @since 3.7.0 - */ - public function setParams($params) - { - $this->params = $params; - } + /** + * Primary key + * + * @var integer + * @since 3.7.0 + */ + public $id; + + /** + * The component name + * + * @var integer + * @since 3.7.0 + */ + public $option; + + /** + * The component parameters + * + * @var string|Registry + * @since 3.7.0 + * @note This field is protected to require reading this field to proxy through the getter to convert the params to a Registry instance + */ + protected $params; + + /** + * The extension namespace + * + * @var string + * @since 4.0.0 + */ + public $namespace; + + /** + * Indicates if this component is enabled + * + * @var integer + * @since 3.7.0 + */ + public $enabled; + + /** + * Class constructor + * + * @param array $data The component record data to load + * + * @since 3.7.0 + */ + public function __construct($data = array()) + { + foreach ((array) $data as $key => $value) { + $this->$key = $value; + } + } + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 3.7.0 + * @deprecated 5.0 Access the item parameters through the `getParams()` method + */ + public function __get($name) + { + if ($name === 'params') { + return $this->getParams(); + } + + return $this->$name; + } + + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 3.7.0 + * @deprecated 5.0 Set the item parameters through the `setParams()` method + */ + public function __set($name, $value) + { + if ($name === 'params') { + $this->setParams($value); + + return; + } + + $this->$name = $value; + } + + /** + * Returns the menu item parameters + * + * @return Registry + * + * @since 3.7.0 + */ + public function getParams() + { + if (!($this->params instanceof Registry)) { + $this->params = new Registry($this->params); + } + + return $this->params; + } + + /** + * Sets the menu item parameters + * + * @param Registry|string $params The data to be stored as the parameters + * + * @return void + * + * @since 3.7.0 + */ + public function setParams($params) + { + $this->params = $params; + } } diff --git a/libraries/src/Component/Exception/MissingComponentException.php b/libraries/src/Component/Exception/MissingComponentException.php index 67947b5b24000..cb0ff6b83067c 100644 --- a/libraries/src/Component/Exception/MissingComponentException.php +++ b/libraries/src/Component/Exception/MissingComponentException.php @@ -1,4 +1,5 @@ app = $app; - } - else - { - $this->app = Factory::getApplication(); - } + /** + * Class constructor. + * + * @param \Joomla\CMS\Application\CMSApplication $app Application-object that the router should use + * @param \Joomla\CMS\Menu\AbstractMenu $menu Menu-object that the router should use + * + * @since 3.4 + */ + public function __construct($app = null, $menu = null) + { + if ($app) { + $this->app = $app; + } else { + $this->app = Factory::getApplication(); + } - if ($menu) - { - $this->menu = $menu; - } - else - { - $this->menu = $this->app->getMenu(); - } - } + if ($menu) { + $this->menu = $menu; + } else { + $this->menu = $this->app->getMenu(); + } + } - /** - * Generic method to preprocess a URL - * - * @param array $query An associative array of URL arguments - * - * @return array The URL arguments to use to assemble the subsequent URL. - * - * @since 3.3 - */ - public function preprocess($query) - { - return $query; - } + /** + * Generic method to preprocess a URL + * + * @param array $query An associative array of URL arguments + * + * @return array The URL arguments to use to assemble the subsequent URL. + * + * @since 3.3 + */ + public function preprocess($query) + { + return $query; + } } diff --git a/libraries/src/Component/Router/RouterFactory.php b/libraries/src/Component/Router/RouterFactory.php index eb82e1aa2aa66..193d175621492 100644 --- a/libraries/src/Component/Router/RouterFactory.php +++ b/libraries/src/Component/Router/RouterFactory.php @@ -1,4 +1,5 @@ namespace = $namespace; - $this->categoryFactory = $categoryFactory; - $this->db = $db; - } + /** + * The namespace must be like: + * Joomla\Component\Content + * + * @param string $namespace The namespace + * @param CategoryFactoryInterface $categoryFactory The category object + * @param DatabaseInterface $db The database object + * + * @since 4.0.0 + */ + public function __construct($namespace, CategoryFactoryInterface $categoryFactory = null, DatabaseInterface $db = null) + { + $this->namespace = $namespace; + $this->categoryFactory = $categoryFactory; + $this->db = $db; + } - /** - * Creates a router. - * - * @param CMSApplicationInterface $application The application - * @param AbstractMenu $menu The menu object to work with - * - * @return RouterInterface - * - * @since 4.0.0 - */ - public function createRouter(CMSApplicationInterface $application, AbstractMenu $menu): RouterInterface - { - $className = trim($this->namespace, '\\') . '\\' . ucfirst($application->getName()) . '\\Service\\Router'; + /** + * Creates a router. + * + * @param CMSApplicationInterface $application The application + * @param AbstractMenu $menu The menu object to work with + * + * @return RouterInterface + * + * @since 4.0.0 + */ + public function createRouter(CMSApplicationInterface $application, AbstractMenu $menu): RouterInterface + { + $className = trim($this->namespace, '\\') . '\\' . ucfirst($application->getName()) . '\\Service\\Router'; - if (!class_exists($className)) - { - throw new \RuntimeException('No router available for this application.'); - } + if (!class_exists($className)) { + throw new \RuntimeException('No router available for this application.'); + } - return new $className($application, $menu, $this->categoryFactory, $this->db); - } + return new $className($application, $menu, $this->categoryFactory, $this->db); + } } diff --git a/libraries/src/Component/Router/RouterFactoryInterface.php b/libraries/src/Component/Router/RouterFactoryInterface.php index b1e6748a899ba..d9fff804dce02 100644 --- a/libraries/src/Component/Router/RouterFactoryInterface.php +++ b/libraries/src/Component/Router/RouterFactoryInterface.php @@ -1,4 +1,5 @@ component = $component; - } - - /** - * Generic preprocess function for missing or legacy component router - * - * @param array $query An associative array of URL arguments - * - * @return array The URL arguments to use to assemble the subsequent URL. - * - * @since 3.3 - */ - public function preprocess($query) - { - return $query; - } - - /** - * Generic build function for missing or legacy component router - * - * @param array &$query An array of URL arguments - * - * @return array The URL arguments to use to assemble the subsequent URL. - * - * @since 3.3 - */ - public function build(&$query) - { - $function = $this->component . 'BuildRoute'; - - if (\function_exists($function)) - { - $segments = $function($query); - $total = \count($segments); - - for ($i = 0; $i < $total; $i++) - { - $segments[$i] = str_replace(':', '-', $segments[$i]); - } - - return $segments; - } - - return array(); - } - - /** - * Generic parse function for missing or legacy component router - * - * @param array &$segments The segments of the URL to parse. - * - * @return array The URL attributes to be used by the application. - * - * @since 3.3 - */ - public function parse(&$segments) - { - $function = $this->component . 'ParseRoute'; - - if (\function_exists($function)) - { - $total = \count($segments); - - for ($i = 0; $i < $total; $i++) - { - $segments[$i] = preg_replace('/-/', ':', $segments[$i], 1); - } - - return $function($segments); - } - - return array(); - } + /** + * Name of the component + * + * @var string + * @since 3.3 + */ + protected $component; + + /** + * Constructor + * + * @param string $component Component name without the com_ prefix this router should react upon + * + * @since 3.3 + */ + public function __construct($component) + { + $this->component = $component; + } + + /** + * Generic preprocess function for missing or legacy component router + * + * @param array $query An associative array of URL arguments + * + * @return array The URL arguments to use to assemble the subsequent URL. + * + * @since 3.3 + */ + public function preprocess($query) + { + return $query; + } + + /** + * Generic build function for missing or legacy component router + * + * @param array &$query An array of URL arguments + * + * @return array The URL arguments to use to assemble the subsequent URL. + * + * @since 3.3 + */ + public function build(&$query) + { + $function = $this->component . 'BuildRoute'; + + if (\function_exists($function)) { + $segments = $function($query); + $total = \count($segments); + + for ($i = 0; $i < $total; $i++) { + $segments[$i] = str_replace(':', '-', $segments[$i]); + } + + return $segments; + } + + return array(); + } + + /** + * Generic parse function for missing or legacy component router + * + * @param array &$segments The segments of the URL to parse. + * + * @return array The URL attributes to be used by the application. + * + * @since 3.3 + */ + public function parse(&$segments) + { + $function = $this->component . 'ParseRoute'; + + if (\function_exists($function)) { + $total = \count($segments); + + for ($i = 0; $i < $total; $i++) { + $segments[$i] = preg_replace('/-/', ':', $segments[$i], 1); + } + + return $function($segments); + } + + return array(); + } } diff --git a/libraries/src/Component/Router/RouterServiceInterface.php b/libraries/src/Component/Router/RouterServiceInterface.php index f949a2352b32b..5a95f08a1ab38 100644 --- a/libraries/src/Component/Router/RouterServiceInterface.php +++ b/libraries/src/Component/Router/RouterServiceInterface.php @@ -1,4 +1,5 @@ routerFactory->createRouter($application, $menu); - } + /** + * Returns the router. + * + * @param CMSApplicationInterface $application The application object + * @param AbstractMenu $menu The menu object to work with + * + * @return RouterInterface + * + * @since 4.0.0 + */ + public function createRouter(CMSApplicationInterface $application, AbstractMenu $menu): RouterInterface + { + return $this->routerFactory->createRouter($application, $menu); + } - /** - * The router factory. - * - * @param RouterFactoryInterface $routerFactory The router factory - * - * @return void - * - * @since 4.0.0 - */ - public function setRouterFactory(RouterFactoryInterface $routerFactory) - { - $this->routerFactory = $routerFactory; - } + /** + * The router factory. + * + * @param RouterFactoryInterface $routerFactory The router factory + * + * @return void + * + * @since 4.0.0 + */ + public function setRouterFactory(RouterFactoryInterface $routerFactory) + { + $this->routerFactory = $routerFactory; + } } diff --git a/libraries/src/Component/Router/RouterView.php b/libraries/src/Component/Router/RouterView.php index 60a22272dc121..dcd9d7b9d4b0c 100644 --- a/libraries/src/Component/Router/RouterView.php +++ b/libraries/src/Component/Router/RouterView.php @@ -1,4 +1,5 @@ views[$view->name] = $view; - } - - /** - * Return an array of registered view objects - * - * @return RouterViewConfiguration[] Array of registered view objects - * - * @since 3.5 - */ - public function getViews() - { - return $this->views; - } - - /** - * Get the path of views from target view to root view - * including content items of a nestable view - * - * @param array $query Array of query elements - * - * @return array List of views including IDs of content items - * - * @since 3.5 - */ - public function getPath($query) - { - $views = $this->getViews(); - $result = array(); - - // Get the right view object - if (isset($query['view']) && isset($views[$query['view']])) - { - $viewobj = $views[$query['view']]; - } - - // Get the path from the current item to the root view with all IDs - if (isset($viewobj)) - { - $path = array_reverse($viewobj->path); - $start = true; - $childkey = false; - - foreach ($path as $element) - { - $view = $views[$element]; - - if ($start) - { - $key = $view->key; - $start = false; - } - else - { - $key = $childkey; - } - - $childkey = $view->parent_key; - - if (($key || $view->key) && \is_callable(array($this, 'get' . ucfirst($view->name) . 'Segment'))) - { - if (isset($query[$key])) - { - $result[$view->name] = \call_user_func_array(array($this, 'get' . ucfirst($view->name) . 'Segment'), array($query[$key], $query)); - } - elseif (isset($query[$view->key])) - { - $result[$view->name] = \call_user_func_array(array($this, 'get' . ucfirst($view->name) . 'Segment'), array($query[$view->key], $query)); - } - else - { - $result[$view->name] = array(); - } - } - else - { - $result[$view->name] = true; - } - } - } - - return $result; - } - - /** - * Get all currently attached rules - * - * @return RulesInterface[] All currently attached rules in an array - * - * @since 3.5 - */ - public function getRules() - { - return $this->rules; - } - - /** - * Add a number of router rules to the object - * - * @param RulesInterface[] $rules Array of JComponentRouterRulesInterface objects - * - * @return void - * - * @since 3.5 - */ - public function attachRules($rules) - { - foreach ($rules as $rule) - { - $this->attachRule($rule); - } - } - - /** - * Attach a build rule - * - * @param RulesInterface $rule The function to be called. - * - * @return void - * - * @since 3.5 - */ - public function attachRule(RulesInterface $rule) - { - $this->rules[] = $rule; - } - - /** - * Remove a build rule - * - * @param RulesInterface $rule The rule to be removed. - * - * @return boolean Was a rule removed? - * - * @since 3.5 - */ - public function detachRule(RulesInterface $rule) - { - foreach ($this->rules as $id => $r) - { - if ($r == $rule) - { - unset($this->rules[$id]); - - return true; - } - } - - return false; - } - - /** - * Generic method to preprocess a URL - * - * @param array $query An associative array of URL arguments - * - * @return array The URL arguments to use to assemble the subsequent URL. - * - * @since 3.5 - */ - public function preprocess($query) - { - // Process the parsed variables based on custom defined rules - foreach ($this->rules as $rule) - { - $rule->preprocess($query); - } - - return $query; - } - - /** - * Build method for URLs - * - * @param array &$query Array of query elements - * - * @return array Array of URL segments - * - * @since 3.5 - */ - public function build(&$query) - { - $segments = array(); - - // Process the parsed variables based on custom defined rules - foreach ($this->rules as $rule) - { - $rule->build($query, $segments); - } - - return $segments; - } - - /** - * Parse method for URLs - * - * @param array &$segments Array of URL string-segments - * - * @return array Associative array of query values - * - * @since 3.5 - */ - public function parse(&$segments) - { - $vars = array(); - - // Process the parsed variables based on custom defined rules - foreach ($this->rules as $rule) - { - $rule->parse($segments, $vars); - } - - return $vars; - } - - /** - * Method to return the name of the router - * - * @return string Name of the router - * - * @since 3.5 - */ - public function getName() - { - if (empty($this->name)) - { - $r = null; - - if (!preg_match('/(.*)Router/i', \get_class($this), $r)) - { - throw new \Exception('JLIB_APPLICATION_ERROR_ROUTER_GET_NAME', 500); - } - - $this->name = str_replace('com_', '', ComponentHelper::getComponentName($this, strtolower($r[1]))); - } - - return $this->name; - } + /** + * Name of the router of the component + * + * @var string + * @since 3.5 + */ + protected $name; + + /** + * Array of rules + * + * @var RulesInterface[] + * @since 3.5 + */ + protected $rules = array(); + + /** + * Views of the component + * + * @var RouterViewConfiguration[] + * @since 3.5 + */ + protected $views = array(); + + /** + * Register the views of a component + * + * @param RouterViewConfiguration $view View configuration object + * + * @return void + * + * @since 3.5 + */ + public function registerView(RouterViewConfiguration $view) + { + $this->views[$view->name] = $view; + } + + /** + * Return an array of registered view objects + * + * @return RouterViewConfiguration[] Array of registered view objects + * + * @since 3.5 + */ + public function getViews() + { + return $this->views; + } + + /** + * Get the path of views from target view to root view + * including content items of a nestable view + * + * @param array $query Array of query elements + * + * @return array List of views including IDs of content items + * + * @since 3.5 + */ + public function getPath($query) + { + $views = $this->getViews(); + $result = array(); + + // Get the right view object + if (isset($query['view']) && isset($views[$query['view']])) { + $viewobj = $views[$query['view']]; + } + + // Get the path from the current item to the root view with all IDs + if (isset($viewobj)) { + $path = array_reverse($viewobj->path); + $start = true; + $childkey = false; + + foreach ($path as $element) { + $view = $views[$element]; + + if ($start) { + $key = $view->key; + $start = false; + } else { + $key = $childkey; + } + + $childkey = $view->parent_key; + + if (($key || $view->key) && \is_callable(array($this, 'get' . ucfirst($view->name) . 'Segment'))) { + if (isset($query[$key])) { + $result[$view->name] = \call_user_func_array(array($this, 'get' . ucfirst($view->name) . 'Segment'), array($query[$key], $query)); + } elseif (isset($query[$view->key])) { + $result[$view->name] = \call_user_func_array(array($this, 'get' . ucfirst($view->name) . 'Segment'), array($query[$view->key], $query)); + } else { + $result[$view->name] = array(); + } + } else { + $result[$view->name] = true; + } + } + } + + return $result; + } + + /** + * Get all currently attached rules + * + * @return RulesInterface[] All currently attached rules in an array + * + * @since 3.5 + */ + public function getRules() + { + return $this->rules; + } + + /** + * Add a number of router rules to the object + * + * @param RulesInterface[] $rules Array of JComponentRouterRulesInterface objects + * + * @return void + * + * @since 3.5 + */ + public function attachRules($rules) + { + foreach ($rules as $rule) { + $this->attachRule($rule); + } + } + + /** + * Attach a build rule + * + * @param RulesInterface $rule The function to be called. + * + * @return void + * + * @since 3.5 + */ + public function attachRule(RulesInterface $rule) + { + $this->rules[] = $rule; + } + + /** + * Remove a build rule + * + * @param RulesInterface $rule The rule to be removed. + * + * @return boolean Was a rule removed? + * + * @since 3.5 + */ + public function detachRule(RulesInterface $rule) + { + foreach ($this->rules as $id => $r) { + if ($r == $rule) { + unset($this->rules[$id]); + + return true; + } + } + + return false; + } + + /** + * Generic method to preprocess a URL + * + * @param array $query An associative array of URL arguments + * + * @return array The URL arguments to use to assemble the subsequent URL. + * + * @since 3.5 + */ + public function preprocess($query) + { + // Process the parsed variables based on custom defined rules + foreach ($this->rules as $rule) { + $rule->preprocess($query); + } + + return $query; + } + + /** + * Build method for URLs + * + * @param array &$query Array of query elements + * + * @return array Array of URL segments + * + * @since 3.5 + */ + public function build(&$query) + { + $segments = array(); + + // Process the parsed variables based on custom defined rules + foreach ($this->rules as $rule) { + $rule->build($query, $segments); + } + + return $segments; + } + + /** + * Parse method for URLs + * + * @param array &$segments Array of URL string-segments + * + * @return array Associative array of query values + * + * @since 3.5 + */ + public function parse(&$segments) + { + $vars = array(); + + // Process the parsed variables based on custom defined rules + foreach ($this->rules as $rule) { + $rule->parse($segments, $vars); + } + + return $vars; + } + + /** + * Method to return the name of the router + * + * @return string Name of the router + * + * @since 3.5 + */ + public function getName() + { + if (empty($this->name)) { + $r = null; + + if (!preg_match('/(.*)Router/i', \get_class($this), $r)) { + throw new \Exception('JLIB_APPLICATION_ERROR_ROUTER_GET_NAME', 500); + } + + $this->name = str_replace('com_', '', ComponentHelper::getComponentName($this, strtolower($r[1]))); + } + + return $this->name; + } } diff --git a/libraries/src/Component/Router/Rules/MenuRules.php b/libraries/src/Component/Router/Rules/MenuRules.php index cd5254bc4925c..ceb00745f20ae 100644 --- a/libraries/src/Component/Router/Rules/MenuRules.php +++ b/libraries/src/Component/Router/Rules/MenuRules.php @@ -1,4 +1,5 @@ router = $router; - - $this->buildLookup(); - } - - /** - * Finds the right Itemid for this query - * - * @param array &$query The query array to process - * - * @return void - * - * @since 3.4 - */ - public function preprocess(&$query) - { - $active = $this->router->menu->getActive(); - - /** - * If the active item id is not the same as the supplied item id or we have a supplied item id and no active - * menu item then we just use the supplied menu item and continue - */ - if (isset($query['Itemid']) && ($active === null || $query['Itemid'] != $active->id)) - { - return; - } - - // Get query language - $language = isset($query['lang']) ? $query['lang'] : '*'; - - // Set the language to the current one when multilang is enabled and item is tagged to ALL - if (Multilanguage::isEnabled() && $language === '*') - { - $language = $this->router->app->get('language'); - } - - if (!isset($this->lookup[$language])) - { - $this->buildLookup($language); - } - - // Check if the active menu item matches the requested query - if ($active !== null && isset($query['Itemid'])) - { - // Check if active->query and supplied query are the same - $match = true; - - foreach ($active->query as $k => $v) - { - if (isset($query[$k]) && $v !== $query[$k]) - { - // Compare again without alias - if (\is_string($v) && $v == current(explode(':', $query[$k], 2))) - { - continue; - } - - $match = false; - break; - } - } - - if ($match) - { - // Just use the supplied menu item - return; - } - } - - $needles = $this->router->getPath($query); - - $layout = isset($query['layout']) && $query['layout'] !== 'default' ? ':' . $query['layout'] : ''; - - if ($needles) - { - foreach ($needles as $view => $ids) - { - $viewLayout = $view . $layout; - - if ($layout && isset($this->lookup[$language][$viewLayout])) - { - if (\is_bool($ids)) - { - $query['Itemid'] = $this->lookup[$language][$viewLayout]; - - return; - } - - foreach ($ids as $id => $segment) - { - if (isset($this->lookup[$language][$viewLayout][(int) $id])) - { - $query['Itemid'] = $this->lookup[$language][$viewLayout][(int) $id]; - - return; - } - } - } - - if (isset($this->lookup[$language][$view])) - { - if (\is_bool($ids)) - { - $query['Itemid'] = $this->lookup[$language][$view]; - - return; - } - - foreach ($ids as $id => $segment) - { - if (isset($this->lookup[$language][$view][(int) $id])) - { - $query['Itemid'] = $this->lookup[$language][$view][(int) $id]; - - return; - } - } - } - } - } - - // Check if the active menuitem matches the requested language - if ($active && $active->component === 'com_' . $this->router->getName() - && ($language === '*' || \in_array($active->language, array('*', $language)) || !Multilanguage::isEnabled())) - { - $query['Itemid'] = $active->id; - - return; - } - - // If not found, return language specific home link - $default = $this->router->menu->getDefault($language); - - if (!empty($default->id)) - { - $query['Itemid'] = $default->id; - } - } - - /** - * Method to build the lookup array - * - * @param string $language The language that the lookup should be built up for - * - * @return void - * - * @since 3.4 - */ - protected function buildLookup($language = '*') - { - // Prepare the reverse lookup array. - if (!isset($this->lookup[$language])) - { - $this->lookup[$language] = array(); - - $component = ComponentHelper::getComponent('com_' . $this->router->getName()); - $views = $this->router->getViews(); - - $attributes = array('component_id'); - $values = array((int) $component->id); - - $attributes[] = 'language'; - $values[] = array($language, '*'); - - $items = $this->router->menu->getItems($attributes, $values); - - foreach ($items as $item) - { - if (isset($item->query['view'], $views[$item->query['view']])) - { - $view = $item->query['view']; - - $layout = ''; - - if (isset($item->query['layout'])) - { - $layout = ':' . $item->query['layout']; - } - - if ($views[$view]->key) - { - if (!isset($this->lookup[$language][$view . $layout])) - { - $this->lookup[$language][$view . $layout] = array(); - } - - if (!isset($this->lookup[$language][$view])) - { - $this->lookup[$language][$view] = array(); - } - - // If menuitem has no key set, we assume 0. - if (!isset($item->query[$views[$view]->key])) - { - $item->query[$views[$view]->key] = 0; - } - - /** - * Here it will become a bit tricky - * language != * can override existing entries - * language == * cannot override existing entries - */ - if (!isset($this->lookup[$language][$view . $layout][$item->query[$views[$view]->key]]) || $item->language !== '*') - { - $this->lookup[$language][$view . $layout][$item->query[$views[$view]->key]] = $item->id; - $this->lookup[$language][$view][$item->query[$views[$view]->key]] = $item->id; - } - } - else - { - /** - * Here it will become a bit tricky - * language != * can override existing entries - * language == * cannot override existing entries - */ - if (!isset($this->lookup[$language][$view . $layout]) || $item->language !== '*') - { - $this->lookup[$language][$view . $layout] = $item->id; - } - } - } - } - } - } - - /** - * Dummy method to fulfil the interface requirements - * - * @param array &$segments The URL segments to parse - * @param array &$vars The vars that result from the segments - * - * @return void - * - * @since 3.4 - * @codeCoverageIgnore - */ - public function parse(&$segments, &$vars) - { - } - - /** - * Dummy method to fulfil the interface requirements - * - * @param array &$query The vars that should be converted - * @param array &$segments The URL segments to create - * - * @return void - * - * @since 3.4 - * @codeCoverageIgnore - */ - public function build(&$query, &$segments) - { - } + /** + * Router this rule belongs to + * + * @var RouterView + * @since 3.4 + */ + protected $router; + + /** + * Lookup array of the menu items + * + * @var array + * @since 3.4 + */ + protected $lookup = array(); + + /** + * Class constructor. + * + * @param RouterView $router Router this rule belongs to + * + * @since 3.4 + */ + public function __construct(RouterView $router) + { + $this->router = $router; + + $this->buildLookup(); + } + + /** + * Finds the right Itemid for this query + * + * @param array &$query The query array to process + * + * @return void + * + * @since 3.4 + */ + public function preprocess(&$query) + { + $active = $this->router->menu->getActive(); + + /** + * If the active item id is not the same as the supplied item id or we have a supplied item id and no active + * menu item then we just use the supplied menu item and continue + */ + if (isset($query['Itemid']) && ($active === null || $query['Itemid'] != $active->id)) { + return; + } + + // Get query language + $language = isset($query['lang']) ? $query['lang'] : '*'; + + // Set the language to the current one when multilang is enabled and item is tagged to ALL + if (Multilanguage::isEnabled() && $language === '*') { + $language = $this->router->app->get('language'); + } + + if (!isset($this->lookup[$language])) { + $this->buildLookup($language); + } + + // Check if the active menu item matches the requested query + if ($active !== null && isset($query['Itemid'])) { + // Check if active->query and supplied query are the same + $match = true; + + foreach ($active->query as $k => $v) { + if (isset($query[$k]) && $v !== $query[$k]) { + // Compare again without alias + if (\is_string($v) && $v == current(explode(':', $query[$k], 2))) { + continue; + } + + $match = false; + break; + } + } + + if ($match) { + // Just use the supplied menu item + return; + } + } + + $needles = $this->router->getPath($query); + + $layout = isset($query['layout']) && $query['layout'] !== 'default' ? ':' . $query['layout'] : ''; + + if ($needles) { + foreach ($needles as $view => $ids) { + $viewLayout = $view . $layout; + + if ($layout && isset($this->lookup[$language][$viewLayout])) { + if (\is_bool($ids)) { + $query['Itemid'] = $this->lookup[$language][$viewLayout]; + + return; + } + + foreach ($ids as $id => $segment) { + if (isset($this->lookup[$language][$viewLayout][(int) $id])) { + $query['Itemid'] = $this->lookup[$language][$viewLayout][(int) $id]; + + return; + } + } + } + + if (isset($this->lookup[$language][$view])) { + if (\is_bool($ids)) { + $query['Itemid'] = $this->lookup[$language][$view]; + + return; + } + + foreach ($ids as $id => $segment) { + if (isset($this->lookup[$language][$view][(int) $id])) { + $query['Itemid'] = $this->lookup[$language][$view][(int) $id]; + + return; + } + } + } + } + } + + // Check if the active menuitem matches the requested language + if ( + $active && $active->component === 'com_' . $this->router->getName() + && ($language === '*' || \in_array($active->language, array('*', $language)) || !Multilanguage::isEnabled()) + ) { + $query['Itemid'] = $active->id; + + return; + } + + // If not found, return language specific home link + $default = $this->router->menu->getDefault($language); + + if (!empty($default->id)) { + $query['Itemid'] = $default->id; + } + } + + /** + * Method to build the lookup array + * + * @param string $language The language that the lookup should be built up for + * + * @return void + * + * @since 3.4 + */ + protected function buildLookup($language = '*') + { + // Prepare the reverse lookup array. + if (!isset($this->lookup[$language])) { + $this->lookup[$language] = array(); + + $component = ComponentHelper::getComponent('com_' . $this->router->getName()); + $views = $this->router->getViews(); + + $attributes = array('component_id'); + $values = array((int) $component->id); + + $attributes[] = 'language'; + $values[] = array($language, '*'); + + $items = $this->router->menu->getItems($attributes, $values); + + foreach ($items as $item) { + if (isset($item->query['view'], $views[$item->query['view']])) { + $view = $item->query['view']; + + $layout = ''; + + if (isset($item->query['layout'])) { + $layout = ':' . $item->query['layout']; + } + + if ($views[$view]->key) { + if (!isset($this->lookup[$language][$view . $layout])) { + $this->lookup[$language][$view . $layout] = array(); + } + + if (!isset($this->lookup[$language][$view])) { + $this->lookup[$language][$view] = array(); + } + + // If menuitem has no key set, we assume 0. + if (!isset($item->query[$views[$view]->key])) { + $item->query[$views[$view]->key] = 0; + } + + /** + * Here it will become a bit tricky + * language != * can override existing entries + * language == * cannot override existing entries + */ + if (!isset($this->lookup[$language][$view . $layout][$item->query[$views[$view]->key]]) || $item->language !== '*') { + $this->lookup[$language][$view . $layout][$item->query[$views[$view]->key]] = $item->id; + $this->lookup[$language][$view][$item->query[$views[$view]->key]] = $item->id; + } + } else { + /** + * Here it will become a bit tricky + * language != * can override existing entries + * language == * cannot override existing entries + */ + if (!isset($this->lookup[$language][$view . $layout]) || $item->language !== '*') { + $this->lookup[$language][$view . $layout] = $item->id; + } + } + } + } + } + } + + /** + * Dummy method to fulfil the interface requirements + * + * @param array &$segments The URL segments to parse + * @param array &$vars The vars that result from the segments + * + * @return void + * + * @since 3.4 + * @codeCoverageIgnore + */ + public function parse(&$segments, &$vars) + { + } + + /** + * Dummy method to fulfil the interface requirements + * + * @param array &$query The vars that should be converted + * @param array &$segments The URL segments to create + * + * @return void + * + * @since 3.4 + * @codeCoverageIgnore + */ + public function build(&$query, &$segments) + { + } } diff --git a/libraries/src/Component/Router/Rules/NomenuRules.php b/libraries/src/Component/Router/Rules/NomenuRules.php index 84f08ad515d8c..c2467ff3b0a69 100644 --- a/libraries/src/Component/Router/Rules/NomenuRules.php +++ b/libraries/src/Component/Router/Rules/NomenuRules.php @@ -1,4 +1,5 @@ router = $router; - } - - /** - * Dummy method to fulfil the interface requirements - * - * @param array &$query The query array to process - * - * @return void - * - * @since 3.4 - * @codeCoverageIgnore - */ - public function preprocess(&$query) - { - } - - /** - * Parse a menu-less URL - * - * @param array &$segments The URL segments to parse - * @param array &$vars The vars that result from the segments - * - * @return void - * - * @since 3.4 - */ - public function parse(&$segments, &$vars) - { - $active = $this->router->menu->getActive(); - - if (!\is_object($active)) - { - $views = $this->router->getViews(); - - if (isset($views[$segments[0]])) - { - $vars['view'] = array_shift($segments); - $view = $views[$vars['view']]; - - if (isset($view->key) && isset($segments[0])) - { - if (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Id'))) - { - if ($view->parent_key && $this->router->app->input->get($view->parent_key)) - { - $vars[$view->parent->key] = $this->router->app->input->get($view->parent_key); - $vars[$view->parent_key] = $this->router->app->input->get($view->parent_key); - } - - if ($view->nestable) - { - $vars[$view->key] = 0; - - while (count($segments)) - { - $segment = array_shift($segments); - $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); - - if (!$result) - { - array_unshift($segments, $segment); - break; - } - - $vars[$view->key] = preg_replace('/-/', ':', $result, 1); - } - } - else - { - $segment = array_shift($segments); - $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); - - $vars[$view->key] = preg_replace('/-/', ':', $result, 1); - } - } - else - { - $vars[$view->key] = preg_replace('/-/', ':', array_shift($segments), 1); - } - } - } - } - } - - /** - * Build a menu-less URL - * - * @param array &$query The vars that should be converted - * @param array &$segments The URL segments to create - * - * @return void - * - * @since 3.4 - */ - public function build(&$query, &$segments) - { - $menu_found = false; - - if (isset($query['Itemid'])) - { - $item = $this->router->menu->getItem($query['Itemid']); - - if (!isset($query['option']) - || ($item && isset($item->query['option']) && $item->query['option'] === $query['option'])) - { - $menu_found = true; - } - } - - if (!$menu_found && isset($query['view'])) - { - $views = $this->router->getViews(); - - if (isset($views[$query['view']])) - { - $view = $views[$query['view']]; - $segments[] = $query['view']; - - if ($view->key && isset($query[$view->key])) - { - if (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Segment'))) - { - $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Segment'), array($query[$view->key], $query)); - - if ($view->nestable) - { - array_pop($result); - - while (count($result)) - { - $segments[] = str_replace(':', '-', array_pop($result)); - } - } - else - { - $segments[] = str_replace(':', '-', array_pop($result)); - } - } - else - { - $segments[] = str_replace(':', '-', $query[$view->key]); - } - - unset($query[$views[$query['view']]->key]); - } - - unset($query['view']); - } - } - } + /** + * Router this rule belongs to + * + * @var RouterView + * @since 3.4 + */ + protected $router; + + /** + * Class constructor. + * + * @param RouterView $router Router this rule belongs to + * + * @since 3.4 + */ + public function __construct(RouterView $router) + { + $this->router = $router; + } + + /** + * Dummy method to fulfil the interface requirements + * + * @param array &$query The query array to process + * + * @return void + * + * @since 3.4 + * @codeCoverageIgnore + */ + public function preprocess(&$query) + { + } + + /** + * Parse a menu-less URL + * + * @param array &$segments The URL segments to parse + * @param array &$vars The vars that result from the segments + * + * @return void + * + * @since 3.4 + */ + public function parse(&$segments, &$vars) + { + $active = $this->router->menu->getActive(); + + if (!\is_object($active)) { + $views = $this->router->getViews(); + + if (isset($views[$segments[0]])) { + $vars['view'] = array_shift($segments); + $view = $views[$vars['view']]; + + if (isset($view->key) && isset($segments[0])) { + if (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Id'))) { + if ($view->parent_key && $this->router->app->input->get($view->parent_key)) { + $vars[$view->parent->key] = $this->router->app->input->get($view->parent_key); + $vars[$view->parent_key] = $this->router->app->input->get($view->parent_key); + } + + if ($view->nestable) { + $vars[$view->key] = 0; + + while (count($segments)) { + $segment = array_shift($segments); + $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); + + if (!$result) { + array_unshift($segments, $segment); + break; + } + + $vars[$view->key] = preg_replace('/-/', ':', $result, 1); + } + } else { + $segment = array_shift($segments); + $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); + + $vars[$view->key] = preg_replace('/-/', ':', $result, 1); + } + } else { + $vars[$view->key] = preg_replace('/-/', ':', array_shift($segments), 1); + } + } + } + } + } + + /** + * Build a menu-less URL + * + * @param array &$query The vars that should be converted + * @param array &$segments The URL segments to create + * + * @return void + * + * @since 3.4 + */ + public function build(&$query, &$segments) + { + $menu_found = false; + + if (isset($query['Itemid'])) { + $item = $this->router->menu->getItem($query['Itemid']); + + if ( + !isset($query['option']) + || ($item && isset($item->query['option']) && $item->query['option'] === $query['option']) + ) { + $menu_found = true; + } + } + + if (!$menu_found && isset($query['view'])) { + $views = $this->router->getViews(); + + if (isset($views[$query['view']])) { + $view = $views[$query['view']]; + $segments[] = $query['view']; + + if ($view->key && isset($query[$view->key])) { + if (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Segment'))) { + $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Segment'), array($query[$view->key], $query)); + + if ($view->nestable) { + array_pop($result); + + while (count($result)) { + $segments[] = str_replace(':', '-', array_pop($result)); + } + } else { + $segments[] = str_replace(':', '-', array_pop($result)); + } + } else { + $segments[] = str_replace(':', '-', $query[$view->key]); + } + + unset($query[$views[$query['view']]->key]); + } + + unset($query['view']); + } + } + } } diff --git a/libraries/src/Component/Router/Rules/RulesInterface.php b/libraries/src/Component/Router/Rules/RulesInterface.php index d56015c2dc633..feb68b3454342 100644 --- a/libraries/src/Component/Router/Rules/RulesInterface.php +++ b/libraries/src/Component/Router/Rules/RulesInterface.php @@ -1,4 +1,5 @@ router = $router; - } - - /** - * Dummy method to fulfil the interface requirements - * - * @param array &$query The query array to process - * - * @return void - * - * @since 3.4 - */ - public function preprocess(&$query) - { - } - - /** - * Parse the URL - * - * @param array &$segments The URL segments to parse - * @param array &$vars The vars that result from the segments - * - * @return void - * - * @since 3.4 - */ - public function parse(&$segments, &$vars) - { - // Get the views and the currently active query vars - $views = $this->router->getViews(); - $active = $this->router->menu->getActive(); - - if ($active) - { - $vars = array_merge($active->query, $vars); - } - - // We don't have a view or its not a view of this component! We stop here - if (!isset($vars['view']) || !isset($views[$vars['view']])) - { - return; - } - - // Copy the segments, so that we can iterate over all of them and at the same time modify the original segments - $tempSegments = $segments; - - // Iterate over the segments as long as a segment fits - foreach ($tempSegments as $segment) - { - // Our current view is nestable. We need to check first if the segment fits to that - if ($views[$vars['view']]->nestable) - { - if (\is_callable(array($this->router, 'get' . ucfirst($views[$vars['view']]->name) . 'Id'))) - { - $key = \call_user_func_array(array($this->router, 'get' . ucfirst($views[$vars['view']]->name) . 'Id'), array($segment, $vars)); - - // Did we get a proper key? If not, we need to look in the child-views - if ($key) - { - $vars[$views[$vars['view']]->key] = $key; - - array_shift($segments); - - continue; - } - } - else - { - // The router is not complete. The getId() method is missing. - return; - } - } - - // Lets find the right view that belongs to this segment - $found = false; - - foreach ($views[$vars['view']]->children as $view) - { - if (!$view->key) - { - if ($view->name === $segment) - { - // The segment is a view name - $parent = $views[$vars['view']]; - $vars['view'] = $view->name; - $found = true; - - if ($view->parent_key && isset($vars[$parent->key])) - { - $parent_key = $vars[$parent->key]; - $vars[$view->parent_key] = $parent_key; - - unset($vars[$parent->key]); - } - - break; - } - } - elseif (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Id'))) - { - // Hand the data over to the router specific method and see if there is a content item that fits - $key = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); - - if ($key) - { - // Found the right view and the right item - $parent = $views[$vars['view']]; - $vars['view'] = $view->name; - $found = true; - - if ($view->parent_key && isset($vars[$parent->key])) - { - $parent_key = $vars[$parent->key]; - $vars[$view->parent_key] = $parent_key; - - unset($vars[$parent->key]); - } - - $vars[$view->key] = $key; - - break; - } - } - } - - if (!$found) - { - return; - } - - array_shift($segments); - } - } - - /** - * Build a standard URL - * - * @param array &$query The vars that should be converted - * @param array &$segments The URL segments to create - * - * @return void - * - * @since 3.4 - */ - public function build(&$query, &$segments) - { - if (!isset($query['Itemid'], $query['view'])) - { - return; - } - - // Get the menu item belonging to the Itemid that has been found - $item = $this->router->menu->getItem($query['Itemid']); - - if ($item === null - || $item->component !== 'com_' . $this->router->getName() - || !isset($item->query['view'])) - { - return; - } - - // Get menu item layout - $mLayout = isset($item->query['layout']) ? $item->query['layout'] : null; - - // Get all views for this component - $views = $this->router->getViews(); - - // Return directly when the URL of the Itemid is identical with the URL to build - if ($item->query['view'] === $query['view']) - { - $view = $views[$query['view']]; - - if (!$view->key) - { - unset($query['view']); - - if (isset($query['layout']) && $mLayout === $query['layout']) - { - unset($query['layout']); - } - - return; - } - - if (isset($query[$view->key]) && $item->query[$view->key] == (int) $query[$view->key]) - { - unset($query[$view->key]); - - while ($view) - { - unset($query[$view->parent_key]); - - $view = $view->parent; - } - - unset($query['view']); - - if (isset($query['layout']) && $mLayout === $query['layout']) - { - unset($query['layout']); - } - - return; - } - } - - // Get the path from the view of the current URL and parse it to the menu item - $path = array_reverse($this->router->getPath($query), true); - $found = false; - - foreach ($path as $element => $ids) - { - $view = $views[$element]; - - if ($found === false && $item->query['view'] === $element) - { - if ($view->nestable) - { - $found = true; - } - elseif ($view->children) - { - $found = true; - - continue; - } - } - - if ($found === false) - { - // Jump to the next view - continue; - } - - if ($ids) - { - if ($view->nestable) - { - $found2 = false; - - foreach (array_reverse($ids, true) as $id => $segment) - { - if ($found2) - { - $segments[] = str_replace(':', '-', $segment); - } - elseif ((int) $item->query[$view->key] === (int) $id) - { - $found2 = true; - } - } - } - elseif ($ids === true) - { - $segments[] = $element; - } - else - { - $segments[] = str_replace(':', '-', current($ids)); - } - } - - if ($view->parent_key) - { - // Remove parent key from query - unset($query[$view->parent_key]); - } - } - - if ($found) - { - unset($query[$views[$query['view']]->key], $query['view']); - - if (isset($query['layout']) && $mLayout === $query['layout']) - { - unset($query['layout']); - } - } - } + /** + * Router this rule belongs to + * + * @var RouterView + * @since 3.4 + */ + protected $router; + + /** + * Class constructor. + * + * @param RouterView $router Router this rule belongs to + * + * @since 3.4 + */ + public function __construct(RouterView $router) + { + $this->router = $router; + } + + /** + * Dummy method to fulfil the interface requirements + * + * @param array &$query The query array to process + * + * @return void + * + * @since 3.4 + */ + public function preprocess(&$query) + { + } + + /** + * Parse the URL + * + * @param array &$segments The URL segments to parse + * @param array &$vars The vars that result from the segments + * + * @return void + * + * @since 3.4 + */ + public function parse(&$segments, &$vars) + { + // Get the views and the currently active query vars + $views = $this->router->getViews(); + $active = $this->router->menu->getActive(); + + if ($active) { + $vars = array_merge($active->query, $vars); + } + + // We don't have a view or its not a view of this component! We stop here + if (!isset($vars['view']) || !isset($views[$vars['view']])) { + return; + } + + // Copy the segments, so that we can iterate over all of them and at the same time modify the original segments + $tempSegments = $segments; + + // Iterate over the segments as long as a segment fits + foreach ($tempSegments as $segment) { + // Our current view is nestable. We need to check first if the segment fits to that + if ($views[$vars['view']]->nestable) { + if (\is_callable(array($this->router, 'get' . ucfirst($views[$vars['view']]->name) . 'Id'))) { + $key = \call_user_func_array(array($this->router, 'get' . ucfirst($views[$vars['view']]->name) . 'Id'), array($segment, $vars)); + + // Did we get a proper key? If not, we need to look in the child-views + if ($key) { + $vars[$views[$vars['view']]->key] = $key; + + array_shift($segments); + + continue; + } + } else { + // The router is not complete. The getId() method is missing. + return; + } + } + + // Lets find the right view that belongs to this segment + $found = false; + + foreach ($views[$vars['view']]->children as $view) { + if (!$view->key) { + if ($view->name === $segment) { + // The segment is a view name + $parent = $views[$vars['view']]; + $vars['view'] = $view->name; + $found = true; + + if ($view->parent_key && isset($vars[$parent->key])) { + $parent_key = $vars[$parent->key]; + $vars[$view->parent_key] = $parent_key; + + unset($vars[$parent->key]); + } + + break; + } + } elseif (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Id'))) { + // Hand the data over to the router specific method and see if there is a content item that fits + $key = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); + + if ($key) { + // Found the right view and the right item + $parent = $views[$vars['view']]; + $vars['view'] = $view->name; + $found = true; + + if ($view->parent_key && isset($vars[$parent->key])) { + $parent_key = $vars[$parent->key]; + $vars[$view->parent_key] = $parent_key; + + unset($vars[$parent->key]); + } + + $vars[$view->key] = $key; + + break; + } + } + } + + if (!$found) { + return; + } + + array_shift($segments); + } + } + + /** + * Build a standard URL + * + * @param array &$query The vars that should be converted + * @param array &$segments The URL segments to create + * + * @return void + * + * @since 3.4 + */ + public function build(&$query, &$segments) + { + if (!isset($query['Itemid'], $query['view'])) { + return; + } + + // Get the menu item belonging to the Itemid that has been found + $item = $this->router->menu->getItem($query['Itemid']); + + if ( + $item === null + || $item->component !== 'com_' . $this->router->getName() + || !isset($item->query['view']) + ) { + return; + } + + // Get menu item layout + $mLayout = isset($item->query['layout']) ? $item->query['layout'] : null; + + // Get all views for this component + $views = $this->router->getViews(); + + // Return directly when the URL of the Itemid is identical with the URL to build + if ($item->query['view'] === $query['view']) { + $view = $views[$query['view']]; + + if (!$view->key) { + unset($query['view']); + + if (isset($query['layout']) && $mLayout === $query['layout']) { + unset($query['layout']); + } + + return; + } + + if (isset($query[$view->key]) && $item->query[$view->key] == (int) $query[$view->key]) { + unset($query[$view->key]); + + while ($view) { + unset($query[$view->parent_key]); + + $view = $view->parent; + } + + unset($query['view']); + + if (isset($query['layout']) && $mLayout === $query['layout']) { + unset($query['layout']); + } + + return; + } + } + + // Get the path from the view of the current URL and parse it to the menu item + $path = array_reverse($this->router->getPath($query), true); + $found = false; + + foreach ($path as $element => $ids) { + $view = $views[$element]; + + if ($found === false && $item->query['view'] === $element) { + if ($view->nestable) { + $found = true; + } elseif ($view->children) { + $found = true; + + continue; + } + } + + if ($found === false) { + // Jump to the next view + continue; + } + + if ($ids) { + if ($view->nestable) { + $found2 = false; + + foreach (array_reverse($ids, true) as $id => $segment) { + if ($found2) { + $segments[] = str_replace(':', '-', $segment); + } elseif ((int) $item->query[$view->key] === (int) $id) { + $found2 = true; + } + } + } elseif ($ids === true) { + $segments[] = $element; + } else { + $segments[] = str_replace(':', '-', current($ids)); + } + } + + if ($view->parent_key) { + // Remove parent key from query + unset($query[$view->parent_key]); + } + } + + if ($found) { + unset($query[$views[$query['view']]->key], $query['view']); + + if (isset($query['layout']) && $mLayout === $query['layout']) { + unset($query['layout']); + } + } + } } diff --git a/libraries/src/Console/AddUserCommand.php b/libraries/src/Console/AddUserCommand.php index c44cf43f7071c..4dccd36fc8bc4 100644 --- a/libraries/src/Console/AddUserCommand.php +++ b/libraries/src/Console/AddUserCommand.php @@ -1,4 +1,5 @@ setDatabase($db); - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - $this->ioStyle->title('Add user'); - $this->user = $this->getStringFromOption('username', 'Please enter a username'); - $this->name = $this->getStringFromOption('name', 'Please enter a name (full name of user)'); - $this->email = $this->getStringFromOption('email', 'Please enter an email address'); - $this->password = $this->getStringFromOption('password', 'Please enter a password'); - $this->userGroups = $this->getUserGroups(); - - if (\in_array("error", $this->userGroups)) - { - $this->ioStyle->error("'" . $this->userGroups[1] . "' user group doesn't exist!"); - - return Command::FAILURE; - } - - // Get filter to remove invalid characters - $filter = new InputFilter; - - $user['username'] = $filter->clean($this->user, 'USERNAME'); - $user['password'] = $this->password; - $user['name'] = $filter->clean($this->name, 'STRING'); - $user['email'] = $this->email; - $user['groups'] = $this->userGroups; - - $userObj = User::getInstance(); - $userObj->bind($user); - - if (!$userObj->save()) - { - switch ($userObj->getError()) - { - case "JLIB_DATABASE_ERROR_USERNAME_INUSE": - $this->ioStyle->error("The username already exists!"); - break; - case "JLIB_DATABASE_ERROR_EMAIL_INUSE": - $this->ioStyle->error("The email address already exists!"); - break; - case "JLIB_DATABASE_ERROR_VALID_MAIL": - $this->ioStyle->error("The email address is invalid!"); - break; - } - - return 1; - } - - $this->ioStyle->success("User created!"); - - return Command::SUCCESS; - } - - /** - * Method to get groupId by groupName - * - * @param string $groupName name of group - * - * @return integer - * - * @since 4.0.0 - */ - protected function getGroupId($groupName) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('title') . ' = :groupName') - ->bind(':groupName', $groupName); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Method to get a value from option - * - * @param string $option set the option name - * @param string $question set the question if user enters no value to option - * - * @return string - * - * @since 4.0.0 - */ - public function getStringFromOption($option, $question): string - { - $answer = (string) $this->cliInput->getOption($option); - - while (!$answer) - { - if ($option === 'password') - { - $answer = (string) $this->ioStyle->askHidden($question); - } - else - { - $answer = (string) $this->ioStyle->ask($question); - } - } - - return $answer; - } - - /** - * Method to get a value from option - * - * @return array - * - * @since 4.0.0 - */ - protected function getUserGroups(): array - { - $groups = $this->getApplication()->getConsoleInput()->getOption('usergroup'); - $db = $this->getDatabase(); - - $groupList = []; - - // Group names have been supplied as input arguments - if (!\is_null($groups) && $groups[0]) - { - $groups = explode(',', $groups); - - foreach ($groups as $group) - { - $groupId = $this->getGroupId($group); - - if (empty($groupId)) - { - $this->ioStyle->error("Invalid group name '" . $group . "'"); - throw new InvalidOptionException("Invalid group name " . $group); - } - - $groupList[] = $this->getGroupId($group); - } - - return $groupList; - } - - // Generate select list for user - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__usergroups')) - ->order($db->quoteName('id') . 'ASC'); - $db->setQuery($query); - - $list = $db->loadColumn(); - - $choice = new ChoiceQuestion( - 'Please select a usergroup (separate multiple groups with a comma)', - $list - ); - $choice->setMultiselect(true); - - $answer = (array) $this->ioStyle->askQuestion($choice); - - foreach ($answer as $group) - { - $groupList[] = $this->getGroupId($group); - } - - return $groupList; - } - - /** - * Configure the IO. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return void - * - * @since 4.0.0 - */ - private function configureIO(InputInterface $input, OutputInterface $output) - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% will add a user + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'user:add'; + + /** + * SymfonyStyle Object + * @var object + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Stores the Input Object + * @var object + * @since 4.0.0 + */ + private $cliInput; + + /** + * The username + * + * @var string + * + * @since 4.0.0 + */ + private $user; + + /** + * The password + * + * @var string + * + * @since 4.0.0 + */ + private $password; + + /** + * The name + * + * @var string + * + * @since 4.0.0 + */ + private $name; + + /** + * The email address + * + * @var string + * + * @since 4.0.0 + */ + private $email; + + /** + * The usergroups + * + * @var array + * + * @since 4.0.0 + */ + private $userGroups = []; + + /** + * Command constructor. + * + * @param DatabaseInterface $db The database + * + * @since 4.2.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $this->ioStyle->title('Add user'); + $this->user = $this->getStringFromOption('username', 'Please enter a username'); + $this->name = $this->getStringFromOption('name', 'Please enter a name (full name of user)'); + $this->email = $this->getStringFromOption('email', 'Please enter an email address'); + $this->password = $this->getStringFromOption('password', 'Please enter a password'); + $this->userGroups = $this->getUserGroups(); + + if (\in_array("error", $this->userGroups)) { + $this->ioStyle->error("'" . $this->userGroups[1] . "' user group doesn't exist!"); + + return Command::FAILURE; + } + + // Get filter to remove invalid characters + $filter = new InputFilter(); + + $user['username'] = $filter->clean($this->user, 'USERNAME'); + $user['password'] = $this->password; + $user['name'] = $filter->clean($this->name, 'STRING'); + $user['email'] = $this->email; + $user['groups'] = $this->userGroups; + + $userObj = User::getInstance(); + $userObj->bind($user); + + if (!$userObj->save()) { + switch ($userObj->getError()) { + case "JLIB_DATABASE_ERROR_USERNAME_INUSE": + $this->ioStyle->error("The username already exists!"); + break; + case "JLIB_DATABASE_ERROR_EMAIL_INUSE": + $this->ioStyle->error("The email address already exists!"); + break; + case "JLIB_DATABASE_ERROR_VALID_MAIL": + $this->ioStyle->error("The email address is invalid!"); + break; + } + + return 1; + } + + $this->ioStyle->success("User created!"); + + return Command::SUCCESS; + } + + /** + * Method to get groupId by groupName + * + * @param string $groupName name of group + * + * @return integer + * + * @since 4.0.0 + */ + protected function getGroupId($groupName) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('title') . ' = :groupName') + ->bind(':groupName', $groupName); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Method to get a value from option + * + * @param string $option set the option name + * @param string $question set the question if user enters no value to option + * + * @return string + * + * @since 4.0.0 + */ + public function getStringFromOption($option, $question): string + { + $answer = (string) $this->cliInput->getOption($option); + + while (!$answer) { + if ($option === 'password') { + $answer = (string) $this->ioStyle->askHidden($question); + } else { + $answer = (string) $this->ioStyle->ask($question); + } + } + + return $answer; + } + + /** + * Method to get a value from option + * + * @return array + * + * @since 4.0.0 + */ + protected function getUserGroups(): array + { + $groups = $this->getApplication()->getConsoleInput()->getOption('usergroup'); + $db = $this->getDatabase(); + + $groupList = []; + + // Group names have been supplied as input arguments + if (!\is_null($groups) && $groups[0]) { + $groups = explode(',', $groups); + + foreach ($groups as $group) { + $groupId = $this->getGroupId($group); + + if (empty($groupId)) { + $this->ioStyle->error("Invalid group name '" . $group . "'"); + throw new InvalidOptionException("Invalid group name " . $group); + } + + $groupList[] = $this->getGroupId($group); + } + + return $groupList; + } + + // Generate select list for user + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__usergroups')) + ->order($db->quoteName('id') . 'ASC'); + $db->setQuery($query); + + $list = $db->loadColumn(); + + $choice = new ChoiceQuestion( + 'Please select a usergroup (separate multiple groups with a comma)', + $list + ); + $choice->setMultiselect(true); + + $answer = (array) $this->ioStyle->askQuestion($choice); + + foreach ($answer as $group) { + $groupList[] = $this->getGroupId($group); + } + + return $groupList; + } + + /** + * Configure the IO. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return void + * + * @since 4.0.0 + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% will add a user \nUsage: php %command.full_name%"; - $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); - $this->addOption('name', null, InputOption::VALUE_OPTIONAL, 'full name of user'); - $this->addOption('password', null, InputOption::VALUE_OPTIONAL, 'password'); - $this->addOption('email', null, InputOption::VALUE_OPTIONAL, 'email address'); - $this->addOption('usergroup', null, InputOption::VALUE_OPTIONAL, 'usergroup (separate multiple groups with comma ",")'); - $this->setDescription('Add a user'); - $this->setHelp($help); - } + $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); + $this->addOption('name', null, InputOption::VALUE_OPTIONAL, 'full name of user'); + $this->addOption('password', null, InputOption::VALUE_OPTIONAL, 'password'); + $this->addOption('email', null, InputOption::VALUE_OPTIONAL, 'email address'); + $this->addOption('usergroup', null, InputOption::VALUE_OPTIONAL, 'usergroup (separate multiple groups with comma ",")'); + $this->setDescription('Add a user'); + $this->setHelp($help); + } } diff --git a/libraries/src/Console/AddUserToGroupCommand.php b/libraries/src/Console/AddUserToGroupCommand.php index 07204d935b033..da2ecb382fc3d 100644 --- a/libraries/src/Console/AddUserToGroupCommand.php +++ b/libraries/src/Console/AddUserToGroupCommand.php @@ -1,4 +1,5 @@ setDatabase($db); - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - $this->username = $this->getStringFromOption('username', 'Please enter a username'); - $this->ioStyle->title('Add user to group'); - - $userId = $this->getUserId($this->username); - - if (empty($userId)) - { - $this->ioStyle->error("The user " . $this->username . " does not exist!"); - - return Command::FAILURE; - } - - // Fetch user - $user = User::getInstance($userId); - - $this->userGroups = $this->getGroups($user); - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('id') . ' = :userGroup'); - - foreach ($this->userGroups as $userGroup) - { - $query->bind(':userGroup', $userGroup); - $db->setQuery($query); - - $result = $db->loadResult(); - - if (UserHelper::addUserToGroup($user->id, $userGroup)) - { - $this->ioStyle->success("Added '" . $user->username . "' to group '" . $result . "'!"); - } - else - { - $this->ioStyle->error("Can't add '" . $user->username . "' to group '" . $result . "'!"); - - return Command::FAILURE; - } - } - - return Command::SUCCESS; - } - - - /** - * Method to get a value from option - * - * @param User $user a UserInstance - * - * @return array - * - * @since 4.0.0 - */ - protected function getGroups($user): array - { - $groups = $this->getApplication()->getConsoleInput()->getOption('group'); - - $db = $this->getDatabase(); - - $groupList = []; - - // Group names have been supplied as input arguments - if ($groups) - { - $groups = explode(',', $groups); - - foreach ($groups as $group) - { - $groupId = $this->getGroupId($group); - - if (empty($groupId)) - { - $this->ioStyle->error("Invalid group name '" . $group . "'"); - throw new InvalidOptionException("Invalid group name " . $group); - } - - $groupList[] = $this->getGroupId($group); - } - - return $groupList; - } - - $userGroups = Access::getGroupsByUser($user->id, false); - - // Generate select list for user - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__usergroups')) - ->whereNotIn($db->quoteName('id'), $userGroups) - ->order($db->quoteName('id') . ' ASC'); - $db->setQuery($query); - - $list = $db->loadColumn(); - - $choice = new ChoiceQuestion( - 'Please select a usergroup (separate multiple groups with a comma)', - $list - ); - $choice->setMultiselect(true); - - $answer = (array) $this->ioStyle->askQuestion($choice); - - foreach ($answer as $group) - { - $groupList[] = $this->getGroupId($group); - } - - return $groupList; - } - - /** - * Method to get groupId by groupName - * - * @param string $groupName name of group - * - * @return integer - * - * @since 4.0.0 - */ - protected function getGroupId($groupName) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('title') . '= :groupName') - ->bind(':groupName', $groupName); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Method to get a user object - * - * @param string $username username - * - * @return object - * - * @since 4.0.0 - */ - protected function getUserId($username) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('username') . '= :username') - ->bind(':username', $username); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Method to get a value from option - * - * @param string $option set the option name - * - * @param string $question set the question if user enters no value to option - * - * @return string - * - * @since 4.0.0 - */ - protected function getStringFromOption($option, $question): string - { - $answer = (string) $this->getApplication()->getConsoleInput()->getOption($option); - - while (!$answer) - { - $answer = (string) $this->ioStyle->ask($question); - } - - return $answer; - } - - /** - * Configure the IO. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return void - * - * @since 4.0.0 - */ - private function configureIO(InputInterface $input, OutputInterface $output) - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% adds a user to a group + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'user:addtogroup'; + + /** + * SymfonyStyle Object + * @var object + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Stores the Input Object + * @var object + * @since 4.0.0 + */ + private $cliInput; + + /** + * The username + * + * @var string + * + * @since 4.0.0 + */ + private $username; + + /** + * The usergroups + * + * @var array + * + * @since 4.0.0 + */ + private $userGroups = []; + + /** + * Command constructor. + * + * @param DatabaseInterface $db The database + * + * @since 4.2.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $this->username = $this->getStringFromOption('username', 'Please enter a username'); + $this->ioStyle->title('Add user to group'); + + $userId = $this->getUserId($this->username); + + if (empty($userId)) { + $this->ioStyle->error("The user " . $this->username . " does not exist!"); + + return Command::FAILURE; + } + + // Fetch user + $user = User::getInstance($userId); + + $this->userGroups = $this->getGroups($user); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('id') . ' = :userGroup'); + + foreach ($this->userGroups as $userGroup) { + $query->bind(':userGroup', $userGroup); + $db->setQuery($query); + + $result = $db->loadResult(); + + if (UserHelper::addUserToGroup($user->id, $userGroup)) { + $this->ioStyle->success("Added '" . $user->username . "' to group '" . $result . "'!"); + } else { + $this->ioStyle->error("Can't add '" . $user->username . "' to group '" . $result . "'!"); + + return Command::FAILURE; + } + } + + return Command::SUCCESS; + } + + + /** + * Method to get a value from option + * + * @param User $user a UserInstance + * + * @return array + * + * @since 4.0.0 + */ + protected function getGroups($user): array + { + $groups = $this->getApplication()->getConsoleInput()->getOption('group'); + + $db = $this->getDatabase(); + + $groupList = []; + + // Group names have been supplied as input arguments + if ($groups) { + $groups = explode(',', $groups); + + foreach ($groups as $group) { + $groupId = $this->getGroupId($group); + + if (empty($groupId)) { + $this->ioStyle->error("Invalid group name '" . $group . "'"); + throw new InvalidOptionException("Invalid group name " . $group); + } + + $groupList[] = $this->getGroupId($group); + } + + return $groupList; + } + + $userGroups = Access::getGroupsByUser($user->id, false); + + // Generate select list for user + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__usergroups')) + ->whereNotIn($db->quoteName('id'), $userGroups) + ->order($db->quoteName('id') . ' ASC'); + $db->setQuery($query); + + $list = $db->loadColumn(); + + $choice = new ChoiceQuestion( + 'Please select a usergroup (separate multiple groups with a comma)', + $list + ); + $choice->setMultiselect(true); + + $answer = (array) $this->ioStyle->askQuestion($choice); + + foreach ($answer as $group) { + $groupList[] = $this->getGroupId($group); + } + + return $groupList; + } + + /** + * Method to get groupId by groupName + * + * @param string $groupName name of group + * + * @return integer + * + * @since 4.0.0 + */ + protected function getGroupId($groupName) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('title') . '= :groupName') + ->bind(':groupName', $groupName); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Method to get a user object + * + * @param string $username username + * + * @return object + * + * @since 4.0.0 + */ + protected function getUserId($username) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('username') . '= :username') + ->bind(':username', $username); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Method to get a value from option + * + * @param string $option set the option name + * + * @param string $question set the question if user enters no value to option + * + * @return string + * + * @since 4.0.0 + */ + protected function getStringFromOption($option, $question): string + { + $answer = (string) $this->getApplication()->getConsoleInput()->getOption($option); + + while (!$answer) { + $answer = (string) $this->ioStyle->ask($question); + } + + return $answer; + } + + /** + * Configure the IO. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return void + * + * @since 4.0.0 + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% adds a user to a group \nUsage: php %command.full_name%"; - $this->setDescription('Add a user to a group'); - $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); - $this->addOption('group', null, InputOption::VALUE_OPTIONAL, 'group'); - $this->setHelp($help); - } + $this->setDescription('Add a user to a group'); + $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); + $this->addOption('group', null, InputOption::VALUE_OPTIONAL, 'group'); + $this->setHelp($help); + } } diff --git a/libraries/src/Console/ChangeUserPasswordCommand.php b/libraries/src/Console/ChangeUserPasswordCommand.php index f556932465be6..adb337f282531 100644 --- a/libraries/src/Console/ChangeUserPasswordCommand.php +++ b/libraries/src/Console/ChangeUserPasswordCommand.php @@ -1,4 +1,5 @@ configureIO($input, $output); - $this->username = $this->getStringFromOption('username', 'Please enter a username'); - $this->ioStyle->title('Change password'); - - $userId = UserHelper::getUserId($this->username); - - if (empty($userId)) - { - $this->ioStyle->error("The user " . $this->username . " does not exist!"); - - return Command::FAILURE; - } - - $user = User::getInstance($userId); - $this->password = $this->getStringFromOption('password', 'Please enter a new password'); - - $user->password = UserHelper::hashPassword($this->password); - - if (!$user->save(true)) - { - $this->ioStyle->error($user->getError()); - - return Command::FAILURE; - } - - $this->ioStyle->success("Password changed!"); - - return Command::SUCCESS; - } - - /** - * Method to get a value from option - * - * @param string $option set the option name - * - * @param string $question set the question if user enters no value to option - * - * @return string - * - * @since 4.0.0 - */ - protected function getStringFromOption($option, $question): string - { - $answer = (string) $this->cliInput->getOption($option); - - while (!$answer) - { - if ($option === 'password') - { - $answer = (string) $this->ioStyle->askHidden($question); - } - else - { - $answer = (string) $this->ioStyle->ask($question); - } - } - - return $answer; - } - - /** - * Configure the IO. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return void - * - * @since 4.0.0 - */ - private function configureIO(InputInterface $input, OutputInterface $output) - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% will change a user's password + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'user:reset-password'; + + /** + * SymfonyStyle Object + * @var object + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Stores the Input Object + * @var object + * @since 4.0.0 + */ + private $cliInput; + + /** + * The username + * + * @var string + * + * @since 4.0.0 + */ + private $username; + + /** + * The password + * + * @var string + * + * @since 4.0.0 + */ + private $password; + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $this->username = $this->getStringFromOption('username', 'Please enter a username'); + $this->ioStyle->title('Change password'); + + $userId = UserHelper::getUserId($this->username); + + if (empty($userId)) { + $this->ioStyle->error("The user " . $this->username . " does not exist!"); + + return Command::FAILURE; + } + + $user = User::getInstance($userId); + $this->password = $this->getStringFromOption('password', 'Please enter a new password'); + + $user->password = UserHelper::hashPassword($this->password); + + if (!$user->save(true)) { + $this->ioStyle->error($user->getError()); + + return Command::FAILURE; + } + + $this->ioStyle->success("Password changed!"); + + return Command::SUCCESS; + } + + /** + * Method to get a value from option + * + * @param string $option set the option name + * + * @param string $question set the question if user enters no value to option + * + * @return string + * + * @since 4.0.0 + */ + protected function getStringFromOption($option, $question): string + { + $answer = (string) $this->cliInput->getOption($option); + + while (!$answer) { + if ($option === 'password') { + $answer = (string) $this->ioStyle->askHidden($question); + } else { + $answer = (string) $this->ioStyle->ask($question); + } + } + + return $answer; + } + + /** + * Configure the IO. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return void + * + * @since 4.0.0 + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% will change a user's password \nUsage: php %command.full_name%"; - $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); - $this->addOption('password', null, InputOption::VALUE_OPTIONAL, 'password'); - $this->setDescription("Change a user's password"); - $this->setHelp($help); - } + $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); + $this->addOption('password', null, InputOption::VALUE_OPTIONAL, 'password'); + $this->setDescription("Change a user's password"); + $this->setHelp($help); + } } diff --git a/libraries/src/Console/CheckJoomlaUpdatesCommand.php b/libraries/src/Console/CheckJoomlaUpdatesCommand.php index cc1661a9a1af0..b304030d4a692 100644 --- a/libraries/src/Console/CheckJoomlaUpdatesCommand.php +++ b/libraries/src/Console/CheckJoomlaUpdatesCommand.php @@ -1,4 +1,5 @@ %command.name% will check for Joomla updates + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'core:check-updates'; + + /** + * Stores the Update Information + * + * @var UpdateModel + * @since 4.0.0 + */ + private $updateInfo; + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% will check for Joomla updates \nUsage: php %command.full_name%"; - $this->setDescription('Check for Joomla updates'); - $this->setHelp($help); - } - - /** - * Retrieves Update Information - * - * @return mixed - * - * @since 4.0.0 - */ - private function getUpdateInformationFromModel() - { - $app = $this->getApplication(); - $updatemodel = $app->bootComponent('com_joomlaupdate')->getMVCFactory($app)->createModel('Update', 'Administrator'); - $updatemodel->purge(); - $updatemodel->refreshUpdates(true); - - return $updatemodel; - } - - /** - * Gets the Update Information - * - * @return mixed - * - * @since 4.0.0 - */ - public function getUpdateInfo() - { - if (!$this->updateInfo) - { - $this->setUpdateInfo(); - } - - return $this->updateInfo; - } - - /** - * Sets the Update Information - * - * @param null $info stores update Information - * - * @return void - * - * @since 4.0.0 - */ - public function setUpdateInfo($info = null): void - { - if (!$info) - { - $this->updateInfo = $this->getUpdateInformationFromModel(); - } - else - { - $this->updateInfo = $info; - } - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $symfonyStyle = new SymfonyStyle($input, $output); - - $model = $this->getUpdateInfo(); - $data = $model->getUpdateInformation(); - $symfonyStyle->title('Joomla! Updates'); - - if (!$data['hasUpdate']) - { - $symfonyStyle->success('You already have the latest Joomla version ' . $data['latest']); - - return Command::SUCCESS; - } - - $symfonyStyle->note('New Joomla Version ' . $data['latest'] . ' is available.'); - - if (!isset($data['object']->downloadurl->_data)) - { - $symfonyStyle->warning('We cannot find an update URL'); - } - - return Command::SUCCESS; - } + $this->setDescription('Check for Joomla updates'); + $this->setHelp($help); + } + + /** + * Retrieves Update Information + * + * @return mixed + * + * @since 4.0.0 + */ + private function getUpdateInformationFromModel() + { + $app = $this->getApplication(); + $updatemodel = $app->bootComponent('com_joomlaupdate')->getMVCFactory($app)->createModel('Update', 'Administrator'); + $updatemodel->purge(); + $updatemodel->refreshUpdates(true); + + return $updatemodel; + } + + /** + * Gets the Update Information + * + * @return mixed + * + * @since 4.0.0 + */ + public function getUpdateInfo() + { + if (!$this->updateInfo) { + $this->setUpdateInfo(); + } + + return $this->updateInfo; + } + + /** + * Sets the Update Information + * + * @param null $info stores update Information + * + * @return void + * + * @since 4.0.0 + */ + public function setUpdateInfo($info = null): void + { + if (!$info) { + $this->updateInfo = $this->getUpdateInformationFromModel(); + } else { + $this->updateInfo = $info; + } + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $symfonyStyle = new SymfonyStyle($input, $output); + + $model = $this->getUpdateInfo(); + $data = $model->getUpdateInformation(); + $symfonyStyle->title('Joomla! Updates'); + + if (!$data['hasUpdate']) { + $symfonyStyle->success('You already have the latest Joomla version ' . $data['latest']); + + return Command::SUCCESS; + } + + $symfonyStyle->note('New Joomla Version ' . $data['latest'] . ' is available.'); + + if (!isset($data['object']->downloadurl->_data)) { + $symfonyStyle->warning('We cannot find an update URL'); + } + + return Command::SUCCESS; + } } diff --git a/libraries/src/Console/CheckUpdatesCommand.php b/libraries/src/Console/CheckUpdatesCommand.php index a1948642eb4d8..2d70f65a84cfe 100644 --- a/libraries/src/Console/CheckUpdatesCommand.php +++ b/libraries/src/Console/CheckUpdatesCommand.php @@ -1,4 +1,5 @@ title('Fetching Extension Updates'); + $symfonyStyle->title('Fetching Extension Updates'); - // Get the update cache time - $component = ComponentHelper::getComponent('com_installer'); + // Get the update cache time + $component = ComponentHelper::getComponent('com_installer'); - $cache_timeout = 3600 * (int) $component->getParams()->get('cachetimeout', 6); + $cache_timeout = 3600 * (int) $component->getParams()->get('cachetimeout', 6); - // Find all updates - $ret = Updater::getInstance()->findUpdates(0, $cache_timeout); + // Find all updates + $ret = Updater::getInstance()->findUpdates(0, $cache_timeout); - if ($ret) - { - $symfonyStyle->note('There are available updates to apply'); - $symfonyStyle->success('Check complete.'); - } - else - { - $symfonyStyle->success('There are no available updates'); - } + if ($ret) { + $symfonyStyle->note('There are available updates to apply'); + $symfonyStyle->success('Check complete.'); + } else { + $symfonyStyle->success('There are no available updates'); + } - return Command::SUCCESS; - } + return Command::SUCCESS; + } - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% command checks for pending extension updates + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% command checks for pending extension updates \nUsage: php %command.full_name%"; - $this->setDescription('Check for pending extension updates'); - $this->setHelp($help); - } + $this->setDescription('Check for pending extension updates'); + $this->setHelp($help); + } } diff --git a/libraries/src/Console/CleanCacheCommand.php b/libraries/src/Console/CleanCacheCommand.php index 55fd1e3aac439..32c10f6af915d 100644 --- a/libraries/src/Console/CleanCacheCommand.php +++ b/libraries/src/Console/CleanCacheCommand.php @@ -1,4 +1,5 @@ title('Cleaning System Cache'); - - $cache = $this->getApplication()->bootComponent('com_cache')->getMVCFactory(); - /** @var Joomla\Component\Cache\Administrator\Model\CacheModel $model */ - $model = $cache->createModel('Cache', 'Administrator', ['ignore_request' => true]); - - if ($input->getArgument('expired')) - { - if (!$model->purge()) - { - $symfonyStyle->error('Expired Cache not cleaned'); - - return Command::FAILURE; - } - - $symfonyStyle->success('Expired Cache cleaned'); - - return Command::SUCCESS; - } - - if (!$model->clean()) - { - $symfonyStyle->error('Cache not cleaned'); - - return Command::FAILURE; - } - - $symfonyStyle->success('Cache cleaned'); - - return Command::SUCCESS; - } - - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% will clear entries from the system cache + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'cache:clean'; + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $symfonyStyle = new SymfonyStyle($input, $output); + + $symfonyStyle->title('Cleaning System Cache'); + + $cache = $this->getApplication()->bootComponent('com_cache')->getMVCFactory(); + /** @var Joomla\Component\Cache\Administrator\Model\CacheModel $model */ + $model = $cache->createModel('Cache', 'Administrator', ['ignore_request' => true]); + + if ($input->getArgument('expired')) { + if (!$model->purge()) { + $symfonyStyle->error('Expired Cache not cleaned'); + + return Command::FAILURE; + } + + $symfonyStyle->success('Expired Cache cleaned'); + + return Command::SUCCESS; + } + + if (!$model->clean()) { + $symfonyStyle->error('Cache not cleaned'); + + return Command::FAILURE; + } + + $symfonyStyle->success('Cache cleaned'); + + return Command::SUCCESS; + } + + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% will clear entries from the system cache \nUsage: php %command.full_name%"; - $this->addArgument('expired', InputArgument::OPTIONAL, 'will clear expired entries from the system cache'); - $this->setDescription('Clean cache entries'); - $this->setHelp($help); - } + $this->addArgument('expired', InputArgument::OPTIONAL, 'will clear expired entries from the system cache'); + $this->setDescription('Clean cache entries'); + $this->setHelp($help); + } } diff --git a/libraries/src/Console/DeleteUserCommand.php b/libraries/src/Console/DeleteUserCommand.php index 704a3b299e7a6..55636c8a81c73 100644 --- a/libraries/src/Console/DeleteUserCommand.php +++ b/libraries/src/Console/DeleteUserCommand.php @@ -1,4 +1,5 @@ setDatabase($db); - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - - $this->ioStyle->title('Delete users'); - - $this->username = $this->getStringFromOption('username', 'Please enter a username'); - - $userId = UserHelper::getUserId($this->username); - $db = $this->getDatabase(); - - if (empty($userId)) - { - $this->ioStyle->error($this->username . ' does not exist!'); - - return Command::FAILURE; - } - - if ($input->isInteractive() && !$this->ioStyle->confirm('Are you sure you want to delete this user?', false)) - { - $this->ioStyle->note('User not deleted'); - - return Command::SUCCESS; - } - - $groups = UserHelper::getUserGroups($userId); - $user = User::getInstance($userId); - - if ($user->block == 0) - { - foreach ($groups as $groupId) - { - if (Access::checkGroup($groupId, 'core.admin')) - { - $queryUser = $db->getQuery(true); - $queryUser->select('COUNT(*)') - ->from($db->quoteName('#__users', 'u')) - ->leftJoin( - $db->quoteName('#__user_usergroup_map', 'g'), - '(' . $db->quoteName('u.id') . ' = ' . $db->quoteName('g.user_id') . ')' - ) - ->where($db->quoteName('g.group_id') . " = :groupId") - ->where($db->quoteName('u.block') . " = 0") - ->bind(':groupId', $groupId, ParameterType::INTEGER); - - $db->setQuery($queryUser); - $activeSuperUser = $db->loadResult(); - - if ($activeSuperUser < 2) - { - $this->ioStyle->error("You can't delete the last active Super User"); - - return Command::FAILURE; - } - } - } - } - - // Trigger delete of user - $result = $user->delete(); - - if (!$result) - { - $this->ioStyle->error("Can't remove " . $this->username . ' from usertable'); - - return Command::FAILURE; - } - - $this->ioStyle->success('User ' . $this->username . ' deleted!'); - - return Command::SUCCESS; - } - - /** - * Method to get a value from option - * - * @param string $option set the option name - * - * @param string $question set the question if user enters no value to option - * - * @return string - * - * @since 4.0.0 - */ - protected function getStringFromOption($option, $question): string - { - $answer = (string) $this->getApplication()->getConsoleInput()->getOption($option); - - while (!$answer) - { - $answer = (string) $this->ioStyle->ask($question); - } - - return $answer; - } - - /** - * Configure the IO. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return void - * - * @since 4.0.0 - */ - private function configureIO(InputInterface $input, OutputInterface $output) - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% deletes a user + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'user:delete'; + + /** + * SymfonyStyle Object + * @var object + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Stores the Input Object + * @var object + * @since 4.0.0 + */ + private $cliInput; + + /** + * The username + * + * @var string + * + * @since 4.0.0 + */ + private $username; + + /** + * Command constructor. + * + * @param DatabaseInterface $db The database + * + * @since 4.2.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + + $this->ioStyle->title('Delete users'); + + $this->username = $this->getStringFromOption('username', 'Please enter a username'); + + $userId = UserHelper::getUserId($this->username); + $db = $this->getDatabase(); + + if (empty($userId)) { + $this->ioStyle->error($this->username . ' does not exist!'); + + return Command::FAILURE; + } + + if ($input->isInteractive() && !$this->ioStyle->confirm('Are you sure you want to delete this user?', false)) { + $this->ioStyle->note('User not deleted'); + + return Command::SUCCESS; + } + + $groups = UserHelper::getUserGroups($userId); + $user = User::getInstance($userId); + + if ($user->block == 0) { + foreach ($groups as $groupId) { + if (Access::checkGroup($groupId, 'core.admin')) { + $queryUser = $db->getQuery(true); + $queryUser->select('COUNT(*)') + ->from($db->quoteName('#__users', 'u')) + ->leftJoin( + $db->quoteName('#__user_usergroup_map', 'g'), + '(' . $db->quoteName('u.id') . ' = ' . $db->quoteName('g.user_id') . ')' + ) + ->where($db->quoteName('g.group_id') . " = :groupId") + ->where($db->quoteName('u.block') . " = 0") + ->bind(':groupId', $groupId, ParameterType::INTEGER); + + $db->setQuery($queryUser); + $activeSuperUser = $db->loadResult(); + + if ($activeSuperUser < 2) { + $this->ioStyle->error("You can't delete the last active Super User"); + + return Command::FAILURE; + } + } + } + } + + // Trigger delete of user + $result = $user->delete(); + + if (!$result) { + $this->ioStyle->error("Can't remove " . $this->username . ' from usertable'); + + return Command::FAILURE; + } + + $this->ioStyle->success('User ' . $this->username . ' deleted!'); + + return Command::SUCCESS; + } + + /** + * Method to get a value from option + * + * @param string $option set the option name + * + * @param string $question set the question if user enters no value to option + * + * @return string + * + * @since 4.0.0 + */ + protected function getStringFromOption($option, $question): string + { + $answer = (string) $this->getApplication()->getConsoleInput()->getOption($option); + + while (!$answer) { + $answer = (string) $this->ioStyle->ask($question); + } + + return $answer; + } + + /** + * Configure the IO. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return void + * + * @since 4.0.0 + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% deletes a user \nUsage: php %command.full_name%"; - $this->setDescription('Delete a user'); - $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); - $this->setHelp($help); - } + $this->setDescription('Delete a user'); + $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); + $this->setHelp($help); + } } diff --git a/libraries/src/Console/ExtensionDiscoverCommand.php b/libraries/src/Console/ExtensionDiscoverCommand.php index 61f488b4653b5..38e605bfc6702 100644 --- a/libraries/src/Console/ExtensionDiscoverCommand.php +++ b/libraries/src/Console/ExtensionDiscoverCommand.php @@ -1,4 +1,5 @@ cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% is used to discover extensions + /** + * The default command name + * + * @var string + * + * @since 4.0.0 + */ + protected static $defaultName = 'extension:discover'; + + /** + * Stores the Input Object + * + * @var InputInterface + * + * @since 4.0.0 + */ + private $cliInput; + + /** + * SymfonyStyle Object + * + * @var SymfonyStyle + * + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% is used to discover extensions \nUsage: \n php %command.full_name%"; - $this->setDescription('Discover extensions'); - $this->setHelp($help); - } - - /** - * Used for discovering extensions - * - * @return integer The count of discovered extensions - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function processDiscover(): int - { - $app = $this->getApplication(); - - $mvcFactory = $app->bootComponent('com_installer')->getMVCFactory(); - - $model = $mvcFactory->createModel('Discover', 'Administrator'); - - return $model->discover(); - } - - /** - * Used for finding the text for the note - * - * @param int $count The count of installed Extensions - * - * @return string The text for the note - * - * @since 4.0.0 - */ - public function getNote(int $count): string - { - if ($count < 1) - { - return 'No extensions were discovered.'; - } - elseif ($count === 1) - { - return $count . ' extension has been discovered.'; - } - else - { - return $count . ' extensions have been discovered.'; - } - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - - $count = $this->processDiscover(); - - $this->ioStyle->note($this->getNote($count)); - - return Command::SUCCESS; - } + $this->setDescription('Discover extensions'); + $this->setHelp($help); + } + + /** + * Used for discovering extensions + * + * @return integer The count of discovered extensions + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function processDiscover(): int + { + $app = $this->getApplication(); + + $mvcFactory = $app->bootComponent('com_installer')->getMVCFactory(); + + $model = $mvcFactory->createModel('Discover', 'Administrator'); + + return $model->discover(); + } + + /** + * Used for finding the text for the note + * + * @param int $count The count of installed Extensions + * + * @return string The text for the note + * + * @since 4.0.0 + */ + public function getNote(int $count): string + { + if ($count < 1) { + return 'No extensions were discovered.'; + } elseif ($count === 1) { + return $count . ' extension has been discovered.'; + } else { + return $count . ' extensions have been discovered.'; + } + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + + $count = $this->processDiscover(); + + $this->ioStyle->note($this->getNote($count)); + + return Command::SUCCESS; + } } diff --git a/libraries/src/Console/ExtensionDiscoverInstallCommand.php b/libraries/src/Console/ExtensionDiscoverInstallCommand.php index 7d03ef5944c4f..1e31d69e6a132 100644 --- a/libraries/src/Console/ExtensionDiscoverInstallCommand.php +++ b/libraries/src/Console/ExtensionDiscoverInstallCommand.php @@ -1,4 +1,5 @@ setDatabase($db); - } - - /** - * Configures the IO - * - * @param InputInterface $input Console Input - * @param OutputInterface $output Console Output - * - * @return void - * - * @since 4.0.0 - * - */ - private function configureIO(InputInterface $input, OutputInterface $output): void - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $this->addOption('eid', null, InputOption::VALUE_REQUIRED, 'The ID of the extension to discover'); - - $help = "%command.name% is used to discover extensions + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'extension:discover:install'; + + /** + * Stores the Input Object + * + * @var InputInterface + * @since 4.0.0 + */ + private $cliInput; + + /** + * SymfonyStyle Object + * + * @var SymfonyStyle + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Instantiate the command. + * + * @param DatabaseInterface $db Database connector + * + * @since 4.0.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $this->addOption('eid', null, InputOption::VALUE_REQUIRED, 'The ID of the extension to discover'); + + $help = "%command.name% is used to discover extensions \nYou can provide the following option to the command: \n --eid: The ID of the extension \n If you do not provide a ID all discovered extensions are installed. \nUsage: \n php %command.full_name% --eid="; - $this->setDescription('Install discovered extensions'); - $this->setHelp($help); - } - - /** - * Used for discovering extensions - * - * @param string $eid Id of the extension - * - * @return integer The count of installed extensions - * - * @throws \Exception - * @since 4.0.0 - */ - public function processDiscover($eid): int - { - $jInstaller = new Installer; - $jInstaller->setDatabase($this->db); - $count = 0; - - if ($eid === -1) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['extension_id'])) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('state') . ' = -1'); - $db->setQuery($query); - $eidsToDiscover = $db->loadObjectList(); - - foreach ($eidsToDiscover as $eidToDiscover) - { - if (!$jInstaller->discover_install($eidToDiscover->extension_id)) - { - return -1; - } - - $count++; - } - - if (empty($eidsToDiscover)) - { - return 0; - } - } - else - { - if ($jInstaller->discover_install($eid)) - { - return 1; - } - else - { - return -1; - } - } - - return $count; - } - - /** - * Used for finding the text for the note - * - * @param int $count Number of extensions to install - * @param int $eid ID of the extension or -1 if no special - * - * @return string The text for the note - * - * @since 4.0.0 - */ - public function getNote(int $count, int $eid): string - { - if ($count < 0 && $eid >= 0) - { - return 'Unable to install the extension with ID ' . $eid; - } - elseif ($count < 0 && $eid < 0) - { - return 'Unable to install discovered extensions.'; - } - elseif ($count === 0) - { - return 'There are no pending discovered extensions for install. Perhaps you need to run extension:discover first?'; - } - elseif ($count === 1 && $eid > 0) - { - return 'Extension with ID ' . $eid . ' installed successfully.'; - } - elseif ($count === 1 && $eid < 0) - { - return $count . ' discovered extension has been installed.'; - } - elseif ($count > 1 && $eid < 0) - { - return $count . ' discovered extensions have been installed.'; - } - else - { - return 'The return value is not possible and has to be checked.'; - } - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - - if ($eid = $this->cliInput->getOption('eid')) - { - $result = $this->processDiscover($eid); - - if ($result === -1) - { - $this->ioStyle->error($this->getNote($result, $eid)); - - return Command::FAILURE; - } - else - { - $this->ioStyle->success($this->getNote($result, $eid)); - - return Command::SUCCESS; - } - } - else - { - $result = $this->processDiscover(-1); - - if ($result < 0) - { - $this->ioStyle->error($this->getNote($result, -1)); - - return Command::FAILURE; - } - elseif ($result === 0) - { - $this->ioStyle->note($this->getNote($result, -1)); - - return Command::SUCCESS; - } - - else - { - $this->ioStyle->note($this->getNote($result, -1)); - - return Command::SUCCESS; - } - } - } + $this->setDescription('Install discovered extensions'); + $this->setHelp($help); + } + + /** + * Used for discovering extensions + * + * @param string $eid Id of the extension + * + * @return integer The count of installed extensions + * + * @throws \Exception + * @since 4.0.0 + */ + public function processDiscover($eid): int + { + $jInstaller = new Installer(); + $jInstaller->setDatabase($this->db); + $count = 0; + + if ($eid === -1) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['extension_id'])) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('state') . ' = -1'); + $db->setQuery($query); + $eidsToDiscover = $db->loadObjectList(); + + foreach ($eidsToDiscover as $eidToDiscover) { + if (!$jInstaller->discover_install($eidToDiscover->extension_id)) { + return -1; + } + + $count++; + } + + if (empty($eidsToDiscover)) { + return 0; + } + } else { + if ($jInstaller->discover_install($eid)) { + return 1; + } else { + return -1; + } + } + + return $count; + } + + /** + * Used for finding the text for the note + * + * @param int $count Number of extensions to install + * @param int $eid ID of the extension or -1 if no special + * + * @return string The text for the note + * + * @since 4.0.0 + */ + public function getNote(int $count, int $eid): string + { + if ($count < 0 && $eid >= 0) { + return 'Unable to install the extension with ID ' . $eid; + } elseif ($count < 0 && $eid < 0) { + return 'Unable to install discovered extensions.'; + } elseif ($count === 0) { + return 'There are no pending discovered extensions for install. Perhaps you need to run extension:discover first?'; + } elseif ($count === 1 && $eid > 0) { + return 'Extension with ID ' . $eid . ' installed successfully.'; + } elseif ($count === 1 && $eid < 0) { + return $count . ' discovered extension has been installed.'; + } elseif ($count > 1 && $eid < 0) { + return $count . ' discovered extensions have been installed.'; + } else { + return 'The return value is not possible and has to be checked.'; + } + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + + if ($eid = $this->cliInput->getOption('eid')) { + $result = $this->processDiscover($eid); + + if ($result === -1) { + $this->ioStyle->error($this->getNote($result, $eid)); + + return Command::FAILURE; + } else { + $this->ioStyle->success($this->getNote($result, $eid)); + + return Command::SUCCESS; + } + } else { + $result = $this->processDiscover(-1); + + if ($result < 0) { + $this->ioStyle->error($this->getNote($result, -1)); + + return Command::FAILURE; + } elseif ($result === 0) { + $this->ioStyle->note($this->getNote($result, -1)); + + return Command::SUCCESS; + } else { + $this->ioStyle->note($this->getNote($result, -1)); + + return Command::SUCCESS; + } + } + } } diff --git a/libraries/src/Console/ExtensionDiscoverListCommand.php b/libraries/src/Console/ExtensionDiscoverListCommand.php index 9c5edaecdaebe..f8c5a410cfb4b 100644 --- a/libraries/src/Console/ExtensionDiscoverListCommand.php +++ b/libraries/src/Console/ExtensionDiscoverListCommand.php @@ -1,4 +1,5 @@ %command.name% is used to list all extensions that could be installed via discoverinstall + /** + * The default command name + * + * @var string + * + * @since 4.0.0 + */ + protected static $defaultName = 'extension:discover:list'; + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% is used to list all extensions that could be installed via discoverinstall \nUsage: \n php %command.full_name%"; - $this->setDescription('List discovered extensions'); - $this->setHelp($help); - } - - /** - * Filters the extension state - * - * @param array $extensions The Extensions - * @param string $state The Extension state - * - * @return array - * - * @since 4.0.0 - */ - public function filterExtensionsBasedOnState($extensions, $state): array - { - $filteredExtensions = []; - - foreach ($extensions as $key => $extension) - { - if ($extension['state'] === $state) - { - $filteredExtensions[] = $extension; - } - } - - return $filteredExtensions; - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - - $extensions = $this->getExtensions(); - $state = -1; - - $discovered_extensions = $this->filterExtensionsBasedOnState($extensions, $state); - - if (empty($discovered_extensions)) - { - $this->ioStyle->note("There are no pending discovered extensions to install. Perhaps you need to run extension:discover first?"); - - return Command::SUCCESS; - } - - $discovered_extensions = $this->getExtensionsNameAndId($discovered_extensions); - - $this->ioStyle->title('Discovered extensions.'); - $this->ioStyle->table(['Name', 'Extension ID', 'Version', 'Type', 'Active'], $discovered_extensions); - - return Command::SUCCESS; - } + $this->setDescription('List discovered extensions'); + $this->setHelp($help); + } + + /** + * Filters the extension state + * + * @param array $extensions The Extensions + * @param string $state The Extension state + * + * @return array + * + * @since 4.0.0 + */ + public function filterExtensionsBasedOnState($extensions, $state): array + { + $filteredExtensions = []; + + foreach ($extensions as $key => $extension) { + if ($extension['state'] === $state) { + $filteredExtensions[] = $extension; + } + } + + return $filteredExtensions; + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + + $extensions = $this->getExtensions(); + $state = -1; + + $discovered_extensions = $this->filterExtensionsBasedOnState($extensions, $state); + + if (empty($discovered_extensions)) { + $this->ioStyle->note("There are no pending discovered extensions to install. Perhaps you need to run extension:discover first?"); + + return Command::SUCCESS; + } + + $discovered_extensions = $this->getExtensionsNameAndId($discovered_extensions); + + $this->ioStyle->title('Discovered extensions.'); + $this->ioStyle->table(['Name', 'Extension ID', 'Version', 'Type', 'Active'], $discovered_extensions); + + return Command::SUCCESS; + } } diff --git a/libraries/src/Console/ExtensionInstallCommand.php b/libraries/src/Console/ExtensionInstallCommand.php index cf3758ecdfc53..5434172bd8537 100644 --- a/libraries/src/Console/ExtensionInstallCommand.php +++ b/libraries/src/Console/ExtensionInstallCommand.php @@ -1,4 +1,5 @@ cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $this->addOption('path', null, InputOption::VALUE_REQUIRED, 'The path to the extension'); - $this->addOption('url', null, InputOption::VALUE_REQUIRED, 'The url to the extension'); - - $help = "%command.name% is used to install extensions + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'extension:install'; + + /** + * Stores the Input Object + * @var InputInterface + * @since 4.0.0 + */ + private $cliInput; + + /** + * SymfonyStyle Object + * @var SymfonyStyle + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Exit Code For installation failure + * @since 4.0.0 + */ + public const INSTALLATION_FAILED = 1; + + /** + * Exit Code For installation Success + * @since 4.0.0 + */ + public const INSTALLATION_SUCCESSFUL = 0; + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $this->addOption('path', null, InputOption::VALUE_REQUIRED, 'The path to the extension'); + $this->addOption('url', null, InputOption::VALUE_REQUIRED, 'The url to the extension'); + + $help = "%command.name% is used to install extensions \nYou must provide one of the following options to the command: \n --path: The path on your local filesystem to the install package \n --url: The URL from where the install package should be downloaded @@ -96,127 +97,119 @@ protected function configure(): void \n php %command.full_name% --path= \n php %command.full_name% --url="; - $this->setDescription('Install an extension from a URL or from a path'); - $this->setHelp($help); - } - - /** - * Used for installing extension from a path - * - * @param string $path Path to the extension zip file - * - * @return boolean - * - * @since 4.0.0 - * - * @throws \Exception - */ - public function processPathInstallation($path): bool - { - if (!file_exists($path)) - { - $this->ioStyle->warning('The file path specified does not exist.'); - - return false; - } - - $tmpPath = $this->getApplication()->get('tmp_path'); - $tmpPath = $tmpPath . '/' . basename($path); - $package = InstallerHelper::unpack($path, true); - - if ($package['type'] === false) - { - return false; - } - - $jInstaller = Installer::getInstance(); - $result = $jInstaller->install($package['extractdir']); - InstallerHelper::cleanupInstall($tmpPath, $package['extractdir']); - - return $result; - } - - - /** - * Used for installing extension from a URL - * - * @param string $url URL to the extension zip file - * - * @return boolean - * - * @since 4.0.0 - * - * @throws \Exception - */ - public function processUrlInstallation($url): bool - { - $filename = InstallerHelper::downloadPackage($url); - - $tmpPath = $this->getApplication()->get('tmp_path'); - - $path = $tmpPath . '/' . basename($filename); - $package = InstallerHelper::unpack($path, true); - - if ($package['type'] === false) - { - return false; - } - - $jInstaller = new Installer; - $result = $jInstaller->install($package['extractdir']); - InstallerHelper::cleanupInstall($path, $package['extractdir']); - - return $result; - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @throws \Exception - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - - if ($path = $this->cliInput->getOption('path')) - { - $result = $this->processPathInstallation($path); - - if (!$result) - { - $this->ioStyle->error('Unable to install extension'); - - return self::INSTALLATION_FAILED; - } - - $this->ioStyle->success('Extension installed successfully.'); - - return self::INSTALLATION_SUCCESSFUL; - } - elseif ($url = $this->cliInput->getOption('url')) - { - $result = $this->processUrlInstallation($url); - - if (!$result) - { - $this->ioStyle->error('Unable to install extension'); - - return self::INSTALLATION_FAILED; - } - - $this->ioStyle->success('Extension installed successfully.'); - - return self::INSTALLATION_SUCCESSFUL; - } - - $this->ioStyle->error('Invalid argument supplied for command.'); - - return self::INSTALLATION_FAILED; - } + $this->setDescription('Install an extension from a URL or from a path'); + $this->setHelp($help); + } + + /** + * Used for installing extension from a path + * + * @param string $path Path to the extension zip file + * + * @return boolean + * + * @since 4.0.0 + * + * @throws \Exception + */ + public function processPathInstallation($path): bool + { + if (!file_exists($path)) { + $this->ioStyle->warning('The file path specified does not exist.'); + + return false; + } + + $tmpPath = $this->getApplication()->get('tmp_path'); + $tmpPath = $tmpPath . '/' . basename($path); + $package = InstallerHelper::unpack($path, true); + + if ($package['type'] === false) { + return false; + } + + $jInstaller = Installer::getInstance(); + $result = $jInstaller->install($package['extractdir']); + InstallerHelper::cleanupInstall($tmpPath, $package['extractdir']); + + return $result; + } + + + /** + * Used for installing extension from a URL + * + * @param string $url URL to the extension zip file + * + * @return boolean + * + * @since 4.0.0 + * + * @throws \Exception + */ + public function processUrlInstallation($url): bool + { + $filename = InstallerHelper::downloadPackage($url); + + $tmpPath = $this->getApplication()->get('tmp_path'); + + $path = $tmpPath . '/' . basename($filename); + $package = InstallerHelper::unpack($path, true); + + if ($package['type'] === false) { + return false; + } + + $jInstaller = new Installer(); + $result = $jInstaller->install($package['extractdir']); + InstallerHelper::cleanupInstall($path, $package['extractdir']); + + return $result; + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @throws \Exception + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + + if ($path = $this->cliInput->getOption('path')) { + $result = $this->processPathInstallation($path); + + if (!$result) { + $this->ioStyle->error('Unable to install extension'); + + return self::INSTALLATION_FAILED; + } + + $this->ioStyle->success('Extension installed successfully.'); + + return self::INSTALLATION_SUCCESSFUL; + } elseif ($url = $this->cliInput->getOption('url')) { + $result = $this->processUrlInstallation($url); + + if (!$result) { + $this->ioStyle->error('Unable to install extension'); + + return self::INSTALLATION_FAILED; + } + + $this->ioStyle->success('Extension installed successfully.'); + + return self::INSTALLATION_SUCCESSFUL; + } + + $this->ioStyle->error('Invalid argument supplied for command.'); + + return self::INSTALLATION_FAILED; + } } diff --git a/libraries/src/Console/ExtensionRemoveCommand.php b/libraries/src/Console/ExtensionRemoveCommand.php index 3b0dfe088cb11..1e7fa9868be80 100644 --- a/libraries/src/Console/ExtensionRemoveCommand.php +++ b/libraries/src/Console/ExtensionRemoveCommand.php @@ -1,4 +1,5 @@ setDatabase($db); - } - - /** - * Configures the IO - * - * @param InputInterface $input Console Input - * @param OutputInterface $output Console Output - * - * @return void - * - * @since 4.0.0 - * - */ - private function configureIO(InputInterface $input, OutputInterface $output): void - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - $language = Factory::getLanguage(); - $language->load('', JPATH_ADMINISTRATOR, null, false, false) || - $language->load('', JPATH_ADMINISTRATOR, null, true); - $language->load('com_installer', JPATH_ADMINISTRATOR, null, false, false)|| - $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $this->addArgument( - 'extensionId', - InputArgument::REQUIRED, - 'ID of extension to be removed (run extension:list command to check)' - ); - - $help = "%command.name% is used to uninstall extensions. + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'extension:remove'; + + /** + * @var InputInterface + * @since 4.0.0 + */ + private $cliInput; + + /** + * @var SymfonyStyle + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Exit Code for extensions remove abort + * @since 4.0.0 + */ + public const REMOVE_ABORT = 3; + + /** + * Exit Code for extensions remove failure + * @since 4.0.0 + */ + public const REMOVE_FAILED = 1; + + /** + * Exit Code for invalid response + * @since 4.0.0 + */ + public const REMOVE_INVALID_RESPONSE = 5; + + /** + * Exit Code for invalid type + * @since 4.0.0 + */ + public const REMOVE_INVALID_TYPE = 6; + + /** + * Exit Code for extensions locked remove failure + * @since 4.0.0 + */ + public const REMOVE_LOCKED = 4; + + /** + * Exit Code for extensions not found + * @since 4.0.0 + */ + public const REMOVE_NOT_FOUND = 2; + + /** + * Exit Code for extensions remove success + * @since 4.0.0 + */ + public const REMOVE_SUCCESSFUL = 0; + + /** + * Command constructor. + * + * @param DatabaseInterface $db The database + * + * @since 4.2.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + $language = Factory::getLanguage(); + $language->load('', JPATH_ADMINISTRATOR, null, false, false) || + $language->load('', JPATH_ADMINISTRATOR, null, true); + $language->load('com_installer', JPATH_ADMINISTRATOR, null, false, false) || + $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $this->addArgument( + 'extensionId', + InputArgument::REQUIRED, + 'ID of extension to be removed (run extension:list command to check)' + ); + + $help = "%command.name% is used to uninstall extensions. \nThe command requires one argument, the ID of the extension to uninstall. \nYou may find this ID by running the extension:list command. \nUsage: php %command.full_name% "; - $this->setDescription('Remove an extension'); - $this->setHelp($help); - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - $extensionId = $this->cliInput->getArgument('extensionId'); - - $response = $this->ioStyle->ask('Are you sure you want to remove this extension?', 'yes/no'); - - if (strtolower($response) === 'yes') - { - // Get an installer object for the extension type - $installer = Installer::getInstance(); - $row = new Extension($this->getDatabase()); - - if ((int) $extensionId === 0 || !$row->load($extensionId)) - { - $this->ioStyle->error("Extension with ID of $extensionId not found."); - - return self::REMOVE_NOT_FOUND; - } - - // Do not allow to uninstall locked extensions. - if ((int) $row->locked === 1) - { - $this->ioStyle->error(Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR_LOCKED_EXTENSION', $row->name, $extensionId)); - - return self::REMOVE_LOCKED; - } - - if ($row->type) - { - if (!$installer->uninstall($row->type, $extensionId)) - { - $this->ioStyle->error('Extension not removed.'); - - return self::REMOVE_FAILED; - } - - $this->ioStyle->success('Extension removed!'); - - return self::REMOVE_SUCCESSFUL; - } - - return self::REMOVE_INVALID_TYPE; - } - elseif (strtolower($response) === 'no') - { - $this->ioStyle->note('Extension not removed.'); - - return self::REMOVE_ABORT; - } - - $this->ioStyle->warning('Invalid response'); - - return self::REMOVE_INVALID_RESPONSE; - } + $this->setDescription('Remove an extension'); + $this->setHelp($help); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $extensionId = $this->cliInput->getArgument('extensionId'); + + $response = $this->ioStyle->ask('Are you sure you want to remove this extension?', 'yes/no'); + + if (strtolower($response) === 'yes') { + // Get an installer object for the extension type + $installer = Installer::getInstance(); + $row = new Extension($this->getDatabase()); + + if ((int) $extensionId === 0 || !$row->load($extensionId)) { + $this->ioStyle->error("Extension with ID of $extensionId not found."); + + return self::REMOVE_NOT_FOUND; + } + + // Do not allow to uninstall locked extensions. + if ((int) $row->locked === 1) { + $this->ioStyle->error(Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR_LOCKED_EXTENSION', $row->name, $extensionId)); + + return self::REMOVE_LOCKED; + } + + if ($row->type) { + if (!$installer->uninstall($row->type, $extensionId)) { + $this->ioStyle->error('Extension not removed.'); + + return self::REMOVE_FAILED; + } + + $this->ioStyle->success('Extension removed!'); + + return self::REMOVE_SUCCESSFUL; + } + + return self::REMOVE_INVALID_TYPE; + } elseif (strtolower($response) === 'no') { + $this->ioStyle->note('Extension not removed.'); + + return self::REMOVE_ABORT; + } + + $this->ioStyle->warning('Invalid response'); + + return self::REMOVE_INVALID_RESPONSE; + } } diff --git a/libraries/src/Console/ExtensionsListCommand.php b/libraries/src/Console/ExtensionsListCommand.php index 51427c6935c59..545b8a9eb8a93 100644 --- a/libraries/src/Console/ExtensionsListCommand.php +++ b/libraries/src/Console/ExtensionsListCommand.php @@ -1,4 +1,5 @@ setDatabase($db); - } - - /** - * Configures the IO - * - * @param InputInterface $input Console Input - * @param OutputInterface $output Console Output - * - * @return void - * - * @since 4.0.0 - * - */ - protected function configureIO(InputInterface $input, OutputInterface $output): void - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - - $this->addOption('type', null, InputOption::VALUE_REQUIRED, 'Type of the extension'); - - $help = "%command.name% lists all installed extensions + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'extension:list'; + + /** + * Stores the installed Extensions + * @var array + * @since 4.0.0 + */ + protected $extensions; + + /** + * Stores the Input Object + * @var InputInterface + * @since 4.0.0 + */ + protected $cliInput; + + /** + * SymfonyStyle Object + * @var SymfonyStyle + * @since 4.0.0 + */ + protected $ioStyle; + + /** + * Instantiate the command. + * + * @param DatabaseInterface $db Database connector + * + * @since 4.0.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + protected function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + + $this->addOption('type', null, InputOption::VALUE_REQUIRED, 'Type of the extension'); + + $help = "%command.name% lists all installed extensions \nUsage: php %command.full_name% \nYou may filter on the type of extension (component, module, plugin, etc.) using the --type option: \n php %command.full_name% --type="; - $this->setDescription('List installed extensions'); - $this->setHelp($help); - } - - /** - * Retrieves all extensions - * - * @return mixed - * - * @since 4.0.0 - */ - public function getExtensions() - { - if (!$this->extensions) - { - $this->setExtensions(); - } - - return $this->extensions; - } - - /** - * Retrieves the extension from the model and sets the class variable - * - * @param null $extensions Array of extensions - * - * @return void - * - * @since 4.0.0 - */ - public function setExtensions($extensions = null): void - { - if (!$extensions) - { - $this->extensions = $this->getAllExtensionsFromDB(); - } - else - { - $this->extensions = $extensions; - } - } - - /** - * Retrieves extension list from DB - * - * @return array - * - * @since 4.0.0 - */ - private function getAllExtensionsFromDB(): array - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $query->select('*') - ->from('#__extensions'); - $db->setQuery($query); - $extensions = $db->loadAssocList('extension_id'); - - return $extensions; - } - - /** - * Transforms extension arrays into required form - * - * @param array $extensions Array of extensions - * - * @return array - * - * @since 4.0.0 - */ - protected function getExtensionsNameAndId($extensions): array - { - $extInfo = []; - - foreach ($extensions as $key => $extension) - { - $manifest = json_decode($extension['manifest_cache']); - $extInfo[] = [ - $extension['name'], - $extension['extension_id'], - $manifest ? $manifest->version : '--', - $extension['type'], - $extension['enabled'] == 1 ? 'Yes' : 'No', - ]; - } - - return $extInfo; - } - - /** - * Filters the extension type - * - * @param string $type Extension type - * - * @return array - * - * @since 4.0.0 - */ - private function filterExtensionsBasedOn($type): array - { - $extensions = []; - - foreach ($this->extensions as $key => $extension) - { - if ($extension['type'] == $type) - { - $extensions[] = $extension; - } - } - - return $extensions; - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - $extensions = $this->getExtensions(); - $type = $this->cliInput->getOption('type'); - - if ($type) - { - $extensions = $this->filterExtensionsBasedOn($type); - } - - if (empty($extensions)) - { - $this->ioStyle->error("Cannot find extensions of the type '$type' specified."); - - return Command::SUCCESS; - } - - $extensions = $this->getExtensionsNameAndId($extensions); - - $this->ioStyle->title('Installed extensions.'); - $this->ioStyle->table(['Name', 'Extension ID', 'Version', 'Type', 'Active'], $extensions); - - return Command::SUCCESS; - } + $this->setDescription('List installed extensions'); + $this->setHelp($help); + } + + /** + * Retrieves all extensions + * + * @return mixed + * + * @since 4.0.0 + */ + public function getExtensions() + { + if (!$this->extensions) { + $this->setExtensions(); + } + + return $this->extensions; + } + + /** + * Retrieves the extension from the model and sets the class variable + * + * @param null $extensions Array of extensions + * + * @return void + * + * @since 4.0.0 + */ + public function setExtensions($extensions = null): void + { + if (!$extensions) { + $this->extensions = $this->getAllExtensionsFromDB(); + } else { + $this->extensions = $extensions; + } + } + + /** + * Retrieves extension list from DB + * + * @return array + * + * @since 4.0.0 + */ + private function getAllExtensionsFromDB(): array + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select('*') + ->from('#__extensions'); + $db->setQuery($query); + $extensions = $db->loadAssocList('extension_id'); + + return $extensions; + } + + /** + * Transforms extension arrays into required form + * + * @param array $extensions Array of extensions + * + * @return array + * + * @since 4.0.0 + */ + protected function getExtensionsNameAndId($extensions): array + { + $extInfo = []; + + foreach ($extensions as $key => $extension) { + $manifest = json_decode($extension['manifest_cache']); + $extInfo[] = [ + $extension['name'], + $extension['extension_id'], + $manifest ? $manifest->version : '--', + $extension['type'], + $extension['enabled'] == 1 ? 'Yes' : 'No', + ]; + } + + return $extInfo; + } + + /** + * Filters the extension type + * + * @param string $type Extension type + * + * @return array + * + * @since 4.0.0 + */ + private function filterExtensionsBasedOn($type): array + { + $extensions = []; + + foreach ($this->extensions as $key => $extension) { + if ($extension['type'] == $type) { + $extensions[] = $extension; + } + } + + return $extensions; + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $extensions = $this->getExtensions(); + $type = $this->cliInput->getOption('type'); + + if ($type) { + $extensions = $this->filterExtensionsBasedOn($type); + } + + if (empty($extensions)) { + $this->ioStyle->error("Cannot find extensions of the type '$type' specified."); + + return Command::SUCCESS; + } + + $extensions = $this->getExtensionsNameAndId($extensions); + + $this->ioStyle->title('Installed extensions.'); + $this->ioStyle->table(['Name', 'Extension ID', 'Version', 'Type', 'Active'], $extensions); + + return Command::SUCCESS; + } } diff --git a/libraries/src/Console/FinderIndexCommand.php b/libraries/src/Console/FinderIndexCommand.php index c15103288ffc6..981597306a15b 100644 --- a/libraries/src/Console/FinderIndexCommand.php +++ b/libraries/src/Console/FinderIndexCommand.php @@ -1,4 +1,5 @@ db = $db; - parent::__construct(); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $this->addArgument('purge', InputArgument::OPTIONAL, 'Purge the index and rebuilds'); - $this->addOption('minproctime', null, InputOption::VALUE_REQUIRED, 'Minimum processing time in seconds, in order to apply a pause', 1); - $this->addOption('pause', null, InputOption::VALUE_REQUIRED, 'Pausing type or defined pause time in seconds', 'division'); - $this->addOption('divisor', null, InputOption::VALUE_REQUIRED, 'The divisor of the division: batch-processing time / divisor', 5); - $help = <<<'EOF' + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'finder:index'; + + /** + * Stores the Input Object + * + * @var InputInterface + * @since 4.0.0 + */ + private $cliInput; + + /** + * SymfonyStyle Object + * + * @var SymfonyStyle + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Database connector + * + * @var DatabaseInterface + * @since 4.0.0 + */ + private $db; + + /** + * Start time for the index process + * + * @var string + * @since 2.5 + */ + private $time; + + /** + * Start time for each batch + * + * @var string + * @since 2.5 + */ + private $qtime; + + /** + * Static filters information. + * + * @var array + * @since 3.3 + */ + private $filters = array(); + + /** + * Pausing type or defined pause time in seconds. + * One pausing type is implemented: 'division' for dynamic calculation of pauses + * + * Defaults to 'division' + * + * @var string|integer + * @since 3.9.12 + */ + private $pause = 'division'; + + /** + * The divisor of the division: batch-processing time / divisor. + * This is used together with --pause=division in order to pause dynamically + * in relation to the processing time + * Defaults to 5 + * + * @var integer + * @since 3.9.12 + */ + private $divisor = 5; + + /** + * Minimum processing time in seconds, in order to apply a pause + * Defaults to 1 + * + * @var integer + * @since 3.9.12 + */ + private $minimumBatchProcessingTime = 1; + + /** + * Instantiate the command. + * + * @param DatabaseInterface $db Database connector + * + * @since 4.0.0 + */ + public function __construct(DatabaseInterface $db) + { + $this->db = $db; + parent::__construct(); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $this->addArgument('purge', InputArgument::OPTIONAL, 'Purge the index and rebuilds'); + $this->addOption('minproctime', null, InputOption::VALUE_REQUIRED, 'Minimum processing time in seconds, in order to apply a pause', 1); + $this->addOption('pause', null, InputOption::VALUE_REQUIRED, 'Pausing type or defined pause time in seconds', 'division'); + $this->addOption('divisor', null, InputOption::VALUE_REQUIRED, 'The divisor of the division: batch-processing time / divisor', 5); + $help = <<<'EOF' The %command.name% Purges and rebuilds the index (search filters are preserved). php %command.full_name% EOF; - $this->setDescription('Purges and rebuild the index'); - $this->setHelp($help); - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - - // Initialize the time value. - $this->time = microtime(true); - $this->configureIO($input, $output); - - $this->ioStyle->writeln( - [ - 'Finder Indexer', - '==========================', - '', - ] - ); - - if ($this->cliInput->getOption('minproctime')) - { - $this->minimumBatchProcessingTime = $this->cliInput->getOption('minproctime'); - } - - if ($this->cliInput->getOption('pause')) - { - $this->pause = $this->cliInput->getOption('pause'); - } - - if ($this->cliInput->getOption('divisor')) - { - $this->divisor = $this->cliInput->getOption('divisor'); - } - - if ($this->cliInput->getArgument('purge')) - { - // Taxonomy ids will change following a purge/index, so save filter information first. - $this->getFilters(); - - // Purge the index. - $this->purge(); - - // Run the indexer. - $this->index(); - - // Restore the filters again. - $this->putFilters(); - } - else - { - $this->index(); - } - - $this->ioStyle->newLine(1); - - // Total reporting. - $this->ioStyle->writeln( - [ - '' . Text::sprintf('FINDER_CLI_PROCESS_COMPLETE', round(microtime(true) - $this->time, 3)) . '', - '' . Text::sprintf('FINDER_CLI_PEAK_MEMORY_USAGE', number_format(memory_get_peak_usage(true))) . '', - ] - ); - - $this->ioStyle->newLine(1); - - return Command::SUCCESS; - } - - /** - * Configures the IO - * - * @param InputInterface $input Console Input - * @param OutputInterface $output Console Output - * - * @return void - * - * @since 4.0.0 - * - */ - private function configureIO(InputInterface $input, OutputInterface $output): void - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - $language = Factory::getLanguage(); - $language->load('', JPATH_ADMINISTRATOR, null, false, false) || - $language->load('', JPATH_ADMINISTRATOR, null, true); - $language->load('finder_cli', JPATH_SITE, null, false, false)|| - $language->load('finder_cli', JPATH_SITE, null, true); - } - - /** - * Save static filters. - * - * Since a purge/index cycle will cause all the taxonomy ids to change, - * the static filters need to be updated with the new taxonomy ids. - * The static filter information is saved prior to the purge/index - * so that it can later be used to update the filters with new ids. - * - * @return void - * - * @since 4.0.0 - */ - private function getFilters(): void - { - $this->ioStyle->text(Text::_('FINDER_CLI_SAVE_FILTERS')); - - // Get the taxonomy ids used by the filters. - $db = $this->db; - $query = $db->getQuery(true); - $query - ->select('filter_id, title, data') - ->from($db->quoteName('#__finder_filters')); - $filters = $db->setQuery($query)->loadObjectList(); - - // Get the name of each taxonomy and the name of its parent. - foreach ($filters as $filter) - { - // Skip empty filters. - if ($filter->data === '') - { - continue; - } - - // Get taxonomy records. - $query = $db->getQuery(true); - $query - ->select('t.title, p.title AS parent') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->leftJoin($db->quoteName('#__finder_taxonomy') . ' AS p ON p.id = t.parent_id') - ->where($db->quoteName('t.id') . ' IN (' . $filter->data . ')'); - $taxonomies = $db->setQuery($query)->loadObjectList(); - - // Construct a temporary data structure to hold the filter information. - foreach ($taxonomies as $taxonomy) - { - $this->filters[$filter->filter_id][] = array( - 'filter' => $filter->title, - 'title' => $taxonomy->title, - 'parent' => $taxonomy->parent, - ); - } - } - - $this->ioStyle->text(Text::sprintf('FINDER_CLI_SAVE_FILTER_COMPLETED', count($filters))); - } - - /** - * Purge the index. - * - * @return void - * - * @since 3.3 - */ - private function purge() - { - $this->ioStyle->text(Text::_('FINDER_CLI_INDEX_PURGE')); - - // Load the model. - $app = $this->getApplication(); - $model = $app->bootComponent('com_finder')->getMVCFactory($app)->createModel('Index', 'Administrator'); - - // Attempt to purge the index. - $return = $model->purge(); - - // If unsuccessful then abort. - if (!$return) - { - $message = Text::_('FINDER_CLI_INDEX_PURGE_FAILED', $model->getError()); - $this->ioStyle->error($message); - exit(); - } - - $this->ioStyle->text(Text::_('FINDER_CLI_INDEX_PURGE_SUCCESS')); - } - - /** - * Run the indexer. - * - * @return void - * - * @since 2.5 - */ - private function index() - { - - // Disable caching. - $app = $this->getApplication(); - $app->set('caching', 0); - $app->set('cache_handler', 'file'); - - // Reset the indexer state. - Indexer::resetState(); - - // Import the plugins. - PluginHelper::importPlugin('system'); - PluginHelper::importPlugin('finder'); - - // Starting Indexer. - $this->ioStyle->text(Text::_('FINDER_CLI_STARTING_INDEXER')); - - // Trigger the onStartIndex event. - $app->triggerEvent('onStartIndex'); - - // Remove the script time limit. - @set_time_limit(0); - - // Get the indexer state. - $state = Indexer::getState(); - - // Setting up plugins. - $this->ioStyle->text(Text::_('FINDER_CLI_SETTING_UP_PLUGINS')); - - // Trigger the onBeforeIndex event. - $app->triggerEvent('onBeforeIndex'); - - // Startup reporting. - $this->ioStyle->text(Text::sprintf('FINDER_CLI_SETUP_ITEMS', $state->totalItems, round(microtime(true) - $this->time, 3))); - - // Get the number of batches. - $t = (int) $state->totalItems; - $c = (int) ceil($t / $state->batchSize); - $c = $c === 0 ? 1 : $c; - - try - { - // Process the batches. - for ($i = 0; $i < $c; $i++) - { - // Set the batch start time. - $this->qtime = microtime(true); - - // Reset the batch offset. - $state->batchOffset = 0; - - // Trigger the onBuildIndex event. - Factory::getApplication()->triggerEvent('onBuildIndex'); - - // Batch reporting. - $text = Text::sprintf('FINDER_CLI_BATCH_COMPLETE', $i + 1, $processingTime = round(microtime(true) - $this->qtime, 3)); - $this->ioStyle->text($text); - - if ($this->pause !== 0) - { - // Pausing Section - $skip = !($processingTime >= $this->minimumBatchProcessingTime); - $pause = 0; - - if ($this->pause === 'division' && $this->divisor > 0) - { - if (!$skip) - { - $pause = round($processingTime / $this->divisor); - } - else - { - $pause = 1; - } - } - elseif ($this->pause > 0) - { - $pause = $this->pause; - } - - if ($pause > 0 && !$skip) - { - $this->ioStyle->text(Text::sprintf('FINDER_CLI_BATCH_PAUSING', $pause)); - sleep($pause); - $this->ioStyle->text(Text::_('FINDER_CLI_BATCH_CONTINUING')); - } - - if ($skip) - { - $this->ioStyle->text( - Text::sprintf( - 'FINDER_CLI_SKIPPING_PAUSE_LOW_BATCH_PROCESSING_TIME', - $processingTime, - $this->minimumBatchProcessingTime - ) - ); - } - - // End of Pausing Section - } - } - } - catch (Exception $e) - { - // Display the error - $this->ioStyle->error($e->getMessage()); - - // Reset the indexer state. - Indexer::resetState(); - - // Close the app - $app->close($e->getCode()); - } - - // Reset the indexer state. - Indexer::resetState(); - } - - /** - * Restore static filters. - * - * Using the saved filter information, update the filter records - * with the new taxonomy ids. - * - * @return void - * - * @since 3.3 - */ - private function putFilters() - { - $this->ioStyle->text(Text::_('FINDER_CLI_RESTORE_FILTERS')); - - $db = $this->db; - - // Use the temporary filter information to update the filter taxonomy ids. - foreach ($this->filters as $filter_id => $filter) - { - $tids = array(); - - foreach ($filter as $element) - { - // Look for the old taxonomy in the new taxonomy table. - $query = $db->getQuery(true); - $query - ->select('t.id') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->leftJoin($db->quoteName('#__finder_taxonomy') . ' AS p ON p.id = t.parent_id') - ->where($db->quoteName('t.title') . ' = ' . $db->quote($element['title'])) - ->where($db->quoteName('p.title') . ' = ' . $db->quote($element['parent'])); - $taxonomy = $db->setQuery($query)->loadResult(); - - // If we found it then add it to the list. - if ($taxonomy) - { - $tids[] = $taxonomy; - } - else - { - $text = Text::sprintf('FINDER_CLI_FILTER_RESTORE_WARNING', $element['parent'], $element['title'], $element['filter']); - $this->ioStyle->text($text); - } - } - - // Construct a comma-separated string from the taxonomy ids. - $taxonomyIds = empty($tids) ? '' : implode(',', $tids); - - // Update the filter with the new taxonomy ids. - $query = $db->getQuery(true); - $query - ->update($db->quoteName('#__finder_filters')) - ->set($db->quoteName('data') . ' = ' . $db->quote($taxonomyIds)) - ->where($db->quoteName('filter_id') . ' = ' . (int) $filter_id); - $db->setQuery($query)->execute(); - } - - $this->ioStyle->text(Text::sprintf('FINDER_CLI_RESTORE_FILTER_COMPLETED', count($this->filters))); - } - + $this->setDescription('Purges and rebuild the index'); + $this->setHelp($help); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + + // Initialize the time value. + $this->time = microtime(true); + $this->configureIO($input, $output); + + $this->ioStyle->writeln( + [ + 'Finder Indexer', + '==========================', + '', + ] + ); + + if ($this->cliInput->getOption('minproctime')) { + $this->minimumBatchProcessingTime = $this->cliInput->getOption('minproctime'); + } + + if ($this->cliInput->getOption('pause')) { + $this->pause = $this->cliInput->getOption('pause'); + } + + if ($this->cliInput->getOption('divisor')) { + $this->divisor = $this->cliInput->getOption('divisor'); + } + + if ($this->cliInput->getArgument('purge')) { + // Taxonomy ids will change following a purge/index, so save filter information first. + $this->getFilters(); + + // Purge the index. + $this->purge(); + + // Run the indexer. + $this->index(); + + // Restore the filters again. + $this->putFilters(); + } else { + $this->index(); + } + + $this->ioStyle->newLine(1); + + // Total reporting. + $this->ioStyle->writeln( + [ + '' . Text::sprintf('FINDER_CLI_PROCESS_COMPLETE', round(microtime(true) - $this->time, 3)) . '', + '' . Text::sprintf('FINDER_CLI_PEAK_MEMORY_USAGE', number_format(memory_get_peak_usage(true))) . '', + ] + ); + + $this->ioStyle->newLine(1); + + return Command::SUCCESS; + } + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + $language = Factory::getLanguage(); + $language->load('', JPATH_ADMINISTRATOR, null, false, false) || + $language->load('', JPATH_ADMINISTRATOR, null, true); + $language->load('finder_cli', JPATH_SITE, null, false, false) || + $language->load('finder_cli', JPATH_SITE, null, true); + } + + /** + * Save static filters. + * + * Since a purge/index cycle will cause all the taxonomy ids to change, + * the static filters need to be updated with the new taxonomy ids. + * The static filter information is saved prior to the purge/index + * so that it can later be used to update the filters with new ids. + * + * @return void + * + * @since 4.0.0 + */ + private function getFilters(): void + { + $this->ioStyle->text(Text::_('FINDER_CLI_SAVE_FILTERS')); + + // Get the taxonomy ids used by the filters. + $db = $this->db; + $query = $db->getQuery(true); + $query + ->select('filter_id, title, data') + ->from($db->quoteName('#__finder_filters')); + $filters = $db->setQuery($query)->loadObjectList(); + + // Get the name of each taxonomy and the name of its parent. + foreach ($filters as $filter) { + // Skip empty filters. + if ($filter->data === '') { + continue; + } + + // Get taxonomy records. + $query = $db->getQuery(true); + $query + ->select('t.title, p.title AS parent') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->leftJoin($db->quoteName('#__finder_taxonomy') . ' AS p ON p.id = t.parent_id') + ->where($db->quoteName('t.id') . ' IN (' . $filter->data . ')'); + $taxonomies = $db->setQuery($query)->loadObjectList(); + + // Construct a temporary data structure to hold the filter information. + foreach ($taxonomies as $taxonomy) { + $this->filters[$filter->filter_id][] = array( + 'filter' => $filter->title, + 'title' => $taxonomy->title, + 'parent' => $taxonomy->parent, + ); + } + } + + $this->ioStyle->text(Text::sprintf('FINDER_CLI_SAVE_FILTER_COMPLETED', count($filters))); + } + + /** + * Purge the index. + * + * @return void + * + * @since 3.3 + */ + private function purge() + { + $this->ioStyle->text(Text::_('FINDER_CLI_INDEX_PURGE')); + + // Load the model. + $app = $this->getApplication(); + $model = $app->bootComponent('com_finder')->getMVCFactory($app)->createModel('Index', 'Administrator'); + + // Attempt to purge the index. + $return = $model->purge(); + + // If unsuccessful then abort. + if (!$return) { + $message = Text::_('FINDER_CLI_INDEX_PURGE_FAILED', $model->getError()); + $this->ioStyle->error($message); + exit(); + } + + $this->ioStyle->text(Text::_('FINDER_CLI_INDEX_PURGE_SUCCESS')); + } + + /** + * Run the indexer. + * + * @return void + * + * @since 2.5 + */ + private function index() + { + + // Disable caching. + $app = $this->getApplication(); + $app->set('caching', 0); + $app->set('cache_handler', 'file'); + + // Reset the indexer state. + Indexer::resetState(); + + // Import the plugins. + PluginHelper::importPlugin('system'); + PluginHelper::importPlugin('finder'); + + // Starting Indexer. + $this->ioStyle->text(Text::_('FINDER_CLI_STARTING_INDEXER')); + + // Trigger the onStartIndex event. + $app->triggerEvent('onStartIndex'); + + // Remove the script time limit. + @set_time_limit(0); + + // Get the indexer state. + $state = Indexer::getState(); + + // Setting up plugins. + $this->ioStyle->text(Text::_('FINDER_CLI_SETTING_UP_PLUGINS')); + + // Trigger the onBeforeIndex event. + $app->triggerEvent('onBeforeIndex'); + + // Startup reporting. + $this->ioStyle->text(Text::sprintf('FINDER_CLI_SETUP_ITEMS', $state->totalItems, round(microtime(true) - $this->time, 3))); + + // Get the number of batches. + $t = (int) $state->totalItems; + $c = (int) ceil($t / $state->batchSize); + $c = $c === 0 ? 1 : $c; + + try { + // Process the batches. + for ($i = 0; $i < $c; $i++) { + // Set the batch start time. + $this->qtime = microtime(true); + + // Reset the batch offset. + $state->batchOffset = 0; + + // Trigger the onBuildIndex event. + Factory::getApplication()->triggerEvent('onBuildIndex'); + + // Batch reporting. + $text = Text::sprintf('FINDER_CLI_BATCH_COMPLETE', $i + 1, $processingTime = round(microtime(true) - $this->qtime, 3)); + $this->ioStyle->text($text); + + if ($this->pause !== 0) { + // Pausing Section + $skip = !($processingTime >= $this->minimumBatchProcessingTime); + $pause = 0; + + if ($this->pause === 'division' && $this->divisor > 0) { + if (!$skip) { + $pause = round($processingTime / $this->divisor); + } else { + $pause = 1; + } + } elseif ($this->pause > 0) { + $pause = $this->pause; + } + + if ($pause > 0 && !$skip) { + $this->ioStyle->text(Text::sprintf('FINDER_CLI_BATCH_PAUSING', $pause)); + sleep($pause); + $this->ioStyle->text(Text::_('FINDER_CLI_BATCH_CONTINUING')); + } + + if ($skip) { + $this->ioStyle->text( + Text::sprintf( + 'FINDER_CLI_SKIPPING_PAUSE_LOW_BATCH_PROCESSING_TIME', + $processingTime, + $this->minimumBatchProcessingTime + ) + ); + } + + // End of Pausing Section + } + } + } catch (Exception $e) { + // Display the error + $this->ioStyle->error($e->getMessage()); + + // Reset the indexer state. + Indexer::resetState(); + + // Close the app + $app->close($e->getCode()); + } + + // Reset the indexer state. + Indexer::resetState(); + } + + /** + * Restore static filters. + * + * Using the saved filter information, update the filter records + * with the new taxonomy ids. + * + * @return void + * + * @since 3.3 + */ + private function putFilters() + { + $this->ioStyle->text(Text::_('FINDER_CLI_RESTORE_FILTERS')); + + $db = $this->db; + + // Use the temporary filter information to update the filter taxonomy ids. + foreach ($this->filters as $filter_id => $filter) { + $tids = array(); + + foreach ($filter as $element) { + // Look for the old taxonomy in the new taxonomy table. + $query = $db->getQuery(true); + $query + ->select('t.id') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->leftJoin($db->quoteName('#__finder_taxonomy') . ' AS p ON p.id = t.parent_id') + ->where($db->quoteName('t.title') . ' = ' . $db->quote($element['title'])) + ->where($db->quoteName('p.title') . ' = ' . $db->quote($element['parent'])); + $taxonomy = $db->setQuery($query)->loadResult(); + + // If we found it then add it to the list. + if ($taxonomy) { + $tids[] = $taxonomy; + } else { + $text = Text::sprintf('FINDER_CLI_FILTER_RESTORE_WARNING', $element['parent'], $element['title'], $element['filter']); + $this->ioStyle->text($text); + } + } + + // Construct a comma-separated string from the taxonomy ids. + $taxonomyIds = empty($tids) ? '' : implode(',', $tids); + + // Update the filter with the new taxonomy ids. + $query = $db->getQuery(true); + $query + ->update($db->quoteName('#__finder_filters')) + ->set($db->quoteName('data') . ' = ' . $db->quote($taxonomyIds)) + ->where($db->quoteName('filter_id') . ' = ' . (int) $filter_id); + $db->setQuery($query)->execute(); + } + + $this->ioStyle->text(Text::sprintf('FINDER_CLI_RESTORE_FILTER_COMPLETED', count($this->filters))); + } } diff --git a/libraries/src/Console/GetConfigurationCommand.php b/libraries/src/Console/GetConfigurationCommand.php index 4b1f14befa16c..54d1b8d7dfbe7 100644 --- a/libraries/src/Console/GetConfigurationCommand.php +++ b/libraries/src/Console/GetConfigurationCommand.php @@ -1,4 +1,5 @@ 'db', - 'options' => [ - 'dbtype', - 'host', - 'user', - 'password', - 'dbprefix', - 'db', - 'dbencryption', - 'dbsslverifyservercert', - 'dbsslkey', - 'dbsslcert', - 'dbsslca', - 'dbsslcipher' - ] - ]; - - /** - * Constant defining the Session option group - * @var array - * @since 4.0.0 - */ - public const SESSION_GROUP = [ - 'name' => 'session', - 'options' => [ - 'session_handler', - 'shared_session', - 'session_metadata' - ] - ]; - - /** - * Constant defining the Mail option group - * @var array - * @since 4.0.0 - */ - public const MAIL_GROUP = [ - 'name' => 'mail', - 'options' => [ - 'mailonline', - 'mailer', - 'mailfrom', - 'fromname', - 'sendmail', - 'smtpauth', - 'smtpuser', - 'smtppass', - 'smtphost', - 'smtpsecure', - 'smtpport' - ] - ]; - - /** - * Return code if configuration is get successfully - * @since 4.0.0 - */ - public const CONFIG_GET_SUCCESSFUL = 0; - - /** - * Return code if configuration group option is not found - * @since 4.0.0 - */ - public const CONFIG_GET_GROUP_NOT_FOUND = 1; - - /** - * Return code if configuration option is not found - * @since 4.0.0 - */ - public const CONFIG_GET_OPTION_NOT_FOUND = 2; - - /** - * Return code if the command has been invoked with wrong options - * @since 4.0.0 - */ - public const CONFIG_GET_OPTION_FAILED = 3; - - /** - * Configures the IO - * - * @param InputInterface $input Console Input - * @param OutputInterface $output Console Output - * - * @return void - * - * @since 4.0.0 - * - */ - private function configureIO(InputInterface $input, OutputInterface $output) - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - - /** - * Displays logically grouped options - * - * @param string $group The group to be processed - * - * @return integer - * - * @since 4.0.0 - */ - public function processGroupOptions($group): int - { - $configs = $this->getApplication()->getConfig()->toArray(); - $configs = $this->formatConfig($configs); - - $groups = $this->getGroups(); - - $foundGroup = false; - - foreach ($groups as $key => $value) - { - if ($value['name'] === $group) - { - $foundGroup = true; - $options = []; - - foreach ($value['options'] as $option) - { - $options[] = [$option, $configs[$option]]; - } - - $this->ioStyle->table(['Option', 'Value'], $options); - } - } - - if (!$foundGroup) - { - $this->ioStyle->error("Group *$group* not found"); - - return self::CONFIG_GET_GROUP_NOT_FOUND; - } - - return self::CONFIG_GET_SUCCESSFUL; - } - - /** - * Gets the defined option groups - * - * @return array - * - * @since 4.0.0 - */ - public function getGroups() - { - return [ - self::DB_GROUP, - self::MAIL_GROUP, - self::SESSION_GROUP - ]; - } - - /** - * Formats the configuration array into desired format - * - * @param array $configs Array of the configurations - * - * @return array - * - * @since 4.0.0 - */ - public function formatConfig(Array $configs): array - { - $newConfig = []; - - foreach ($configs as $key => $config) - { - $config = $config === false ? "false" : $config; - $config = $config === true ? "true" : $config; - - if (!in_array($key, ['cwd', 'execution'])) - { - $newConfig[$key] = $config; - } - } - - return $newConfig; - } - - /** - * Handles the command when a single option is requested - * - * @param string $option The option we want to get its value - * - * @return integer - * - * @since 4.0.0 - */ - public function processSingleOption($option): int - { - $configs = $this->getApplication()->getConfig()->toArray(); - - if (!array_key_exists($option, $configs)) - { - $this->ioStyle->error("Can't find option *$option* in configuration list"); - - return self::CONFIG_GET_OPTION_NOT_FOUND; - } - - $value = $this->formatConfigValue($this->getApplication()->get($option)); - - $this->ioStyle->table(['Option', 'Value'], [[$option, $value]]); - - return self::CONFIG_GET_SUCCESSFUL; - } - - /** - * Formats the Configuration value - * - * @param mixed $value Value to be formatted - * - * @return string - * - * @since 4.0.0 - */ - protected function formatConfigValue($value): string - { - if ($value === false) - { - return 'false'; - } - elseif ($value === true) - { - return 'true'; - } - elseif ($value === null) - { - return 'Not Set'; - } - elseif (\is_array($value)) - { - return \json_encode($value); - } - elseif (\is_object($value)) - { - return \json_encode(\get_object_vars($value)); - } - else - { - return $value; - } - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $groups = $this->getGroups(); - - foreach ($groups as $key => $group) - { - $groupNames[] = $group['name']; - } - - $groupNames = implode(', ', $groupNames); - - $this->addArgument('option', null, 'Name of the option'); - $this->addOption('group', 'g', InputOption::VALUE_REQUIRED, 'Name of the option'); - - $help = "%command.name% displays the current value of a configuration option + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'config:get'; + + /** + * Stores the Input Object + * @var Input + * @since 4.0.0 + */ + private $cliInput; + + /** + * SymfonyStyle Object + * @var SymfonyStyle + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Constant defining the Database option group + * @var array + * @since 4.0.0 + */ + public const DB_GROUP = [ + 'name' => 'db', + 'options' => [ + 'dbtype', + 'host', + 'user', + 'password', + 'dbprefix', + 'db', + 'dbencryption', + 'dbsslverifyservercert', + 'dbsslkey', + 'dbsslcert', + 'dbsslca', + 'dbsslcipher' + ] + ]; + + /** + * Constant defining the Session option group + * @var array + * @since 4.0.0 + */ + public const SESSION_GROUP = [ + 'name' => 'session', + 'options' => [ + 'session_handler', + 'shared_session', + 'session_metadata' + ] + ]; + + /** + * Constant defining the Mail option group + * @var array + * @since 4.0.0 + */ + public const MAIL_GROUP = [ + 'name' => 'mail', + 'options' => [ + 'mailonline', + 'mailer', + 'mailfrom', + 'fromname', + 'sendmail', + 'smtpauth', + 'smtpuser', + 'smtppass', + 'smtphost', + 'smtpsecure', + 'smtpport' + ] + ]; + + /** + * Return code if configuration is get successfully + * @since 4.0.0 + */ + public const CONFIG_GET_SUCCESSFUL = 0; + + /** + * Return code if configuration group option is not found + * @since 4.0.0 + */ + public const CONFIG_GET_GROUP_NOT_FOUND = 1; + + /** + * Return code if configuration option is not found + * @since 4.0.0 + */ + public const CONFIG_GET_OPTION_NOT_FOUND = 2; + + /** + * Return code if the command has been invoked with wrong options + * @since 4.0.0 + */ + public const CONFIG_GET_OPTION_FAILED = 3; + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + + /** + * Displays logically grouped options + * + * @param string $group The group to be processed + * + * @return integer + * + * @since 4.0.0 + */ + public function processGroupOptions($group): int + { + $configs = $this->getApplication()->getConfig()->toArray(); + $configs = $this->formatConfig($configs); + + $groups = $this->getGroups(); + + $foundGroup = false; + + foreach ($groups as $key => $value) { + if ($value['name'] === $group) { + $foundGroup = true; + $options = []; + + foreach ($value['options'] as $option) { + $options[] = [$option, $configs[$option]]; + } + + $this->ioStyle->table(['Option', 'Value'], $options); + } + } + + if (!$foundGroup) { + $this->ioStyle->error("Group *$group* not found"); + + return self::CONFIG_GET_GROUP_NOT_FOUND; + } + + return self::CONFIG_GET_SUCCESSFUL; + } + + /** + * Gets the defined option groups + * + * @return array + * + * @since 4.0.0 + */ + public function getGroups() + { + return [ + self::DB_GROUP, + self::MAIL_GROUP, + self::SESSION_GROUP + ]; + } + + /** + * Formats the configuration array into desired format + * + * @param array $configs Array of the configurations + * + * @return array + * + * @since 4.0.0 + */ + public function formatConfig(array $configs): array + { + $newConfig = []; + + foreach ($configs as $key => $config) { + $config = $config === false ? "false" : $config; + $config = $config === true ? "true" : $config; + + if (!in_array($key, ['cwd', 'execution'])) { + $newConfig[$key] = $config; + } + } + + return $newConfig; + } + + /** + * Handles the command when a single option is requested + * + * @param string $option The option we want to get its value + * + * @return integer + * + * @since 4.0.0 + */ + public function processSingleOption($option): int + { + $configs = $this->getApplication()->getConfig()->toArray(); + + if (!array_key_exists($option, $configs)) { + $this->ioStyle->error("Can't find option *$option* in configuration list"); + + return self::CONFIG_GET_OPTION_NOT_FOUND; + } + + $value = $this->formatConfigValue($this->getApplication()->get($option)); + + $this->ioStyle->table(['Option', 'Value'], [[$option, $value]]); + + return self::CONFIG_GET_SUCCESSFUL; + } + + /** + * Formats the Configuration value + * + * @param mixed $value Value to be formatted + * + * @return string + * + * @since 4.0.0 + */ + protected function formatConfigValue($value): string + { + if ($value === false) { + return 'false'; + } elseif ($value === true) { + return 'true'; + } elseif ($value === null) { + return 'Not Set'; + } elseif (\is_array($value)) { + return \json_encode($value); + } elseif (\is_object($value)) { + return \json_encode(\get_object_vars($value)); + } else { + return $value; + } + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $groups = $this->getGroups(); + + foreach ($groups as $key => $group) { + $groupNames[] = $group['name']; + } + + $groupNames = implode(', ', $groupNames); + + $this->addArgument('option', null, 'Name of the option'); + $this->addOption('group', 'g', InputOption::VALUE_REQUIRED, 'Name of the option'); + + $help = "%command.name% displays the current value of a configuration option \nUsage: php %command.full_name%